fix(agents): add compact summaries for developer MCP tool rows

- Add agentSessionToolSummary helper to classify buzz-dev-mcp tools and build status-aware labels with parameter previews
- Extend AgentSessionToolItem to use the muted compact row for shell, read_file, view_image, str_replace, todo, and hook tools while preserving Buzz relay tool UI
- Add unit tests for developer tool detection, previews, and running/failed labels
This commit is contained in:
Taylor Ho
2026-06-15 00:30:43 -07:00
parent d49f05a9eb
commit ed4b37c195
3 changed files with 349 additions and 27 deletions
@@ -15,8 +15,11 @@ import {
formatToolTitle,
getBuzzToolInfo,
getToolStatusDisplay,
normalizeToolNameText,
} from "./agentSessionToolCatalog";
import {
buildCompactToolSummary,
isCompactDeveloperTool,
} from "./agentSessionToolSummary";
import {
asRecord,
formatCodeValue,
@@ -46,7 +49,10 @@ export function ToolItem({
const ToolIcon = buzzTool?.icon ?? Wrench;
const showStatus = status.state !== "output-available";
const toolTitle = formatToolTitle(canonicalToolName, item.title);
const isCommandTool = isShellCommandTool(item);
const useCompactSummary = isCompactDeveloperTool(item);
const compactSummary = useCompactSummary
? buildCompactToolSummary(item)
: null;
const duration = getToolDuration(item);
const handleToggle = React.useCallback(
(event: React.SyntheticEvent<HTMLDetailsElement>) => {
@@ -73,11 +79,15 @@ export function ToolItem({
<summary
className={cn(
"inline-flex max-w-full cursor-pointer list-none items-center gap-1.5 py-px",
isCommandTool && "text-muted-foreground",
useCompactSummary && "text-muted-foreground",
)}
>
{isCommandTool ? (
<CommandToolSummary item={item} duration={duration} />
{compactSummary ? (
<CompactToolSummaryRow
duration={duration}
preview={compactSummary.preview}
label={compactSummary.label}
/>
) : (
<>
{ToolIcon ? (
@@ -135,30 +145,24 @@ export function ToolItem({
);
}
function CommandToolSummary({
function CompactToolSummaryRow({
duration,
item,
label,
preview,
}: {
duration: string | null;
item: Extract<TranscriptItem, { type: "tool" }>;
label: string;
preview: string | null;
}) {
const command = getToolString(item.args, ["command"]);
const label =
item.status === "failed" || item.isError
? "Command failed"
: item.status === "executing" || item.status === "pending"
? "Running command"
: "Ran command";
return (
<>
<span className="shrink-0 text-sm font-semibold">{label}</span>
{command ? (
{preview ? (
<span
className="min-w-0 max-w-48 truncate text-sm text-muted-foreground/70"
title={command}
title={preview}
>
{command}
{preview}
</span>
) : null}
{duration ? (
@@ -171,14 +175,6 @@ function CommandToolSummary({
);
}
function isShellCommandTool(item: Extract<TranscriptItem, { type: "tool" }>) {
return [item.buzzToolName, item.toolName, item.title].some((value) => {
if (!value) return false;
const normalized = normalizeToolNameText(value);
return normalized === "shell" || normalized.endsWith("_shell");
});
}
function getToolDuration(item: Extract<TranscriptItem, { type: "tool" }>) {
if (item.startedAt && item.completedAt) {
return formatDuration(item.startedAt, item.completedAt);
@@ -0,0 +1,123 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
buildCompactToolSummary,
isCompactDeveloperTool,
} from "./agentSessionToolSummary.ts";
const baseTimestamp = "2026-06-14T19:00:00.000Z";
function makeTool(overrides = {}) {
return {
id: "tool:1",
type: "tool",
title: "Tool call",
toolName: "shell",
buzzToolName: null,
status: "completed",
args: {},
result: "",
isError: false,
timestamp: baseTimestamp,
startedAt: baseTimestamp,
completedAt: "2026-06-14T19:00:01.000Z",
...overrides,
};
}
test("isCompactDeveloperTool returns false for Buzz relay tools", () => {
assert.equal(
isCompactDeveloperTool(
makeTool({
toolName: "send_message",
buzzToolName: "send_message",
title: "Send Message",
}),
),
false,
);
});
test("isCompactDeveloperTool detects buzz-dev-mcp shell tools", () => {
assert.equal(
isCompactDeveloperTool(
makeTool({
toolName: "buzz-dev-mcp__shell",
title: "buzz-dev-mcp__shell",
}),
),
true,
);
});
test("buildCompactToolSummary formats shell command preview", () => {
const summary = buildCompactToolSummary(
makeTool({
toolName: "buzz-dev-mcp__shell",
args: { command: "git status" },
}),
);
assert.equal(summary.label, "Ran command");
assert.equal(summary.preview, "git status");
});
test("buildCompactToolSummary formats view_image source preview", () => {
const summary = buildCompactToolSummary(
makeTool({
toolName: "buzz-dev-mcp__view_image",
args: {
source:
"https://sprout-oss.stage.blox.sqprod.co/media/ffd1b2721f2d52e19f0ca2be9aa7842cdec5b4e0215aaab2a67c26a2a76a6a83.png",
},
}),
);
assert.equal(summary.label, "Viewed image");
assert.ok(summary.preview?.startsWith("https://sprout-oss"));
});
test("buildCompactToolSummary formats read_file path preview", () => {
const summary = buildCompactToolSummary(
makeTool({
toolName: "read_file",
args: { path: "desktop/src/app/App.tsx" },
}),
);
assert.equal(summary.label, "Read file");
assert.equal(summary.preview, "desktop/src/app/App.tsx");
});
test("buildCompactToolSummary formats todo list preview", () => {
const summary = buildCompactToolSummary(
makeTool({
toolName: "todo",
args: {
todos: [
{ text: "Ship compact summaries", done: false },
{ text: "Verify UI", done: false },
],
},
}),
);
assert.equal(summary.label, "Updated todos");
assert.equal(summary.preview, "Ship compact summaries (+1)");
});
test("buildCompactToolSummary uses running and failed labels", () => {
assert.equal(
buildCompactToolSummary(
makeTool({ toolName: "str_replace", status: "executing" }),
).label,
"Editing file",
);
assert.equal(
buildCompactToolSummary(
makeTool({ toolName: "str_replace", status: "failed", isError: true }),
).label,
"Edit failed",
);
});
@@ -0,0 +1,203 @@
import type { ToolStatus, TranscriptItem } from "./agentSessionTypes";
import {
getBuzzToolInfo,
normalizeToolNameText,
} from "./agentSessionToolCatalog";
import { asRecord, getToolString } from "./agentSessionUtils";
export type CompactToolKind =
| "shell"
| "read_file"
| "view_image"
| "str_replace"
| "todo"
| "stop_hook"
| "post_compact_hook"
| "dev_mcp";
export type CompactToolSummary = {
kind: CompactToolKind;
label: string;
preview: string | null;
};
const DEVELOPER_TOOL_BASES = new Set([
"shell",
"read_file",
"view_image",
"str_replace",
"todo",
"stop",
"postcompact",
]);
type ToolItem = Extract<TranscriptItem, { type: "tool" }>;
/** Whether this tool row should use the muted compact developer summary. */
export function isCompactDeveloperTool(item: ToolItem): boolean {
if (item.buzzToolName && getBuzzToolInfo(item.buzzToolName)) {
return false;
}
return resolveDeveloperToolKind(item) !== null;
}
/** Build the compact summary label and preview for developer MCP tool rows. */
export function buildCompactToolSummary(item: ToolItem): CompactToolSummary {
const kind = resolveDeveloperToolKind(item) ?? "dev_mcp";
const preview = extractCompactToolPreview(item, kind);
return {
kind,
label: compactToolLabel(kind, item.status, item.isError),
preview,
};
}
function resolveDeveloperToolKind(item: ToolItem): CompactToolKind | null {
for (const value of [item.toolName, item.title, item.buzzToolName]) {
const kind = classifyDeveloperToolName(value);
if (kind) return kind;
}
return null;
}
function classifyDeveloperToolName(
value: string | null | undefined,
): CompactToolKind | null {
if (!value) return null;
const normalized = normalizeToolNameText(value);
const base = stripMcpServerPrefix(normalized);
if (base === "shell" || normalized.endsWith("_shell")) {
return "shell";
}
if (base === "read_file") return "read_file";
if (base === "view_image") return "view_image";
if (base === "str_replace") return "str_replace";
if (base === "todo") return "todo";
if (base === "stop") return "stop_hook";
if (base === "postcompact") return "post_compact_hook";
if (DEVELOPER_TOOL_BASES.has(base)) {
return base === "shell" ? "shell" : "dev_mcp";
}
if (normalized.includes("buzz_dev_mcp")) {
return "dev_mcp";
}
return null;
}
function stripMcpServerPrefix(normalized: string): string {
return normalized.replace(/^buzz_dev_mcp_/, "");
}
function compactToolLabel(
kind: CompactToolKind,
status: ToolStatus,
isError: boolean,
): string {
const failed = isError || status === "failed";
const running = status === "executing" || status === "pending";
const labels: Record<
CompactToolKind,
{ completed: string; running: string; failed: string }
> = {
shell: {
completed: "Ran command",
running: "Running command",
failed: "Command failed",
},
read_file: {
completed: "Read file",
running: "Reading file",
failed: "Read failed",
},
view_image: {
completed: "Viewed image",
running: "Viewing image",
failed: "View failed",
},
str_replace: {
completed: "Edited file",
running: "Editing file",
failed: "Edit failed",
},
todo: {
completed: "Updated todos",
running: "Updating todos",
failed: "Todo update failed",
},
stop_hook: {
completed: "Checked todos",
running: "Checking todos",
failed: "Todo check failed",
},
post_compact_hook: {
completed: "Synced todos",
running: "Syncing todos",
failed: "Todo sync failed",
},
dev_mcp: {
completed: "Ran tool",
running: "Running tool",
failed: "Tool failed",
},
};
const set = labels[kind];
if (failed) return set.failed;
if (running) return set.running;
return set.completed;
}
function extractCompactToolPreview(
item: ToolItem,
kind: CompactToolKind,
): string | null {
const args = item.args;
switch (kind) {
case "shell":
return getToolString(args, ["command"]);
case "read_file":
case "str_replace":
return getToolString(args, ["path"]);
case "view_image":
return getToolString(args, ["source"]);
case "todo":
return getTodoPreview(args);
case "stop_hook":
case "post_compact_hook":
return null;
case "dev_mcp":
return (
getToolString(args, ["command", "path", "source", "query", "name"]) ??
null
);
}
}
function getTodoPreview(args: Record<string, unknown>): string | null {
const todos = args.todos;
if (!Array.isArray(todos)) {
return "todo list";
}
if (todos.length === 0) {
return "empty list";
}
const first = todos[0];
const firstText =
first && typeof first === "object"
? getToolString(asRecord(first), ["text"])
: null;
if (firstText) {
return todos.length > 1 ? `${firstText} (+${todos.length - 1})` : firstText;
}
return `${todos.length} item${todos.length === 1 ? "" : "s"}`;
}