Skip to content

Commit b31cfcc

Browse files
clkaoclaudewesm
authored
fix: dispatch tool metadata on category for cross-agent consistency (wesm#116)
## Summary - `extractToolParamMeta` now dispatches on normalized `category` (Read, Bash, Edit, etc.) instead of raw `tool_name`, so Gemini tools (`run_command`, `read_file`, `grep_search`, etc.) render with the same metadata tags as their Claude equivalents - Adds `cmd` meta tag for Bash category showing first line of command, truncated to 80 chars - Normalizes param name lookups across agents (`file_path`/`path`, `pattern`/`query`, `command`/`cmd`) - Simplifies ToolBlock callsite: single `extractToolParamMeta(toolName, params, category)` call replaces the try-category-then-fallback-to-toolName pattern Follow-up to wesm#114. Missing ABOUTME file brief for `gemini.go` will be addressed in a separate PR. ## Test plan - [ ] Verify Gemini tool calls show metadata tags (file path, command, pattern) in collapsed ToolBlock header - [ ] Verify Claude tool calls still render identically (no regression) - [ ] `npx vitest run src/lib/utils/tool-params.test.ts` — 57 tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Wes McKinney <wesmckinn+git@gmail.com>
1 parent 6a5e424 commit b31cfcc

File tree

3 files changed

+88
-19
lines changed

3 files changed

+88
-19
lines changed

frontend/src/lib/components/content/ToolBlock.svelte

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,7 @@
103103
/** Extract metadata tags for common tool types */
104104
let toolParamMeta = $derived.by(() => {
105105
if (!inputParams || !toolCall) return null;
106-
const cat = toolCall.category || null;
107-
const result = cat ? extractToolParamMeta(cat, inputParams) : null;
108-
return result ?? extractToolParamMeta(toolCall.tool_name, inputParams);
106+
return extractToolParamMeta(toolCall.tool_name, inputParams, toolCall.category);
109107
});
110108
111109
/** Combined metadata for any tool type */

frontend/src/lib/utils/tool-params.test.ts

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,20 +109,33 @@ describe("extractToolParamMeta", () => {
109109
]);
110110
});
111111

