Files
clawdbot/src/tui/components/tool-execution.ts
2026-01-03 12:35:23 +00:00

136 lines
3.6 KiB
TypeScript

import { Box, Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
import {
formatToolDetail,
resolveToolDisplay,
} from "../../agents/tool-display.js";
import { markdownTheme, theme } from "../theme/theme.js";
type ToolResultContent = {
type?: string;
text?: string;
mimeType?: string;
bytes?: number;
omitted?: boolean;
};
type ToolResult = {
content?: ToolResultContent[];
details?: Record<string, unknown>;
};
const PREVIEW_LINES = 12;
function formatArgs(toolName: string, args: unknown): string {
const display = resolveToolDisplay({ name: toolName, args });
const detail = formatToolDetail(display);
if (detail) return detail;
if (!args || typeof args !== "object") return "";
try {
return JSON.stringify(args);
} catch {
return "";
}
}
function extractText(result?: ToolResult): string {
if (!result?.content) return "";
const lines: string[] = [];
for (const entry of result.content) {
if (entry.type === "text" && entry.text) {
lines.push(entry.text);
} else if (entry.type === "image") {
const mime = entry.mimeType ?? "image";
const size = entry.bytes ? ` ${Math.round(entry.bytes / 1024)}kb` : "";
const omitted = entry.omitted ? " (omitted)" : "";
lines.push(`[${mime}${size}${omitted}]`);
}
}
return lines.join("\n").trim();
}
export class ToolExecutionComponent extends Container {
private box: Box;
private header: Text;
private argsLine: Text;
private output: Markdown;
private toolName: string;
private args: unknown;
private result?: ToolResult;
private expanded = false;
private isError = false;
private isPartial = true;
constructor(toolName: string, args: unknown) {
super();
this.toolName = toolName;
this.args = args;
this.box = new Box(1, 1, (line) => theme.toolPendingBg(line));
this.header = new Text("", 0, 0);
this.argsLine = new Text("", 0, 0);
this.output = new Markdown("", 0, 0, markdownTheme, {
color: (line) => theme.toolOutput(line),
});
this.addChild(new Spacer(1));
this.addChild(this.box);
this.box.addChild(this.header);
this.box.addChild(this.argsLine);
this.box.addChild(this.output);
this.refresh();
}
setArgs(args: unknown) {
this.args = args;
this.refresh();
}
setExpanded(expanded: boolean) {
this.expanded = expanded;
this.refresh();
}
setResult(result: ToolResult | undefined, opts?: { isError?: boolean }) {
this.result = result;
this.isPartial = false;
this.isError = Boolean(opts?.isError);
this.refresh();
}
setPartialResult(result: ToolResult | undefined) {
this.result = result;
this.isPartial = true;
this.refresh();
}
private refresh() {
const bg = this.isPartial
? theme.toolPendingBg
: this.isError
? theme.toolErrorBg
: theme.toolSuccessBg;
this.box.setBgFn((line) => bg(line));
const display = resolveToolDisplay({
name: this.toolName,
args: this.args,
});
const title = `${display.emoji} ${display.label}${this.isPartial ? " (running)" : ""}`;
this.header.setText(theme.toolTitle(theme.bold(title)));
const argLine = formatArgs(this.toolName, this.args);
this.argsLine.setText(argLine ? theme.dim(argLine) : theme.dim(" "));
const raw = extractText(this.result);
const text = raw || (this.isPartial ? "…" : "");
if (!this.expanded && text) {
const lines = text.split("\n");
const preview =
lines.length > PREVIEW_LINES
? `${lines.slice(0, PREVIEW_LINES).join("\n")}\n…`
: text;
this.output.setText(preview);
} else {
this.output.setText(text);
}
}
}