diff --git a/src/renderer/src/lib/orca-cloud/cloud-web-host-launch-hold.test.ts b/src/renderer/src/lib/orca-cloud/cloud-web-host-launch-hold.test.ts index cb05f020493..6fe08328ee5 100644 --- a/src/renderer/src/lib/orca-cloud/cloud-web-host-launch-hold.test.ts +++ b/src/renderer/src/lib/orca-cloud/cloud-web-host-launch-hold.test.ts @@ -291,6 +291,8 @@ describe('NOACCT-MISLABEL: no Claude account is a setup hold, never "didn’t st const { toast } = await import('sonner') vi.mocked(toast).mockClear() useAppStore.setState({ + activeView: 'terminal', + activeWorktreeId: 'w1', tabsByWorktree: { w1: [makeTerminalTab({ id: toWebTerminalSurfaceTabId('h1'), worktreeId: 'w1' })] } @@ -302,4 +304,30 @@ describe('NOACCT-MISLABEL: no Claude account is a setup hold, never "didn’t st await result expect(toast).toHaveBeenCalledWith('The agent didn’t start. The shell is still open.') }) + + it('never confirms a cancel over another workspace or Settings (N-03)', async () => { + const { useAppStore } = await import('@/store') + const { makeTerminalTab } = await import('@/store/terminals/terminal-row.test-fixture') + const { toast } = await import('sonner') + const tabs = { + w1: [makeTerminalTab({ id: toWebTerminalSurfaceTabId('h1'), worktreeId: 'w1' })] + } + const cancelHold = async (): Promise => { + setOrcaCloudAgentAccountsStateForTest(accounts({ state: 'none' })) + const result = createWithCloudAgentHold(CREATE_AGENT, hostCreate()) + await flush() + releaseCloudAgentHold(cloudTabHoldKey(toWebTerminalSurfaceTabId('h1')), 'cancel') + await result + } + vi.mocked(toast).mockClear() + useAppStore.setState({ + activeView: 'terminal', + activeWorktreeId: 'other', + tabsByWorktree: tabs + }) + await cancelHold() + useAppStore.setState({ activeView: 'settings', activeWorktreeId: 'w1', tabsByWorktree: tabs }) + await cancelHold() + expect(toast).not.toHaveBeenCalled() + }) }) diff --git a/src/renderer/src/lib/orca-cloud/cloud-web-host-launch-hold.ts b/src/renderer/src/lib/orca-cloud/cloud-web-host-launch-hold.ts index 9b1430224f6..23f9b36a471 100644 --- a/src/renderer/src/lib/orca-cloud/cloud-web-host-launch-hold.ts +++ b/src/renderer/src/lib/orca-cloud/cloud-web-host-launch-hold.ts @@ -83,7 +83,8 @@ export async function createWithCloudAgentHold( // NOACCT-MISLABEL: the confirmation answers the person's own Cancel, while the shell is still // there. A hold that ended because its tab or workspace went away (deleted, archived, closed) // says nothing: there is no shell left, and nothing failed to start. - if (isTabOpen(args.worktreeId, tabId)) { + // Defense in depth (N-03): only over its own workspace, never over another one or Settings. + if (isTabOpen(args.worktreeId, tabId) && workspaceOnScreen(args.worktreeId)) { toast( translate( 'agentSignInStrip.cancelledToast', @@ -122,6 +123,12 @@ export function isTabOpen(worktreeId: string, tabId: string): boolean { return (useAppStore.getState().tabsByWorktree[worktreeId] ?? []).some((tab) => tab.id === tabId) } +/** The workspace is the one in front, in the workspace view (not Settings or another page). */ +function workspaceOnScreen(worktreeId: string): boolean { + const state = useAppStore.getState() + return state.activeView === 'terminal' && state.activeWorktreeId === worktreeId +} + // The agent's tab can reach the store after the placeholder's close (a create's sync is slower). const AGENT_TAB_FOCUS_WAIT_MS = 10_000