diff --git a/packages/app/e2e/browser/turn-collapse.spec.ts b/packages/app/e2e/browser/turn-collapse.spec.ts index 4d98dea506..ddbff1e2f3 100644 --- a/packages/app/e2e/browser/turn-collapse.spec.ts +++ b/packages/app/e2e/browser/turn-collapse.spec.ts @@ -475,7 +475,20 @@ async function expectThinkingPresentation(page: Page, autoExpandReasoning: boole await expect(group).toContainText(/read 1 file/i); await expect(group.getByRole("button").first()).not.toContainText(/thought/i); await group.getByRole("button").first().click(); - await expect(reasoning).toBeVisible(); + // Expand thinking is off: the group opens its rows, but the thought row stays closed + // until the reader opens it. + const thought = group + .getByTestId("tool-call-badge") + .filter({ hasText: /^Thinking/ }) + .first(); + await expect(thought).toBeVisible(); + await expect(reasoning).toHaveCount(0); + // A press right after the group opens occasionally does not register; retry until the + // thought row opens. + await expect(async () => { + await thought.getByRole("button").first().click(); + await expect(reasoning).toBeVisible({ timeout: 2_000 }); + }).toPass(); } } diff --git a/packages/app/src/agent-stream/presentation.test.ts b/packages/app/src/agent-stream/presentation.test.ts index f7d146d92a..ce3c0352ff 100644 --- a/packages/app/src/agent-stream/presentation.test.ts +++ b/packages/app/src/agent-stream/presentation.test.ts @@ -702,6 +702,28 @@ it("does not reopen the previous turn while a new run waits for its first row", expect(result.tail.map((item) => item.id)).toEqual(["prompt", "reply"]); }); +// A sent prompt counts as activity before the daemon opens its turn, so the only known turn is +// the completed one; it must stay folded until the new turn's rows arrive. +it("keeps the previous turn folded while a sent prompt waits for its turn to open", () => { + const timestamp = new Date(); + const tail: StreamItem[] = [ + { kind: "user_message", id: "prompt", text: "Work", timestamp }, + toolCall("one", "Bash"), + { kind: "assistant_message", id: "reply", text: "Done", timestamp }, + ]; + for (const item of tail) item.turnId = "completed"; + const result = createStreamPresentation()({ + ...presentationOptions, + transform: undefined, + tail, + head: [], + display: "collapseTurns", + isTurnActive: true, + activeTurnId: null, + }); + expect(result.tail.map((item) => item.id)).toEqual(["prompt", "reply"]); +}); + it("groups reasoning with tools unless Expand thinking is enabled, including setting changes", () => { const present = createStreamPresentation(); const timestamp = new Date(); diff --git a/packages/app/src/agent-stream/presentation.ts b/packages/app/src/agent-stream/presentation.ts index b449cb63cc..94140ca395 100644 --- a/packages/app/src/agent-stream/presentation.ts +++ b/packages/app/src/agent-stream/presentation.ts @@ -113,14 +113,16 @@ export function createStreamPresentation() { } // Live text updates reach the head only, so the folded history is reused until the // retained rows, the latest turn's liveness, or the reader's expanded turns change. - const isLatestTurnOpen = - (input.isTurnActive || projection.head.length > 0) && - !isTurnBoundary(projection.tail.at(-1) ?? null, projection.head[0] ?? null) && - !( - input.activeTurnId && - projection.tail.at(-1)?.turnId && - input.activeTurnId !== projection.tail.at(-1)?.turnId - ); + const last = projection.tail.at(-1) ?? null; + const first = projection.head[0] ?? null; + const activeTurnIsAnother = Boolean( + input.activeTurnId && last?.turnId && input.activeTurnId !== last.turnId, + ); + // Without head rows, the latest retained turn is live only while the active run is that + // turn. A run that has not opened yet carries no id and belongs to the next turn. + const isLatestTurnOpen = first + ? !isTurnBoundary(last, first) && !activeTurnIsAnother + : input.isTurnActive && (last?.turnId === undefined || input.activeTurnId === last.turnId); if ( !collapsed || collapsedSource !== projection.tail || diff --git a/packages/app/src/agent-stream/view.tsx b/packages/app/src/agent-stream/view.tsx index f35af10498..90c88fb0d1 100644 --- a/packages/app/src/agent-stream/view.tsx +++ b/packages/app/src/agent-stream/view.tsx @@ -898,12 +898,14 @@ const AgentStreamViewComponent = forwardRef ( {call.kind === "thought" ? ( + // Reasoning is grouped only while Expand thinking is off, so an opened + // group shows each thought as a closed row like any other call. ) : (