| 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"; |
| import { sanitizeRenderableText } from "../tui-formatters.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 sanitizeRenderableText(detail); |
| } |
| if (!args || typeof args !== "object") { |
| return ""; |
| } |
| try { |
| return sanitizeRenderableText(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(sanitizeRenderableText(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); |
| } |
| } |
| } |
|
|