This commit is contained in:
Jinjing
2026-11-13 18:25:09 -08:00
parent aa9564793b
commit c7c249b485
4 changed files with 11 additions and 46 deletions
@@ -1,7 +1,5 @@
import type { StoredAgentAttentionUnread } from '@/attention/agent-attention-contract'
import { emitAgentSubjectReads } from '@/attention/agent-subject-read-actions'
import type { AppState } from '@/store/types'
import { paneKeyTabId } from '@/store/slices/workspace-attention/workspace-live-markers'
import type { UISlice, UISliceGet, UISliceSet } from './ui-slice-contract'
import {
collectAcknowledgedAgentNotificationId,
@@ -21,34 +19,11 @@ type ActivityActions = Pick<
| 'clearManuallyUnreadTurns'
>
/** Workspaces holding these panes: terminal tabs, structured chat tabs, or the agent's own record. */
function workspaceIdsOfPaneKeys(s: AppState, paneKeys: readonly string[]): Set<string> {
const ids = new Set<string>()
for (const paneKey of paneKeys) {
const tabId = paneKeyTabId(paneKey)
for (const worktreeId of [
resolvePaneKeyWorktreeIdFromTabs(s, paneKey),
s.agentStatusByPaneKey?.[paneKey]?.worktreeId,
s.retainedAgentsByPaneKey?.[paneKey]?.worktreeId,
...Object.entries(s.unifiedTabsByWorktree ?? {})
.filter(([, tabs]) => tabs.some((tab) => tab.id === tabId))
.map(([worktreeId]) => worktreeId)
]) {
if (worktreeId) {
ids.add(worktreeId)
}
}
}
return ids
}
export function createUiActivityActions(set: UISliceSet, get: UISliceGet): ActivityActions {
export function createUiActivityActions(set: UISliceSet, _get: UISliceGet): ActivityActions {
return {
acknowledgedAgentsByPaneKey: {},
acknowledgeAgents: (paneKeys, reads, intent) => {
emitAgentSubjectReads(paneKeys, reads, intent)
const readWorkspaceIds =
intent === 'explicit' ? workspaceIdsOfPaneKeys(get(), paneKeys) : null
const notificationIdsToDismiss = new Set<string>()
set((s) => {
if (paneKeys.length === 0) {
@@ -116,10 +91,6 @@ export function createUiActivityActions(set: UISliceSet, get: UISliceGet): Activ
...(nextManual ? { manuallyUnreadTurnsByPaneKey: nextManual } : {})
}
})
// Explicit reads skip the on-screen scan that would otherwise lower the workspace flag.
if (readWorkspaceIds && readWorkspaceIds.size > 0) {
get().settleWorkspacesAfterExplicitRead(readWorkspaceIds)
}
// Dismiss main’s announced subjects; rebuilt IDs cover restart-empty notification records.
if (paneKeys.length > 0 && typeof window !== 'undefined') {
void window.api?.notifications?.dismiss?.([...notificationIdsToDismiss], paneKeys)
@@ -43,6 +43,4 @@ export type WorkspaceAttentionSlice = {
isUnread: boolean,
executionHostId: ExecutionHostId
) => void
/** An explicit read elsewhere (Activity, dashboard, a jump) lowers flags left with nothing unread behind them. */
settleWorkspacesAfterExplicitRead: (worktreeIds: Iterable<string>) => void
}
@@ -145,7 +145,6 @@ export const createWorkspaceAttentionSlice: StateCreator<
noteWorkspaceRead(worktreeId)
const removed = removeAndDismiss(workspaceMarkerTabIds(get(), worktreeId))
// Update only the clicked host’s row; markers stay workspace-wide because tab keys omit host.
// Before the acknowledgement below, so its settle finds the flag already down and skips a second persist.
void get().updateWorktreeMeta(worktreeId, { isUnread }, { executionHostId })
// Mark read reads the agents too, or their lines keep the dot the user just cleared.
// Journal-positioned chat alerts retire only with this explicit read that bounds them.
@@ -156,15 +155,6 @@ export const createWorkspaceAttentionSlice: StateCreator<
if (paneKeys.size > 0) {
get().acknowledgeAgents([...paneKeys], undefined, 'explicit')
}
},
settleWorkspacesAfterExplicitRead: (worktreeIds) => {
const marked = get().workspacesMarkedUnreadByUser
for (const worktreeId of new Set(worktreeIds)) {
// The user's own mark is a source of its own; reading an agent there doesn't answer it.
if (!marked[worktreeId]) {
settleWorkspaceUnreadFlag(set, get, worktreeId, onScreenHostId(get(), worktreeId))
}
}
}
}
}
@@ -12,6 +12,7 @@ import {
} from './workspace-unread-characterization-fixture'
import { completeAgentTurn, toggleUnreadByUser } from './workspace-unread-characterization-driver'
import { isWorkspaceFlagSet } from './workspace-unread-flag'
import { selectAgentRowsUnread } from '@/components/sidebar/workspace-card/activity/worktree-card-agent-ack-inputs'
vi.mock('sonner', () => ({
toast: { info: vi.fn(), success: vi.fn(), error: vi.fn(), warning: vi.fn(), dismiss: vi.fn() }
@@ -51,7 +52,7 @@ function finishedAgentOffScreen(store: AttentionStore): PaneTarget {
return pane
}
describe('explicit reads keep the workspace flag and its agents in step', () => {
describe('explicit workspace and agent reads', () => {
it('Mark read also reads the agents, so their lines lose the dot with the flag', () => {
const { store } = createAttentionStore()
const pane = finishedAgentOffScreen(store)
@@ -61,16 +62,21 @@ describe('explicit reads keep the workspace flag and its agents in step', () =>
toggleUnreadByUser(store, row)
expect(isWorkspaceFlagSet(store.getState(), WS_A)).toBe(false)
expect(store.getState().acknowledgedAgentsByPaneKey[pane.paneKey]).toBeGreaterThanOrEqual(900)
expect(selectAgentRowsUnread(store.getState(), [pane])).toEqual([false])
})
it('reading the only unread agent elsewhere lowers the flag instead of leaving a phantom dot', () => {
it('reading an agent elsewhere preserves a workspace marked unread before restart', () => {
const { store: previousStore } = createAttentionStore()
previousStore.getState().setWorkspaceUnreadByUser(WS_A, true, 'local')
const { store } = createAttentionStore()
store.setState({ worktreesByRepo: previousStore.getState().worktreesByRepo })
const pane = finishedAgentOffScreen(store)
expect(isWorkspaceFlagSet(store.getState(), WS_A)).toBe(true)
expect(selectAgentRowsUnread(store.getState(), [pane])).toEqual([true])
store.getState().acknowledgeAgents([pane.paneKey], undefined, 'explicit')
expect(isWorkspaceFlagSet(store.getState(), WS_A)).toBe(false)
expect(isWorkspaceFlagSet(store.getState(), WS_A)).toBe(true)
expect(selectAgentRowsUnread(store.getState(), [pane])).toEqual([false])
})
})