From 785c58c8efdebcf33b568bb895113427180a0de2 Mon Sep 17 00:00:00 2001 From: Eivind Jonassen Date: Sun, 30 Aug 2026 20:59:49 +0200 Subject: [PATCH] Label in-flight transcript activity with present tense Reasoning, exploration, and shell labels now reflect whether the activity is still running. Reasoning settles from THINKING/Thinking to THOUGHT/Thought, exploration from Exploring to Explored, and running shells from Running to Ran. Settled reasoning falls back to the message completion time when a snapshot omits the part completion time. --- .../src/screens/session-transcript.test.tsx | 198 +++++++++++++++++- .../mobile/src/screens/session-transcript.tsx | 20 +- .../workspace-screen.integration.test.tsx | 12 +- 3 files changed, 219 insertions(+), 11 deletions(-) diff --git a/apps/mobile/src/screens/session-transcript.test.tsx b/apps/mobile/src/screens/session-transcript.test.tsx index c28c965..4d94a01 100644 --- a/apps/mobile/src/screens/session-transcript.test.tsx +++ b/apps/mobile/src/screens/session-transcript.test.tsx @@ -277,35 +277,107 @@ test("renders an unfinished code fence while assistant text streams", () => { expect(screen.queryByText(/```/)).toBeNull(); }); -test("renders short reasoning inline with bold markdown", () => { +test("renders finished short reasoning inline with bold markdown", () => { render( , ); expect(screen.getByText("THOUGHT")).toBeOnTheScreen(); + expect(screen.queryByText("THINKING")).toBeNull(); expect(screen.getByText("Adding mocks to repository tests")).toHaveStyle({ fontWeight: "800" }); expect(screen.queryByText(/\*\*/)).toBeNull(); expect(screen.queryByRole("button", { name: /Thought/ })).toBeNull(); }); +test("labels reasoning as thinking until the part completes", () => { + const { rerender } = render( + , + ); + + expect(screen.getByText("THINKING")).toBeOnTheScreen(); + expect(screen.queryByText("THOUGHT")).toBeNull(); + + rerender( + , + ); + + expect(screen.getByText("THOUGHT")).toBeOnTheScreen(); + expect(screen.queryByText("THINKING")).toBeNull(); +}); + +test("settles reasoning when the message completes without a part completion time", () => { + render( + , + ); + + expect(screen.queryByRole("button", { name: /Thinking/ })).toBeNull(); + fireEvent.press(screen.getByRole("button", { name: /Thought/ })); + expect(screen.getByText("First step\nSecond step")).toBeOnTheScreen(); +}); + test("keeps multiline reasoning in a disclosure", () => { render( , @@ -316,6 +388,122 @@ test("keeps multiline reasoning in a disclosure", () => { expect(screen.getByText("First step\nSecond step")).toBeOnTheScreen(); }); +test("labels an exploration group as exploring while a search is in flight", () => { + const { rerender } = render( + , + ); + + expect(screen.getByText("Exploring")).toBeOnTheScreen(); + expect(screen.getByText("2 searches · Running")).toBeOnTheScreen(); + expect(screen.queryByText("Explored")).toBeNull(); + + rerender( + , + ); + + expect(screen.getByText("Explored")).toBeOnTheScreen(); + expect(screen.getByText("2 searches")).toBeOnTheScreen(); + expect(screen.queryByText("Exploring")).toBeNull(); +}); + +test("labels a shell message as running until it exits", () => { + const { rerender } = render( + , + ); + + expect(screen.getByText("Running")).toBeOnTheScreen(); + expect(screen.getByText("pnpm test · Running")).toBeOnTheScreen(); + expect(screen.queryByText("Ran")).toBeNull(); + + rerender( + , + ); + + expect(screen.getByText("Ran")).toBeOnTheScreen(); + expect(screen.getByText("pnpm test")).toBeOnTheScreen(); + expect(screen.queryByText("Running")).toBeNull(); +}); + test("groups completed assistant activity and places narrative metadata in the footer", () => { const openDiff = jest.fn(); render( diff --git a/apps/mobile/src/screens/session-transcript.tsx b/apps/mobile/src/screens/session-transcript.tsx index 8293dc9..c08fb65 100644 --- a/apps/mobile/src/screens/session-transcript.tsx +++ b/apps/mobile/src/screens/session-transcript.tsx @@ -99,10 +99,11 @@ export const SessionTranscriptRow = memo(function SessionTranscriptRow({ ); } if (part.type === "reasoning") { + const settled = isSettledReasoning(message, part); return isInlineReasoning(part.text) ? ( setExpanded((current) => !current)} /> @@ -538,7 +539,7 @@ function ShellDisclosure({ largeText, message }: { largeText: boolean; message: canExpand={canExpand} detail={detail} expanded={expanded} - label={message.status === "exited" ? "Ran" : "Shell"} + label={message.status === "running" ? "Running" : "Ran"} largeText={largeText} onPress={() => setExpanded((current) => !current)} /> @@ -1206,6 +1207,10 @@ function activityGroupStatus(tools: AssistantTool[]) { return undefined; } +function isActivityInFlight(tools: AssistantTool[]) { + return tools.some((tool) => tool.state.status === "running" || tool.state.status === "streaming"); +} + function shellStatusLabel(message: ShellMessage) { if (message.status === "running") return "Running"; if (message.status === "timeout") return "Timed out"; @@ -1230,6 +1235,13 @@ function isInlineReasoning(text: string) { return text.length <= maxInlineReasoning && !/[\r\n]/.test(text); } +function isSettledReasoning( + message: AssistantMessage, + part: Extract, +) { + return part.time?.completed !== undefined || message.time.completed !== undefined; +} + function basename(path: string) { return path.split(/[\\/]/).filter(Boolean).at(-1) ?? "Location"; } diff --git a/apps/mobile/src/screens/workspace-screen.integration.test.tsx b/apps/mobile/src/screens/workspace-screen.integration.test.tsx index a7fb6b3..4574399 100644 --- a/apps/mobile/src/screens/workspace-screen.integration.test.tsx +++ b/apps/mobile/src/screens/workspace-screen.integration.test.tsx @@ -76,8 +76,16 @@ jest.mock("@opencode2-mobile/opencode-adapter", () => ({ agent: "build", content: [ { text: "Newest answer", type: "text" }, - { text: "Private reasoning", type: "reasoning" }, - { text: "Detailed reasoning\nSecond step", type: "reasoning" }, + { + text: "Private reasoning", + time: { completed: 4, created: 3 }, + type: "reasoning", + }, + { + text: "Detailed reasoning\nSecond step", + time: { completed: 5, created: 3 }, + type: "reasoning", + }, ], id: "msg_assistant", model: { id: "model-1", providerID: "provider" },