import { useEffect, useState } from "react";
import type { Block } from "../../lib/block";
import { cached, highlight } from "../../lib/highlighter";
import { iconFor } from "../../lib/tool-icon";
import { cn } from "../../lib/utils";
import { Button } from "../../ui/button";
import { SMALL_CODE_FONT, SMALL_LINE_HEIGHT } from "./constants";
import { useToolContext } from "./tool-context";
import {
agentPreview,
isSubagent,
language,
loadingStatus,
RESULT_PAD_X,
RESULT_PAD_Y,
ROW_HEIGHT,
titleFor,
TOOL_TEXT_FONT,
type ToolData,
} from "./tool-layout";
import { IconChevronRightMicro, IconCircleX } from "../../icons";
import "./tool.css";
type HighlightState = { source: string; lang?: string; html: string };
function ToolView(
{ data, expanded, settling, height, clipped, diff, images, width }: {
data: ToolData;
expanded: boolean;
settling: boolean;
height: number;
clipped: boolean;
diff?: Block;
images?: Block;
width: number;
},
) {
let preview = agentPreview(data);
let subagent = isSubagent(data);
let open = expanded && Boolean(data.result || preview || images);
let total = ROW_HEIGHT + (open ? height + (images?.measure(width).height || 0) : 0);
return (
{open && height > 0 && (
subagent && preview
?
: diff
? (
)
:
)}
{open && images && (
{images.render(width)}
)}
);
}
function headerClass(interactive: boolean, open: boolean, error: boolean) {
return cn(
"flex items-center gap-1.5 border-x border-t border-transparent px-1.5 text-left text-sm text-muted-foreground transition-[background-color,border-color,color] duration-150 ease-out squircle contain-strict",
interactive
&& (
error
? "hover:bg-muted data-[state=open]:hover:bg-code data-[state=closing]:hover:bg-code"
: "hover:bg-muted hover:text-foreground data-[state=open]:hover:bg-code data-[state=closing]:hover:bg-code"
),
open && "border-border bg-code",
open ? "rounded-t-md" : "rounded-md",
error && "text-destructive",
);
}
function ToolLabel(
{ data, expanded, settling, interactive }: {
data: ToolData;
expanded: boolean;
settling: boolean;
interactive: boolean;
},
) {
let Icon = iconFor(isSubagent(data) ? "subagent" : data.name);
let title = titleFor(data);
let loading = loadingStatus(data.status) || data.agent?.status === "pending"
|| data.agent?.status === "running";
let error = data.status === "error" || data.agent?.status === "error";
return (
<>
{title}
{interactive && (
)}
{error && (
)}
>
);
}
function ToolHeader(
{ data, expanded, settling, open }: {
data: ToolData;
expanded: boolean;
settling: boolean;
open: boolean;
},
) {
let context = useToolContext();
let toggle = context?.toggle;
let preview = agentPreview(data);
let interactive = Boolean((data.result || preview || data.images?.length) && toggle);
let title = titleFor(data);
let error = data.status === "error" || data.agent?.status === "error";
let cls = headerClass(interactive, open, error);
return (
<>
{interactive
? (
)
: (
)}
>
);
}
function numbered(result: string) {
let lines = result.split("\n");
let rows = lines.map(line => line.match(/^(\d+)\.\s?(.*)$/));
if (rows.some(row => !row)) return null;
let seen = new Map();
return {
nums: rows.map(row => {
let value = row![1]!;
let key = `${value}-${seen.get(value) || 0}`;
seen.set(value, (seen.get(value) || 0) + 1);
return { key, value };
}),
source: rows.map(row => row![2]!).join("\n"),
};
}
function ToolResult(
{ data, height, clipped, settling }: {
data: ToolData;
height: number;
clipped: boolean;
settling: boolean;
},
) {
let result = data.result!;
let nums = numbered(result);
let source = nums?.source || result;
let lang = language(data);
let context = useToolContext();
let title = titleFor(data);
let [state, setState] = useState(() => {
let html = cached(source, lang);
return html ? { source, lang, html } : null;
});
// Shiki highlighting is async: render plain text first, then swap in cached HTML when ready.
useEffect(() => {
let current = cached(source, lang);
if (current) {
setState(prev =>
sameHighlight(prev, source, lang, current) ? prev : {
source,
lang,
html: current,
}
);
return;
}
let cancelled = false;
highlight(source, lang).then(h => {
if (cancelled) return;
setState(prev => sameHighlight(prev, source, lang, h) ? prev : { source, lang, html: h });
});
return () => {
cancelled = true;
};
}, [source, lang]);
return (
{nums && (
{nums.nums.map(num => (
{num.value}
))}
)}
{state && state.source === source && state.lang === lang
?
: source}
{clipped && (
)}
);
}
function ToolAgentResult(
{ data, height, settling }: {
data: ToolData;
height: number;
settling: boolean;
},
) {
let preview = agentPreview(data) || "";
return (
);
}
function sameHighlight(
state: HighlightState | null,
source: string,
lang: string | undefined,
html: string,
) {
return state?.source === source && state.lang === lang && state.html === html;
}
function ToolDiffResult(
{ block, height, width, settling, last }: {
block: Block;
height: number;
width: number;
settling: boolean;
last: boolean;
},
) {
return (
*]:rounded-t-none [&>*]:!border-t-0",
!last && "[&>*]:rounded-b-none",
settling ? "tool-result-expand-out" : "tool-result-expand-in",
)}
style={{
blockSize: height,
boxSizing: "border-box",
}}
>
{block.render(width)}
);
}
export { ToolView };