mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
fix(runtime): a new terminal the host can't confirm says why, instead of nothing (#30725)
* fix(runtime): say why a new terminal on a host failed, with a retry * fix(runtime): never offer a retry or say refused when the host's reply was lost * fix(runtime): only a host error reply counts as refused; the wake flow keeps its own toast * refactor(runtime): one 'couldn't confirm' notice carrying the reason, no failure classification * fix(runtime): one create notice per workspace, so retried seeding can't stack toasts
This commit is contained in:
@@ -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.",
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 })
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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<RuntimeTerminalCreate, 'tabId' | 'paneKey'> & {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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 } : {})
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<WebRuntimeTerminalCreateOutcome> {
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user