From 0be1d53215e55c84e55673cf41e30ef06b02bb71 Mon Sep 17 00:00:00 2001 From: Brendonovich <14191578+Brendonovich@users.noreply.github.com> Date: Thu, 24 Sep 2026 10:00:28 +0000 Subject: [PATCH] fix(app): keep Working during reasoning-only turns --- .../session-timeline-working.spec.ts | 24 +++++++++++++++++++ .../session-ui/src/timeline/projection.ts | 14 ++++++++++- .../src/timeline/rows-current.test.ts | 24 +++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/packages/app/e2e/regression/session-timeline-working.spec.ts b/packages/app/e2e/regression/session-timeline-working.spec.ts index abebeabf954..f09cbfa6e96 100644 --- a/packages/app/e2e/regression/session-timeline-working.spec.ts +++ b/packages/app/e2e/regression/session-timeline-working.spec.ts @@ -51,6 +51,30 @@ for (const width of [1400, 390]) { }) } +for (const width of [1400, 390]) { + test(`keeps Working instead of Used 1 Thought while reasoning is the only activity at ${width}px`, async ({ page }) => { + const timeline = await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage([reasoningPart("prt_only_thought", "Still considering the answer.")], { completed: false }), + ], + settings: { timelineDetail: timelinePresets[2].value }, + viewport: { width, height: 900 }, + }) + const working = page.locator('[data-component="session-working"]') + const group = page.locator('[data-component="collapsed-tool-group"]') + await expect(working).toBeVisible() + await expect(group).toHaveCount(0) + await expect(page.getByRole("button", { name: "Used 1 Thought" })).toHaveCount(0) + + await timeline.send(partUpdated(toolPart("prt_after_thought", "read", "running", { filePath: "package.json" }))) + if (width === 390) await page.getByRole("button", { name: "Jump to latest" }).click() + await expect(group).toBeVisible() + await expect(group).toHaveAttribute("data-timeline-part-ids", `${assistantID}:reasoning:0,prt_after_thought`) + await expect(working).toBeVisible() + }) +} + for (const name of ["shell", "patch", "subagent"] as const) { test(`hides Working during ${name} input and execution, then restores it on completion`, async ({ page }) => { const timeline = await setupTimeline(page, { diff --git a/packages/session-ui/src/timeline/projection.ts b/packages/session-ui/src/timeline/projection.ts index effcaa9ba2f..a9d5f57de5e 100644 --- a/packages/session-ui/src/timeline/projection.ts +++ b/packages/session-ui/src/timeline/projection.ts @@ -307,6 +307,16 @@ export namespace Timeline { !lastAssistant?.retry && lastContent?.type === "reasoning" && lastContent.time?.completed === undefined + const onlyThinking = + detail?.thinking.placement === "grouped" && + isActive && + status.type === "busy" && + !!lastAssistant && + !lastAssistant.error && + !lastAssistant.retry && + entries.every( + (entry) => entry.type === "assistant" && entry.message.content.every((content) => content.type === "reasoning"), + ) if (previousUserMessage) rows.push(new TimelineRow.TurnGap({ userMessageID: turnID })) if (userMessage) rows.push(new TimelineRow.UserMessage({ userMessageID: turnID })) @@ -320,7 +330,9 @@ export namespace Timeline { contentEntries(message) .filter( (entry) => - isRenderable(entry.content, showReasoning, detail) && !(thinking && entry.content === lastContent), + isRenderable(entry.content, showReasoning, detail) && + !(thinking && entry.content === lastContent) && + !(onlyThinking && entry.content.type === "reasoning"), ) .map((entry) => ({ messageID: message.id, messageIndex, partID: entry.id, content: entry.content })), ) diff --git a/packages/session-ui/src/timeline/rows-current.test.ts b/packages/session-ui/src/timeline/rows-current.test.ts index 8571629a02f..808da81ad0b 100644 --- a/packages/session-ui/src/timeline/rows-current.test.ts +++ b/packages/session-ui/src/timeline/rows-current.test.ts @@ -5,6 +5,7 @@ import type { SessionMessageInfo, } from "@opencode/client/promise" import { storyDocument, storyTool } from "../storybook/current-session-scenarios" +import { timelinePresets } from "./detail" import { createTimelineProjection, Timeline, TimelineRow } from "./projection" describe("current session timeline rows", () => { @@ -267,6 +268,29 @@ describe("current session timeline rows", () => { }) }) + test("omits a grouped reasoning-only row while busy until other content or completion", () => { + const thought = { type: "reasoning", text: "Thinking", time: { created: 1, completed: 2 } } as const + const rows = (content: SessionMessageAssistant["content"], busy: boolean) => + createTimelineProjection({ + sessionMessages: storyDocument(content, busy).messages, + status: { type: busy ? "busy" : "idle" }, + reasoningMode: "compact", + timelineDetail: timelinePresets[2].value, + }).rows + + expect(rows([{ ...thought, time: { created: 1 } }], true).map((row) => row._tag)).toEqual(["UserMessage"]) + expect(rows([thought], true).map((row) => row._tag)).toEqual(["UserMessage"]) + expect(rows([thought, { type: "text", text: "Answer" }], true).map((row) => row._tag)).toEqual([ + "UserMessage", + "AssistantPart", + "AssistantPart", + ]) + expect( + rows([thought, storyTool("read", "read", "running", { filePath: "package.json" })], true).map((row) => row._tag), + ).toEqual(["UserMessage", "AssistantPart"]) + expect(rows([thought], false).map((row) => row._tag)).toEqual(["UserMessage", "AssistantPart"]) + }) + test("stops thinking on idle, message completion, errors and retries", () => { const document = storyDocument([{ type: "reasoning", text: "Current thought" }], true) expect(