diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index e628f6ea6a..4f09c7e1dc 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -660,7 +660,6 @@ export default function ChatView({ threadId }: ChatViewProps) { const phase = derivePhase(activeThread?.session ?? null); const isSendBusy = sendPhase !== "idle"; const isPreparingWorktree = sendPhase === "preparing-worktree"; - const isWorking = phase === "running" || isSendBusy || isConnecting || isRevertingCheckpoint; const nowIso = new Date(nowTick).toISOString(); const activeWorkStartedAt = deriveActiveWorkStartedAt( activeLatestTurn, @@ -684,6 +683,13 @@ export default function ChatView({ threadId }: ChatViewProps) { () => derivePendingUserInputs(threadActivities), [threadActivities], ); + // phase === "running" alone isn't sufficient: the session can stay "running" even after + // the latest turn has completed (server lifecycle guard can reject status updates while + // completedAt is set through a separate path) or while waiting for user input. + // Use completedAt on the latest turn as the reliable completion signal. + const turnDone = !!activeLatestTurn?.completedAt; + const isActivelyRunning = phase === "running" && pendingUserInputs.length === 0 && !turnDone; + const isWorking = isActivelyRunning || isSendBusy || isConnecting || isRevertingCheckpoint; const activePendingUserInput = pendingUserInputs[0] ?? null; const activePendingDraftAnswers = useMemo( () => diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index ef338dab67..abc52754ac 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -115,7 +115,11 @@ export function resolveThreadStatusPill(input: { }; } - if (thread.session?.status === "running") { + // Use completedAt as the reliable completion signal — activeTurnId can be permanently + // stale when the server lifecycle guard rejects status updates. + const sidebarTurnDone = !!thread.latestTurn?.completedAt; + + if (thread.session?.status === "running" && !sidebarTurnDone) { return { label: "Working", colorClass: "text-sky-600 dark:text-sky-300/80",