fix(app): keep Working during reasoning-only turns (#51090)

Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
This commit is contained in:
opencode-agent[bot]
2026-10-02 12:14:04 +08:00
committed by GitHub
Unverified
parent 77c5f7a516
commit 81efb7f146
3 changed files with 82 additions and 3 deletions
@@ -543,6 +543,26 @@ test.describe("Working", () => {
await expect(working).toHaveCount(0)
})
test("keeps Working instead of Used 1 Thought while reasoning is the only activity", async ({ page }) => {
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([reasoningPart("prt_only_thought", "Still considering the answer.")], { completed: false }),
],
settings: { timelineDetail: detailed },
})
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" })))
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, {
+14 -3
View File
@@ -308,15 +308,25 @@ export namespace Timeline {
const previousUserMessage = index > 0
const compaction = entries.some((entry) => entry.type === "notice" && entry.message.type === "compaction")
const lastContent = lastAssistant?.content.at(-1)
const thinking =
(detail ? detail.thinking.placement === "separate" : showReasoning) &&
const working =
isActive &&
status.type === "busy" &&
lastAssistant?.time.completed === undefined &&
!lastAssistant?.error &&
!lastAssistant?.retry &&
!lastAssistant?.retry
const thinking =
working &&
(detail ? detail.thinking.placement === "separate" : showReasoning) &&
lastContent?.type === "reasoning" &&
lastContent.time?.completed === undefined
const thoughtOnly =
working &&
detail?.thinking.placement === "grouped" &&
assistantMessages.every((message) =>
message.content.every(
(content) => content.type === "reasoning" || !isRenderable(content, showReasoning, detail),
),
)
if (previousUserMessage) rows.push(new TimelineRow.TurnGap({ userMessageID: turnID }))
if (userMessage) rows.push(new TimelineRow.UserMessage({ userMessageID: turnID }))
@@ -326,6 +336,7 @@ export namespace Timeline {
// An assistant message can produce several rows because its content parts are
// rendered separately. Notices end a segment so none of those rows cross it.
const appendAssistantSegment = (messages: SessionMessageAssistant[]) => {
if (thoughtOnly) return
const refs = messages.flatMap((message, messageIndex) =>
contentEntries(message)
.filter(
@@ -264,6 +264,54 @@ 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: "" }], 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"])
const failed = storyDocument([thought], true)
const messages = failed.messages.map((message) =>
message.type === "assistant" ? { ...message, error: { type: "provider.error", message: "Failed" } } : message,
)
expect(
createTimelineProjection({
sessionMessages: messages,
status: failed.status,
reasoningMode: "compact",
timelineDetail: timelinePresets[2].value,
}).rows.map((row) => row._tag),
).toEqual(["UserMessage", "AssistantPart", "Error"])
expect(
createTimelineProjection({
sessionMessages: failed.messages.map((message) =>
message.type === "assistant" ? { ...message, error: { type: "Interrupted", message: "Stopped" } } : message,
),
status: failed.status,
reasoningMode: "compact",
timelineDetail: { ...timelinePresets[2].value, notices: { placement: "hidden" } },
}).rows.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(