"use client";
import { memo, useMemo } from "react";
import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
import { cn } from "@/lib/utils";
import {
ToolStatusIcon,
basename,
getPatchInfo,
isCancelledToolStatus,
str,
truncate,
type ToolCallStatusLike,
} from "@/components/tools/tool-ui-shared";
// ── Helpers ────────────────────────────────────────────────────────────
const shortenPath = (filepath: string, depth = 2): string => {
const parts = filepath.split("/").filter(Boolean);
if (parts.length <= depth) return filepath;
return parts.slice(-depth).join("/");
};
// ── ToolCallShell ──────────────────────────────────────────────────────
const ToolCallShell = ({
toolName,
status,
children,
}: {
toolName: string;
status?: ToolCallStatusLike;
children?: React.ReactNode;
}) => {
const isCancelled = isCancelledToolStatus(status);
return (
{toolName}
{children}
);
};
// ── Factory for simple inline tools ────────────────────────────────────
const inlineTool = (
argKeys: string | string[],
format: (v: string) => string = truncate,
): ToolCallMessagePartComponent => {
const keys = Array.isArray(argKeys) ? argKeys : [argKeys];
const Component: ToolCallMessagePartComponent = memo(
({ toolName, args, status }) => {
const value = keys.reduce(
(acc, key) => acc || str((args as Record)?.[key]),
"",
);
return (
{value && {format(value)}}
);
},
);
return Component;
};
// ── Per-tool inline components ─────────────────────────────────────────
export const ReadInline = inlineTool(
["file_path", "filePath", "path", "file"],
(v) => truncate(shortenPath(v)),
);
ReadInline.displayName = "ReadInline";
export const EditInline = inlineTool("file_path", basename);
EditInline.displayName = "EditInline";
export const WriteInline = inlineTool("file_path", basename);
WriteInline.displayName = "WriteInline";
export const GrepInline = inlineTool("pattern");
GrepInline.displayName = "GrepInline";
export const GlobInline = inlineTool("pattern");
GlobInline.displayName = "GlobInline";
export const WebSearchInline = inlineTool("query");
WebSearchInline.displayName = "WebSearchInline";
export const WebFetchInline = inlineTool("url");
WebFetchInline.displayName = "WebFetchInline";
// Kept as a compact reference implementation even though the example
// currently uses the richer diff viewer from tool-ui-apply-patch.tsx.
export const ApplyPatchInline: ToolCallMessagePartComponent = memo(
({ toolName, args, status }) => {
const patchText = str(args?.patchText);
const patchInfo = useMemo(() => getPatchInfo(patchText), [patchText]);
const isRunning = status?.type === "running";
return (
{patchInfo.files.length > 0 && (
{patchInfo.files.length === 1
? patchInfo.files[0]
: `${patchInfo.files.length} files`}
)}
{(patchInfo.added > 0 || patchInfo.removed > 0) && !isRunning && (
{patchInfo.added > 0 && (
+{patchInfo.added}
)}
{patchInfo.removed > 0 && (
-{patchInfo.removed}
)}
)}
);
},
);
ApplyPatchInline.displayName = "ApplyPatchInline";
// ── Fallback — generic, for unknown/MCP tools ─────────────────────────
const SUMMARY_KEYS = [
"file_path",
"path",
"pattern",
"command",
"query",
"glob",
"url",
] as const;
const ToolCallFallbackImpl: ToolCallMessagePartComponent = ({
toolName,
args,
status,
}) => {
const summary = useMemo(() => {
if (!args || typeof args !== "object") return "";
for (const key of SUMMARY_KEYS) {
const v = (args as Record)[key];
if (typeof v === "string" && v) return truncate(v);
}
return "";
}, [args]);
return (
{summary && {summary}}
);
};
export const ToolCallFallback = memo(ToolCallFallbackImpl);
ToolCallFallback.displayName = "ToolCallFallback";