diff --git a/frontend/src/hooks/useSessionList.ts b/frontend/src/hooks/useSessionList.ts index e70799be..d4aa90cf 100644 --- a/frontend/src/hooks/useSessionList.ts +++ b/frontend/src/hooks/useSessionList.ts @@ -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', () => { + 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(); }; }, []); diff --git a/server/app.ts b/server/app.ts index 1389d7bc..d7467e85 100644 --- a/server/app.ts +++ b/server/app.ts @@ -489,6 +489,8 @@ async function handleSessionCreate( log.info('headless session started', { sessionId: wtId, prompt: initialPrompt }); } + sseRegistry.broadcast('sessions_changed', {}); + 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' }); diff --git a/server/chat.ts b/server/chat.ts index 819914f9..10718894 100644 --- a/server/chat.ts +++ b/server/chat.ts @@ -60,6 +60,11 @@ let _onSessionChange: SessionChangeCallback | null = null; export function setSessionChangeCallback(cb: SessionChangeCallback): void { _onSessionChange = cb; } + +let _onSessionsChanged: (() => void) | null = null; +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 // 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) => { diff --git a/server/index.ts b/server/index.ts index 1c586780..8a656106 100644 --- a/server/index.ts +++ b/server/index.ts @@ -29,6 +29,7 @@ import { getRepoConfig, setConnectionRegistry, setSessionChangeCallback, + setSessionsChangedCallback, reconcileSessionsBackground, } from './chat.js'; import { cleanupStaleWorktrees, countWorktrees } from './worktree.js'; @@ -271,6 +272,10 @@ setSessionChangeCallback((clientId, event, sessionId) => { overviewEmitter.scheduleBroadcast(); }); +setSessionsChangedCallback(() => { + sseRegistry.broadcast('sessions_changed', {}); +}); + server.on('upgrade', async (req, socket, head) => { const url = new URL(req.url || '', `http://${req.headers.host}`);