mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
Deduplicate activity unread counts across source overlaps
Live pane status is the primary source; retained and migration entries serve as fallback caches that may briefly overlap it during lifecycle transitions. Count each pane only once by tracking seen keys, prioritizing the live status as the canonical source. Also fix monitoring state display: it's a distinct agent state, not a tool-running row state, so exclude it from tool preview checks.
This commit is contained in:
@@ -143,13 +143,15 @@ export function activityThreadRowCopy(thread: AgentPaneThread): ActivityThreadRo
|
||||
responsePreview: thread.responsePreview
|
||||
})
|
||||
const liveState = thread.currentAgentState ?? thread.latestEvent?.state ?? null
|
||||
const toolPreviewState = liveState === 'monitoring' ? null : liveState
|
||||
const state = threadAgentState(thread)
|
||||
const needsAttention = state === 'waiting' || state === 'blocked' || state === 'permission'
|
||||
if (renderedPreview && !previewDuplicatesIdentity(renderedPreview, taskTitle, workspaceLabel)) {
|
||||
return {
|
||||
taskTitle,
|
||||
statusLine: renderedPreview,
|
||||
statusKind: showsAgentToolPreview(liveState) ? 'tool' : 'message',
|
||||
// Monitoring is a distinct live state, not a tool-running row state.
|
||||
statusKind: showsAgentToolPreview(toolPreviewState) ? 'tool' : 'message',
|
||||
needsAttention,
|
||||
workspaceLabel
|
||||
}
|
||||
|
||||
@@ -88,3 +88,24 @@ describe('countActivityUnread with Clear completed cutoffs', () => {
|
||||
expect(countActivityUnread(source)).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('countActivityUnread source overlap', () => {
|
||||
it('counts an overlapping live and retained pane only once', () => {
|
||||
const entry = makeEntry({})
|
||||
const source = {
|
||||
acknowledgedAgentsByPaneKey: { [PANE]: 0 },
|
||||
agentStatusByPaneKey: { [PANE]: entry },
|
||||
retainedAgentsByPaneKey: {
|
||||
[PANE]: {
|
||||
entry,
|
||||
worktreeId: 'wt-1',
|
||||
tab: {} as never,
|
||||
agentType: 'claude',
|
||||
startedAt: 1_000
|
||||
}
|
||||
},
|
||||
migrationUnsupportedByPtyId: {}
|
||||
}
|
||||
expect(countActivityUnread(source)).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -24,6 +24,7 @@ function isUnreadAgentState(state: AgentStatusState): boolean {
|
||||
/** Counts unread done/blocked/waiting events for the Activity page titlebar badge. */
|
||||
export function countActivityUnread(source: ActivityUnreadCountSource): number {
|
||||
let count = 0
|
||||
const seenPaneKeys = new Set<string>()
|
||||
|
||||
// Why no worktree.isUnread here: Activity lists only agent threads, so a worktree
|
||||
// unread would light a badge with no row to read and no way to clear it.
|
||||
@@ -50,14 +51,21 @@ export function countActivityUnread(source: ActivityUnreadCountSource): number {
|
||||
}
|
||||
|
||||
for (const [paneKey, entry] of Object.entries(source.agentStatusByPaneKey)) {
|
||||
seenPaneKeys.add(paneKey)
|
||||
countEntry(entry, source.acknowledgedAgentsByPaneKey[paneKey] ?? 0)
|
||||
}
|
||||
for (const [paneKey, retained] of Object.entries(source.retainedAgentsByPaneKey)) {
|
||||
// Live status is the primary source; retained is a handoff cache and may briefly overlap it.
|
||||
if (seenPaneKeys.has(paneKey)) {
|
||||
continue
|
||||
}
|
||||
seenPaneKeys.add(paneKey)
|
||||
countEntry(retained.entry, source.acknowledgedAgentsByPaneKey[paneKey] ?? 0)
|
||||
}
|
||||
for (const unsupported of Object.values(source.migrationUnsupportedByPtyId)) {
|
||||
const entry = migrationUnsupportedToAgentStatusEntry(unsupported)
|
||||
if (entry) {
|
||||
if (entry && !seenPaneKeys.has(entry.paneKey)) {
|
||||
seenPaneKeys.add(entry.paneKey)
|
||||
countEntry(entry, source.acknowledgedAgentsByPaneKey[entry.paneKey] ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user