From ca276bf0da9a85f7ec913ad9e5c0bac4f2773326 Mon Sep 17 00:00:00 2001 From: Neil Date: Wed, 11 Nov 2026 11:39:26 -0800 Subject: [PATCH 1/3] fix(cloud): N-03 no away toast for a held cloud agent; the sidebar row marks it instead journey-audit v2: the 'Claude isn't set up' toast followed the user through other workspaces and the sleep view. design-squad (PC-3 reversed on purpose): in the workspace the pane's Set up once strip asks; away, the sidebar row shows a key mark with the tooltip and aria-label 'Claude isn't set up for Orca Cloud' (or 'sign-in expired'). The away toast and its modules are removed. Toast-stack scoping (push notice) is env-check-ux's half. --- .../run-on/use-cloud-start-composer.ts | 28 +- .../orca-cloud/CloudAgentHeldMark.fixture.tsx | 65 +++++ .../orca-cloud/CloudAgentHeldMark.test.tsx | 86 ++++++ .../orca-cloud/CloudAgentHeldMark.tsx | 61 +++++ .../surface/worktree-card-header.tsx | 4 + .../src/i18n/en-runtime-required.json | 13 + src/renderer/src/i18n/locales/en.json | 4 + .../lib/orca-cloud/cloud-agent-launch-hold.ts | 23 ++ .../cloud-agent-not-started-toast.test.ts | 195 -------------- .../cloud-agent-not-started-toast.ts | 247 ------------------ .../orca-cloud/cloud-agent-sign-in-view.ts | 110 -------- 11 files changed, 260 insertions(+), 576 deletions(-) create mode 100644 src/renderer/src/components/orca-cloud/CloudAgentHeldMark.fixture.tsx create mode 100644 src/renderer/src/components/orca-cloud/CloudAgentHeldMark.test.tsx create mode 100644 src/renderer/src/components/orca-cloud/CloudAgentHeldMark.tsx delete mode 100644 src/renderer/src/lib/orca-cloud/cloud-agent-not-started-toast.test.ts delete mode 100644 src/renderer/src/lib/orca-cloud/cloud-agent-not-started-toast.ts delete mode 100644 src/renderer/src/lib/orca-cloud/cloud-agent-sign-in-view.ts 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/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index ea18b7e9b7d..33b01fed6d7 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -2165,6 +2165,19 @@ "cloudSetup": { "trialOffer_one": "{{hours}} sandbox hour to try Orca Cloud", "trialOffer_other": "{{hours}} sandbox hours to try Orca Cloud" + }, + "cloudStart": { + "agentNotStartedIn": "{{agent}} isn’t set up for Orca Cloud, so it didn’t start in {{workspace}}.", + "agentNotStartedShort": "{{agent}} isn’t set up for Orca Cloud, so it didn’t start.", + "agentWillNotStartIn": "{{agent}} isn’t set up for Orca Cloud, so it won’t start in {{workspace}}.", + "agentWillNotStartShort": "{{agent}} isn’t set up for Orca Cloud, so it won’t start.", + "copyCode": "Copy code", + "openSettings": "Open Settings", + "setUpAgentOnce": "Set up {{agent}} once", + "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…" } }, "onboarding": { diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 4451455e021..7762abe43dc 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -27547,6 +27547,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 }) - } - } - } - } -} From 4dca3ca244f0cd14bac7d5ea52e1aac62da1db6f Mon Sep 17 00:00:00 2001 From: Neil Date: Wed, 11 Nov 2026 11:46:06 -0800 Subject: [PATCH 2/3] chore(cloud): N-03 drop the removed toast's strings --- src/renderer/src/i18n/en-runtime-required.json | 13 ------------- src/renderer/src/i18n/locales/en.json | 13 ------------- 2 files changed, 26 deletions(-) diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index 33b01fed6d7..ea18b7e9b7d 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -2165,19 +2165,6 @@ "cloudSetup": { "trialOffer_one": "{{hours}} sandbox hour to try Orca Cloud", "trialOffer_other": "{{hours}} sandbox hours to try Orca Cloud" - }, - "cloudStart": { - "agentNotStartedIn": "{{agent}} isn’t set up for Orca Cloud, so it didn’t start in {{workspace}}.", - "agentNotStartedShort": "{{agent}} isn’t set up for Orca Cloud, so it didn’t start.", - "agentWillNotStartIn": "{{agent}} isn’t set up for Orca Cloud, so it won’t start in {{workspace}}.", - "agentWillNotStartShort": "{{agent}} isn’t set up for Orca Cloud, so it won’t start.", - "copyCode": "Copy code", - "openSettings": "Open Settings", - "setUpAgentOnce": "Set up {{agent}} once", - "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…" } }, "onboarding": { diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 7762abe43dc..ae1a9738a21 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -23312,19 +23312,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": { From 6a8a0f997a1d241e194da70b93ca3c7dae780a51 Mon Sep 17 00:00:00 2001 From: Neil Date: Wed, 11 Nov 2026 11:48:11 -0800 Subject: [PATCH 3/3] fix(cloud): N-03 the row mark in the template-based sidebar card too (real-run find) --- .../template/card-identity-parts.tsx | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) 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 : } + ) }