diff --git a/src-tauri/src/claude_cli.rs b/src-tauri/src/claude_cli.rs
index 3876ccf2..726dfd5f 100644
--- a/src-tauri/src/claude_cli.rs
+++ b/src-tauri/src/claude_cli.rs
@@ -19,6 +19,8 @@ pub enum ClaudeStreamEvent {
Init { session_id: String },
/// Incremental text chunk.
TextDelta { text: String },
+ /// Incremental thinking/reasoning chunk.
+ ThinkingDelta { text: String },
/// A tool call started (agent mode only).
ToolStart {
tool_name: String,
@@ -402,6 +404,13 @@ where
});
}
}
+ Some("thinking_delta") => {
+ if let Some(text) = delta["thinking"].as_str() {
+ emit(ClaudeStreamEvent::ThinkingDelta {
+ text: text.to_string(),
+ });
+ }
+ }
Some("input_json_delta") => {
if let (Some(partial), Some(ref tid)) =
(delta["partial_json"].as_str(), &state.current_tool_id)
diff --git a/src/components/AiMessage.test.tsx b/src/components/AiMessage.test.tsx
index aa8a040f..7137c34d 100644
--- a/src/components/AiMessage.test.tsx
+++ b/src/components/AiMessage.test.tsx
@@ -2,15 +2,20 @@ import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import { AiMessage } from './AiMessage'
+vi.mock('./MarkdownContent', () => ({
+ MarkdownContent: ({ content }: { content: string }) =>
{content}
,
+}))
+
describe('AiMessage', () => {
it('renders user message', () => {
render()
expect(screen.getByText('Hello AI')).toBeTruthy()
})
- it('renders response text', () => {
- render()
- expect(screen.getByText('Here is the answer')).toBeTruthy()
+ it('renders response as markdown', () => {
+ render()
+ expect(screen.getByTestId('markdown-content')).toBeTruthy()
+ expect(screen.getByText('Here is the **answer**')).toBeTruthy()
})
it('shows undo button with response', () => {
@@ -18,23 +23,31 @@ describe('AiMessage', () => {
expect(screen.getByTestId('undo-button')).toBeTruthy()
})
- it('renders reasoning toggle collapsed by default', () => {
- render()
+ it('shows reasoning expanded while streaming (reasoningDone=false)', () => {
+ render()
expect(screen.getByTestId('reasoning-toggle')).toBeTruthy()
- expect(screen.queryByTestId('reasoning-content')).toBeNull()
- })
-
- it('expands reasoning on toggle click', () => {
- render()
- fireEvent.click(screen.getByTestId('reasoning-toggle'))
expect(screen.getByTestId('reasoning-content')).toBeTruthy()
expect(screen.getByText('Thinking about it...')).toBeTruthy()
})
- it('collapses reasoning on second click', () => {
- render()
+ it('auto-collapses reasoning when reasoningDone=true', () => {
+ render()
+ expect(screen.getByTestId('reasoning-toggle')).toBeTruthy()
+ expect(screen.queryByTestId('reasoning-content')).toBeNull()
+ })
+
+ it('expands collapsed reasoning on toggle click', () => {
+ render()
+ // Starts collapsed (reasoningDone=true)
+ expect(screen.queryByTestId('reasoning-content')).toBeNull()
fireEvent.click(screen.getByTestId('reasoning-toggle'))
expect(screen.getByTestId('reasoning-content')).toBeTruthy()
+ })
+
+ it('collapses expanded reasoning on toggle click', () => {
+ render()
+ // Starts expanded (reasoningDone=false)
+ expect(screen.getByTestId('reasoning-content')).toBeTruthy()
fireEvent.click(screen.getByTestId('reasoning-toggle'))
expect(screen.queryByTestId('reasoning-content')).toBeNull()
})
diff --git a/src/components/AiMessage.tsx b/src/components/AiMessage.tsx
index 3202cb12..a47c24c9 100644
--- a/src/components/AiMessage.tsx
+++ b/src/components/AiMessage.tsx
@@ -1,6 +1,7 @@
-import { useState, useCallback } from 'react'
+import { useState, useCallback, useEffect, useRef } from 'react'
import { CaretRight, CaretDown, Brain, ArrowCounterClockwise } from '@phosphor-icons/react'
import { AiActionCard, type AiActionStatus } from './AiActionCard'
+import { MarkdownContent } from './MarkdownContent'
export interface AiAction {
tool: string
@@ -15,6 +16,7 @@ export interface AiAction {
export interface AiMessageProps {
userMessage: string
reasoning?: string
+ reasoningDone?: boolean
actions: AiAction[]
response?: string
isStreaming?: boolean
@@ -44,6 +46,14 @@ function UserBubble({ content }: { content: string }) {
function ReasoningBlock({ text, expanded, onToggle }: {
text: string; expanded: boolean; onToggle: () => void
}) {
+ const contentRef = useRef(null)
+
+ useEffect(() => {
+ if (expanded && contentRef.current) {
+ contentRef.current.scrollTop = contentRef.current.scrollHeight
+ }
+ }, [expanded, text])
+
return (