131 lines
3.8 KiB
TypeScript
131 lines
3.8 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|
|
}
|