diff --git a/apps/desktop/src/app/desktop-controller.tsx b/apps/desktop/src/app/desktop-controller.tsx index f585cfec579..5ff162a2ca4 100644 --- a/apps/desktop/src/app/desktop-controller.tsx +++ b/apps/desktop/src/app/desktop-controller.tsx @@ -77,6 +77,7 @@ import { setSessionsLoading, setSessionsTotal } from '../store/session' +import { onSessionsChanged } from '../store/session-sync' import { clearSessionTodos, setSessionTodos, todoListActive } from '../store/todos' import { openUpdatesWindow, startUpdatePoller, stopUpdatePoller } from '../store/updates' import { isSecondaryWindow } from '../store/windows' @@ -464,6 +465,17 @@ export function DesktopController() { void refreshSessions() }, [refreshSessions]) + // Another window mutated the shared session list (e.g. a chat started in the + // pop-out). Re-pull so the sidebar reflects it. Pop-outs have no sidebar, so + // only real windows bother. + useEffect(() => { + if (isSecondaryWindow()) { + return + } + + return onSessionsChanged(() => void refreshSessions().catch(() => undefined)) + }, [refreshSessions]) + // ALL-profiles view pages one profile at a time: fetch that profile's next // page and merge it in place, leaving every other profile's rows untouched. const loadMoreSessionsForProfile = useCallback(async (profile: string) => { diff --git a/apps/desktop/src/app/session/hooks/use-message-stream.ts b/apps/desktop/src/app/session/hooks/use-message-stream.ts index c4718b88ac3..c07222c6890 100644 --- a/apps/desktop/src/app/session/hooks/use-message-stream.ts +++ b/apps/desktop/src/app/session/hooks/use-message-stream.ts @@ -47,6 +47,7 @@ import { setTurnStartedAt, setYoloActive } from '@/store/session' +import { broadcastSessionsChanged } from '@/store/session-sync' import { clearSessionSubagents, pruneDelegateFallbackSubagents, upsertSubagent } from '@/store/subagents' import { setSessionTodos } from '@/store/todos' import { recordToolDiff } from '@/store/tool-diffs' @@ -641,6 +642,9 @@ export function useMessageStream({ }) void refreshSessions().catch(() => undefined) + // Sync the freshly-titled row to other windows (e.g. main, when the turn + // ran in the pop-out). + broadcastSessionsChanged() if (compactedTurnRef.current.delete(sessionId)) { shouldHydrate = false diff --git a/apps/desktop/src/app/session/hooks/use-session-actions.ts b/apps/desktop/src/app/session/hooks/use-session-actions.ts index 9ce2ff1a8ff..50b6bb0d270 100644 --- a/apps/desktop/src/app/session/hooks/use-session-actions.ts +++ b/apps/desktop/src/app/session/hooks/use-session-actions.ts @@ -42,6 +42,7 @@ import { setYoloActive, workspaceCwdForNewSession } from '@/store/session' +import { broadcastSessionsChanged } from '@/store/session-sync' import { reportBackendContract } from '@/store/updates' import { isWatchWindow } from '@/store/windows' import type { SessionCreateResponse, SessionInfo, SessionResumeResponse, SessionRuntimeInfo, UsageStats } from '@/types/hermes' @@ -472,6 +473,9 @@ export function useSessionActions({ // server later returns its own preview/title and supersedes this. upsertOptimisticSession(created, stored, null, preview?.trim() || null) navigate(sessionRoute(stored), { replace: true }) + // Other windows (e.g. the main window when this is the pop-out) can't + // see this session until they re-pull the shared list. + broadcastSessionsChanged() } setFreshDraftReady(false) diff --git a/apps/desktop/src/store/session-sync.ts b/apps/desktop/src/store/session-sync.ts new file mode 100644 index 00000000000..66bf6d08202 --- /dev/null +++ b/apps/desktop/src/store/session-sync.ts @@ -0,0 +1,25 @@ +// Cross-window session-list sync. Each desktop window is its own renderer +// process with its own gateway socket and session store, so a mutation in one +// (e.g. a new chat started in the compact pop-out) never reaches another +// window. This bus pings every window to re-pull the shared session list; the +// data already lives in the backend, the other window just doesn't know to look. +const CHANNEL = 'hermes:sessions' + +const channel = typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel(CHANNEL) + +// A window that mutated the session list (created / titled a chat) tells the +// others to refresh. A BroadcastChannel never delivers to its own poster, so the +// caller refreshes locally as it already does. +export function broadcastSessionsChanged(): void { + channel?.postMessage(1) +} + +export function onSessionsChanged(handler: () => void): () => void { + if (!channel) { + return () => {} + } + + channel.addEventListener('message', handler) + + return () => channel.removeEventListener('message', handler) +}