From e27b3639b93659a24d461e522f8062fc09350c24 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Thu, 22 Oct 2026 04:49:57 -0500 Subject: [PATCH 1/4] fix(web): show working state while provider sessions start Derive the connecting flag from the persisted runtime phase so it survives route changes and reloads. Co-Authored-By: GPT-6.1 Sol --- apps/web/src/components/ChatView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 92707af8ca119..e7926605efb45 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -634,7 +634,6 @@ export default function ChatView(props: ChatViewProps) { const [localServerErrorsByThreadKey, setLocalServerErrorsByThreadKey] = useState< Record >({}); - const [isConnecting, _setIsConnecting] = useState(false); const isRevertingCheckpoint = useComposerDraftStore((store) => store.rewindingThreadKeys.has(routeThreadKey), ); @@ -1270,6 +1269,7 @@ export default function ChatView(props: ChatViewProps) { conversationProviderStatus !== null && conversationProviderStatus.supportsConversationRollback !== false; const phase = derivePhase(activeRuntime); + const isConnecting = phase === "connecting"; const serverThreadId = serverThread?.id; const serverThreadEnvironmentId = serverThread?.environmentId; const setThreadError = makeThreadErrorSetter({ From 78f09aa3d18297ae07183bfd2425f88e9668a72d Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Thu, 22 Oct 2026 06:03:08 -0500 Subject: [PATCH 2/4] fix(web): preserve setup sends while showing startup activity Derive timeline activity separately from the existing composer gates. Cover restored startup activity and follow-ups queued behind setup. Co-Authored-By: GPT-6.1 Sol --- .../web/src/components/ChatView.logic.test.ts | 43 +++++++++++++++++++ apps/web/src/components/ChatView.logic.ts | 16 +++++++ apps/web/src/components/ChatView.tsx | 15 ++++--- 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/ChatView.logic.test.ts b/apps/web/src/components/ChatView.logic.test.ts index a03a7929b0917..2dab5f2160132 100644 --- a/apps/web/src/components/ChatView.logic.test.ts +++ b/apps/web/src/components/ChatView.logic.test.ts @@ -3,6 +3,7 @@ import { findRecordedWorktreeSetup, mergeDirectPreviewAnnotation, planComposerTurn, + deriveTimelineWorking, snapshotComposerSend, withMessageContext, deriveAvailableRevertTurnCounts, @@ -36,6 +37,7 @@ import { appAtomRegistry } from "../rpc/atomRegistry"; import { environmentThreadDetails } from "../state/threads"; import type { Thread } from "../types"; +import { derivePhase } from "../session-logic"; import { makeThreadFixture, makeThreadProjectionFixture } from "../test-fixtures"; import { getAntigravitySendBlockReason, @@ -1071,6 +1073,25 @@ describe("snapshotComposerSend", () => { }); }); +describe("deriveTimelineWorking", () => { + it.each(["preparing", "starting", "queued"] as const)( + "restores activity from a persisted %s session without a local send", + (status) => { + const state = { + phase: derivePhase({ ...readySession, status }), + isSendBusy: false, + isCompacting: false, + runlessWorkStartedAt: null, + }; + expect(deriveTimelineWorking(state)).toBe(true); + expect(deriveTimelineWorking({ ...state, phase: "ready" })).toBe(false); + expect( + deriveTimelineWorking({ ...state, phase: derivePhase({ ...readySession, status: "failed" }) }), + ).toBe(false); + }, + ); +}); + describe("planComposerTurn", () => { const base = { messageText: "hello", @@ -1112,6 +1133,28 @@ describe("planComposerTurn", () => { ).toMatchObject({ compactBeforeSend: false, turnDispatchMode: "auto" }); }); + it.each(["preparing", "starting"] as const)( + "queues a follow-up behind %s even when the dispatch preference is steer", + (status) => { + const phase = derivePhase({ ...readySession, status }); + expect( + planComposerTurn({ + ...base, + isServerThread: true, + activeMessageCount: 1, + sendQueuesBehindSetup: true, + phase, + dispatchMode: "steer", + }), + ).toMatchObject({ + turnDispatchMode: "queue", + shouldQueueBehindActiveRun: true, + missingWorktreeBaseBranch: false, + baseBranchForWorktree: null, + }); + }, + ); + it("queues behind a running turn only when asked to", () => { const running = { ...base, diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index 856a2b4e548c3..f42933ea7c8f5 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -1517,6 +1517,22 @@ export function snapshotComposerSend(contents: ComposerSendContents) { export type ComposerSendSnapshot = ReturnType; +/** Timeline activity survives a remount without making setup sends busy. */ +export function deriveTimelineWorking(input: { + readonly phase: SessionPhase; + readonly isSendBusy: boolean; + readonly isCompacting: boolean; + readonly runlessWorkStartedAt: string | null; +}): boolean { + return ( + input.phase === "connecting" || + input.phase === "running" || + input.isSendBusy || + input.isCompacting || + input.runlessWorkStartedAt !== null + ); +} + /** How a composer send dispatches: where it runs and whether it waits behind other work. */ export function planComposerTurn(input: { readonly messageText: string; diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index e7926605efb45..bbc2a4ec86fb5 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -318,6 +318,7 @@ import { buildLocalDraftThread, collectUserMessageBlobPreviewUrls, deriveCommittedServerUserMessageIds, + deriveTimelineWorking, dismissBranchMismatchForSession, shouldShowPlanFollowUpPrompt, shouldRenderPreviewMiniPlayer, @@ -634,6 +635,7 @@ export default function ChatView(props: ChatViewProps) { const [localServerErrorsByThreadKey, setLocalServerErrorsByThreadKey] = useState< Record >({}); + const [isConnecting, _setIsConnecting] = useState(false); const isRevertingCheckpoint = useComposerDraftStore((store) => store.rewindingThreadKeys.has(routeThreadKey), ); @@ -1269,7 +1271,6 @@ export default function ChatView(props: ChatViewProps) { conversationProviderStatus !== null && conversationProviderStatus.supportsConversationRollback !== false; const phase = derivePhase(activeRuntime); - const isConnecting = phase === "connecting"; const serverThreadId = serverThread?.id; const serverThreadEnvironmentId = serverThread?.environmentId; const setThreadError = makeThreadErrorSetter({ @@ -1400,12 +1401,12 @@ export default function ChatView(props: ChatViewProps) { const isCompacting = (isSendBusy || phase === "connecting" || phase === "running") && (compactionRunning || (compactRequestIsActive && !compactionSettled)); - const isWorking = - phase === "running" || - isSendBusy || - isConnecting || - isCompacting || - runlessWorkStartedAt !== null; + const isWorking = deriveTimelineWorking({ + phase, + isSendBusy, + isCompacting, + runlessWorkStartedAt, + }); const activeContextWindow = deriveLatestContextWindowSnapshot( serverVisibleTurnItems ?? [], activeThreadLiveTokenUsage, From 1e52c15a13843c83f632d4a9dd8a1ef4a98dc39d Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Thu, 22 Oct 2026 06:05:01 -0500 Subject: [PATCH 3/4] style(web): format startup regression coverage Co-Authored-By: GPT-6.1 Sol --- apps/web/src/components/ChatView.logic.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/ChatView.logic.test.ts b/apps/web/src/components/ChatView.logic.test.ts index 2dab5f2160132..670100650a213 100644 --- a/apps/web/src/components/ChatView.logic.test.ts +++ b/apps/web/src/components/ChatView.logic.test.ts @@ -1086,7 +1086,10 @@ describe("deriveTimelineWorking", () => { expect(deriveTimelineWorking(state)).toBe(true); expect(deriveTimelineWorking({ ...state, phase: "ready" })).toBe(false); expect( - deriveTimelineWorking({ ...state, phase: derivePhase({ ...readySession, status: "failed" }) }), + deriveTimelineWorking({ + ...state, + phase: derivePhase({ ...readySession, status: "failed" }), + }), ).toBe(false); }, ); From 66772ba5cbef84ba5f8d3afc8e853f856dda57e8 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 14 Nov 2026 08:03:42 +0000 Subject: [PATCH 4/4] docs(web): describe what the working-state helper derives Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/ChatView.logic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index 67dd59632856c..14590bb0ff386 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -1294,7 +1294,7 @@ export function snapshotComposerSend(contents: ComposerSendContents) { export type ComposerSendSnapshot = ReturnType; -/** Timeline activity survives a remount without making setup sends busy. */ +/** Derived from the persisted run phase, so a starting run still reads as work after a reload. */ export function deriveTimelineWorking(input: { readonly phase: SessionPhase; readonly isSendBusy: boolean;