Repository navigation
feat(web-shell): Consume live-state session activity timestamps #9476
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ import type { | |
| DaemonSessionListPage, | ||
| } from '@qwen-code/sdk/daemon'; | ||
| import type { SessionCatalogQuery } from './session-catalog-store'; | ||
| import { getSessionCatalogStore } from './session-catalog-store'; | ||
|
|
||
| interface Deferred<T> { | ||
| promise: Promise<T>; | ||
|
|
@@ -52,9 +53,11 @@ vi.mock('@qwen-code/webui/daemon-react-sdk', () => ({ | |
| useWorkspace: () => mocks.workspace, | ||
| })); | ||
|
|
||
| const { useSessionCatalogQuery, useWebShellSessions } = await import( | ||
| './session-catalog-hooks' | ||
| ); | ||
| const { | ||
| useSessionCatalogQuery, | ||
| useSessionCatalogController, | ||
| useWebShellSessions, | ||
| } = await import('./session-catalog-hooks'); | ||
|
|
||
| let root: Root; | ||
| let container: HTMLDivElement; | ||
|
|
@@ -214,4 +217,36 @@ describe('session catalog hooks', () => { | |
| }); | ||
| expect(result).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('routes turn completions by live-state ownership', () => { | ||
| const client = mocks.workspace.client as DaemonClient; | ||
| const store = getSessionCatalogStore(client); | ||
| let controller: ReturnType<typeof useSessionCatalogController> | undefined; | ||
|
|
||
| function ControllerProbe() { | ||
| controller = useSessionCatalogController(client); | ||
| return null; | ||
| } | ||
|
|
||
| act(() => root.render(<ControllerProbe />)); | ||
| const record = vi.spyOn(store, 'recordSessionActivity'); | ||
| const invalidate = vi.spyOn(store, 'invalidateWorkspace'); | ||
| const schedule = vi.spyOn(store, 'scheduleWorkspaceRefresh'); | ||
|
|
||
| // Without live-state ownership the legacy rescan path stays. | ||
| act(() => controller!.turnCompleted('/w', 'sess-1')); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The new hooks test leaks a real 2-second timer and three unrestored spies, breaking the teardown convention its sibling suites follow. This file has no Reviewed with Claude Code (claude-opus-5, 1M context) — |
||
| expect(invalidate).toHaveBeenCalledWith('/w'); | ||
| expect(schedule).toHaveBeenCalledWith('/w'); | ||
| expect(store.snapshotSessionActivity('/w')).toBeUndefined(); | ||
|
|
||
| invalidate.mockClear(); | ||
| schedule.mockClear(); | ||
| const releaseLiveState = store.retainWorkspaceLiveState('/w'); | ||
| act(() => controller!.turnCompleted('/w', 'sess-1')); | ||
| expect(record).toHaveBeenCalledWith('/w', 'sess-1'); | ||
| expect(store.snapshotSessionActivity('/w')?.get('sess-1')).toBeDefined(); | ||
| expect(invalidate).not.toHaveBeenCalled(); | ||
| expect(schedule).not.toHaveBeenCalled(); | ||
| releaseLiveState(); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -218,13 +218,13 @@ export function useSessionCatalogController(client: DaemonClient) { | |
| store.invalidateWorkspace(workspaceCwd); | ||
| }); | ||
| }, | ||
| turnCompleted(workspaceCwd: string) { | ||
| turnCompleted(workspaceCwd: string, sessionId: string) { | ||
| update(() => { | ||
| if (store.isWorkspaceLiveStateEnabled(workspaceCwd)) { | ||
| // The catalog revision doesn't advance on turn completion and | ||
| // the live-state overlay carries no updatedAt — flag a | ||
| // rate-limited reconcile so activity stamps keep refreshing. | ||
| store.requestWorkspaceLiveStateInvalidation(workspaceCwd); | ||
| // The catalog revision doesn't advance on turn completion; record | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This PR is the "follow-up consumer" the design doc describes, but the doc still states the consumer is unimplemented.
Reviewed with Claude Code (claude-opus-5, 1M context) — |
||
| // the completion so the live-state loop can settle it from the | ||
| // response's updatedAt watermark instead of a full catalog scan. | ||
| store.recordSessionActivity(workspaceCwd, sessionId); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Turn completions were the only steady-state catalog reader once live-state is on, so retiring the rescan freezes every row whose recency the live overlay does not carry. With live-state enabled the store refuses all background/poll catalog work ( Reviewed with Claude Code (claude-opus-5, 1M context) — |
||
| return; | ||
| } | ||
| store.invalidateWorkspace(workspaceCwd); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In split view ChatPane and App can both report the same completion, and the second record bumps the sequence so the in-flight settle no-ops, costing a full extra poll cycle.
SplitView.tsx:488-491setsreportCatalogTurnCompletion={sessionId !== currentSessionId || paneWorkspaceCwd !== connection.workspaceCwd}, andpaneWorkspaceCwd = workspaceCwdById.get(sessionId)(SplitView.tsx:420) isundefineduntil the session list resolves, so the second disjunct is true even for a pane showing the current session. React runs the child effect first: ChatPane records seq 1, the wake starts a poll that snapshots{S:1}, then App's effect records seq 2, soresolveSessionActivity(cwd, S, 1)hitsif (pending?.get(sessionId) !== sequence) return;and no-ops. Probe: a single record leavessnapshotSessionActivity('/work') === undefinedafter the wake poll, a double record leavesMap(1) { 'session-a' => 2 }— the completion waits anotherSESSION_LIVE_STATE_POLL_MS, and if the row is off-page it raisesinvalidationRequestedtwice, scheduling a duplicate full reconcile.Reviewed with Claude Code (claude-opus-5, 1M context) —
/code-review 9476 --comment.