fix(desktop): key the completed-unread dot on the focused session, not the selected one
This commit is contained in:
@@ -182,7 +182,9 @@ function handleTransition(previous: ClientSessionState | null, next: ClientSessi
|
||||
} else if (!next.busy && wasWorking) {
|
||||
markSettled(storedId)
|
||||
|
||||
if (storedId !== $selectedStoredSessionId.get()) {
|
||||
// FOCUSED, not selected: a session finishing in the tile the user is
|
||||
// watching is already seen, and a tile is never the primary selection.
|
||||
if (storedId !== $focusedStoredSessionId.get()) {
|
||||
const cur = $unreadFinishedSessionIds.get()
|
||||
|
||||
if (!cur.includes(storedId)) {
|
||||
@@ -912,6 +914,18 @@ export const $focusedSessionState = computed([$focusedRuntimeId, $sessionStates]
|
||||
export const selectionHomesToWorkspace = (selected: null | string, tiles: readonly SessionTile[]): boolean =>
|
||||
!(selected && tiles.some(t => t.storedSessionId === selected))
|
||||
|
||||
// Bringing a finished session to the front clears its green dot. Keyed on the
|
||||
// FOCUSED session, not the selected one: a tile is never $selectedStoredSessionId,
|
||||
// and a tile tab click goes through activateTreePane rather than focusOpenSession,
|
||||
// so this is the one hook that catches every way a tile reaches the front.
|
||||
$focusedStoredSessionId.listen(focused => {
|
||||
const cur = $unreadFinishedSessionIds.get()
|
||||
|
||||
if (focused && cur.includes(focused)) {
|
||||
$unreadFinishedSessionIds.set(cur.filter(id => id !== focused))
|
||||
}
|
||||
})
|
||||
|
||||
// Cold-start restore is the one selection change that is NOT a navigation: the
|
||||
// route already pointed at the primary session before the window loaded, and
|
||||
// homing on it would front the workspace tab over the PERSISTED active tab —
|
||||
|
||||
84
apps/desktop/src/store/session-unread-tile.test.ts
Normal file
84
apps/desktop/src/store/session-unread-tile.test.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// The completed-unread dot is keyed on the FOCUSED session, not the selected
|
||||
// one. A tile is never $selectedStoredSessionId, so keying either half on the
|
||||
// selection left a tiled session's dot green with no way to clear it.
|
||||
|
||||
describe('completed-unread dot follows the focused session', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
async function setup() {
|
||||
const tree = await import('@/components/pane-shell/tree/store')
|
||||
const model = await import('@/components/pane-shell/tree/model')
|
||||
const { registry } = await import('@/contrib/registry')
|
||||
const { createClientSessionState } = await import('@/lib/chat-runtime')
|
||||
const session = await import('./session')
|
||||
const states = await import('./session-states')
|
||||
|
||||
for (const id of ['workspace', 'session-tile:tiled']) {
|
||||
registry.register({
|
||||
area: 'panes',
|
||||
data: id === 'workspace' ? { placement: 'main', uncloseable: true } : { placement: 'main' },
|
||||
id,
|
||||
render: () => null,
|
||||
title: id
|
||||
})
|
||||
}
|
||||
|
||||
// The workspace holds the primary chat, a second zone holds the tile.
|
||||
tree.declareDefaultTree(
|
||||
model.split('row', [
|
||||
model.group(['workspace'], { active: 'workspace', id: 'grp-main' }),
|
||||
model.group(['session-tile:tiled'], { active: 'session-tile:tiled', id: 'grp-tile' })
|
||||
])
|
||||
)
|
||||
|
||||
session.$unreadFinishedSessionIds.set([])
|
||||
session.$selectedStoredSessionId.set('primary')
|
||||
|
||||
const finishTurn = (storedSessionId: string) => {
|
||||
const working = { ...createClientSessionState(null), busy: true, storedSessionId }
|
||||
states.publishSessionState(`rt-${storedSessionId}`, working)
|
||||
states.publishSessionState(`rt-${storedSessionId}`, { ...working, busy: false })
|
||||
}
|
||||
|
||||
return { finishTurn, session, tree }
|
||||
}
|
||||
|
||||
it('clears the dot when an already-open tile is fronted', async () => {
|
||||
const { finishTurn, session, tree } = await setup()
|
||||
|
||||
tree.noteActiveTreeGroup('grp-main')
|
||||
finishTurn('tiled')
|
||||
expect(session.$unreadFinishedSessionIds.get()).toEqual(['tiled'])
|
||||
|
||||
// Fronting the tile is what a tab click does. Before the fix nothing on
|
||||
// this path cleared the marker, so the dot stayed green.
|
||||
tree.noteActiveTreeGroup('grp-tile')
|
||||
expect(session.$unreadFinishedSessionIds.get()).toEqual([])
|
||||
})
|
||||
|
||||
it('never marks a tile that finishes while it is the focused one', async () => {
|
||||
const { finishTurn, session, tree } = await setup()
|
||||
|
||||
tree.noteActiveTreeGroup('grp-tile')
|
||||
finishTurn('tiled')
|
||||
|
||||
expect(session.$unreadFinishedSessionIds.get()).toEqual([])
|
||||
})
|
||||
|
||||
it('marks the primary session when a tile has focus', async () => {
|
||||
const { finishTurn, session, tree } = await setup()
|
||||
|
||||
tree.noteActiveTreeGroup('grp-tile')
|
||||
finishTurn('primary')
|
||||
|
||||
expect(session.$unreadFinishedSessionIds.get()).toEqual(['primary'])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user