mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user