fix(ui): tool-summary suffix-match bridges MCP-prefixed names

LiveRows.tsx already used `name === s || name.endsWith("_" + s)` so MCP
tools like `filesystem_read_file` picked up the same per-tool argument
summary as the bare local `read_file`. tool-summary.ts used exact-name
match only, so bridged tools fell through to the generic "first line +
size hint" path in the compact scrollback row. Switch to the same
suffix predicate; add a write_file branch while there. +3 tests
covering the bridged path + a non-underscore-prefix non-match.
This commit is contained in:
esengine committed 2026-04-25 06:21:57 -07:00
1 parent 98f19d1d47
commit 1de2349350
2 files changed
+37 -4

No files matched your search

+14 -4
View File
@@ -123,9 +123,14 @@ function summarizeStructured(content: string): ToolSummary | null {
* Per-tool-name overrides for cases where a smarter summary is cheap
* to compute. Returning null means "fall through to the generic
* summary path."
*
* Suffix-match is on purpose: MCP-bridged tools come in prefixed
* (`filesystem_read_file`, `git_search_files`) and we want them to
* pick up the same specialized summary as the bare local name.
*/
function summarizeKnownTool(toolName: string, content: string): ToolSummary | null {
if (toolName === "read_file") {
const hasSuffix = (s: string) => toolName === s || toolName.endsWith(`_${s}`);
if (hasSuffix("read_file")) {
const lines = formatLineCount(content);
const bytes = formatBytes(content.length);
const head = clip(
@@ -137,11 +142,11 @@ function summarizeKnownTool(toolName: string, content: string): ToolSummary | nu
isError: false,
};
}
if (toolName === "list_directory" || toolName === "directory_tree") {
if (hasSuffix("list_directory") || hasSuffix("directory_tree")) {
const entries = content.split(/\r?\n/).filter((l) => l.trim()).length;
return { summary: `${entries} entr${entries === 1 ? "y" : "ies"}`, isError: false };
}
if (toolName === "search_files" || toolName === "search_content") {
if (hasSuffix("search_files") || hasSuffix("search_content")) {
const matches = content.split(/\r?\n/).filter((l) => l.trim()).length;
if (matches === 0) return { summary: "no matches", isError: false };
const first = firstNonEmptyLine(content);
@@ -150,7 +155,12 @@ function summarizeKnownTool(toolName: string, content: string): ToolSummary | nu
isError: false,
};
}
if (toolName === "run_command" || toolName === "run_background") {
if (hasSuffix("write_file")) {
const lines = formatLineCount(content);
const bytes = formatBytes(content.length);
return { summary: `wrote ${lines} · ${bytes}`, isError: false };
}
if (hasSuffix("run_command") || hasSuffix("run_background")) {
// Native shell tools prepend "exit 0:" / "exit N:" or the result
// already mentions exit code. Try to surface it.
const exitMatch = content.match(/exit (?:code )?(-?\d+)/i);
+23
View File
@@ -99,6 +99,29 @@ describe("summarizeToolResult — known tools", () => {
expect(out.isError).toBe(true);
expect(out.summary).toMatch(/exit 1/);
});
it("write_file: shows wrote line count + size", () => {
const out = summarizeToolResult("write_file", "alpha\nbeta\ngamma\n");
expect(out.isError).toBe(false);
expect(out.summary).toMatch(/wrote/);
expect(out.summary).toMatch(/4 lines/);
});
it("MCP-bridged tools pick up the same summary via suffix match", () => {
// `filesystem_read_file` should hit the read_file branch.
const out = summarizeToolResult(
"filesystem_read_file",
"import { foo } from 'bar';\nexport function baz() {}\n",
);
expect(out.summary).toMatch(/lines/);
expect(out.summary).toMatch(/import.*foo/);
});
it("suffix match doesn't false-trigger on non-underscore prefixes", () => {
// `myread_file` (no underscore separator) should NOT match read_file.
const out = summarizeToolResult("myread_file", "anything");
expect(out.summary).not.toMatch(/lines/);
});
});
describe("formatDuration", () => {