-
Notifications
You must be signed in to change notification settings - Fork 0
fix(sessions): live-refresh session list via SSE #346
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
base: main
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
|
|
@@ -88,6 +88,19 @@ export function useSessionList(): UseSessionListReturn { | |
| }; | ||
| document.addEventListener('visibilitychange', onVisible); | ||
|
|
||
| // Refetch session list when sessions are created, renamed, or deleted | ||
| const unsubChanged = eventBus.on('sessions_changed', () => { | ||
|
Owner
Author
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. 🔵 unsafe_assumptions: Self-initiated operations ( |
||
| apiFetch('/api/sessions') | ||
| .then((r) => r.json()) | ||
| .then((data) => { | ||
| const { sessions: page, hasMore: more } = parseSessionsResponse(data); | ||
| setSessions(page); | ||
| setHasMore(more); | ||
| nextOffset.current = page.length; | ||
| }) | ||
| .catch(() => {}); | ||
| }); | ||
|
|
||
| // Live session dots via SSE — update isActive/isAttached without full refetch | ||
| const unsubActivity = eventBus.on('session_activity', (data) => { | ||
| const activities = data as SessionActivity[]; | ||
|
|
@@ -105,6 +118,7 @@ export function useSessionList(): UseSessionListReturn { | |
|
|
||
| return () => { | ||
| document.removeEventListener('visibilitychange', onVisible); | ||
| unsubChanged(); | ||
| unsubActivity(); | ||
| }; | ||
| }, []); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -489,6 +489,8 @@ async function handleSessionCreate( | |
| log.info('headless session started', { sessionId: wtId, prompt: initialPrompt }); | ||
| } | ||
|
|
||
| sseRegistry.broadcast('sessions_changed', {}); | ||
|
Owner
Author
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. 🟡 missing_tests: No tests verify that |
||
|
|
||
| res.status(initialPrompt ? 201 : 200).json({ | ||
| sessionId: wtId, | ||
| worktrees, | ||
|
|
@@ -1100,6 +1102,7 @@ app.get('/api/sessions/:id/messages', async (req, res) => { | |
|
|
||
| app.delete('/api/sessions/:id', (req, res) => { | ||
| hideSession(req.params.id as string); | ||
| sseRegistry.broadcast('sessions_changed', {}); | ||
| res.json({ ok: true }); | ||
| }); | ||
|
|
||
|
|
@@ -1136,6 +1139,7 @@ app.get('/api/sessions/:id/events', (req, res) => { | |
|
|
||
| app.delete('/api/sessions', (_req, res) => { | ||
| hideAllSessions(); | ||
| sseRegistry.broadcast('sessions_changed', {}); | ||
| res.json({ ok: true }); | ||
| }); | ||
|
|
||
|
|
@@ -1147,6 +1151,7 @@ app.put('/api/sessions/:id/rename', async (req, res) => { | |
| } | ||
| try { | ||
| await renameSessionById(req.params.id, title.slice(0, 200)); | ||
| sseRegistry.broadcast('sessions_changed', {}); | ||
| res.json({ ok: true }); | ||
| } catch { | ||
| res.status(404).json({ error: 'Session not found' }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,6 +60,11 @@ let _onSessionChange: SessionChangeCallback | null = null; | |
| export function setSessionChangeCallback(cb: SessionChangeCallback): void { | ||
| _onSessionChange = cb; | ||
| } | ||
|
|
||
| let _onSessionsChanged: (() => void) | null = null; | ||
|
Owner
Author
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. 🔵 style: The new |
||
| export function setSessionsChangedCallback(cb: () => void): void { | ||
| _onSessionsChanged = cb; | ||
| } | ||
| import { EventStore } from './event-store.js'; | ||
| import { capturePromptComparison } from './prompt-compare.js'; | ||
| import { shouldAutoRename, extractRecentPrompts, generateSessionName } from './auto-rename.js'; | ||
|
|
@@ -995,6 +1000,7 @@ async function tryAutoRename(sessionId: string, clientId: string): Promise<void> | |
|
|
||
| // Persist to EventStore first — survives SDK rename failures. | ||
| eventStore.upsertSession({ sessionId, summary: newName }); | ||
| _onSessionsChanged?.(); | ||
|
|
||
| // Update the SDK session name (best-effort, fire-and-forget) | ||
| renameSessionById(sessionId, newName, false).catch((err: unknown) => { | ||
|
|
||
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.
🔵 unsafe_assumptions: 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]