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_list_projects`: same passthrough execute as the generic tools, but with * a bespoke TUI renderer. The raw JSON carries per-project metadata, so the * renderer collapses it to "N projects" / "none" (expandable to a short list). * Node/edge counts only appear with `include_details: true` (0.10.x); the * envelope carries `total`/`has_more` for paging. The LLM still sees the full * JSON in `content`. */ type Project = { name?: string; root_path?: string; branch?: string; nodes?: number; edges?: number; }; type ProjectList = { projects?: Project[]; total?: number; has_more?: boolean }; export function registerListProjects(pi: ExtensionAPI) { const def = TOOLS.find((t) => t.tool === "list_projects")!; pi.registerTool({ name: "cbm_list_projects", label: "cbm:list_projects", 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("list_projects", params, signal ?? undefined); let parsed: ProjectList | null = null; try { parsed = JSON.parse(out) as ProjectList; } 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) { return new Text(theme.fg("toolTitle", theme.bold("cbm:list_projects")), 0, 0); }, renderResult(result, { expanded, isPartial }, theme) { if (isPartial) { return new Text(theme.fg("warning", "⏳ Listing…"), 0, 0); } const d = result.details as ProjectList & { raw?: string }; if (!d || d.raw !== undefined) { return new Text(theme.fg("error", `✗ ${d?.raw ?? "list failed"}`), 0, 0); } const projects = d.projects ?? []; const count = d.total ?? projects.length; // None indexed → single dim line. if (count === 0) { return new Text(theme.fg("dim", "∅ no indexed projects"), 0, 0); } const head = theme.fg("success", theme.bold(`✔ ${count} project${count === 1 ? "" : "s"}`)) + (d.has_more ? " " + theme.fg("dim", "· more") : ""); if (!expanded) { return new Text( head + " " + theme.fg("dim", `(${keyHint("app.tools.expand", "expand")})`), 0, 0, ); } const lines: string[] = [head]; for (const p of projects.slice(0, 20)) { const name = theme.fg("accent", p.name ?? "?"); const stats = p.nodes != null ? " " + theme.fg("muted", `${p.nodes} nodes · ${p.edges ?? "?"} edges`) : p.root_path ? " " + theme.fg("muted", p.root_path) : ""; lines.push(" " + name + stats); } if (projects.length > 20) { lines.push(" " + theme.fg("dim", `… ${projects.length - 20} more`)); } return new Text(lines.join("\n"), 0, 0); }, }); }