web: tool cards show what ran — summary line (bash command / file path) + edit/write line diffs (LCS, +/- coloring); 286 tests green
This commit is contained in:
+132
-2
@@ -5,8 +5,10 @@ import type { ChatMessage, ToolState } from "./derive";
|
||||
import ChatStream, {
|
||||
Bubble,
|
||||
TypingIndicator,
|
||||
lineDiff,
|
||||
renderMarkdown,
|
||||
toolArgsEntries,
|
||||
toolSummaryText,
|
||||
} from "./ChatStream";
|
||||
|
||||
function msg(partial: Partial<ChatMessage>): ChatMessage {
|
||||
@@ -772,7 +774,7 @@ describe("pretty tool args", () => {
|
||||
render(<Bubble msg={{ key: "k", role: "assistant", text: "", thinking: null,
|
||||
toolCalls: [{ id: "t1", name: "bash", argsJson: "{}" }], toolCallId: null, streaming: false, ts: 0 }}
|
||||
tools={new Map([["t1", tool('{"command":"ls -la","foo":1}')]])} />);
|
||||
expect(screen.getByText("ls -la")).toBeInTheDocument();
|
||||
expect(document.querySelector(".tool-summary")?.textContent).toBe("ls -la");
|
||||
expect(document.querySelector(".tool-args")?.textContent).not.toContain('"command"');
|
||||
});
|
||||
|
||||
@@ -786,7 +788,7 @@ describe("pretty tool args", () => {
|
||||
render(<Bubble msg={{ key: "k", role: "assistant", text: "", thinking: null,
|
||||
toolCalls: [{ id: "t1", name: "read", argsJson: "{}" }], toolCallId: null, streaming: false, ts: 0 }}
|
||||
tools={new Map([["t1", tool('{"path":"src/main.ts"}')]])} />);
|
||||
expect(screen.getByText("src/main.ts")).toBeInTheDocument();
|
||||
expect(document.querySelector(".tool-summary")?.textContent).toBe("src/main.ts");
|
||||
expect(document.querySelector(".tool-args .arg-k")).toBeNull();
|
||||
});
|
||||
|
||||
@@ -805,3 +807,131 @@ describe("pretty tool args", () => {
|
||||
expect(entries[0]?.v).toBe("1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("tool summary + diff", () => {
|
||||
const tool = (args: string, name = "bash"): ToolState => ({
|
||||
id: "t1",
|
||||
name,
|
||||
args,
|
||||
running: false,
|
||||
isError: false,
|
||||
preview: "",
|
||||
});
|
||||
it("bash summary shows the command", () => {
|
||||
render(
|
||||
<Bubble
|
||||
msg={msg({
|
||||
toolCalls: [{ id: "t1", name: "bash", argsJson: "{}" }],
|
||||
})}
|
||||
tools={new Map([["t1", tool('{"command":"ls -la","timeout":30}')]])}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("🛠 bash")).toBeInTheDocument();
|
||||
expect(document.querySelector(".tool-summary")?.textContent).toBe("ls -la");
|
||||
});
|
||||
|
||||
it("read summary shows the path", () => {
|
||||
render(
|
||||
<Bubble
|
||||
msg={msg({
|
||||
toolCalls: [{ id: "t1", name: "read", argsJson: "{}" }],
|
||||
})}
|
||||
tools={new Map([["t1", tool('{"path":"src/main.ts"}', "read")]])}
|
||||
/>,
|
||||
);
|
||||
expect(document.querySelector(".tool-summary")?.textContent).toBe("src/main.ts");
|
||||
});
|
||||
|
||||
it("long summary is truncated with ellipsis", () => {
|
||||
const long: string = "x".repeat(300);
|
||||
expect(toolSummaryText(`{"command":"${long}"}`)).toBe(long);
|
||||
render(
|
||||
<Bubble
|
||||
msg={msg({
|
||||
toolCalls: [{ id: "t1", name: "bash", argsJson: "{}" }],
|
||||
})}
|
||||
tools={new Map([["t1", tool(`{"command":"${long}"}`)]])}
|
||||
/>,
|
||||
);
|
||||
const summary = document.querySelector(".tool-summary");
|
||||
expect(summary?.textContent?.endsWith("…")).toBe(true);
|
||||
});
|
||||
|
||||
it("edit tool renders a line diff of old/new text", () => {
|
||||
render(
|
||||
<Bubble
|
||||
msg={msg({
|
||||
toolCalls: [{ id: "t1", name: "edit", argsJson: "{}" }],
|
||||
})}
|
||||
tools={new Map([
|
||||
[
|
||||
"t1",
|
||||
tool(
|
||||
'{"path":"a.ts","edits":[{"oldText":"const a = 1;\\nconst b = 2;","newText":"const a = 3;\\nconst b = 2;"}]}',
|
||||
"edit",
|
||||
),
|
||||
],
|
||||
])}
|
||||
/>,
|
||||
);
|
||||
expect(document.querySelector(".diff-del")?.textContent).toContain("-const a = 1;");
|
||||
expect(document.querySelector(".diff-add")?.textContent).toContain("+const a = 3;");
|
||||
expect(document.querySelector(".diff-ctx")?.textContent).toContain(" const b = 2;");
|
||||
expect(document.querySelector(".tool-args")).toBeNull();
|
||||
});
|
||||
|
||||
it("write tool renders content as added lines", () => {
|
||||
render(
|
||||
<Bubble
|
||||
msg={msg({
|
||||
toolCalls: [{ id: "t1", name: "write", argsJson: "{}" }],
|
||||
})}
|
||||
tools={new Map([
|
||||
["t1", tool('{"path":"new.ts","content":"hello\\nworld"}', "write")],
|
||||
])}
|
||||
/>,
|
||||
);
|
||||
const adds = document.querySelectorAll(".diff-add");
|
||||
expect(adds).toHaveLength(2);
|
||||
expect(adds[0]?.textContent).toContain("+hello");
|
||||
expect(adds[1]?.textContent).toContain("+world");
|
||||
});
|
||||
});
|
||||
|
||||
describe("lineDiff", () => {
|
||||
it("identical text is all context", () => {
|
||||
const d = lineDiff("a\nb", "a\nb");
|
||||
expect(d).toEqual([
|
||||
{ kind: "ctx", text: "a" },
|
||||
{ kind: "ctx", text: "b" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("single line change is del + add", () => {
|
||||
const d = lineDiff("a\nb\nc", "a\nx\nc");
|
||||
expect(d).toEqual([
|
||||
{ kind: "ctx", text: "a" },
|
||||
{ kind: "del", text: "b" },
|
||||
{ kind: "add", text: "x" },
|
||||
{ kind: "ctx", text: "c" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("pure addition and deletion", () => {
|
||||
expect(lineDiff("a", "a\nb")).toEqual([
|
||||
{ kind: "ctx", text: "a" },
|
||||
{ kind: "add", text: "b" },
|
||||
]);
|
||||
expect(lineDiff("a\nb", "a")).toEqual([
|
||||
{ kind: "ctx", text: "a" },
|
||||
{ kind: "del", text: "b" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("oversized blocks fall back to del-all + add-all", () => {
|
||||
const a = Array.from({ length: 401 }, (_, i) => `l${i}`).join("\n");
|
||||
const d = lineDiff(a, "x");
|
||||
expect(d.filter((l) => l.kind === "del")).toHaveLength(401);
|
||||
expect(d.filter((l) => l.kind === "add")).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user