diff --git a/src/main/agent-hooks/server.test.ts b/src/main/agent-hooks/server.test.ts index f78b507479a..65b3229b8e6 100644 --- a/src/main/agent-hooks/server.test.ts +++ b/src/main/agent-hooks/server.test.ts @@ -8920,11 +8920,19 @@ describe('AgentHookServer ingestRemote', () => { }) describe('AgentHookServer ingestTerminalStatus', () => { - it('preserves a hook turn stamp across an equivalent OSC repaint', () => { + it('preserves a hook turn stamp when an OSC repaint omits hook-only completion text', () => { const server = new AgentHookServer() const listener = vi.fn() server.setListener(listener) + server.ingestRemote( + { + paneKey: PANE, + payload: { state: 'working', prompt: 'review the PR', agentType: 'claude' } + }, + 'conn-1' + ) + listener.mockClear() server.ingestRemote( { paneKey: PANE, @@ -8932,6 +8940,7 @@ describe('AgentHookServer ingestTerminalStatus', () => { state: 'working', prompt: 'review the PR', agentType: 'claude', + lastAssistantMessage: 'Review complete.', turnCompletedAt: 1_700_000_005_000 } }, @@ -8946,8 +8955,20 @@ describe('AgentHookServer ingestTerminalStatus', () => { expect(listener).toHaveBeenCalledOnce() expect(server.getStatusSnapshot()[0]).toMatchObject({ state: 'working', + lastAssistantMessage: 'Review complete.', turnCompletedAt: 1_700_000_005_000 }) + + server.ingestTerminalStatus({ + paneKey: PANE, + connectionId: 'conn-1', + payload: { state: 'done', prompt: 'review the PR', agentType: 'claude' } + }) + + expect(listener).toHaveBeenCalledTimes(2) + expect(listener).toHaveBeenLastCalledWith( + expect.objectContaining({ payload: expect.objectContaining({ state: 'done' }) }) + ) }) it('accepts an identical-prompt hook boundary after a stamped turn', () => { diff --git a/src/main/agent-hooks/server.ts b/src/main/agent-hooks/server.ts index af1f7e29e5a..48ff387fad4 100644 --- a/src/main/agent-hooks/server.ts +++ b/src/main/agent-hooks/server.ts @@ -426,7 +426,8 @@ function equivalentParsedAgentStatusPayload( a.toolName === b.toolName && a.toolInput === b.toolInput && a.interactivePrompt === b.interactivePrompt && - a.lastAssistantMessage === b.lastAssistantMessage && + (a.lastAssistantMessage === b.lastAssistantMessage || + (preserveActiveTurnStamp && b.lastAssistantMessage === undefined)) && a.interrupted === b.interrupted && // Why: a session-boundary done must never be deduped against a cached real done — // the flag has to reach receivers deterministically (STA-3386). diff --git a/src/renderer/src/components/terminal-pane/agent-completion-coordinator.test.ts b/src/renderer/src/components/terminal-pane/agent-completion-coordinator.test.ts index 04a94417e44..87213077bcb 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-coordinator.test.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-coordinator.test.ts @@ -1373,6 +1373,7 @@ describe('agent completion coordinator', () => { state: 'working', prompt: 'review the PR', agentType: 'claude', + lastAssistantMessage: 'Review complete.', stateStartedAt: 2_000, turnCompletedAt: 2_500 })