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 05595414b74..67feb4e00c6 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
@@ -31,22 +31,10 @@ import {
orcaCloudNeedsGitHubRepo,
type CloudStartView
} from './cloud-start-view'
-import { showCloudAgentNotStartedToast } from '@/lib/orca-cloud/cloud-agent-not-started-toast'
import { orcaCloudWallNotice, useRunOnPreflightEnabled } from '@/lib/orca-cloud/run-on-preflight'
-import { isOrcaCloudAccountAgent } from '../../../../../shared/orca-cloud-agents/orca-cloud-agents-wire'
export type { CloudStartView } from './cloud-start-view'
-/**
- * The agent isn't set up: its pane holds on the "Set up once" strip (PC-3b), so the toast only
- * names the workspace once the user has moved to another one (design-squad journey).
- */
-function showAgentNotStartedToast(agent: string | null, workspaceName: string): void {
- if (isOrcaCloudAccountAgent(agent)) {
- showCloudAgentNotStartedToast(agent, workspaceName, { onlyWhenAway: true })
- }
-}
-
export function useCloudStartComposer({
active,
availability,
@@ -121,12 +109,10 @@ export function useCloudStartComposer({
})
const { steps, held: switchTeam } = useSwitchTeamSteps(rowSteps, cloudSelected)
const setupNeeded = cloudSetupRequired(steps, { teamSwitch: switchTeam !== null })
- // The agent has no Orca Cloud account (the user skipped setting it up). It is still created with
- // the workspace: its launch holds on the pane's "Set up once" strip and never reaches the agent's
- // own /login (PC-3b, #385), and its first prompt waits for it.
- const agentUnset =
- cloudSelected && steps.some((step) => step.id === 'agent' && step.state === 'optional')
- const onCreateRaw = useCloudOnlyCreate({
+ // An agent with no Orca Cloud account is still created with the workspace: its launch holds on
+ // the pane's "Set up once" strip and never reaches the agent's own /login (PC-3b, #385). In the
+ // workspace the strip asks; away, the sidebar row's mark does (N-03: no toast follows the user).
+ const onCreate = useCloudOnlyCreate({
cloudOnly,
createWorkspace,
prompt,
@@ -134,12 +120,6 @@ export function useCloudStartComposer({
profileId,
onClose
})
- const onCreate = useCallback(() => {
- onCreateRaw()
- if (agentUnset) {
- showAgentNotStartedToast(agent, prompt)
- }
- }, [agent, agentUnset, onCreateRaw, prompt])
const { markTouched } = useRunOnDefault({
active,
diff --git a/src/renderer/src/components/orca-cloud/CloudAgentHeldMark.fixture.tsx b/src/renderer/src/components/orca-cloud/CloudAgentHeldMark.fixture.tsx
new file mode 100644
index 00000000000..2d2a2dfe7d0
--- /dev/null
+++ b/src/renderer/src/components/orca-cloud/CloudAgentHeldMark.fixture.tsx
@@ -0,0 +1,65 @@
+import { Cloud } from 'lucide-react'
+import { defineFixture } from '../../component-fixtures/component-fixture'
+import { useAppStore } from '@/store'
+import { holdCloudAgentLaunch } from '@/lib/orca-cloud/cloud-agent-launch-hold'
+import { setOrcaCloudAgentAccountsStateForTest } from '@/lib/orca-cloud/orca-cloud-agent-accounts-store'
+import { CloudAgentHeldMark } from './CloudAgentHeldMark'
+
+// A cloud workspace row whose Claude waits on its pane's "Set up once" strip (N-03).
+function seed(): void {
+ setOrcaCloudAgentAccountsStateForTest({
+ load: {
+ kind: 'ready',
+ status: {
+ credentials: {
+ claude: { state: 'none' },
+ codex: { state: 'none' },
+ providerEnabled: { claude: true, codex: true }
+ },
+ sources: { defaults: {}, coveredByKey: { claude: false, codex: false } }
+ }
+ },
+ setups: { claude: { phase: 'idle' }, codex: { phase: 'idle' } },
+ keyKinds: { claude: null, codex: null },
+ queue: { agents: [], index: 0 }
+ })
+ useAppStore.setState({
+ // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mark reads only tab ids
+ tabsByWorktree: { gudgeon: [{ id: 'web-terminal-g' }] } as never
+ })
+ void holdCloudAgentLaunch({
+ decided: true,
+ paneKey: 'tab:web-terminal-g',
+ agent: 'claude',
+ prompt: null
+ })
+}
+
+function Row(): React.JSX.Element {
+ return (
+
+
+ gudgeon
+
+
+ )
+}
+
+export const sidebarRowMark = defineFixture({
+ width: 340,
+ render: () => {
+ seed()
+ return (
+
+
+
+ )
+ },
+ interact: async (frame: HTMLElement) => {
+ await new Promise((resolve) => setTimeout(resolve, 50))
+ const mark = frame.querySelector('[data-cloud-agent-held]')
+ mark?.dispatchEvent(new PointerEvent('pointermove', { bubbles: true }))
+ mark?.dispatchEvent(new PointerEvent('pointerenter', { bubbles: true }))
+ await new Promise((resolve) => setTimeout(resolve, 900))
+ }
+})
diff --git a/src/renderer/src/components/orca-cloud/CloudAgentHeldMark.test.tsx b/src/renderer/src/components/orca-cloud/CloudAgentHeldMark.test.tsx
new file mode 100644
index 00000000000..63a88faeb73
--- /dev/null
+++ b/src/renderer/src/components/orca-cloud/CloudAgentHeldMark.test.tsx
@@ -0,0 +1,86 @@
+// @vitest-environment happy-dom
+import { act, cleanup, render } from '@testing-library/react'
+import { afterEach, beforeEach, describe, expect, it } from 'vitest'
+import { TooltipProvider } from '@/components/ui/tooltip'
+import { useAppStore } from '@/store/index'
+import {
+ holdCloudAgentLaunch,
+ releaseCloudAgentHold
+} from '@/lib/orca-cloud/cloud-agent-launch-hold'
+import {
+ setOrcaCloudAgentAccountsStateForTest,
+ type OrcaCloudAgentAccountsState
+} from '@/lib/orca-cloud/orca-cloud-agent-accounts-store'
+import { CloudAgentHeldMark } from './CloudAgentHeldMark'
+
+function accountsNotSetUp(): OrcaCloudAgentAccountsState {
+ return {
+ load: {
+ kind: 'ready',
+ status: {
+ credentials: {
+ claude: { state: 'none' },
+ codex: { state: 'none' },
+ providerEnabled: { claude: true, codex: true }
+ },
+ sources: { defaults: {}, coveredByKey: { claude: false, codex: false } }
+ }
+ },
+ setups: { claude: { phase: 'idle' }, codex: { phase: 'idle' } },
+ keyKinds: { claude: null, codex: null },
+ queue: { agents: [], index: 0 }
+ }
+}
+
+function renderMark(worktreeId: string) {
+ return render(
+
+
+
+ )
+}
+
+beforeEach(() => {
+ setOrcaCloudAgentAccountsStateForTest(accountsNotSetUp())
+ useAppStore.setState({
+ // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mark reads only tab ids
+ tabsByWorktree: { wt1: [{ id: 'web-terminal-1' }], wt2: [{ id: 'web-terminal-2' }] } as never
+ })
+})
+
+afterEach(() => {
+ act(() => releaseCloudAgentHold('tab:web-terminal-1', 'cancel'))
+ cleanup()
+})
+
+describe('sidebar mark for a held cloud agent (N-03)', () => {
+ it('marks the workspace whose pane holds Claude, with the tooltip text as its label', async () => {
+ await act(async () => {
+ void holdCloudAgentLaunch({
+ decided: true,
+ paneKey: 'tab:web-terminal-1',
+ agent: 'claude',
+ prompt: null
+ })
+ })
+ const { getByRole } = renderMark('wt1')
+ expect(getByRole('img').getAttribute('aria-label')).toBe('Claude isn’t set up for Orca Cloud')
+ })
+
+ it('marks only that workspace, and clears once the hold is released (Start)', async () => {
+ await act(async () => {
+ void holdCloudAgentLaunch({
+ decided: true,
+ paneKey: 'tab:web-terminal-1',
+ agent: 'claude',
+ prompt: null
+ })
+ })
+ expect(renderMark('wt2').queryByRole('img')).toBeNull()
+ cleanup()
+ const { queryByRole } = renderMark('wt1')
+ expect(queryByRole('img')).not.toBeNull()
+ act(() => releaseCloudAgentHold('tab:web-terminal-1', 'proceed'))
+ expect(queryByRole('img')).toBeNull()
+ })
+})
diff --git a/src/renderer/src/components/orca-cloud/CloudAgentHeldMark.tsx b/src/renderer/src/components/orca-cloud/CloudAgentHeldMark.tsx
new file mode 100644
index 00000000000..45c2de07775
--- /dev/null
+++ b/src/renderer/src/components/orca-cloud/CloudAgentHeldMark.tsx
@@ -0,0 +1,61 @@
+import React from 'react'
+import { KeyRound } from 'lucide-react'
+import { useAppStore } from '@/store'
+import { useTranslate } from '@/i18n/use-translate'
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
+import { agentName } from '@/components/settings/agent-accounts/orca-cloud-accounts-copy'
+import {
+ cloudAgentHoldTabId,
+ useCloudAgentHolds,
+ type CloudAgentHold
+} from '@/lib/orca-cloud/cloud-agent-launch-hold'
+
+/** The hold waiting in one of this workspace's tabs, if any. */
+export function useWorktreeCloudAgentHold(worktreeId: string): CloudAgentHold | null {
+ const holds = useCloudAgentHolds()
+ const tabIds = useAppStore((state) => state.tabsByWorktree[worktreeId])
+ if (holds.length === 0 || !tabIds?.length) {
+ return null
+ }
+ const ids = new Set(tabIds.map((tab) => tab.id))
+ return holds.find((hold) => ids.has(cloudAgentHoldTabId(hold.paneKey))) ?? null
+}
+
+/**
+ * N-03 (design-squad): the sidebar row's mark for a workspace whose agent waits on its pane's strip
+ * ("Set up once" or a sign-in renewal). It replaces the toast that followed the user around: away
+ * from the workspace this mark is the signal; in it, the strip asks.
+ */
+export function CloudAgentHeldMark({
+ worktreeId
+}: {
+ worktreeId: string
+}): React.JSX.Element | null {
+ const t = useTranslate()
+ const hold = useWorktreeCloudAgentHold(worktreeId)
+ if (!hold) {
+ return null
+ }
+ const agent = agentName(hold.agent, t)
+ const label =
+ hold.reason === 'accountExpired'
+ ? t('orcaCloud.agentHold.expired', '{{agent}} sign-in expired', { agent })
+ : t('orcaCloud.agentHold.notSetUp', '{{agent}} isn’t set up for Orca Cloud', { agent })
+ return (
+
+
+
+
+
+
+
+ {label}
+
+
+ )
+}
diff --git a/src/renderer/src/components/sidebar/workspace-card/surface/worktree-card-header.tsx b/src/renderer/src/components/sidebar/workspace-card/surface/worktree-card-header.tsx
index 02547dd4fda..d071f00ac30 100644
--- a/src/renderer/src/components/sidebar/workspace-card/surface/worktree-card-header.tsx
+++ b/src/renderer/src/components/sidebar/workspace-card/surface/worktree-card-header.tsx
@@ -13,6 +13,7 @@ import {
} from '../identity/worktree-card-header-items'
import { WorktreeCardSshHostControl } from '../host/WorktreeCardSshHostControl'
import { WorktreeCardHostMark } from '../host/WorktreeCardHostMark'
+import { CloudAgentHeldMark } from '@/components/orca-cloud/CloudAgentHeldMark'
import { WorktreeTitleInlineRename } from '../identity/WorktreeTitleInlineRename'
import type { WorktreeCardController } from '../controller/use-worktree-card-controller'
@@ -112,6 +113,9 @@ export function WorktreeCardHeader({
onBeginEditingConsumed={affiliateListMode ? undefined : () => setRenamingWorktreeId(null)}
/>
+ {/* N-03: the agent waits on its pane's strip (Orca Cloud): this mark is the signal away. */}
+ {!titleRenaming ? : null}
+
{typeof worktree.firstAgentMessageRenameError === 'string' &&
worktree.firstAgentMessageRenameError.length > 0 &&
!titleRenaming ? (
diff --git a/src/renderer/src/components/sidebar/workspace-card/template/card-identity-parts.tsx b/src/renderer/src/components/sidebar/workspace-card/template/card-identity-parts.tsx
index ab1c836ac4a..fd7f14ba442 100644
--- a/src/renderer/src/components/sidebar/workspace-card/template/card-identity-parts.tsx
+++ b/src/renderer/src/components/sidebar/workspace-card/template/card-identity-parts.tsx
@@ -24,6 +24,7 @@ import { WorktreeCardSshHostControl } from '../host/WorktreeCardSshHostControl'
import { WorktreeHostContextBadge } from '../host/WorktreeHostContextBadge'
import { WorktreeCardHostMark, WorktreeCardHostStateWord } from '../host/WorktreeCardHostMark'
import { WorktreeTitleInlineRename } from '../identity/WorktreeTitleInlineRename'
+import { CloudAgentHeldMark } from '@/components/orca-cloud/CloudAgentHeldMark'
import { cardElement } from './card-template-compiler'
type Card = WorktreeCardController
@@ -35,24 +36,28 @@ const withoutTitleTooltip = (title: React.ReactElement): React.ReactElement => t
function title(props: Readonly>, card: Card): React.ReactNode {
const { affiliateListMode, worktree } = card
return (
- card.setRenamingWorktreeId(null)
- }
- />
+ <>
+ card.setRenamingWorktreeId(null)
+ }
+ />
+ {/* N-03: the agent waits on its pane's strip (Orca Cloud): this mark is the signal away. */}
+ {card.titleRenaming ? null : }
+ >
)
}
diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json
index 34d1f80ee96..f8d5b8f469d 100644
--- a/src/renderer/src/i18n/locales/en.json
+++ b/src/renderer/src/i18n/locales/en.json
@@ -23313,19 +23313,6 @@
"showMore": "Show more repositories…",
"folderDetail": "Orca Cloud starts from a GitHub repository. This project is a folder on this computer."
},
- "cloudStart": {
- "setUpAgentOnce": "Set up {{agent}} once",
- "agentNotStartedShort": "{{agent}} isn’t set up for Orca Cloud, so it didn’t start.",
- "agentNotStartedIn": "{{agent}} isn’t set up for Orca Cloud, so it didn’t start in {{workspace}}.",
- "copyCode": "Copy code",
- "openSettings": "Open Settings",
- "signInBrowser": "Finish signing in to {{agent}} in your browser…",
- "signInDevice": "Enter {{code}} at {{host}} to sign in to {{agent}}.",
- "signInPaste": "Paste the {{agent}} setup token in Settings to finish.",
- "signInSaving": "Saving…",
- "agentWillNotStartShort": "{{agent}} isn’t set up for Orca Cloud, so it won’t start.",
- "agentWillNotStartIn": "{{agent}} isn’t set up for Orca Cloud, so it won’t start in {{workspace}}."
- },
"agentLabel": "Agent"
},
"JiraIssueWorkspace": {
@@ -27550,6 +27537,10 @@
"failedLocalNextV2": "Use Move to this computer from the sandbox panel to bring them over. Your files are safe in the cloud workspace."
},
"orcaCloud": {
+ "agentHold": {
+ "notSetUp": "{{agent}} isn’t set up for Orca Cloud",
+ "expired": "{{agent}} sign-in expired"
+ },
"continueOffer": {
"title": "An agent was interrupted by the restart",
"action": "Continue"
diff --git a/src/renderer/src/lib/orca-cloud/cloud-agent-launch-hold.ts b/src/renderer/src/lib/orca-cloud/cloud-agent-launch-hold.ts
index ef420d0cadc..e740c6cab65 100644
--- a/src/renderer/src/lib/orca-cloud/cloud-agent-launch-hold.ts
+++ b/src/renderer/src/lib/orca-cloud/cloud-agent-launch-hold.ts
@@ -97,7 +97,16 @@ type Entry = CloudAgentHold & { release: (outcome: 'proceed' | 'cancel') => void
const holds = new Map()
const listeners = new Set<() => void>()
+/** Every current hold, as one stable snapshot for subscribers (rebuilt on each change). */
+let holdsSnapshot: readonly CloudAgentHold[] = []
+
function notify(): void {
+ holdsSnapshot = [...holds.values()].map(({ paneKey, agent, prompt, reason }) => ({
+ paneKey,
+ agent,
+ prompt,
+ reason
+ }))
for (const listener of listeners) {
listener()
}
@@ -116,6 +125,20 @@ export function useCloudAgentHold(paneKey: string): CloudAgentHold | null {
)
}
+/** All held launches (N-03: the sidebar row marks a workspace whose agent waits on its strip). */
+export function useCloudAgentHolds(): readonly CloudAgentHold[] {
+ return useSyncExternalStore(
+ subscribe,
+ () => holdsSnapshot,
+ () => holdsSnapshot
+ )
+}
+
+/** The tab a hold's pane key belongs to: `tab:` for a host tab, else `:`. */
+export function cloudAgentHoldTabId(paneKey: string): string {
+ return paneKey.startsWith('tab:') ? paneKey.slice(4) : (paneKey.split(':')[0] ?? paneKey)
+}
+
export function readCloudAgentHold(paneKey: string): CloudAgentHold | null {
return holds.get(paneKey) ?? null
}
diff --git a/src/renderer/src/lib/orca-cloud/cloud-agent-not-started-toast.test.ts b/src/renderer/src/lib/orca-cloud/cloud-agent-not-started-toast.test.ts
deleted file mode 100644
index 513f4945a49..00000000000
--- a/src/renderer/src/lib/orca-cloud/cloud-agent-not-started-toast.test.ts
+++ /dev/null
@@ -1,195 +0,0 @@
-// @vitest-environment happy-dom
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import type { CloudToastView } from '@/components/orca-cloud/cloud-toast-stack'
-
-const stack = vi.hoisted(() => ({ show: vi.fn(), hide: vi.fn() }))
-vi.mock('@/components/orca-cloud/cloud-toast-stack', () => ({
- showCloudToast: stack.show,
- hideCloudToast: stack.hide
-}))
-vi.mock('@/components/new-workspace/OrcaCloudAgentSetupRow', () => ({
- openOrcaCloudAgentAccounts: vi.fn()
-}))
-
-import { useAppStore } from '@/store/index'
-import type { OrcaCloudAgentsStatus } from '../../../../shared/orca-cloud-agents/orca-cloud-agents-wire'
-import {
- setOrcaCloudAgentAccountsStateForTest,
- type OrcaCloudAgentAccountsState
-} from './orca-cloud-agent-accounts-store'
-import {
- cloudAgentNotStartedMessage,
- showCloudAgentNotStartedToast
-} from './cloud-agent-not-started-toast'
-
-const TOAST_ID = 'orca-cloud-agent-not-started'
-
-function accounts(
- claude: OrcaCloudAgentsStatus['credentials']['claude']
-): OrcaCloudAgentAccountsState {
- return {
- load: {
- kind: 'ready',
- status: {
- credentials: {
- claude,
- codex: { state: 'none' },
- providerEnabled: { claude: true, codex: true }
- },
- sources: { defaults: {}, coveredByKey: { claude: false, codex: false } }
- }
- },
- setups: { claude: { phase: 'idle' }, codex: { phase: 'idle' } },
- keyKinds: { claude: null, codex: null },
- queue: { agents: [], index: 0 }
- }
-}
-
-/** The last view shown for the agent toast. */
-function lastView(): CloudToastView | undefined {
- const call = stack.show.mock.calls.findLast((args) => args[0] === TOAST_ID)
- return call?.[1]
-}
-function lastText(): string | undefined {
- return lastView()?.lines[0]?.text
-}
-
-const NOW = 1_800_000_000_000
-
-/** The create for `name` runs (`phase`) and then finishes: the sandbox is up, the prompt may show. */
-function creation(name: string, phase: string, status = 'creating'): void {
- useAppStore.setState({
- pendingWorktreeCreations: {
- // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the toast reads only these fields
- c1: { creationId: 'c1', startedAt: NOW, status, phase, request: { name } } as never
- }
- })
-}
-function created(name: string): void {
- creation(name, 'creating')
- useAppStore.setState({ pendingWorktreeCreations: {} })
-}
-
-describe('cloud agent not-started toast', () => {
- beforeEach(() => {
- vi.useFakeTimers({ now: NOW })
- stack.show.mockClear()
- stack.hide.mockClear()
- setOrcaCloudAgentAccountsStateForTest(accounts({ state: 'none' }))
- useAppStore.setState({ activeWorktreeId: null, pendingWorktreeCreations: {} })
- })
- afterEach(() => {
- vi.useRealTimers()
- })
-
- it('says it once, with no extra sentence, and names the workspace only after a switch', () => {
- expect(cloudAgentNotStartedMessage('Claude', null)).toBe(
- 'Claude isn’t set up for Orca Cloud, so it didn’t start.'
- )
- expect(cloudAgentNotStartedMessage('Claude', 'fix-readme-typo')).toBe(
- 'Claude isn’t set up for Orca Cloud, so it didn’t start in fix-readme-typo.'
- )
- showCloudAgentNotStartedToast('claude', 'fix-readme-typo')
- created('fix-readme-typo')
- expect(lastText()).toBe('Claude isn’t set up for Orca Cloud, so it didn’t start.')
- // DS-q: an action-required prompt, with "Set up Claude once" as the one primary button.
- expect(lastView()?.priority).toBe('action')
- expect(lastView()?.lines[0]?.action).toMatchObject({
- label: 'Set up Claude once',
- primary: true
- })
- // The create activates the new workspace, then the user moves elsewhere.
- stack.show.mockClear()
- useAppStore.setState({ activeWorktreeId: 'repo::new' })
- expect(stack.show).not.toHaveBeenCalled()
- useAppStore.setState({ activeWorktreeId: 'repo::other' })
- expect(lastText()).toBe(
- 'Claude isn’t set up for Orca Cloud, so it didn’t start in fix-readme-typo.'
- )
- })
-
- it('clears itself once the agent account turns active', () => {
- showCloudAgentNotStartedToast('claude', 'w')
- created('w')
- setOrcaCloudAgentAccountsStateForTest(accounts({ state: 'expired' }))
- expect(stack.hide).not.toHaveBeenCalled()
- setOrcaCloudAgentAccountsStateForTest(accounts({ state: 'active' }))
- expect(stack.hide).toHaveBeenCalledWith(TOAST_ID)
- })
-
- it('"Set up Claude once" starts the sign-in right from the toast, which stays up', () => {
- const start = vi.fn(async () => undefined)
- Object.assign(window, {
- api: {
- orcaCloudAgents: {
- start,
- setups: vi.fn(async () => ({ claude: { phase: 'idle' }, codex: { phase: 'idle' } })),
- onSetupChanged: vi.fn(() => () => undefined)
- }
- }
- })
- showCloudAgentNotStartedToast('claude', 'w')
- created('w')
- lastView()?.lines[0]?.action?.onClick()
- expect(start).toHaveBeenCalledWith({ agent: 'claude' })
- expect(stack.hide).not.toHaveBeenCalled()
- })
-
- it('a close by the user (by its logical id) ends the prompt for good', () => {
- showCloudAgentNotStartedToast('claude', 'w')
- created('w')
- const options = stack.show.mock.calls.findLast((args) => args[0] === TOAST_ID)?.[2]
- options?.onDismiss?.()
- stack.show.mockClear()
- // Nothing brings it back afterwards: a switch away no longer re-shows it.
- useAppStore.setState({ activeWorktreeId: 'repo::new' })
- useAppStore.setState({ activeWorktreeId: 'repo::other' })
- expect(stack.show).not.toHaveBeenCalled()
- })
-
- it('in the workspace itself it stays hidden (its pane strip asks); away, it names it', () => {
- showCloudAgentNotStartedToast('claude', 'fix-readme-typo', { onlyWhenAway: true })
- created('fix-readme-typo')
- expect(stack.show).not.toHaveBeenCalled()
- useAppStore.setState({ activeWorktreeId: 'repo::new' })
- useAppStore.setState({ activeWorktreeId: 'repo::other' })
- expect(lastText()).toBe(
- 'Claude isn’t set up for Orca Cloud, so it didn’t start in fix-readme-typo.'
- )
- })
-
- it('says it won’t start while the sandbox is starting, then didn’t start once ready', () => {
- expect(cloudAgentNotStartedMessage('Claude', null, { starting: true })).toBe(
- 'Claude isn’t set up for Orca Cloud, so it won’t start.'
- )
- showCloudAgentNotStartedToast('claude', 'w')
- // Still provisioning: nothing has started yet, so no prompt (DS-p).
- creation('w', 'provisioning-vm')
- expect(stack.show).not.toHaveBeenCalled()
- creation('w', 'fetching')
- expect(lastText()).toBe('Claude isn’t set up for Orca Cloud, so it won’t start.')
- useAppStore.setState({ pendingWorktreeCreations: {} })
- expect(lastText()).toBe('Claude isn’t set up for Orca Cloud, so it didn’t start.')
- })
-
- it('a refused create (the running limit) never shows the agent prompt (DS-p)', () => {
- showCloudAgentNotStartedToast('claude', 'w')
- creation('w', 'provisioning-vm')
- creation('w', 'provisioning-vm', 'error')
- useAppStore.setState({ pendingWorktreeCreations: {} })
- expect(stack.show).not.toHaveBeenCalled()
- })
-
- it('follows the create it was asked for even when its name is generated later (DS-p)', () => {
- // An empty name field: the create names the workspace itself ("chinook").
- showCloudAgentNotStartedToast('claude', '')
- creation('chinook', 'provisioning-vm')
- expect(stack.show).not.toHaveBeenCalled()
- useAppStore.setState({ pendingWorktreeCreations: {} })
- expect(lastText()).toBe('Claude isn’t set up for Orca Cloud, so it didn’t start.')
- // Away from it, the toast names the real workspace, not the empty field.
- useAppStore.setState({ activeWorktreeId: 'chinook-id' })
- useAppStore.setState({ activeWorktreeId: 'other' })
- expect(lastText()).toBe('Claude isn’t set up for Orca Cloud, so it didn’t start in chinook.')
- })
-})
diff --git a/src/renderer/src/lib/orca-cloud/cloud-agent-not-started-toast.ts b/src/renderer/src/lib/orca-cloud/cloud-agent-not-started-toast.ts
deleted file mode 100644
index 60967396dc9..00000000000
--- a/src/renderer/src/lib/orca-cloud/cloud-agent-not-started-toast.ts
+++ /dev/null
@@ -1,247 +0,0 @@
-// After a cloud create whose agent has no Orca Cloud account, the workspace starts with a terminal
-// (never the agent's /login). This one toast is the only sign-in prompt (design-squad journey rule;
-// PC-1, PC-3).
-// - It stays until the user acts on it or dismisses it.
-// - It names the workspace once the user has moved to another one.
-// - "Set up Claude once" runs the S0 sign-in right from the toast: Claude via setup-token in the
-// browser, Codex via a device code. The toast shows progress and errors, with Try again.
-// - It clears itself as soon as the account turns active.
-import { hideCloudToast, showCloudToast } from '@/components/orca-cloud/cloud-toast-stack'
-import { translate } from '@/i18n/i18n'
-import { useAppStore } from '@/store/index'
-import { agentName } from '@/components/settings/agent-accounts/orca-cloud-accounts-copy'
-import type { OrcaCloudAccountAgent } from '../../../../shared/orca-cloud-agents/orca-cloud-agents-wire'
-import { cloudAgentSignInView, type ToastView } from './cloud-agent-sign-in-view'
-import { orcaCloudAgentCoverageFromAccounts } from './orca-cloud-agent-accounts-model'
-import {
- readOrcaCloudAgentAccounts,
- subscribeOrcaCloudAgentAccounts
-} from './orca-cloud-agent-accounts-store'
-
-const TOAST_ID = 'orca-cloud-agent-not-started'
-
-let teardown: (() => void) | null = null
-
-export function cloudAgentNotStartedMessage(
- agent: string,
- workspaceName: string | null,
- { starting = false }: { starting?: boolean } = {}
-): string {
- // While the sandbox is still starting nothing has failed yet: say it won't start (design-squad).
- if (starting) {
- return workspaceName
- ? translate(
- 'components.newWorkspace.cloudStart.agentWillNotStartIn',
- '{{agent}} isn’t set up for Orca Cloud, so it won’t start in {{workspace}}.',
- { agent, workspace: workspaceName }
- )
- : translate(
- 'components.newWorkspace.cloudStart.agentWillNotStartShort',
- '{{agent}} isn’t set up for Orca Cloud, so it won’t start.',
- { agent }
- )
- }
- return workspaceName
- ? translate(
- 'components.newWorkspace.cloudStart.agentNotStartedIn',
- '{{agent}} isn’t set up for Orca Cloud, so it didn’t start in {{workspace}}.',
- { agent, workspace: workspaceName }
- )
- : translate(
- 'components.newWorkspace.cloudStart.agentNotStartedShort',
- '{{agent}} isn’t set up for Orca Cloud, so it didn’t start.',
- { agent }
- )
-}
-
-/** True once the agent signs in from the Orca account or a key covers it. */
-export function isCloudAgentCovered(agent: OrcaCloudAccountAgent): boolean {
- const coverage = orcaCloudAgentCoverageFromAccounts(agent, readOrcaCloudAgentAccounts())
- return coverage?.kind === 'account' || coverage?.kind === 'key'
-}
-
-/** A create counts as this toast's when it started at most this long before the toast was asked for. */
-const CREATION_MATCH_SLACK_MS = 5_000
-
-export function showCloudAgentNotStartedToast(
- agent: OrcaCloudAccountAgent,
- requestedName: string,
- {
- onlyWhenAway = false
- }: {
- /**
- * The workspace's agent pane shows its own held strip (PC-3), so the toast appears only while
- * the user is in another workspace, naming this one.
- */
- onlyWhenAway?: boolean
- } = {}
-): void {
- teardown?.()
- const name = agentName(agent, translate)
- const askedAt = Date.now()
- // The create this prompt is about: the one with the requested name, else the newest create started
- // around this call (the name is often generated later, e.g. an empty name becomes "chinook").
- // Once found it is followed by id, and its real name is the one the toast shows.
- let creationId: string | null = null
- let workspaceName = requestedName.trim()
- const creationFor = (state: ReturnType) => {
- const creations = Object.values(state.pendingWorktreeCreations ?? {})
- if (creationId) {
- return creations.find((creation) => creation.creationId === creationId)
- }
- const found =
- creations.find((creation) => workspaceName && creation.request.name === workspaceName) ??
- creations
- .filter((creation) => creation.startedAt >= askedAt - CREATION_MATCH_SLACK_MS)
- .sort((a, b) => b.startedAt - a.startedAt)[0]
- if (found) {
- creationId = found.creationId
- workspaceName = found.request.name.trim() || workspaceName
- }
- return found
- }
- // The workspace this prompt is about is still being created (its sandbox is starting).
- const isStarting = (state: ReturnType): boolean =>
- creationFor(state)?.status === 'creating'
- // DS-p: the prompt is about a workspace that exists. It waits until the sandbox is up (the create
- // moved past provisioning, or finished); a refused create (e.g. the team's running limit) never
- // shows it, only its own error.
- let sandboxUp = false
- let sawCreation = false
- const settle = (state: ReturnType): 'up' | 'refused' | 'waiting' => {
- const creation = creationFor(state)
- if (creation) {
- sawCreation = true
- if (creation.status === 'error') {
- return 'refused'
- }
- return creation.phase === 'fetching' || creation.phase === 'creating' ? 'up' : 'waiting'
- }
- // Gone from the pending list without an error: the workspace was created.
- return sawCreation ? 'up' : 'waiting'
- }
- let switchedAway = false
- const render = (): void => {
- const signIn = cloudAgentSignInView(agent, readOrcaCloudAgentAccounts().setups[agent])
- const view: ToastView = signIn ?? {
- message: cloudAgentNotStartedMessage(
- name,
- switchedAway && workspaceName ? workspaceName : null,
- { starting: isStarting(useAppStore.getState()) }
- ),
- action: {
- label: translate(
- 'components.newWorkspace.cloudStart.setUpAgentOnce',
- 'Set up {{agent}} once',
- { agent: name }
- ),
- primary: true,
- run: () => void globalThis.window?.api?.orcaCloudAgents?.start({ agent })
- }
- }
- // The shared cloud toast (DS-q): same chrome as the push notice, and as an action-required
- // prompt it sits on top of the stack. Updating the same id keeps its place and state.
- showCloudToast(
- TOAST_ID,
- {
- priority: 'action',
- lines: [
- {
- key: 'agent',
- text: view.message,
- ...(view.description ? { description: view.description } : {}),
- ...(view.action
- ? {
- action: {
- label: view.action.label,
- primary: view.action.primary === true,
- // Keeps the toast: it turns into the sign-in's progress.
- onClick: () => view.action?.run()
- }
- }
- : {})
- }
- ]
- },
- {
- // Only a close by the user ends it; a hide on returning to the workspace doesn't.
- onDismiss: () => {
- if (!onlyWhenAway || switchedAway) {
- teardown?.()
- }
- }
- }
- )
- }
- const update = (): void => {
- if (!sandboxUp) {
- return
- }
- if (onlyWhenAway && !switchedAway) {
- hideCloudToast(TOAST_ID)
- return
- }
- render()
- }
- const checkCreation = (state: ReturnType): boolean => {
- if (sandboxUp) {
- return true
- }
- const result = settle(state)
- if (result === 'refused') {
- hideCloudToast(TOAST_ID)
- teardown?.()
- return false
- }
- if (result === 'up') {
- sandboxUp = true
- update()
- }
- return true
- }
- checkCreation(useAppStore.getState())
-
- // The create activates the new workspace (first change); any later change means the user moved.
- let createdOn: string | null | undefined
- let starting = isStarting(useAppStore.getState())
- const stopWorkspace = useAppStore.subscribe((state, previous) => {
- if (!checkCreation(state)) {
- return
- }
- const nowStarting = isStarting(state)
- if (nowStarting !== starting) {
- starting = nowStarting
- update()
- }
- if (state.activeWorktreeId === previous.activeWorktreeId) {
- return
- }
- if (createdOn === undefined) {
- createdOn = state.activeWorktreeId ?? null
- return
- }
- const away = state.activeWorktreeId !== createdOn
- if (away !== switchedAway) {
- switchedAway = away
- update()
- }
- })
- let lastPhase = readOrcaCloudAgentAccounts().setups[agent].phase
- const stopAccounts = subscribeOrcaCloudAgentAccounts(() => {
- if (isCloudAgentCovered(agent)) {
- hideCloudToast(TOAST_ID)
- teardown?.()
- return
- }
- const phase = readOrcaCloudAgentAccounts().setups[agent].phase
- if (phase !== lastPhase) {
- lastPhase = phase
- update()
- }
- })
- teardown = () => {
- stopWorkspace()
- stopAccounts()
- teardown = null
- }
-}
diff --git a/src/renderer/src/lib/orca-cloud/cloud-agent-sign-in-view.ts b/src/renderer/src/lib/orca-cloud/cloud-agent-sign-in-view.ts
deleted file mode 100644
index ba530475b2a..00000000000
--- a/src/renderer/src/lib/orca-cloud/cloud-agent-sign-in-view.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-// What the agent prompt toast says while the S0 sign-in runs (split from
-// cloud-agent-not-started-toast so that file stays within its size budget).
-import { translate } from '@/i18n/i18n'
-import { openOrcaCloudAgentAccounts } from '@/components/new-workspace/OrcaCloudAgentSetupRow'
-import {
- agentName,
- setupFailureCopy
-} from '@/components/settings/agent-accounts/orca-cloud-accounts-copy'
-import type {
- OrcaCloudAccountAgent,
- OrcaCloudAgentSetup
-} from '../../../../shared/orca-cloud-agents/orca-cloud-agents-wire'
-
-export type ToastView = {
- message: string
- description?: string
- /** `primary`: the next required step (design-squad's toast chrome, DS-q). */
- action?: { label: string; run: () => void; primary?: boolean }
-}
-
-function deviceHost(url: string): string {
- try {
- return new URL(url).hostname
- } catch {
- return url
- }
-}
-
-/** What the toast says while the S0 sign-in runs; null while it is idle (the plain prompt). */
-export function cloudAgentSignInView(
- agent: OrcaCloudAccountAgent,
- setup: OrcaCloudAgentSetup
-): ToastView | null {
- const api = globalThis.window?.api?.orcaCloudAgents
- const name = agentName(agent, translate)
- switch (setup.phase) {
- case 'idle':
- case 'saved':
- return null
- case 'browser':
- return {
- message: translate(
- 'components.newWorkspace.cloudStart.signInBrowser',
- 'Finish signing in to {{agent}} in your browser…',
- { agent: name }
- ),
- // Every phase has a button: an updated toast keeps the last one otherwise.
- action: setup.canReopen
- ? {
- label: translate('orcaCloudAccounts.openAgain', 'Open again'),
- run: () => void api?.reopen({ agent })
- }
- : {
- label: translate('orcaCloudAccounts.cancel', 'Cancel'),
- run: () => void api?.cancel({ agent })
- }
- }
- case 'preparing':
- return {
- message: translate('orcaCloudAccounts.preparing', 'Getting ChatGPT sign-in ready'),
- action: {
- label: translate('orcaCloudAccounts.cancel', 'Cancel'),
- run: () => void api?.cancel({ agent })
- }
- }
- case 'device':
- return {
- message: translate(
- 'components.newWorkspace.cloudStart.signInDevice',
- 'Enter {{code}} at {{host}} to sign in to {{agent}}.',
- { code: setup.userCode, host: deviceHost(setup.verificationUrl), agent: name }
- ),
- action: {
- label: translate('components.newWorkspace.cloudStart.copyCode', 'Copy code'),
- primary: true,
- run: () => void window.api.ui.writeClipboardText(setup.userCode).catch(() => {})
- }
- }
- case 'paste':
- // The token goes in Settings' masked field, never in a toast.
- return {
- message: translate(
- 'components.newWorkspace.cloudStart.signInPaste',
- 'Paste the {{agent}} setup token in Settings to finish.',
- { agent: name }
- ),
- action: {
- label: translate('components.newWorkspace.cloudStart.openSettings', 'Open Settings'),
- primary: true,
- run: openOrcaCloudAgentAccounts
- }
- }
- case 'saving':
- return {
- message: translate('components.newWorkspace.cloudStart.signInSaving', 'Saving…')
- }
- case 'failed': {
- const copy = setupFailureCopy(agent, setup.error, translate)
- return {
- message: copy.title,
- ...(copy.detail ? { description: copy.detail } : {}),
- action: {
- label: translate('orcaCloudAccounts.tryAgain', 'Try again'),
- primary: true,
- run: () => void api?.start({ agent })
- }
- }
- }
- }
-}