- {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..a80779b6e57 100644
--- a/src/renderer/src/components/new-workspace/RunTargetCombobox.tsx
+++ b/src/renderer/src/components/new-workspace/RunTargetCombobox.tsx
@@ -16,6 +16,7 @@ import {
getRunTargetFieldText,
getRecipeSubmenuItemKey,
getRunTargetSubmenuItemKeys,
+ closingFirst,
getRunTargetCurrentRowKey,
getRunTargetOptionId,
RUN_TARGET_ADD_HOST_KEY,
@@ -50,19 +51,11 @@ type RunTargetComboboxProps = {
orcaCloudAccess?: OrcaCloudRunTargetAccess
/** From main: drives the sign-in/upsell row when there is no Orca Cloud recipe. */
orcaCloudAvailability?: OrcaCloudAvailability
-}
-
-/** Add-host actions open dialogs, so the picker closes before they run. */
-function closingFirst(
- close: () => void,
- action: (() => void) | undefined
-): (() => void) | undefined {
- return action
- ? () => {
- close()
- action()
- }
- : undefined
+ /**
+ * 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
}
const ROOT_ATTRIBUTE = 'data-run-target-combobox-root'
@@ -81,7 +74,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.
@@ -173,7 +167,8 @@ export default function RunTargetCombobox({
override: orcaCloudAccessOverride,
open,
close,
- selectRecipe
+ selectRecipe,
+ ...(onSelectOrcaCloud ? { onSelectOrcaCloud } : {})
})
const addSshHost = useMemo(() => closingFirst(close, onAddSshHost), [close, onAddSshHost])
@@ -332,7 +327,9 @@ export default function RunTargetCombobox({
onKeyDown={handleKeyDown}
/>
void connectHost(option)}
onSetLocation={setLocation}
/>
diff --git a/src/renderer/src/components/new-workspace/RunTargetOrcaCloudRow.tsx b/src/renderer/src/components/new-workspace/RunTargetOrcaCloudRow.tsx
index c3f574b3f7e..b23cd91a6c9 100644
--- a/src/renderer/src/components/new-workspace/RunTargetOrcaCloudRow.tsx
+++ b/src/renderer/src/components/new-workspace/RunTargetOrcaCloudRow.tsx
@@ -270,6 +270,24 @@ function OrcaCloudMeta({
}
}
+/**
+ * In the composer, hours and plan details wait until after create (Neil): the row only keeps what
+ * explains why it can't be picked, "Ask an owner or admin".
+ */
+function showsMetaAfterStart(access: OrcaCloudRunTargetAccess): boolean {
+ switch (access.state) {
+ case 'no-plan':
+ case 'out-of-hours':
+ case 'payment-issue':
+ return !access.canManageBilling
+ case 'loading':
+ case 'entitled':
+ case 'signed-out':
+ case 'unknown':
+ return false
+ }
+}
+
/** Orca Cloud as a first-class Run-on target, with its hours or the fix for what blocks it. */
export function RunTargetOrcaCloudRow({
recipe,
@@ -280,7 +298,8 @@ export function RunTargetOrcaCloudRow({
optionId,
onArm,
onSelect,
- onBlockedAction
+ onBlockedAction,
+ setupAfterStart = false
}: {
recipe: EphemeralVmRecipeOption | null
access: OrcaCloudRunTargetAccess
@@ -292,6 +311,8 @@ export function RunTargetOrcaCloudRow({
onArm: () => void
onSelect: () => void
onBlockedAction: () => void
+ /** The composer walks setup on a page after Start: no inline Sign in / Upgrade here. */
+ setupAfterStart?: boolean
}): React.JSX.Element {
const t = useTranslate()
return (
@@ -320,7 +341,11 @@ export function RunTargetOrcaCloudRow({
onArm={onArm}
onCommit={onSelect}
description={orcaCloudRowHint(access, t)}
- trailing={ }
+ trailing={
+ setupAfterStart && !showsMetaAfterStart(access) ? undefined : (
+
+ )
+ }
/>
)
}
diff --git a/src/renderer/src/components/new-workspace/RunTargetRowList.tsx b/src/renderer/src/components/new-workspace/RunTargetRowList.tsx
index 89fe146ff91..b8c283ab812 100644
--- a/src/renderer/src/components/new-workspace/RunTargetRowList.tsx
+++ b/src/renderer/src/components/new-workspace/RunTargetRowList.tsx
@@ -34,6 +34,7 @@ export default function RunTargetRowList({
onSelectRecipe,
onSelectOrcaCloud,
onOrcaCloudBlockedAction,
+ orcaCloudSetupAfterStart = false,
onConnectHost,
onSetLocation
}: {
@@ -61,6 +62,8 @@ export default function RunTargetRowList({
onSelectOrcaCloud: (recipe: EphemeralVmRecipeOption | null) => void
/** Plans or billing, for an account that cannot start a sandbox. */
onOrcaCloudBlockedAction: () => void
+ /** Picking Orca Cloud just picks it; setup comes after Start, so the row offers no fixes. */
+ orcaCloudSetupAfterStart?: boolean
onConnectHost: (option: NeedsSetupProjectHostOption) => void
onSetLocation: (option: NeedsSetupProjectHostOption) => void
}): React.JSX.Element {
@@ -101,6 +104,7 @@ export default function RunTargetRowList({
onArm={() => onArm(row.key, null)}
onSelect={() => onSelectOrcaCloud(row.recipe)}
onBlockedAction={onOrcaCloudBlockedAction}
+ setupAfterStart={orcaCloudSetupAfterStart}
/>
)
}
diff --git a/src/renderer/src/components/new-workspace/cloud-setup/CloudSetupBrowserTracker.fixture.tsx b/src/renderer/src/components/new-workspace/cloud-setup/CloudSetupBrowserTracker.fixture.tsx
deleted file mode 100644
index b62a4e322e6..00000000000
--- a/src/renderer/src/components/new-workspace/cloud-setup/CloudSetupBrowserTracker.fixture.tsx
+++ /dev/null
@@ -1,207 +0,0 @@
-import { defineFixture } from '../../../component-fixtures/component-fixture'
-import { OrcaCloudSetupPanel } from '@/components/new-workspace/run-on/OrcaCloudSetupPanel'
-import type { CloudSetupStep } from '@/components/new-workspace/run-on/cloud-start-model'
-import { CloudSetupBrowserTracker } from './CloudSetupBrowserTracker'
-import { CloudSetupErrorNotice } from './CloudSetupErrorNotice'
-import { useCloudSetupSwitchTeamFor } from './CloudSetupSwitchTeam'
-import type { CloudSetupError } from './cloud-setup-errors'
-import type { PlannedTeam } from './use-switch-to-planned-team'
-
-const noop = (): void => {}
-// Agent row in agent-accounts v4 wording (glossary "Agent source").
-const NOT_SET_UP =
- 'Claude isn’t set up for Orca Cloud yet. Set it up once in Settings → Agent accounts.'
-const FROM_ACCOUNT = 'Claude · from your Orca account'
-const SET_UP_ONCE = { label: 'Set up once', onClick: noop }
-
-// A6: account and team done in the browser, GitHub in progress.
-const waitingGithub: CloudSetupStep[] = [
- { id: 'account', state: 'done', detail: 'neil@acme.dev' },
- { id: 'team', state: 'done', detail: 'acme · 2 sandbox hours to try Orca Cloud' },
- { id: 'github', state: 'active', detail: 'Waiting for GitHub…' },
- { id: 'agent', state: 'optional', detail: NOT_SET_UP, action: SET_UP_ONCE }
-]
-
-// Rows derived from billing before the setup session exists: the plan row is the browser's.
-const derived: CloudSetupStep[] = [
- { id: 'account', state: 'done' },
- { id: 'plan', state: 'needed', need: 'plan', canFix: true },
- { id: 'agent', state: 'optional', detail: NOT_SET_UP, action: SET_UP_ONCE }
-]
-
-// Signed in, nothing finished yet: the account step is in the browser.
-const signingIn: CloudSetupStep[] = [
- { id: 'account', state: 'active' },
- { id: 'team', state: 'pending' },
- { id: 'github', state: 'pending' },
- { id: 'agent', state: 'optional', detail: NOT_SET_UP, action: SET_UP_ONCE }
-]
-
-function Tracker({
- steps = waitingGithub,
- error = null,
- canManageBilling = true
-}: {
- steps?: CloudSetupStep[]
- error?: CloudSetupError | null
- canManageBilling?: boolean
-}): React.JSX.Element {
- return (
-
-
-
- )
-}
-
-export const trackerWaitingGithub = defineFixture({ width: 560, render: () => })
-export const trackerSigningIn = defineFixture({
- width: 560,
- render: () =>
-})
-export const trackerDerivedRows = defineFixture({
- width: 560,
- render: () =>
-})
-
-const stopped: CloudSetupStep[] = [
- { id: 'account', state: 'done', detail: 'neil@acme.dev' },
- { id: 'team', state: 'done', detail: 'acme' },
- { id: 'plan', state: 'needed', need: 'plan', canFix: true },
- { id: 'agent', state: 'optional', detail: NOT_SET_UP, action: SET_UP_ONCE }
-]
-
-export const trackerTimedOut = defineFixture({
- width: 560,
- render: () =>
-})
-export const trackerCheckoutCanceled = defineFixture({
- width: 560,
- render: () =>
-})
-export const trackerBankStep = defineFixture({
- width: 560,
- render: () =>
-})
-export const trackerTrialUnavailable = defineFixture({
- width: 560,
- render: () =>
-})
-export const trackerTrialUnavailableMember = defineFixture({
- width: 560,
- render: () => (
-
- )
-})
-export const trackerTeamCreateFailed = defineFixture({
- width: 560,
- render: () => (
-
- )
-})
-export const trackerWrongAccount = defineFixture({
- width: 560,
- render: () => (
-
- )
-})
-
-// X1: every setup error notice, as in the spec's sheet.
-export const x1SetupErrors = defineFixture({
- width: 560,
- render: () => (
-
- {(
- [
- { kind: 'timed_out' },
- { kind: 'checkout_canceled' },
- { kind: 'bank_step' },
- { kind: 'trial_unavailable' },
- { kind: 'team_create_failed' },
- { kind: 'wrong_account', detail: 'sam@other.dev', teamName: 'acme' }
- ] satisfies CloudSetupError[]
- ).map((error) => (
-
- ))}
-
-
- )
-})
-
-// B1: an owner whose team never had a trial: the trial is the first plan, no trip.
-export const b1OwnerTrialAvailable = defineFixture({
- width: 560,
- render: () => (
-
-
-
- )
-})
-
-function SwitchTeamPanel({ teams }: { teams: PlannedTeam[] }): React.JSX.Element {
- const b2 = useCloudSetupSwitchTeamFor(teams, noop)
- return (
-
-
-
- )
-}
-
-// B2: personal account, but a team the user is on has a plan: switch instead of buying.
-export const b2SwitchTeam = defineFixture({
- width: 560,
- render: () =>
-})
-export const b2SwitchTeamMenu = defineFixture({
- width: 560,
- render: () => (
-
- )
-})
diff --git a/src/renderer/src/components/new-workspace/cloud-setup/CloudSetupBrowserTracker.tsx b/src/renderer/src/components/new-workspace/cloud-setup/CloudSetupBrowserTracker.tsx
deleted file mode 100644
index 8b053dca6c1..00000000000
--- a/src/renderer/src/components/new-workspace/cloud-setup/CloudSetupBrowserTracker.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-// A6 "Finish in your browser": while the one setup tab is open, the panel ticks each step in place
-// (from the setup session, or rows derived from billing until it exists), says why it stopped (X1)
-// and keeps the way back to the browser one click away. The prompt stays editable above it.
-import React from 'react'
-import { Copy, ExternalLink } from 'lucide-react'
-import { Button } from '@/components/ui/button'
-import { useTranslate } from '@/i18n/use-translate'
-import { OrcaCloudSetupPanel } from '@/components/new-workspace/run-on/OrcaCloudSetupPanel'
-import type {
- CloudSetupStep,
- CloudSetupStepId
-} from '@/components/new-workspace/run-on/cloud-start-model'
-import { reportCloudSetupProgress } from '@/lib/orca-cloud/cloud-setup-telemetry'
-import { CloudSetupErrorNotice } from './CloudSetupErrorNotice'
-import type { CloudSetupError, CloudSetupErrorAction } from './cloud-setup-errors'
-
-export type CloudSetupBrowserTrackerProps = {
- steps: readonly CloudSetupStep[]
- /** Why setup stopped (X1), from the session's `error`; null while it's going. */
- error: CloudSetupError | null
- /** Members get "Ask an owner or admin…" instead of Choose plan on trial_unavailable. */
- canManageBilling?: boolean
- /** Setup session id, for telemetry only (never sent). Omit for derived rows. */
- sessionId?: string | null
- /** Ends the trip; finished steps stay done. */
- onCancel: () => void
- /** For a default browser that's the wrong one; hidden when there's no session link to copy. */
- onCopyLink?: () => void
- onReopen: () => void
- onErrorAction: (action: CloudSetupErrorAction) => void
- /** Per-row actions passed through to the panel (e.g. the Agent row's "Set up once"). */
- rowActions?: Partial>
- /** Trial → plan conversion context for telemetry. */
- afterTrial?: boolean
- className?: string
-}
-
-/**
- * Rows derived from billing (before the setup session reports its own) never say 'active'. While
- * the browser is open, the first unfinished required row is the one it's working on, so it reads
- * "In browser" rather than "Needed". Session rows that already mark a step active pass through.
- */
-export function trackerSteps(
- steps: readonly CloudSetupStep[],
- stopped: boolean
-): readonly CloudSetupStep[] {
- if (stopped || steps.some((step) => step.state === 'active')) {
- return steps
- }
- const index = steps.findIndex((step) => step.state === 'needed')
- if (index === -1) {
- return steps
- }
- return steps.map((step, i) => (i === index ? { ...step, state: 'active' } : step))
-}
-
-export function CloudSetupBrowserTracker({
- steps,
- error,
- canManageBilling = true,
- sessionId = null,
- onCancel,
- onCopyLink,
- onReopen,
- onErrorAction,
- rowActions,
- afterTrial = false,
- className
-}: CloudSetupBrowserTrackerProps): React.JSX.Element {
- const t = useTranslate()
- const errorKind = error?.kind ?? null
- const rows = React.useMemo(() => trackerSteps(steps, error !== null), [steps, error])
- React.useEffect(() => {
- if (sessionId) {
- reportCloudSetupProgress(sessionId, rows, errorKind, { afterTrial })
- }
- }, [sessionId, rows, errorKind, afterTrial])
- // The error's own button already offers the browser for these; don't show it twice.
- const reopenInError = error !== null && (error.kind === 'timed_out' || error.kind === 'bank_step')
- return (
-
- {error ? (
-
- ) : null}
-
-
- {t('components.newWorkspace.cloudSetup.cancelSetup', 'Cancel setup')}
-
-
- {onCopyLink ? (
-
-
- {t('components.newWorkspace.cloudSetup.copySetupLink', 'Copy setup link')}
-
- ) : null}
- {reopenInError ? null : (
-
- {t('components.newWorkspace.cloudSetup.reopenBrowser', 'Reopen browser')}
-
-
- )}
-
-
-
- )
-}
diff --git a/src/renderer/src/components/new-workspace/cloud-setup/cloud-setup-tracker-actions.ts b/src/renderer/src/components/new-workspace/cloud-setup/cloud-setup-tracker-actions.ts
index b8ef4759f3e..07bbbdeb2a9 100644
--- a/src/renderer/src/components/new-workspace/cloud-setup/cloud-setup-tracker-actions.ts
+++ b/src/renderer/src/components/new-workspace/cloud-setup/cloud-setup-tracker-actions.ts
@@ -20,8 +20,8 @@ async function freshLink(sessionId: string): Promise {
function linkUnavailable(): void {
toast.error(
translate(
- 'components.newWorkspace.cloudSetup.linkUnavailable',
- 'Couldn’t get a setup link. Try Set up and start again.'
+ 'components.newWorkspace.cloudSetup.linkUnavailableRetry',
+ 'Couldn’t get a setup link. Try Continue in browser again.'
)
)
}
diff --git a/src/renderer/src/components/new-workspace/cloud-setup/cloud-setup-tracker.test.ts b/src/renderer/src/components/new-workspace/cloud-setup/cloud-setup-tracker.test.ts
index a169c440a3e..913f163430c 100644
--- a/src/renderer/src/components/new-workspace/cloud-setup/cloud-setup-tracker.test.ts
+++ b/src/renderer/src/components/new-workspace/cloud-setup/cloud-setup-tracker.test.ts
@@ -1,6 +1,5 @@
import { describe, expect, it } from 'vitest'
import type { CloudSetupStep } from '@/components/new-workspace/run-on/cloud-start-model'
-import { trackerSteps } from './CloudSetupBrowserTracker'
import { applySwitchTeamStep } from './CloudSetupSwitchTeam'
const derived: CloudSetupStep[] = [
@@ -9,25 +8,6 @@ const derived: CloudSetupStep[] = [
{ id: 'agent', state: 'optional' }
]
-describe('trackerSteps', () => {
- it('marks the first open row as the browser’s for rows derived from billing', () => {
- expect(trackerSteps(derived, false).map((step) => step.state)).toEqual([
- 'done',
- 'active',
- 'optional'
- ])
- })
-
- it('passes session rows that already say active, and stopped trips, through', () => {
- const session: CloudSetupStep[] = [
- { id: 'account', state: 'active' },
- { id: 'team', state: 'needed' }
- ]
- expect(trackerSteps(session, false)).toBe(session)
- expect(trackerSteps(derived, true)).toBe(derived)
- })
-})
-
describe('applySwitchTeamStep', () => {
const team: CloudSetupStep = { id: 'team', state: 'needed', detail: 'acme has a Team plan.' }
diff --git a/src/renderer/src/components/new-workspace/cloud-setup/use-cloud-setup-tracker.ts b/src/renderer/src/components/new-workspace/cloud-setup/use-cloud-setup-tracker.ts
index a4b64b924a5..7db939a3c06 100644
--- a/src/renderer/src/components/new-workspace/cloud-setup/use-cloud-setup-tracker.ts
+++ b/src/renderer/src/components/new-workspace/cloud-setup/use-cloud-setup-tracker.ts
@@ -1,117 +1,11 @@
-// The composer's side of the setup tracker (A6/X1) and the B2 switch row: what the tracker shows
-// while the browser trip is open, and the setup rows with B2's Team row swapped in.
-import { useCallback, useMemo, type ReactNode } from 'react'
-import { readCloudStartDraft, saveCloudStartDraft } from '@/lib/orca-cloud/cloud-start-draft'
-import type { CloudSetupSessionSnapshot } from '@/lib/orca-cloud/cloud-setup-session-seam'
-import type {
- CloudSetupStep,
- CloudSetupStepId
-} from '@/components/new-workspace/run-on/cloud-start-model'
-import { setupActionFor } from '@/components/new-workspace/run-on/cloud-start-actions'
-import type { OrcaCloudRunTargetAccess } from '../orca-cloud-run-target-access'
-import type { OrcaCloudAvailability } from '../../../../../shared/orca-yaml-hook-types'
-import type { CloudSetupBrowserTrackerProps } from './CloudSetupBrowserTracker'
+// B2 for Orca Cloud setup: the setup rows with the "Switch to " Team row swapped in.
+import { useMemo } from 'react'
+import type { CloudSetupStep } from '@/components/new-workspace/run-on/cloud-start-model'
import {
applySwitchTeamStep,
useCloudSetupSwitchTeam,
type CloudSetupSwitchTeam
} from './CloudSetupSwitchTeam'
-import { parseCloudSetupError } from './cloud-setup-errors'
-import {
- canCopyCloudSetupLink,
- copyCloudSetupLink,
- reopenCloudSetup
-} from './cloud-setup-tracker-actions'
-
-export type CloudSetupTrackerView = Omit<
- CloudSetupBrowserTrackerProps,
- 'steps' | 'rowActions' | 'className'
->
-
-/** What the tracker and B2 add to the composer's CloudStartView. */
-export type CloudSetupViewExtras = {
- /** Per-row actions in the setup panel (B2's "Switch to "). */
- rowActions?: Partial>
- /** While the setup trip is open in the browser: the tracker replaces the panel (A6, X1). */
- tracker?: CloudSetupTrackerView
- /** Start is held (aria-disabled) with this reason, e.g. "Switch to acme first" (B2). */
- primaryHeldReason?: string
-}
-
-/** "Cancel setup": the draft keeps its prompt but forgets the trip (finished steps stay done). */
-export function clearCloudSetupSessionFromDraft(profileId: string | null): void {
- const draft = readCloudStartDraft(profileId)
- if (draft) {
- saveCloudStartDraft(profileId, { ...draft, setupSessionId: null })
- }
-}
-
-/**
- * The tracker's props while a trip from this composer is still waiting in the browser, or stopped
- * on an error; undefined otherwise. Rows derived from billing (no session yet) work too: Reopen then
- * re-runs today's setup action and there is no link to copy.
- */
-export function useComposerSetupTracker({
- cloudSelected,
- setupStarted,
- allSet,
- readyToStart,
- session,
- sessionId,
- steps,
- access,
- availability,
- profileId,
- runSetup,
- onCancel
-}: {
- cloudSelected: boolean
- setupStarted: boolean
- allSet: boolean
- readyToStart: boolean
- session: CloudSetupSessionSnapshot | null
- sessionId: string | null
- steps: readonly CloudSetupStep[]
- access: OrcaCloudRunTargetAccess
- availability: OrcaCloudAvailability
- profileId: string | null
- /** A fresh setup trip for whatever is still open. */
- runSetup: () => void
- /** Ends the trip in the composer; the draft forgets the session here. */
- onCancel: () => unknown
-}): CloudSetupTrackerView | undefined {
- const error = useMemo(() => {
- const parsed = parseCloudSetupError(session?.error)
- return parsed && session?.teamName ? { ...parsed, teamName: session.teamName } : parsed
- }, [session?.error, session?.teamName])
- const open = cloudSelected && setupStarted && !allSet && (!readyToStart || error !== null)
- const rerun = useCallback(
- () => setupActionFor(steps, access, availability)?.(),
- [access, availability, steps]
- )
- const canManageBilling = access.state === 'no-plan' ? access.canManageBilling : true
- return useMemo(() => {
- if (!open) {
- return undefined
- }
- const reopen = (): void => void reopenCloudSetup(sessionId, rerun)
- return {
- error,
- canManageBilling,
- sessionId,
- onCancel: () => {
- onCancel()
- clearCloudSetupSessionFromDraft(profileId)
- },
- ...(sessionId && canCopyCloudSetupLink(sessionId)
- ? { onCopyLink: () => void copyCloudSetupLink(sessionId) }
- : {}),
- onReopen: reopen,
- // A canceled Checkout, a used trial or a failed team: a fresh trip covers what's still open.
- onErrorAction: (action) => (action === 'reopen-browser' ? reopen() : runSetup())
- }
- }, [canManageBilling, error, onCancel, open, profileId, rerun, runSetup, sessionId])
-}
/** B2 applied to the setup rows: the rows to show, and the switch when it holds Start. */
export function useSwitchTeamSteps(
@@ -126,16 +20,3 @@ export function useSwitchTeamSteps(
)
return { steps: switched ?? baseSteps, held: switched ? switchTeam : null }
}
-
-/** The view fields the tracker and B2 add. */
-export function setupViewExtras(
- switchTeam: CloudSetupSwitchTeam | null,
- tracker: CloudSetupTrackerView | undefined
-): CloudSetupViewExtras {
- return {
- ...(switchTeam
- ? { rowActions: { team: switchTeam.action }, primaryHeldReason: switchTeam.startHeldReason }
- : {}),
- ...(tracker ? { tracker } : {})
- }
-}
diff --git a/src/renderer/src/components/new-workspace/composer-cloud-auto-start.tsx b/src/renderer/src/components/new-workspace/composer-cloud-auto-start.tsx
new file mode 100644
index 00000000000..c327e43e5c7
--- /dev/null
+++ b/src/renderer/src/components/new-workspace/composer-cloud-auto-start.tsx
@@ -0,0 +1,61 @@
+// The composer's side of the Orca Cloud setup page (composer-redo): the snapshot the page keeps for
+// Back and for its auto-start, and the hidden composer that submits once setup is done.
+import React, { useEffect, useState } from 'react'
+import { Dialog } from '@/components/ui/dialog'
+import type { ComposerModalData } from './composer-modal-data'
+
+/** How long a hidden Orca Cloud auto-start may take before the dialog shows instead. */
+const CLOUD_AUTO_START_REVEAL_MS = 8_000
+
+/** The composer as it is now: prompt, project, base branch, linked item and agent pick. */
+export function buildComposerSnapshot(
+ modalData: ComposerModalData,
+ card: {
+ name: string
+ repoId: string
+ baseBranch?: string
+ linkedWorkItem?: ComposerModalData['linkedWorkItem']
+ note: string
+ },
+ agentPick: Omit, 'note'>
+): ComposerModalData {
+ return {
+ ...modalData,
+ prefilledName: card.name,
+ initialRepoId: card.repoId,
+ ...(card.baseBranch ? { initialBaseBranch: card.baseBranch } : {}),
+ linkedWorkItem: card.linkedWorkItem ?? null,
+ recovery: { ...agentPick, note: card.note }
+ }
+}
+
+/**
+ * Setup finished on its page: the composer submits with the dialog unseen. If the start can't run
+ * by itself in time (a form error, a revoked grant), the dialog appears with everything kept.
+ * Returns the modal data to render once visible (no second auto-start), or null while hidden.
+ */
+export function useCloudAutoStartReveal(modalData: ComposerModalData): ComposerModalData | null {
+ const [revealed, setRevealed] = useState(false)
+ useEffect(() => {
+ if (!modalData.cloudAutoStart) {
+ return
+ }
+ const timer = window.setTimeout(() => setRevealed(true), CLOUD_AUTO_START_REVEAL_MS)
+ return () => window.clearTimeout(timer)
+ }, [modalData.cloudAutoStart])
+ if (!modalData.cloudAutoStart) {
+ return modalData
+ }
+ return revealed ? { ...modalData, cloudAutoStart: false } : null
+}
+
+/** A closed Root still gives the title its context; nothing is portaled or shown. */
+export function HiddenComposer({ children }: { children: React.ReactNode }): React.JSX.Element {
+ return (
+
+
+ {children}
+
+
+ )
+}
diff --git a/src/renderer/src/components/new-workspace/composer-modal-data.ts b/src/renderer/src/components/new-workspace/composer-modal-data.ts
index 482ac94c762..0a2a0f21224 100644
--- a/src/renderer/src/components/new-workspace/composer-modal-data.ts
+++ b/src/renderer/src/components/new-workspace/composer-modal-data.ts
@@ -38,4 +38,6 @@ export type ComposerModalData = {
telemetrySource?: WorkspaceCreateTelemetrySource
contextualTourSource?: string
setupGuideTourRequestId?: string
+ /** Orca Cloud setup finished on its page: submit without showing the dialog (falls back to it). */
+ cloudAutoStart?: boolean
}
diff --git a/src/renderer/src/components/new-workspace/run-on/CloudStart.fixture.tsx b/src/renderer/src/components/new-workspace/run-on/CloudStart.fixture.tsx
deleted file mode 100644
index 177660c20a0..00000000000
--- a/src/renderer/src/components/new-workspace/run-on/CloudStart.fixture.tsx
+++ /dev/null
@@ -1,328 +0,0 @@
-// "Start in Orca Cloud" composer frames from cloud-start-onboarding spec v2: E1, A1, A6, A7, T1,
-// T2, E, X2, the panel's GitHub rows D1–D3 and D4's GitHub-only projects. Entry points and T4 are in CloudStartSurfaces.
-import { defineFixture } from '../../../component-fixtures/component-fixture'
-import type { OrcaCloudRunTargetAccess } from '../orca-cloud-run-target-access'
-import type { CloudSetupStep } from './cloud-start-model'
-import { ComposerPreview } from './cloud-start-preview-fixture'
-
-const setup = (): void => {
- window.api.agentApiKeys = undefined
- window.api.cloudSecrets = undefined
- // No agent-accounts bridge (as in builds without it): the Agent row reads "unavailable" instead
- // of awaiting the fixture harness's catch-all mock, which resolves to undefined.
- window.api.orcaCloudAgents = undefined
- window.api.cloudWorkspaces = { ...window.api.cloudWorkspaces, available: async () => true }
-}
-const entitled: OrcaCloudRunTargetAccess = { state: 'entitled', remainingSeconds: 41 * 3600 }
-const trial = (hours: number, canManageBilling = true): OrcaCloudRunTargetAccess => ({
- state: 'entitled',
- remainingSeconds: hours * 3600,
- trial: true,
- canManageBilling
-})
-const githubDone: CloudSetupStep = {
- id: 'github',
- state: 'done',
- label: 'GitHub access',
- status: 'Done',
- detail: 'acme/web'
-}
-const githubNeeded: CloudSetupStep = {
- id: 'github',
- state: 'needed',
- need: 'github',
- canFix: true,
- label: 'GitHub access',
- status: 'Needed',
- detail: 'Orca Cloud can’t reach acme/billing yet.',
- note: 'It clones your code from GitHub. Give access to this repository once.'
-}
-
-/** E1: Run on as segments, this computer picked; the machine moves to the hint line. */
-export const e1RunOnDefault = defineFixture({
- width: 560,
- setup,
- render: () =>
-})
-/** E1 with "⋯" open: today's host picker without the Orca Cloud row. */
-export const e1MorePlaces = defineFixture({
- ...e1RunOnDefault,
- interact: (frame) => {
- frame.querySelector('[aria-label="More places to run"]')?.click()
- }
-})
-/** A1: Orca Cloud picked while signed out; trial included, one setup trip. */
-export const a1SignedOut = defineFixture({
- width: 560,
- setup,
- render: () => (
-
- )
-})
-/** B1: signed in, the team can still get its trial. */
-export const b1TrialAvailable = defineFixture({
- width: 560,
- setup,
- render: () => (
-
- )
-})
-/** A6: Finish in your browser; each step ticks in place. */
-export const a6FinishInBrowser = defineFixture({
- width: 560,
- setup,
- render: () => (
-
- )
-})
-/** A7: everything done; "All set", then the composer starts by itself. */
-export const a7AllSet = defineFixture({
- width: 560,
- setup,
- render: () => (
-
- )
-})
-/** T1: trial hours in the composer hint. */
-export const t1TrialHint = defineFixture({
- width: 560,
- setup,
- render: () =>
-})
-/** T1 under half an hour: warning, with Choose plan for owners. */
-export const t1TrialLow = defineFixture({
- width: 560,
- setup,
- render: () =>
-})
-/** T1 low, member: who can choose a plan. */
-export const t1TrialLowMember = defineFixture({
- width: 560,
- setup,
- render: () =>
-})
-/** T2: the trial is used up at New workspace; the prompt is kept. */
-export const t2TrialUsedUp = defineFixture({
- width: 560,
- setup,
- render: () => (
-
- )
-})
-/** C1 with the trial used up: a member can't buy, so ⌘↵ runs on this computer. */
-export const t2TrialUsedUpMember = defineFixture({
- width: 560,
- setup,
- render: () => (
-
- )
-})
-/** D1: GitHub checked up front; "Give access and start". */
-export const d1GitHubNeeded = defineFixture({
- width: 560,
- setup,
- render: () => (
-
- )
-})
-/** D2: waiting for an owner of the GitHub org; ⌘↵ runs on this computer. */
-export const d2GitHubRequested = defineFixture({
- width: 560,
- setup,
- render: () => (
-
- )
-})
-/** D3: the project isn't on GitHub; outranks account and plan rows. */
-export const d3NotOnGitHub = defineFixture({
- width: 560,
- setup,
- render: () => (
- {} }
- }}
- />
- )
-})
-const cloudRepos = [
- { fullName: 'acme/billing', url: 'https://github.com/acme/billing', private: true },
- { fullName: 'acme/infra', url: 'https://github.com/acme/infra', private: true },
- { fullName: 'acme/docs', url: 'https://github.com/acme/docs', private: false }
-]
-/** D4: Orca Cloud picked; the Project picker adds GitHub repositories with no clone here. */
-export const d4GitHubProjects = defineFixture({
- width: 560,
- setup,
- render: () => (
-
- ),
- interact: async (frame) => {
- frame.querySelector('[aria-label="Browse projects"]')?.click()
- await new Promise((resolve) => setTimeout(resolve, 200))
- }
-})
-/** D4 picked: the repository stands in for the project; This computer says why it can't run it. */
-export const d4GitHubProjectPicked = defineFixture({
- width: 560,
- setup,
- render: () => (
-
- )
-})
-/** D3 for a folder: Orca Cloud stays selectable; the panel offers a GitHub repository instead. */
-export const d3FolderProject = defineFixture({
- width: 560,
- setup,
- render: () => (
-
- )
-})
-/** D3 for a folder, after "Choose a GitHub repository": the picker opens at the D4 section. */
-export const d3FolderChooseRepository = defineFixture({
- ...d3FolderProject,
- interact: async (frame) => {
- const link = [...frame.querySelectorAll('button')].find((b) =>
- b.textContent?.includes('Choose a GitHub repository')
- )
- link?.click()
- await new Promise((resolve) => setTimeout(resolve, 300))
- }
-})
-/** E: returning, entitled; remembered Orca Cloud, no panel, "Start in Orca Cloud". */
-export const eReturning = defineFixture({
- width: 560,
- setup,
- render: () => (
-
- )
-})
-/** X2: Orca Cloud can't be reached; the segment stays, disabled, with the reason. */
-export const x2Unreachable = defineFixture({
- width: 560,
- setup,
- render: () => (
-
- ),
- interact: async (frame) => {
- frame
- .querySelector('[role="radio"][aria-disabled="true"]')
- ?.dispatchEvent(new PointerEvent('pointermove', { bubbles: true, pointerType: 'mouse' }))
- await new Promise((resolve) => setTimeout(resolve, 500))
- }
-})
diff --git a/src/renderer/src/components/new-workspace/run-on/CloudStartAgentRow.fixture.tsx b/src/renderer/src/components/new-workspace/run-on/CloudStartAgentRow.fixture.tsx
deleted file mode 100644
index 35823b4b3f3..00000000000
--- a/src/renderer/src/components/new-workspace/run-on/CloudStartAgentRow.fixture.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-// Cloud-start G inside the "Set up Orca Cloud" panel: the Agent row from the Orca account's setup.
-import { defineFixture } from '../../../component-fixtures/component-fixture'
-import { translate } from '@/i18n/i18n'
-import type { OrcaCloudAgentSetupRowModel } from '@/lib/orca-cloud/orca-cloud-agent-setup-row'
-import { OrcaCloudSetupPanel } from './OrcaCloudSetupPanel'
-import { cloudStartAgentStep } from './cloud-start-agent-step'
-import type { CloudSetupStep } from './cloud-start-model'
-
-const BASE: CloudSetupStep[] = [
- { id: 'account', state: 'done', detail: 'neil@acme.dev' },
- { id: 'github', state: 'needed', need: 'github', canFix: true }
-]
-
-function panel(row: OrcaCloudAgentSetupRowModel) {
- return defineFixture({
- width: 560,
- render: () => {
- const agent = cloudStartAgentStep({ id: 'agent', state: 'optional' }, row, translate)
- return (
-
-
-
- )
- }
- })
-}
-
-export const gFromAccount = panel({ kind: 'covered', agent: 'claude', via: 'account' })
-export const gTeamKey = panel({ kind: 'covered', agent: 'codex', via: 'team' })
-export const gNotSetUp = panel({ kind: 'notSetUp', agent: 'claude' })
-export const gKillSwitch = panel({ kind: 'killSwitch', agent: 'claude' })
-export const gSignedOut = panel({ kind: 'signedOut', agent: 'claude' })
-export const gLoading = panel({ kind: 'loading', agent: 'claude' })
diff --git a/src/renderer/src/components/new-workspace/run-on/CloudStartBilling.fixture.tsx b/src/renderer/src/components/new-workspace/run-on/CloudStartBilling.fixture.tsx
deleted file mode 100644
index b56940016c5..00000000000
--- a/src/renderer/src/components/new-workspace/run-on/CloudStartBilling.fixture.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-// Orca Cloud picked on the segmented Run on, blocked by billing: the hint under Run on names the fix
-// for an owner or admin, and says who can for a member (DS-15). These replace the pre-#273 dropdown
-// fixtures NewWorkspaceComposerProjectSection:orcaCloudSelected* (DS-33).
-import { defineFixture } from '../../../component-fixtures/component-fixture'
-import type { OrcaCloudRunTargetAccess } from '../orca-cloud-run-target-access'
-import { ComposerPreview } from './cloud-start-preview-fixture'
-
-const setup = (): void => {
- window.api.agentApiKeys = undefined
- window.api.cloudSecrets = undefined
- // No agent-accounts bridge: the Agent row reads "unavailable" instead of awaiting the harness's
- // catch-all mock, which resolves to undefined (the page error DS-33 found).
- window.api.orcaCloudAgents = undefined
- window.api.cloudWorkspaces = { ...window.api.cloudWorkspaces, available: async () => true }
-}
-
-function blocked(access: OrcaCloudRunTargetAccess) {
- return defineFixture({
- width: 560,
- setup,
- render: () =>
- })
-}
-
-export const noPlanOwner = blocked({ state: 'no-plan', lapsed: false, canManageBilling: true })
-export const noPlanMember = blocked({ state: 'no-plan', lapsed: false, canManageBilling: false })
-export const outOfHoursMember = blocked({
- state: 'out-of-hours',
- cause: 'hours',
- canManageBilling: false
-})
-export const spendCapMember = blocked({
- state: 'out-of-hours',
- cause: 'cap',
- canManageBilling: false
-})
-export const paymentIssueAdmin = blocked({ state: 'payment-issue', canManageBilling: true })
diff --git a/src/renderer/src/components/new-workspace/run-on/CloudStartModalWidth.fixture.tsx b/src/renderer/src/components/new-workspace/run-on/CloudStartModalWidth.fixture.tsx
deleted file mode 100644
index c0f0b899ede..00000000000
--- a/src/renderer/src/components/new-workspace/run-on/CloudStartModalWidth.fixture.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-// "Start in Orca Cloud" at the real modal's width (sm:max-w-lg, ~448px of content) with its
-// "Create more" switch, where np4-e2e saw the primary button overflow the dialog's right edge.
-// Also the setup session's plan row (no raw "subscription") and the plan + GitHub subtitle.
-import { defineFixture } from '../../../component-fixtures/component-fixture'
-import type { OrcaCloudRunTargetAccess } from '../orca-cloud-run-target-access'
-import type { CloudSetupStep } from './cloud-start-model'
-import { ComposerPreview } from './cloud-start-preview-fixture'
-
-const setup = (): void => {
- window.api.agentApiKeys = undefined
- window.api.cloudSecrets = undefined
- window.api.orcaCloudAgents = undefined
- window.api.cloudWorkspaces = { ...window.api.cloudWorkspaces, available: async () => true }
-}
-const CARD_WIDTH = 480
-const noPlan: OrcaCloudRunTargetAccess = {
- state: 'no-plan',
- lapsed: false,
- canManageBilling: true
-}
-const githubNeeded: CloudSetupStep = {
- id: 'github',
- state: 'needed',
- need: 'github',
- canFix: true,
- label: 'GitHub access',
- status: 'Needed',
- detail: 'Orca Cloud can’t reach nwparker/orca-sandbox-push-test yet.',
- note: 'It clones your code from GitHub. Give access to this repository once.'
-}
-
-/** Signed out at the modal's width: "Create more", "Use this computer instead", "Set up and start". */
-export const modalSignedOut = defineFixture({
- width: CARD_WIDTH + 40,
- setup,
- render: () => (
-
- )
-})
-
-/** Only GitHub is missing: the longer "Give access and start" at the modal's width. */
-export const modalGiveAccess = defineFixture({
- width: CARD_WIDTH + 40,
- setup,
- render: () => (
-
- )
-})
-
-/** No plan and no GitHub access: the subtitle names both parts of the one browser visit. */
-export const modalPlanAndGitHub = defineFixture({
- width: CARD_WIDTH + 40,
- setup,
- render: () => (
-
- )
-})
-
-/** A setup session after Checkout: Team plan done with no raw "subscription" under it. */
-export const sessionPlanDone = defineFixture({
- width: CARD_WIDTH + 40,
- setup,
- render: () => (
-
- )
-})
-
-/** A setup session mid-Checkout, with an owner's GitHub approval pending: codes read as words. */
-export const sessionCheckoutAndRequested = defineFixture({
- width: CARD_WIDTH + 40,
- setup,
- render: () => (
-
- )
-})
diff --git a/src/renderer/src/components/new-workspace/run-on/FolderGitHubActions.tsx b/src/renderer/src/components/new-workspace/run-on/FolderGitHubActions.tsx
deleted file mode 100644
index d87118dd03c..00000000000
--- a/src/renderer/src/components/new-workspace/run-on/FolderGitHubActions.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-// The setup panel's way forward for a plain folder on Orca Cloud (design-squad: D3 for folders):
-// choose a GitHub repository from the Project picker (D4), or add a GitHub project.
-import React from 'react'
-import { Button } from '@/components/ui/button'
-import { useTranslate } from '@/i18n/use-translate'
-
-export function FolderGitHubActions({
- canChoose,
- onChooseRepository,
- onAddProject
-}: {
- /** Signed in with GitHub access: the Project picker lists repositories to choose from. */
- canChoose: boolean
- onChooseRepository: () => void
- onAddProject: () => void
-}): React.JSX.Element {
- const t = useTranslate()
- return (
-
- {canChoose ? (
-
- {t('components.newWorkspace.cloudOnly.chooseRepository', 'Choose a GitHub repository')}
-
- ) : null}
-
- {t('components.newWorkspace.cloudOnly.addGitHubProject', 'Add a GitHub project…')}
-
-
- )
-}
diff --git a/src/renderer/src/components/new-workspace/run-on/OrcaCloudSetupPanel.tsx b/src/renderer/src/components/new-workspace/run-on/OrcaCloudSetupPanel.tsx
deleted file mode 100644
index c57a8a6e4eb..00000000000
--- a/src/renderer/src/components/new-workspace/run-on/OrcaCloudSetupPanel.tsx
+++ /dev/null
@@ -1,351 +0,0 @@
-// "Set up Orca Cloud": what still stands between this prompt and a running sandbox (spec A1/B1/C1).
-// Shown when Orca Cloud is picked and anything required is missing; hidden once everything is done.
-// The rows come from billing today and from the one-trip setup session once it exists.
-import React from 'react'
-import { Check, CircleDashed, CircleDot, Loader2 } from 'lucide-react'
-import { Button } from '@/components/ui/button'
-import { Skeleton } from '@/components/ui/skeleton'
-import { cn } from '@/lib/utils'
-import { useTranslate, type Translate } from '@/i18n/use-translate'
-import {
- cloudSetupNeedsGitHub,
- cloudSetupNeedsPlan,
- type CloudSetupStep,
- type CloudSetupStepId
-} from './cloud-start-model'
-
-type RowCopy = { title: string; detail: string | null; note?: string; status?: string }
-
-function rowCopy(step: CloudSetupStep, t: Translate): RowCopy {
- switch (step.id) {
- case 'account':
- return {
- title: t('components.newWorkspace.cloudSetup.account', 'Orca account'),
- detail:
- step.state === 'done'
- ? null
- : t(
- 'components.newWorkspace.cloudSetup.accountNeededTeam',
- 'Sign in or create one. We’ll make a team for you.'
- )
- }
- case 'team':
- return { title: t('components.newWorkspace.cloudSetup.team', 'Team'), detail: null }
- case 'plan':
- return step.need === 'trial' ? trialCopy(step, t) : planCopy(step, t)
- case 'github':
- return {
- title: t('components.newWorkspace.cloudSetup.github', 'GitHub access'),
- detail:
- step.state === 'done'
- ? null
- : t(
- 'components.newWorkspace.cloudSetup.githubNeeded',
- 'Orca Cloud clones your code from GitHub. Give access to this repository once.'
- )
- }
- case 'agent':
- // The composer names the agent in `detail`; it never asks for a per-sandbox sign-in (G).
- return {
- title: t('components.newWorkspace.cloudSetup.agent', 'Agent'),
- detail: null,
- status: t('components.newWorkspace.cloudSetup.stateOptional', 'Optional')
- }
- }
-}
-
-/** A1/B1: the one-time trial, counted from billing's offer, never a literal. */
-function trialCopy(step: CloudSetupStep, t: Translate): RowCopy {
- const hours = step.trialSeconds ? Math.round((step.trialSeconds / 3600) * 10) / 10 : null
- return {
- title: t('components.newWorkspace.cloudSetup.trial', 'Trial'),
- detail:
- hours === null
- ? null
- : t(
- 'components.newWorkspace.cloudSetup.trialOffer',
- '{{hours}} sandbox hours to try Orca Cloud',
- {
- count: hours,
- hours: String(hours),
- defaultValue_one: '{{hours}} sandbox hour to try Orca Cloud'
- }
- ),
- note: t(
- 'components.newWorkspace.cloudSetup.trialNoCard',
- 'No card needed. Nothing is charged until you choose a plan.'
- ),
- status: t('components.newWorkspace.cloudSetup.stateIncluded', 'Included')
- }
-}
-
-function planCopy(step: CloudSetupStep, t: Translate): RowCopy {
- if (step.need === 'trial-used-up' && step.state === 'needed') {
- // T2 / C1: neutral, nothing failed. Members read who can fix it.
- return {
- title: t('components.newWorkspace.cloudSetup.plan', 'Team plan'),
- detail: t('components.newWorkspace.cloudSetup.trialUsedUp', 'Your trial is used up.'),
- note:
- step.canFix === false
- ? t(
- 'components.newWorkspace.cloudSetup.trialUsedUpMember',
- 'Only owners and admins can choose a plan.'
- )
- : t(
- 'components.newWorkspace.cloudSetup.trialUsedUpOwner',
- 'Choose a plan to keep using Orca Cloud. Your prompt is still here.'
- )
- }
- }
- return {
- title: t('components.newWorkspace.cloudSetup.plan', 'Team plan'),
- detail: planDetail(step, t)
- }
-}
-
-function planDetail(step: CloudSetupStep, t: Translate): string | null {
- if (step.state === 'pending') {
- return t('components.newWorkspace.cloudSetup.planAfterSignIn', 'Checked after you sign in')
- }
- if (step.state !== 'needed') {
- return null
- }
- const memberSuffix =
- step.canFix === false
- ? ` ${t('components.newWorkspace.cloudSetup.askOwner', 'Ask an owner or admin to fix it.')}`
- : ''
- switch (step.need) {
- case 'renew-plan':
- return (
- t('components.newWorkspace.cloudSetup.planEnded', 'Your Team plan ended.') + memberSuffix
- )
- // Members read who fixes it and how, per cause (glossary), not a generic "fix it".
- case 'hours':
- return step.canFix === false
- ? t(
- 'components.newWorkspace.cloudSetup.outOfHoursMember',
- 'Out of sandbox hours. Ask an owner or admin to add hours.'
- )
- : t('components.newWorkspace.cloudSetup.outOfHours', 'Out of sandbox hours.')
- case 'spend-cap':
- return step.canFix === false
- ? t(
- 'components.newWorkspace.cloudSetup.spendCapMember',
- 'Spend cap reached. Ask an owner or admin to raise the spend cap.'
- )
- : t('components.newWorkspace.cloudSetup.spendCap', 'Spend cap reached.')
- case 'payment':
- return step.canFix === false
- ? t(
- 'components.newWorkspace.cloudSetup.paymentFailedMember',
- 'Payment failed. Ask an owner or admin to update the payment method.'
- )
- : t('components.newWorkspace.cloudSetup.paymentFailed', 'Payment failed.')
- case 'github':
- return t(
- 'components.newWorkspace.cloudSetup.githubNeeded',
- 'Orca Cloud clones your code from GitHub. Give access to this repository once.'
- )
- case 'sign-in':
- case 'plan':
- case 'trial':
- case 'trial-used-up':
- case undefined:
- return step.canFix === false
- ? t(
- 'components.newWorkspace.cloudSetup.planMember',
- 'Orca Cloud needs a Team plan. Only owners and admins can choose one.'
- )
- : t('components.newWorkspace.cloudSetup.planNeeded', 'Orca Cloud needs a Team plan.')
- }
-}
-
-/**
- * A setup session's rows name how a step is covered with a code ('subscription', 'trial',
- * 'checkout', 'requested'); say it in words, or nothing. Undefined: not a session code.
- */
-export function sessionDetailCopy(step: CloudSetupStep, t: Translate): string | null | undefined {
- if (step.id === 'plan' && step.detail === 'subscription') {
- // "Team plan · Done" already says it.
- return null
- }
- if (step.id === 'plan' && step.detail === 'trial') {
- return t('components.newWorkspace.cloudSetup.planCoveredByTrial', 'Your trial covers it.')
- }
- if (step.id === 'plan' && step.detail === 'checkout') {
- return t(
- 'components.newWorkspace.cloudSetup.planCheckoutOpen',
- 'Finish checkout in your browser.'
- )
- }
- if (step.id === 'github' && step.detail === 'requested') {
- return t(
- 'components.newWorkspace.cloudSetup.githubRequestedSession',
- 'Waiting for an owner on GitHub to approve Orca Cloud.'
- )
- }
- return undefined
-}
-
-function stateLabel(step: CloudSetupStep, t: Translate): string {
- switch (step.state) {
- case 'done':
- return t('components.newWorkspace.cloudSetup.stateDone', 'Done')
- case 'optional':
- return t('components.newWorkspace.cloudSetup.stateOptional', 'Optional')
- case 'pending':
- return t('components.newWorkspace.cloudSetup.stateNext', 'Next')
- case 'active':
- return t('components.newWorkspace.cloudSetup.stateInBrowser', 'In browser')
- case 'failed':
- return t('components.newWorkspace.cloudSetup.stateFailed', "Didn't finish")
- case 'loading':
- return t('components.newWorkspace.cloudSetup.stateChecking', 'Checking')
- case 'needed':
- return t('components.newWorkspace.cloudSetup.stateNeeded', 'Needed')
- }
-}
-
-function StateIcon({ step }: { step: CloudSetupStep }): React.JSX.Element {
- // Neutral checks: done and an included trial are facts, not alerts.
- if (step.state === 'done' || step.need === 'trial') {
- return
- }
- if (step.state === 'active') {
- // The browser is working on this step; static under reduced motion, the label carries the state.
- return (
-
- )
- }
- if (step.state === 'needed' || step.state === 'failed') {
- return
- }
- return
-}
-
-export function OrcaCloudSetupPanel({
- steps,
- allSet = false,
- title,
- subtitle,
- children,
- rowActions,
- className
-}: {
- steps: readonly CloudSetupStep[]
- /** Setup just finished: "All set", then the composer starts the workspace by itself. */
- allSet?: boolean
- /** Overrides for variants such as the "Finish in your browser" tracker. */
- title?: string
- subtitle?: string
- /** Rendered under the rows (tracker errors and actions). */
- children?: React.ReactNode
- /** Per-row slot under a row's detail, e.g. the member "Ask for a Team plan" action on 'plan'. */
- rowActions?: Partial>
- className?: string
-}): React.JSX.Element {
- const t = useTranslate()
- const titleId = React.useId()
- return (
-
-
- {allSet
- ? t('components.newWorkspace.cloudSetup.allSet', 'All set')
- : (title ?? t('components.newWorkspace.cloudSetup.title', 'Set up Orca Cloud'))}
-
-
- {allSet
- ? t('components.newWorkspace.cloudSetup.starting', 'Starting your cloud workspace…')
- : (subtitle ??
- // No browser trip fixes a project without a GitHub repository (D3): say what holds.
- (steps.some((step) => step.blocksStart && step.state === 'failed')
- ? t(
- 'components.newWorkspace.cloudSetup.subtitleNeedsRepository',
- 'Orca Cloud needs a GitHub repository. Your prompt stays here.'
- )
- : // A member can't fix billing, and no browser trip will.
- steps.some((step) => step.state === 'needed' && step.canFix === false)
- ? t(
- 'components.newWorkspace.cloudSetup.subtitleOwnerFixes',
- 'An owner or admin needs to fix this. Your prompt stays here.'
- )
- : cloudSetupNeedsPlan(steps) && cloudSetupNeedsGitHub(steps)
- ? t(
- 'components.newWorkspace.cloudSetup.subtitleChoosePlanAndGitHub',
- 'One visit to your browser to choose a plan and give Orca Cloud access on GitHub. Your prompt stays here and starts when you’re done.'
- )
- : cloudSetupNeedsPlan(steps)
- ? t(
- 'components.newWorkspace.cloudSetup.subtitleChoosePlan',
- 'One visit to your browser to choose a plan. Your prompt stays here and starts when you’re done.'
- )
- : t(
- 'components.newWorkspace.cloudSetup.subtitleNoCard',
- 'One visit to your browser, no card. Your prompt stays here and starts when you’re done.'
- )))}
-
- {/* One polite announcement per row change ("Team plan, done"). */}
-
- {steps.map((step) => {
- const copy = rowCopy(step, t)
- const title = step.label ?? copy.title
- const state = step.status ?? copy.status ?? stateLabel(step, t)
- const note = step.note ?? copy.note
- const sessionDetail = sessionDetailCopy(step, t)
- const detail = sessionDetail === undefined ? (step.detail ?? copy.detail) : sessionDetail
- return (
-
-
-
-
-
- {title}
- {step.state === 'loading' ? (
-
- ) : detail ? (
- {detail}
- ) : null}
- {note ? (
- {note}
- ) : null}
- {step.action ? (
- // One shared in-row action ("Set up once", "How to push"): it must read as a button.
-
- {step.action.label}
-
- ) : null}
- {rowActions?.[step.id] ?? null}
-
-
- {`${title}, `}
- {state}
-
-
- )
- })}
-
- {children}
-
- )
-}
diff --git a/src/renderer/src/components/new-workspace/run-on/RunOnHintLine.tsx b/src/renderer/src/components/new-workspace/run-on/RunOnHintLine.tsx
deleted file mode 100644
index 9c3b64cc2ab..00000000000
--- a/src/renderer/src/components/new-workspace/run-on/RunOnHintLine.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-// The hint under Run on for this computer or a "⋯" target: where it runs (spec E1). When Orca
-// Cloud can't be reached, the line says so (X2) so the disabled segment never looks like a bug.
-import React from 'react'
-import { useTranslate } from '@/i18n/use-translate'
-import type { ProjectHostSetupOption } from '@/lib/project-host-setup-options'
-import type { OrcaCloudAvailability } from '../../../../../shared/orca-yaml-hook-types'
-import { getRecipeDetail, type EphemeralVmRecipeOption } from '../run-target-options'
-
-export function RunOnHintLine({
- hostOptions,
- selectedHostId,
- recipes,
- selectedRecipeId,
- orcaCloudAvailability
-}: {
- hostOptions: readonly ProjectHostSetupOption[]
- selectedHostId: string | null
- recipes: readonly EphemeralVmRecipeOption[]
- selectedRecipeId: string | null
- orcaCloudAvailability: OrcaCloudAvailability
-}): React.JSX.Element | null {
- const t = useTranslate()
- const recipe = recipes.find((candidate) => candidate.id === selectedRecipeId) ?? null
- const host = hostOptions.find((option) => option.id === selectedHostId) ?? null
- const where = recipe ? getRecipeDetail(recipe, t) : host ? `${host.label} · ${host.detail}` : null
- return (
-
- {where ?
{where}
: null}
- {orcaCloudAvailability === 'unreachable' ? (
-
- {t(
- 'components.newWorkspace.runOn.cloudUnreachableReason',
- "Orca Cloud: can't reach it right now. Check your connection."
- )}
-
- ) : null}
-
- )
-}
diff --git a/src/renderer/src/components/new-workspace/run-on/RunOnSegmentedControl.tsx b/src/renderer/src/components/new-workspace/run-on/RunOnSegmentedControl.tsx
deleted file mode 100644
index d4bf68717da..00000000000
--- a/src/renderer/src/components/new-workspace/run-on/RunOnSegmentedControl.tsx
+++ /dev/null
@@ -1,212 +0,0 @@
-// Run on as one click: "This computer · Orca Cloud · ⋯" (spec D1). ⋯ opens today's host picker
-// without its Orca Cloud row, and a target picked there replaces ⋯ so the choice stays visible.
-import React, { useMemo, useState } from 'react'
-import { Cloud, Ellipsis, Monitor } from 'lucide-react'
-import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover'
-import { SettingsSegmentedControl } from '@/components/settings/form-controls/SettingsSegmentedControl'
-import { useTranslate } from '@/i18n/use-translate'
-import type { ReadyProjectHostSetupOption } from '@/lib/project-host-setup-options'
-import { LOCAL_EXECUTION_HOST_ID } from '../../../../../shared/execution-host'
-import { ORCA_CLOUD_RECIPE_ID } from '../../../../../shared/hosted-sandbox/orca-cloud-recipe-id'
-import type { OrcaCloudAvailability } from '../../../../../shared/orca-yaml-hook-types'
-import RunTargetCombobox from '../RunTargetCombobox'
-import { isOrcaCloudRecipe } from '../run-target-options'
-import { getOrcaCloudSegmentState, getRunOnSegment, type RunOnSegment } from './cloud-start-model'
-
-type RunTargetComboboxProps = React.ComponentProps
-
-export type RunOnSegmentedControlProps = RunTargetComboboxProps & {
- orcaCloudAvailability: OrcaCloudAvailability
- /** Selects Orca Cloud even when the account still needs setup; the panel below says what's missing. */
- onSelectOrcaCloud: () => void
- /** Marks the control as touched so a remembered default never overrides a user pick. */
- onUserPick?: () => void
- /** D4: the picked project is on GitHub only, so this computer has nothing to run. */
- cloudOnlyRepo?: boolean
-}
-
-export default function RunOnSegmentedControl(
- props: RunOnSegmentedControlProps
-): React.JSX.Element {
- const {
- hostOptions,
- hostValue,
- hostExecutionHostId,
- onHostChange,
- recipes,
- recipeValue,
- onRecipeChange,
- orcaCloudAvailability,
- onSelectOrcaCloud,
- onUserPick,
- cloudOnlyRepo = false
- } = props
- const t = useTranslate()
- const [moreOpen, setMoreOpen] = useState(false)
-
- const localOption = useMemo(
- () =>
- hostOptions.find(
- (option): option is ReadyProjectHostSetupOption =>
- option.kind === 'ready' && option.hostId === LOCAL_EXECUTION_HOST_ID
- ) ?? null,
- [hostOptions]
- )
- const selectedRecipe = recipes.find((recipe) => recipe.id === recipeValue) ?? null
- const orcaCloudSelected = recipeValue === ORCA_CLOUD_RECIPE_ID
- const selectedHost =
- hostOptions.find(
- (option) =>
- option.kind === 'ready' &&
- option.id === hostValue &&
- (!hostExecutionHostId || option.hostId === hostExecutionHostId)
- ) ?? null
- const segment = getRunOnSegment({
- orcaCloudSelected,
- otherRecipeSelected: selectedRecipe !== null && !orcaCloudSelected,
- selectedHostIsLocal: selectedHost ? selectedHost.hostId === LOCAL_EXECUTION_HOST_ID : null
- })
- const cloudState = getOrcaCloudSegmentState(orcaCloudAvailability)
- const localReason = cloudOnlyRepo
- ? t(
- 'components.newWorkspace.cloudOnly.notOnThisComputer',
- 'Not on this computer · Orca Cloud only'
- )
- : t('components.newWorkspace.runOn.locationNotSet', 'Project location not set')
-
- const pickLocal = (): void => {
- if (!localOption || cloudOnlyRepo) {
- return
- }
- onUserPick?.()
- onRecipeChange?.(null)
- onHostChange?.(localOption.id)
- }
- const pickCloud = (): void => {
- if (cloudState !== 'enabled') {
- return
- }
- onUserPick?.()
- onSelectOrcaCloud()
- }
-
- const otherLabel =
- segment === 'other'
- ? ((selectedRecipe && !isOrcaCloudRecipe(selectedRecipe)
- ? selectedRecipe.name
- : selectedHost?.label) ?? null)
- : null
-
- const options = [
- {
- value: 'local' as const,
- label: (
-
-
- {t('components.newWorkspace.runOn.thisComputer', 'This computer')}
-
- ),
- disabled: !localOption || cloudOnlyRepo,
- // A disabled option says why in its accessible name too (spec E1).
- ariaLabel:
- localOption && !cloudOnlyRepo
- ? undefined
- : t('components.newWorkspace.runOn.thisComputerUnusable', 'This computer, {{reason}}', {
- reason: localReason
- }),
- tooltip: localOption && !cloudOnlyRepo ? localOption.label : localReason
- },
- ...(cloudState === 'hidden'
- ? []
- : [
- {
- value: 'orca-cloud' as const,
- label: (
-
-
- {t('components.newWorkspace.runOn.orcaCloud', 'Orca Cloud')}
-
- ),
- disabled: cloudState === 'unreachable',
- ariaLabel:
- cloudState === 'unreachable'
- ? t(
- 'components.newWorkspace.runOn.cloudUnreachableName',
- "Orca Cloud, can't reach it right now"
- )
- : undefined,
- tooltip:
- cloudState === 'unreachable'
- ? t(
- 'components.newWorkspace.runOn.cloudUnreachableReason',
- "Orca Cloud: can't reach it right now. Check your connection."
- )
- : t('components.newWorkspace.runOn.cloudHosted', 'Hosted sandbox')
- }
- ]),
- {
- value: 'other' as const,
- ariaLabel: otherLabel
- ? t('components.newWorkspace.runOn.moreWithTarget', 'More places to run: {{name}}', {
- name: otherLabel
- })
- : t('components.newWorkspace.runOn.more', 'More places to run'),
- label: otherLabel ? (
- {otherLabel}
- ) : (
-
- ),
- tooltip: t('components.newWorkspace.runOn.more', 'More places to run')
- }
- ]
-
- // The host picker keeps everything except Orca Cloud, which now has its own segment.
- const otherRecipes = useMemo(
- () => recipes.filter((recipe) => !isOrcaCloudRecipe(recipe)),
- [recipes]
- )
-
- return (
-
-
-
-
- value={segment ?? 'local'}
- activation="manual"
- size="sm"
- ariaLabel={t('auto.components.NewWorkspaceComposerCard.runOn', 'Run on')}
- options={options}
- onChange={(value) => {
- if (value === 'local') {
- pickLocal()
- } else if (value === 'orca-cloud') {
- pickCloud()
- } else {
- setMoreOpen(true)
- }
- }}
- />
-
-
-
- {
- onUserPick?.()
- onRecipeChange?.(null)
- onHostChange?.(setupId)
- setMoreOpen(false)
- }}
- onRecipeChange={(recipeId) => {
- onUserPick?.()
- onRecipeChange?.(recipeId)
- setMoreOpen(false)
- }}
- />
-
-
- )
-}
diff --git a/src/renderer/src/components/new-workspace/run-on/OrcaCloudSetupPanel.test.tsx b/src/renderer/src/components/new-workspace/run-on/cloud-setup-session-copy.test.ts
similarity index 94%
rename from src/renderer/src/components/new-workspace/run-on/OrcaCloudSetupPanel.test.tsx
rename to src/renderer/src/components/new-workspace/run-on/cloud-setup-session-copy.test.ts
index c36929e963a..7b424cd31b5 100644
--- a/src/renderer/src/components/new-workspace/run-on/OrcaCloudSetupPanel.test.tsx
+++ b/src/renderer/src/components/new-workspace/run-on/cloud-setup-session-copy.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { translate } from '@/i18n/i18n'
-import { sessionDetailCopy } from './OrcaCloudSetupPanel'
+import { sessionDetailCopy } from './cloud-setup-session-copy'
describe('sessionDetailCopy', () => {
// np4-e2e: the plan row read the session's raw "subscription".
diff --git a/src/renderer/src/components/new-workspace/run-on/cloud-setup-session-copy.ts b/src/renderer/src/components/new-workspace/run-on/cloud-setup-session-copy.ts
new file mode 100644
index 00000000000..2ad45b29562
--- /dev/null
+++ b/src/renderer/src/components/new-workspace/run-on/cloud-setup-session-copy.ts
@@ -0,0 +1,30 @@
+// Setup-session codes in words, for the Orca Cloud setup page.
+import type { Translate } from '@/i18n/use-translate'
+import type { CloudSetupStep } from './cloud-start-model'
+
+/**
+ * A setup session's rows name how a step is covered with a code ('subscription', 'trial',
+ * 'checkout', 'requested'); say it in words, or nothing. Undefined: not a session code.
+ */
+export function sessionDetailCopy(step: CloudSetupStep, t: Translate): string | null | undefined {
+ if (step.id === 'plan' && step.detail === 'subscription') {
+ // "Team plan · Done" already says it.
+ return null
+ }
+ if (step.id === 'plan' && step.detail === 'trial') {
+ return t('components.newWorkspace.cloudSetup.planCoveredByTrial', 'Your trial covers it.')
+ }
+ if (step.id === 'plan' && step.detail === 'checkout') {
+ return t(
+ 'components.newWorkspace.cloudSetup.planCheckoutOpen',
+ 'Finish checkout in your browser.'
+ )
+ }
+ if (step.id === 'github' && step.detail === 'requested') {
+ return t(
+ 'components.newWorkspace.cloudSetup.githubRequestedSession',
+ 'Waiting for an owner on GitHub to approve Orca Cloud.'
+ )
+ }
+ return undefined
+}
diff --git a/src/renderer/src/components/new-workspace/run-on/cloud-start-actions.ts b/src/renderer/src/components/new-workspace/run-on/cloud-start-actions.ts
index c2781995838..054aaaa6ced 100644
--- a/src/renderer/src/components/new-workspace/run-on/cloud-start-actions.ts
+++ b/src/renderer/src/components/new-workspace/run-on/cloud-start-actions.ts
@@ -1,39 +1,8 @@
-// What the "Start in Orca Cloud" primary button says, and the fix one click runs before a session
-// exists (sign in first, else the plan/billing action).
-import { toast } from 'sonner'
-import { translate } from '@/i18n/i18n'
+// The fix one click runs before a setup session exists (sign in first, else the plan/billing
+// action); the setup page's fallback when the session can't open.
import type { OrcaCloudAvailability } from '../../../../../shared/orca-yaml-hook-types'
import { getOrcaCloudPick, type OrcaCloudRunTargetAccess } from '../orca-cloud-run-target-access'
-import type { CloudPrimaryAction, CloudSetupStep } from './cloud-start-model'
-
-export function getCloudPrimaryLabel(action: CloudPrimaryAction): string {
- switch (action) {
- case 'start':
- case 'startTrial':
- return translate('components.newWorkspace.cloudStart.start', 'Start in Orca Cloud')
- case 'setUpAndStart':
- return translate('components.newWorkspace.cloudStart.setUpAndStart', 'Set up and start')
- case 'choosePlanAndStart':
- return translate(
- 'components.newWorkspace.cloudStart.choosePlanAndStart',
- 'Choose plan and start'
- )
- case 'renewAndStart':
- return translate('components.newWorkspace.cloudStart.renewAndStart', 'Renew and start')
- case 'giveAccessAndStart':
- return translate(
- 'components.newWorkspace.cloudStart.giveAccessAndStart',
- 'Give access and start'
- )
- case 'useThisComputer':
- return translate(
- 'components.newWorkspace.cloudStart.useThisComputer',
- 'Use this computer instead'
- )
- case 'checking':
- return translate('components.newWorkspace.cloudStart.checking', 'Checking Orca Cloud…')
- }
-}
+import type { CloudSetupStep } from './cloud-start-model'
/** The fix one click on the primary button runs: sign in first, else the plan/billing action. */
export function setupActionFor(
@@ -52,17 +21,3 @@ export function setupActionFor(
const pick = getOrcaCloudPick(effective)
return pick.kind === 'act' ? pick.run : null
}
-
-/** D2: an owner approved on GitHub while this composer waits; offer the start (never auto). */
-export function showGitHubGrantedToast(repoName: string, onStart: () => void): void {
- toast.message(
- translate(
- 'components.newWorkspace.cloudStart.githubGranted',
- 'GitHub access granted for {{repo}}',
- {
- repo: repoName
- }
- ),
- { action: { label: getCloudPrimaryLabel('start'), onClick: onStart } }
- )
-}
diff --git a/src/renderer/src/components/new-workspace/run-on/cloud-start-model.test.ts b/src/renderer/src/components/new-workspace/run-on/cloud-start-model.test.ts
index d5d4fe9bada..7cb5e31ca4b 100644
--- a/src/renderer/src/components/new-workspace/run-on/cloud-start-model.test.ts
+++ b/src/renderer/src/components/new-workspace/run-on/cloud-start-model.test.ts
@@ -1,54 +1,18 @@
import { describe, expect, it } from 'vitest'
import {
- cloudSetupNeedsGitHub,
deriveCloudSetupSteps,
- getCloudPrimaryAction,
getOrcaCloudSegmentState,
- getRunOnSegment,
- isCloudSetupPanelVisible,
resolveDefaultRunOn,
+ type CloudSetupStep,
withReachableGitHub
} from './cloud-start-model'
+import {
+ cloudSetupPageModel,
+ cloudSetupRequired,
+ sessionNeedsFrom
+} from '../../orca-cloud-setup/cloud-setup-page-model'
-describe('Run on segment', () => {
- it('maps the committed target to a segment', () => {
- expect(
- getRunOnSegment({
- orcaCloudSelected: true,
- otherRecipeSelected: false,
- selectedHostIsLocal: true
- })
- ).toBe('orca-cloud')
- expect(
- getRunOnSegment({
- orcaCloudSelected: false,
- otherRecipeSelected: true,
- selectedHostIsLocal: true
- })
- ).toBe('other')
- expect(
- getRunOnSegment({
- orcaCloudSelected: false,
- otherRecipeSelected: false,
- selectedHostIsLocal: false
- })
- ).toBe('other')
- expect(
- getRunOnSegment({
- orcaCloudSelected: false,
- otherRecipeSelected: false,
- selectedHostIsLocal: true
- })
- ).toBe('local')
- expect(
- getRunOnSegment({
- orcaCloudSelected: false,
- otherRecipeSelected: false,
- selectedHostIsLocal: null
- })
- ).toBeNull()
- })
-
+describe('Orca Cloud availability', () => {
it('keeps Orca Cloud visible but disabled when it cannot be reached (X2)', () => {
expect(getOrcaCloudSegmentState('unreachable')).toBe('unreachable')
expect(getOrcaCloudSegmentState('unavailable')).toBe('hidden')
@@ -58,17 +22,29 @@ describe('Run on segment', () => {
})
})
-describe('setup rows and the primary label', () => {
+describe('setup rows and the setup page', () => {
+ const page = (steps: Parameters[0]) =>
+ cloudSetupPageModel(steps, { agentSkipped: true }).action
+
it('starts directly when the account is entitled', () => {
const steps = deriveCloudSetupSteps({
availability: 'offered',
access: { state: 'entitled', remainingSeconds: 3600 }
})
- expect(isCloudSetupPanelVisible(steps)).toBe(false)
- expect(getCloudPrimaryAction(steps)).toBe('start')
+ expect(cloudSetupRequired(steps, { agentSkipped: true })).toBe(false)
+ expect(page(steps)).toBe('done')
})
- it('signed out: one setup trip ("Set up and start"), plan checked after sign-in', () => {
+ it('an agent with no account opens the page until it is skipped once', () => {
+ const steps = deriveCloudSetupSteps({
+ availability: 'offered',
+ access: { state: 'entitled', remainingSeconds: 3600 }
+ })
+ expect(cloudSetupRequired(steps)).toBe(true)
+ expect(cloudSetupPageModel(steps).action).toBe('agent')
+ })
+
+ it('signed out: sign-in first, plan checked after sign-in', () => {
const steps = deriveCloudSetupSteps({
availability: 'needs-sign-in',
access: { state: 'loading' }
@@ -78,49 +54,57 @@ describe('setup rows and the primary label', () => {
['plan', 'pending'],
['agent', 'optional']
])
- expect(isCloudSetupPanelVisible(steps)).toBe(true)
- expect(getCloudPrimaryAction(steps)).toBe('setUpAndStart')
+ expect(page(steps)).toBe('sign-in')
})
- it('owner without a plan: "Choose plan and start"; lapsed: "Renew and start"', () => {
+ it('owner without a plan chooses one in the browser; lapsed renews there', () => {
const noPlan = deriveCloudSetupSteps({
availability: 'needs-capability',
access: { state: 'no-plan', lapsed: false, canManageBilling: true }
})
- expect(getCloudPrimaryAction(noPlan)).toBe('choosePlanAndStart')
+ expect(page(noPlan)).toBe('choose-plan')
const lapsed = deriveCloudSetupSteps({
- availability: 'needs-capability',
+ availability: 'offered',
access: { state: 'no-plan', lapsed: true, canManageBilling: true }
})
- expect(getCloudPrimaryAction(lapsed)).toBe('renewAndStart')
+ expect(page(lapsed)).toBe('choose-plan')
+ expect(lapsed.find((s) => s.id === 'plan')?.need).toBe('renew-plan')
})
- it('a member who cannot fix it gets "Use this computer instead" and no agent row', () => {
+ it('a member who cannot fix billing is told to ask an owner, with no agent row', () => {
const steps = deriveCloudSetupSteps({
availability: 'needs-capability',
access: { state: 'no-plan', lapsed: false, canManageBilling: false }
})
expect(steps.some((s) => s.id === 'agent')).toBe(false)
- expect(getCloudPrimaryAction(steps)).toBe('useThisComputer')
+ expect(page(steps)).toBe('ask-owner')
const outOfHours = deriveCloudSetupSteps({
availability: 'offered',
access: { state: 'out-of-hours', cause: 'hours', canManageBilling: false }
})
- expect(getCloudPrimaryAction(outOfHours)).toBe('useThisComputer')
+ expect(page(outOfHours)).toBe('ask-owner')
})
- it('a missing GitHub grant alone reads "Give access and start"', () => {
+ it('an owner out of hours fixes it in billing', () => {
+ const steps = deriveCloudSetupSteps({
+ availability: 'offered',
+ access: { state: 'out-of-hours', cause: 'hours', canManageBilling: true }
+ })
+ expect(page(steps)).toBe('fix-billing')
+ })
+
+ it('a missing GitHub grant alone is the GitHub step', () => {
expect(
- getCloudPrimaryAction([
+ page([
{ id: 'account', state: 'done' },
{ id: 'plan', state: 'done' },
{ id: 'github', state: 'needed', need: 'github', canFix: true },
{ id: 'agent', state: 'optional' }
])
- ).toBe('giveAccessAndStart')
+ ).toBe('github-install')
})
- it('a team that can still get its trial just starts (B1); setup grants it with no card', () => {
+ it('a team that can still get its trial claims it in the app (B1)', () => {
const steps = deriveCloudSetupSteps({
availability: 'offered',
access: { state: 'no-plan', lapsed: false, canManageBilling: false, trialOfferSeconds: 7200 }
@@ -129,23 +113,21 @@ describe('setup rows and the primary label', () => {
expect(plan).toMatchObject({ need: 'trial', trialSeconds: 7200, canFix: true })
// Members aren't blocked from the trial: the agent row stays.
expect(steps.some((s) => s.id === 'agent')).toBe(true)
- expect(getCloudPrimaryAction(steps)).toBe('startTrial')
- // An included trial isn't a task: no panel, the hint line carries it.
- expect(isCloudSetupPanelVisible(steps)).toBe(false)
+ expect(page(steps)).toBe('start-trial')
})
- it('trial used up: owner "Choose plan and start"; member runs on this computer (T2, C1)', () => {
+ it('trial used up: owner chooses a plan; member asks an owner (T2, C1)', () => {
const owner = deriveCloudSetupSteps({
availability: 'offered',
access: { state: 'no-plan', lapsed: false, canManageBilling: true, trialUsedUp: true }
})
expect(owner.find((s) => s.id === 'plan')?.need).toBe('trial-used-up')
- expect(getCloudPrimaryAction(owner)).toBe('choosePlanAndStart')
+ expect(page(owner)).toBe('choose-plan')
const member = deriveCloudSetupSteps({
availability: 'offered',
access: { state: 'no-plan', lapsed: false, canManageBilling: false, trialUsedUp: true }
})
- expect(getCloudPrimaryAction(member)).toBe('useThisComputer')
+ expect(page(member)).toBe('ask-owner')
})
it('signed out: the trial row is included and GitHub is checked up front (A1)', () => {
@@ -156,17 +138,18 @@ describe('setup rows and the primary label', () => {
})
expect(steps.map((s) => s.id)).toEqual(['account', 'plan', 'github', 'agent'])
expect(steps.find((s) => s.id === 'plan')?.need).toBe('trial')
- expect(getCloudPrimaryAction(steps)).toBe('setUpAndStart')
+ expect(sessionNeedsFrom(steps)).toEqual(['account', 'plan', 'github'])
+ expect(page(steps)).toBe('sign-in')
})
- it('GitHub waiting on an owner blocks the start, so ⌘↵ runs locally (D2)', () => {
+ it('GitHub waiting on an owner: the page waits and starts on its own (D2)', () => {
const steps = deriveCloudSetupSteps({
availability: 'offered',
access: { state: 'entitled', remainingSeconds: 3600 },
github: { id: 'github', state: 'pending', need: 'github', blocksStart: true }
})
- expect(isCloudSetupPanelVisible(steps)).toBe(true)
- expect(getCloudPrimaryAction(steps)).toBe('useThisComputer')
+ expect(cloudSetupRequired(steps, { agentSkipped: true })).toBe(true)
+ expect(page(steps)).toBe('github-waiting')
})
it('no GitHub remote outranks account and plan rows (D3)', () => {
@@ -176,21 +159,36 @@ describe('setup rows and the primary label', () => {
github: { id: 'github', state: 'failed', need: 'github', blocksStart: true }
})
expect(steps.map((s) => s.id)).toEqual(['github'])
- expect(getCloudPrimaryAction(steps)).toBe('useThisComputer')
+ expect(page(steps)).toBe('needs-repository')
})
- it('a GitHub check still running holds the start (D1 cache miss)', () => {
- const steps = deriveCloudSetupSteps({
+ it('checks while GitHub or billing are still being read', () => {
+ const github = deriveCloudSetupSteps({
availability: 'offered',
access: { state: 'entitled', remainingSeconds: 3600 },
github: { id: 'github', state: 'loading' }
})
- expect(getCloudPrimaryAction(steps)).toBe('checking')
+ expect(page(github)).toBe('checking')
+ const billing = deriveCloudSetupSteps({ availability: 'offered', access: { state: 'loading' } })
+ expect(page(billing)).toBe('checking')
})
- it('waits while billing is still being read', () => {
- const steps = deriveCloudSetupSteps({ availability: 'offered', access: { state: 'loading' } })
- expect(getCloudPrimaryAction(steps)).toBe('checking')
+ it('B2: a team switch is its own step and always opens the page', () => {
+ const steps: CloudSetupStep[] = [
+ { id: 'account', state: 'done' },
+ { id: 'team', state: 'needed' }
+ ]
+ expect(cloudSetupRequired(steps, { teamSwitch: true })).toBe(true)
+ expect(cloudSetupPageModel(steps, { teamSwitch: true }).action).toBe('switch-team')
+ })
+
+ it('a still-loading agent row never holds a ready start', () => {
+ const steps: CloudSetupStep[] = [
+ { id: 'account', state: 'done' },
+ { id: 'plan', state: 'done' },
+ { id: 'agent', state: 'loading' }
+ ]
+ expect(cloudSetupRequired(steps)).toBe(false)
})
})
@@ -266,7 +264,7 @@ describe('withReachableGitHub', () => {
{ id: 'plan', state: 'done', detail: 'subscription' },
{ id: 'github', state: 'done' }
])
- expect(getCloudPrimaryAction([...withReachableGitHub([...session], true)])).toBe('start')
+ expect(cloudSetupPageModel([...withReachableGitHub([...session], true)]).action).toBe('done')
})
it('leaves the rows alone while the repository is still unreachable', () => {
@@ -274,15 +272,3 @@ describe('withReachableGitHub', () => {
expect(withReachableGitHub(steps, false)).toBe(steps)
})
})
-
-describe('cloudSetupNeedsGitHub', () => {
- it('counts GitHub access the browser visit gives, not an approval, a missing remote or a check', () => {
- expect(cloudSetupNeedsGitHub([{ id: 'github', state: 'needed', need: 'github' }])).toBe(true)
- expect(cloudSetupNeedsGitHub([{ id: 'github', state: 'active' }])).toBe(true)
- expect(cloudSetupNeedsGitHub([{ id: 'github', state: 'done' }])).toBe(false)
- expect(cloudSetupNeedsGitHub([{ id: 'github', state: 'loading' }])).toBe(false)
- expect(
- cloudSetupNeedsGitHub([{ id: 'github', state: 'pending', need: 'github', blocksStart: true }])
- ).toBe(false)
- })
-})
diff --git a/src/renderer/src/components/new-workspace/run-on/cloud-start-model.ts b/src/renderer/src/components/new-workspace/run-on/cloud-start-model.ts
index 4ea27621248..22137588ce5 100644
--- a/src/renderer/src/components/new-workspace/run-on/cloud-start-model.ts
+++ b/src/renderer/src/components/new-workspace/run-on/cloud-start-model.ts
@@ -1,30 +1,8 @@
-// "Start in Orca Cloud": what the Run on control shows, which setup rows are missing, and what the
-// composer's primary button says and does. Pure, so every state the spec draws is unit-tested here.
+// Orca Cloud from the composer: whether its Run on option is offered, which setup rows are missing,
+// and the remembered default. Pure, so every state the spec draws is unit-tested here.
import type { OrcaCloudAvailability } from '../../../../../shared/orca-yaml-hook-types'
import type { OrcaCloudRunTargetAccess } from '../orca-cloud-run-target-access'
-/** The three segments of Run on: this computer, Orca Cloud, and everything else behind "⋯". */
-export type RunOnSegment = 'local' | 'orca-cloud' | 'other'
-
-export function getRunOnSegment({
- orcaCloudSelected,
- otherRecipeSelected,
- selectedHostIsLocal
-}: {
- orcaCloudSelected: boolean
- otherRecipeSelected: boolean
- /** Null when no host is committed (the picker's "Choose where to run" state). */
- selectedHostIsLocal: boolean | null
-}): RunOnSegment | null {
- if (orcaCloudSelected) {
- return 'orca-cloud'
- }
- if (otherRecipeSelected || selectedHostIsLocal === false) {
- return 'other'
- }
- return selectedHostIsLocal === null ? null : 'local'
-}
-
/**
* How the Orca Cloud segment behaves. `hidden`: this build doesn't ship Orca Cloud.
* `unreachable`: it does, but the account check failed (offline); the segment stays visible and
@@ -47,7 +25,7 @@ export function getOrcaCloudSegmentState(
}
}
-/** One row of the "Set up Orca Cloud" panel. Shape shared with the setup session (team-billing-ux). */
+/** One step of the Orca Cloud setup page. Shape shared with the setup session (team-billing-ux). */
export type CloudSetupStepId = 'account' | 'team' | 'plan' | 'github' | 'agent'
export type CloudSetupStepState =
| 'done'
@@ -173,79 +151,6 @@ function derivePlanStep(
}
}
-/** Required rows still open; `optional` (agent) never counts. */
-export function getOpenSetupSteps(steps: readonly CloudSetupStep[]): CloudSetupStep[] {
- return steps.filter((step) => step.state !== 'done' && step.state !== 'optional')
-}
-
-/** The panel shows while anything required is open (or still loading). */
-export function isCloudSetupPanelVisible(steps: readonly CloudSetupStep[]): boolean {
- // An included trial isn't a task: with nothing else open, the hint line carries it (B1).
- return getOpenSetupSteps(steps).some((step) => step.need !== 'trial')
-}
-
-/** The browser trip is for choosing a plan (T2, lapsed, no plan), not a free trial. */
-export function cloudSetupNeedsPlan(steps: readonly CloudSetupStep[]): boolean {
- return getOpenSetupSteps(steps).some(
- (step) => step.need === 'plan' || step.need === 'renew-plan' || step.need === 'trial-used-up'
- )
-}
-
-/**
- * The same browser trip also gives Orca Cloud access to the repository on GitHub. An owner's
- * approval (D2) or a project not on GitHub (D3) isn't part of the trip; still checking isn't either.
- */
-export function cloudSetupNeedsGitHub(steps: readonly CloudSetupStep[]): boolean {
- return getOpenSetupSteps(steps).some(
- (step) => step.id === 'github' && !step.blocksStart && step.state !== 'loading'
- )
-}
-
-export type CloudPrimaryAction =
- | 'start'
- | 'setUpAndStart'
- | 'choosePlanAndStart'
- | 'renewAndStart'
- | 'giveAccessAndStart'
- /** Only the trial is missing: setup grants it, so the button just says Start (B1). */
- | 'startTrial'
- | 'useThisComputer'
- | 'checking'
-
-/**
- * The composer's primary action while Orca Cloud is selected. The label names where the work runs
- * (spec D10); a member who can't fix a block gets "Use this computer instead" so ⌘↵ always works.
- */
-export function getCloudPrimaryAction(steps: readonly CloudSetupStep[]): CloudPrimaryAction {
- const open = getOpenSetupSteps(steps)
- if (open.length === 0) {
- return 'start'
- }
- if (open.some((step) => (step.state === 'needed' && step.canFix === false) || step.blocksStart)) {
- return 'useThisComputer'
- }
- if (open.every((step) => step.state === 'loading')) {
- return 'checking'
- }
- const needed = open.filter((step) => step.state !== 'pending' && step.state !== 'loading')
- if (needed.length === 1) {
- const need = needed[0]?.need
- if (need === 'plan' || need === 'trial-used-up') {
- return 'choosePlanAndStart'
- }
- if (need === 'trial') {
- return 'startTrial'
- }
- if (need === 'renew-plan') {
- return 'renewAndStart'
- }
- if (need === 'github') {
- return 'giveAccessAndStart'
- }
- }
- return 'setUpAndStart'
-}
-
export type RunOnPreference = 'last' | 'local' | 'orca-cloud'
/**
diff --git a/src/renderer/src/components/new-workspace/run-on/cloud-start-preview-fixture.tsx b/src/renderer/src/components/new-workspace/run-on/cloud-start-preview-fixture.tsx
deleted file mode 100644
index 15445595c82..00000000000
--- a/src/renderer/src/components/new-workspace/run-on/cloud-start-preview-fixture.tsx
+++ /dev/null
@@ -1,194 +0,0 @@
-// Shared preview for the cloud-start fixtures: the composer's project section and footer wired to
-// the pure setup model, so every spec frame renders without a store or network.
-import React, { useState } from 'react'
-import type { ProjectHostSetupOption } from '@/lib/project-host-setup-options'
-import type { NewWorkspaceProjectOption } from '@/lib/new-workspace-project-options'
-import type { OrcaCloudAvailability } from '../../../../../shared/orca-yaml-hook-types'
-import { NewWorkspaceComposerProjectSection } from '../NewWorkspaceComposerProjectSection'
-import { NewWorkspaceComposerFooter } from '../NewWorkspaceComposerFooter'
-import type { EphemeralVmRecipeOption } from '../run-target-options'
-import type { OrcaCloudRunTargetAccess } from '../orca-cloud-run-target-access'
-import {
- deriveCloudSetupSteps,
- getCloudPrimaryAction,
- isCloudSetupPanelVisible,
- type CloudSetupStep
-} from './cloud-start-model'
-import type { CloudStartView } from './use-cloud-start-composer'
-import { getCloudPrimaryLabel } from './cloud-start-actions'
-import { decorateCloudSetupSteps } from './use-cloud-start-steps'
-import type { OrcaCloudGitHubRepository } from '@/lib/orca-cloud/use-orca-cloud-github-repositories'
-
-const projects: NewWorkspaceProjectOption[] = [
- {
- kind: 'project',
- id: 'web',
- projectId: 'web',
- displayName: 'acme/web',
- badgeColor: 'var(--muted-foreground)',
- detail: 'acme/web'
- }
-]
-/** A plain folder (no git): the D3-for-folders frames. */
-const folderProjects: NewWorkspaceProjectOption[] = [
- {
- kind: 'project',
- id: 'web',
- projectId: 'web',
- displayName: 'meeting-notes',
- badgeColor: 'var(--muted-foreground)',
- detail: '~/Documents/meeting-notes'
- }
-]
-const hosts: ProjectHostSetupOption[] = [
- {
- kind: 'ready',
- id: 'local',
- projectId: 'web',
- repoId: 'web',
- hostId: 'local',
- label: 'MacBook Pro',
- detail: '~/code/acme-web',
- path: '/Users/neil/code/acme-web'
- },
- {
- kind: 'ready',
- id: 'devbox',
- projectId: 'web',
- repoId: 'web-devbox',
- hostId: 'ssh:devbox',
- label: 'devbox',
- detail: 'SSH',
- path: '/home/neil/acme-web'
- }
-]
-const orcaCloudRecipe: EphemeralVmRecipeOption = {
- id: 'cloud',
- name: 'Orca Cloud',
- description: 'Hosted by Orca. Sleeps when idle and keeps your files.',
- provider: 'orca-cloud',
- create: 'orca-cloud create'
-}
-
-export function ComposerPreview({
- availability,
- access,
- cloudSelected,
- steps: pinnedSteps,
- github = null,
- allSet = false,
- prompt = 'Fix the flaky login test in e2e/auth.spec.ts and open a PR.',
- cloudRepos,
- pickedCloudRepo = null,
- folderProject = false,
- showCreateMultiple = false,
- width
-}: {
- availability: OrcaCloudAvailability
- access: OrcaCloudRunTargetAccess
- cloudSelected: boolean
- steps?: CloudSetupStep[]
- /** The GitHub row (D1–D3), as the repository check would report it. */
- github?: CloudSetupStep | null
- allSet?: boolean
- prompt?: string
- /** D4: GitHub repositories with no clone here. */
- cloudRepos?: OrcaCloudGitHubRepository[]
- pickedCloudRepo?: string | null
- /** The project is a plain folder (D3 for folders). */
- folderProject?: boolean
- /** The modal's "Create more" switch, which shares the footer row with the actions. */
- showCreateMultiple?: boolean
- /** Pins the card's width, e.g. the modal's content width. */
- width?: number
-}): React.JSX.Element {
- const [picked, setPicked] = useState(pickedCloudRepo)
- const [host, setHost] = useState('local')
- const [recipe, setRecipe] = useState(cloudSelected ? 'cloud' : null)
- const signedOut = availability === 'needs-sign-in' || access.state === 'signed-out'
- // G: Claude not set up yet, so the Agent row offers the one-time setup (or waits for sign-in).
- const steps = decorateCloudSetupSteps(
- pinnedSteps ?? deriveCloudSetupSteps({ availability, access, github }),
- signedOut ? { kind: 'signedOut', agent: 'claude' } : { kind: 'notSetUp', agent: 'claude' }
- )
- const selected = recipe === 'cloud'
- const panelVisible = selected && (isCloudSetupPanelVisible(steps) || allSet)
- const action = getCloudPrimaryAction(steps)
- const cloudStart: CloudStartView = {
- steps,
- panelVisible,
- allSet,
- onSelectOrcaCloud: () => setRecipe('cloud'),
- onUserPick: () => {},
- folderProject,
- ...(cloudRepos
- ? {
- cloudOnly: {
- repos: cloudRepos,
- showMore: () => {},
- picked: cloudRepos.find((repo) => repo.fullName === picked) ?? null,
- githubStep: null,
- folderProject,
- pick: setPicked,
- clear: () => setPicked(null),
- moreAccess: () => {},
- start: async () => true
- }
- }
- : {}),
- ...(panelVisible && !allSet && action !== 'useThisComputer'
- ? { onUseThisComputer: () => setRecipe(null) }
- : {})
- }
- return (
-
-
Create workspace
-
{prompt}
-
{}}
- projectDescriptionId="fixture-project-description"
- onAddProject={() => {}}
- focusNameInput={() => {}}
- shouldShowRunTargetPicker
- projectHostSetupOptions={hosts}
- selectedProjectHostSetupId={host}
- handleProjectHostSetupChange={(id) => setHost(id)}
- ephemeralVmRecipes={availability === 'offered' ? [orcaCloudRecipe] : []}
- selectedEphemeralVmRecipeId={recipe}
- onEphemeralVmRecipeChange={setRecipe}
- orcaCloudAvailability={availability}
- orcaCloudAccess={access}
- cloudStart={cloudStart}
- handleAddSshHost={() => {}}
- handleAddRemoteServer={() => {}}
- handleConnectRunTargetHost={async () => {}}
- handleSetLocation={() => {}}
- onConnectSelectedRepo={async () => {}}
- sshStatusLabel="Connected"
- connectButtonLabel="Connect"
- selectedProjectName={folderProject ? 'meeting-notes' : 'acme/web'}
- />
- {}}
- createDisabled={allSet || action === 'checking'}
- creating={allSet}
- primaryActionLabel={
- selected ? getCloudPrimaryLabel(allSet ? 'start' : action) : 'Create worktree'
- }
- submitShortcutModifierLabel="⌘"
- cloudStart={cloudStart}
- showCreateMultiple={showCreateMultiple}
- />
-
- )
-}
diff --git a/src/renderer/src/components/new-workspace/run-on/cloud-start-view.ts b/src/renderer/src/components/new-workspace/run-on/cloud-start-view.ts
index d2f656c4f3a..36193ccbb95 100644
--- a/src/renderer/src/components/new-workspace/run-on/cloud-start-view.ts
+++ b/src/renderer/src/components/new-workspace/run-on/cloud-start-view.ts
@@ -1,17 +1,11 @@
-// What the composer card renders for "Start in Orca Cloud", shared by the hook and the card.
-import type { CloudSetupStep } from './cloud-start-model'
+// What the composer card needs for Orca Cloud, shared by the hook and the card. Picking Orca Cloud
+// changes nothing else in the dialog (composer-redo): setup lives on its own page after Start.
import type { CloudOnlyRepo } from './use-cloud-only-repo'
-import type { CloudSetupViewExtras } from '../cloud-setup/use-cloud-setup-tracker'
-/** What the card renders: Run on's Orca Cloud segment, the setup panel and the fallback button. */
-export type CloudStartView = CloudSetupViewExtras & {
- steps: CloudSetupStep[]
- panelVisible: boolean
- allSet: boolean
+export type CloudStartView = {
+ /** Picks Orca Cloud even while the account still needs setup; Start walks it on a page. */
onSelectOrcaCloud: () => void
onUserPick: () => void
- /** Present while setup blocks a start: creates the same prompt on this computer. */
- onUseThisComputer?: () => void
/** D4: GitHub repositories with no clone here, and the one picked. */
cloudOnly?: CloudOnlyRepo
/** The project is a plain folder: Orca Cloud needs a GitHub repository instead (D3/D4). */
diff --git a/src/renderer/src/components/new-workspace/run-on/use-cloud-start-composer.ts b/src/renderer/src/components/new-workspace/run-on/use-cloud-start-composer.ts
index 8f5ae3564e7..28cc960bfd6 100644
--- a/src/renderer/src/components/new-workspace/run-on/use-cloud-start-composer.ts
+++ b/src/renderer/src/components/new-workspace/run-on/use-cloud-start-composer.ts
@@ -1,60 +1,81 @@
-// Composer glue for "Start in Orca Cloud": the setup rows, the primary button's label and action,
-// setup instead of a build that billing or GitHub would refuse, the start on its own once setup
-// finishes (O4), the draft on disk (N7), and where each project last started (O5).
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+// Composer glue for Orca Cloud (composer-redo). Picking Orca Cloud changes nothing else in the
+// dialog: Start either creates the workspace (everything is set up) or saves the draft, closes the
+// dialog and opens the full-page setup, which starts the prompt by itself when done. Also: where
+// each project last started (O5), the remembered default (D7) and GitHub-only repositories (D4).
+import { useCallback, useEffect, useMemo, useRef } from 'react'
import { toast } from 'sonner'
import { useAppStore } from '@/store/index'
-import { claimOrcaCloudTrial } from '@/components/billing/claim-trial'
import { translate } from '@/i18n/i18n'
-import type {
- ProjectHostSetupOption,
- ReadyProjectHostSetupOption
-} from '@/lib/project-host-setup-options'
+import { clearCloudStartDraft, saveCloudStartDraft } from '@/lib/orca-cloud/cloud-start-draft'
import {
- clearCloudStartDraft,
- readCloudStartDraft,
- saveCloudStartDraft
-} from '@/lib/orca-cloud/cloud-start-draft'
-import { getCloudSetupStarter } from '@/lib/orca-cloud/cloud-setup-session-seam'
+ isCloudSetupAgentSkipped,
+ openCloudSetupPage
+} from '@/lib/orca-cloud/cloud-setup-page-store'
import { recordSuccessfulRunOn } from '@/lib/orca-cloud/run-on-memory'
-import { LOCAL_EXECUTION_HOST_ID } from '../../../../../shared/execution-host'
import { ORCA_CLOUD_RECIPE_ID } from '../../../../../shared/hosted-sandbox/orca-cloud-recipe-id'
import type { OrcaCloudAvailability } from '../../../../../shared/orca-yaml-hook-types'
import { useOrcaCloudRunTargetAccess } from '../orca-cloud-billing-cache'
import type { OrcaCloudRunTargetAccess } from '../orca-cloud-run-target-access'
-import { getCloudPrimaryLabel, setupActionFor, showGitHubGrantedToast } from './cloud-start-actions'
-import {
- getCloudPrimaryAction,
- getOpenSetupSteps,
- getOrcaCloudSegmentState,
- isCloudSetupPanelVisible
-} from './cloud-start-model'
+import type { ComposerModalData } from '../composer-modal-data'
+import { getOrcaCloudSegmentState } from './cloud-start-model'
import { getOrcaCloudStartToast } from './orca-cloud-hours-copy'
import { useCloudStartSteps } from './use-cloud-start-steps'
-import { useSetupAutoStart } from './use-setup-auto-start'
-import {
- setupViewExtras,
- useComposerSetupTracker,
- useSwitchTeamSteps
-} from '../cloud-setup/use-cloud-setup-tracker'
+import { useSwitchTeamSteps } from '../cloud-setup/use-cloud-setup-tracker'
+import { cloudSetupRequired } from '../../orca-cloud-setup/cloud-setup-page-model'
import { useRunOnDefault } from './use-run-on-default'
import { useCloudOnlyCreate, useCloudOnlyRepo } from './use-cloud-only-repo'
import type { CloudStartView } from './cloud-start-view'
+import { openOrcaCloudAgentAccounts } from '../OrcaCloudAgentSetupRow'
+import { agentName } from '../../settings/agent-accounts/orca-cloud-accounts-copy'
+import { isOrcaCloudAccountAgent } from '../../../../../shared/orca-cloud-agents/orca-cloud-agents-wire'
export type { CloudStartView } from './cloud-start-view'
+/** The workspace started without its agent: one clear way to set the agent up for next time. */
+function showAgentNotStartedToast(agent: string | null): void {
+ const name = isOrcaCloudAccountAgent(agent) ? agentName(agent, translate) : null
+ toast.message(
+ name
+ ? translate(
+ 'components.newWorkspace.cloudStart.agentNotStarted',
+ '{{agent}} isn’t set up for Orca Cloud, so it didn’t start. The workspace has a terminal.',
+ { agent: name }
+ )
+ : translate(
+ 'components.newWorkspace.cloudStart.agentNotStartedGeneric',
+ 'Your agent isn’t set up for Orca Cloud, so it didn’t start. The workspace has a terminal.'
+ ),
+ {
+ action: {
+ label: name
+ ? translate(
+ 'components.newWorkspace.cloudStart.setUpAgentOnce',
+ 'Set up {{agent}} once',
+ {
+ agent: name
+ }
+ )
+ : translate('components.newWorkspace.cloudStart.setUpAgentOnceGeneric', 'Set up once'),
+ onClick: openOrcaCloudAgentAccounts
+ }
+ }
+ )
+}
+
export function useCloudStartComposer({
active,
availability,
selectedRecipeId,
setRecipeId,
- hostOptions,
- onHostChange,
repoId,
prompt,
agent,
initialTargetFixed,
+ autoStart = false,
+ createReady = true,
+ composerSnapshot,
onCreate: createWorkspace,
+ onCreateWithoutAgent,
onClose,
accessOverride
}: {
@@ -62,26 +83,28 @@ export function useCloudStartComposer({
availability: OrcaCloudAvailability
selectedRecipeId: string | null
setRecipeId: ((recipeId: string | null) => void) | undefined
- hostOptions: readonly ProjectHostSetupOption[]
- onHostChange: ((setupId: string) => void) | undefined
repoId: string
prompt: string
agent: string | null
/** The composer was opened on an explicit target (Landing card, a recovery). */
initialTargetFixed: boolean
+ /** Reopened by the setup page once everything is done: submit as soon as the form is ready. */
+ autoStart?: boolean
+ /** The composer's own create would run now (project picked, agent resolved). */
+ createReady?: boolean
+ /** The composer as it is now, for the setup page's Back and its auto-start. */
+ composerSnapshot: () => ComposerModalData
onCreate: () => void
- /** Closes the composer once a cloud-only repository has started (D4). */
+ /** The same create with no agent launched (the agent has no Orca Cloud account yet). */
+ onCreateWithoutAgent?: () => void
+ /** Closes the composer (setup moves to its page; a cloud-only repository started, D4). */
onClose?: () => void
/** Fixtures pin the account's access. */
accessOverride?: OrcaCloudRunTargetAccess
}): {
view: CloudStartView | null
- primaryActionLabel: string | null
/** Replaces the plain create while Orca Cloud is selected. */
onPrimary: (() => void) | null
- primaryDisabled: boolean
- /** False while the primary runs setup or the local fallback instead of a plain create. */
- primaryIsCreate: boolean
/** Call from the composer's onCreated: only a successful start is remembered. */
recordCreated: () => void
} {
@@ -96,66 +119,46 @@ export function useCloudStartComposer({
cloudOffered: segment === 'enabled',
signedIn: availability !== 'needs-sign-in' && access.state !== 'signed-out'
})
- const onCreate = useCloudOnlyCreate({
- cloudOnly,
- createWorkspace,
- prompt,
- agent,
- profileId,
- hoursToast: getOrcaCloudStartToast(access, translate, navigator.language),
- onClose
- })
- const [startedSessionId, setSetupSessionId] = useState(null)
- // A composer reopened after setup (e.g. the Checkout return) resumes its saved draft: derived,
- // not copied into state, so it is right on the first render.
- const restoredDraft = useMemo(
- () => (active && cloudSelected ? readCloudStartDraft(profileId) : null),
- [active, cloudSelected, profileId]
- )
- const [restoreDone, setRestoreDone] = useState(false)
- const restored = restoredDraft !== null && restoredDraft.repoId === repoId && !restoreDone
- const setupSessionId = startedSessionId ?? (restored ? restoredDraft.setupSessionId : null)
- const [startedHere, setSetupStarted] = useState(false)
- const setupStarted = startedHere || restored
- const setupStartedRef = useRef(setupStarted)
- const onCreateRef = useRef(onCreate)
- useEffect(() => {
- setupStartedRef.current = setupStarted
- onCreateRef.current = onCreate
- }, [onCreate, setupStarted])
- const {
- steps: rowSteps,
- session,
- openInstall
- } = useCloudStartSteps({
+ const { steps: rowSteps } = useCloudStartSteps({
availability,
access,
repoId,
agent,
cloudSelected,
- setupSessionId,
- githubOverride: cloudOnly.githubStep,
- onGranted: (repoName) => {
- // D2: an owner approved on GitHub. Setup already running starts by itself; otherwise ask.
- if (setupStartedRef.current) {
- return
- }
- showGitHubGrantedToast(repoName, () => onCreateRef.current())
- }
+ setupSessionId: null,
+ githubOverride: cloudOnly.githubStep
})
- // B2: on the personal account while a team the user is on has a plan, switch instead of buying.
- const { steps, held: switchTeam } = useSwitchTeamSteps(rowSteps, cloudSelected && !session)
- const action = getCloudPrimaryAction(steps)
- const panelVisible = cloudSelected && isCloudSetupPanelVisible(steps)
-
- const localOption = useMemo(
- () =>
- hostOptions.find(
- (option): option is ReadyProjectHostSetupOption =>
- option.kind === 'ready' && option.hostId === LOCAL_EXECUTION_HOST_ID
- ) ?? null,
- [hostOptions]
- )
+ const { steps, held: switchTeam } = useSwitchTeamSteps(rowSteps, cloudSelected)
+ const setupNeeded = cloudSetupRequired(steps, {
+ teamSwitch: switchTeam !== null,
+ agentSkipped: isCloudSetupAgentSkipped(agent)
+ })
+ // The agent has no Orca Cloud account (the user skipped setting it up): never launch it into its
+ // /login. Start a plain terminal and say how to set the agent up once.
+ const agentUnset =
+ cloudSelected && steps.some((step) => step.id === 'agent' && step.state === 'optional')
+ const onCreateRaw = useCloudOnlyCreate({
+ cloudOnly,
+ createWorkspace,
+ prompt,
+ agent: agentUnset ? null : agent,
+ profileId,
+ hoursToast: getOrcaCloudStartToast(access, translate, navigator.language),
+ onClose
+ })
+ const onCreate = useCallback(() => {
+ if (!agentUnset) {
+ onCreateRaw()
+ return
+ }
+ // A GitHub-only start already leaves the agent out (above); a local project's create skips it.
+ if (cloudOnly.picked || !onCreateWithoutAgent) {
+ onCreateRaw()
+ } else {
+ onCreateWithoutAgent()
+ }
+ showAgentNotStartedToast(agent)
+ }, [agent, agentUnset, cloudOnly.picked, onCreateRaw, onCreateWithoutAgent])
const { markTouched } = useRunOnDefault({
active,
@@ -172,95 +175,41 @@ export function useCloudStartComposer({
setRecipeId?.(ORCA_CLOUD_RECIPE_ID)
}, [markTouched, setRecipeId])
- // "Use this computer instead": switch, then submit once the switch has rendered.
- const localSubmitPendingRef = useRef(false)
- const useThisComputer = useCallback(() => {
- if (!localOption) {
- return
- }
- markTouched()
- localSubmitPendingRef.current = true
- setRecipeId?.(null)
- onHostChange?.(localOption.id)
- }, [localOption, markTouched, onHostChange, setRecipeId])
- useEffect(() => {
- if (localSubmitPendingRef.current && !cloudSelected) {
- localSubmitPendingRef.current = false
- createWorkspace()
- }
- }, [cloudSelected, createWorkspace])
-
- // O4: setup started here (or its restored draft) starts by itself once every row is done.
- const startAfterSetup = useCallback(() => {
- setSetupStarted(false)
- setRestoreDone(true)
- onCreate()
- }, [onCreate])
- const allSet = useSetupAutoStart({
- armed: setupStarted && cloudSelected,
- done: action === 'start' || session?.finished === true,
- setupSessionId,
- onStart: startAfterSetup
- })
-
- // The draft survives the browser trip and a quit; it is cleared once a workspace starts.
- const runSetup = useCallback(() => {
- const draft = {
+ // Start with setup still open: the dialog closes and the page takes over, prompt kept (N6).
+ const openSetup = useCallback(() => {
+ saveCloudStartDraft(profileId, {
prompt,
repoId,
agent,
- target: 'orca-cloud' as const,
- setupSessionId,
+ target: 'orca-cloud',
+ setupSessionId: null,
createdAt: Date.now()
- }
- saveCloudStartDraft(profileId, draft)
- setSetupStarted(true)
- const open = getOpenSetupSteps(steps)
- // D1: GitHub is the only thing missing, so its install page is the whole trip.
- if (open.length > 0 && open.every((step) => step.id === 'github' || step.id === 'agent')) {
- void openInstall()
- return
- }
- const startSession = getCloudSetupStarter()
- const fallback = setupActionFor(steps, access, availability)
- if (!startSession) {
- fallback?.()
- return
- }
- // One browser trip for every missing step; the session id rides on the draft across a quit.
- void startSession({ repoId, needs: open.map((step) => step.id) })
- .then((opened) => {
- if (!opened) {
- fallback?.()
- return
- }
- setSetupSessionId(opened.sessionId)
- saveCloudStartDraft(profileId, { ...draft, setupSessionId: opened.sessionId })
- })
- .catch(() => fallback?.())
- }, [access, agent, availability, openInstall, profileId, prompt, repoId, setupSessionId, steps])
-
- // B1: only the trial is missing. Claiming it is the whole setup; the billing cache then reads
- // entitled and the auto-start above submits. A personal account needs a team first: setup trip.
- const startTrial = useCallback(() => {
- setSetupStarted(true)
- void claimOrcaCloudTrial().then((result) => {
- if (result.status !== 'ok') {
- runSetup()
- }
})
- }, [runSetup])
+ openCloudSetupPage({
+ prompt,
+ repoId,
+ agent,
+ availability,
+ cloudOnlyRepo: cloudOnly.picked?.fullName ?? null,
+ composer: composerSnapshot()
+ })
+ onClose?.()
+ }, [agent, availability, cloudOnly.picked, composerSnapshot, onClose, profileId, prompt, repoId])
- const trip = { cloudSelected, setupStarted, allSet, session, steps, access, availability }
- const tracker = useComposerSetupTracker({
- ...trip,
- profileId,
- readyToStart: action === 'start',
- sessionId: setupSessionId,
- runSetup,
- // Ends the trip here; the tracker hook also clears the draft's session id.
- onCancel: () => [setSetupStarted(false), setRestoreDone(true), setSetupSessionId(null)]
- })
+ // Reopened by the setup page: submit once the account and form are ready. Anything still open
+ // (a race, a revoked grant) leaves the composer for the user, never a second trip on its own.
+ const autoStartedRef = useRef(false)
+ const stepsSettled = !steps.some((step) => step.state === 'loading' && step.id !== 'agent')
+ useEffect(() => {
+ if (!autoStart || autoStartedRef.current || !cloudSelected || !createReady || !stepsSettled) {
+ return
+ }
+ if (setupNeeded) {
+ return
+ }
+ autoStartedRef.current = true
+ onCreate()
+ }, [autoStart, cloudSelected, createReady, onCreate, setupNeeded, stepsSettled])
const recordCreated = useCallback(() => {
if (!repoId) {
@@ -278,51 +227,20 @@ export function useCloudStartComposer({
}
}, [access, cloudSelected, profileId, repoId])
- // Orca Cloud isn't the target (or this build has none): the composer's own create runs.
- const plainCreate = {
- primaryActionLabel: null,
- onPrimary: null,
- primaryDisabled: false,
- primaryIsCreate: true,
- recordCreated
- }
- if (segment === 'hidden') {
- return { view: null, ...plainCreate }
- }
- const view: CloudStartView = {
- steps,
- panelVisible: panelVisible || allSet,
- allSet,
- onSelectOrcaCloud,
- onUserPick: markTouched,
- cloudOnly,
- folderProject: cloudOnly.folderProject,
- ...setupViewExtras(switchTeam, tracker),
- ...(panelVisible && localOption && action !== 'useThisComputer' && !cloudOnly.picked
- ? { onUseThisComputer: useThisComputer }
- : {})
- }
- if (!cloudSelected) {
- return { view, ...plainCreate }
- }
- const onPrimary = switchTeam
- ? () => undefined
- : action === 'start'
- ? onCreate
- : action === 'useThisComputer'
- ? useThisComputer
- : action === 'checking'
- ? () => undefined
- : action === 'startTrial'
- ? startTrial
- : runSetup
- return {
- view,
- primaryActionLabel: getCloudPrimaryLabel(allSet || switchTeam ? 'start' : action),
- onPrimary,
- primaryDisabled:
- action === 'checking' || allSet || (action === 'useThisComputer' && !localOption),
- primaryIsCreate: action === 'start',
- recordCreated
+ const view = useMemo(
+ () =>
+ segment === 'hidden'
+ ? null
+ : {
+ onSelectOrcaCloud,
+ onUserPick: markTouched,
+ cloudOnly,
+ folderProject: cloudOnly.folderProject
+ },
+ [cloudOnly, markTouched, onSelectOrcaCloud, segment]
+ )
+ if (!view || !cloudSelected) {
+ return { view, onPrimary: null, recordCreated }
}
+ return { view, onPrimary: setupNeeded ? openSetup : onCreate, recordCreated }
}
diff --git a/src/renderer/src/components/new-workspace/run-on/use-composer-cloud-start.ts b/src/renderer/src/components/new-workspace/run-on/use-composer-cloud-start.ts
index b4d3b564034..fd9ec93edc9 100644
--- a/src/renderer/src/components/new-workspace/run-on/use-composer-cloud-start.ts
+++ b/src/renderer/src/components/new-workspace/run-on/use-composer-cloud-start.ts
@@ -1,20 +1,22 @@
-// The composer modal's side of "Start in Orca Cloud": feeds the composer state into
-// useCloudStartComposer and returns what the primary button and ⌘↵ do.
+// The composer modal's side of Orca Cloud: feeds the composer state into useCloudStartComposer and
+// returns what the primary button and ⌘↵ do.
import { useCallback, useEffect, type MutableRefObject } from 'react'
import type { ComposerModalData } from '../composer-modal-data'
import type { NewWorkspaceComposerCardProps } from '../new-workspace-composer-card-props'
import { useCloudStartComposer } from './use-cloud-start-composer'
+import { buildComposerSnapshot } from '../composer-cloud-auto-start'
+import type { TuiAgent } from '../../../../../shared/agent-launch/catalog/tui-agent'
type ComposerCardState = Pick<
NewWorkspaceComposerCardProps,
| 'orcaCloudAvailability'
| 'selectedEphemeralVmRecipeId'
| 'onEphemeralVmRecipeChange'
- | 'projectHostSetupOptions'
- | 'onProjectHostSetupChange'
| 'repoId'
| 'name'
->
+ | 'baseBranch'
+ | 'note'
+> & { linkedWorkItem?: ComposerModalData['linkedWorkItem'] }
export function useComposerCloudStart({
active,
@@ -23,15 +25,21 @@ export function useComposerCloudStart({
quickAgent,
handleCreate,
createDisabled,
+ quickAgentPick,
+ noAgentRef,
recordCreatedRef,
onClose
}: {
active: boolean
modalData: ComposerModalData
cardProps: ComposerCardState
- quickAgent: string | null
+ quickAgent: TuiAgent | null
handleCreate: () => Promise
+ /** Set right before a create to start it with no agent. */
+ noAgentRef: MutableRefObject
createDisabled: boolean
+ /** The agent pick (surface, draft) as the composer keeps it, for the setup page's snapshot. */
+ quickAgentPick: Omit, 'note'>
/** The composer's onCreated calls this: only a start that happened is remembered. */
recordCreatedRef: MutableRefObject<(() => void) | null>
onClose?: () => void
@@ -39,31 +47,39 @@ export function useComposerCloudStart({
// The latest handleCreate, never a ref copied after render: a submit right after an edit must
// see that edit.
const runCreate = useCallback(() => void handleCreate(), [handleCreate])
+ const runCreateWithoutAgent = useCallback(() => {
+ noAgentRef.current = true
+ void handleCreate()
+ }, [handleCreate, noAgentRef])
+ const composerSnapshot = useCallback(
+ () => buildComposerSnapshot(modalData, cardProps, { ...quickAgentPick, agent: quickAgent }),
+ [cardProps, modalData, quickAgent, quickAgentPick]
+ )
const cloudStart = useCloudStartComposer({
active,
availability: cardProps.orcaCloudAvailability ?? 'unavailable',
selectedRecipeId: cardProps.selectedEphemeralVmRecipeId ?? null,
setRecipeId: cardProps.onEphemeralVmRecipeChange,
- hostOptions: cardProps.projectHostSetupOptions ?? [],
- onHostChange: cardProps.onProjectHostSetupChange,
repoId: cardProps.repoId,
prompt: cardProps.name,
agent: quickAgent,
initialTargetFixed: Boolean(
modalData.initialEphemeralVmRecipeId || modalData.initialWorkspaceTarget || modalData.recovery
),
+ autoStart: modalData.cloudAutoStart === true,
+ createReady: !createDisabled,
+ composerSnapshot,
onCreate: runCreate,
+ onCreateWithoutAgent: runCreateWithoutAgent,
onClose
})
useEffect(() => {
recordCreatedRef.current = cloudStart.recordCreated
}, [cloudStart.recordCreated, recordCreatedRef])
- const primaryCreateDisabled = cloudStart.primaryIsCreate
- ? createDisabled || cloudStart.primaryDisabled
- : cloudStart.primaryDisabled
return {
cloudStart,
- primaryCreateDisabled,
+ // Local and Orca Cloud share one button: same label, same rules for when it's enabled.
+ primaryCreateDisabled: createDisabled,
onPrimary: cloudStart.onPrimary ?? runCreate
}
}
diff --git a/src/renderer/src/components/new-workspace/run-on/use-setup-auto-start.ts b/src/renderer/src/components/new-workspace/run-on/use-setup-auto-start.ts
deleted file mode 100644
index bd3793ddef4..00000000000
--- a/src/renderer/src/components/new-workspace/run-on/use-setup-auto-start.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-// O4: setup started from this composer (or its restored draft) starts the workspace by itself.
-// When every required row is done, hold "All set", then submit as if ⌘↵ had been pressed.
-import { useEffect, useState } from 'react'
-import { CLOUD_SETUP_ALL_SET_HOLD_MS } from '@/lib/orca-cloud/cloud-setup-session-seam'
-import { recordCloudSetupFirstSandbox } from '@/lib/orca-cloud/cloud-setup-telemetry'
-import { prefersReducedMotion } from './cloud-start-view'
-
-/** Returns whether "All set" is showing. `onStart` ends the trip state, then creates. */
-export function useSetupAutoStart({
- armed,
- done,
- setupSessionId,
- onStart
-}: {
- /** Setup was started here and Orca Cloud is still the target. */
- armed: boolean
- /** Every required row is done, or the session says it finished. */
- done: boolean
- setupSessionId: string | null
- onStart: () => void
-}): boolean {
- const [allSet, setAllSet] = useState(false)
- useEffect(() => {
- if (!armed || !done) {
- return
- }
- setAllSet(true)
- const timer = window.setTimeout(
- () => {
- setAllSet(false)
- // N10: time to first sandbox, from when the tracker first saw this trip.
- if (setupSessionId) {
- recordCloudSetupFirstSandbox({ sessionId: setupSessionId, hoursSource: 'unknown' })
- }
- onStart()
- },
- prefersReducedMotion() ? 0 : CLOUD_SETUP_ALL_SET_HOLD_MS
- )
- return () => window.clearTimeout(timer)
- }, [armed, done, onStart, setupSessionId])
- return allSet
-}
diff --git a/src/renderer/src/components/new-workspace/run-target-options.ts b/src/renderer/src/components/new-workspace/run-target-options.ts
index 9ed7fe869a5..c484b9d4b5c 100644
--- a/src/renderer/src/components/new-workspace/run-target-options.ts
+++ b/src/renderer/src/components/new-workspace/run-target-options.ts
@@ -308,3 +308,16 @@ export function buildRunTargetRows(
]
return { rows, matchedRecipes, hiddenHostCount: hostRows.hiddenHostCount }
}
+
+/** Add-host actions open dialogs, so the picker closes before they run. */
+export function closingFirst(
+ close: () => void,
+ action: (() => void) | undefined
+): (() => void) | undefined {
+ return action
+ ? () => {
+ close()
+ action()
+ }
+ : undefined
+}
diff --git a/src/renderer/src/components/new-workspace/use-composer-quick-agent.ts b/src/renderer/src/components/new-workspace/use-composer-quick-agent.ts
index 2178b08b831..e8d235303eb 100644
--- a/src/renderer/src/components/new-workspace/use-composer-quick-agent.ts
+++ b/src/renderer/src/components/new-workspace/use-composer-quick-agent.ts
@@ -1,7 +1,5 @@
import { useMemo } from 'react'
import { useAppStore } from '@/store/index'
-import { useTranslate } from '@/i18n/use-translate'
-import { formatAgentTypeLabel } from '../../../../shared/agent-status/presentation/agent-type-label'
import { agentChoicePending } from '@/lib/host-chat-program'
import { useAgentsOfferedForLaunch } from '@/lib/host-chat-program-acquisition'
import {
@@ -11,7 +9,6 @@ import {
} from '@/lib/quick-workspace-agent-selection'
import {
ORCA_CLOUD_AGENT_IDS,
- orcaCloudAgentFallback,
orcaCloudAgentMenuModel,
pickOrcaCloudAgent
} from '@/lib/orca-cloud/orca-cloud-agent-picker'
@@ -38,7 +35,6 @@ export function useComposerQuickAgent(
quickAgentOverride: TuiAgent | null | undefined,
setQuickAgentOverride: (agent: TuiAgent | null | undefined) => void
) {
- const t = useTranslate()
// The picker rows' workspace when known, so the offered list and the rows read one host.
const launchWorkspace =
cardProps.agentRows?.target.workspace ?? composerLaunchWorkspace(cardProps)
@@ -83,23 +79,8 @@ export function useComposerQuickAgent(
const chipAgentIds =
offeredAgentIds &&
new Set([...offeredAgentIds, ...(quickAgent && choicePending(quickAgent) ? [quickAgent] : [])])
- // Said, not silent: the agent the user wanted isn't on the image, so another one is selected.
- const orcaCloudFallback = orcaCloud
- ? orcaCloudAgentFallback({
- override: quickAgentOverride,
- defaultAgent: settings?.defaultTuiAgent,
- selected: quickAgent
- })
- : null
- const agentNotice = orcaCloudFallback
- ? t(
- 'components.newWorkspace.orcaCloudAgentFallback',
- '{{agent}} isn’t available on Orca Cloud yet, so {{fallback}} is selected.',
- {
- agent: formatAgentTypeLabel(orcaCloudFallback.wanted),
- fallback: formatAgentTypeLabel(orcaCloudFallback.selected)
- }
- )
- : undefined
+ // Orca Cloud adds nothing to the dialog (Neil): an agent it can't run falls back silently; the
+ // picker shows what will start.
+ const agentNotice: string | undefined = undefined
return { orcaCloud, orcaCloudModel, quickAgent, chipAgentIds, agentNotice }
}
diff --git a/src/renderer/src/components/new-workspace/use-run-target-orca-cloud.ts b/src/renderer/src/components/new-workspace/use-run-target-orca-cloud.ts
index 0de3f1d8ec9..291b1352c47 100644
--- a/src/renderer/src/components/new-workspace/use-run-target-orca-cloud.ts
+++ b/src/renderer/src/components/new-workspace/use-run-target-orca-cloud.ts
@@ -51,7 +51,8 @@ export function useRunTargetOrcaCloud({
override,
open,
close,
- selectRecipe
+ selectRecipe,
+ onSelectOrcaCloud
}: {
recipes: readonly EphemeralVmRecipeOption[]
offer: OrcaCloudRunTargetAccess | null
@@ -60,6 +61,8 @@ export function useRunTargetOrcaCloud({
open: boolean
close: () => void
selectRecipe: (recipeId: string) => void
+ /** Picks Orca Cloud whatever setup it still needs (the composer walks it after Start). */
+ onSelectOrcaCloud?: () => void
}): {
access: OrcaCloudRunTargetAccess
/** A pick is waiting on billing; the row shows busy. */
@@ -97,6 +100,11 @@ export function useRunTargetOrcaCloud({
const pickFor = useCallback(
(resolved: OrcaCloudRunTargetAccess, recipe: EphemeralVmRecipeOption | null): void => {
const pick = getOrcaCloudPick(resolved)
+ if (onSelectOrcaCloud && pick.kind !== 'blocked') {
+ close()
+ onSelectOrcaCloud()
+ return
+ }
if (pick.kind === 'act') {
close()
// The fix finishes elsewhere (plans, portal, settings): reread billing on return.
@@ -106,7 +114,7 @@ export function useRunTargetOrcaCloud({
selectRecipe(recipe.id)
}
},
- [close, selectRecipe]
+ [close, onSelectOrcaCloud, selectRecipe]
)
const runBlockedAction = useCallback((): void => pickFor(access, null), [access, pickFor])
const select = useCallback(
diff --git a/src/renderer/src/components/orca-cloud-setup/CloudSetupPage.tsx b/src/renderer/src/components/orca-cloud-setup/CloudSetupPage.tsx
new file mode 100644
index 00000000000..d9dabad6168
--- /dev/null
+++ b/src/renderer/src/components/orca-cloud-setup/CloudSetupPage.tsx
@@ -0,0 +1,296 @@
+// "Set up Orca Cloud" as a page (composer-redo). Start in the composer closes the dialog and opens
+// this when the account isn't ready. One step at a time: the current step is the page; finished
+// steps are ticks in the stepper. Browser hops only for sign-in, Stripe and GitHub. When every
+// step is done, the saved prompt starts by itself.
+import React from 'react'
+import { ArrowLeft, Check, Cloud, Copy, ExternalLink, LoaderCircle } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+import { Skeleton } from '@/components/ui/skeleton'
+import { cn } from '@/lib/utils'
+import { useTranslate } from '@/i18n/use-translate'
+import type { CloudSetupStep } from '@/components/new-workspace/run-on/cloud-start-model'
+import { CloudSetupErrorNotice } from '@/components/new-workspace/cloud-setup/CloudSetupErrorNotice'
+import { currentStepCopy, stepLabel } from './cloud-setup-page-copy'
+import type { CloudSetupFlow } from './use-cloud-setup-flow'
+import { openOrcaCloudAgentAccounts } from '@/components/new-workspace/OrcaCloudAgentSetupRow'
+
+export type CloudSetupPageProps = {
+ flow: CloudSetupFlow
+ projectName: string | null
+ prompt: string
+ agentLabel: string | null
+ /** Back to the composer with the prompt, project and agent as they were. */
+ onBack: () => void
+ /** "All set": shown for a moment before the workspace starts. */
+ starting: boolean
+}
+
+type StepState = 'done' | 'current' | 'todo'
+
+function SetupStepper({
+ steps,
+ current,
+ allDone
+}: {
+ steps: readonly CloudSetupStep[]
+ current: CloudSetupStep | null
+ allDone: boolean
+}): React.JSX.Element {
+ const t = useTranslate()
+ const currentIndex = current ? steps.findIndex((step) => step.id === current.id) : steps.length
+ return (
+
+ {steps.map((step, index) => {
+ const state: StepState =
+ allDone || index < currentIndex ? 'done' : index === currentIndex ? 'current' : 'todo'
+ const label = stepLabel(step, t)
+ return (
+
+ {index > 0 ? : null}
+
+ {state === 'done' ? : null}
+ {state === 'current' ? (
+
+ ) : null}
+
+
+ {label}
+
+ {state === 'done'
+ ? `, ${t('cloudSetupPage.stepDone', 'done')}`
+ : state === 'current'
+ ? `, ${t('cloudSetupPage.stepCurrent', 'current step')}`
+ : ''}
+
+
+
+ )
+ })}
+
+ )
+}
+
+function BrowserWaiting({ flow }: { flow: CloudSetupFlow }): React.JSX.Element {
+ const t = useTranslate()
+ return (
+
+
+
+ {t('cloudSetupPage.waitingBrowser', 'Waiting for your browser…')}
+
+
+ {flow.copyLink ? (
+
+
+ {t('cloudSetupPage.copyLink', 'Copy link')}
+
+ ) : null}
+
+ {t('cloudSetupPage.reopenBrowser', 'Reopen browser')}
+
+
+
+
+ )
+}
+
+function StepActions({ flow }: { flow: CloudSetupFlow }): React.JSX.Element | null {
+ const t = useTranslate()
+ const browser = (label: string, onClick: () => void): React.JSX.Element => (
+
+ {flow.busy ? : null}
+ {label}
+
+
+ )
+ const inBrowserAction =
+ flow.action === 'sign-in' ||
+ flow.action === 'choose-plan' ||
+ flow.action === 'fix-billing' ||
+ flow.action === 'github-install'
+ if (flow.inBrowser && inBrowserAction && !flow.error) {
+ return
+ }
+ const continueLabel = t('cloudSetupPage.continueInBrowser', 'Continue in browser')
+ switch (flow.action) {
+ case 'sign-in':
+ case 'choose-plan':
+ return browser(continueLabel, flow.continueInBrowser)
+ case 'fix-billing':
+ // A failed payment is fixed in the billing portal (browser); hours and the cap in Settings.
+ return flow.current?.need === 'payment' ? (
+ browser(continueLabel, flow.fixBilling)
+ ) : (
+
+ {t('cloudSetupPage.billing.openSettings', 'Open billing settings')}
+
+ )
+ case 'github-install':
+ return browser(continueLabel, flow.installGitHub)
+ case 'start-trial':
+ return (
+
+ {flow.busy ? : null}
+ {t('cloudSetupPage.trial.start', 'Start free trial')}
+
+ )
+ case 'switch-team':
+ return {flow.teamAction}
+ case 'agent':
+ return (
+
+
+ {flow.current?.action?.label ?? t('cloudSetupPage.agent.setUp', 'Set up once')}
+
+
+ {t('cloudSetupPage.agent.skip', 'Skip for now')}
+
+
+ )
+ case 'needs-repository':
+ return flow.current?.action ? (
+
+ {flow.current.action.label}
+
+
+ ) : null
+ case 'ask-owner':
+ return (
+
+ {t('cloudSetupPage.checkAgain', 'Check again')}
+
+ )
+ case 'github-waiting':
+ return (
+
+
+ {t('cloudSetupPage.github.waiting', 'This page starts the workspace once they approve.')}
+
+ )
+ case 'checking':
+ return
+ case 'done':
+ return (
+
+
+ {t('cloudSetupPage.starting', 'Starting…')}
+
+ )
+ }
+}
+
+export function CloudSetupPage({
+ flow,
+ projectName,
+ prompt,
+ agentLabel,
+ onBack,
+ starting
+}: CloudSetupPageProps): React.JSX.Element {
+ const t = useTranslate()
+ const titleId = React.useId()
+ const headingRef = React.useRef(null)
+ // Every row reads done but hasn't held yet: still "Checking" until the layer says it's starting.
+ const shown: CloudSetupFlow = starting
+ ? { ...flow, action: 'done' }
+ : flow.action === 'done'
+ ? { ...flow, action: 'checking' }
+ : flow
+ const copy = currentStepCopy(shown, t, agentLabel)
+ // Each new step moves focus to its heading so screen readers hear the change once.
+ const stepKey = `${shown.current?.id ?? 'done'}:${shown.action}`
+ React.useEffect(() => {
+ headingRef.current?.focus({ preventScroll: true })
+ }, [stepKey])
+ return (
+
+
+
+
+
+ {t('cloudSetupPage.back', 'Back')}
+
+
+
+ {t('cloudSetupPage.eyebrow', 'Orca Cloud setup')}
+
+
+
+
+
+
+
+
+
+ {copy.title}
+
+ {shown.action === 'checking' ? (
+
+ ) : copy.lead ? (
+
{copy.lead}
+ ) : null}
+ {copy.note ?
{copy.note}
: null}
+
+
+ {flow.error && !starting ? (
+
+ action === 'reopen-browser' ? flow.reopenBrowser() : flow.continueInBrowser()
+ }
+ className="mt-6"
+ />
+ ) : null}
+
+
+
+
+
+
+
+
+ {t('cloudSetupPage.startsAfter', 'Starts when setup is done')}
+
+
+ {prompt.trim() || t('cloudSetupPage.untitled', 'New cloud workspace')}
+
+
+ {[projectName, agentLabel, t('cloudSetupPage.orcaCloud', 'Orca Cloud')]
+ .filter(Boolean)
+ .join(' · ')}
+
+
+
+
+
+ )
+}
diff --git a/src/renderer/src/components/orca-cloud-setup/CloudSetupPageLayer.tsx b/src/renderer/src/components/orca-cloud-setup/CloudSetupPageLayer.tsx
new file mode 100644
index 00000000000..13f653faa59
--- /dev/null
+++ b/src/renderer/src/components/orca-cloud-setup/CloudSetupPageLayer.tsx
@@ -0,0 +1,122 @@
+// Mounts the Orca Cloud setup page over the workspace area while a Start is waiting on setup. Other
+// pages (Settings for billing or agent accounts) show over it; coming back shows it again. Opening
+// another workspace from the sidebar puts setup away (Back in the composer still has the prompt).
+import React, { useCallback, useEffect, useRef, useState } from 'react'
+import { useAppStore } from '@/store'
+import { useTranslate } from '@/i18n/use-translate'
+import {
+ closeCloudSetupPage,
+ useCloudSetupPageRequest,
+ type CloudSetupPageRequest
+} from '@/lib/orca-cloud/cloud-setup-page-store'
+import { CLOUD_SETUP_ALL_SET_HOLD_MS } from '@/lib/orca-cloud/cloud-setup-session-seam'
+import { isOrcaCloudAccountAgent } from '../../../../shared/orca-cloud-agents/orca-cloud-agents-wire'
+import { ORCA_CLOUD_RECIPE_ID } from '../../../../shared/hosted-sandbox/orca-cloud-recipe-id'
+import { agentName } from '@/components/settings/agent-accounts/orca-cloud-accounts-copy'
+import { prefersReducedMotion } from '@/components/new-workspace/run-on/cloud-start-view'
+import { CloudSetupPage } from './CloudSetupPage'
+import { useCloudSetupFlow } from './use-cloud-setup-flow'
+
+/** How long every row must read done before the page starts the workspace. */
+const SETTLE_MS = 1_500
+
+function reopenComposer(request: CloudSetupPageRequest, autoStart: boolean): void {
+ closeCloudSetupPage()
+ useAppStore.getState().openModal('new-workspace-composer', {
+ ...request.composer,
+ initialEphemeralVmRecipeId: ORCA_CLOUD_RECIPE_ID,
+ // A GitHub-only repository can't be preselected; the composer opens for one more Start.
+ ...(autoStart && !request.cloudOnlyRepo ? { cloudAutoStart: true } : {})
+ })
+}
+
+function ActiveCloudSetupPage({ request }: { request: CloudSetupPageRequest }): React.JSX.Element {
+ const t = useTranslate()
+ const flow = useCloudSetupFlow(request, request.debugAccess)
+ const projectName = useAppStore(
+ (s) => s.repos.find((repo) => repo.id === request.repoId)?.displayName ?? null
+ )
+ const agentLabel = isOrcaCloudAccountAgent(request.agent) ? agentName(request.agent, t) : null
+ const [starting, setStarting] = useState(false)
+ // Rows can all read done for a moment right after a sign-in, before billing, GitHub and agent
+ // accounts answer for the new account. Done must hold for SETTLE_MS before "All set".
+ const done = flow.action === 'done'
+ useEffect(() => {
+ if (!done) {
+ return
+ }
+ const timer = window.setTimeout(() => setStarting(true), SETTLE_MS)
+ return () => window.clearTimeout(timer)
+ }, [done])
+ useEffect(() => {
+ if (!starting) {
+ return
+ }
+ const timer = window.setTimeout(
+ () => reopenComposer(request, true),
+ prefersReducedMotion() ? 0 : CLOUD_SETUP_ALL_SET_HOLD_MS
+ )
+ return () => window.clearTimeout(timer)
+ }, [request, starting])
+ const onBack = useCallback(() => reopenComposer(request, false), [request])
+ return (
+
+ )
+}
+
+export function CloudSetupPageLayer(): React.JSX.Element | null {
+ const request = useCloudSetupPageRequest()
+ const onWorkspaceView = useAppStore((s) => s.activeView === 'terminal')
+ const activeWorktreeId = useAppStore((s) => s.activeWorktreeId ?? null)
+ const openedOnRef = useRef(undefined)
+ useEffect(() => {
+ if (!request) {
+ openedOnRef.current = undefined
+ return
+ }
+ if (openedOnRef.current === undefined) {
+ openedOnRef.current = activeWorktreeId
+ return
+ }
+ if (openedOnRef.current !== activeWorktreeId) {
+ closeCloudSetupPage()
+ }
+ }, [activeWorktreeId, request])
+ // The right sidebar (files, source control) is noise during setup: hide it while the page shows,
+ // and put it back as it was when the page closes or another page takes over.
+ const showing = request !== null && onWorkspaceView
+ useEffect(() => {
+ if (!showing) {
+ return
+ }
+ const store = useAppStore.getState()
+ const wasOpen = store.rightSidebarOpen
+ if (wasOpen) {
+ store.setRightSidebarOpen(false)
+ }
+ return () => {
+ if (wasOpen && !useAppStore.getState().rightSidebarOpen) {
+ useAppStore.getState().setRightSidebarOpen(true)
+ }
+ }
+ }, [showing])
+ if (!request) {
+ return null
+ }
+ // Stays mounted under other pages so an open browser trip keeps its session and waiting state.
+ return (
+
+ )
+}
diff --git a/src/renderer/src/components/orca-cloud-setup/cloud-setup-page-copy.ts b/src/renderer/src/components/orca-cloud-setup/cloud-setup-page-copy.ts
new file mode 100644
index 00000000000..edd0e67a473
--- /dev/null
+++ b/src/renderer/src/components/orca-cloud-setup/cloud-setup-page-copy.ts
@@ -0,0 +1,170 @@
+// Words for the Orca Cloud setup page: each step's stepper label, and the current step's title,
+// lead and note. Copy follows the product glossary (Orca Cloud, sandbox hours, Team plan).
+import type { Translate } from '@/i18n/use-translate'
+import type { CloudSetupStep } from '@/components/new-workspace/run-on/cloud-start-model'
+import { sessionDetailCopy } from '@/components/new-workspace/run-on/cloud-setup-session-copy'
+import type { CloudSetupPageModel } from './cloud-setup-page-model'
+
+export function stepLabel(step: CloudSetupStep, t: Translate): string {
+ switch (step.id) {
+ case 'account':
+ return t('cloudSetupPage.step.account', 'Account')
+ case 'team':
+ return t('cloudSetupPage.step.team', 'Team')
+ case 'plan':
+ return step.need === 'trial'
+ ? t('cloudSetupPage.step.trial', 'Trial')
+ : t('cloudSetupPage.step.plan', 'Plan')
+ case 'github':
+ return t('cloudSetupPage.step.github', 'GitHub')
+ case 'agent':
+ return t('cloudSetupPage.step.agent', 'Agent')
+ }
+}
+
+type StepCopy = { title: string; lead: string | null; note?: string | null }
+
+function trialHours(step: CloudSetupStep): string | null {
+ return step.trialSeconds ? String(Math.round((step.trialSeconds / 3600) * 10) / 10) : null
+}
+
+export function currentStepCopy(
+ flow: Pick,
+ t: Translate,
+ agentLabel: string | null
+): StepCopy {
+ const step = flow.current
+ const detail = step
+ ? ((sessionDetailCopy(step, t) === undefined ? step.detail : sessionDetailCopy(step, t)) ??
+ null)
+ : null
+ switch (flow.action) {
+ case 'sign-in':
+ return {
+ title: t('cloudSetupPage.signIn.title', 'Sign in to Orca'),
+ lead: t(
+ 'cloudSetupPage.signIn.lead',
+ 'Orca Cloud runs your agents in a cloud workspace, so they keep going with your laptop closed. Sign in or create an account in your browser. We’ll make a team for you.'
+ ),
+ note: t(
+ 'cloudSetupPage.signIn.note',
+ 'New teams get a trial with no card needed. This page picks up each step on its own.'
+ )
+ }
+ case 'start-trial': {
+ const hours = step ? trialHours(step) : null
+ return {
+ title: t('cloudSetupPage.trial.title', 'Start your free trial'),
+ lead:
+ hours === null
+ ? t('cloudSetupPage.trial.leadNoHours', 'Try Orca Cloud on your team. No card needed.')
+ : t(
+ 'cloudSetupPage.trial.lead',
+ '{{hours}} sandbox hours to try Orca Cloud. No card needed.',
+ {
+ hours,
+ count: Number(hours),
+ defaultValue_one: '{{hours}} sandbox hour to try Orca Cloud. No card needed.'
+ }
+ ),
+ note: t('cloudSetupPage.trial.note', 'Nothing is charged until you choose a plan.')
+ }
+ }
+ case 'choose-plan':
+ return step?.need === 'trial-used-up'
+ ? {
+ title: t('cloudSetupPage.plan.trialUsedUpTitle', 'Your trial is used up'),
+ lead: t(
+ 'cloudSetupPage.plan.trialUsedUpLead',
+ 'Choose a Team plan to keep using Orca Cloud. Checkout opens in your browser.'
+ )
+ }
+ : step?.need === 'renew-plan'
+ ? {
+ title: t('cloudSetupPage.plan.renewTitle', 'Renew your Team plan'),
+ lead: t(
+ 'cloudSetupPage.plan.renewLead',
+ 'Your Team plan ended. Renew it in your browser to start this workspace.'
+ )
+ }
+ : {
+ title: t('cloudSetupPage.plan.title', 'Choose a Team plan'),
+ lead: t(
+ 'cloudSetupPage.plan.lead',
+ 'Orca Cloud needs a Team plan. Checkout opens in your browser.'
+ )
+ }
+ case 'fix-billing':
+ return {
+ title:
+ step?.need === 'payment'
+ ? t('cloudSetupPage.billing.paymentTitle', 'Update your payment method')
+ : step?.need === 'spend-cap'
+ ? t('cloudSetupPage.billing.capTitle', 'Spend cap reached')
+ : t('cloudSetupPage.billing.hoursTitle', 'Out of sandbox hours'),
+ lead:
+ step?.need === 'payment'
+ ? t(
+ 'cloudSetupPage.billing.paymentLead',
+ 'The last payment failed. Update the card to keep going.'
+ )
+ : step?.need === 'spend-cap'
+ ? t('cloudSetupPage.billing.capLead', 'Raise the spend cap to start this workspace.')
+ : t('cloudSetupPage.billing.hoursLead', 'Turn on overage to start this workspace.')
+ }
+ case 'switch-team':
+ return { title: t('cloudSetupPage.team.title', 'Switch to your team'), lead: detail }
+ case 'github-install':
+ return {
+ title: t('cloudSetupPage.github.title', 'Give Orca Cloud access on GitHub'),
+ // The row names the repository ("Orca Cloud can't reach acme/web yet").
+ lead: detail,
+ note: t(
+ 'cloudSetupPage.github.lead',
+ 'Orca Cloud clones your code from GitHub. Install the Orca Cloud app on this repository once.'
+ )
+ }
+ case 'github-waiting':
+ return {
+ title: t('cloudSetupPage.github.waitingTitle', 'Waiting for approval on GitHub'),
+ lead: detail,
+ note: step?.note ?? null
+ }
+ case 'needs-repository':
+ return {
+ title: t('cloudSetupPage.repository.title', 'Orca Cloud needs a GitHub repository'),
+ lead: detail,
+ note: step?.note ?? null
+ }
+ case 'ask-owner':
+ return {
+ title: t('cloudSetupPage.owner.title', 'An owner or admin needs to fix this'),
+ lead:
+ detail ??
+ t(
+ 'cloudSetupPage.owner.lead',
+ 'Orca Cloud needs a Team plan. Only owners and admins can choose one.'
+ )
+ }
+ case 'agent':
+ return {
+ title: agentLabel
+ ? t('cloudSetupPage.agent.title', 'Sign in to {{agent}} once', { agent: agentLabel })
+ : t('cloudSetupPage.agent.titleGeneric', 'Sign in to your agent once'),
+ lead: t(
+ 'cloudSetupPage.agent.lead',
+ 'Save your agent sign-in to your Orca account and every cloud workspace uses it. You can also skip this and sign in inside the workspace.'
+ )
+ }
+ case 'checking':
+ return {
+ title: t('cloudSetupPage.checking.title', 'Checking your Orca Cloud account…'),
+ lead: null
+ }
+ case 'done':
+ return {
+ title: t('cloudSetupPage.done.title', 'All set'),
+ lead: t('cloudSetupPage.done.lead', 'Starting your cloud workspace…')
+ }
+ }
+}
diff --git a/src/renderer/src/components/orca-cloud-setup/cloud-setup-page-model.test.ts b/src/renderer/src/components/orca-cloud-setup/cloud-setup-page-model.test.ts
new file mode 100644
index 00000000000..1fbeb8eca20
--- /dev/null
+++ b/src/renderer/src/components/orca-cloud-setup/cloud-setup-page-model.test.ts
@@ -0,0 +1,54 @@
+import { describe, expect, it } from 'vitest'
+import type { CloudSetupStep } from '@/components/new-workspace/run-on/cloud-start-model'
+import { actionForStep, cloudSetupPageModel, sessionNeedsFrom } from './cloud-setup-page-model'
+
+describe('cloudSetupPageModel', () => {
+ it('walks one step at a time: the first unfinished step is current', () => {
+ const steps: CloudSetupStep[] = [
+ { id: 'account', state: 'done' },
+ { id: 'plan', state: 'needed', need: 'trial', canFix: true, trialSeconds: 7200 },
+ { id: 'github', state: 'needed', need: 'github', canFix: true },
+ { id: 'agent', state: 'optional' }
+ ]
+ const model = cloudSetupPageModel(steps)
+ expect(model.current?.id).toBe('plan')
+ expect(model.action).toBe('start-trial')
+ expect(sessionNeedsFrom(steps)).toEqual(['plan', 'github'])
+ })
+
+ it('a skipped agent finishes setup', () => {
+ const steps: CloudSetupStep[] = [
+ { id: 'account', state: 'done' },
+ { id: 'plan', state: 'done' },
+ { id: 'agent', state: 'optional' }
+ ]
+ expect(cloudSetupPageModel(steps).action).toBe('agent')
+ expect(cloudSetupPageModel(steps, { agentSkipped: true })).toMatchObject({
+ current: null,
+ action: 'done'
+ })
+ })
+
+ it('maps each blocker to its one action', () => {
+ expect(actionForStep({ id: 'plan', state: 'pending', need: 'trial' })).toBe('sign-in')
+ expect(actionForStep({ id: 'plan', state: 'needed', need: 'payment', canFix: true })).toBe(
+ 'fix-billing'
+ )
+ expect(actionForStep({ id: 'plan', state: 'needed', need: 'plan', canFix: false })).toBe(
+ 'ask-owner'
+ )
+ expect(actionForStep({ id: 'team', state: 'needed' }, { teamSwitch: true })).toBe('switch-team')
+ expect(actionForStep({ id: 'github', state: 'loading' })).toBe('checking')
+ })
+})
+
+describe('page settles before it starts', () => {
+ it('a still-loading agent row reads Checking, not All set', () => {
+ const steps: CloudSetupStep[] = [
+ { id: 'account', state: 'done' },
+ { id: 'plan', state: 'done' },
+ { id: 'agent', state: 'loading' }
+ ]
+ expect(cloudSetupPageModel(steps).action).toBe('checking')
+ })
+})
diff --git a/src/renderer/src/components/orca-cloud-setup/cloud-setup-page-model.ts b/src/renderer/src/components/orca-cloud-setup/cloud-setup-page-model.ts
new file mode 100644
index 00000000000..ca7c8884e5f
--- /dev/null
+++ b/src/renderer/src/components/orca-cloud-setup/cloud-setup-page-model.ts
@@ -0,0 +1,143 @@
+// The full-page Orca Cloud setup (composer-redo): which step is current, what its one action is,
+// and when the saved prompt can start. Pure, so every state is unit-tested here.
+import type { CloudSetupStep } from '@/components/new-workspace/run-on/cloud-start-model'
+
+/** What the current step's primary button does. */
+export type CloudSetupPageAction =
+ /** Sign in (and whatever the browser trip covers next): one browser hop. */
+ | 'sign-in'
+ /** The team's trial, claimed in the app: no browser, no card. */
+ | 'start-trial'
+ /** Stripe Checkout for a plan: one browser hop. */
+ | 'choose-plan'
+ /** Hours, spend cap or payment, fixed in billing settings or the portal. */
+ | 'fix-billing'
+ /** Personal account while a team has the plan: switch in place (B2). */
+ | 'switch-team'
+ /** Install the GitHub App on this repository: one browser hop. */
+ | 'github-install'
+ /** An owner must approve on GitHub; the page waits and starts on its own. */
+ | 'github-waiting'
+ /** The project isn't on GitHub; nothing here fixes it. */
+ | 'needs-repository'
+ /** A member can't fix billing; an owner or admin has to. */
+ | 'ask-owner'
+ /** Optional agent account: set it up once, or skip. */
+ | 'agent'
+ /** Still reading account, plan or repository state. */
+ | 'checking'
+ /** Every step is done: the prompt starts. */
+ | 'done'
+
+export type CloudSetupPageModel = {
+ steps: readonly CloudSetupStep[]
+ current: CloudSetupStep | null
+ action: CloudSetupPageAction
+}
+
+function isFinished(step: CloudSetupStep, agentSkipped: boolean): boolean {
+ if (step.state === 'done') {
+ return true
+ }
+ if (step.id === 'agent') {
+ // Still loading reads "Checking": the page only says All set once every row has answered.
+ return agentSkipped && step.state !== 'loading'
+ }
+ return false
+}
+
+export function actionForStep(
+ step: CloudSetupStep,
+ { teamSwitch = false }: { teamSwitch?: boolean } = {}
+): CloudSetupPageAction {
+ if (step.state === 'loading') {
+ return 'checking'
+ }
+ switch (step.id) {
+ case 'account':
+ return 'sign-in'
+ case 'team':
+ return teamSwitch ? 'switch-team' : 'sign-in'
+ case 'plan':
+ if (step.state === 'pending') {
+ return 'sign-in'
+ }
+ if (step.canFix === false) {
+ return 'ask-owner'
+ }
+ switch (step.need) {
+ case 'trial':
+ return 'start-trial'
+ case 'hours':
+ case 'spend-cap':
+ case 'payment':
+ return 'fix-billing'
+ case 'plan':
+ case 'renew-plan':
+ case 'trial-used-up':
+ case 'sign-in':
+ case 'github':
+ case undefined:
+ return 'choose-plan'
+ }
+ return 'choose-plan'
+ case 'github':
+ if (step.state === 'failed' && step.blocksStart) {
+ return 'needs-repository'
+ }
+ if (step.state === 'pending' && step.blocksStart) {
+ return 'github-waiting'
+ }
+ return 'github-install'
+ case 'agent':
+ return 'agent'
+ }
+}
+
+/**
+ * The page's state from the setup rows. Steps come in walk order (account, team, plan, GitHub,
+ * agent); the first unfinished one is current. A signed-out plan row is "pending" until sign-in, so
+ * account stays current until then.
+ */
+export function cloudSetupPageModel(
+ steps: readonly CloudSetupStep[],
+ {
+ agentSkipped = false,
+ teamSwitch = false
+ }: { agentSkipped?: boolean; teamSwitch?: boolean } = {}
+): CloudSetupPageModel {
+ const current = steps.find((step) => !isFinished(step, agentSkipped)) ?? null
+ if (!current) {
+ return { steps, current: null, action: 'done' }
+ }
+ return { steps, current, action: actionForStep(current, { teamSwitch }) }
+}
+
+/** Needs that a browser trip (setup session) can cover, from the current step on. */
+export function sessionNeedsFrom(steps: readonly CloudSetupStep[]): CloudSetupStep['id'][] {
+ return steps
+ .filter((step) => step.state !== 'done' && step.state !== 'optional' && step.id !== 'agent')
+ .map((step) => step.id)
+}
+
+/**
+ * Pressing Start goes straight to create only when nothing is open. An agent without an account
+ * counts until the user skips it once; a still-loading agent row never holds a start.
+ */
+export function cloudSetupRequired(
+ steps: readonly CloudSetupStep[],
+ {
+ teamSwitch = false,
+ agentSkipped = false
+ }: { teamSwitch?: boolean; agentSkipped?: boolean } = {}
+): boolean {
+ if (teamSwitch) {
+ return true
+ }
+ return steps.some((step) => {
+ if (step.id === 'agent') {
+ return step.state === 'optional' && !agentSkipped
+ }
+ return step.state !== 'done'
+ })
+}
diff --git a/src/renderer/src/components/orca-cloud-setup/use-cloud-setup-flow.ts b/src/renderer/src/components/orca-cloud-setup/use-cloud-setup-flow.ts
new file mode 100644
index 00000000000..165dbd03b21
--- /dev/null
+++ b/src/renderer/src/components/orca-cloud-setup/use-cloud-setup-flow.ts
@@ -0,0 +1,246 @@
+// The setup page's state and actions (composer-redo). It reuses the composer's setup plumbing: the
+// rows from billing, the GitHub repository check and the agent accounts (useCloudStartSteps), the
+// one-trip setup session (#255) with its deep-link return (#257), B2's team switch, and the
+// tracker's error parsing (#260). Only the UI moved: from a card in the dialog to this page.
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { claimOrcaCloudTrial } from '@/components/billing/claim-trial'
+import {
+ invalidateOrcaCloudBilling,
+ prefetchOrcaCloudBilling,
+ useOrcaCloudRunTargetAccess
+} from '@/components/new-workspace/orca-cloud-billing-cache'
+import { setupActionFor } from '@/components/new-workspace/run-on/cloud-start-actions'
+import type { CloudSetupStep } from '@/components/new-workspace/run-on/cloud-start-model'
+import { useCloudStartSteps } from '@/components/new-workspace/run-on/use-cloud-start-steps'
+import { useSwitchTeamSteps } from '@/components/new-workspace/cloud-setup/use-cloud-setup-tracker'
+import { parseCloudSetupError } from '@/components/new-workspace/cloud-setup/cloud-setup-errors'
+import {
+ canCopyCloudSetupLink,
+ copyCloudSetupLink,
+ reopenCloudSetup
+} from '@/components/new-workspace/cloud-setup/cloud-setup-tracker-actions'
+import { getCloudSetupStarter } from '@/lib/orca-cloud/cloud-setup-session-seam'
+import {
+ isCloudSetupAgentSkipped,
+ rememberCloudSetupAgentSkipped,
+ type CloudSetupPageRequest
+} from '@/lib/orca-cloud/cloud-setup-page-store'
+import { reportCloudSetupProgress } from '@/lib/orca-cloud/cloud-setup-telemetry'
+import { installWindowVisibilityInterval } from '@/lib/window-visibility-interval'
+import type { OrcaCloudAvailability } from '../../../../shared/orca-yaml-hook-types'
+import type { OrcaCloudRunTargetAccess } from '@/components/new-workspace/orca-cloud-run-target-access'
+import {
+ cloudSetupPageModel,
+ sessionNeedsFrom,
+ type CloudSetupPageModel
+} from './cloud-setup-page-model'
+
+/** Billing rows re-read this often while the page waits on a browser step (focus also re-reads). */
+const BILLING_POLL_MS = 4_000
+
+/**
+ * Main's Orca Cloud availability for the project (needs-sign-in, needs-capability, offered), read
+ * the way the composer reads it and re-read while the page waits, so a sign-in in the browser (or
+ * the dev sign-in) moves the page on. Starts from the composer's answer at Start.
+ */
+function useLiveAvailability(
+ repoId: string,
+ initial: OrcaCloudAvailability,
+ enabled: boolean
+): OrcaCloudAvailability {
+ const [availability, setAvailability] = useState(initial)
+ useEffect(() => {
+ if (!enabled) {
+ return
+ }
+ let cancelled = false
+ const read = (): void => {
+ void Promise.resolve()
+ .then(() => window.api.ephemeralVm.listRecipes({ repoId }))
+ .then((result) => {
+ if (!cancelled && result.orcaCloud) {
+ setAvailability(result.orcaCloud)
+ }
+ })
+ .catch(() => undefined)
+ }
+ read()
+ // Paused while the window is hidden; returning from the browser re-reads at once.
+ const stop = installWindowVisibilityInterval({ run: read, intervalMs: BILLING_POLL_MS })
+ window.addEventListener('focus', read)
+ return () => {
+ cancelled = true
+ stop()
+ window.removeEventListener('focus', read)
+ }
+ }, [enabled, repoId])
+ return availability
+}
+
+export type CloudSetupFlow = CloudSetupPageModel & {
+ /** True from the click until the browser opens or the trial answers. */
+ busy: boolean
+ /** The browser trip is open: the current step reads "Waiting for your browser". */
+ inBrowser: boolean
+ error: ReturnType
+ canManageBilling: boolean
+ teamAction: React.ReactNode | null
+ continueInBrowser: () => void
+ startTrial: () => void
+ fixBilling: () => void
+ installGitHub: () => void
+ skipAgent: () => void
+ checkAgain: () => void
+ copyLink: (() => void) | null
+ reopenBrowser: () => void
+}
+
+export function useCloudSetupFlow(
+ request: CloudSetupPageRequest,
+ /** Fixtures pin the account; the app reads billing. */
+ accessOverride?: OrcaCloudRunTargetAccess
+): CloudSetupFlow {
+ const fetched = useOrcaCloudRunTargetAccess(!accessOverride)
+ const access = accessOverride ?? fetched
+ const availability = useLiveAvailability(request.repoId, request.availability, !accessOverride)
+ const [sessionId, setSessionId] = useState(null)
+ // The step that was current when the browser opened. A setup session covers every step it was
+ // opened for, so it waits on all of them; a single hop (GitHub install, billing) only on its own.
+ const [browserStep, setBrowserStep] = useState(null)
+ const [busy, setBusy] = useState(false)
+ const [agentSkipped, setAgentSkipped] = useState(() => isCloudSetupAgentSkipped(request.agent))
+ const githubOverride = useMemo(
+ () => (request.cloudOnlyRepo ? { id: 'github', state: 'done' } : null),
+ [request.cloudOnlyRepo]
+ )
+ const {
+ steps: rowSteps,
+ session,
+ openInstall
+ } = useCloudStartSteps({
+ availability,
+ access,
+ repoId: request.repoId,
+ agent: request.agent,
+ cloudSelected: true,
+ setupSessionId: sessionId,
+ githubOverride
+ })
+ const { steps, held: switchTeam } = useSwitchTeamSteps(rowSteps, !session)
+ const model = cloudSetupPageModel(steps, { agentSkipped, teamSwitch: switchTeam !== null })
+ const error = useMemo(() => {
+ const parsed = parseCloudSetupError(session?.error)
+ return parsed && session?.teamName ? { ...parsed, teamName: session.teamName } : parsed
+ }, [session?.error, session?.teamName])
+
+ // Rows derived from billing tick on their own: re-read while a step is waiting elsewhere.
+ const waiting = model.action !== 'done' && model.action !== 'agent'
+ useEffect(() => {
+ if (!waiting || accessOverride) {
+ return
+ }
+ return installWindowVisibilityInterval({
+ run: () => void prefetchOrcaCloudBilling(true),
+ intervalMs: BILLING_POLL_MS
+ })
+ }, [accessOverride, waiting])
+
+ useEffect(() => {
+ if (sessionId) {
+ reportCloudSetupProgress(sessionId, steps, error?.kind ?? null, { afterTrial: false })
+ }
+ }, [error?.kind, sessionId, steps])
+
+ const stepsRef = useRef(steps)
+ useEffect(() => {
+ stepsRef.current = steps
+ }, [steps])
+
+ const markInBrowser = useCallback(
+ () => setBrowserStep(cloudSetupPageModel(stepsRef.current).current?.id ?? null),
+ []
+ )
+
+ const fallback = useCallback(
+ () => setupActionFor(stepsRef.current, access, availability)?.(),
+ [access, availability]
+ )
+
+ // One browser trip for everything still open from here (sign-in, team, trial or Checkout, GitHub).
+ const continueInBrowser = useCallback(() => {
+ const needs = sessionNeedsFrom(stepsRef.current)
+ if (needs.length > 0 && needs.every((need) => need === 'github')) {
+ markInBrowser()
+ void openInstall()
+ return
+ }
+ const start = getCloudSetupStarter()
+ if (!start) {
+ markInBrowser()
+ fallback()
+ return
+ }
+ setBusy(true)
+ void start({ repoId: request.repoId, needs })
+ .then((opened) => {
+ markInBrowser()
+ if (opened) {
+ setSessionId(opened.sessionId)
+ } else {
+ fallback()
+ }
+ })
+ .catch(() => fallback())
+ .finally(() => setBusy(false))
+ }, [fallback, markInBrowser, openInstall, request.repoId])
+
+ const startTrial = useCallback(() => {
+ setBusy(true)
+ void claimOrcaCloudTrial()
+ .then((result) => {
+ if (result.status !== 'ok') {
+ // A personal account needs a team first, or the trial is gone: the browser covers it.
+ continueInBrowser()
+ }
+ })
+ .finally(() => setBusy(false))
+ }, [continueInBrowser])
+
+ const fixBilling = useCallback(() => {
+ markInBrowser()
+ invalidateOrcaCloudBilling({ refetchNow: false })
+ fallback()
+ }, [fallback, markInBrowser])
+
+ const installGitHub = useCallback(() => {
+ markInBrowser()
+ void openInstall()
+ }, [markInBrowser, openInstall])
+
+ const skipAgent = useCallback(() => {
+ rememberCloudSetupAgentSkipped(request.agent)
+ setAgentSkipped(true)
+ }, [request.agent])
+
+ const checkAgain = useCallback(() => void prefetchOrcaCloudBilling(true), [])
+
+ return {
+ ...model,
+ busy,
+ inBrowser: browserStep !== null && (sessionId !== null || model.current?.id === browserStep),
+ error,
+ canManageBilling: access.state === 'no-plan' ? access.canManageBilling : true,
+ teamAction: switchTeam?.action ?? null,
+ continueInBrowser,
+ startTrial,
+ fixBilling,
+ installGitHub,
+ skipAgent,
+ checkAgain,
+ copyLink:
+ sessionId && canCopyCloudSetupLink(sessionId)
+ ? () => void copyCloudSetupLink(sessionId)
+ : null,
+ reopenBrowser: () => void reopenCloudSetup(sessionId, continueInBrowser)
+ }
+}
diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json
index 57070f86d45..5a71a0907a4 100644
--- a/src/renderer/src/i18n/en-runtime-required.json
+++ b/src/renderer/src/i18n/en-runtime-required.json
@@ -415,8 +415,6 @@
"new": {
"workspace": {
"RunTargetCombobox": {
- "orcaCloudIdleSleep_one": "Sleeps after {{count}} minute idle",
- "orcaCloudIdleSleep_other": "Sleeps after {{count}} minutes idle",
"orcaCloudSandboxHoursLeft_one": "{{hours}} sandbox hour left",
"orcaCloudSandboxHoursLeft_other": "{{hours}} sandbox hours left"
}
@@ -1562,6 +1560,13 @@
"save_one": "Save secret",
"save_other": "Save {{count}} secrets"
},
+ "cloudSetupPage": {
+ "trial": {
+ "lead": "{{hours}} sandbox hours to try Orca Cloud. No card needed.",
+ "lead_one": "{{hours}} sandbox hour to try Orca Cloud. No card needed.",
+ "lead_other": "{{hours}} sandbox hours to try Orca Cloud. No card needed."
+ }
+ },
"codeHost": {
"bitbucketConnect": {
"accessTokenRequired": "Enter an access token.",
diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json
index cd109e0d6fe..13ddd1561e6 100644
--- a/src/renderer/src/i18n/locales/en.json
+++ b/src/renderer/src/i18n/locales/en.json
@@ -16715,20 +16715,12 @@
"currentSuffix": "(current)",
"orcaCloudSignInDetail": "Sign in to start a sandbox",
"orcaCloudChecking": "Checking your plan…",
- "orcaCloudNeedsTeamPlan": "Orca Cloud needs a Team plan.",
"orcaCloudOutOfHours": "Out of sandbox hours",
"orcaCloudRaiseSpendCap": "Raise spend cap",
- "orcaCloudPaymentEffect": "New sandboxes can't start.",
"orcaCloudUpdatePaymentMethod": "Update payment method",
"orcaCloudSandboxHoursLeft_one": "{{hours}} sandbox hour left",
"orcaCloudSandboxHoursLeft_other": "{{hours}} sandbox hours left",
"orcaCloudAskOwnerOrAdmin": "Ask an owner or admin",
- "orcaCloudAskPayment": "Ask an owner or admin to update the payment method.",
- "orcaCloudIdleSleep_one": "Sleeps after {{count}} minute idle",
- "orcaCloudIdleSleep_other": "Sleeps after {{count}} minutes idle",
- "orcaCloudAskAddHoursOnly": "Ask an owner or admin to add hours.",
- "orcaCloudAskChoosePlan": "Ask an owner or admin to choose one.",
- "orcaCloudAskRaiseSpendCap": "Ask an owner or admin to raise the spend cap.",
"hintConnect": "{{host}}, not connected. Press Enter to connect.",
"hintSetLocation": "{{host}}, project location not set. Press Enter to set it.",
"hintSignIn": "Orca Cloud, signed out. Press Enter to sign in.",
@@ -23099,68 +23091,15 @@
"bothSources": "orca.yaml + local",
"localSource": "local settings"
},
- "cloudStart": {
- "useThisComputer": "Use this computer instead",
- "start": "Start in Orca Cloud",
- "setUpAndStart": "Set up and start",
- "choosePlanAndStart": "Choose plan and start",
- "renewAndStart": "Renew and start",
- "giveAccessAndStart": "Give access and start",
- "checking": "Checking Orca Cloud…",
- "githubGranted": "GitHub access granted for {{repo}}"
- },
"cloudSetup": {
- "account": "Orca account",
- "team": "Team",
- "plan": "Team plan",
- "github": "GitHub access",
- "githubNeeded": "Orca Cloud clones your code from GitHub. Give access to this repository once.",
- "agent": "Agent",
- "planAfterSignIn": "Checked after you sign in",
- "askOwner": "Ask an owner or admin to fix it.",
- "planEnded": "Your Team plan ended.",
- "outOfHours": "Out of sandbox hours.",
- "spendCap": "Spend cap reached.",
- "paymentFailed": "Payment failed.",
- "planMember": "Orca Cloud needs a Team plan. Only owners and admins can choose one.",
- "planNeeded": "Orca Cloud needs a Team plan.",
- "stateDone": "Done",
- "stateOptional": "Optional",
- "stateNext": "Next",
- "stateInBrowser": "In browser",
- "stateFailed": "Didn't finish",
- "stateChecking": "Checking",
- "stateNeeded": "Needed",
- "allSet": "All set",
- "title": "Set up Orca Cloud",
- "starting": "Starting your cloud workspace…",
- "accountNeededTeam": "Sign in or create one. We’ll make a team for you.",
- "trial": "Trial",
- "trialNoCard": "No card needed. Nothing is charged until you choose a plan.",
- "stateIncluded": "Included",
- "trialUsedUp": "Your trial is used up.",
- "trialUsedUpMember": "Only owners and admins can choose a plan.",
- "trialUsedUpOwner": "Choose a plan to keep using Orca Cloud. Your prompt is still here.",
- "subtitleNoCard": "One visit to your browser, no card. Your prompt stays here and starts when you’re done.",
"trialUsedUpTitle": "Your trial is used up",
"choosePlan": "Choose plan",
"askChoosePlan": "Ask an owner or admin to choose a plan.",
- "trialUsedUpChoose": "Choose a plan to keep using Orca Cloud.",
"trialOffer_one": "{{hours}} sandbox hour to try Orca Cloud",
"trialOffer_other": "{{hours}} sandbox hours to try Orca Cloud",
- "subtitleChoosePlan": "One visit to your browser to choose a plan. Your prompt stays here and starts when you’re done.",
- "subtitleNeedsRepository": "Orca Cloud needs a GitHub repository. Your prompt stays here.",
- "outOfHoursMember": "Out of sandbox hours. Ask an owner or admin to add hours.",
- "spendCapMember": "Spend cap reached. Ask an owner or admin to raise the spend cap.",
- "paymentFailedMember": "Payment failed. Ask an owner or admin to update the payment method.",
- "subtitleOwnerFixes": "An owner or admin needs to fix this. Your prompt stays here.",
- "cancelSetup": "Cancel setup",
- "copySetupLink": "Copy setup link",
"reopenBrowser": "Reopen browser",
"tryAgain": "Try again",
"switchAccount": "Switch account",
- "trackerTitle": "Finish in your browser",
- "trackerSubtitle": "Orca picks up each step the moment it’s done, then starts this workspace.",
"teamRequired": "Cloud workspaces belong to a team.",
"teamHasPlan": "{{team}} has a Team plan. Switch to start this workspace there.",
"teamsHavePlans": "{{teams}} have a Team plan. Switch to one to start this workspace there.",
@@ -23168,7 +23107,6 @@
"switchTeam": "Switch team",
"switchFirst": "Switch to {{team}} first",
"switchTeamFirst": "Switch to a team first",
- "linkUnavailable": "Couldn’t get a setup link. Try Set up and start again.",
"linkCopied": "Setup link copied. Open it in the browser you use for Orca.",
"error": {
"timedOut": "Setup didn’t finish in your browser. Your prompt is still here.",
@@ -23183,18 +23121,11 @@
"planCoveredByTrial": "Your trial covers it.",
"planCheckoutOpen": "Finish checkout in your browser.",
"githubRequestedSession": "Waiting for an owner on GitHub to approve Orca Cloud.",
- "subtitleChoosePlanAndGitHub": "One visit to your browser to choose a plan and give Orca Cloud access on GitHub. Your prompt stays here and starts when you’re done."
+ "linkUnavailableRetry": "Couldn’t get a setup link. Try Continue in browser again."
},
"runOn": {
"thisComputer": "This computer",
- "orcaCloud": "Orca Cloud",
- "moreWithTarget": "More places to run: {{name}}",
- "more": "More places to run",
- "cloudUnreachableReason": "Orca Cloud: can't reach it right now. Check your connection.",
- "thisComputerUnusable": "This computer, {{reason}}",
- "locationNotSet": "Project location not set",
- "cloudUnreachableName": "Orca Cloud, can't reach it right now",
- "cloudHosted": "Hosted sandbox"
+ "orcaCloud": "Orca Cloud"
},
"cloudHours": {
"trialChip_one": "Trial · {{hours}} sandbox hour left",
@@ -23218,18 +23149,20 @@
"heading": "On GitHub · Orca Cloud only",
"detail": "Orca Cloud clones it. No copy on this computer needed.",
"moreAccess": "Give Orca Cloud access to more repositories…",
- "notOnThisComputer": "Not on this computer · Orca Cloud only",
"startFailed": "Couldn’t start the cloud workspace. Nothing was created, and you weren’t charged.",
"starting": "Starting {{repo}} in Orca Cloud…",
"openLater": "Your cloud workspace started, but this computer couldn’t open it yet. Open it from Cloud workspaces.",
"private": "{{owner}} · Private",
"public": "{{owner}} · Public",
"showMore": "Show more repositories…",
- "chooseRepository": "Choose a GitHub repository",
- "addGitHubProject": "Add a GitHub project…",
"folderDetail": "Orca Cloud starts from a GitHub repository. This project is a folder on this computer."
},
- "orcaCloudAgentFallback": "{{agent}} isn’t available on Orca Cloud yet, so {{fallback}} is selected."
+ "cloudStart": {
+ "agentNotStarted": "{{agent}} isn’t set up for Orca Cloud, so it didn’t start. The workspace has a terminal.",
+ "agentNotStartedGeneric": "Your agent isn’t set up for Orca Cloud, so it didn’t start. The workspace has a terminal.",
+ "setUpAgentOnce": "Set up {{agent}} once",
+ "setUpAgentOnceGeneric": "Set up once"
+ }
},
"JiraIssueWorkspace": {
"pendingChange": "Another issue change is in progress."
@@ -28064,5 +27997,90 @@
"signInHere": "{{agent}} isn’t signed in for this workspace.",
"pausedFix": "Orca account sign-in is paused. Sign in for this workspace; the sign-in stays with this workspace.",
"notOfferedFix": "Signs in for each cloud workspace for now. Sign in for this workspace; the sign-in stays with this workspace."
+ },
+ "cloudSetupPage": {
+ "agent": {
+ "lead": "Save your agent sign-in to your Orca account and every cloud workspace uses it. You can also skip this and sign in inside the workspace.",
+ "setUp": "Set up once",
+ "skip": "Skip for now",
+ "title": "Sign in to {{agent}} once",
+ "titleGeneric": "Sign in to your agent once"
+ },
+ "back": "Back",
+ "billing": {
+ "capLead": "Raise the spend cap to start this workspace.",
+ "capTitle": "Spend cap reached",
+ "hoursLead": "Turn on overage to start this workspace.",
+ "hoursTitle": "Out of sandbox hours",
+ "paymentLead": "The last payment failed. Update the card to keep going.",
+ "paymentTitle": "Update your payment method",
+ "openSettings": "Open billing settings"
+ },
+ "checkAgain": "Check again",
+ "checking": {
+ "title": "Checking your Orca Cloud account…"
+ },
+ "continueInBrowser": "Continue in browser",
+ "copyLink": "Copy link",
+ "done": {
+ "lead": "Starting your cloud workspace…",
+ "title": "All set"
+ },
+ "eyebrow": "Orca Cloud setup",
+ "github": {
+ "lead": "Orca Cloud clones your code from GitHub. Install the Orca Cloud app on this repository once.",
+ "title": "Give Orca Cloud access on GitHub",
+ "waiting": "This page starts the workspace once they approve.",
+ "waitingTitle": "Waiting for approval on GitHub"
+ },
+ "orcaCloud": "Orca Cloud",
+ "owner": {
+ "lead": "Orca Cloud needs a Team plan. Only owners and admins can choose one.",
+ "title": "An owner or admin needs to fix this"
+ },
+ "plan": {
+ "lead": "Orca Cloud needs a Team plan. Checkout opens in your browser.",
+ "renewLead": "Your Team plan ended. Renew it in your browser to start this workspace.",
+ "renewTitle": "Renew your Team plan",
+ "title": "Choose a Team plan",
+ "trialUsedUpLead": "Choose a Team plan to keep using Orca Cloud. Checkout opens in your browser.",
+ "trialUsedUpTitle": "Your trial is used up"
+ },
+ "reopenBrowser": "Reopen browser",
+ "repository": {
+ "title": "Orca Cloud needs a GitHub repository"
+ },
+ "signIn": {
+ "lead": "Orca Cloud runs your agents in a cloud workspace, so they keep going with your laptop closed. Sign in or create an account in your browser. We’ll make a team for you.",
+ "note": "New teams get a trial with no card needed. This page picks up each step on its own.",
+ "title": "Sign in to Orca"
+ },
+ "starting": "Starting…",
+ "startsAfter": "Starts when setup is done",
+ "step": {
+ "account": "Account",
+ "agent": "Agent",
+ "github": "GitHub",
+ "plan": "Plan",
+ "team": "Team",
+ "trial": "Trial"
+ },
+ "stepCurrent": "current step",
+ "stepDone": "done",
+ "stepsLabel": "Setup steps",
+ "team": {
+ "title": "Switch to your team"
+ },
+ "trial": {
+ "lead": "{{hours}} sandbox hours to try Orca Cloud. No card needed.",
+ "leadNoHours": "Try Orca Cloud on your team. No card needed.",
+ "note": "Nothing is charged until you choose a plan.",
+ "start": "Start free trial",
+ "title": "Start your free trial",
+ "lead_one": "{{hours}} sandbox hour to try Orca Cloud. No card needed.",
+ "lead_other": "{{hours}} sandbox hours to try Orca Cloud. No card needed."
+ },
+ "untitled": "New cloud workspace",
+ "waitingBrowser": "Waiting for your browser…"
}
}
diff --git a/src/renderer/src/lib/orca-cloud/cloud-setup-page-store.ts b/src/renderer/src/lib/orca-cloud/cloud-setup-page-store.ts
new file mode 100644
index 00000000000..1a927a81feb
--- /dev/null
+++ b/src/renderer/src/lib/orca-cloud/cloud-setup-page-store.ts
@@ -0,0 +1,98 @@
+// The full-page "Set up Orca Cloud" view (composer-redo): Start in the composer closes the dialog
+// and opens this page when the account isn't ready yet. The page walks the missing steps, then
+// starts the saved prompt. One request at a time; a new Start replaces an older one.
+import { useSyncExternalStore } from 'react'
+import type { OrcaCloudAvailability } from '../../../../shared/orca-yaml-hook-types'
+import type { ComposerModalData } from '@/components/new-workspace/composer-modal-data'
+import type { OrcaCloudRunTargetAccess } from '@/components/new-workspace/orca-cloud-run-target-access'
+import { e2eConfig } from '@/lib/e2e-config'
+
+export type CloudSetupPageRequest = {
+ prompt: string
+ repoId: string
+ agent: string | null
+ /** What main reported when Start was pressed; the page upgrades it once sign-in lands. */
+ availability: OrcaCloudAvailability
+ /** D4: a GitHub repository with no clone here; the start goes through the composer visibly. */
+ cloudOnlyRepo: string | null
+ /** The composer as it was: Back and the auto-start reopen it with everything kept. */
+ composer: ComposerModalData
+ /**
+ * Dev and e2e only: pins the account's billing answer so every step can be walked against the
+ * real page (dev sign-in has no billing). Never set by the app.
+ */
+ debugAccess?: OrcaCloudRunTargetAccess
+ openedAt: number
+}
+
+let current: CloudSetupPageRequest | null = null
+const listeners = new Set<() => void>()
+
+function emit(): void {
+ for (const listener of listeners) {
+ listener()
+ }
+}
+
+export function openCloudSetupPage(request: Omit): void {
+ current = { ...request, openedAt: Date.now() }
+ emit()
+}
+
+export function closeCloudSetupPage(): void {
+ if (current === null) {
+ return
+ }
+ current = null
+ emit()
+}
+
+export function getCloudSetupPageRequest(): CloudSetupPageRequest | null {
+ return current
+}
+
+export function useCloudSetupPageRequest(): CloudSetupPageRequest | null {
+ return useSyncExternalStore(
+ (listener) => {
+ listeners.add(listener)
+ return () => listeners.delete(listener)
+ },
+ () => current
+ )
+}
+
+const AGENT_SKIP_KEY = 'orca.cloudSetup.agentSkipped.v1'
+
+/** "Skip for now" on the agent step is remembered, so a ready account isn't asked on every Start. */
+export function isCloudSetupAgentSkipped(agent: string | null): boolean {
+ if (!agent || typeof localStorage === 'undefined') {
+ return false
+ }
+ try {
+ return localStorage.getItem(`${AGENT_SKIP_KEY}:${agent}`) === '1'
+ } catch {
+ return false
+ }
+}
+
+export function rememberCloudSetupAgentSkipped(agent: string | null): void {
+ if (!agent || typeof localStorage === 'undefined') {
+ return
+ }
+ try {
+ localStorage.setItem(`${AGENT_SKIP_KEY}:${agent}`, '1')
+ } catch {
+ // Private mode: the step asks again next time.
+ }
+}
+
+// Dev and e2e: drive the page from the console or CDP (as `__store` is), e.g. to walk the trial step.
+if ((import.meta.env.DEV || e2eConfig.exposeStore) && typeof window !== 'undefined') {
+ Object.assign(window, {
+ __cloudSetupPage: {
+ open: openCloudSetupPage,
+ close: closeCloudSetupPage,
+ get: getCloudSetupPageRequest
+ }
+ })
+}