diff --git a/apps/desktop/src/app/chat/sidebar/session-row.tsx b/apps/desktop/src/app/chat/sidebar/session-row.tsx index c6f8ebc7a332..67b45607cf3c 100644 --- a/apps/desktop/src/app/chat/sidebar/session-row.tsx +++ b/apps/desktop/src/app/chat/sidebar/session-row.tsx @@ -313,10 +313,11 @@ const DOT_VARIANTS: Record = { role: 'status' }, // Pulsing gray — a terminal(background=true) process is alive while the LLM - // is idle. Gray (not accent) reads as "something chugging along". + // is idle. Gray (not accent) reads as "something chugging along". Brighter + // than muted-foreground so it's visible against the sidebar surface. background: { ariaLabel: r => r.backgroundRunning, - className: `${DOT_BASE} bg-muted-foreground/50 ${PING} before:bg-muted-foreground/50 before:opacity-50`, + className: `${DOT_BASE} bg-muted-foreground/80 ${PING} before:bg-muted-foreground/80 before:opacity-60`, role: 'status', title: r => r.backgroundRunning }, diff --git a/apps/desktop/src/app/session/hooks/use-message-stream/gateway-event.ts b/apps/desktop/src/app/session/hooks/use-message-stream/gateway-event.ts index 0d585b017e40..ebef5492070f 100644 --- a/apps/desktop/src/app/session/hooks/use-message-stream/gateway-event.ts +++ b/apps/desktop/src/app/session/hooks/use-message-stream/gateway-event.ts @@ -218,17 +218,30 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) { } if (sessionId && hasStatePatch) { - updateSessionState(sessionId, state => ({ - ...state, - ...statePatch, - branch: statePatch.branch ?? state.branch, - cwd: statePatch.cwd ?? state.cwd - })) + updateSessionState( + sessionId, + state => ({ + ...state, + ...statePatch, + branch: statePatch.branch ?? state.branch, + cwd: statePatch.cwd ?? state.cwd + }), + payload?.stored_session_id || undefined + ) } - if (apply) { - if (runningChanged && sessionId) { - updateSessionState(sessionId, state => { + // The running→busy transition must reach EVERY session, not just the + // active one. The `apply` gate above correctly scopes view-only side + // effects (setCurrentModel, setCurrentCwd, etc.) to the focused chat, + // but the per-session busy state is what drives the sidebar working + // indicator — a background session's turn start/finish must update + // its dot without the user opening it. updateSessionState only + // mutates the per-runtime cache entry, and syncSessionStateToView + // guards the view publish to the active session, so this is safe. + if (runningChanged && sessionId) { + updateSessionState( + sessionId, + state => { const busy = Boolean(payload!.running) if (state.busy === busy && (busy || !state.awaitingResponse)) { @@ -255,8 +268,9 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) { streamId: null, turnStartedAt: null } - }) - } + }, + payload?.stored_session_id || undefined + ) } if (payload?.usage && (!explicitSid || isActiveEvent)) { diff --git a/apps/desktop/src/lib/chat-messages.ts b/apps/desktop/src/lib/chat-messages.ts index c622b90d85e7..9488bd4cb4f0 100644 --- a/apps/desktop/src/lib/chat-messages.ts +++ b/apps/desktop/src/lib/chat-messages.ts @@ -79,6 +79,10 @@ export type GatewayEventPayload = { // session.title (live auto-title push) — stored session id + generated title session_id?: string title?: string + // session.info — the stored (durable) session id for this runtime session. + // Lets the desktop app map runtime→stored for background sessions it hasn't + // opened, so the sidebar working indicator updates without opening the chat. + stored_session_id?: string // moa.reference / moa.aggregating (Mixture of Agents per-model relay) label?: string index?: number diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 47b10a0d4be0..2f92833d33b3 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -3656,6 +3656,7 @@ def _session_info(agent, session: dict | None = None) -> dict: "personality": str(personality or ""), "running": bool((session or {}).get("running")), "title": _session_live_title(session or {}, session_key) if session_key else "", + "stored_session_id": session_key or "", "desktop_contract": DESKTOP_BACKEND_CONTRACT, "version": "", "release_date": "",