From f5771a022640662a7004bc379d7b8a32ccbbd8c1 Mon Sep 17 00:00:00 2001
From: Anthony Ronning <101225832+AnthonyRonning@users.noreply.github.com>
Date: Tue, 28 Jul 2026 05:23:02 +0000
Subject: [PATCH] feat(agent): compact tool call rows
---
frontend/src/components/AgentMode.tsx | 72 ++++++++++++---
.../services/agentToolPresentation.test.ts | 61 +++++++++++++
.../src/services/agentToolPresentation.ts | 87 +++++++++++++++++++
3 files changed, 208 insertions(+), 12 deletions(-)
create mode 100644 frontend/src/services/agentToolPresentation.test.ts
create mode 100644 frontend/src/services/agentToolPresentation.ts
diff --git a/frontend/src/components/AgentMode.tsx b/frontend/src/components/AgentMode.tsx
index 8a45c6b9f..59b15eda2 100644
--- a/frontend/src/components/AgentMode.tsx
+++ b/frontend/src/components/AgentMode.tsx
@@ -12,6 +12,7 @@ import { useOpenAI } from "@/ai/useOpenAi";
import {
AlertCircle,
ArrowUp,
+ Blocks,
Brain,
Camera,
Check,
@@ -20,17 +21,23 @@ import {
ChevronRight,
Circle,
Expand,
+ FilePenLine,
+ FileSearch,
FolderOpen,
+ Globe2,
Loader2,
Lock,
MessageSquarePlus,
MoreHorizontal,
ShieldCheck,
Shrink,
+ SquareTerminal,
Trash,
+ Wrench,
X,
Zap
} from "lucide-react";
+import type { LucideIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
AlertDialog,
@@ -109,6 +116,11 @@ import {
} from "@/services/agentMcpErrors";
import { reconcileNewChatMcpServerNames } from "@/services/agentMcpServers";
import { agentOperationFence } from "@/services/agentOperationFence";
+import {
+ agentToolKind,
+ agentToolKindLabel,
+ type AgentToolKind
+} from "@/services/agentToolPresentation";
import {
AgentThoughtLabelFinalRequestRegistry,
AgentThoughtLabelProvisionalScheduler,
@@ -4061,23 +4073,47 @@ function ToolCallRow({ item }: { item: AgentTimelineItem }) {
const status = item.status || "running";
const failed = status === "failed" || status === "error";
const active = isActiveAgentStatus(status);
+ const toolKind = agentToolKind(item.id, item.title);
+ const ToolKindIcon = AGENT_TOOL_KIND_ICONS[toolKind];
+ const toolKindLabel = agentToolKindLabel(toolKind);
const hasDetails =
Boolean(item.text?.trim()) || item.input !== undefined || item.output !== undefined;
const statusIcon = active ? (
-
+
) : failed ? (
-
+
) : (
-
+
);
const summary = (
-
- {statusIcon}
-
{toolTitle(item)}
-
+
+
+
+
+
+ {toolTitle(item)}
+
+
{formatStatus(status)}
+ {statusIcon}
);
@@ -4085,7 +4121,7 @@ function ToolCallRow({ item }: { item: AgentTimelineItem }) {
return (
@@ -4098,15 +4134,18 @@ function ToolCallRow({ item }: { item: AgentTimelineItem }) {
-
-
+
+
{summary}
-
+
{item.text ?
: null}
{item.input !== undefined ? (
@@ -4119,6 +4158,15 @@ function ToolCallRow({ item }: { item: AgentTimelineItem }) {
);
}
+const AGENT_TOOL_KIND_ICONS: Record
= {
+ shell: SquareTerminal,
+ "file-read": FileSearch,
+ "file-write": FilePenLine,
+ web: Globe2,
+ mcp: Blocks,
+ generic: Wrench
+};
+
function PermissionRow({
item,
onPermissionDecision
diff --git a/frontend/src/services/agentToolPresentation.test.ts b/frontend/src/services/agentToolPresentation.test.ts
new file mode 100644
index 000000000..65534a413
--- /dev/null
+++ b/frontend/src/services/agentToolPresentation.test.ts
@@ -0,0 +1,61 @@
+import { describe, expect, test } from "bun:test";
+import { agentToolKind, agentToolKindLabel } from "./agentToolPresentation";
+
+describe("agentToolKind", () => {
+ test.each([
+ ["functions.shell:1", "shell"],
+ ["functions.developer__shell:2", "shell"],
+ ["functions.read_file:3", "file-read"],
+ ["functions.developer__read_image:4", "file-read"],
+ ["functions.read:4", "file-read"],
+ ["functions.edit_file:5", "file-write"],
+ ["functions.write_file:6", "file-write"],
+ ["functions.edit:6", "file-write"],
+ ["functions.write:6", "file-write"],
+ ["functions.web_search:7", "web"],
+ ["functions.developer__open_url:8", "web"],
+ ["functions.github__search_code:9", "mcp"],
+ ["functions.skills__load_skill:10", "mcp"],
+ ["functions.skills__shell:11", "mcp"]
+ ] as const)("classifies %s as %s", (id, expected) => {
+ expect(agentToolKind(id)).toBe(expected);
+ });
+
+ test.each([
+ ["Terminal: pwd", "shell"],
+ ["Read file: notes.txt", "file-read"],
+ ["read: notes.txt", "file-read"],
+ ["Editor: report.txt", "file-write"],
+ ["edit: report.txt", "file-write"],
+ ["write: report.txt", "file-write"],
+ ["Write file: report.txt", "file-write"],
+ ["Web Search: Example Domain", "web"],
+ ["Open url: https://example.com", "web"]
+ ] as const)(
+ "uses Maple's stable %s label when a provider ID has no tool name",
+ (title, expected) => {
+ expect(agentToolKind("chatcmpl-tool-123", title)).toBe(expected);
+ }
+ );
+
+ test("uses the generic type when the canonical tool name and stable label are unsupported", () => {
+ expect(agentToolKind("chatcmpl-tool-123", "Unrecognized action: shell in prose")).toBe(
+ "generic"
+ );
+ expect(agentToolKind("functions.load_skill:10", "Loading skill: example")).toBe("generic");
+ expect(agentToolKind("functions.unknown_tool:11", "Terminal: ignored fallback")).toBe(
+ "generic"
+ );
+ });
+});
+
+describe("agentToolKindLabel", () => {
+ test("provides a text alternative for every visual type", () => {
+ expect(agentToolKindLabel("shell")).toBe("Shell command");
+ expect(agentToolKindLabel("file-read")).toBe("File read");
+ expect(agentToolKindLabel("file-write")).toBe("File change");
+ expect(agentToolKindLabel("web")).toBe("Web tool");
+ expect(agentToolKindLabel("mcp")).toBe("MCP tool");
+ expect(agentToolKindLabel("generic")).toBe("Tool call");
+ });
+});
diff --git a/frontend/src/services/agentToolPresentation.ts b/frontend/src/services/agentToolPresentation.ts
new file mode 100644
index 000000000..f8b3e4b3c
--- /dev/null
+++ b/frontend/src/services/agentToolPresentation.ts
@@ -0,0 +1,87 @@
+export type AgentToolKind = "shell" | "file-read" | "file-write" | "web" | "mcp" | "generic";
+
+const SHELL_TOOL_NAMES = new Set(["shell"]);
+const FILE_READ_TOOL_NAMES = new Set([
+ "read",
+ "read_file",
+ "read_image",
+ "list_files",
+ "glob",
+ "grep"
+]);
+const FILE_WRITE_TOOL_NAMES = new Set([
+ "edit",
+ "write",
+ "edit_file",
+ "write_file",
+ "text_editor",
+ "str_replace_editor",
+ "str_replace_based_edit_tool",
+ "apply_patch"
+]);
+const WEB_TOOL_NAMES = new Set(["web_search", "open_url"]);
+const MAPLE_EXTENSION_NAMES = new Set(["developer"]);
+const SHELL_TOOL_LABELS = new Set(["terminal", "shell"]);
+const FILE_READ_TOOL_LABELS = new Set([
+ "read",
+ "read file",
+ "read image",
+ "list files",
+ "find files",
+ "search"
+]);
+const FILE_WRITE_TOOL_LABELS = new Set(["edit", "write", "editor", "edit file", "write file"]);
+const WEB_TOOL_LABELS = new Set(["web search", "open url"]);
+
+function toolNameFromTimelineId(id: string): string | null {
+ const encodedName = id.startsWith("functions.") ? id.slice("functions.".length) : null;
+ if (!encodedName) return null;
+
+ const sequenceSeparator = encodedName.lastIndexOf(":");
+ const name = sequenceSeparator >= 0 ? encodedName.slice(0, sequenceSeparator) : encodedName;
+ return name.trim() || null;
+}
+
+function agentToolKindFromTitle(title: string | null | undefined): AgentToolKind {
+ const label = title?.split(":", 1)[0]?.trim().toLowerCase();
+ if (!label) return "generic";
+ if (SHELL_TOOL_LABELS.has(label)) return "shell";
+ if (FILE_READ_TOOL_LABELS.has(label)) return "file-read";
+ if (FILE_WRITE_TOOL_LABELS.has(label)) return "file-write";
+ if (WEB_TOOL_LABELS.has(label)) return "web";
+ return "generic";
+}
+
+export function agentToolKind(id: string, title?: string | null): AgentToolKind {
+ const encodedName = toolNameFromTimelineId(id);
+ if (!encodedName) return agentToolKindFromTitle(title);
+
+ const namespaceSeparator = encodedName.indexOf("__");
+ const namespace = namespaceSeparator >= 0 ? encodedName.slice(0, namespaceSeparator) : null;
+ const toolName =
+ namespaceSeparator >= 0 ? encodedName.slice(namespaceSeparator + 2) : encodedName;
+
+ if (namespace && !MAPLE_EXTENSION_NAMES.has(namespace)) return "mcp";
+ if (SHELL_TOOL_NAMES.has(toolName)) return "shell";
+ if (FILE_READ_TOOL_NAMES.has(toolName)) return "file-read";
+ if (FILE_WRITE_TOOL_NAMES.has(toolName)) return "file-write";
+ if (WEB_TOOL_NAMES.has(toolName)) return "web";
+ return "generic";
+}
+
+export function agentToolKindLabel(kind: AgentToolKind): string {
+ switch (kind) {
+ case "shell":
+ return "Shell command";
+ case "file-read":
+ return "File read";
+ case "file-write":
+ return "File change";
+ case "web":
+ return "Web tool";
+ case "mcp":
+ return "MCP tool";
+ default:
+ return "Tool call";
+ }
+}