import type { ToolCallTimelineItem } from "../server/agent/agent-sdk-types.js"; import { getPaseoToolLeafName, isPaseoToolName } from "../server/agent/tool-name-normalization.js"; import { stripCwdPrefix } from "./path-utils.js"; export type ToolCallDisplayInput = Pick< ToolCallTimelineItem, "name" | "status" | "error" | "metadata" | "detail" > & { cwd?: string; }; export interface ToolCallDisplayModel { displayName: string; summary?: string; errorText?: string; } interface DetailDisplay { displayName?: string; summary?: string; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function readString(value: unknown): string | undefined { return typeof value === "string" && value.length > 0 ? value : undefined; } function humanizeToolName(name: string): string { const trimmed = name.trim(); if (!trimmed) { return name; } if (isPaseoToolName(trimmed)) { const leaf = getPaseoToolLeafName(trimmed); if (leaf) { return humanizeToolName(leaf); } } if (/[:./]/.test(trimmed) || trimmed.includes("__")) { return trimmed; } return trimmed .replace(/[._-]+/g, " ") .split(" ") .filter((segment) => segment.length > 0) .map((segment) => `${segment[0]?.toUpperCase() ?? ""}${segment.slice(1)}`) .join(" "); } function formatErrorText(error: unknown): string | undefined { if (error === null || error === undefined) { return undefined; } if (typeof error === "string") { return error; } if (isRecord(error) && typeof error.content === "string") { return error.content; } try { return JSON.stringify(error, null, 2); } catch { return String(error); } } function buildFilePathDisplay( displayName: string, filePath: string, cwd: string | undefined, ): DetailDisplay { return { displayName, summary: stripCwdPrefix(filePath, cwd), }; } function buildCanonicalDetailDisplay(input: ToolCallDisplayInput): DetailDisplay { switch (input.detail.type) { case "shell": return { displayName: "Shell", summary: input.detail.command, }; case "read": return buildFilePathDisplay("Read", input.detail.filePath, input.cwd); case "edit": return buildFilePathDisplay("Edit", input.detail.filePath, input.cwd); case "write": return buildFilePathDisplay("Write", input.detail.filePath, input.cwd); case "search": return { displayName: "Search", summary: input.detail.query, }; case "fetch": return { displayName: "Fetch", summary: input.detail.url, }; case "worktree_setup": return { displayName: "Worktree Setup", summary: input.detail.branchName, }; case "sub_agent": return { displayName: readString(input.detail.subAgentType) ?? "Task", summary: readString(input.detail.description), }; case "plain_text": return { summary: input.detail.label, }; case "plan": return { displayName: "Plan", }; case "unknown": return {}; } } function buildUnknownDetailOverride(input: ToolCallDisplayInput): DetailDisplay { const lowerName = input.name.trim().toLowerCase(); if (input.detail.type === "unknown" && lowerName === "task") { return { displayName: "Task", summary: isRecord(input.metadata) ? readString(input.metadata.subAgentActivity) : undefined, }; } if (input.detail.type === "unknown" && lowerName === "thinking") { return { displayName: "Thinking", }; } if (lowerName === "terminal") { return { displayName: "Terminal", summary: input.detail.type === "plain_text" ? readString(input.detail.label) : undefined, }; } return {}; } export function buildToolCallDisplayModel(input: ToolCallDisplayInput): ToolCallDisplayModel { const canonicalDisplay = buildCanonicalDetailDisplay(input); const unknownDetailOverride = buildUnknownDetailOverride(input); const displayName = unknownDetailOverride.displayName ?? canonicalDisplay.displayName ?? humanizeToolName(input.name); const summary = unknownDetailOverride.summary ?? canonicalDisplay.summary; const errorText = input.status === "failed" ? formatErrorText(input.error) : undefined; return { displayName, ...(summary ? { summary } : {}), ...(errorText ? { errorText } : {}), }; }