From c59ca46940ea3d3c06a70f20da75c02547777ea3 Mon Sep 17 00:00:00 2001 From: UnathiCodex Date: Mon, 20 Jul 2026 16:31:58 +0200 Subject: [PATCH] fix(desktop): keep quiet sessions visibly running (#65870) * fix(desktop): keep quiet sessions visibly running * fix(desktop): restore running status after reconnect * fix(desktop): keep live session status unmistakable --- .../chat/sidebar/session-row-state.test.ts | 43 ++++++ .../src/app/chat/sidebar/session-row-state.ts | 42 ++++++ .../src/app/chat/sidebar/session-row.tsx | 31 ++--- .../contrib/hooks/use-background-sync.test.ts | 75 ++++++++++ .../app/contrib/hooks/use-background-sync.ts | 122 ++++++++++++++++ apps/desktop/src/app/contrib/wiring.tsx | 1 + .../session/hooks/use-session-state-cache.ts | 29 +--- apps/desktop/src/store/gateway-switch.test.ts | 4 + apps/desktop/src/store/gateway-switch.ts | 4 +- apps/desktop/src/store/session-states.ts | 44 ++++-- .../src/store/session-watchdog.test.ts | 130 +++++++----------- 11 files changed, 390 insertions(+), 135 deletions(-) create mode 100644 apps/desktop/src/app/chat/sidebar/session-row-state.test.ts create mode 100644 apps/desktop/src/app/chat/sidebar/session-row-state.ts create mode 100644 apps/desktop/src/app/contrib/hooks/use-background-sync.test.ts diff --git a/apps/desktop/src/app/chat/sidebar/session-row-state.test.ts b/apps/desktop/src/app/chat/sidebar/session-row-state.test.ts new file mode 100644 index 000000000000..3cb05a323019 --- /dev/null +++ b/apps/desktop/src/app/chat/sidebar/session-row-state.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest' + +import { sessionDotState, sessionShowsRunningArc } from './session-row-state' + +describe('session row running appearance', () => { + it('keeps the running arc when an authoritative turn becomes quiet', () => { + expect(sessionShowsRunningArc({ isWorking: true, needsInput: false })).toBe(true) + expect( + sessionDotState({ + hasBackground: false, + isStalled: true, + isUnread: false, + isWorking: true, + needsInput: false + }) + ).toBe('stalled') + }) + + it('uses the needs-input treatment instead of the running arc', () => { + expect(sessionShowsRunningArc({ isWorking: true, needsInput: true })).toBe(false) + expect( + sessionDotState({ + hasBackground: true, + isStalled: true, + isUnread: true, + isWorking: true, + needsInput: true + }) + ).toBe('needs-input') + }) + + it('keeps background and unread states below active-turn states', () => { + expect( + sessionDotState({ + hasBackground: true, + isStalled: false, + isUnread: true, + isWorking: false, + needsInput: false + }) + ).toBe('background') + }) +}) diff --git a/apps/desktop/src/app/chat/sidebar/session-row-state.ts b/apps/desktop/src/app/chat/sidebar/session-row-state.ts new file mode 100644 index 000000000000..cb149fb4e007 --- /dev/null +++ b/apps/desktop/src/app/chat/sidebar/session-row-state.ts @@ -0,0 +1,42 @@ +export type SessionDotState = 'background' | 'idle' | 'needs-input' | 'stalled' | 'unread' | 'working' + +interface SessionRowState { + hasBackground: boolean + isStalled: boolean + isUnread: boolean + isWorking: boolean + needsInput: boolean +} + +/** Resolve the sidebar dot's mutually-exclusive display state by priority. */ +export function sessionDotState({ + hasBackground, + isStalled, + isUnread, + isWorking, + needsInput +}: SessionRowState): SessionDotState { + if (needsInput) { + return 'needs-input' + } + + if (isWorking) { + return isStalled ? 'stalled' : 'working' + } + + if (hasBackground) { + return 'background' + } + + return isUnread ? 'unread' : 'idle' +} + +/** A quiet turn is still authoritatively running. Keep the unmistakable row + * arc until the gateway reports completion; only a blocking prompt suppresses + * it in favour of the needs-input treatment. */ +export function sessionShowsRunningArc({ + isWorking, + needsInput +}: Pick): boolean { + return isWorking && !needsInput +} diff --git a/apps/desktop/src/app/chat/sidebar/session-row.tsx b/apps/desktop/src/app/chat/sidebar/session-row.tsx index a03a37574a0f..895d18bedbf8 100644 --- a/apps/desktop/src/app/chat/sidebar/session-row.tsx +++ b/apps/desktop/src/app/chat/sidebar/session-row.tsx @@ -17,11 +17,12 @@ import { cn } from '@/lib/utils' import { $backgroundRunningSessionIds } from '@/store/composer-status' import { $unreadFinishedSessionIds } from '@/store/session' import { $sessionColorById } from '@/store/session-color' -import { $attentionSessionIds, openSessionTile } from '@/store/session-states' +import { $attentionSessionIds, $stalledSessionIds, openSessionTile } from '@/store/session-states' import { canOpenSessionWindow, openSessionInNewWindow } from '@/store/windows' import { SidebarRowBody, SidebarRowGrab, SidebarRowLabel, SidebarRowLead, SidebarRowShell } from './chrome' import { SessionActionsMenu, SessionContextMenu } from './session-actions-menu' +import { type SessionDotState, sessionDotState, sessionShowsRunningArc } from './session-row-state' import { useProfilePrewarm } from './use-profile-prewarm' interface SidebarSessionRowProps extends React.ComponentProps<'div'> { @@ -90,6 +91,9 @@ export function SidebarSessionRow({ // True when the session's most recent turn finished in the background (while // the user was viewing a different session) and hasn't been opened since. const isUnread = useStore($unreadFinishedSessionIds).includes(session.id) + // True when the turn is still running but the stream has been quiet long + // enough to soften the animation. This must never look like an idle row. + const isStalled = useStore($stalledSessionIds).includes(session.id) // True when a terminal(background=true) process is alive in this session. const hasBackground = useStore($backgroundRunningSessionIds).includes(session.id) // The session's resolved color (idle dot tint), read from the ONE shared map @@ -99,15 +103,7 @@ export function SidebarSessionRow({ // Resolve the dot's display state once — the four signals are mutually // exclusive by priority, so threading them as booleans through wrappers just // to collapse them at the leaf is backwards. - const dotState: SessionDotState = needsInput - ? 'needs-input' - : isWorking - ? 'working' - : hasBackground - ? 'background' - : isUnread - ? 'unread' - : 'idle' + const dotState = sessionDotState({ hasBackground, isStalled, isUnread, isWorking, needsInput }) return ( - {isWorking && !needsInput &&