From 0eedb593dd175e6d8c2d5e03f88027f637b9e6b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:36:49 +0200 Subject: [PATCH 1/2] feat(cli): show reasoning stream in the TUI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Ink TUI dropped say:reasoning at two points (useChatMessages filter + a ChatMessage early return), so the model's chain-of-thought was never visible interactively — even though the core streams it and the ACP path already surfaces it as agent_thought_chunk. Render reasoning dimmed + italic (distinct from the answer), streamed live in the dynamic zone and kept in scroll history. Also show full command output once a command completes (was truncated to 8 lines), keeping a bounded tail while it runs. Adds ChatMessage reasoning render tests. --- cli/src/components/ChatMessage.test.tsx | 32 +++++++++++++++++++++++++ cli/src/components/ChatMessage.tsx | 16 ++++++++++--- cli/src/hooks/useChatMessages.ts | 2 +- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/cli/src/components/ChatMessage.test.tsx b/cli/src/components/ChatMessage.test.tsx index 91ee5ec1..8ffc2114 100644 --- a/cli/src/components/ChatMessage.test.tsx +++ b/cli/src/components/ChatMessage.test.tsx @@ -104,3 +104,35 @@ 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("") + }) +}) diff --git a/cli/src/components/ChatMessage.tsx b/cli/src/components/ChatMessage.tsx index ecf8a890..4ec6f8c0 100644 --- a/cli/src/components/ChatMessage.tsx +++ b/cli/src/components/ChatMessage.tsx @@ -311,9 +311,19 @@ export const ChatMessage: React.FC = ({ message, mode, isStrea ) } - // Assistant text response (hide reasoning traces - they're verbose and clutter the UI) + // 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 +464,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} diff --git a/cli/src/hooks/useChatMessages.ts b/cli/src/hooks/useChatMessages.ts index 36727249..cfc6429d 100644 --- a/cli/src/hooks/useChatMessages.ts +++ b/cli/src/hooks/useChatMessages.ts @@ -13,7 +13,7 @@ export function useChatMessages(messages: any[]) { 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 + // reasoning (chain-of-thought) is shown — rendered dimmed in ChatMessage return true }) const withHooks = combineHookSequences(filtered) From 69cbfb73402e00a65c1d1dc404fd3c387b9e0b73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Mon, 1 Jun 2026 13:04:01 +0200 Subject: [PATCH 2/2] feat(cli): surface activity journal in the TUI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stop filtering api_req_started, checkpoint_created and api_req_retried in the TUI and render them as compact dim markers (→ querying , ✓ checkpoint saved, ⟳ retrying) so the user sees what the agent is doing between answers. Replaces the old api_req_started null-return that hid the request entirely. Adds render tests. --- cli/src/components/ChatMessage.test.tsx | 26 +++++++++++++++++ cli/src/components/ChatMessage.tsx | 37 +++++++++++++++++++++---- cli/src/hooks/useChatMessages.ts | 7 ++--- 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/cli/src/components/ChatMessage.test.tsx b/cli/src/components/ChatMessage.test.tsx index 8ffc2114..9b828421 100644 --- a/cli/src/components/ChatMessage.test.tsx +++ b/cli/src/components/ChatMessage.test.tsx @@ -136,3 +136,29 @@ describe("ChatMessage reasoning rendering", () => { 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 4ec6f8c0..cfe32bcd 100644 --- a/cli/src/components/ChatMessage.tsx +++ b/cli/src/components/ChatMessage.tsx @@ -311,6 +311,37 @@ export const ChatMessage: React.FC = ({ message, mode, isStrea ) } + // 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") { @@ -612,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 cfc6429d..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 - // reasoning (chain-of-thought) is shown — rendered dimmed in ChatMessage + // 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)