fix(sessions): live-refresh session list via SSE#346
Conversation
The "all sessions" list only refreshed on page visibility change, so new sessions, renames, and deletions were invisible until navigating away and back. Broadcast a `sessions_changed` SSE event from every session mutation (create, delete, delete-all, rename, auto-rename) and subscribe to it in useSessionList to trigger a refetch. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 4 issue(s) (1 warning).
frontend/src/hooks/useSessionList.ts
Clean, minimal implementation that correctly wires SSE broadcasts for session mutations. Main gaps are missing test coverage for the new broadcast calls and a lack of debouncing on the frontend consumer.
- 🔵 unsafe_assumptions (L92): No debounce/coalescing on the
sessions_changedhandler. If multiple events fire in rapid succession (e.g. auto-rename right after create), each triggers a full/api/sessionsfetch. The existingsession_activitypath coalesces viascheduleBroadcast()(200ms), butsessions_changedfires and is consumed immediately. Consider debouncing the handler or coalescing on the server side.[fixable] - 🔵 unsafe_assumptions (L92): Self-initiated operations (
dismissSession,clearAll,handleRename) optimistically update state and then call the server API, which broadcastssessions_changedback to the same client. This triggers a redundant full refetch that overwrites the already-correct optimistic state. Not harmful, but wasteful — could skip the SSE-triggered refetch for actions the client initiated itself.[fixable]
server/app.ts
Clean, minimal implementation that correctly wires SSE broadcasts for session mutations. Main gaps are missing test coverage for the new broadcast calls and a lack of debouncing on the frontend consumer.
- 🟡 missing_tests (L492): No tests verify that
sseRegistry.broadcast('sessions_changed', {})is called after session create, delete, delete-all, or rename. The existing route tests inserver/__tests__/routes.test.ts(lines 402-431) test these endpoints but don't mock or assert onsseRegistry. Similarly, the newsetSessionsChangedCallbackwiring inchat.ts(line 1003) andindex.ts(line 275) has no test coverage.[fixable]
server/chat.ts
Clean, minimal implementation that correctly wires SSE broadcasts for session mutations. Main gaps are missing test coverage for the new broadcast calls and a lack of debouncing on the frontend consumer.
- 🔵 style (L64): The new
_onSessionsChangeddeclaration (lines 64–67) sits between module-level code and an import statement on line 68 (import { EventStore }). This matches the existing pattern for_onSessionChangeabove it, but imports-after-code is unusual and would be flagged byimport/firstlinting rules. Consider moving the callback declarations below all imports if the file is ever restructured.[fixable]
| document.addEventListener('visibilitychange', onVisible); | ||
|
|
||
| // Refetch session list when sessions are created, renamed, or deleted | ||
| const unsubChanged = eventBus.on('sessions_changed', () => { |
There was a problem hiding this comment.
🔵 unsafe_assumptions: No debounce/coalescing on the sessions_changed handler. If multiple events fire in rapid succession (e.g. auto-rename right after create), each triggers a full /api/sessions fetch. The existing session_activity path coalesces via scheduleBroadcast() (200ms), but sessions_changed fires and is consumed immediately. Consider debouncing the handler or coalescing on the server side. [fixable]
| document.addEventListener('visibilitychange', onVisible); | ||
|
|
||
| // Refetch session list when sessions are created, renamed, or deleted | ||
| const unsubChanged = eventBus.on('sessions_changed', () => { |
There was a problem hiding this comment.
🔵 unsafe_assumptions: Self-initiated operations (dismissSession, clearAll, handleRename) optimistically update state and then call the server API, which broadcasts sessions_changed back to the same client. This triggers a redundant full refetch that overwrites the already-correct optimistic state. Not harmful, but wasteful — could skip the SSE-triggered refetch for actions the client initiated itself. [fixable]
| log.info('headless session started', { sessionId: wtId, prompt: initialPrompt }); | ||
| } | ||
|
|
||
| sseRegistry.broadcast('sessions_changed', {}); |
There was a problem hiding this comment.
🟡 missing_tests: No tests verify that sseRegistry.broadcast('sessions_changed', {}) is called after session create, delete, delete-all, or rename. The existing route tests in server/__tests__/routes.test.ts (lines 402-431) test these endpoints but don't mock or assert on sseRegistry. Similarly, the new setSessionsChangedCallback wiring in chat.ts (line 1003) and index.ts (line 275) has no test coverage. [fixable]
| _onSessionChange = cb; | ||
| } | ||
|
|
||
| let _onSessionsChanged: (() => void) | null = null; |
There was a problem hiding this comment.
🔵 style: The new _onSessionsChanged declaration (lines 64–67) sits between module-level code and an import statement on line 68 (import { EventStore }). This matches the existing pattern for _onSessionChange above it, but imports-after-code is unusual and would be flagged by import/first linting rules. Consider moving the callback declarations below all imports if the file is ever restructured. [fixable]
Summary
sessions_changedSSE event broadcast from every session mutation point (create, delete, delete-all, rename, auto-rename)useSessionListhook subscribes to the event and refetches the listTest plan
🤖 Generated with Claude Code