import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { keyHint } from "@earendil-works/pi-coding-agent"; import { Text } from "@earendil-works/pi-tui"; import { runCli } from "../cli.ts"; import { TOOLS } from "../schemas.ts"; /** * `cbm_index_repository`: same passthrough execute as the generic tools, but * with a bespoke TUI renderer (collapsed one-liner + expanded detail table). */ type IndexResult = { project?: string; status?: string; nodes?: number; edges?: number; expected_nodes?: number; expected_edges?: number; skipped_count?: number; parse_partial_count?: number; not_indexed_files_count?: number; excluded?: { count?: number; dirs?: string[]; truncated?: boolean }; adr_present?: boolean; adr_hint?: string; artifact_present?: boolean; }; export function registerIndexRepository(pi: ExtensionAPI) { const def = TOOLS.find((t) => t.tool === "index_repository")!; pi.registerTool({ name: "cbm_index_repository", label: "cbm:index_repository", description: def.description, parameters: def.parameters, async execute(_toolCallId, params, signal) { if (signal?.aborted) { return { content: [{ type: "text", text: "Cancelled" }], details: {} }; } const out = await runCli("index_repository", params, signal ?? undefined); let parsed: IndexResult | null = null; try { parsed = JSON.parse(out) as IndexResult; } catch { /* fall back to raw text below */ } return { // LLM still sees the full JSON. content: [{ type: "text", text: out || "{}" }], // Renderer reads structured fields from details. details: parsed ?? { raw: out }, }; }, renderCall(args, theme) { const a = args as { repo_path?: string; name?: string; mode?: string }; let s = theme.fg("toolTitle", theme.bold("cbm:index_repository ")); s += theme.fg("accent", a.name || a.repo_path || "…"); if (a.mode) s += " " + theme.fg("muted", `[${a.mode}]`); return new Text(s, 0, 0); }, renderResult(result, { expanded, isPartial }, theme) { if (isPartial) { return new Text(theme.fg("warning", "⏳ Indexing…"), 0, 0); } const d = result.details as IndexResult & { raw?: string }; if (!d || d.raw !== undefined) { return new Text(theme.fg("error", `✗ ${d?.raw ?? "index failed"}`), 0, 0); } const check = d.expected_nodes != null && d.nodes !== d.expected_nodes; const head = theme.fg("success", theme.bold("✔ indexed ")) + theme.fg("accent", d.project ?? "?") + " " + theme.fg("muted", `${d.nodes ?? "?"} nodes · ${d.edges ?? "?"} edges`) + (check ? " " + theme.fg("warning", "(counts differ from expected)") : ""); if (!expanded) { return new Text(head + " " + theme.fg("dim", `(${keyHint("app.tools.expand", "expand")})`), 0, 0); } const lines: string[] = [head]; const row = (label: string, val: string) => " " + theme.fg("dim", label.padEnd(10)) + theme.fg("muted", val); lines.push(row("status", d.status ?? "?")); if (d.expected_nodes != null) lines.push(row("expected", `${d.expected_nodes} nodes · ${d.expected_edges ?? "?"} edges`)); if (d.skipped_count) lines.push(row("skipped", String(d.skipped_count))); if (d.parse_partial_count) lines.push(row("partial", String(d.parse_partial_count))); if (d.not_indexed_files_count) lines.push(row("unindexed", `${d.not_indexed_files_count} files (by design)`)); if (d.excluded?.count) { const dirs = (d.excluded.dirs ?? []).join(", "); lines.push(row("excluded", `${d.excluded.count}${dirs ? ` (${dirs})` : ""}`)); } lines.push(row("adr", d.adr_present ? theme.fg("success", "present") : theme.fg("dim", "none"))); lines.push( row("artifact", d.artifact_present ? theme.fg("success", "written") : theme.fg("dim", "none")), ); if (!d.adr_present && d.adr_hint) { lines.push(""); lines.push(" " + theme.fg("warning", " hint ") + theme.fg("dim", d.adr_hint)); } return new Text(lines.join("\n"), 0, 0); }, }); }