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" },