From 9d519f5e870c0b842f9476251f6feda96aa0ced7 Mon Sep 17 00:00:00 2001 From: draxxris <24984408+draxxris@users.noreply.github.com> Date: Wed, 30 Sep 2026 06:34:57 -0400 Subject: [PATCH 1/2] Render write tool content as readable file text (cherry picked from commit 8476b85b6579054a61a7da97592deb0eb7646dd0) --- components/MessageView.test.mjs | 26 ++++++++++++++++++++++++++ components/MessageView.tsx | 6 ++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/components/MessageView.test.mjs b/components/MessageView.test.mjs index f02622703a..08bd9e4435 100644 --- a/components/MessageView.test.mjs +++ b/components/MessageView.test.mjs @@ -18,6 +18,7 @@ const { } = await jiti.import("./MessageView.tsx"); const { I18nProvider } = await jiti.import("@/hooks/useI18n"); const { splitFinalAssistantBlocks } = await jiti.import("@/lib/message-display"); +const { clearExpandedToolCalls, setToolCallExpanded } = await jiti.import("@/lib/tool-call-expansion"); function renderMessage(message, props = {}) { return renderToStaticMarkup( @@ -128,6 +129,31 @@ test("keeps streamed tool input out of collapsed markup while counting it", () = assert.equal(getTokenEstimateText(block), block.rawInput); }); +test("renders write tool content as readable file text", () => { + const block = { + type: "toolCall", + toolCallId: "call-write-file", + toolName: "write", + input: { path: "src/example.ts", content: "first line\nsecond line\n" }, + }; + clearExpandedToolCalls(); + setToolCallExpanded(block.toolCallId, true); + try { + const html = renderMessage({ + role: "assistant", + provider: "anthropic", + model: "claude-test", + content: [block], + }); + + assert.ok(html.includes("src/example.ts")); + assert.match(html, /first line\nsecond line\n/); + assert.doesNotMatch(html, /"content":/); + } finally { + clearExpandedToolCalls(); + } +}); + test("renders subagents as standard tool calls with only an extra session button", () => { const block = { type: "toolCall", diff --git a/components/MessageView.tsx b/components/MessageView.tsx index b5d759d941..dd1cf0937d 100644 --- a/components/MessageView.tsx +++ b/components/MessageView.tsx @@ -11,7 +11,7 @@ import { parseCompactionSummary } from "@/lib/compaction-summary"; import { getAssistantErrorMessage, getThinkingPreview, hasAssistantAnswer, isAssistantTruncated, isEmptyThinkingBlock } from "@/lib/message-display"; import { parseUnifiedPatch, type SplitDiffCell, type SplitDiffFile } from "@/lib/patch"; import { applyPatchPreviewToFiles, applyPatchResultHasFailures, extractApplyPatchPaths, getApplyPatchInputText, parseApplyPatchInput } from "@/lib/apply-patch"; -import { isApplyPatchToolName, isEditToolName } from "@/lib/tool-names"; +import { isApplyPatchToolName, isEditToolName, isWriteToolName } from "@/lib/tool-names"; import { isToolCallExpanded, setToolCallExpanded } from "@/lib/tool-call-expansion"; import { isThinkingExpandedByDefault, THINKING_EXPANDED_EVENT } from "@/lib/thinking-expansion-preference"; import { TurnWrittenFiles } from "./TurnWrittenFiles"; @@ -1086,7 +1086,9 @@ function ToolCallBlock({ block, result, duration, onOpenSession }: { block: Tool setToolCallExpanded(block.toolCallId, next); setExpanded(next); }; - const inputStr = getToolCallInputText(block); + const inputStr = isWriteToolName(block.toolName) && typeof block.input.content === "string" + ? block.input.content + : getToolCallInputText(block); const isStreamingInput = block.rawInput !== undefined; const isEditTool = isEditToolName(block.toolName); const resultDiff = result && !result.isError ? getResultDiff(result) : null; From 2358693f8851a494f940321bd15e5713d7e4ac9b Mon Sep 17 00:00:00 2001 From: Alex Yang Date: Sun, 4 Oct 2026 14:45:43 +0900 Subject: [PATCH 2/2] fix(chat): keep write's JSON view when the file text would hide arguments Show the file text only for a write whose input is just path/file_path and content, so another argument (a mode, a title) stays visible. Streamed input, which is still incomplete JSON, and an empty file keep the generic view too. Drop the duplicate tool-call-expansion import the merge with main left in the test file, which stopped MessageView.test.mjs from loading. Co-Authored-By: Claude Sonnet 5.5 --- components/MessageView.test.mjs | 21 +++++++++++++++++++-- components/MessageView.tsx | 16 +++++++++++++--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/components/MessageView.test.mjs b/components/MessageView.test.mjs index cbcb0e923b..0925e076c5 100644 --- a/components/MessageView.test.mjs +++ b/components/MessageView.test.mjs @@ -154,6 +154,25 @@ test("renders write tool content as readable file text", () => { } }); +test("keeps the input JSON for a write with another argument, an empty file or streamed input", () => { + const cases = [ + { id: "call-write-mode", input: { path: "notes.md", content: "text", mode: "append" } }, + { id: "call-write-empty", input: { path: "empty.txt", content: "" } }, + { id: "call-write-streaming", input: {}, rawInput: "{\"path\":\"a.ts\",\"content\":\"one\\ntwo" }, + ]; + for (const { id, input, rawInput } of cases) { + const block = { type: "toolCall", toolCallId: id, toolName: "write", input, ...(rawInput === undefined ? {} : { rawInput }) }; + clearExpandedToolCalls(); + setToolCallExpanded(id, true); + try { + const html = renderMessage({ role: "assistant", provider: "anthropic", model: "claude-test", content: [block] }); + assert.equal(textOf(html).includes(getToolCallInputText(block)), true, id); + } finally { + clearExpandedToolCalls(); + } + } +}); + test("renders subagents as standard tool calls with only an extra session button", () => { const block = { type: "toolCall", @@ -480,8 +499,6 @@ test("uses the unanswered truncation notice for an empty length reply", () => { assert.doesNotMatch(html, /follow-up/i); }); -const { setToolCallExpanded } = await jiti.import("@/lib/tool-call-expansion"); - function textOf(html) { return html.replace(/<[^>]+>/g, "").replace(/"/g, "\"").replace(/&/g, "&").replace(/'/g, "'"); } diff --git a/components/MessageView.tsx b/components/MessageView.tsx index 4aa8b0d26d..caa88e2b00 100644 --- a/components/MessageView.tsx +++ b/components/MessageView.tsx @@ -1120,9 +1120,7 @@ function ToolCallBlock({ block, result, duration, onOpenSession }: { block: Tool setToolCallExpanded(block.toolCallId, next); setExpanded(next); }; - const inputStr = isWriteToolName(block.toolName) && typeof block.input.content === "string" - ? block.input.content - : getToolCallInputText(block); + const inputStr = getWrittenFileText(block) ?? getToolCallInputText(block); const isStreamingInput = block.rawInput !== undefined; const isEditTool = isEditToolName(block.toolName); const resultDiff = result && !result.isError ? getResultDiff(result) : null; @@ -1913,6 +1911,18 @@ export function getToolCallInputText(block: ToolCallContent): string { return block.rawInput ?? JSON.stringify(block.input, null, 2); } +const WRITE_VIEW_KEYS = new Set(["path", "file_path", "content"]); + +// A write's file text in place of its JSON. Streamed input is still incomplete +// JSON, and any other argument (a mode, a title) would vanish from this view, +// so those calls, and an empty file, keep the generic view. +function getWrittenFileText(block: ToolCallContent): string | null { + if (block.rawInput !== undefined || !isWriteToolName(block.toolName)) return null; + const { content } = block.input; + if (typeof content !== "string" || content === "") return null; + return Object.keys(block.input).every((key) => WRITE_VIEW_KEYS.has(key)) ? content : null; +} + function formatCustomType(type: string): string { return type || "extension"; }