fix(desktop): sidebar status indicators lag for background sessions

The sidebar working dot didn't update for background sessions until the
user opened them. Two coupled causes:

1. The gateway's session.info event payload omitted stored_session_id,
   so the desktop app had no way to map a background session's runtime
   id to its stored id. Without the stored id, setSessionWorking(null,
   ...) was a no-op — the $workingSessionIds atom never updated.

2. The running→busy transition in the session.info handler was gated on
   `apply` (active session only). The gate correctly scopes view-only
   side effects (setCurrentModel, setCurrentCwd, etc.) to the focused
   chat, but the per-session busy state drives the sidebar indicator and
   must reach every session. updateSessionState only mutates the
   per-runtime cache entry, and syncSessionStateToView already guards
   the view publish to the active session, so ungating is safe.

Fix: add stored_session_id to _session_info() in tui_gateway/server.py,
add the field to GatewayEventPayload, pass it to updateSessionState in
the session.info handler, and ungate the running→busy transition.
This commit is contained in:
ethernet 2026-07-16 12:49:09 -04:00
parent 311a5b0a55
commit 42bd4368ae
4 changed files with 33 additions and 13 deletions

View file

@ -313,10 +313,11 @@ const DOT_VARIANTS: Record<SessionDotState, DotVariant> = {
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
},

View file

@ -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)) {

View file

@ -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

View file

@ -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": "",