diff --git a/cli/src/components/ChatMessage.test.tsx b/cli/src/components/ChatMessage.test.tsx index 91ee5ec1..9b828421 100644 --- a/cli/src/components/ChatMessage.test.tsx +++ b/cli/src/components/ChatMessage.test.tsx @@ -104,3 +104,61 @@ describe("ChatMessage subagent rendering", () => { expect(frame).toContain("5 tool uses · 28.9k tokens · $0.00") }) }) + +describe("ChatMessage reasoning rendering", () => { + it("renders the reasoning (chain-of-thought) text instead of hiding it", () => { + const message: DiracMessage = { + ts: Date.now(), + type: "say", + say: "reasoning", + text: "Let me first inspect the failing test before editing.", + } + const { lastFrame } = render(React.createElement(ChatMessage, { message, mode: "act" })) + const frame = lastFrame() || "" + expect(frame).toContain("Let me first inspect the failing test before editing.") + }) + + it("renders streaming reasoning while partial", () => { + const message: DiracMessage = { + ts: Date.now(), + type: "say", + say: "reasoning", + text: "Thinking through the approach", + partial: true, + } + const { lastFrame } = render(React.createElement(ChatMessage, { message, mode: "act", isStreaming: true })) + expect(lastFrame() || "").toContain("Thinking through the approach") + }) + + it("renders nothing for empty reasoning", () => { + const message: DiracMessage = { ts: Date.now(), type: "say", say: "reasoning", text: " " } + const { lastFrame } = render(React.createElement(ChatMessage, { message, mode: "act" })) + expect((lastFrame() || "").trim()).toBe("") + }) +}) + +describe("ChatMessage activity journal", () => { + it("shows the queried model for api_req_started", () => { + const message = { + ts: Date.now(), + type: "say", + say: "api_req_started", + text: "{}", + modelInfo: { modelId: "ailiance-reasoning-r1" }, + } as unknown as DiracMessage + const { lastFrame } = render(React.createElement(ChatMessage, { message, mode: "act" })) + expect(lastFrame() || "").toContain("querying ailiance-reasoning-r1") + }) + + it("shows a checkpoint marker", () => { + const message: DiracMessage = { ts: Date.now(), type: "say", say: "checkpoint_created" } + const { lastFrame } = render(React.createElement(ChatMessage, { message, mode: "act" })) + expect(lastFrame() || "").toContain("checkpoint saved") + }) + + it("shows a retry marker", () => { + const message: DiracMessage = { ts: Date.now(), type: "say", say: "api_req_retried" } + const { lastFrame } = render(React.createElement(ChatMessage, { message, mode: "act" })) + expect(lastFrame() || "").toContain("retrying") + }) +}) diff --git a/cli/src/components/ChatMessage.tsx b/cli/src/components/ChatMessage.tsx index ecf8a890..cfe32bcd 100644 --- a/cli/src/components/ChatMessage.tsx +++ b/cli/src/components/ChatMessage.tsx @@ -311,9 +311,50 @@ export const ChatMessage: React.FC = ({ message, mode, isStrea ) } - // Assistant text response (hide reasoning traces - they're verbose and clutter the UI) + // Activity journal — compact dim markers so the user sees what the agent is + // doing between answers (which model is queried, checkpoints, retries). + if (say === "api_req_started") { + const modelId = (message as { modelInfo?: { modelId?: string } }).modelInfo?.modelId + return ( + + + → {modelId ? `querying ${modelId}` : "querying model"} + + + ) + } + if (say === "checkpoint_created") { + return ( + + + ✓ checkpoint saved + + + ) + } + if (say === "api_req_retried") { + return ( + + + ⟳ retrying request… + + + ) + } + + // Reasoning / chain-of-thought. Streamed live and kept in history, dimmed + // and italic so it stays visually distinct from the assistant's answer. if (say === "reasoning") { - return null + if (!text?.trim()) return null + return ( + + + + {text} + + + + ) } if (say === "text") { if (!text?.trim()) return null @@ -454,7 +495,7 @@ export const ChatMessage: React.FC = ({ message, mode, isStrea )} {output && ( - {formatToolResult(output, 8).map((line, idx) => ( + {formatToolResult(output, isExecuting ? 20 : Number.MAX_SAFE_INTEGER).map((line, idx) => ( {line} @@ -602,11 +643,7 @@ export const ChatMessage: React.FC = ({ message, mode, isStrea ) } - // API request info (show cost/tokens inline) - if (say === "api_req_started" && text) { - // Skip showing these - they're summarized in the status bar - return null - } + // (api_req_started is handled earlier as a compact activity marker.) // Browser actions if (say === "browser_action" || say === "browser_action_launch") { diff --git a/cli/src/hooks/useChatMessages.ts b/cli/src/hooks/useChatMessages.ts index 36727249..f74f16d6 100644 --- a/cli/src/hooks/useChatMessages.ts +++ b/cli/src/hooks/useChatMessages.ts @@ -10,10 +10,9 @@ export function useChatMessages(messages: any[]) { const displayMessages = useMemo(() => { const filtered = messages.filter((m) => { if (m.say === "api_req_finished") return false - if (m.say === "checkpoint_created") return false - if (m.say === "api_req_started") return false - if (m.say === "api_req_retried") return false - if (m.say === "reasoning") return false + // Activity journal: api_req_started, checkpoint_created, api_req_retried + // and reasoning are all surfaced — rendered as compact dim lines in + // ChatMessage so the user sees what the agent is doing. return true }) const withHooks = combineHookSequences(filtered)