feat(app): improve tool call sheet with word-level diff and better UX
- Add word-level highlighting using LCS algorithm for edit diffs - Match diff colors to explorer sidebar (GitHub-style rgba backgrounds) - Use discriminated union for tool call parsing (shell, edit, generic) - Add horizontal scroll for bash/command output instead of wrapping - Remove status badge from bottom sheet header - Fix sheet snap points (60%/95%) and disable dynamic sizing - Add min-width to diff viewer for full-width backgrounds 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import {
|
||||
extractKeyValuePairs,
|
||||
parseToolCallDisplay,
|
||||
type ToolCallDisplay,
|
||||
} from "./tool-call-parsers";
|
||||
|
||||
describe("tool-call-parsers - real runtime shapes", () => {
|
||||
// Real data captured from Claude agent test: "shows the command inside pending tool calls"
|
||||
// Run: npx vitest run claude-agent.test.ts -t "shows the command"
|
||||
|
||||
test("bash tool call - input shape", () => {
|
||||
// REAL shape from Claude SDK timeline event (status: pending/completed)
|
||||
const bashInput = {
|
||||
command: "pwd",
|
||||
description: "Print working directory",
|
||||
};
|
||||
|
||||
const pairs = extractKeyValuePairs(bashInput);
|
||||
expect(pairs).toContainEqual({ key: "command", value: "pwd" });
|
||||
expect(pairs).toContainEqual({ key: "description", value: "Print working directory" });
|
||||
});
|
||||
|
||||
test("bash tool call - output shape (completed)", () => {
|
||||
// REAL shape from Claude SDK timeline event (status: completed)
|
||||
// NOTE: output already has type: "command" discriminator!
|
||||
const bashOutput = {
|
||||
type: "command",
|
||||
command: "pwd",
|
||||
output: "/private/var/folders/xl/kkk9drfd3ms_t8x7rmy4z6900000gn/T/claude-agent-e2e-9tnmUm",
|
||||
};
|
||||
|
||||
const pairs = extractKeyValuePairs(bashOutput);
|
||||
expect(pairs).toContainEqual({ key: "type", value: "command" });
|
||||
expect(pairs).toContainEqual({ key: "command", value: "pwd" });
|
||||
expect(pairs).toContainEqual({ key: "output", value: expect.stringContaining("claude-agent") });
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseToolCallDisplay", () => {
|
||||
test("parses completed bash tool call into shell type", () => {
|
||||
const input = { command: "pwd", description: "Print working directory" };
|
||||
const result = { type: "command", command: "pwd", output: "/some/path" };
|
||||
|
||||
const display: ToolCallDisplay = parseToolCallDisplay(input, result);
|
||||
expect(display.type).toBe("shell");
|
||||
if (display.type === "shell") {
|
||||
expect(display.command).toBe("pwd");
|
||||
expect(display.output).toBe("/some/path");
|
||||
}
|
||||
});
|
||||
|
||||
test("parses pending bash tool call into shell type with empty output", () => {
|
||||
// When tool is pending, we have input but no result yet
|
||||
const input = { command: "pwd", description: "Print working directory" };
|
||||
const result = undefined;
|
||||
|
||||
const display: ToolCallDisplay = parseToolCallDisplay(input, result);
|
||||
expect(display.type).toBe("shell");
|
||||
if (display.type === "shell") {
|
||||
expect(display.command).toBe("pwd");
|
||||
expect(display.output).toBe("");
|
||||
}
|
||||
});
|
||||
|
||||
test("handles command as array", () => {
|
||||
const input = { command: ["git", "status"] };
|
||||
const result = { type: "command", output: "On branch main" };
|
||||
|
||||
const display: ToolCallDisplay = parseToolCallDisplay(input, result);
|
||||
expect(display.type).toBe("shell");
|
||||
if (display.type === "shell") {
|
||||
expect(display.command).toBe("git status");
|
||||
expect(display.output).toBe("On branch main");
|
||||
}
|
||||
});
|
||||
|
||||
test("parses non-command tool call into generic type", () => {
|
||||
const input = { file_path: "/some/file.txt" };
|
||||
const result = { content: "file contents here", lineCount: 42 };
|
||||
|
||||
const display: ToolCallDisplay = parseToolCallDisplay(input, result);
|
||||
expect(display.type).toBe("generic");
|
||||
if (display.type === "generic") {
|
||||
expect(display.input).toContainEqual({ key: "file_path", value: "/some/file.txt" });
|
||||
expect(display.output).toContainEqual({ key: "content", value: "file contents here" });
|
||||
expect(display.output).toContainEqual({ key: "lineCount", value: "42" });
|
||||
}
|
||||
});
|
||||
|
||||
test("handles file_write output as generic", () => {
|
||||
const input = { file_path: "/some/file.txt", content: "new content" };
|
||||
const result = { type: "file_write", filePath: "/some/file.txt" };
|
||||
|
||||
const display: ToolCallDisplay = parseToolCallDisplay(input, result);
|
||||
expect(display.type).toBe("generic");
|
||||
});
|
||||
|
||||
test("handles undefined input and result gracefully", () => {
|
||||
const display: ToolCallDisplay = parseToolCallDisplay(undefined, undefined);
|
||||
expect(display.type).toBe("generic");
|
||||
if (display.type === "generic") {
|
||||
expect(display.input).toEqual([]);
|
||||
expect(display.output).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
test("parses edit tool call into edit type with old_string/new_string", () => {
|
||||
const input = {
|
||||
file_path: "/some/file.txt",
|
||||
old_string: "const foo = 1;",
|
||||
new_string: "const foo = 2;",
|
||||
};
|
||||
const result = {
|
||||
type: "file_edit",
|
||||
filePath: "/some/file.txt",
|
||||
};
|
||||
|
||||
const display: ToolCallDisplay = parseToolCallDisplay(input, result);
|
||||
expect(display.type).toBe("edit");
|
||||
if (display.type === "edit") {
|
||||
expect(display.filePath).toBe("/some/file.txt");
|
||||
expect(display.oldString).toBe("const foo = 1;");
|
||||
expect(display.newString).toBe("const foo = 2;");
|
||||
}
|
||||
});
|
||||
|
||||
test("parses edit tool call with old_str/new_str variants", () => {
|
||||
const input = {
|
||||
file_path: "/some/file.txt",
|
||||
old_str: "line 1",
|
||||
new_str: "line 2",
|
||||
};
|
||||
const result = undefined;
|
||||
|
||||
const display: ToolCallDisplay = parseToolCallDisplay(input, result);
|
||||
expect(display.type).toBe("edit");
|
||||
if (display.type === "edit") {
|
||||
expect(display.filePath).toBe("/some/file.txt");
|
||||
expect(display.oldString).toBe("line 1");
|
||||
expect(display.newString).toBe("line 2");
|
||||
}
|
||||
});
|
||||
|
||||
test("parses pending edit tool call (no result yet)", () => {
|
||||
const input = {
|
||||
file_path: "/some/file.txt",
|
||||
old_string: "old content",
|
||||
new_string: "new content",
|
||||
};
|
||||
|
||||
const display: ToolCallDisplay = parseToolCallDisplay(input, undefined);
|
||||
expect(display.type).toBe("edit");
|
||||
if (display.type === "edit") {
|
||||
expect(display.filePath).toBe("/some/file.txt");
|
||||
expect(display.oldString).toBe("old content");
|
||||
expect(display.newString).toBe("new content");
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user