diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-dead-tui-recovery.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-dead-tui-recovery.ts new file mode 100644 index 00000000000..1255cc85ff3 --- /dev/null +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-dead-tui-recovery.ts @@ -0,0 +1,35 @@ +import { randomUUID } from 'node:crypto' +import type { AgentSessionOwnerProbe } from '../../../shared/agent-session-lease-adjudication' +import type { AgentSessionRecord } from '../../../shared/agent-session-record' +import type { AgentSessionHandoffStatus } from '../../../shared/agent-session-wire' +import { createStructuredAgentSessionOperationId } from '../../../shared/structured-agent-session-mutation' +import { recoverStoredDeadTuiOwnerForHandoff } from '../../runtime/agent-session-handoff-record-transitions' +import type { AgentSessionRecordStore } from '../../runtime/agent-session-record-store' +import { idleStructuredHandoffStatus } from './structured-agent-session-handoff-status' + +export async function recoverDeadTuiHandoffStatus(input: { + store: AgentSessionRecordStore + now: () => number + record: AgentSessionRecord + expectedFence: number + probe: AgentSessionOwnerProbe +}): Promise { + const { expectedFence, now, probe, record, store } = input + if ( + record.lease.runtimeFence !== expectedFence || + record.lease.runtimeKind !== 'tui' || + record.lease.handoffStage !== null || + record.lease.claimStatus !== 'live' || + record.lease.ownerProcess === null + ) { + return null + } + const recovered = await recoverStoredDeadTuiOwnerForHandoff(store, { + sessionId: record.sessionId, + expectedFence, + operationId: createStructuredAgentSessionOperationId(randomUUID, now()), + probe, + now: now() + }) + return idleStructuredHandoffStatus(recovered) +} diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-handoff-options.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-handoff-options.test.ts index 18b74feb416..4df16f37261 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-handoff-options.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-handoff-options.test.ts @@ -29,15 +29,18 @@ import type { const CALLER = { callerKey: 'client-1' } const DEFAULT_MODEL = 'gpt-default' const PICKED_MODEL = 'gpt-picked' +const PICKED_EFFORT = 'medium' let root: string let store: AgentSessionRecordStore let host: StructuredAgentSessionHost let acquire: Mock let activeModel: string +let activeEffort: string | null let transcriptPath: string let optionFailure: Error | null const dispatchedModels: string[] = [] +const launchedOptions: (Readonly> | undefined)[] = [] function envelope(method: string, fields: Record): AgentSessionMutationEnvelope { return { @@ -80,7 +83,10 @@ function tuiOwner(fence: number, spawnToken: string): StructuredTuiOwner { function handoffTransport(): StructuredAgentSessionHandoffTransport { return { hostLabel: 'Test host', - launchTui: async ({ fence, spawnToken }) => tuiOwner(fence, spawnToken), + launchTui: async ({ record, fence, spawnToken }) => { + launchedOptions.push(record.options) + return tuiOwner(fence, spawnToken) + }, reproveTuiOwner: async ({ owner }) => owner, recoverTuiOwner: async (record) => tuiOwner( @@ -97,6 +103,7 @@ function handoffTransport(): StructuredAgentSessionHandoffTransport { function adapter(): StructuredAgentSessionAdapter { acquire = vi.fn(async ({ fence, spawnToken, options }) => { activeModel = options?.model ?? DEFAULT_MODEL + activeEffort = options?.effort ?? null return { process: { hostId: 'local', @@ -124,16 +131,26 @@ function adapter(): StructuredAgentSessionAdapter { }), cancelTurn: vi.fn(async () => ({ cancelled: true })), answerPrompt: vi.fn(async () => undefined), - setOption: vi.fn(async ({ value }) => { + setOption: vi.fn(async ({ key, value }) => { if (optionFailure) { const error = optionFailure optionFailure = null throw error } - activeModel = value - return { model: value } + if (key === 'model') { + activeModel = value + } else if (key === 'effort') { + activeEffort = value + } + return { + model: activeModel, + ...(activeEffort ? { effort: activeEffort } : {}) + } }), - readOptions: vi.fn(async () => ({ current: { model: activeModel }, models: [] })), + readOptions: vi.fn(async () => ({ + current: { model: activeModel, ...(activeEffort ? { effort: activeEffort } : {}) }, + models: [] + })), closeSession: vi.fn(async () => { activeModel = DEFAULT_MODEL }) @@ -144,8 +161,10 @@ beforeEach(async () => { root = await mkdtemp(join(tmpdir(), 'orca-handoff-options-')) resetHostTestOperationIds() activeModel = DEFAULT_MODEL + activeEffort = null optionFailure = null dispatchedModels.length = 0 + launchedOptions.length = 0 const accountHome = join(root, 'codex-home') const sessionsDir = join(accountHome, 'sessions', '2026', '08', '12') transcriptPath = join(sessionsDir, `rollout-2026-08-12T10-00-00-${THREAD}.jsonl`) @@ -217,6 +236,18 @@ describe('structured session handoff options', () => { ).toMatchObject({ ok: true }) expect(store.getRecord(SESSION)?.options).toEqual({ model: PICKED_MODEL }) + const effortFields = { key: 'effort', value: PICKED_EFFORT } + expect( + await host.setOption(CALLER, { + envelope: envelope('agentSession.setOption', effortFields), + ...effortFields + }) + ).toMatchObject({ ok: true }) + expect(store.getRecord(SESSION)?.options).toEqual({ + model: PICKED_MODEL, + effort: PICKED_EFFORT + }) + expect(await host.requestHandoff(CALLER, handoff('to-tui'))).toMatchObject({ ok: true }) await vi.waitFor(async () => expect(await host.handoffStatus(SESSION)).toMatchObject({ owner: 'tui' }) @@ -226,8 +257,15 @@ describe('structured session handoff options', () => { expect(await host.handoffStatus(SESSION)).toMatchObject({ owner: 'native' }) ) - expect(acquire.mock.calls[1]?.[0].options).toEqual({ model: PICKED_MODEL }) - expect(store.getRecord(SESSION)?.options).toEqual({ model: PICKED_MODEL }) + expect(launchedOptions).toEqual([{ model: PICKED_MODEL, effort: PICKED_EFFORT }]) + expect(acquire.mock.calls[1]?.[0].options).toEqual({ + model: PICKED_MODEL, + effort: PICKED_EFFORT + }) + expect(store.getRecord(SESSION)?.options).toEqual({ + model: PICKED_MODEL, + effort: PICKED_EFFORT + }) const body = hostTestMessage('use the selected model') expect( await host.send(CALLER, { diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-handoff.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-handoff.ts index 2c8e54b18d7..e266c4702cc 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-handoff.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-handoff.ts @@ -302,7 +302,7 @@ export class StructuredAgentSessionHandoffCoordinator { ) } - private setStatus(sessionId: string, status: AgentSessionHandoffStatus): void { + setStatus(sessionId: string, status: AgentSessionHandoffStatus): void { this.statuses.set(sessionId, status) this.deps.publish(sessionId, status) } diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-host-handoff.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-host-handoff.ts index cd26408b7c1..313d5d591b2 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-host-handoff.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-host-handoff.ts @@ -1,4 +1,5 @@ import { join } from 'node:path' +import type { AgentSessionOwnerProbe } from '../../../shared/agent-session-lease-adjudication' import type { AgentSessionRecord } from '../../../shared/agent-session-record' import type { LegacyImportOptions } from '../agent-session-journal/journal-legacy-import' import { importLegacyTranscriptIntoJournal } from '../agent-session-journal/journal-legacy-import' @@ -8,6 +9,7 @@ import type { DeferredStructuredAgentSessionEventSink } from './structured-agent import type { StructuredAgentSessionHostDeps } from './structured-agent-session-host' import type { StructuredAgentSessionHostSession } from './structured-agent-session-host-types' import { StructuredAgentSessionHandoffCoordinator } from './structured-agent-session-handoff' +import { recoverDeadTuiHandoffStatus } from './structured-agent-session-dead-tui-recovery' import { readNativeHandoffSessionOptions } from './structured-agent-session-handoff-options' import type { AgentSessionSubscribers } from './structured-agent-session-subscribers' import { StructuredTuiTranscriptCatchup } from './structured-tui-transcript-catchup' @@ -23,6 +25,11 @@ type HostHandoffAccess = { export type StructuredAgentSessionHostHandoff = StructuredAgentSessionHandoffCoordinator & { stopTuiHistoryCatchup: () => void + recoverDeadTuiOwner: ( + sessionId: string, + expectedFence: number, + probe: AgentSessionOwnerProbe + ) => Promise } export async function refreshRecoverableStructuredHandoffStatus( @@ -81,7 +88,29 @@ export function createStructuredAgentSessionHostHandoff( schedule: host.serialize, now: host.now }) - return Object.assign(coordinator, { stopTuiHistoryCatchup: () => tuiHistoryCatchup.stopAll() }) + return Object.assign(coordinator, { + stopTuiHistoryCatchup: () => tuiHistoryCatchup.stopAll(), + recoverDeadTuiOwner: async ( + sessionId: string, + expectedFence: number, + probe: AgentSessionOwnerProbe + ) => { + const record = deps.store.getRecord(sessionId) + if (!record) { + return + } + const status = await recoverDeadTuiHandoffStatus({ + store: deps.store, + now: host.now, + record, + expectedFence, + probe + }) + if (status) { + coordinator.setStatus(sessionId, status) + } + } + }) } async function importTuiHistory( diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-host-runtime-state.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-host-runtime-state.ts index e2f74f719f3..a2d7f574bd9 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-host-runtime-state.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-host-runtime-state.ts @@ -13,13 +13,15 @@ export class StructuredAgentSessionHostRuntimeState { constructor( private readonly deps: StructuredAgentSessionHostDeps, - onLeaseRenewed?: (record: AgentSessionRecord) => Promise + onLeaseRenewed?: (record: AgentSessionRecord) => Promise, + onDeadTuiOwner?: (record: AgentSessionRecord, probe: AgentSessionOwnerProbe) => Promise ) { this.leaseRenewer = new StructuredAgentSessionLeaseRenewer({ store: deps.store, probe: (record) => this.probeRecord(record), now: () => deps.now?.() ?? Date.now(), ...(onLeaseRenewed ? { onRenewed: onLeaseRenewed } : {}), + ...(onDeadTuiOwner ? { onDeadTuiOwner } : {}), onError: ({ sessionId, error }) => deps.onEventSinkError?.({ sessionId, error }) }) } diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts index a55e4bb0ac5..b95863e3605 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts @@ -68,8 +68,15 @@ export class StructuredAgentSessionHost { private readonly restartRestore = new StructuredAgentSessionRestartRestoreGate() constructor(readonly deps: StructuredAgentSessionHostDeps) { - this.runtimeState = new StructuredAgentSessionHostRuntimeState(deps, (record) => - this.restoreRenewedHandoff(record.sessionId) + this.runtimeState = new StructuredAgentSessionHostRuntimeState( + deps, + (record) => this.restoreRenewedHandoff(record.sessionId), + (record, probe) => + this.sessions.has(record.sessionId) + ? this.serialize(record.sessionId, () => + this.handoffs.recoverDeadTuiOwner(record.sessionId, record.lease.runtimeFence, probe) + ) + : Promise.resolve() ) this.reconcileLeases = createRestartReconciler({ store: deps.store, diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-lease-renewer.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-lease-renewer.test.ts index 17bfff24c8b..d9e3fec0cc0 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-lease-renewer.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-lease-renewer.test.ts @@ -132,4 +132,30 @@ describe('structured agent-session lease renewal', () => { error: expect.any(Error) }) }) + + it('routes a proven dead TUI owner into handoff recovery', async () => { + const store = await liveStore() + await store.transitionHandoff('session-renewal', (record) => ({ + ...record, + lease: { ...record.lease, runtimeKind: 'tui' } + })) + const onDeadTuiOwner = vi.fn(async () => undefined) + const onError = vi.fn() + const renewer = new StructuredAgentSessionLeaseRenewer({ + store, + probe: async () => ({ outcome: 'pid-absent' }), + now: () => NOW + 10_000, + onDeadTuiOwner, + onError + }) + + await renewer.renewNow() + + expect(onDeadTuiOwner).toHaveBeenCalledWith( + expect.objectContaining({ sessionId: 'session-renewal' }), + { outcome: 'pid-absent' } + ) + expect(store.getRecord('session-renewal')?.lease.lastRenewedAt).toBe(NOW) + expect(onError).not.toHaveBeenCalled() + }) }) diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-lease-renewer.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-lease-renewer.ts index dcec74d1d1a..751fa065bf0 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-lease-renewer.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-lease-renewer.ts @@ -1,4 +1,7 @@ -import type { AgentSessionOwnerProbe } from '../../../shared/agent-session-lease-adjudication' +import { + isProvenDeadProbe, + type AgentSessionOwnerProbe +} from '../../../shared/agent-session-lease-adjudication' import type { AgentSessionRecord } from '../../../shared/agent-session-record' import { AGENT_SESSION_LEASE_TTL_MS, @@ -17,6 +20,7 @@ export class StructuredAgentSessionLeaseRenewer { probe: (record: AgentSessionRecord) => Promise now: () => number onRenewed?: (record: AgentSessionRecord) => Promise + onDeadTuiOwner?: (record: AgentSessionRecord, probe: AgentSessionOwnerProbe) => Promise onError?: (input: { sessionId: string; error: unknown }) => void intervalMs?: number } @@ -61,10 +65,19 @@ export class StructuredAgentSessionLeaseRenewer { private async renewRecord(record: AgentSessionRecord): Promise { try { + const probe = await this.input.probe(record) + if ( + record.lease.runtimeKind === 'tui' && + isProvenDeadProbe(probe) && + this.input.onDeadTuiOwner + ) { + await this.input.onDeadTuiOwner(record, probe) + return + } const renewed = await this.input.store.renewLease({ sessionId: record.sessionId, fence: record.lease.runtimeFence, - childProbe: await this.input.probe(record), + childProbe: probe, now: this.input.now() }) await this.input.onRenewed?.(renewed) diff --git a/src/main/runtime/agent-session-handoff-lease-transitions.test.ts b/src/main/runtime/agent-session-handoff-lease-transitions.test.ts index d19869ff342..85eb7ed8571 100644 --- a/src/main/runtime/agent-session-handoff-lease-transitions.test.ts +++ b/src/main/runtime/agent-session-handoff-lease-transitions.test.ts @@ -3,9 +3,35 @@ import { agentSessionLeaseFixture, agentSessionRecordFixture } from '../../shared/agent-session-record.test-fixture' +import { recoverDeadTuiOwnerForHandoff } from './agent-session-handoff-lease-transitions' import { applyAgentSessionRestartAdjudication } from './agent-session-lease-transitions' describe('agent session handoff restart transitions', () => { + it('turns a proven dead TUI owner into one durable retry owner', () => { + const operationId = '1800000000000-00000000000000000000000000000001' + const record = agentSessionRecordFixture( + agentSessionLeaseFixture({ runtimeKind: 'tui', runtimeFence: 3 }) + ) + + const next = recoverDeadTuiOwnerForHandoff({ + record, + expectedFence: 3, + operationId, + probe: { outcome: 'pid-absent' }, + now: 1_800_000_001_000 + }) + + expect(next.lease).toMatchObject({ + runtimeKind: 'tui', + runtimeFence: 4, + handoffStage: 'old-owner-stopped', + handoffOperationId: operationId, + claimStatus: 'released', + ownerProcess: null, + deathEvidence: { kind: 'pid-absent' } + }) + }) + it('preserves the stopped owner and operation for durable retry', () => { const handoffOperationId = '1800000000000-00000000000000000000000000000001' const record = agentSessionRecordFixture( diff --git a/src/main/runtime/agent-session-handoff-lease-transitions.ts b/src/main/runtime/agent-session-handoff-lease-transitions.ts index 255102cde33..e487d21331e 100644 --- a/src/main/runtime/agent-session-handoff-lease-transitions.ts +++ b/src/main/runtime/agent-session-handoff-lease-transitions.ts @@ -1,8 +1,39 @@ +import type { AgentSessionOwnerProbe } from '../../shared/agent-session-lease-adjudication' import type { AgentSessionOwnerRuntimeKind, AgentSessionRecord } from '../../shared/agent-session-record' -import { assertFence, reserveAgentSessionOwner, withLease } from './agent-session-lease-transitions' +import { + assertFence, + evictAgentSessionOwner, + reserveAgentSessionOwner, + withLease +} from './agent-session-lease-transitions' + +export function recoverDeadTuiOwnerForHandoff(args: { + record: AgentSessionRecord + expectedFence: number + operationId: string + probe: AgentSessionOwnerProbe + now: number +}): AgentSessionRecord { + const { record } = args + assertFence(record.lease, args.expectedFence) + if ( + record.lease.runtimeKind !== 'tui' || + record.lease.handoffStage !== null || + record.lease.claimStatus !== 'live' || + record.lease.ownerProcess === null + ) { + throw new Error('agent_session_ownership_unknown') + } + const evicted = evictAgentSessionOwner(args) + return withLease(evicted, { + ...evicted.lease, + handoffStage: 'old-owner-stopped', + handoffOperationId: args.operationId + }) +} export function stopAgentSessionOwnerForHandoff(args: { record: AgentSessionRecord diff --git a/src/main/runtime/agent-session-handoff-record-transitions.ts b/src/main/runtime/agent-session-handoff-record-transitions.ts index 8d27065d4a0..dd8e25531b7 100644 --- a/src/main/runtime/agent-session-handoff-record-transitions.ts +++ b/src/main/runtime/agent-session-handoff-record-transitions.ts @@ -1,6 +1,8 @@ +import type { AgentSessionOwnerProbe } from '../../shared/agent-session-lease-adjudication' import type { AgentSessionHandoffStage } from '../../shared/agent-session-record' import { abandonAgentSessionHandoffAttempt, + recoverDeadTuiOwnerForHandoff, reserveAgentSessionHandoffOwner, rollbackAgentSessionHandoffPreparation, stopAgentSessionOwnerForHandoff, @@ -27,6 +29,21 @@ export function setStoredAgentSessionHandoffStage( ) } +export function recoverStoredDeadTuiOwnerForHandoff( + store: AgentSessionRecordStore, + args: { + sessionId: string + expectedFence: number + operationId: string + probe: AgentSessionOwnerProbe + now: number + } +) { + return store.transitionHandoff(args.sessionId, (record) => + recoverDeadTuiOwnerForHandoff({ ...args, record }) + ) +} + export function stopStoredAgentSessionOwnerForHandoff( store: AgentSessionRecordStore, args: { sessionId: string; expectedFence: number; operationId: string; now: number } diff --git a/src/main/runtime/orca-runtime-structured-tui-tab-binding.test.ts b/src/main/runtime/orca-runtime-structured-tui-tab-binding.test.ts index 6537a7f80e9..a0cd5fd7fa3 100644 --- a/src/main/runtime/orca-runtime-structured-tui-tab-binding.test.ts +++ b/src/main/runtime/orca-runtime-structured-tui-tab-binding.test.ts @@ -180,7 +180,9 @@ describe('structured TUI launch tab binding', () => { getSettings: () => ({ disabledTuiAgents: [], agentCmdOverrides: {}, - agentDefaultArgs: {}, + agentDefaultArgs: { + codex: '-m gpt-5.6-sol -c model_reasoning_effort=high' + }, agentDefaultEnv: {} }) } as never, @@ -190,8 +192,9 @@ describe('structured TUI launch tab binding', () => { } ) runtime.setNotifier(notifier(revealTerminalSession) as never) + const spawn = vi.fn().mockResolvedValue({ id: 'pty-structured', pid: 4242 }) runtime.setPtyController({ - spawn: vi.fn().mockResolvedValue({ id: 'pty-structured', pid: 4242 }), + spawn, write: () => true, kill: () => true, getForegroundProcess: async () => null @@ -264,6 +267,7 @@ describe('structured TUI launch tab binding', () => { sessionId: 'session-1', location: { workspaceId: WORKTREE_ID, executionHostId: 'local' }, accountHome: { variable: 'CODEX_HOME', path: '/tmp/codex-home' }, + options: { model: 'gpt-5.6-terra', effort: 'medium' }, providerHandleChain: [ { handle: { provider: 'codex', threadId: 'thread-1' }, observedAt: 1 } ] @@ -289,6 +293,11 @@ describe('structured TUI launch tab binding', () => { expect(waitForStructuredTuiProof.mock.invocationCallOrder[0]).toBeLessThan( revealTerminalSession.mock.invocationCallOrder[0]! ) + const launchCommand = spawn.mock.calls[0]?.[0]?.command + expect(launchCommand).toContain("'-m' 'gpt-5.6-terra'") + expect(launchCommand).toContain("'-c' 'model_reasoning_effort=medium'") + expect(launchCommand).not.toContain('gpt-5.6-sol') + expect(launchCommand).not.toContain('model_reasoning_effort=high') Object.assign(internal.handles.get(owner.terminal.handle)!, { rendererGraphEpoch: -1, diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 05cf985f2b4..244117cc7ff 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -9133,6 +9133,7 @@ export class OrcaRuntimeService { worktree: `id:${record.location.workspaceId}`, agent: 'codex', providerSession: { key: 'session_id', id: head.handle.threadId }, + ...(record.options ? { launchPreferences: record.options } : {}), presentation: 'background' }, {}, @@ -25904,6 +25905,7 @@ export class OrcaRuntimeService { }, ompResumeFilePath: request.ompResumeFilePath, sessionOptions: this.toAgentSessionOptions(request.launchPreferences), + sessionOptionsOverrideAgentArgs: Boolean(request.launchPreferences), platform, shell, isRemote diff --git a/src/renderer/src/components/native-chat/StructuredAgentSessionHandoffChrome.test.tsx b/src/renderer/src/components/native-chat/StructuredAgentSessionHandoffChrome.test.tsx index b0fc96bb475..4a185a3d630 100644 --- a/src/renderer/src/components/native-chat/StructuredAgentSessionHandoffChrome.test.tsx +++ b/src/renderer/src/components/native-chat/StructuredAgentSessionHandoffChrome.test.tsx @@ -30,4 +30,29 @@ describe('StructuredAgentSessionHandoffChrome', () => { expect(onRequest).toHaveBeenCalledWith('to-tui', 'after-turn') }) + + it('offers one Retry action for a recoverable dead TUI owner', () => { + const onRequest = vi.fn() + render( + + ) + + expect(screen.queryByRole('button', { name: 'Return to chat' })).toBeNull() + fireEvent.click(screen.getByRole('button', { name: 'Retry' })) + expect(onRequest).toHaveBeenCalledWith('to-native', 'now', 'retry') + }) }) diff --git a/src/renderer/src/components/native-chat/StructuredAgentSessionHandoffChrome.tsx b/src/renderer/src/components/native-chat/StructuredAgentSessionHandoffChrome.tsx index 367cd422822..840039564d8 100644 --- a/src/renderer/src/components/native-chat/StructuredAgentSessionHandoffChrome.tsx +++ b/src/renderer/src/components/native-chat/StructuredAgentSessionHandoffChrome.tsx @@ -95,7 +95,7 @@ export function StructuredAgentSessionHandoffChrome({ {translate('components.native-chat.handoff.cancel', 'Cancel')} - ) : owner === 'native' && !switching ? ( + ) : owner === 'native' && phase === 'idle' ? ( isWorking ? ( <>