- {canChoose
- ? t(
- 'auto.components.new.workspace.RunTargetCombobox.orcaCloudIdleSleep',
- 'Sleeps after {{count}} minutes idle',
- {
- count: ORCA_CLOUD_IDLE_SLEEP_MINUTES,
- defaultValue_one: 'Sleeps after {{count}} minute idle'
- }
- )
- : t(
- 'components.newWorkspace.cloudSetup.askChoosePlan',
- 'Ask an owner or admin to choose a plan.'
- )}
-
- >
- )
-}
-
-type BlockCopy = {
- title: string
- detail: string
- tone: 'error' | 'neutral'
- /** Owners/admins only; members read who can fix it in `detail`. */
- action?: string
-}
-
-function useBlockCopy(access: OrcaCloudRunTargetAccess): BlockCopy | null {
- const t = useTranslate()
- switch (access.state) {
- case 'no-plan':
- if (access.trialOfferSeconds) {
- // B1: the trial is included; Start claims it, so nothing blocks.
- return null
- }
- if (access.trialUsedUp) {
- // T2: neutral, nothing failed.
- return {
- title: t('components.newWorkspace.cloudSetup.trialUsedUpTitle', 'Your trial is used up'),
- detail: access.canManageBilling
- ? t(
- 'components.newWorkspace.cloudSetup.trialUsedUpChoose',
- 'Choose a plan to keep using Orca Cloud.'
- )
- : t(
- 'components.newWorkspace.cloudSetup.askChoosePlan',
- 'Ask an owner or admin to choose a plan.'
- ),
- tone: 'neutral',
- ...(access.canManageBilling
- ? { action: t('components.newWorkspace.cloudSetup.choosePlan', 'Choose plan') }
- : {})
- }
- }
- return {
- title: access.lapsed
- ? t(
- 'auto.components.new.workspace.RunTargetCombobox.orcaCloudPlanEnded',
- 'Your Team plan ended'
- )
- : t(
- 'auto.components.new.workspace.RunTargetCombobox.orcaCloudNeedsTeamPlan',
- 'Orca Cloud needs a Team plan.'
- ),
- detail: access.canManageBilling
- ? ''
- : t(
- 'auto.components.new.workspace.RunTargetCombobox.orcaCloudAskChoosePlan',
- 'Ask an owner or admin to choose one.'
- ),
- tone: 'neutral',
- ...(access.canManageBilling
- ? {
- action: access.lapsed
- ? t('auto.components.new.workspace.RunTargetCombobox.orcaCloudRenew', 'Renew')
- : t('auto.components.new.workspace.RunTargetCombobox.orcaCloudUpgrade', 'Upgrade')
- }
- : {})
- }
- case 'out-of-hours': {
- const cap = access.cause === 'cap'
- // Create time: no workspace exists yet, so "Your files are safe." does not apply here.
- return {
- title: cap
- ? t(
- 'auto.components.new.workspace.RunTargetCombobox.orcaCloudCapReached',
- 'Spend cap reached'
- )
- : t(
- 'auto.components.new.workspace.RunTargetCombobox.orcaCloudOutOfHours',
- 'Out of sandbox hours'
- ),
- detail: access.canManageBilling
- ? ''
- : cap
- ? t(
- 'auto.components.new.workspace.RunTargetCombobox.orcaCloudAskRaiseSpendCap',
- 'Ask an owner or admin to raise the spend cap.'
- )
- : t(
- 'auto.components.new.workspace.RunTargetCombobox.orcaCloudAskAddHoursOnly',
- 'Ask an owner or admin to add hours.'
- ),
- tone: 'neutral',
- ...(access.canManageBilling
- ? {
- action:
- access.cause === 'cap'
- ? t(
- 'auto.components.new.workspace.RunTargetCombobox.orcaCloudRaiseSpendCap',
- 'Raise spend cap'
- )
- : t(
- 'auto.components.new.workspace.RunTargetCombobox.orcaCloudTurnOnOverage',
- 'Turn on overage'
- )
- }
- : {})
- }
- }
- case 'payment-issue':
- return {
- title: t(
- 'auto.components.new.workspace.RunTargetCombobox.orcaCloudPaymentFailed',
- 'Payment failed'
- ),
- detail: access.canManageBilling
- ? t(
- 'auto.components.new.workspace.RunTargetCombobox.orcaCloudPaymentEffect',
- "New sandboxes can't start."
- )
- : t(
- 'auto.components.new.workspace.RunTargetCombobox.orcaCloudAskPayment',
- 'Ask an owner or admin to update the payment method.'
- ),
- tone: 'error',
- ...(access.canManageBilling
- ? {
- action: t(
- 'auto.components.new.workspace.RunTargetCombobox.orcaCloudUpdatePaymentMethod',
- 'Update payment method'
- )
- }
- : {})
- }
- case 'loading':
- case 'unknown':
- case 'signed-out':
- case 'entitled':
- return null
- }
-}
-
-/**
- * Context under Run on once Orca Cloud is the committed target, in exactly two reserved lines so
- * neither picking nor a late billing answer moves the form: specs and sandbox hours over idle
- * sleep, or what blocks it over who can fix it, with the fix as an inline link. The wrapper is an
- * always-mounted polite live region, so a block that arrives after the pick is announced.
- */
-export function OrcaCloudSelectionHint({
- access,
- visible
-}: {
- access: OrcaCloudRunTargetAccess
- /** Orca Cloud is the committed target; otherwise the two lines stay empty. */
- visible: boolean
-}): React.JSX.Element {
- const t = useTranslate()
- const block = useBlockCopy(access)
- const hoursLeft = useOrcaCloudHoursLeft(access)
- // T1: under half an hour of trial left, announced once as it crosses (the live region below).
- const low = isOrcaCloudTrialLow(access)
- const pick = getOrcaCloudPick(access)
- const action =
- block?.action && pick.kind === 'act' ? (
-
- {visible && !block && !low ? (
- // Outside the live region: hour ticks on refresh are not re-announced.
- <>
-
-
-
-
- {hoursLeft ? (
- // pl-1: a flex item trims its leading space, so the gap before "·" comes from padding.
-
- {'· '}
- {hoursLeft}
-
- ) : null}
-
-
- {/* One source for n: the control plane, billing and every surface read this constant. */}
- {t(
- 'auto.components.new.workspace.RunTargetCombobox.orcaCloudIdleSleep',
- 'Sleeps after {{count}} minutes idle',
- {
- count: ORCA_CLOUD_IDLE_SLEEP_MINUTES,
- defaultValue_one: 'Sleeps after {{count}} minute idle'
- }
- )}
-
- >
- ) : null}
- {/* Always mounted, so a block that arrives after the pick (late billing, restored draft) is announced. */}
-
- {visible && !block && low ?
: null}
- {visible && block ? (
- <>
-
- {block.tone === 'error' ? (
-
- ) : null}
- {block.title}
- {action ? · : null}
- {action}
-
- {block.detail ?
{block.detail}
: null}
- >
- ) : null}
-
-
- )
-}
diff --git a/src/renderer/src/components/new-workspace/RunTargetCombobox.tsx b/src/renderer/src/components/new-workspace/RunTargetCombobox.tsx
index b07abf4670e..ec6ce2b237c 100644
--- a/src/renderer/src/components/new-workspace/RunTargetCombobox.tsx
+++ b/src/renderer/src/components/new-workspace/RunTargetCombobox.tsx
@@ -9,6 +9,7 @@ import {
type ReadyProjectHostSetupOption
} from '@/lib/project-host-setup-options'
import type { ExecutionHostId } from '../../../../shared/execution-host'
+import { ORCA_CLOUD_RECIPE_ID } from '../../../../shared/hosted-sandbox/orca-cloud-recipe-id'
import { isWithinComboboxRoot, useTypeAheadCombobox } from './use-type-ahead-combobox'
import { COMBOBOX_POPOVER_SURFACE } from './type-ahead-combobox-styles'
import {
@@ -17,6 +18,9 @@ import {
getRecipeSubmenuItemKey,
getRunTargetSubmenuItemKeys,
getRunTargetCurrentRowKey,
+ getOrcaCloudDetail,
+ ORCA_CLOUD_LABEL,
+ RUN_TARGET_ORCA_CLOUD_KEY,
getRunTargetOptionId,
RUN_TARGET_ADD_HOST_KEY,
RUN_TARGET_ADD_REMOTE_SERVER_ITEM,
@@ -50,6 +54,11 @@ type RunTargetComboboxProps = {
orcaCloudAccess?: OrcaCloudRunTargetAccess
/** From main: drives the sign-in/upsell row when there is no Orca Cloud recipe. */
orcaCloudAvailability?: OrcaCloudAvailability
+ /**
+ * The composer's pick: Orca Cloud is selected even while the account needs setup, which then
+ * happens on a page after Start. Without it, a blocked account's pick runs the fix instead.
+ */
+ onSelectOrcaCloud?: () => void
}
/** Add-host actions open dialogs, so the picker closes before they run. */
@@ -81,7 +90,8 @@ export default function RunTargetCombobox({
onConnectHost,
onSetLocation,
orcaCloudAccess: orcaCloudAccessOverride,
- orcaCloudAvailability = 'unavailable'
+ orcaCloudAvailability = 'unavailable',
+ onSelectOrcaCloud
}: RunTargetComboboxProps): React.JSX.Element {
const t = useTranslate()
// Track in-flight connects per host so one stalling connect never blocks the others.
@@ -100,8 +110,13 @@ export default function RunTargetCombobox({
// An unlisted target (e.g. a per-workspace VM) shows "Choose target", never another host's name (#38669).
null
const selectedRecipe = recipes.find((recipe) => recipe.id === recipeValue) ?? null
+ // Signed out, main lists no Orca Cloud recipe, yet the composer can still pick it.
+ const orcaCloudPicked =
+ selectedRecipe === null && recipeValue === ORCA_CLOUD_RECIPE_ID && Boolean(onSelectOrcaCloud)
// Opening highlights the committed row, so only one row reads as chosen.
- const currentRowKey = getRunTargetCurrentRowKey(selectedRecipe, selectedHost)
+ const currentRowKey = orcaCloudPicked
+ ? RUN_TARGET_ORCA_CLOUD_KEY
+ : getRunTargetCurrentRowKey(selectedRecipe, selectedHost)
const hasAddHost = Boolean(onAddSshHost || onAddRemoteServer)
const orcaCloudOffer = getRunTargetOrcaCloudOffer({
@@ -137,7 +152,8 @@ export default function RunTargetCombobox({
const { submenu, armedItemKey, openSubmenu, closeSubmenu, armItem, moveItemArm, armItemEdge } =
useRunTargetSubmenu(submenuItemKeys)
const armedRow = rows.find((row) => row.key === armedKey) ?? rows[0] ?? null
- const committed = query.length === 0 && (selectedRecipe !== null || selectedHost !== null)
+ const committed =
+ query.length === 0 && (orcaCloudPicked || selectedRecipe !== null || selectedHost !== null)
// Closing also drops any open submenu, which the shared hook doesn't know about.
const close = useCallback((): void => {
@@ -173,7 +189,8 @@ export default function RunTargetCombobox({
override: orcaCloudAccessOverride,
open,
close,
- selectRecipe
+ selectRecipe,
+ ...(onSelectOrcaCloud ? { onSelectOrcaCloud } : {})
})
const addSshHost = useMemo(() => closingFirst(close, onAddSshHost), [close, onAddSshHost])
@@ -307,7 +324,9 @@ export default function RunTargetCombobox({
const activeDescendantId = activeDescendantKey ? optionIdFor(activeDescendantKey) : undefined
const addHostOptionId = optionIdFor(RUN_TARGET_ADD_HOST_KEY)
- const field = getRunTargetFieldText(selectedRecipe, selectedHost, t)
+ const field = orcaCloudPicked
+ ? { label: ORCA_CLOUD_LABEL, detail: getOrcaCloudDetail(t), recipeKind: 'orca-cloud' as const }
+ : getRunTargetFieldText(selectedRecipe, selectedHost, t)
return (