Forward paired host completion stamps

This commit is contained in:
Brennan Benson
2026-08-14 15:37:06 -07:00
parent 20b9821ff3
commit fdeef5b662
2 changed files with 84 additions and 13 deletions
@@ -13,6 +13,11 @@ vi.mock('@/hooks/agent-hook-completion-notifications', () => ({
import { useAppStore } from '@/store'
import { createAgentCompletionCoordinator } from '@/components/terminal-pane/agent-completion-coordinator'
import {
markRendererOwnedAgentStatusWrite,
registerRendererOwnedAgentStatusPane,
resetRendererOwnedAgentStatusPanesForTests
} from '@/components/terminal-pane/renderer-owned-agent-status-registry'
import {
applyWebSessionTabsSnapshot,
applyWebSessionTabsStorePatch,
@@ -79,12 +84,14 @@ describe('paired session-tab agent completion notifications', () => {
vi.useFakeTimers()
mocks.observeAgentHookCompletionForNotification.mockReset()
resetWebSessionTabsSnapshotFreshnessForTests()
resetRendererOwnedAgentStatusPanesForTests()
useAppStore.setState(initialState, true)
})
afterEach(() => {
useAppStore.setState(initialState, true)
resetWebSessionTabsSnapshotFreshnessForTests()
resetRendererOwnedAgentStatusPanesForTests()
vi.useRealTimers()
})
@@ -131,6 +138,60 @@ describe('paired session-tab agent completion notifications', () => {
expect(dispatchCompletion).toHaveBeenCalledTimes(1)
})
it('announces a host-stamped turn while client OSC status owns the pane', () => {
const paneKey = makePaneKey(toWebTerminalSurfaceTabId(HOST_TAB_ID), LEAF_ID)
registerRendererOwnedAgentStatusPane(paneKey, ENVIRONMENT_ID)
markRendererOwnedAgentStatusWrite(paneKey)
useAppStore.setState({
agentStatusByPaneKey: {
[paneKey]: {
state: 'working',
prompt: 'review the PR',
updatedAt: NOW,
stateStartedAt: NOW,
agentType: 'claude',
paneKey,
tabId: toWebTerminalSurfaceTabId(HOST_TAB_ID),
worktreeId: WORKTREE_ID,
stateHistory: []
}
}
})
const dispatchCompletion = vi.fn()
const coordinator = createAgentCompletionCoordinator({
paneKey,
getPtyId: () => 'pty-1',
getSettings: () => null,
inspectProcess: vi.fn(),
dispatchCompletion,
isLive: () => true
})
mocks.observeAgentHookCompletionForNotification.mockImplementation(({ payload }) =>
coordinator.observeHookStatus(payload)
)
applySnapshot(makeAgentSnapshot(1, NOW - 2_000), true)
const turnCompletedAt = NOW + 20_000
applySnapshot(makeAgentSnapshot(2, NOW - 1_000, turnCompletedAt), true)
applySnapshot(makeAgentSnapshot(3, NOW, turnCompletedAt, 'done'), true)
expect(useAppStore.getState().agentStatusByPaneKey[paneKey]?.state).toBe('working')
expect(mocks.observeAgentHookCompletionForNotification).toHaveBeenCalledTimes(3)
expect(dispatchCompletion).toHaveBeenCalledTimes(1)
expect(dispatchCompletion).toHaveBeenCalledWith(
'claude',
expect.objectContaining({
source: 'hook',
quietedHookDone: false,
agentStatus: expect.objectContaining({
state: 'done',
prompt: 'review the PR',
turnCompletedAt
})
})
)
})
it('suppresses a stamped reconnect tail and its live all-clear', () => {
const dispatchCompletion = vi.fn()
const coordinator = createAgentCompletionCoordinator({
@@ -3431,7 +3431,7 @@ export function applyWebSessionTabsStorePatch(
useAppStore.setState((state) => {
const patch = buildPatch(state)
mirroredAgentStatusChanged = patch !== state && Object.hasOwn(patch, 'agentStatusByPaneKey')
if (mirroredAgentStatusChanged && agentStatusSnapshots) {
if (agentStatusSnapshots) {
const nextAgentStatuses = patch.agentStatusByPaneKey ?? state.agentStatusByPaneKey
const snapshots = Array.isArray(agentStatusSnapshots)
? agentStatusSnapshots
@@ -3443,30 +3443,40 @@ export function applyWebSessionTabsStorePatch(
}
const remapped = remapHostAgentStatus(surface)
const accepted = remapped ? nextAgentStatuses[remapped.paneKey] : undefined
const turnCompletedAt = remapped
? normalizeTurnCompletedAtField(surface.turnCompletedAt, remapped.state)
: undefined
// Why: client OSC owns display state, but only the host hook stream carries background-turn stamps.
const clientOwnedNotification = Boolean(
remapped &&
isClientAuthoritativeAgentStatusPane(remapped.paneKey) &&
(remapped.state === 'working' || turnCompletedAt !== undefined)
)
if (
!remapped ||
!accepted ||
accepted === state.agentStatusByPaneKey[remapped.paneKey] ||
!agentStatusEntryEqual(accepted, remapped)
(!clientOwnedNotification &&
(!accepted ||
accepted === state.agentStatusByPaneKey[remapped.paneKey] ||
!agentStatusEntryEqual(accepted, remapped)))
) {
continue
}
const turnCompletedAt = normalizeTurnCompletedAtField(
surface.turnCompletedAt,
accepted.state
)
if (!allowCompletionNotification && accepted.state !== 'working') {
const notificationStatus = clientOwnedNotification ? remapped : accepted
if (!notificationStatus) {
continue
}
if (!allowCompletionNotification && notificationStatus.state !== 'working') {
continue
}
acceptedNotificationStatuses.push({
paneKey: accepted.paneKey,
worktreeId: accepted.worktreeId ?? snapshot.worktree,
paneKey: notificationStatus.paneKey,
worktreeId: notificationStatus.worktreeId ?? snapshot.worktree,
payload: {
...pickParsedAgentStatusPayload({
...accepted,
...notificationStatus,
...(turnCompletedAt !== undefined ? { turnCompletedAt } : {})
}),
stateStartedAt: accepted.stateStartedAt
stateStartedAt: notificationStatus.stateStartedAt
}
})
}