diff --git a/src/main/agent-hooks/server-escape-navigation-inference.test.ts b/src/main/agent-hooks/server-escape-navigation-inference.test.ts index 91a605da383..e5fc6eaea60 100644 --- a/src/main/agent-hooks/server-escape-navigation-inference.test.ts +++ b/src/main/agent-hooks/server-escape-navigation-inference.test.ts @@ -157,6 +157,7 @@ describe('navigation Escape during an open tool call', () => { }) it.each([ + ['antigravity', 'PreToolUse'], ['pi', 'tool_call'], ['prime-agent', 'tool_execution_start'] ])('leaves %s work running when Escape closes an overlay mid-%s', (agentType, hookEventName) => { @@ -175,6 +176,37 @@ describe('navigation Escape during an open tool call', () => { expect(server.getStatusSnapshotForPane(PANE)[0]).toMatchObject({ state: 'working' }) }) + it('keeps Antigravity working through repeated navigation Escape until its Stop event', () => { + const server = new AgentHookServer() + const turn = { source: 'antigravity', prompt: 'inspect files', agentType: 'antigravity' } + ingest(server, { ...turn, hookEventName: 'PreInvocation', state: 'working' }) + const published = collectPublishedStates(server) + vi.setSystemTime(1_200) + expect(pressInterruptKey(server, 'plain-escape', 2)).toBe(false) + expect(server.getStatusSnapshotForPane(PANE)[0]).toMatchObject({ state: 'working' }) + expect(published).toEqual([]) + vi.setSystemTime(1_500) + ingest(server, { ...turn, hookEventName: 'Stop', state: 'done' }) + expect(server.getStatusSnapshotForPane(PANE)[0]).toMatchObject({ state: 'done' }) + }) + + it('still accepts Ctrl+C for Antigravity work', () => { + const server = new AgentHookServer() + ingest(server, { + source: 'antigravity', + hookEventName: 'PreInvocation', + state: 'working', + prompt: 'inspect files', + agentType: 'antigravity' + }) + vi.setSystemTime(1_200) + expect(pressInterruptKey(server, 'ctrl-c')).toBe(true) + expect(server.getStatusSnapshotForPane(PANE)[0]).toMatchObject({ + state: 'done', + interrupted: true + }) + }) + it('leaves OMP work running when Escape lands between approval and execution (#9208)', () => { const server = new AgentHookServer() ingest(server, { diff --git a/src/renderer/src/components/terminal-pane/agent-interrupt-inference.test.ts b/src/renderer/src/components/terminal-pane/agent-interrupt-inference.test.ts index 0a5efcb4b43..2e9cd0b5cae 100644 --- a/src/renderer/src/components/terminal-pane/agent-interrupt-inference.test.ts +++ b/src/renderer/src/components/terminal-pane/agent-interrupt-inference.test.ts @@ -348,7 +348,7 @@ describe('agent interrupt inference', () => { entry = undefined }) - it.each([['claude'], ['omp'], ['pi'], ['prime-agent']] as const)( + it.each([['claude'], ['omp'], ['pi'], ['prime-agent'], ['antigravity']] as const)( 'never asks main to interrupt %s on a single Escape while working', (agentType) => { // Why: Escape is ambiguous at the source for these TUIs, so the renderer does not spend a @@ -372,33 +372,36 @@ describe('agent interrupt inference', () => { } ) - it.each([['claude'], ['omp']] as const)('still forwards Ctrl+C for %s', (agentType) => { - vi.useFakeTimers() - let entry: AgentStatusEntry | undefined = makeEntry({ agentType, toolName: 'Bash' }) - const inferInterrupt = vi.fn().mockReturnValue(true) - const tracker = createAgentInterruptInference({ - paneKey: PANE_KEY, - getStatusEntry: () => entry, - inferInterrupt, - now: () => 1_100 - }) + it.each([['claude'], ['omp'], ['antigravity']] as const)( + 'still forwards Ctrl+C for %s', + (agentType) => { + vi.useFakeTimers() + let entry: AgentStatusEntry | undefined = makeEntry({ agentType, toolName: 'Bash' }) + const inferInterrupt = vi.fn().mockReturnValue(true) + const tracker = createAgentInterruptInference({ + paneKey: PANE_KEY, + getStatusEntry: () => entry, + inferInterrupt, + now: () => 1_100 + }) - tracker.observeInputIntent('ctrl-c') - vi.advanceTimersByTime(500) + tracker.observeInputIntent('ctrl-c') + vi.advanceTimersByTime(500) - expect(inferInterrupt).toHaveBeenCalledWith({ - paneKey: PANE_KEY, - baselineUpdatedAt: 1_000, - baselineStateStartedAt: 900, - baselinePrompt: 'write tests', - baselineAgentType: agentType, - intent: 'ctrl-c' - }) - tracker.dispose() - entry = undefined - }) + expect(inferInterrupt).toHaveBeenCalledWith({ + paneKey: PANE_KEY, + baselineUpdatedAt: 1_000, + baselineStateStartedAt: 900, + baselinePrompt: 'write tests', + baselineAgentType: agentType, + intent: 'ctrl-c' + }) + tracker.dispose() + entry = undefined + } + ) - it.each([['claude'], ['omp'], ['pi'], ['prime-agent']] as const)( + it.each([['claude'], ['omp'], ['pi'], ['prime-agent'], ['antigravity']] as const)( 'keeps a pending Ctrl+C for %s when a navigation Escape lands before it settles', (agentType) => { // Why: Escape is not a retraction. The user asked to interrupt; dismissing an overlay diff --git a/src/shared/agent-interrupt-intent.ts b/src/shared/agent-interrupt-intent.ts index 2aaf61ec1b8..2ddd83cb66d 100644 --- a/src/shared/agent-interrupt-intent.ts +++ b/src/shared/agent-interrupt-intent.ts @@ -18,11 +18,12 @@ export function isAgentInterruptInputIntent(intent: unknown): intent is AgentInt return intent === 'plain-escape' || intent === 'ctrl-c' } -// Why: these TUIs also close an overlay on a bare Escape (Claude's /btw composer, OMP/Pi's -// focused-child and settings views). The keypress is ambiguous at the source and nothing outside +// Why: these TUIs also close an overlay on a bare Escape (Antigravity's /usage, Claude's /btw, +// OMP/Pi's focused-child and settings views). The keypress is ambiguous at the source and nothing outside // the TUI can disambiguate it, so it is never evidence a turn ended — only the provider's own // hook may retire the row (#13547, #9208). Ctrl+C is unaffected; it has no navigation meaning. const ESCAPE_ALSO_NAVIGATES_AGENT_TYPES: ReadonlySet = new Set([ + 'antigravity', 'claude', 'omp', 'pi',