/** * session-tool-calls.ts — Tool call visualization component. * * Shows active and recent tool calls as bordered boxes, * rendered inline within the live session view. */ import { type Component, type TUI, truncateToWidth, } from "@mariozechner/pi-tui"; import type { AgentSessionEvent, SessionEventStream } from "../session-events.js"; import type { Theme } from "./agent-widget.js"; /** Maximum number of tool call boxes shown simultaneously. */ const MAX_VISIBLE = 3; interface ToolCallBox { name: string; toolUseId: string; status: "active" | "completed"; detail: string; } /** * Pane displaying active and recent tool calls as bordered boxes. */ export class SessionToolCalls implements Component { private toolCalls: ToolCallBox[] = []; private unsubscribe: (() => void) | null = null; private closed = false; constructor( private tui: TUI,events: SessionEventStream, private theme: Theme, ) { this.unsubscribe = events.subscribe((event: AgentSessionEvent) => { if (this.closed) return; this.handleEvent(event); this.tui.requestRender(); }); } private handleEvent(event: AgentSessionEvent): void { switch (event.type) { case "tool_start": this.toolCalls.unshift({ name: event.toolName, toolUseId: event.toolUseId, status: "active", detail: typeof event.args === "string" ? event.args : JSON.stringify(event.args ?? "").slice(0, 120), }); if (this.toolCalls.length > MAX_VISIBLE) { this.toolCalls = this.toolCalls.slice(0, MAX_VISIBLE); } break; case "tool_end": // Mark matching tool as completed — match by unique toolUseId for concurrent safety for (const tc of this.toolCalls) { if (tc.toolUseId === event.toolUseId && tc.status === "active") { tc.status = "completed"; // Keep detail but add result summary const out = event.output?.length > 80 ? event.output.slice(0, 80) + "..." : event.output ?? ""; if (out) tc.detail = out; break; } } break; default: break; } } // ---- Component Interface ---- render(width: number): string[] { if (this.closed || this.toolCalls.length === 0) return []; if (width < 20) return []; const th = this.theme; const lines: string[] = []; for (const tc of this.toolCalls) { const color = tc.status === "active" ? "accent" : "dim"; const innerW = Math.max(1, width - 4); const title = tc.status === "active" ? `◉ ${tc.name}` : `✓ ${tc.name}`; const topBorder = `╭─ ${title} ${"─".repeat(Math.max(1, innerW - title.length - 2))}╮`; lines.push(th.fg(color, truncateToWidth(topBorder, width))); if (tc.detail) { const detailLine = `│ ${tc.detail}`; lines.push(th.fg(tc.status === "active" ? "fg" : "dim", truncateToWidth(detailLine, width))); } const bottomBorder = `╰${"─".repeat(Math.max(1, width - 2))}╯`; lines.push(th.fg(color, truncateToWidth(bottomBorder, width))); // Spacing between tool call boxes lines.push(""); } return lines; } handleInput(_data: string): void { // No input handling needed } invalidate(): void { // No cached state } dispose(): void { this.closed = true; if (this.unsubscribe) { this.unsubscribe(); this.unsubscribe = null; } this.toolCalls = []; } }