fix: render Write tool calls that use path/file_text aliases (#1161)

Claude Code 2.1.280 (SDK 0.3.280, bumped in #1157) now accepts Write
calls where the model sends `path` instead of `file_path`, or
`file_text`/`file_content` instead of `content`. The CLI renames these
for its own validation, but the streamed `tool_use` block keeps the
model's original field names. So we showed the call as "Preparing file…"
with no diff or location, even though the write went through.

`toolInfoFromToolUse` now reads those fields the same way the CLI does.
When both spellings are present, the canonical fields win.

Nothing else in the 0.3.280 / 2.1.280 changelog needed an adapter
change. I checked Opus 5.5 model rows, the hand-back frame header,
`requires_action` during MCP elicitation, and headless elicitation
cancellation.

## Test plan
- New `toolInfoFromToolUse - Write input aliases` tests
- `npm run build`, `npm run check`, `npm run test:run`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Brandt
2026-09-23 01:34:20 -07:00
committed by GitHub
parent d571358e26
commit 90cde4b397
2 changed files with 51 additions and 1 deletions

View File

@@ -1867,6 +1867,43 @@ describe("toolInfoFromToolUse - ExitPlanMode", () => {
});
});
describe("toolInfoFromToolUse - Write input aliases", () => {
it.each([
["path + file_text", { path: "/repo/a.ts", file_text: "x" }],
["path + file_content", { path: "/repo/a.ts", file_content: "x" }],
["file_path + file_text", { file_path: "/repo/a.ts", file_text: "x" }],
])("renders %s like file_path + content", (_label, input) => {
const info = toolInfoFromToolUse(
{ name: "Write", id: "toolu_write_alias", input },
false,
"/repo",
);
expect(info.title).toBe("Write a.ts");
expect(info.locations).toEqual([{ path: "/repo/a.ts" }]);
expect(info.content).toEqual([
{ type: "diff", path: "/repo/a.ts", oldText: null, newText: "x" },
]);
});
it("prefers the canonical fields when both spellings are present", () => {
const info = toolInfoFromToolUse(
{
name: "Write",
id: "toolu_write_both",
input: { file_path: "/repo/a.ts", path: "/repo/b.ts", content: "a", file_text: "b" },
},
false,
"/repo",
);
expect(info.title).toBe("Write a.ts");
expect(info.content).toEqual([
{ type: "diff", path: "/repo/a.ts", oldText: null, newText: "a" },
]);
});
});
describe("toolInfoFromToolUse - undefined input regression", () => {
it("Read with undefined input should not throw", () => {
const toolUse = { name: "Read", id: "toolu_read_undef", input: undefined };

View File

@@ -134,6 +134,19 @@ export function toDisplayPath(filePath: string, cwd?: string): string {
return filePath;
}
/** Read a Write tool_use input the way the CLI validates it. Since 2.1.280 the
* CLI accepts `path` for `file_path` and `file_text`/`file_content` for
* `content` when a model sends those spellings, but the streamed tool_use
* block still carries them raw — without this the call renders as
* "Preparing file…" with no diff while the write goes through. */
function normalizeWriteInput(input: unknown): FileWriteInput | undefined {
if (!input || typeof input !== "object") return undefined;
const raw = input as Record<string, unknown>;
const filePath = raw.file_path ?? (typeof raw.path === "string" ? raw.path : undefined);
const content = raw.content ?? raw.file_text ?? raw.file_content;
return { ...raw, file_path: filePath, content } as FileWriteInput;
}
export function toolInfoFromToolUse(
toolUse: any,
supportsTerminalOutput: boolean = false,
@@ -204,7 +217,7 @@ export function toolInfoFromToolUse(
}
case "Write": {
const input = toolUse.input as FileWriteInput | undefined;
const input = normalizeWriteInput(toolUse.input);
let content: ToolCallContent[] = [];
if (input && input.file_path) {
content = [