Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions components/MessageView.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -128,6 +129,50 @@ 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("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",
Expand Down Expand Up @@ -454,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(/&quot;/g, "\"").replace(/&amp;/g, "&").replace(/&#x27;/g, "'");
}
Expand Down
16 changes: 14 additions & 2 deletions components/MessageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -1120,7 +1120,7 @@ function ToolCallBlock({ block, result, duration, onOpenSession }: { block: Tool
setToolCallExpanded(block.toolCallId, next);
setExpanded(next);
};
const inputStr = 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;
Expand Down Expand Up @@ -1911,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";
}
Expand Down
Loading