/** * Plan Mode — v1 * * Scope (deliberately minimal, grown iteratively): * - Toggle a read-only "plan mode" via `/plan`, `Ctrl+Alt+P`, or `--plan` * - While active: built-in write tools (`write`, `edit`) are disabled * - Footer status shows when plan mode is active * - The model is told it cannot make changes * - State survives session resume * - `/handover` distils the session into a plan document under `.pi/plans/` * * NOT handled yet (future iterations): bash write guarding, approval flow, * execution tracking. */ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { Key } from "@earendil-works/pi-tui"; import { writeHandover } from "./handover.ts"; /** Built-in tools that can mutate the filesystem. */ const WRITE_TOOLS = new Set(["write", "edit"]); const STATE_ENTRY = "plan-mode"; interface PlanModeState { enabled: boolean; /** Active tool list captured before plan mode narrowed it. */ savedTools?: string[]; } const PLAN_MODE_PROMPT = `[PLAN MODE ACTIVE] You are in plan mode. The built-in \`write\` and \`edit\` tools are disabled for this turn, so you cannot modify files. Investigate, read code, and answer. When the user asks for changes, describe what you would do instead of attempting it. Do not try to route around the restriction (for example by writing files through \`bash\`).`; export default function planModeExtension(pi: ExtensionAPI): void { let enabled = false; let savedTools: string[] | undefined; pi.registerFlag("plan", { description: "Start in plan mode (read-only)", type: "boolean", default: false, }); function persist(): void { pi.appendEntry(STATE_ENTRY, { enabled, savedTools } satisfies PlanModeState); } function applyToolRestrictions(): void { if (savedTools === undefined) { savedTools = pi.getActiveTools(); } pi.setActiveTools(savedTools.filter((name) => !WRITE_TOOLS.has(name))); } function restoreTools(): void { if (savedTools !== undefined) { pi.setActiveTools(savedTools); savedTools = undefined; return; } // No snapshot (e.g. resumed session): re-enable write tools additively. pi.setActiveTools([...new Set([...pi.getActiveTools(), ...WRITE_TOOLS])]); } function updateStatus(ctx: ExtensionContext): void { ctx.ui.setStatus("plan-mode", enabled ? ctx.ui.theme.fg("warning", "plan") : undefined); } function setEnabled(next: boolean, ctx: ExtensionContext): void { if (next === enabled) return; enabled = next; if (enabled) { applyToolRestrictions(); ctx.ui.notify("Plan mode on — write and edit disabled.", "info"); } else { restoreTools(); ctx.ui.notify("Plan mode off — write access restored.", "info"); } updateStatus(ctx); persist(); } pi.registerCommand("plan", { description: "Toggle plan mode (read-only)", handler: async (_args, ctx) => setEnabled(!enabled, ctx), }); pi.registerCommand("handover", { description: "Write a handover document for this session to .pi/plans/", handler: async (args, ctx) => { await writeHandover(ctx, args.trim()); }, }); pi.registerShortcut(Key.ctrlAlt("p"), { description: "Toggle plan mode", handler: async (ctx) => setEnabled(!enabled, ctx), }); pi.on("before_agent_start", async (event) => { if (!enabled) return; return { systemPrompt: `${event.systemPrompt}\n\n${PLAN_MODE_PROMPT}` }; }); pi.on("session_start", async (_event, ctx) => { const entries = ctx.sessionManager.getEntries(); const last = entries .filter((e: { type: string; customType?: string }) => e.type === "custom" && e.customType === STATE_ENTRY) .pop() as { data?: PlanModeState } | undefined; if (last?.data) { enabled = last.data.enabled; savedTools = last.data.savedTools; } if (pi.getFlag("plan") === true) { enabled = true; } if (enabled) applyToolRestrictions(); updateStatus(ctx); }); }