131 lines
3.5 KiB
TypeScript
131 lines
3.5 KiB
TypeScript
import { Box, Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
|
|
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 {
|
|
if (!args || typeof args !== "object") return "";
|
|
const record = args as Record<string, unknown>;
|
|
if (toolName === "bash" && typeof record.command === "string") {
|
|
return record.command;
|
|
}
|
|
const path = typeof record.path === "string" ? record.path : undefined;
|
|
if (path) return path;
|
|
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 title = `${this.toolName}${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);
|
|
}
|
|
}
|
|
}
|