From 31fb65bf0bd2f2a7e2730cf8974800af0ba55b7d Mon Sep 17 00:00:00 2001 From: brennanb2025 Date: Tue, 19 May 2026 01:01:09 -0700 Subject: [PATCH] Ignore Droid Ctrl-C for interrupt inference Co-authored-by: Orca --- src/main/agent-hooks/server.test.ts | 39 +++++++++++++++++++ src/main/agent-hooks/server.ts | 5 +++ .../agent-interrupt-inference.test.ts | 19 +++++++++ .../agent-interrupt-inference.ts | 11 ++++++ 4 files changed, 74 insertions(+) diff --git a/src/main/agent-hooks/server.test.ts b/src/main/agent-hooks/server.test.ts index 82383ed98b7..7d1ee5e3a66 100644 --- a/src/main/agent-hooks/server.test.ts +++ b/src/main/agent-hooks/server.test.ts @@ -317,6 +317,45 @@ describe('AgentHookServer listener replay', () => { } ) + it('rejects Ctrl+C inference for Droid', () => { + vi.useFakeTimers() + vi.setSystemTime(1_000) + try { + const server = new AgentHookServer() + server.ingestRemote( + { + paneKey: PANE, + tabId: 'tab-1', + worktreeId: 'wt-1', + payload: { state: 'working', prompt: 'long task', agentType: 'droid' } + }, + 'conn-1' + ) + const baseline = server.getStatusSnapshot()[0] + + vi.setSystemTime(1_500) + const applied = server.inferInterrupt({ + paneKey: PANE, + baselineUpdatedAt: baseline.receivedAt, + baselineStateStartedAt: baseline.stateStartedAt, + baselinePrompt: 'long task', + baselineAgentType: 'droid', + intent: 'ctrl-c' + }) + + expect(applied).toBe(false) + expect(server.getStatusSnapshot()).toEqual([ + expect.objectContaining({ + state: 'working', + prompt: 'long task', + agentType: 'droid' + }) + ]) + } finally { + vi.useRealTimers() + } + }) + it('does not let late same-turn working hooks resurrect an inferred interrupt', () => { vi.useFakeTimers() vi.setSystemTime(1_000) diff --git a/src/main/agent-hooks/server.ts b/src/main/agent-hooks/server.ts index 172815ae263..65940cedf92 100644 --- a/src/main/agent-hooks/server.ts +++ b/src/main/agent-hooks/server.ts @@ -311,6 +311,11 @@ export class AgentHookServer { } const payload = existing.payload const agentType: AgentType | undefined = payload.agentType + // Why: Droid's Ctrl+C does not interrupt the current turn; repeated Ctrl+C + // exits the CLI, which is handled by process/PTY lifecycle cleanup. + if (agentType === 'droid' && request.intent === 'ctrl-c') { + return false + } // Why: these agents use the first Escape as a TUI/editor cancel. A single // Escape can leave the turn running, so only a deliberate double Escape // may infer an interrupted turn. 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 f48de9b6af4..780137e1184 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 @@ -128,6 +128,25 @@ describe('agent interrupt inference', () => { entry = undefined }) + it('does not infer Ctrl+C for Droid', () => { + vi.useFakeTimers() + let entry: AgentStatusEntry | undefined = makeEntry({ agentType: 'droid' }) + const inferInterrupt = vi.fn() + const tracker = createAgentInterruptInference({ + paneKey: PANE_KEY, + getStatusEntry: () => entry, + inferInterrupt, + now: () => 1_100 + }) + + tracker.observeInputIntent('ctrl-c') + vi.advanceTimersByTime(500) + + expect(inferInterrupt).not.toHaveBeenCalled() + tracker.dispose() + entry = undefined + }) + it.each(['opencode', 'copilot'] as const)( 'infers immediately on double Escape for %s', (agentType) => { diff --git a/src/renderer/src/components/terminal-pane/agent-interrupt-inference.ts b/src/renderer/src/components/terminal-pane/agent-interrupt-inference.ts index 55f579524a2..801afd2440c 100644 --- a/src/renderer/src/components/terminal-pane/agent-interrupt-inference.ts +++ b/src/renderer/src/components/terminal-pane/agent-interrupt-inference.ts @@ -49,6 +49,13 @@ function shouldFlushInterruptImmediately( ) } +function shouldIgnoreInterruptIntent( + agentType: AgentStatusEntry['agentType'], + intent: AgentInterruptInputIntent +): boolean { + return agentType === 'droid' && intent === 'ctrl-c' +} + function isSameTurnBaseline( left: CapturedInterruptBaseline, right: CapturedInterruptBaseline @@ -192,6 +199,10 @@ export function createAgentInterruptInference({ clearPending() return } + if (shouldIgnoreInterruptIntent(baseline.agentType, intent)) { + clearPending() + return + } if (requiresDoubleEscapeForAgent(baseline.agentType, intent)) { const isSecondEscape = doubleEscapeBaseline !== null && isSameTurnBaseline(doubleEscapeBaseline, baseline)