import { StringEnum } from "@earendil-works/pi-ai"; import { defineTool, keyHint } from "@earendil-works/pi-coding-agent"; import { Text } from "@earendil-works/pi-tui"; import { Type } from "typebox"; import type { BundleSlot } from "./bundle-slot.js"; import { loadBundleSlot } from "./loaders.js"; import { expandableFallback } from "./tool-rendering.js"; const DESCRIPTION = "Reserve a unique System document ID and canonical bundle path from its type and semantic title."; const GUIDANCE = [ "Resolve needed clarifications before calling system_bundle_slot.", "Call system_bundle_slot once per required bundle after choosing its final type and title; use each returned ID and path exactly.", ]; const BUNDLE_TYPES = ["research", "spec", "phase", "plan", "issue"] as const; export function createBundleSlotTool(onReserved?: () => void) { const reserved = new Set(); return defineTool({ name: "system_bundle_slot", label: "system_bundle_slot", description: DESCRIPTION, promptSnippet: "Reserve an ID and path for a System bundle", promptGuidelines: GUIDANCE, parameters: Type.Object({ type: StringEnum(BUNDLE_TYPES, { description: "System bundle type.", }), title: Type.String({ minLength: 1, description: "Final semantic bundle title used to derive the slug.", }), }), renderCall(args, theme, context) { const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); text.setText( `${theme.fg("toolTitle", theme.bold("system_bundle_slot"))} ${theme.fg("accent", String(args.type ?? "bundle"))} ${theme.fg("toolOutput", `· ${bounded(String(args.title ?? "untitled"), 72)}`)}`, ); return text; }, renderResult(result, { expanded }, theme, context) { const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); const slot = result.details as BundleSlot | undefined; if ( context.isError || !slot || typeof slot.id !== "string" || typeof slot.slug !== "string" || typeof slot.path !== "string" ) { text.setText( expandableFallback( result.content, expanded, theme, context.isError ? "error" : "muted", ), ); return text; } let output = `${theme.fg("success", "✓ reserved")} ${theme.fg("accent", slot.id)}${theme.fg("dim", ` · ${bounded(slot.slug, 40)}`)}`; if (!expanded) { output += theme.fg( "dim", ` (${keyHint("app.tools.expand", "to expand")})`, ); } else { output += [ `\n${theme.fg("dim", "type ")}${theme.fg("toolOutput", slot.type)}`, `${theme.fg("dim", "title ")}${theme.fg("toolOutput", slot.title)}`, `${theme.fg("dim", "path ")}${theme.fg("toolOutput", slot.path)}`, ].join("\n"); } text.setText(output); return text; }, async execute(_toolCallId, params, _signal, _onUpdate, ctx) { const { reserveBundleSlot } = await loadBundleSlot(); const slot = reserveBundleSlot(ctx.cwd, params, reserved); onReserved?.(); return { content: [ { type: "text" as const, text: [ `Reserved ${slot.type} bundle.`, `Title: ${slot.title}`, `ID: ${slot.id}`, `Path: ${slot.path}`, "Use the ID and path exactly.", ].join("\n"), }, ], details: slot, }; }, }); } function bounded(value: string, limit: number): string { return value.length <= limit ? value : `${value.slice(0, limit - 1)}…`; }