Skip to content

Commit a9fd4ca

Browse files
authored
🤖 feat: inline single-line reasoning trace display (#614)
## Summary - render single-line reasoning traces inline with full markdown formatting and skip the expand affordance when there’s no additional content - exercise the scenario in ActiveWorkspaceWithChat so the full-app Storybook captures it ## Testing - Not run (not requested) _Generated with `mux`_
1 parent af7d802 commit a9fd4ca

File tree

6 files changed

+101
-21
lines changed

6 files changed

+101
-21
lines changed

src/browser/App.stories.tsx

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,34 @@ export const ActiveWorkspaceWithChat: Story = {
871871
},
872872
});
873873

874+
// Assistant quick update with a single-line reasoning trace to exercise inline display
875+
callback({
876+
id: "msg-9a",
877+
role: "assistant",
878+
parts: [
879+
{
880+
type: "reasoning",
881+
text: "Cache is warm already; rerunning the full suite would be redundant.",
882+
},
883+
{
884+
type: "text",
885+
text: "Cache is warm from the last test run, so I'll shift focus to documentation next.",
886+
},
887+
],
888+
metadata: {
889+
historySequence: 10,
890+
timestamp: STABLE_TIMESTAMP - 165000,
891+
model: "anthropic:claude-sonnet-4-5",
892+
usage: {
893+
inputTokens: 1200,
894+
outputTokens: 180,
895+
totalTokens: 1380,
896+
reasoningTokens: 20,
897+
},
898+
duration: 900,
899+
},
900+
});
901+
874902
// Assistant message with status_set tool to show agent status
875903
callback({
876904
id: "msg-10",
@@ -899,7 +927,7 @@ export const ActiveWorkspaceWithChat: Story = {
899927
},
900928
],
901929
metadata: {
902-
historySequence: 10,
930+
historySequence: 11,
903931
timestamp: STABLE_TIMESTAMP - 160000,
904932
model: "anthropic:claude-sonnet-4-5",
905933
usage: {
@@ -922,7 +950,7 @@ export const ActiveWorkspaceWithChat: Story = {
922950
},
923951
],
924952
metadata: {
925-
historySequence: 11,
953+
historySequence: 12,
926954
timestamp: STABLE_TIMESTAMP - 150000,
927955
},
928956
});
@@ -936,7 +964,7 @@ export const ActiveWorkspaceWithChat: Story = {
936964
workspaceId: workspaceId,
937965
messageId: "msg-12",
938966
model: "anthropic:claude-sonnet-4-5",
939-
historySequence: 12,
967+
historySequence: 13,
940968
});
941969

942970
// Send reasoning delta

src/browser/components/Messages/MarkdownRenderer.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@ import { cn } from "@/common/lib/utils";
55
interface MarkdownRendererProps {
66
content: string;
77
className?: string;
8+
style?: React.CSSProperties;
89
}
910

10-
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, className }) => {
11+
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
12+
content,
13+
className,
14+
style,
15+
}) => {
1116
return (
12-
<div className={cn("markdown-content", className)}>
17+
<div className={cn("markdown-content", className)} style={style}>
1318
<MarkdownCore content={content} />
1419
</div>
1520
);

src/browser/components/Messages/ReasoningMessage.stories.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,13 @@ export const EmptyContent: Story = {
135135
message: createReasoningMessage(""),
136136
},
137137
};
138+
export const ExpandablePreview: Story = {
139+
args: {
140+
message: createReasoningMessage(
141+
"Assessing quicksort mechanics and choosing example array...\n" +
142+
"Plan: explain pivot selection, partitioning, recursion, base case.\n" +
143+
"Next, I'll outline best practices for implementing the partition step.",
144+
{ isStreaming: false }
145+
),
146+
},
147+
};

