feat(core,web,cli): add file tools and per-tool call descriptions (#62)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei Zheng
2026-07-26 21:37:51 +08:00
committed by GitHub
parent 1e23452f48
commit abd5b13d52
43 changed files with 3449 additions and 181 deletions
+10 -4
View File
@@ -48,10 +48,16 @@ describe("getMessages", () => {
expect(getMessages("zh").langInvalid("fr")).toContain("fr");
});
it("header order is agent → workspace → model", () => {
const h = getMessages("en").header("run", "ag", "/ws", "mod");
expect(h.indexOf("agent=ag")).toBeLessThan(h.indexOf("workspace=/ws"));
expect(h.indexOf("workspace=/ws")).toBeLessThan(h.indexOf("model=mod"));
it("header shows the version and Agent / Workspace / Model on their own lines", () => {
for (const lang of ["en", "zh"] as const) {
const lines = getMessages(lang).header("run", "1.2.3", "ag", "/ws", "mod").split("\n");
expect(lines).toHaveLength(4);
expect(lines[0]).toContain("run");
expect(lines[0]).toContain("v1.2.3");
expect(lines[1]).toContain("ag");
expect(lines[2]).toContain("/ws");
expect(lines[3]).toContain("mod");
}
});
});
+187 -18
View File
@@ -117,19 +117,165 @@ describe("StreamRenderer", () => {
r.handle(partialToolCall({ eventType: "stop", name: "", toolCallId: "c4" }));
r.handle(toolCall({ name: "exec_command", arguments: '{"cmd":"ls"}', toolCallId: "c4" }));
// The call line carries a [tool-<last-3-chars-of-id>] pairing tag matching the output line.
expect(stripAnsi(text())).toBe("[tool-c4] $ ls\n");
expect(stripAnsi(text())).toBe("[tool-c4] exec_command <- $ ls\n");
});
it("streams partial_tool_call_output with a tagged gutter and skips the complete tool_call_output", () => {
it("renders one call line when the description arrives after the command", () => {
const { stream, text } = collector();
const r = new StreamRenderer(stream, t);
// The assembled schema carries the description argument, so the preview waits for it:
// with payload-first emission (models don't always honour schema order) the plain form
// must never reach the screen, or it would be stranded above the described one.
r.useToolSchemas([
{
name: "exec_command",
description: "run a command",
parameters: { type: "object", properties: { description: {}, cmd: {} } },
},
]);
r.handle(partialToolCall({ eventType: "start", name: "exec_command", toolCallId: "c9" }));
r.handle(
partialToolCall({
eventType: "delta",
name: "",
arguments: '{"cmd":"ls -la",',
toolCallId: "c9",
}),
);
r.handle(
partialToolCall({
eventType: "delta",
name: "",
arguments: '"description":"列出当前目录的文件"}',
toolCallId: "c9",
}),
);
r.handle(partialToolCall({ eventType: "stop", name: "", toolCallId: "c9" }));
expect(stripAnsi(text())).toBe("[tool-c9] exec_command <- 列出当前目录的文件 ($ ls -la)\n");
});
it("streams the command live when the schema has no description argument", () => {
const { stream, text } = collector();
const r = new StreamRenderer(stream, t);
// call_description switched off for this tool: nothing can supersede the plain form, so
// it streams as the arguments arrive rather than waiting for them to settle.
r.useToolSchemas([
{
name: "exec_command",
description: "run a command",
parameters: { type: "object", properties: { cmd: {} } },
},
]);
r.handle(partialToolCall({ eventType: "start", name: "exec_command", toolCallId: "c7" }));
r.handle(
partialToolCall({ eventType: "delta", name: "", arguments: '{"cmd":"ls', toolCallId: "c7" }),
);
expect(stripAnsi(text())).toBe("[tool-c7] exec_command <- $ ls");
r.handle(
partialToolCall({ eventType: "delta", name: "", arguments: ' -la"}', toolCallId: "c7" }),
);
r.handle(partialToolCall({ eventType: "stop", name: "", toolCallId: "c7" }));
expect(stripAnsi(text())).toBe("[tool-c7] exec_command <- $ ls -la\n");
});
it("still renders a call line whose arguments never settled", () => {
const { stream, text } = collector();
const r = new StreamRenderer(stream, t);
// Interrupted mid-arguments while awaiting a description: the call must not vanish.
r.useToolSchemas([
{
name: "exec_command",
description: "run a command",
parameters: { type: "object", properties: { description: {}, cmd: {} } },
},
]);
r.handle(partialToolCall({ eventType: "start", name: "exec_command", toolCallId: "c8" }));
r.handle(
partialToolCall({ eventType: "delta", name: "", arguments: '{"cmd":"sle', toolCallId: "c8" }),
);
r.handle(partialToolCall({ eventType: "stop", name: "", toolCallId: "c8" }));
expect(stripAnsi(text())).toBe("[tool-c8] exec_command <- $ sle\n");
});
it("prefixes streamed tool output with the tool name and skips the complete tool_call_output", () => {
const { stream, text } = collector();
const r = new StreamRenderer(stream, t);
// The call precedes its output and supplies the gutter's tool name.
r.handle(partialToolCall({ eventType: "start", name: "exec_command", toolCallId: "c3" }));
r.handle(
partialToolCall({
eventType: "delta",
name: "",
arguments: '{"cmd":"ls"}',
toolCallId: "c3",
}),
);
r.handle(partialToolCall({ eventType: "stop", name: "", toolCallId: "c3" }));
r.handle(partialToolCallOutput({ eventType: "start", toolCallId: "c3" }));
r.handle(partialToolCallOutput({ eventType: "delta", output: "line1\n", toolCallId: "c3" }));
r.handle(partialToolCallOutput({ eventType: "delta", output: "line2", toolCallId: "c3" }));
r.handle(partialToolCallOutput({ eventType: "stop", toolCallId: "c3" }));
r.handle(toolCallOutput({ output: "line1\nline2", toolCallId: "c3" })); // must not be re-rendered
// Each line starts with a tagged gutter (no indent) matching the call line.
expect(stripAnsi(text())).toBe("[tool-c3] >> line1\n[tool-c3] >> line2\n");
// Call line first, then each output line repeats the `[tool-xxx] <toolName>` prefix.
expect(stripAnsi(text())).toBe(
"[tool-c3] exec_command <- $ ls\n[tool-c3] exec_command -> line1\n[tool-c3] exec_command -> line2\n",
);
});
it("colors edit_file diff output lines green/red and dims hunk headers", () => {
const { stream, text } = collector();
const r = new StreamRenderer(stream, t);
r.handle(partialToolCall({ eventType: "start", name: "edit_file", toolCallId: "d1" }));
r.handle(
partialToolCall({
eventType: "delta",
name: "",
arguments: '{"file_path":"x.ts","old_string":"old","new_string":"new"}',
toolCallId: "d1",
}),
);
r.handle(partialToolCall({ eventType: "stop", name: "", toolCallId: "d1" }));
r.handle(partialToolCallOutput({ eventType: "start", toolCallId: "d1" }));
r.handle(
partialToolCallOutput({
eventType: "delta",
output: 'Replaced 1 occurrence in "x.ts".\n@@ -1,1 +1,1 @@\n-old\n+new\n',
toolCallId: "d1",
}),
);
r.handle(partialToolCallOutput({ eventType: "stop", toolCallId: "d1" }));
const raw = text();
// Diff lines are wrapped in green/red; the hunk header is dimmed; the summary line stays plain.
expect(raw).toContain("\x1b[32m+new\x1b[0m");
expect(raw).toContain("\x1b[31m-old\x1b[0m");
expect(raw).toContain("\x1b[2m@@ -1,1 +1,1 @@\x1b[0m");
// The stripped view still reads as labeled gutter lines.
const plain = stripAnsi(raw);
expect(plain).toContain("[tool-d1] edit_file -> -old");
expect(plain).toContain("[tool-d1] edit_file -> +new");
});
it("does not diff-color non-file-tool output", () => {
const { stream, text } = collector();
const r = new StreamRenderer(stream, t);
r.handle(partialToolCall({ eventType: "start", name: "exec_command", toolCallId: "d2" }));
r.handle(
partialToolCall({ eventType: "delta", name: "", arguments: '{"cmd":"x"}', toolCallId: "d2" }),
);
r.handle(partialToolCall({ eventType: "stop", name: "", toolCallId: "d2" }));
r.handle(partialToolCallOutput({ eventType: "start", toolCallId: "d2" }));
r.handle(partialToolCallOutput({ eventType: "delta", output: "+plus\n", toolCallId: "d2" }));
r.handle(partialToolCallOutput({ eventType: "stop", toolCallId: "d2" }));
expect(text()).not.toContain("\x1b[32m");
});
it("falls back to the pairing tag on output whose call was never seen", () => {
const { stream, text } = collector();
const r = new StreamRenderer(stream, t);
r.handle(partialToolCallOutput({ eventType: "start", toolCallId: "c3" }));
r.handle(partialToolCallOutput({ eventType: "delta", output: "line1", toolCallId: "c3" }));
r.handle(partialToolCallOutput({ eventType: "stop", toolCallId: "c3" }));
expect(stripAnsi(text())).toBe("[tool-c3] -> line1\n");
});
it("prints the retry line only when the retry request actually begins", () => {
@@ -165,10 +311,11 @@ describe("StreamRenderer", () => {
r.handle(partialText("start", ""));
r.handle(partialText("delta", "hello"));
r.handle(partialToolCallOutput({ eventType: "delta", output: "a2\n", toolCallId: "tA" }));
expect(stripAnsi(text())).toBe("[tool-tA] >> a1\n[tool-tA] >> a2\n"); // hello is still queued
// No call preceded tA in this stream: the gutter falls back to the pairing tag.
expect(stripAnsi(text())).toBe("[tool-tA] -> a1\n[tool-tA] -> a2\n"); // hello is still queued
r.handle(partialToolCallOutput({ eventType: "stop", toolCallId: "tA" }));
r.handle(partialText("stop", "", "completed"));
expect(stripAnsi(text())).toBe("[tool-tA] >> a1\n[tool-tA] >> a2\nhello\n");
expect(stripAnsi(text())).toBe("[tool-tA] -> a1\n[tool-tA] -> a2\nhello\n");
});
it("queues everything while a user prompt is active and flushes after it ends", () => {
@@ -411,10 +558,30 @@ describe("StreamRenderer", () => {
r.beginUserPrompt(tc);
r.noteApprovalDecision(tc, "allow");
r.endUserPrompt();
expect(stripAnsi(text())).toBe("[tool-p8] $ pwd\n✓ [approved]\n");
expect(stripAnsi(text())).toBe("[tool-p8] exec_command <- $ pwd\n✓ [approved]\n");
// A late approval_decision event is deduped by key and not re-rendered.
r.handle(approvalDecision("allow", "p8"));
expect(stripAnsi(text())).toBe("[tool-p8] $ pwd\n✓ [approved]\n");
expect(stripAnsi(text())).toBe("[tool-p8] exec_command <- $ pwd\n✓ [approved]\n");
});
it("prints the decoded file-tool payload before the approval prompt, without duplicating the call line", () => {
const { stream, text } = collector();
const r = new StreamRenderer(stream, t);
const tc = toolCall({
name: "edit_file",
arguments: JSON.stringify({ file_path: "src/x.ts", old_string: "a", new_string: "b" }),
toolCallId: "fp1",
});
r.beginUserPrompt(tc);
r.noteApprovalDecision(tc, "allow");
r.endUserPrompt();
// Call line, payload lines (what the user is approving), then the result — with no
// duplicated call line after the payload.
expect(stripAnsi(text())).toBe(
"[tool-fp1] edit_file src/x.ts\n" +
"file_path: src/x.ts\nold_string: a\nnew_string: b\n" +
"✓ [approved]\n",
);
});
it("re-renders a half-streamed call line at approval and suppresses its late tail deltas", () => {
@@ -447,7 +614,7 @@ describe("StreamRenderer", () => {
// At approval time, the full call line is re-rendered in place from the complete message, right next to
// the result; after unlocking, the late tail is deduped and must not start a duplicate call line after
// the result line.
expect(s).toContain("[tool-h7] $ git status\n✓ [approved]\n");
expect(s).toContain("[tool-h7] exec_command <- $ git status\n✓ [approved]\n");
expect(s.slice(s.indexOf("[approved]"))).not.toContain("[tool-h7]");
});
@@ -531,7 +698,9 @@ describe("StreamRenderer", () => {
toolCall({ name: "exec_command", arguments: '{"cmd":"ls"}', toolCallId: "c5" }),
"allow",
);
expect(stripAnsi(text())).toBe("[tool-c5] $ ls\nhi\n[tool-c5] $ ls\n✓ [approved]\n");
expect(stripAnsi(text())).toBe(
"[tool-c5] exec_command <- $ ls\nhi\n[tool-c5] exec_command <- $ ls\n✓ [approved]\n",
);
});
it("does not re-render the call line when it is already adjacent to the decision", () => {
@@ -551,7 +720,7 @@ describe("StreamRenderer", () => {
toolCall({ name: "exec_command", arguments: '{"cmd":"ls"}', toolCallId: "c6" }),
"deny",
);
expect(stripAnsi(text())).toBe("[tool-c6] $ ls\n× [denied]\n");
expect(stripAnsi(text())).toBe("[tool-c6] exec_command <- $ ls\n× [denied]\n");
});
});
@@ -574,7 +743,7 @@ describe("StreamRenderer — nested (origin-tagged) subagent messages", () => {
),
);
r.handle(withOrigin(approvalDecision("allow", "cc1"), hop));
expect(stripAnsi(text())).toBe("[agent-ild-tool-cc1] $ ls\n✓ [approved]\n");
expect(stripAnsi(text())).toBe("[agent-ild-tool-cc1] exec_command <- $ ls\n✓ [approved]\n");
});
it("renders the pending nested tool call at approval time when its stream copy has not arrived; dedupes the late copy", () => {
@@ -586,11 +755,11 @@ describe("StreamRenderer — nested (origin-tagged) subagent messages", () => {
);
// The approval callback arrives before the forwarded message: beginUserPrompt renders the call line directly from the complete message.
r.beginUserPrompt(tc);
expect(stripAnsi(text())).toBe("[agent-ild-tool-cc9] $ ls\n");
expect(stripAnsi(text())).toBe("[agent-ild-tool-cc9] exec_command <- $ ls\n");
r.endUserPrompt();
// The late forwarded copy is deduped by key and not re-rendered.
r.handle(tc);
expect(stripAnsi(text())).toBe("[agent-ild-tool-cc9] $ ls\n");
expect(stripAnsi(text())).toBe("[agent-ild-tool-cc9] exec_command <- $ ls\n");
});
it("renders the pending parent tool call at approval time and suppresses its late partial stream", () => {
@@ -611,7 +780,7 @@ describe("StreamRenderer — nested (origin-tagged) subagent messages", () => {
}),
);
r.handle(partialToolCall({ eventType: "stop", name: "", toolCallId: "p7" }));
expect(stripAnsi(text())).toBe("[tool-p7] $ pwd\n");
expect(stripAnsi(text())).toBe("[tool-p7] exec_command <- $ pwd\n");
});
it("adds nested token_usage request totals to the task delta and the session total", () => {
@@ -691,9 +860,9 @@ describe("renderHistory (resume)", () => {
expect(s).toContain("> hello");
expect(s).toContain("pondering");
expect(s).toContain("hi there");
expect(s).toContain("[tool-653] $ ls");
expect(s).toContain("[tool-653] >> a.txt");
expect(s).toContain("[tool-653] >> b.txt");
expect(s).toContain("[tool-653] exec_command <- $ ls");
expect(s).toContain("[tool-653] exec_command -> a.txt");
expect(s).toContain("[tool-653] exec_command -> b.txt");
// An interrupted message carries a marker (rendering includes the interrupted turn).
expect(s).toContain("half answer [aborted]");
});
+224 -42
View File
@@ -1,72 +1,174 @@
import { describe, expect, it } from "vitest";
import { renderPartialToolCall } from "../src/tool-render.js";
import {
renderFileToolApprovalPayload,
renderPartialToolCall,
shortenPath,
} from "../src/tool-render.js";
describe("renderPartialToolCall", () => {
it("renders partial exec_command args as $ <cmd-so-far>", () => {
describe("renderPartialToolCall — exec_command", () => {
it("streams `exec_command <- $ {cmd}` when the schema has no description argument", () => {
// The default path: the switch is off (or the tool is unknown), so nothing can supersede
// the plain form and it streams character by character.
expect(renderPartialToolCall("exec_command", '{"cmd":')).toBeNull();
expect(renderPartialToolCall("exec_command", '{"cmd":"l')).toBe("$ l");
expect(renderPartialToolCall("exec_command", '{"cmd":"ls"}')).toBe("$ ls");
expect(renderPartialToolCall("exec_command", '{"cmd":"echo \\"hi\\"')).toBe('$ echo "hi"');
});
it("renders run_subagent as run_subagent << <prompt>, folded to one line", () => {
expect(renderPartialToolCall("run_subagent", '{"prompt":')).toBeNull();
expect(renderPartialToolCall("run_subagent", '{"prompt":"analy')).toBe("run_subagent << analy");
expect(renderPartialToolCall("run_subagent", '{"prompt":"line1\\nline2"}')).toBe(
"run_subagent << line1 line2",
expect(renderPartialToolCall("exec_command", '{"cmd":"l')).toBe("exec_command <- $ l");
expect(renderPartialToolCall("exec_command", '{"cmd":"ls"}')).toBe("exec_command <- $ ls");
expect(renderPartialToolCall("exec_command", '{"cmd":"echo \\"hi\\"')).toBe(
'exec_command <- $ echo "hi"',
);
});
it("renders input_command polls (empty chars) without a payload", () => {
expect(renderPartialToolCall("input_command", '{"process_id":')).toBeNull();
expect(renderPartialToolCall("input_command", '{"process_id":"proc-1a2b3c4d"}')).toBe(
"⌨ input_command → proc-1a2b3c4d",
it("waits for the description when the schema carries the argument", () => {
const described = { expectDescription: true };
// Nothing renders while the description could still be the first thing shown...
expect(renderPartialToolCall("exec_command", '{"cmd":"ls -la', described)).toBeNull();
// ...until the arguments settle without one, or the fragment is final.
expect(renderPartialToolCall("exec_command", '{"cmd":"ls -la"}', described)).toBe(
"exec_command <- $ ls -la",
);
expect(
renderPartialToolCall("input_command", '{"process_id":"proc-1a2b3c4d","chars":""}'),
).toBe("⌨ input_command → proc-1a2b3c4d");
renderPartialToolCall("exec_command", '{"cmd":"ls -la', { ...described, final: true }),
).toBe("exec_command <- $ ls -la");
});
it("renders non-empty input_command chars with visible control characters", () => {
it("renders `exec_command <- {description} ($ {cmd})` when a description is present", () => {
expect(
renderPartialToolCall("input_command", '{"process_id":"proc-1a2b3c4d","chars":"y\\n"}'),
).toBe("⌨ input_command → proc-1a2b3c4d << y\\n");
// U+0003 (Ctrl-C) is rendered in caret notation.
renderPartialToolCall("exec_command", '{"description":"List files","cmd":"ls -la"}'),
).toBe("exec_command <- List files ($ ls -la)");
// Same final form regardless of the model's property order.
expect(
renderPartialToolCall("input_command", '{"process_id":"proc-1a2b3c4d","chars":"\\u0003"}'),
).toBe("⌨ input_command → proc-1a2b3c4d << ^C");
// Disambiguates literal backslash escapes: chars "a", "\", "n" render as a\\n, distinct from a real newline \n.
expect(
renderPartialToolCall("input_command", '{"process_id":"proc-1a2b3c4d","chars":"a\\\\n"}'),
).toBe("⌨ input_command → proc-1a2b3c4d << a\\\\n");
renderPartialToolCall("exec_command", '{"cmd":"ls -la","description":"List files"}'),
).toBe("exec_command <- List files ($ ls -la)");
// Multi-line descriptions fold to one line; an empty description falls back to the plain form.
expect(renderPartialToolCall("exec_command", '{"cmd":"ls","description":"a\\nb"}')).toBe(
"exec_command <- a b ($ ls)",
);
expect(renderPartialToolCall("exec_command", '{"cmd":"ls","description":""}')).toBe(
"exec_command <- $ ls",
);
});
it("keeps input_command previews append-only across \\uXXXX delta boundaries", () => {
it("streams the description form append-only when the description arrives first", () => {
const stages = [
'{"process_id":"proc-1a2b3c4d","chars":"y',
'{"process_id":"proc-1a2b3c4d","chars":"y\\u0',
'{"process_id":"proc-1a2b3c4d","chars":"y\\u0003',
'{"description":"List fi', // description streams live
'{"description":"List files"', // description complete
'{"description":"List files","cmd":"ls', // cmd streaming inside the open parenthesis
'{"description":"List files","cmd":"ls -la"}', // cmd complete: parenthesis closes
];
const previews = stages.map((s) => renderPartialToolCall("input_command", s)!);
expect(previews[0]).toBe("⌨ input_command → proc-1a2b3c4d << y");
// An incomplete \u escape is treated as "stop here" rather than emitting the raw hex as literal text.
expect(previews[1]).toBe("⌨ input_command → proc-1a2b3c4d << y");
expect(previews[2]).toBe("⌨ input_command → proc-1a2b3c4d << y^C");
const previews = stages.map((s) =>
renderPartialToolCall("exec_command", s, { expectDescription: true }),
);
expect(previews[0]).toBe("exec_command <- List fi");
expect(previews[1]).toBe("exec_command <- List files");
expect(previews[2]).toBe("exec_command <- List files ($ ls");
expect(previews[3]).toBe("exec_command <- List files ($ ls -la)");
for (let i = 1; i < previews.length; i++) {
expect(previews[i]!.startsWith(previews[i - 1]!)).toBe(true);
}
});
it("renders input_subagent polls without a payload and follow-up prompts with one", () => {
it("never shows the plain form first when the model emits the payload before the description", () => {
// The regression this guards: a plain line followed by a described one for the same call.
const stages = [
'{"cmd":"ls -la', // withheld: the schema says a description is coming
'{"cmd":"ls -la","description":"List fi', // description streams; payload waits for it
'{"cmd":"ls -la","description":"List files"}', // settled: payload appended
];
const previews = stages.map((s) =>
renderPartialToolCall("exec_command", s, { expectDescription: true }),
);
expect(previews[0]).toBeNull();
expect(previews[1]).toBe("exec_command <- List fi");
expect(previews[2]).toBe("exec_command <- List files ($ ls -la)");
expect(previews[2]!.startsWith(previews[1]!)).toBe(true);
expect(previews.some((p) => p === "exec_command <- $ ls -la")).toBe(false);
});
});
describe("renderPartialToolCall — run_subagent", () => {
it("renders `run_subagent <- {prompt}` and the description form", () => {
expect(renderPartialToolCall("run_subagent", '{"prompt":')).toBeNull();
expect(renderPartialToolCall("run_subagent", '{"prompt":"analy')).toBe("run_subagent <- analy");
expect(renderPartialToolCall("run_subagent", '{"prompt":"line1\\nline2"}')).toBe(
"run_subagent <- line1 line2",
);
expect(
renderPartialToolCall(
"run_subagent",
'{"description":"Delegating research","prompt":"do the thing"}',
),
).toBe("run_subagent <- Delegating research (do the thing)");
});
});
describe("renderPartialToolCall — input_command / input_subagent", () => {
it("renders polls (empty chars) without a payload", () => {
expect(renderPartialToolCall("input_command", '{"process_id":')).toBeNull();
expect(renderPartialToolCall("input_command", '{"process_id":"proc-1a2b3c4d"}')).toBe(
"input_command <- proc-1a2b3c4d",
);
expect(
renderPartialToolCall("input_command", '{"process_id":"proc-1a2b3c4d","chars":""}'),
).toBe("input_command <- proc-1a2b3c4d");
});
it("renders non-empty chars with visible control characters", () => {
expect(
renderPartialToolCall("input_command", '{"process_id":"proc-1a2b3c4d","chars":"y\\n"}'),
).toBe("input_command <- proc-1a2b3c4d << y\\n");
// U+0003 (Ctrl-C) is rendered in caret notation.
expect(
renderPartialToolCall("input_command", '{"process_id":"proc-1a2b3c4d","chars":"\\u0003"}'),
).toBe("input_command <- proc-1a2b3c4d << ^C");
// Disambiguates literal backslash escapes: chars "a", "\", "n" render as a\\n, distinct from a real newline \n.
expect(
renderPartialToolCall("input_command", '{"process_id":"proc-1a2b3c4d","chars":"a\\\\n"}'),
).toBe("input_command <- proc-1a2b3c4d << a\\\\n");
});
it("wraps the payload in parentheses after the description", () => {
expect(
renderPartialToolCall(
"input_command",
'{"description":"Confirm the prompt","process_id":"proc-1a2b3c4d","chars":"y\\n"}',
),
).toBe("input_command <- Confirm the prompt (proc-1a2b3c4d << y\\n)");
expect(
renderPartialToolCall(
"input_subagent",
'{"description":"Poll for progress","subagent_id":"subagent-9f8e7d6c"}',
),
).toBe("input_subagent <- Poll for progress (subagent-9f8e7d6c)");
});
it("keeps input_command previews append-only across \\uXXXX delta boundaries", () => {
// Schema order (description first) keeps the whole call streaming live.
const stages = [
'{"description":"Confirm","process_id":"proc-1a2b3c4d","chars":"y',
'{"description":"Confirm","process_id":"proc-1a2b3c4d","chars":"y\\u0',
'{"description":"Confirm","process_id":"proc-1a2b3c4d","chars":"y\\u0003',
];
const previews = stages.map((s) =>
renderPartialToolCall("input_command", s, { expectDescription: true })!,
);
expect(previews[0]).toBe("input_command <- Confirm (proc-1a2b3c4d << y");
// An incomplete \u escape is treated as "stop here" rather than emitting the raw hex as literal text.
expect(previews[1]).toBe("input_command <- Confirm (proc-1a2b3c4d << y");
expect(previews[2]).toBe("input_command <- Confirm (proc-1a2b3c4d << y^C");
for (let i = 1; i < previews.length; i++) {
expect(previews[i]!.startsWith(previews[i - 1]!)).toBe(true);
}
});
it("renders input_subagent polls and follow-up prompts", () => {
expect(
renderPartialToolCall("input_subagent", '{"subagent_id":"subagent-9f8e7d6c","prompt":""}'),
).toBe("⌨ input_subagent → subagent-9f8e7d6c");
).toBe("input_subagent <- subagent-9f8e7d6c");
expect(
renderPartialToolCall(
"input_subagent",
'{"subagent_id":"subagent-9f8e7d6c","prompt":"continue with the tests"}',
),
).toBe("⌨ input_subagent → subagent-9f8e7d6c << continue with the tests");
).toBe("input_subagent <- subagent-9f8e7d6c << continue with the tests");
});
it("truncates long payload previews and stops growing afterwards", () => {
@@ -75,15 +177,95 @@ describe("renderPartialToolCall", () => {
"input_subagent",
`{"subagent_id":"subagent-9f8e7d6c","prompt":"${long}"}`,
);
expect(capped).toBe(`⌨ input_subagent → subagent-9f8e7d6c << ${"x".repeat(120)}…`);
expect(capped).toBe(`input_subagent <- subagent-9f8e7d6c << ${"x".repeat(120)}…`);
const longer = renderPartialToolCall(
"input_subagent",
`{"subagent_id":"subagent-9f8e7d6c","prompt":"${long}yyy"}`,
);
expect(longer).toBe(capped);
});
});
describe("renderPartialToolCall — file tools", () => {
it("renders `<name> <shortened path>` only once the path is complete", () => {
expect(renderPartialToolCall("read_file", '{"file_path":')).toBeNull();
// A still-streaming path is withheld: shortening a growing path would rewrite the line.
expect(renderPartialToolCall("read_file", '{"file_path":"src/ap')).toBeNull();
expect(renderPartialToolCall("read_file", '{"file_path":"src/app.py","offset":10}')).toBe(
"read_file src/app.py",
);
expect(
renderPartialToolCall("edit_file", '{"file_path":"a.txt","old_string":"x","new_string":"y"}'),
).toBe("edit_file a.txt");
expect(
renderPartialToolCall("write_file", '{"file_path":"packages/core/src/state/out.ts"}'),
).toBe("write_file …/state/out.ts");
});
});
describe("renderPartialToolCall — fallback", () => {
it("falls back to name(args-prefix) for unknown tools", () => {
expect(renderPartialToolCall("search", '{"q":"hi')).toBe('search({"q":"hi');
});
});
describe("shortenPath", () => {
it("keeps at most one parent directory plus the filename", () => {
expect(shortenPath("file.ts")).toBe("file.ts");
expect(shortenPath("src/file.ts")).toBe("src/file.ts");
expect(shortenPath("/etc/hosts")).toBe("/etc/hosts");
expect(shortenPath("packages/core/src/state/default-config.ts")).toBe(
"…/state/default-config.ts",
);
expect(shortenPath("/home/user/project/src/app.py")).toBe("…/src/app.py");
});
});
describe("renderFileToolApprovalPayload", () => {
it("prints the decoded edit_file payload with gutters for multi-line fields", () => {
const payload = renderFileToolApprovalPayload(
"edit_file",
JSON.stringify({
file_path: "src/app.py",
old_string: "a\nb",
new_string: "a\nc",
}),
);
expect(payload).toBe(
[
"file_path: src/app.py",
"old_string:",
" | a",
" | b",
"new_string:",
" | a",
" | c",
].join("\n"),
);
});
it("prints write_file content and read_file window arguments", () => {
expect(
renderFileToolApprovalPayload("write_file", '{"file_path":"out.md","content":"hello"}'),
).toBe(["file_path: out.md", "content: hello"].join("\n"));
expect(
renderFileToolApprovalPayload("read_file", '{"file_path":"a.txt","offset":3,"limit":5}'),
).toBe(["file_path: a.txt", "offset: 3", "limit: 5"].join("\n"));
});
it("bounds the payload to a line count with an explicit elision note", () => {
const content = Array.from({ length: 60 }, (_, i) => `line-${i + 1}`).join("\n");
const payload = renderFileToolApprovalPayload(
"write_file",
JSON.stringify({ file_path: "big.txt", content }),
)!;
const lines = payload.split("\n");
// 24 shown lines + the elision note.
expect(lines).toHaveLength(25);
expect(lines[lines.length - 1]).toMatch(/\[… \d+ more lines not shown\]/);
});
it("returns null for non-file tools", () => {
expect(renderFileToolApprovalPayload("exec_command", '{"cmd":"ls"}')).toBeNull();
});
});