diff --git a/src/browser/App.stories.tsx b/src/browser/App.stories.tsx index b03c5f51c..a734844ce 100644 --- a/src/browser/App.stories.tsx +++ b/src/browser/App.stories.tsx @@ -871,6 +871,34 @@ export const ActiveWorkspaceWithChat: Story = { }, }); + // Assistant quick update with a single-line reasoning trace to exercise inline display + callback({ + id: "msg-9a", + role: "assistant", + parts: [ + { + type: "reasoning", + text: "Cache is warm already; rerunning the full suite would be redundant.", + }, + { + type: "text", + text: "Cache is warm from the last test run, so I'll shift focus to documentation next.", + }, + ], + metadata: { + historySequence: 10, + timestamp: STABLE_TIMESTAMP - 165000, + model: "anthropic:claude-sonnet-4-5", + usage: { + inputTokens: 1200, + outputTokens: 180, + totalTokens: 1380, + reasoningTokens: 20, + }, + duration: 900, + }, + }); + // Assistant message with status_set tool to show agent status callback({ id: "msg-10", @@ -899,7 +927,7 @@ export const ActiveWorkspaceWithChat: Story = { }, ], metadata: { - historySequence: 10, + historySequence: 11, timestamp: STABLE_TIMESTAMP - 160000, model: "anthropic:claude-sonnet-4-5", usage: { @@ -922,7 +950,7 @@ export const ActiveWorkspaceWithChat: Story = { }, ], metadata: { - historySequence: 11, + historySequence: 12, timestamp: STABLE_TIMESTAMP - 150000, }, }); @@ -936,7 +964,7 @@ export const ActiveWorkspaceWithChat: Story = { workspaceId: workspaceId, messageId: "msg-12", model: "anthropic:claude-sonnet-4-5", - historySequence: 12, + historySequence: 13, }); // Send reasoning delta diff --git a/src/browser/components/Messages/MarkdownRenderer.tsx b/src/browser/components/Messages/MarkdownRenderer.tsx index 9eef6e3ef..1eb4f7543 100644 --- a/src/browser/components/Messages/MarkdownRenderer.tsx +++ b/src/browser/components/Messages/MarkdownRenderer.tsx @@ -5,11 +5,16 @@ import { cn } from "@/common/lib/utils"; interface MarkdownRendererProps { content: string; className?: string; + style?: React.CSSProperties; } -export const MarkdownRenderer: React.FC = ({ content, className }) => { +export const MarkdownRenderer: React.FC = ({ + content, + className, + style, +}) => { return ( -
+
); diff --git a/src/browser/components/Messages/ReasoningMessage.stories.tsx b/src/browser/components/Messages/ReasoningMessage.stories.tsx index 19253b7fb..0c7d50c6a 100644 --- a/src/browser/components/Messages/ReasoningMessage.stories.tsx +++ b/src/browser/components/Messages/ReasoningMessage.stories.tsx @@ -135,3 +135,13 @@ export const EmptyContent: Story = { message: createReasoningMessage(""), }, }; +export const ExpandablePreview: Story = { + args: { + message: createReasoningMessage( + "Assessing quicksort mechanics and choosing example array...\n" + + "Plan: explain pivot selection, partitioning, recursion, base case.\n" + + "Next, I'll outline best practices for implementing the partition step.", + { isStreaming: false } + ), + }, +}; diff --git a/src/browser/components/Messages/ReasoningMessage.tsx b/src/browser/components/Messages/ReasoningMessage.tsx index 7c7d62497..5b96d42da 100644 --- a/src/browser/components/Messages/ReasoningMessage.tsx +++ b/src/browser/components/Messages/ReasoningMessage.tsx @@ -16,6 +16,14 @@ export const ReasoningMessage: React.FC = ({ message, cla const content = message.content; const isStreaming = message.isStreaming; + const trimmedContent = content?.trim() ?? ""; + const hasContent = trimmedContent.length > 0; + const summaryLine = hasContent ? (trimmedContent.split(/\r?\n/)[0] ?? "") : ""; + const hasAdditionalLines = hasContent && /[\r\n]/.test(trimmedContent); + // OpenAI models often emit terse, single-line traces; surface them inline instead of hiding behind the label. + const isSingleLineTrace = !isStreaming && hasContent && !hasAdditionalLines; + const isCollapsible = !isStreaming && hasContent && hasAdditionalLines; + const showEllipsis = isCollapsible && !isExpanded; // Auto-collapse when streaming ends useEffect(() => { @@ -25,9 +33,11 @@ export const ReasoningMessage: React.FC = ({ message, cla }, [isStreaming]); const toggleExpanded = () => { - if (!isStreaming) { - setIsExpanded(!isExpanded); + if (!isCollapsible) { + return; } + + setIsExpanded(!isExpanded); }; // Render appropriate content based on state @@ -55,24 +65,44 @@ export const ReasoningMessage: React.FC = ({ message, cla >
-
+
- +
{isStreaming ? ( Thinking... + ) : hasContent ? ( + ) : ( - "Thought..." + "Thought" )} - + {showEllipsis && ( + + ... + + )} +
- {!isStreaming && ( + {isCollapsible && ( = ({ message, cla )}
- {isExpanded && ( + {isExpanded && !isSingleLineTrace && (
{renderContent()}
diff --git a/src/node/services/mock/scenarios/toolFlows.ts b/src/node/services/mock/scenarios/toolFlows.ts index 905605395..6b96e2c57 100644 --- a/src/node/services/mock/scenarios/toolFlows.ts +++ b/src/node/services/mock/scenarios/toolFlows.ts @@ -317,7 +317,7 @@ const reasoningQuicksortTurn: ScenarioTurn = { { kind: "reasoning-delta", delay: STREAM_BASE_DELAY, - text: "Assessing quicksort mechanics and choosing example array...", + text: "Assessing quicksort mechanics and choosing example array...\n", }, { kind: "reasoning-delta", diff --git a/tests/e2e/scenarios/toolFlows.spec.ts b/tests/e2e/scenarios/toolFlows.spec.ts index 42ae704f3..5e1ed07bc 100644 --- a/tests/e2e/scenarios/toolFlows.spec.ts +++ b/tests/e2e/scenarios/toolFlows.spec.ts @@ -141,11 +141,18 @@ test.describe("tool and reasoning flows", () => { } const transcript = page.getByRole("log", { name: "Conversation transcript" }); - const thinkingHeader = transcript.getByText("Thought..."); - await expect(thinkingHeader).toBeVisible(); - await thinkingHeader.click(); + const reasoningPreview = transcript + .getByText("Assessing quicksort mechanics and choosing example array...") + .first(); + await expect(reasoningPreview).toBeVisible(); + + const ellipsisIndicator = transcript.getByTestId("reasoning-ellipsis").first(); + await expect(ellipsisIndicator).toBeVisible(); + + await reasoningPreview.click(); + await expect( - transcript.getByText("Assessing quicksort mechanics and choosing example array...") + transcript.getByText("Plan: explain pivot selection, partitioning, recursion, base case.") ).toBeVisible(); await ui.chat.expectTranscriptContains("Quicksort works by picking a pivot"); });