112-
it("extracts Bash description", () => {
112+
it("extracts Bash description and cmd", () => {
113113
const meta = extractToolParamMeta("Bash", {
114114
command: "npm test",
115115
description: "Run test suite",
116116
});
117117
expect(meta).toEqual([
118118
{ label: "description", value: "Run test suite" },
119+
{ label: "cmd", value: "npm test" },
119120
]);
120121
});
121122

122-
it("returns null for Bash without description", () => {
123-
expect(
124-
extractToolParamMeta("Bash", { command: "ls" }),
125-
).toBeNull();
123+
it("extracts Bash cmd without description", () => {
124+
const meta = extractToolParamMeta("Bash", {
125+
command: "ls -la",
126+
});
127+
expect(meta).toEqual([
128+
{ label: "cmd", value: "ls -la" },
129+
]);
130+
});
131+
132+
it("shows only first line of multiline Bash command", () => {
133+
const meta = extractToolParamMeta("Bash", {
134+
command: "echo hello\necho world",
135+
});
136+
expect(meta).toEqual([
137+
{ label: "cmd", value: "echo hello" },
138+
]);
126139
});
127140

128141
it("extracts Skill name", () => {
@@ -134,6 +147,50 @@ describe("extractToolParamMeta", () => {
134147
]);
135148
});
136149

150+
it("dispatches on category for Gemini read_file", () => {
151+
const meta = extractToolParamMeta(
152+
"read_file",
153+
{ file_path: "/src/main.go" },
154+
"Read",
155+
);
156+
expect(meta).toEqual([
157+
{ label: "file", value: "/src/main.go" },
158+
]);
159+
});
160+
161+
it("dispatches on category for Gemini run_command", () => {
162+
const meta = extractToolParamMeta(
163+
"run_command",
164+
{ command: "go test ./..." },
165+
"Bash",
166+
);
167+
expect(meta).toEqual([
168+
{ label: "cmd", value: "go test ./..." },
169+
]);
170+
});
171+
172+
it("dispatches on category for Gemini grep_search", () => {
173+
const meta = extractToolParamMeta(
174+
"grep_search",
175+
{ query: "TODO" },
176+
"Grep",
177+
);
178+
expect(meta).toEqual([
179+
{ label: "pattern", value: "TODO" },
180+
]);
181+
});
182+
183+
it("falls back to toolName when category is empty string", () => {
184+
const meta = extractToolParamMeta(
185+
"Read",
186+
{ file_path: "/src/app.ts" },
187+
"",
188+
);
189+
expect(meta).toEqual([
190+
{ label: "file", value: "/src/app.ts" },
191+
]);
192+
});
193+
137194
it("returns null for unknown tool with no matching params", () => {
138195
expect(
139196
extractToolParamMeta("CustomTool", { foo: "bar" }),

frontend/src/lib/utils/tool-params.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,19 @@ export function truncate(s: string, max: number): string {
1212
type Params = Record<string, unknown>;
1313

1414
/** Extract metadata tags for common tool types.
15+
* Dispatches on normalized category so all agents (Claude,
16+
* Gemini, Codex, etc.) render consistently.
1517
* Returns null for Task/TaskCreate/TaskUpdate (handled separately). */
1618
export function extractToolParamMeta(
1719
toolName: string,
1820
params: Params,
21+
category?: string,
1922
): MetaTag[] | null {
2023
const skip = ["Task", "TaskCreate", "TaskUpdate"];
2124
if (skip.includes(toolName)) return null;
25+
const cat = category || toolName;
2226
const meta: MetaTag[] = [];
23-
if (toolName === "Read") {
27+
if (cat === "Read") {
2428
const filePath = params.file_path ?? params.path;
2529
if (filePath)
2630
meta.push({
@@ -42,7 +46,7 @@ export function extractToolParamMeta(
4246
label: "pages",
4347
value: String(params.pages),
4448
});
45-
} else if (toolName === "Edit") {
49+
} else if (cat === "Edit") {
4650
const filePath = params.file_path ?? params.path ?? params.filePath;
4751
if (filePath)
4852
meta.push({
@@ -51,18 +55,19 @@ export function extractToolParamMeta(
5155
});
5256
if (params.replace_all)
5357
meta.push({ label: "mode", value: "replace_all" });
54-
} else if (toolName === "Write") {
58+
} else if (cat === "Write") {
5559
const filePath = params.file_path ?? params.path;
5660
if (filePath)
5761
meta.push({
5862
label: "file",
5963
value: truncate(String(filePath), 80),
6064
});
61-
} else if (toolName === "Grep") {
62-
if (params.pattern)
65+
} else if (cat === "Grep") {
66+
const pattern = params.pattern ?? params.query;
67+
if (pattern)
6368
meta.push({
6469
label: "pattern",
65-
value: truncate(String(params.pattern), 60),
70+
value: truncate(String(pattern), 60),
6671
});
6772
if (params.path)
6873
meta.push({
@@ -76,7 +81,7 @@ export function extractToolParamMeta(
7681
label: "mode",
7782
value: String(params.output_mode),
7883
});
79-
} else if (toolName === "Glob") {
84+
} else if (cat === "Glob") {
8085
if (params.pattern)
8186
meta.push({
8287
label: "pattern",
@@ -87,17 +92,26 @@ export function extractToolParamMeta(
8792
label: "path",
8893
value: truncate(String(params.path), 80),
8994
});
90-
} else if (toolName === "Bash") {
95+
} else if (cat === "Bash") {
9196
if (params.description)
9297
meta.push({
9398
label: "description",
9499
value: truncate(String(params.description), 80),
95100
});
96-
} else if (toolName === "Skill") {
97-
if (params.skill)
101+
const cmd = params.command ?? params.cmd;
102+
if (cmd) {
103+
const firstLine = String(cmd).split("\n")[0];
104+
meta.push({
105+
label: "cmd",
106+
value: truncate(firstLine, 80),
107+
});
108+
}
109+
} else if (toolName === "Skill" || toolName === "skill") {
110+
const skill = params.skill ?? params.name;
111+
if (skill)
98112
meta.push({
99113
label: "skill",
100-
value: String(params.skill),
114+
value: String(skill),
101115
});
102116
}
103117
return meta.length ? meta : null;

0 commit comments

Comments
 (0)