Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type { StateCreator } from 'zustand'
import type { AgentAttentionUnreadReason } from '@/attention/agent-attention-contract'
import { emitAgentSubjectReads } from '@/attention/agent-subject-read-actions'
import type { AppState } from '../../types'
import type { WorkspaceAttentionSlice } from './workspace-attention-contract'
import {
collectRemovedAttention,
dismissReadNotifications,
sameMarkers
sameMarkers,
type RemovedAttention
} from './attention-removal-follow-up'
import { removeTabsAttention } from './subject-attention-transitions'
import {
Expand Down Expand Up @@ -58,8 +60,8 @@ export const createWorkspaceAttentionSlice: StateCreator<
: { unreadAgentCompletionPanes: { ...s.unreadAgentCompletionPanes, [paneKey]: reason } }
)
}
/** Removes every marker of the tabs and dismisses their panes' notifications; true if any. */
const removeAndDismiss = (tabIds: Iterable<string>): boolean => {
/** Removes every marker of the tabs and dismisses their panes' notifications. */
const removeAndDismiss = (tabIds: Iterable<string>): RemovedAttention | null => {
const before = get()
set((s) => {
const next = removeTabsAttention(s, tabIds)
Expand All @@ -69,7 +71,7 @@ export const createWorkspaceAttentionSlice: StateCreator<
if (removed) {
dismissReadNotifications(removed.paneKeys)
}
return removed !== null
return removed
}
const paneAttentionEnabled = (): boolean => get().settings?.experimentalTerminalAttention === true

Expand Down Expand Up @@ -106,9 +108,8 @@ export const createWorkspaceAttentionSlice: StateCreator<
}
},
readOnScreenAttention: ({ workspaceIds, targets }, { screenArrived }) => {
const removed = removeAndDismiss(targets.map((target) => target.tabId))
// Read marker-free flags on screen arrival (manual unread/restart/peer), not on every scan.
if (removed || screenArrived) {
if (removeAndDismiss(targets.map((target) => target.tabId)) !== null || screenArrived) {
for (const workspaceId of workspaceIds) {
settleWorkspaceUnreadFlag(set, get, workspaceId, onScreenHostId(get(), workspaceId))
}
Expand All @@ -117,7 +118,11 @@ export const createWorkspaceAttentionSlice: StateCreator<
setWorkspaceUnreadByUser: (worktreeId, isUnread, executionHostId) => {
if (!isUnread) {
noteWorkspaceRead(worktreeId)
removeAndDismiss(workspaceMarkerTabIds(get(), worktreeId))
const removed = removeAndDismiss(workspaceMarkerTabIds(get(), worktreeId))
// Journal-positioned chat alerts retire only with the explicit read that bounds them.
if (removed) {
emitAgentSubjectReads([...removed.paneKeys], undefined, 'explicit')
}
}
// Update only the clicked host’s row; markers stay workspace-wide because tab keys omit host.
void get().updateWorktreeMeta(worktreeId, { isUnread }, { executionHostId })
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as WorktreeMetaPersistModule from '../worktrees/metadata/worktree-meta-persist'
import { structuredAgentSessionPaneKey } from '../../../../../shared/structured-agent-session-projection'
import {
registerAgentSubjectReadCapture,
subscribeAgentSubjectReads,
type AgentSubjectRead,
type AgentSubjectReadIntent,
type StructuredSubjectRead
} from '@/attention/agent-subject-read-actions'
import { makeWorktree } from '../worktrees-slice-test-fixtures'
import {
completeAgentTurn,
Expand Down Expand Up @@ -288,6 +296,40 @@ describe('the flag follows its markers', () => {
expect(dismissedPaneKeys()).toEqual([[first.paneKey, second.paneKey]])
})

it('Mark as read emits an explicit read for each cleared chat', () => {
const { store } = createAttentionStore()
const chat = store
.getState()
.createUnifiedTab(WS_A, 'agent-session', { id: 'chat-a1', entityId: 'session-1' })
const paneKey = structuredAgentSessionPaneKey(chat.id, chat.entityId)
store.getState().markAgentAttentionUnread({
workspaceId: WS_A,
subjectKey: paneKey,
groupId: null,
reason: 'agent-completion'
})
const structured: StructuredSubjectRead = {
sessionId: 'session-1',
target: { kind: 'local' },
observedCursor: { epoch: 'epoch-1', sequence: 10 },
observationKey: 'edge:epoch-1:10'
}
const intents: AgentSubjectReadIntent[] = []
const emitted: AgentSubjectRead[] = []
const stopCapture = registerAgentSubjectReadCapture(paneKey, (intent) => {
intents.push(intent)
return structured
})
const stopReads = subscribeAgentSubjectReads((reads) => emitted.push(...reads))

toggleUnreadByUser(store, store.getState().worktreesByRepo.repo1[0]!)
stopReads()
stopCapture()

expect(intents).toEqual(['explicit'])
expect(emitted).toEqual([{ subjectKey: paneKey, structured }])
})

it('closing an unmarked tab keeps a manual unread; closing a marked one dismisses its panes', () => {
const { store, persists } = createAttentionStore()
const marked = addLiveTerminalTab(store, WS_A, 'tab-a1')
Expand Down
Loading