import type { Component } from "@f5-sales-demo/pi-tui"; import { Container, Text } from "@f5-sales-demo/pi-tui"; import { theme } from "../../modes/theme/theme"; import { shortenPath } from "../../tools/render-utils"; import type { ToolExecutionHandle } from "./tool-execution"; type ReadRenderArgs = { path?: string; file_path?: string; sel?: string; }; type ReadToolSuffixResolution = { from: string; to: string; }; type ReadToolResultDetails = { suffixResolution?: { from?: string; to?: string; }; }; function getSuffixResolution(details: ReadToolResultDetails | undefined): ReadToolSuffixResolution | undefined { if (typeof details?.suffixResolution?.from !== "string" || typeof details.suffixResolution.to !== "string") { return undefined; } return { from: details.suffixResolution.from, to: details.suffixResolution.to }; } type ReadEntry = { toolCallId: string; path: string; sel?: string; status: "pending" | "success" | "warning" | "error"; correctedFrom?: string; }; export class ReadToolGroupComponent extends Container implements ToolExecutionHandle { #entries = new Map(); #text: Text; constructor() { super(); this.#text = new Text("", 0, 0); this.addChild(this.#text); this.#updateDisplay(); } updateArgs(args: ReadRenderArgs, toolCallId?: string): void { if (!toolCallId) return; const rawPath = args.file_path || args.path || ""; const entry: ReadEntry = this.#entries.get(toolCallId) ?? { toolCallId, path: rawPath, sel: args.sel, status: "pending", }; entry.path = rawPath; entry.sel = args.sel; this.#entries.set(toolCallId, entry); this.#updateDisplay(); } updateResult( result: { content: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean }, isPartial = false, toolCallId?: string, ): void { if (!toolCallId) return; const entry = this.#entries.get(toolCallId); if (!entry) return; if (isPartial) return; const details = result.details as ReadToolResultDetails | undefined; const suffixResolution = getSuffixResolution(details); if (suffixResolution) { entry.path = suffixResolution.to; entry.correctedFrom = suffixResolution.from; } else { entry.correctedFrom = undefined; } entry.status = result.isError ? "error" : suffixResolution ? "warning" : "success"; this.#updateDisplay(); } setArgsComplete(_toolCallId?: string): void { this.#updateDisplay(); } setExpanded(_expanded: boolean): void { this.#updateDisplay(); } getComponent(): Component { return this; } #updateDisplay(): void { const entries = [...this.#entries.values()]; if (entries.length === 0) { this.#text.setText(theme.fg("toolTitle", theme.bold("Read"))); return; } if (entries.length === 1) { const entry = entries[0]; const pathDisplay = this.#formatPath(entry); this.#text.setText(`${theme.fg("toolTitle", theme.bold("Read"))} ${pathDisplay}`.trimEnd()); return; } const header = `${theme.fg("toolTitle", theme.bold("Read"))}${theme.fg("dim", ` (${entries.length})`)}`; const lines = [header]; const total = entries.length; for (const [index, entry] of entries.entries()) { const connector = index === total - 1 ? theme.tree.last : theme.tree.branch; const pathDisplay = this.#formatPath(entry); lines.push(` ${theme.fg("dim", connector)} ${pathDisplay}`.trimEnd()); } this.#text.setText(lines.join("\n")); } #formatPath(entry: ReadEntry): string { const filePath = shortenPath(entry.path); let pathDisplay = filePath ? theme.fg("contentAccent", filePath) : theme.fg("toolOutput", "…"); if (entry.sel) { pathDisplay += theme.fg("warning", `:${entry.sel}`); } if (entry.correctedFrom) { pathDisplay += theme.fg("dim", ` (corrected from ${shortenPath(entry.correctedFrom)})`); } return pathDisplay; } }