repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/the-system/bundle-slot-tool.ts
Rawimport { 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<string>();
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)}…`;
}