fix(agents): clear the unread completion marker when acknowledging agents (#17924)

Acknowledging is one action against two records, but only
clearTerminalPaneUnread cleared unreadAgentCompletionPanes. Acking from
the Activity page, the dashboard drawer or the popout bridge left the tab
dot, the ⌘J row and the floating-workspace dot lit with nothing left to
read; only the terminal-view auto-ack path cleared both.

Cleared inside the existing set so one ack is one commit, and only the
agent marker is touched — clearTerminalPaneUnread also drops
unreadTerminalPanes, which would silence a BEL the user never saw.

Refs #15445 (step 2 of that issue's fix; steps 1 and 3 remain open).

Co-authored-by: kaluli123123 <295758798+kaluli123123@users.noreply.github.com>
This commit is contained in:
Neil
2026-09-01 17:15:15 -07:00
committed by GitHub
co-authored by kaluli123123
parent 80a52bb9b3
commit 1e82f66e80
3 changed files with 46 additions and 1 deletions
@@ -117,3 +117,31 @@ describe('acknowledgedAgentsByPaneKey cleanup on teardown', () => {
expect(ackAt < newEntry.stateStartedAt).toBe(true)
})
})
// Why: only the terminal-view path cleared unreadAgentCompletionPanes, so an
// Activity-page ack left the tab dot lit.
describe('acknowledgeAgents clears the unread agent-completion marker', () => {
it('drops the pane from unreadAgentCompletionPanes', () => {
const store = createTestStore()
store.getState().setAgentStatus('tab-1:0', { state: 'done', prompt: 'p', agentType: 'claude' })
store.getState().markAgentCompletionPaneUnread('tab-1:0')
expect(store.getState().unreadAgentCompletionPanes['tab-1:0']).toBe(true)
store.getState().acknowledgeAgents(['tab-1:0'])
expect(store.getState().unreadAgentCompletionPanes['tab-1:0']).toBeUndefined()
})
it('leaves other panes and the terminal-bell unread map untouched', () => {
const store = createTestStore()
store.getState().markAgentCompletionPaneUnread('tab-1:0')
store.getState().markAgentCompletionPaneUnread('tab-2:0')
store.getState().markTerminalPaneUnread('tab-1:0')
store.getState().acknowledgeAgents(['tab-1:0'])
expect(store.getState().unreadAgentCompletionPanes['tab-2:0']).toBe(true)
// Why: a BEL is a separate signal; acking the agent must not silence it.
expect(store.getState().unreadTerminalPanes['tab-1:0']).toBe(true)
})
})
@@ -20,6 +20,8 @@ export function createUIStore(): StoreApi<AppState> {
combinedDiffFileTreeWidth: 256,
rightSidebarTab: 'explorer',
rightSidebarExplorerView: 'files',
// Why: acknowledgeAgents clears the agent-completion marker the terminal slice owns.
unreadAgentCompletionPanes: {},
...createSettingsSearchState(args[0]),
...createWorktreeNavHistorySlice(...(args as Parameters<typeof createWorktreeNavHistorySlice>)),
...createUISlice(...(args as Parameters<typeof createUISlice>))
@@ -217,7 +217,16 @@ export function createUiAgentActions(
const migrationUnsupported = Object.values(s.migrationUnsupportedByPtyId ?? {})
// Why: only reallocate if an ack advances; compare prev<stamp not !== — the stamp ticks every ms and !== would rewrite the map every call.
let next: Record<string, number> | null = null
// Why: one ack, two records — leaving the completion marker set keeps the tab dot,
// the ⌘J row and the floating-workspace dot lit with nothing left to read.
let nextUnreadCompletions: Record<string, true> | null = null
for (const key of paneKeys) {
if (s.unreadAgentCompletionPanes[key]) {
if (nextUnreadCompletions === null) {
nextUnreadCompletions = { ...s.unreadAgentCompletionPanes }
}
delete nextUnreadCompletions[key]
}
const prev = s.acknowledgedAgentsByPaneKey[key] ?? 0
// Why not plain Date.now(): a remote/SSH execution host can stamp a turn ahead of this clock,
// and every unread rule is `ackAt < turnTimestamp`. A behind-the-turn ack can never clear the
@@ -258,7 +267,13 @@ export function createUiAgentActions(
next[key] = stamp
}
}
return next ? { acknowledgedAgentsByPaneKey: next } : s
if (!next && !nextUnreadCompletions) {
return s
}
return {
...(next ? { acknowledgedAgentsByPaneKey: next } : {}),
...(nextUnreadCompletions ? { unreadAgentCompletionPanes: nextUnreadCompletions } : {})
}
})
const notificationIds = [...notificationIdsToDismiss]
if (notificationIds.length > 0 && typeof window !== 'undefined') {