diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index aeba47918b9..c948413aedf 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -18219,7 +18219,8 @@ "reconnectTimeout": "The server did not reconnect on the updated version." }, "webRuntimeSession": { - "remoteHostDisconnected": "The workspace is not connected to a remote Orca host." + "remoteHostDisconnected": "The workspace is not connected to a remote Orca host.", + "terminalCreateUnconfirmed": "Orca couldn’t confirm that the new terminal opened on the host." }, "gitlabJobTraceClient": { "loadFailed": "Failed to load the GitLab job log.", diff --git a/src/renderer/src/lib/launch-agent-in-new-tab-web-runtime.test.ts b/src/renderer/src/lib/launch-agent-in-new-tab-web-runtime.test.ts index 903c7d51d13..024755a0d76 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab-web-runtime.test.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab-web-runtime.test.ts @@ -98,15 +98,18 @@ describe('launchAgentInNewTab paired web runtime', () => { pasteDraftAfterLaunch: false }) ) - expect(mocks.createWebRuntimeSessionTerminal).toHaveBeenCalledWith({ - worktreeId: 'wt-1', - environmentId: 'web-runtime', - targetGroupId: 'group-1', - activate: true, - agentSessionKind: 'fresh', - agent: 'claude', - viewMode: 'terminal' - }) + expect(mocks.createWebRuntimeSessionTerminal).toHaveBeenCalledWith( + { + worktreeId: 'wt-1', + environmentId: 'web-runtime', + targetGroupId: 'group-1', + activate: true, + agentSessionKind: 'fresh', + agent: 'claude', + viewMode: 'terminal' + }, + { notifyFailure: false } + ) expect(mocks.createTab).not.toHaveBeenCalled() await Promise.resolve() // Why: host creation is async, so the user may be viewing another worktree by the time it lands. @@ -134,25 +137,28 @@ describe('launchAgentInNewTab paired web runtime', () => { pasteDraftAfterLaunch: false }) ) - expect(mocks.createWebRuntimeSessionTerminal).toHaveBeenCalledWith({ - worktreeId: 'wt-1', - environmentId: 'web-runtime', - targetGroupId: 'group-1', - activate: true, - agentSessionKind: 'fresh', - launchAgent: 'codex', - command: "codex '--model' 'gpt-5' '--reasoning-effort' 'high' 'fix the spinner'", - env: { CODEX_PROFILE: 'captured' }, - launchConfig: { - agentCommand: "codex '--model' 'gpt-5' '--reasoning-effort' 'high'", - agentArgs: '--model gpt-5 --reasoning-effort high', - agentEnv: { CODEX_PROFILE: 'captured' } + expect(mocks.createWebRuntimeSessionTerminal).toHaveBeenCalledWith( + { + worktreeId: 'wt-1', + environmentId: 'web-runtime', + targetGroupId: 'group-1', + activate: true, + agentSessionKind: 'fresh', + launchAgent: 'codex', + command: "codex '--model' 'gpt-5' '--reasoning-effort' 'high' 'fix the spinner'", + env: { CODEX_PROFILE: 'captured' }, + launchConfig: { + agentCommand: "codex '--model' 'gpt-5' '--reasoning-effort' 'high'", + agentArgs: '--model gpt-5 --reasoning-effort high', + agentEnv: { CODEX_PROFILE: 'captured' } + }, + startupCommandDelivery: 'shell-ready', + prompt: 'fix the spinner', + promptDelivery: 'auto-submit', + viewMode: 'terminal' }, - startupCommandDelivery: 'shell-ready', - prompt: 'fix the spinner', - promptDelivery: 'auto-submit', - viewMode: 'terminal' - }) + { notifyFailure: false } + ) expect(mocks.createTab).not.toHaveBeenCalled() }) }) diff --git a/src/renderer/src/lib/launch-agent-in-new-tab.test.ts b/src/renderer/src/lib/launch-agent-in-new-tab.test.ts index 885be5b6ae8..3b7d4051ad0 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab.test.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab.test.ts @@ -544,15 +544,13 @@ describe('launchAgentInNewTab', () => { launchAgentInNewTab({ requestId: 'request-12', agent: 'codex', worktreeId: 'wt-1' }) - expect(mockCreateWebRuntimeSessionTerminal).toHaveBeenCalledWith( - expect.objectContaining({ - worktreeId: 'wt-1', - environmentId: 'web-runtime', - agentSessionKind: 'fresh', - agent: 'codex', - viewMode: 'terminal' - }) - ) + expect(mockCreateWebRuntimeSessionTerminal.mock.calls[0]?.[0]).toMatchObject({ + worktreeId: 'wt-1', + environmentId: 'web-runtime', + agentSessionKind: 'fresh', + agent: 'codex', + viewMode: 'terminal' + }) }) it('propagates the resolved terminal mode to paired web runtime launches', async () => { @@ -568,15 +566,13 @@ describe('launchAgentInNewTab', () => { launchAgentInNewTab({ requestId: 'request-13', agent: 'codex', worktreeId: 'wt-1' }) - expect(mockCreateWebRuntimeSessionTerminal).toHaveBeenCalledWith( - expect.objectContaining({ - worktreeId: 'wt-1', - environmentId: 'web-runtime', - agentSessionKind: 'fresh', - agent: 'codex', - viewMode: 'terminal' - }) - ) + expect(mockCreateWebRuntimeSessionTerminal.mock.calls[0]?.[0]).toMatchObject({ + worktreeId: 'wt-1', + environmentId: 'web-runtime', + agentSessionKind: 'fresh', + agent: 'codex', + viewMode: 'terminal' + }) }) it('surfaces a toast when host agent launch fails in paired web clients', async () => { diff --git a/src/renderer/src/lib/launch-agent-web-host-tab.ts b/src/renderer/src/lib/launch-agent-web-host-tab.ts index 16fb8da79bd..352bc88f79f 100644 --- a/src/renderer/src/lib/launch-agent-web-host-tab.ts +++ b/src/renderer/src/lib/launch-agent-web-host-tab.ts @@ -122,7 +122,7 @@ export function launchAgentInWebHostTab(args: { launchDraft: prompt }).then((outcome) => handleCreation({ outcome, promptDelivered: outcome.status === 'created' })) } - return createWebRuntimeSessionTerminal(launch).then((outcome) => + return createWebRuntimeSessionTerminal(launch, { notifyFailure: false }).then((outcome) => handleCreation({ outcome, promptDelivered: outcome.status === 'created' && hasPrompt }) ) } diff --git a/src/renderer/src/lib/launch-ai-vault-session.test.ts b/src/renderer/src/lib/launch-ai-vault-session.test.ts index f51805a4526..5e8ed72b57f 100644 --- a/src/renderer/src/lib/launch-ai-vault-session.test.ts +++ b/src/renderer/src/lib/launch-ai-vault-session.test.ts @@ -178,24 +178,28 @@ describe('launchAiVaultSessionInNewTab', () => { }) expect(result.tabId).toBeNull() - expect(runtimeMocks.createWebRuntimeSessionTerminal).toHaveBeenCalledWith({ - worktreeId: 'wt-1', - environmentId: 'env-1', - targetGroupId: 'group-1', - agentSessionKind: 'resume', - launchAgent: 'codex', - command: "codex resume 'session-1'", - env: { CODEX_PROFILE: 'runtime' }, - envToDelete: ['CODEX_HOME', 'ORCA_CODEX_HOME'], - launchConfig: { - agentCommand: 'codex', + expect(runtimeMocks.createWebRuntimeSessionTerminal).toHaveBeenCalledWith( + { + worktreeId: 'wt-1', + environmentId: 'env-1', + targetGroupId: 'group-1', + agentSessionKind: 'resume', + launchAgent: 'codex', + command: "codex resume 'session-1'", + env: { CODEX_PROFILE: 'runtime' }, + envToDelete: ['CODEX_HOME', 'ORCA_CODEX_HOME'], + launchConfig: { + agentCommand: 'codex', + agentArgs: '', + agentEnv: { CODEX_PROFILE: 'runtime' } + }, + providerSession: { key: 'session_id', id: 'session-1' }, agentArgs: '', - agentEnv: { CODEX_PROFILE: 'runtime' } + activate: true }, - providerSession: { key: 'session_id', id: 'session-1' }, - agentArgs: '', - activate: true - }) + // Its callers toast the failure with the agent's own wording. + { notifyFailure: false } + ) expect(mockCreateTab).not.toHaveBeenCalled() expect(queuedStartups()).toEqual([]) @@ -220,7 +224,8 @@ describe('launchAiVaultSessionInNewTab', () => { expect(mockCreateEmptySplitGroup).toHaveBeenCalledWith('wt-1', 'group-1', 'right') expect(runtimeMocks.createWebRuntimeSessionTerminal).toHaveBeenCalledWith( - expect.objectContaining({ targetGroupId: 'group-new' }) + expect.objectContaining({ targetGroupId: 'group-new' }), + { notifyFailure: false } ) if (result.tabId === null) { await result.runtimeLaunch diff --git a/src/renderer/src/lib/launch-ai-vault-session.ts b/src/renderer/src/lib/launch-ai-vault-session.ts index dc1e849853f..b630483e51a 100644 --- a/src/renderer/src/lib/launch-ai-vault-session.ts +++ b/src/renderer/src/lib/launch-ai-vault-session.ts @@ -44,21 +44,24 @@ export function launchAiVaultSessionInNewTab(args: { ? store.createEmptySplitGroup(args.worktreeId, split.sourceGroupId, split.splitDirection) : null const targetGroupId = splitGroupId ?? args.targetGroupId - const runtimeLaunch = createWebRuntimeSessionTerminal({ - worktreeId: args.worktreeId, - environmentId: runtimeEnvironmentId, - ...(targetGroupId ? { targetGroupId } : {}), - agentSessionKind: 'resume', - launchAgent: args.agent, - command: args.command, - ...(args.cwd ? { cwd: args.cwd } : {}), - ...(args.env ? { env: args.env } : {}), - ...(args.envToDelete ? { envToDelete: args.envToDelete } : {}), - ...(args.launchConfig ? { launchConfig: args.launchConfig } : {}), - ...(args.providerSession ? { providerSession: args.providerSession } : {}), - ...(args.launchConfig ? { agentArgs: args.launchConfig.agentArgs } : {}), - activate: true - }) + const runtimeLaunch = createWebRuntimeSessionTerminal( + { + worktreeId: args.worktreeId, + environmentId: runtimeEnvironmentId, + ...(targetGroupId ? { targetGroupId } : {}), + agentSessionKind: 'resume', + launchAgent: args.agent, + command: args.command, + ...(args.cwd ? { cwd: args.cwd } : {}), + ...(args.env ? { env: args.env } : {}), + ...(args.envToDelete ? { envToDelete: args.envToDelete } : {}), + ...(args.launchConfig ? { launchConfig: args.launchConfig } : {}), + ...(args.providerSession ? { providerSession: args.providerSession } : {}), + ...(args.launchConfig ? { agentArgs: args.launchConfig.agentArgs } : {}), + activate: true + }, + { notifyFailure: false } + ) const observedRuntimeLaunch = runtimeLaunch.then((outcome) => { if (outcome.status === 'created') { useAppStore.getState().setActiveTabType('terminal', args.worktreeId) diff --git a/src/renderer/src/lib/web-runtime-worktree-terminal-after-wake.ts b/src/renderer/src/lib/web-runtime-worktree-terminal-after-wake.ts index f0fa00a14b5..ec3652b8bf7 100644 --- a/src/renderer/src/lib/web-runtime-worktree-terminal-after-wake.ts +++ b/src/renderer/src/lib/web-runtime-worktree-terminal-after-wake.ts @@ -115,26 +115,30 @@ export function ensureWebRuntimeWorktreeTerminalAfterWake( const startup = opts?.startup // Why: sleep keeps tab rows but terminal.stop clears host PTYs, while a failed create receipt leaves a selected agent with no host surface. - void createWebRuntimeSessionTerminal({ - worktreeId, - environmentId: runtimeEnvironmentId, - ...(startup - ? { - command: startup.command, - ...(startup.env ? { env: startup.env } : {}), - ...(startup.launchConfig ? { launchConfig: startup.launchConfig } : {}), - ...(startup.launchToken ? { launchToken: startup.launchToken } : {}), - ...(launchAgent ? { launchAgent, preparedAgentCommand: true } : {}), - ...(startup.startupCommandDelivery - ? { startupCommandDelivery: startup.startupCommandDelivery } - : {}) - } - : launchAgent - ? { agent: launchAgent } - : {}), - activate: opts?.activate !== false, - selectWorktree: false - }) + void createWebRuntimeSessionTerminal( + { + worktreeId, + environmentId: runtimeEnvironmentId, + ...(startup + ? { + command: startup.command, + ...(startup.env ? { env: startup.env } : {}), + ...(startup.launchConfig ? { launchConfig: startup.launchConfig } : {}), + ...(startup.launchToken ? { launchToken: startup.launchToken } : {}), + ...(launchAgent ? { launchAgent, preparedAgentCommand: true } : {}), + ...(startup.startupCommandDelivery + ? { startupCommandDelivery: startup.startupCommandDelivery } + : {}) + } + : launchAgent + ? { agent: launchAgent } + : {}), + activate: opts?.activate !== false, + selectWorktree: false + }, + // This flow toasts every failure itself, including a disconnected host. + { notifyFailure: false } + ) .then((outcome) => { if (outcome.status === 'failed') { toast.error(outcome.message, { diff --git a/src/renderer/src/runtime/web-runtime-session-types.ts b/src/renderer/src/runtime/web-runtime-session-types.ts index 97fa9d4aa4d..aeb015419d9 100644 --- a/src/renderer/src/runtime/web-runtime-session-types.ts +++ b/src/renderer/src/runtime/web-runtime-session-types.ts @@ -47,6 +47,8 @@ export type CreateWebRuntimeSessionTerminalArgs = { export type CreatedWebRuntimeSessionTerminal = { outcome: WebRuntimeTerminalCreateOutcome hostTabId?: string + /** A live session existed and the create was not confirmed, as opposed to no session to ask. */ + hostFailed?: true } export type CreatedAgentTerminalIdentity = Pick & { diff --git a/src/renderer/src/runtime/web-runtime-terminal-create-failure-notice.test.ts b/src/renderer/src/runtime/web-runtime-terminal-create-failure-notice.test.ts new file mode 100644 index 00000000000..23ecb7e7390 --- /dev/null +++ b/src/renderer/src/runtime/web-runtime-terminal-create-failure-notice.test.ts @@ -0,0 +1,171 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { createWebRuntimeSessionTerminal } from './web-runtime-terminal-creation' +import { resetWebSessionCloseIntentForTests } from './web-session-close-intent' +import { + WORKTREE_ID, + resetTerminalCreateEnvironment, + stubTerminalCreateEnvironment +} from './web-runtime-session-test-harness' +import { stubRuntimeEnvironmentBridge } from './runtime-environment-bridge-fixture' + +const mocks = vi.hoisted(() => ({ + getState: vi.fn(), + setState: vi.fn(), + subscribe: vi.fn(), + setActiveWorktree: vi.fn(), + createBrowserTab: vi.fn(), + closeEmptyGroup: vi.fn(), + moveUnifiedTabToGroup: vi.fn(), + setRemoteBrowserPageHandle: vi.fn(), + focusBrowserTabInWorktree: vi.fn(), + applyWebSessionTabsSnapshot: vi.fn(), + decideWebSessionTabsSnapshot: vi.fn(() => ({ apply: true, settlesHostMirror: true })), + getWebSessionTabsTrackingGeneration: vi.fn(() => 0), + acceptReplayedWebSessionTabsSnapshot: vi.fn(), + resolveHostSessionTabIdForWebSessionTab: vi.fn(), + trackTerminalPaneSplit: vi.fn(), + deliverLaunchPromptToAgentTab: vi.fn(), + seedNativeChatLaunchDraftForAgentTab: vi.fn(), + hasMaterializedWebRuntimeBrowserPage: vi.fn(), + toastError: vi.fn() +})) + +vi.mock('../store', () => ({ + useAppStore: { + getState: mocks.getState, + setState: mocks.setState, + subscribe: mocks.subscribe + } +})) + +vi.mock('./web-session-tabs-sync/tracking-lifecycle', () => ({ + acceptReplayedWebSessionTabsSnapshot: mocks.acceptReplayedWebSessionTabsSnapshot, + getWebSessionTabsTrackingGeneration: mocks.getWebSessionTabsTrackingGeneration +})) +vi.mock('./web-session-tabs-sync/snapshot-api', () => ({ + applyWebSessionTabsSnapshot: mocks.applyWebSessionTabsSnapshot +})) +vi.mock('./web-session-tabs-sync/tracking-decisions', () => ({ + decideWebSessionTabsSnapshot: mocks.decideWebSessionTabsSnapshot +})) +vi.mock('./web-session-tabs-sync/store-patch', () => ({ + applyWebSessionTabsStorePatch: (buildPatch: (state: unknown) => unknown) => { + mocks.setState(buildPatch) + // The production caller invokes the returned settle receipt. + return () => {} + } +})) +vi.mock('./web-session-tabs-sync/tracking-mappings', () => ({ + resolveHostSessionTabIdForWebSessionTab: mocks.resolveHostSessionTabIdForWebSessionTab +})) + +vi.mock('@/lib/feature-education-telemetry', () => ({ + trackTerminalPaneSplit: mocks.trackTerminalPaneSplit +})) + +vi.mock('@/lib/agent-launch-prompt-delivery', () => ({ + deliverLaunchPromptToAgentTab: mocks.deliverLaunchPromptToAgentTab, + seedNativeChatLaunchDraftForAgentTab: mocks.seedNativeChatLaunchDraftForAgentTab +})) + +vi.mock('./web-runtime-browser-materialization', () => ({ + hasMaterializedWebRuntimeBrowserPage: mocks.hasMaterializedWebRuntimeBrowserPage +})) + +vi.mock('sonner', () => ({ toast: { error: mocks.toastError } })) + +afterEach(() => resetWebSessionCloseIntentForTests()) + +const HOST_REFUSAL = 'Profile state write failed without a confirmed rollback' + +describe('a terminal the host fails to create', () => { + beforeEach(() => { + stubTerminalCreateEnvironment(mocks) + }) + + afterEach(() => { + resetTerminalCreateEnvironment() + }) + + it('passes on the host’s reason, since no tab or pane will ever show it', async () => { + stubRuntimeEnvironmentBridge({ + call: vi.fn().mockResolvedValue({ + id: 'create', + ok: false, + error: { code: 'runtime_error', message: HOST_REFUSAL } + }) + }) + vi.spyOn(console, 'warn').mockImplementation(() => undefined) + + await expect( + createWebRuntimeSessionTerminal({ worktreeId: WORKTREE_ID, activate: true }) + ).resolves.toMatchObject({ status: 'failed' }) + + expect(mocks.toastError).toHaveBeenCalledExactlyOnceWith( + 'Orca couldn’t confirm that the new terminal opened on the host.', + expect.objectContaining({ description: expect.stringContaining(HOST_REFUSAL) }) + ) + // One toast per workspace however often automatic seeding retries. + expect(mocks.toastError.mock.calls[0][1]).toMatchObject({ + id: `web-runtime-terminal-create:active:${WORKTREE_ID}` + }) + }) + + it('says unconfirmed, never failed, when the reply was lost', async () => { + stubRuntimeEnvironmentBridge({ + call: vi.fn().mockResolvedValue({ + id: 'create', + ok: false, + error: { code: 'runtime_timeout', message: 'Timed out waiting for the remote Orca runtime' } + }) + }) + vi.spyOn(console, 'warn').mockImplementation(() => undefined) + + await createWebRuntimeSessionTerminal({ worktreeId: WORKTREE_ID, activate: true }) + + expect(mocks.toastError).toHaveBeenCalledExactlyOnceWith( + 'Orca couldn’t confirm that the new terminal opened on the host.', + expect.objectContaining({ description: expect.stringContaining('Timed out') }) + ) + }) + + it('says the same for a browser client request timeout', async () => { + stubRuntimeEnvironmentBridge({ + call: vi.fn().mockRejectedValue(new Error('Request timed out: session.tabs.createTerminal')) + }) + vi.spyOn(console, 'warn').mockImplementation(() => undefined) + + await createWebRuntimeSessionTerminal({ worktreeId: WORKTREE_ID, activate: true }) + + expect(mocks.toastError).toHaveBeenCalledExactlyOnceWith( + 'Orca couldn’t confirm that the new terminal opened on the host.', + expect.objectContaining({ description: expect.stringContaining('Request timed out') }) + ) + }) + + it('stays quiet for a caller that reports the failure itself', async () => { + stubRuntimeEnvironmentBridge({ + call: vi.fn().mockResolvedValue({ + id: 'create', + ok: false, + error: { code: 'runtime_error', message: HOST_REFUSAL } + }) + }) + vi.spyOn(console, 'warn').mockImplementation(() => undefined) + + await createWebRuntimeSessionTerminal( + { worktreeId: WORKTREE_ID, activate: true }, + { notifyFailure: false } + ) + expect(mocks.toastError).not.toHaveBeenCalled() + }) + + it('stays quiet when no host session exists, so callers can fall back to a local tab', async () => { + vi.stubGlobal('__ORCA_WEB_CLIENT__', false) + mocks.getState.mockReturnValue({ ...mocks.getState(), settings: {} }) + await expect( + createWebRuntimeSessionTerminal({ worktreeId: WORKTREE_ID, activate: true }) + ).resolves.toMatchObject({ status: 'failed' }) + expect(mocks.toastError).not.toHaveBeenCalled() + }) +}) diff --git a/src/renderer/src/runtime/web-runtime-terminal-create-failure-notice.ts b/src/renderer/src/runtime/web-runtime-terminal-create-failure-notice.ts new file mode 100644 index 00000000000..2fefbb0aff6 --- /dev/null +++ b/src/renderer/src/runtime/web-runtime-terminal-create-failure-notice.ts @@ -0,0 +1,24 @@ +import { toast } from 'sonner' +import { translate } from '@/i18n/i18n' + +/** + * The user asked for a terminal and the host never mirrored one, so no pane exists to show why. + * "Couldn't confirm" stays true when the reply was lost and the terminal appears later; the reason + * carries the host's refusal or the transport failure as reported. No retry: it could duplicate. + */ +export function notifyWebRuntimeTerminalCreateFailure( + reason: string, + scope: { environmentId: string | null; worktreeId: string } +): void { + toast.error( + translate( + 'auto.runtime.webRuntimeSession.terminalCreateUnconfirmed', + 'Orca couldn’t confirm that the new terminal opened on the host.' + ), + { + // Why an id: automatic seeding retries on every reconnect and must not stack toasts. + id: `web-runtime-terminal-create:${scope.environmentId ?? 'active'}:${scope.worktreeId}`, + ...(reason ? { description: reason } : {}) + } + ) +} diff --git a/src/renderer/src/runtime/web-runtime-terminal-create-operation.ts b/src/renderer/src/runtime/web-runtime-terminal-create-operation.ts index fefa66f4f66..955e51e9b08 100644 --- a/src/renderer/src/runtime/web-runtime-terminal-create-operation.ts +++ b/src/renderer/src/runtime/web-runtime-terminal-create-operation.ts @@ -296,7 +296,8 @@ export async function createWebRuntimeSessionTerminalResult( // to retry with a new operation ID and can duplicate a fresh agent. return { outcome: hostCreated ? { status: 'created' } : { status: 'failed', message }, - ...(createdTabId ? { hostTabId: createdTabId } : {}) + ...(createdTabId ? { hostTabId: createdTabId } : {}), + ...(hostCreated ? {} : { hostFailed: true as const }) } } } diff --git a/src/renderer/src/runtime/web-runtime-terminal-creation.ts b/src/renderer/src/runtime/web-runtime-terminal-creation.ts index 42567f76ba5..bde5df66a78 100644 --- a/src/renderer/src/runtime/web-runtime-terminal-creation.ts +++ b/src/renderer/src/runtime/web-runtime-terminal-creation.ts @@ -4,16 +4,33 @@ import { seedNativeChatLaunchDraftForAgentTab } from '../lib/agent-launch-prompt-delivery' import { createWebRuntimeSessionTerminalResult } from './web-runtime-terminal-create-operation' +import { notifyWebRuntimeTerminalCreateFailure } from './web-runtime-terminal-create-failure-notice' import { toWebTerminalSurfaceTabId } from '../../../shared/terminal-surface-id' import type { CreateWebRuntimeSessionTerminalArgs, WebRuntimeTerminalCreateOutcome } from './web-runtime-session-types' +/** + * A host-side failure is toasted here because the host never mirrors a tab for it, so no pane + * exists to show the error. `notifyFailure: false` is for callers that report it themselves. + */ export async function createWebRuntimeSessionTerminal( - args: CreateWebRuntimeSessionTerminalArgs + args: CreateWebRuntimeSessionTerminalArgs, + options: { notifyFailure?: boolean } = {} ): Promise { - return (await createWebRuntimeSessionTerminalResult(args)).outcome + const created = await createWebRuntimeSessionTerminalResult(args) + if ( + created.hostFailed && + created.outcome.status === 'failed' && + options.notifyFailure !== false + ) { + notifyWebRuntimeTerminalCreateFailure(created.outcome.message, { + environmentId: args.environmentId ?? null, + worktreeId: args.worktreeId + }) + } + return created.outcome } export async function createWebRuntimeAgentSessionTerminal(