The host raises the shared unread dot whatever each client's Filters say (dark) (#30581)

* fix(sidebar): the host raises the shared unread dot regardless of Filters (dark)

* fix(sidebar): tighten the shared-dot comment and tests after review
This commit is contained in:
Jinwoo Hong
2026-10-19 01:58:52 -04:00
committed by GitHub
parent 6c40f9190d
commit 7395d30dc7
7 changed files with 31 additions and 54 deletions
@@ -48,7 +48,6 @@ export abstract class OrcaRuntimeWithSidebarTemporal extends OrcaRuntimeWithWork
subscribeToPtyExit: (ptyId, listener) => this.subscribeToPtyExit(ptyId, listener),
onClientEvent: (listener, options) => this.onClientEvent(listener, options),
isViewing: (worktreeId, paneKey) => this.workspaceViewers.isViewing(worktreeId, paneKey),
isNotificationMuted: (worktreeId) => this.isWorkspaceNotificationMuted(worktreeId),
onStampsWritten: (worktreeId) => this.notifySortStampsChanged(worktreeId),
now: Date.now
})
@@ -46,7 +46,6 @@ function createHarness() {
let listener: (event: RuntimeClientEvent) => void = () => {}
// Stamping tests watch every workspace, so no unread flag is raised; the unread tests unwatch.
const viewing = { isViewing: (_id: string, _pane: string | null): boolean => true }
const muted = new Set<string>()
const store: SortStampsStore = {
getWorktreeMeta: (id) => meta.get(id),
setWorktreeMeta: (id, updates) => {
@@ -83,7 +82,6 @@ function createHarness() {
return () => {}
},
isViewing: (id, pane) => viewing.isViewing(id, pane),
isNotificationMuted: (id) => muted.has(id),
onStampsWritten: (id) => notified.push(id),
now: () => Date.now()
})
@@ -102,7 +100,6 @@ function createHarness() {
fenced,
notified,
viewing,
muted,
sideEffects: (
facts: TerminalSideEffectFact[],
extra: { pane?: string; ptyId?: string; connectionId?: string } = {}
@@ -632,30 +629,6 @@ describe('SortStampsProducer: unread flag', () => {
expect(h.folders.get(FOLDER_ID)?.isUnread).toBe(true)
})
it('raises no flag for a muted workspace, but still stamps it', () => {
const h = unwatched()
h.muted.add(WT)
h.muted.add(FOLDER)
hook(h.server, 'working')
advance(20_000)
hook(h.server, 'done')
h.sideEffects([{ kind: 'bell' }])
expect(h.meta.get(WT)?.isUnread).toBeUndefined()
expect(h.meta.get(WT)?.lastActivityAt).toBeGreaterThan(0)
h.server.ingestTerminalStatus({
paneKey: PANE,
tabId: 'tab-a',
worktreeId: FOLDER,
payload: { state: 'waiting', prompt: 'task', agentType: 'codex' }
})
expect(h.folders.get(FOLDER_ID)?.isUnread).toBeUndefined()
h.muted.delete(WT)
h.sideEffects([{ kind: 'bell' }])
expect(h.meta.get(WT)?.isUnread).toBe(true)
})
it('raises nothing for a restored status, a frozen project or a workspace with no record', () => {
const h = createHarness()
h.viewing.isViewing = () => false
@@ -49,8 +49,6 @@ export type SortStampsSources = {
) => () => void
/** Whether a client is looking at the workspace, or at the pane's tab. */
isViewing: (worktreeId: string, paneKey: string | null) => boolean
/** "Notify About Hidden Workspaces" is off and the Filters hide it: news raises no flag. */
isNotificationMuted: (worktreeId: string) => boolean
/** A record's stamps or unread flag changed; clients re-list it. */
onStampsWritten: (worktreeId: string) => void
now: () => number
@@ -63,8 +61,8 @@ type TitlePane = { ptyId: string; releaseExit: () => void }
* Writes the sort stamps of every workspace this host owns, at the source: the transition
* detector's hook panes (terminal agents and structured chats), hookless agents' titles, bells,
* and terminals a CLI starts. Creation stamps itself where the record is made. Rest panes are not
* kept. The same news raises the workspace's unread flag unless a client is looking at it or its
* notifications are muted; clients clear it.
* kept. The same news raises the workspace's unread flag unless a client is looking at it; clients
* clear it.
*/
export class SortStampsProducer {
private readonly panes = new Map<string, TrackedPane>()
@@ -301,10 +299,10 @@ export class SortStampsProducer {
if (next) {
writeSortStamps(this.sources.store(), workspace, next)
}
// Filters never gate this shared dot (clients' Filters differ); banners, sounds and pushes mute per surface.
const raised =
isUnreadNews(event) &&
!this.sources.isViewing(workspace.worktreeId, paneKey) &&
!this.sources.isNotificationMuted(workspace.worktreeId) &&
writeUnreadFlag(this.sources.store(), workspace, true)
if (next || raised) {
this.sources.onStampsWritten(workspace.worktreeId)
@@ -1,5 +1,6 @@
// The host's read of "Notify About Hidden Workspaces": with it off, news from a workspace the
// desktop's sidebar Filters switches hide raises nothing the host decides (phone push, unread flag).
// desktop's sidebar Filters switches hide sends no phone push from the host. The shared unread dot
// ignores it: clients' Filters differ.
// The setting and the switches are this host's own persisted state; a paired client's never apply.
//
// WHY NOT "HIDE SLEEPING": main sees the persisted switches and its in-memory workspace records,
@@ -198,7 +198,7 @@ export abstract class OrcaRuntimeWithGetWorktreePs extends OrcaRuntimeWithWorktr
})
}
/** Host decisions (phone push, unread flag) raise nothing for this workspace; see the module. */
/** The host pushes no phone notification for this workspace; see the module. */
isWorkspaceNotificationMuted(workspaceId: string): boolean {
const store = this.store
return (
@@ -4,6 +4,7 @@ import { execInTerminal, waitForActivePanePtyId } from './helpers/terminal'
import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
import { getRendererTitleLog, installRendererTitleLog } from './helpers/terminal-title-log'
import { waitForPtyShellEcho } from './terminal-pty-readiness'
import { SIDEBAR_INPUTS_BUILD } from './helpers/sidebar-inputs-build'
type NotificationDispatch = { source?: string; worktreeId?: string }
@@ -117,12 +118,16 @@ test.describe('Hidden workspace notifications', () => {
await expect.poll(() => bellRequests(electronApp, worktreeId), { timeout: 5_000 }).toBe(1)
expect(await isWorkspaceUnread(orcaPage, worktreeId)).toBe(true)
// Hidden and the setting off: no unread dot and no request, past the bell's 250 ms delay.
// Hidden and the setting off: no request, past the bell's 250 ms delay. The window raises no
// dot; a switch-on host raises its shared dot, which ignores every client's Filters.
await setFilterAndSetting(orcaPage, true, false)
await ringBellOffScreen(orcaPage, ptyId, worktreeId, 'hwn-hidden')
if (SIDEBAR_INPUTS_BUILD) {
await expect.poll(() => isWorkspaceUnread(orcaPage, worktreeId)).toBe(true)
}
await orcaPage.waitForTimeout(1_500)
expect(await bellRequests(electronApp, worktreeId)).toBe(1)
expect(await isWorkspaceUnread(orcaPage, worktreeId)).toBe(false)
expect(await isWorkspaceUnread(orcaPage, worktreeId)).toBe(SIDEBAR_INPUTS_BUILD)
// Hidden with "Notify About Hidden Workspaces" on: both come back.
await setFilterAndSetting(orcaPage, true, true)
+18 -17
View File
@@ -80,7 +80,7 @@ test('the host raises the unread flag where the window’s user is not looking,
await expect.poll(unread).toBe(true)
})
test('the host raises no flag for a workspace the Filters hide while hidden workspaces stay quiet', async ({
test('the host raises the flag even for a workspace the Filters hide while hidden workspaces stay quiet', async ({
orcaPage,
electronApp
}) => {
@@ -94,20 +94,15 @@ test('the host raises no flag for a workspace the Filters hide while hidden work
const rowKey = `local|${pane.worktreeId}`
const unread = async (): Promise<boolean | undefined> =>
(await readHostRow(orcaPage, rowKey))?.isUnread
const setNotifyHidden = (notify: boolean): Promise<void> =>
orcaPage.evaluate(async (value) => {
const state = window.__store!.getState()
await state.updateSettings({
notifications: { ...state.settings!.notifications, notifyHiddenWorkspaces: value }
})
}, notify)
// The seeded workspace is the primary checkout, which "Hide default branch" hides.
await orcaPage.evaluate(() => {
await orcaPage.evaluate(async () => {
const state = window.__store!.getState()
state.setHideDefaultBranchWorkspace(true)
state.setActiveView('tasks')
await state.updateSettings({
notifications: { ...state.settings!.notifications, notifyHiddenWorkspaces: false }
})
})
await setNotifyHidden(false)
// The host reads the persisted switch, not the window's copy.
await expect
.poll(() =>
@@ -116,14 +111,20 @@ test('the host raises no flag for a workspace the Filters hide while hidden work
.toBe(true)
await expect.poll(unread).toBe(false)
// The flag is shared by every client, whatever its Filters; only banners and sounds are muted.
await postClaudeHook(endpoint, pane, { hook_event_name: 'UserPromptSubmit', prompt: 'go' })
await expect.poll(async () => (await readHostRow(orcaPage, rowKey))?.smartTier).toBe('working')
await postClaudeHook(endpoint, pane, { hook_event_name: 'Stop', last_assistant_message: 'ok' })
await expect.poll(async () => (await readHostRow(orcaPage, rowKey))?.smartTier).toBe('done')
expect(await unread()).toBe(false)
await setNotifyHidden(true)
await postClaudeHook(endpoint, pane, { hook_event_name: 'UserPromptSubmit', prompt: 'more' })
await postClaudeHook(endpoint, pane, { hook_event_name: 'Stop', last_assistant_message: 'ok' })
await expect.poll(unread).toBe(true)
// The window keeps the host's dot; its Filters mute only its own banner and sound.
await expect
.poll(() =>
orcaPage.evaluate(
(id) =>
Object.values(window.__store?.getState().worktreesByRepo ?? {})
.flat()
.find((worktree) => worktree.id === id)?.isUnread,
pane.worktreeId
)
)
.toBe(true)
})