src/browser/components/Messages/ReasoningMessage.tsx

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ export const ReasoningMessage: React.FC<ReasoningMessageProps> = ({ message, cla
1616

1717
const content = message.content;
1818
const isStreaming = message.isStreaming;
19+
const trimmedContent = content?.trim() ?? "";
20+
const hasContent = trimmedContent.length > 0;
21+
const summaryLine = hasContent ? (trimmedContent.split(/\r?\n/)[0] ?? "") : "";
22+
const hasAdditionalLines = hasContent && /[\r\n]/.test(trimmedContent);
23+
// OpenAI models often emit terse, single-line traces; surface them inline instead of hiding behind the label.
24+
const isSingleLineTrace = !isStreaming && hasContent && !hasAdditionalLines;
25+
const isCollapsible = !isStreaming && hasContent && hasAdditionalLines;
26+
const showEllipsis = isCollapsible && !isExpanded;
1927

2028
// Auto-collapse when streaming ends
2129
useEffect(() => {
@@ -25,9 +33,11 @@ export const ReasoningMessage: React.FC<ReasoningMessageProps> = ({ message, cla
2533
}, [isStreaming]);
2634

2735
const toggleExpanded = () => {
28-
if (!isStreaming) {
29-
setIsExpanded(!isExpanded);
36+
if (!isCollapsible) {
37+
return;
3038
}
39+
40+
setIsExpanded(!isExpanded);
3141
};
3242

3343
// Render appropriate content based on state
@@ -55,24 +65,44 @@ export const ReasoningMessage: React.FC<ReasoningMessageProps> = ({ message, cla
5565
>
5666
<div
5767
className={cn(
58-
"flex cursor-pointer items-center justify-between gap-2 select-none",
59-
isExpanded && "mb-1.5"
68+
"flex items-center justify-between gap-2 select-none",
69+
isCollapsible && "cursor-pointer",
70+
isExpanded && !isSingleLineTrace && "mb-1.5"
6071
)}
61-
onClick={toggleExpanded}
72+
onClick={isCollapsible ? toggleExpanded : undefined}
6273
>
63-
<div className="text-thinking-mode flex items-center gap-1 text-xs opacity-80">
74+
<div
75+
className={cn(
76+
"flex flex-1 items-center gap-1 min-w-0 text-xs opacity-80",
77+
"text-thinking-mode"
78+
)}
79+
>
6480
<span className="text-xs">
6581
<Lightbulb className={cn("size-3.5", isStreaming && "animate-pulse")} />
6682
</span>
67-
<span>
83+
<div className="flex min-w-0 items-center gap-1 truncate">
6884
{isStreaming ? (
6985
<Shimmer colorClass="var(--color-thinking-mode)">Thinking...</Shimmer>
86+
) : hasContent ? (
87+
<MarkdownRenderer
88+
content={summaryLine}
89+
className="truncate [&_*]:inline [&_*]:align-baseline [&_*]:whitespace-nowrap"
90+
style={{ fontSize: 12, lineHeight: "18px" }}
91+
/>
7092
) : (
71-
"Thought..."
93+
"Thought"
7294
)}
73-
</span>
95+
{showEllipsis && (
96+
<span
97+
className="text-[11px] tracking-widest text-[color:var(--color-text)] opacity-70"
98+
data-testid="reasoning-ellipsis"
99+
>
100+
...
101+
</span>
102+
)}
103+
</div>
74104
</div>
75-
{!isStreaming && (
105+
{isCollapsible && (
76106
<span
77107
className={cn(
78108
"text-thinking-mode opacity-60 transition-transform duration-200 ease-in-out text-xs",
@@ -84,7 +114,7 @@ export const ReasoningMessage: React.FC<ReasoningMessageProps> = ({ message, cla
84114
)}
85115
</div>
86116

87-
{isExpanded && (
117+
{isExpanded && !isSingleLineTrace && (
88118
<div className="font-primary text-sm leading-6 italic opacity-85 [&_p]:mt-0 [&_p]:mb-1 [&_p:last-child]:mb-0">
89119
{renderContent()}
90120
</div>

src/node/services/mock/scenarios/toolFlows.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ const reasoningQuicksortTurn: ScenarioTurn = {
317317
{
318318
kind: "reasoning-delta",
319319
delay: STREAM_BASE_DELAY,
320-
text: "Assessing quicksort mechanics and choosing example array...",
320+
text: "Assessing quicksort mechanics and choosing example array...\n",
321321
},
322322
{
323323
kind: "reasoning-delta",

tests/e2e/scenarios/toolFlows.spec.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,18 @@ test.describe("tool and reasoning flows", () => {
141141
}
142142

143143
const transcript = page.getByRole("log", { name: "Conversation transcript" });
144-
const thinkingHeader = transcript.getByText("Thought...");
145-
await expect(thinkingHeader).toBeVisible();
146-
await thinkingHeader.click();
144+
const reasoningPreview = transcript
145+
.getByText("Assessing quicksort mechanics and choosing example array...")
146+
.first();
147+
await expect(reasoningPreview).toBeVisible();
148+
149+
const ellipsisIndicator = transcript.getByTestId("reasoning-ellipsis").first();
150+
await expect(ellipsisIndicator).toBeVisible();
151+
152+
await reasoningPreview.click();
153+
147154
await expect(
148-
transcript.getByText("Assessing quicksort mechanics and choosing example array...")
155+
transcript.getByText("Plan: explain pivot selection, partitioning, recursion, base case.")
149156
).toBeVisible();
150157
await ui.chat.expectTranscriptContains("Quicksort works by picking a pivot");
151158
});

0 commit comments

Comments
 (0)