mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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:
@@ -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"}`;
|
||||
}
|
||||
Reference in New Issue
Block a user