@bacnh85/pi-plan 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +21 -11
  2. package/index.ts +257 -62
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,10 +2,11 @@
2
2
 
3
3
  Pi extension that adds a lightweight plan mode inspired by Codex and Claude Code:
4
4
 
5
- - Toggle plan mode with `/plan`, `/plan on`, `/plan off`, or `Ctrl+Alt+P`.
6
- - Pick separate thinking/reasoning levels for planning and normal execution with `/plan levels`.
5
+ - Toggle plan mode with `/plan` or `Ctrl+Alt+P`.
6
+ - Remembers separate thinking/reasoning levels for planning and normal execution across sessions based on the active mode when you change Pi's reasoning level.
7
7
  - Keeps planning read-only by disabling built-in `edit`/`write` and blocking destructive shell commands.
8
8
  - Provides a `write_plan` tool so the agent writes reviewable Markdown plans into `.agents/plans/` in the current workspace.
9
+ - Provides an `ask_plan_question` tool so the agent can ask selection-style clarifying questions during planning, with an option for free-form user input.
9
10
  - Prompts after a plan is written so you can approve execution, approve only, keep planning, or refine with feedback.
10
11
 
11
12
  ## Install
@@ -39,29 +40,38 @@ pi --plan
39
40
  | Command / shortcut | Description |
40
41
  | --- | --- |
41
42
  | `/plan` | Toggle plan mode. |
42
- | `/plan on` / `/plan off` | Explicitly enter or exit plan mode. |
43
- | `/plan levels` | Select planning and normal/execution thinking levels. |
44
- | `/plan status` | Show current mode, thinking levels, and last plan file. |
45
- | `/plan high` | Set the plan-mode thinking level directly (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`). |
46
43
  | `Ctrl+Alt+P` | Toggle plan mode. |
47
44
 
48
45
  ## Workflow
49
46
 
50
47
  1. Enter plan mode.
51
48
  2. Ask pi to research the task and propose an implementation.
52
- 3. The model explores read-only context and calls `write_plan`.
53
- 4. The plan is saved under `.agents/plans/<timestamp>-<title>.md`.
54
- 5. Choose one of the approval options:
55
- - **Approve and execute**: exits plan mode, restores tools, applies normal thinking level, and sends a follow-up execution prompt.
49
+ 3. The model explores read-only context. If a consequential decision remains ambiguous, it can call `ask_plan_question` so you can choose an option or type your own answer.
50
+ 4. The model calls `write_plan`.
51
+ 5. The plan is saved under `.agents/plans/<timestamp>-<title>.md`.
52
+ 6. Choose one of the approval options:
53
+ - **Execute in current session**: exits plan mode, restores tools, applies normal thinking level, shows current context usage in the option label, and sends a follow-up execution prompt.
54
+ - **Execute in new session**: exits plan mode and prepares a fresh session that uses the saved Markdown plan file as the handoff artifact.
56
55
  - **Approve only**: exits plan mode without starting work.
57
56
  - **Keep planning**: remains in plan mode.
58
57
  - **Refine with feedback**: sends your feedback as a follow-up planning prompt.
59
58
 
59
+ Agents should ask blocking, user-answerable planning questions with `ask_plan_question` before finalizing a plan. Final plans may still list non-blocking uncertainties or implementation-time checks, but should not leave consequential user decisions unresolved.
60
+
61
+ ## Reasoning levels
62
+
63
+ pi-plan remembers two reasoning levels:
64
+
65
+ - Change Pi's active reasoning level while plan mode is active to update the planning level.
66
+ - Change Pi's active reasoning level in normal or execution mode to update the normal/execution level.
67
+
68
+ The remembered levels are restored when switching modes and across independent or resumed sessions. pi-plan stores only these non-sensitive preferences under your user Pi agent directory.
69
+
60
70
  ## Design notes
61
71
 
62
72
  Research findings used for this extension:
63
73
 
64
- - Pi has no built-in plan mode by design, but extensions can implement it with commands, shortcuts, tool gating, status widgets, and prompt injection.
74
+ - Pi has no built-in plan mode by design, but extensions can implement it with commands, shortcuts, tools, tool gating, and prompt injection.
65
75
  - Claude Code plan mode emphasizes read-only exploration, writing a Markdown plan, and an approval flow before edits.
66
76
  - Codex guidance recommends plan mode for complex or ambiguous tasks and using higher reasoning levels for harder planning/debugging work.
67
77
 
package/index.ts CHANGED
@@ -1,14 +1,18 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { isToolCallEventType, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
3
3
  import { Type } from "typebox";
4
- import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
5
+ import os from "node:os";
5
6
  import path from "node:path";
6
7
 
7
8
  const STATUS_KEY = "pi-plan";
8
9
  const PLAN_DIR = path.join(".agents", "plans");
9
10
  const PLAN_TOOL = "write_plan";
11
+ const PLAN_QUESTION_TOOL = "ask_plan_question";
12
+ const PLAN_EXECUTE_COMMAND = "plan-execute";
13
+ const PREFERENCES_FILE = path.join(os.homedir(), ".pi", "agent", "pi-plan", "preferences.json");
10
14
  const PLAN_MODE_DISABLED_TOOLS = new Set(["edit", "write"]);
11
- const DEFAULT_PLAN_TOOLS = ["read", "bash", "grep", "find", "ls", PLAN_TOOL];
15
+ const DEFAULT_PLAN_TOOLS = ["read", "bash", "grep", "find", "ls", PLAN_TOOL, PLAN_QUESTION_TOOL];
12
16
  const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
13
17
 
14
18
  type ThinkingLevel = (typeof THINKING_LEVELS)[number];
@@ -25,12 +29,29 @@ interface PlanState {
25
29
  lastPlanStatus?: PlanStatus;
26
30
  }
27
31
 
32
+ interface PlanPreferences {
33
+ version: 1;
34
+ planThinking: ThinkingLevel;
35
+ normalThinking: ThinkingLevel;
36
+ }
37
+
28
38
  interface WritePlanParams {
29
39
  title: string;
30
40
  content: string;
31
41
  status?: PlanStatus;
32
42
  }
33
43
 
44
+ interface PlanQuestionOption {
45
+ label: string;
46
+ description?: string;
47
+ }
48
+
49
+ interface PlanQuestionParams {
50
+ question: string;
51
+ options: PlanQuestionOption[];
52
+ allowOther?: boolean;
53
+ }
54
+
34
55
  const DESTRUCTIVE_BASH_PATTERNS = [
35
56
  /\brm\b/i,
36
57
  /\brmdir\b/i,
@@ -90,6 +111,43 @@ function relativeToCwd(cwd: string, absolutePath: string): string {
90
111
  return path.relative(cwd, absolutePath).split(path.sep).join("/");
91
112
  }
92
113
 
114
+ function formatContextUsage(ctx: ExtensionContext): string {
115
+ const usage = ctx.getContextUsage();
116
+ if (!usage || usage.percent === null) return "context usage unknown";
117
+ const percent = `${Math.round(usage.percent)}% context used`;
118
+ if (usage.tokens === null) return percent;
119
+ return `${percent} (${Math.round(usage.tokens / 1000)}k / ${Math.round(usage.contextWindow / 1000)}k tokens)`;
120
+ }
121
+
122
+ function buildExecutionPrompt(relativePlan: string, mode: "current" | "new"): string {
123
+ const prefix = mode === "new" ? "This is a fresh session created from an approved pi-plan. " : "";
124
+ return `${prefix}Execute the approved plan in ${relativePlan}. Read the plan file if needed, keep the implementation scoped to the plan, update the plan if reality differs materially, and run the verification described there.`;
125
+ }
126
+
127
+ function hasOpenQuestionWarning(content: string): boolean {
128
+ return /(^|\n)#{1,6}\s+.*open questions?.*\n[\s\S]*\?/i.test(content);
129
+ }
130
+
131
+ async function loadPreferences(): Promise<PlanPreferences | undefined> {
132
+ try {
133
+ const raw = await readFile(PREFERENCES_FILE, "utf8");
134
+ const parsed = JSON.parse(raw) as Partial<PlanPreferences>;
135
+ const savedPlanThinking = parsed.planThinking;
136
+ const savedNormalThinking = parsed.normalThinking;
137
+ if (parsed.version !== 1 || !savedPlanThinking || !savedNormalThinking || !isThinkingLevel(savedPlanThinking) || !isThinkingLevel(savedNormalThinking)) return undefined;
138
+ return { version: 1, planThinking: savedPlanThinking, normalThinking: savedNormalThinking };
139
+ } catch {
140
+ return undefined;
141
+ }
142
+ }
143
+
144
+ async function savePreferences(preferences: PlanPreferences): Promise<void> {
145
+ await mkdir(path.dirname(PREFERENCES_FILE), { recursive: true });
146
+ const temporaryPath = `${PREFERENCES_FILE}.${process.pid}.tmp`;
147
+ await writeFile(temporaryPath, `${JSON.stringify(preferences, null, 2)}\n`, "utf8");
148
+ await rename(temporaryPath, PREFERENCES_FILE);
149
+ }
150
+
93
151
  function isDestructiveBash(command: string): boolean {
94
152
  return DESTRUCTIVE_BASH_PATTERNS.some((pattern) => pattern.test(command));
95
153
  }
@@ -103,6 +161,7 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
103
161
  let lastPlanPath: string | undefined;
104
162
  let lastPlanTitle: string | undefined;
105
163
  let lastPlanStatus: PlanStatus | undefined;
164
+ let applyingStoredThinking = false;
106
165
 
107
166
  function setStatus(ctx: ExtensionContext): void {
108
167
  if (planModeEnabled) {
@@ -116,17 +175,8 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
116
175
  ctx.ui.setStatus(STATUS_KEY, undefined);
117
176
  }
118
177
 
119
- function setPlanWidget(ctx: ExtensionContext): void {
120
- if (!planModeEnabled) {
121
- ctx.ui.setWidget(STATUS_KEY, undefined);
122
- return;
123
- }
124
- ctx.ui.setWidget(STATUS_KEY, [
125
- ctx.ui.theme.fg("warning", "Plan mode: read-only exploration"),
126
- `Plan file target: ${PLAN_DIR}/`,
127
- `Plan thinking: ${planThinking}; normal thinking: ${normalThinking}`,
128
- "Use write_plan when the plan is ready for approval.",
129
- ]);
178
+ function clearPlanWidget(ctx: ExtensionContext): void {
179
+ ctx.ui.setWidget(STATUS_KEY, undefined);
130
180
  }
131
181
 
132
182
  function persistState(): void {
@@ -142,6 +192,10 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
142
192
  } satisfies PlanState);
143
193
  }
144
194
 
195
+ function persistPreferences(): void {
196
+ void savePreferences({ version: 1, planThinking, normalThinking }).catch(() => undefined);
197
+ }
198
+
145
199
  function enablePlanTools(): void {
146
200
  const baseline = toolsBeforePlan ?? pi.getActiveTools();
147
201
  toolsBeforePlan = baseline;
@@ -154,7 +208,24 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
154
208
  }
155
209
 
156
210
  function applyThinking(level: ThinkingLevel): void {
211
+ applyingStoredThinking = true;
157
212
  pi.setThinkingLevel(level);
213
+ setTimeout(() => {
214
+ applyingStoredThinking = false;
215
+ }, 0);
216
+ }
217
+
218
+ function recordActiveThinkingLevel(level: ThinkingLevel, ctx: ExtensionContext): void {
219
+ if (planModeEnabled) {
220
+ if (planThinking === level) return;
221
+ planThinking = level;
222
+ } else {
223
+ if (normalThinking === level) return;
224
+ normalThinking = level;
225
+ }
226
+ setStatus(ctx);
227
+ persistState();
228
+ persistPreferences();
158
229
  }
159
230
 
160
231
  function enterPlanMode(ctx: ExtensionContext): void {
@@ -163,7 +234,7 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
163
234
  enablePlanTools();
164
235
  applyThinking(planThinking);
165
236
  setStatus(ctx);
166
- setPlanWidget(ctx);
237
+ clearPlanWidget(ctx);
167
238
  persistState();
168
239
  ctx.ui.notify(`Plan mode enabled. Thinking=${planThinking}. Plans will be written to ${PLAN_DIR}/`, "info");
169
240
  }
@@ -174,47 +245,62 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
174
245
  restoreTools();
175
246
  if (restoreThinking) applyThinking(normalThinking);
176
247
  setStatus(ctx);
177
- setPlanWidget(ctx);
248
+ clearPlanWidget(ctx);
178
249
  persistState();
179
250
  ctx.ui.notify(`Plan mode disabled. Thinking=${pi.getThinkingLevel()}.`, "info");
180
251
  }
181
252
 
182
- async function chooseThinkingLevels(ctx: ExtensionContext): Promise<void> {
183
- const plan = await ctx.ui.select("Plan mode reasoning level", [...THINKING_LEVELS]);
184
- if (plan && isThinkingLevel(plan)) planThinking = plan;
185
- const normal = await ctx.ui.select("Normal/execution reasoning level", [...THINKING_LEVELS]);
186
- if (normal && isThinkingLevel(normal)) normalThinking = normal;
187
- if (planModeEnabled) applyThinking(planThinking);
188
- else applyThinking(normalThinking);
253
+ async function handlePlanCommand(args: string, ctx: ExtensionContext): Promise<void> {
254
+ if (args.trim().length > 0) {
255
+ ctx.ui.notify("/plan does not take arguments; use /plan or Ctrl+Alt+P to toggle plan mode.", "warning");
256
+ return;
257
+ }
258
+ if (planModeEnabled) leavePlanMode(ctx);
259
+ else enterPlanMode(ctx);
260
+ }
261
+
262
+ function beginCurrentSessionExecution(ctx: ExtensionContext, relativePlan: string): void {
263
+ planModeEnabled = false;
264
+ executionMode = true;
265
+ lastPlanStatus = "approved";
266
+ restoreTools();
267
+ applyThinking(normalThinking);
189
268
  setStatus(ctx);
190
- setPlanWidget(ctx);
269
+ clearPlanWidget(ctx);
191
270
  persistState();
192
- ctx.ui.notify(`pi-plan levels: plan=${planThinking}, normal=${normalThinking}`, "info");
271
+ persistPreferences();
272
+ pi.sendUserMessage(buildExecutionPrompt(relativePlan, "current"), { deliverAs: "followUp" });
193
273
  }
194
274
 
195
- async function handlePlanCommand(args: string, ctx: ExtensionContext): Promise<void> {
196
- const subcommand = args.trim();
197
- if (subcommand === "levels") return chooseThinkingLevels(ctx);
198
- if (subcommand === "on" || subcommand === "start") return enterPlanMode(ctx);
199
- if (subcommand === "off" || subcommand === "stop") return leavePlanMode(ctx);
200
- if (subcommand === "status") {
201
- ctx.ui.notify(
202
- [`Mode: ${planModeEnabled ? "planning" : executionMode ? "executing" : "normal"}`, `Plan thinking: ${planThinking}`, `Normal thinking: ${normalThinking}`, `Last plan: ${lastPlanPath ? relativeToCwd(ctx.cwd, lastPlanPath) : "none"}`].join("\n"),
203
- "info",
204
- );
205
- return;
206
- }
207
- if (subcommand.length > 0 && isThinkingLevel(subcommand)) {
208
- planThinking = subcommand;
209
- if (planModeEnabled) applyThinking(planThinking);
210
- setStatus(ctx);
211
- setPlanWidget(ctx);
212
- persistState();
213
- ctx.ui.notify(`Plan thinking set to ${planThinking}`, "info");
275
+ async function beginNewSessionExecution(ctx: ExtensionCommandContext): Promise<void> {
276
+ if (!lastPlanPath) {
277
+ ctx.ui.notify("No approved plan is available to execute.", "error");
214
278
  return;
215
279
  }
216
- if (planModeEnabled) leavePlanMode(ctx);
217
- else enterPlanMode(ctx);
280
+ await ctx.waitForIdle();
281
+ const planPathToExecute = lastPlanPath;
282
+ const planTitleToExecute = lastPlanTitle;
283
+ const relativePlan = relativeToCwd(ctx.cwd, planPathToExecute);
284
+ const parentSession = ctx.sessionManager.getSessionFile();
285
+ const state: PlanState = {
286
+ enabled: false,
287
+ executing: true,
288
+ planThinking,
289
+ normalThinking,
290
+ lastPlanPath: planPathToExecute,
291
+ lastPlanTitle: planTitleToExecute,
292
+ lastPlanStatus: "approved",
293
+ };
294
+ const result = await ctx.newSession({
295
+ parentSession,
296
+ setup: async (sessionManager) => {
297
+ sessionManager.appendCustomEntry("pi-plan", state);
298
+ },
299
+ withSession: async (replacementCtx) => {
300
+ await replacementCtx.sendUserMessage(buildExecutionPrompt(relativePlan, "new"));
301
+ },
302
+ });
303
+ if (result.cancelled) ctx.ui.notify("New-session execution cancelled.", "info");
218
304
  }
219
305
 
220
306
  pi.registerFlag("plan", {
@@ -228,7 +314,10 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
228
314
  label: "Write Plan",
229
315
  description: `Write or replace the current implementation plan as Markdown under ${PLAN_DIR}/. Use in plan mode when the plan is ready for user review.`,
230
316
  promptSnippet: `Write the implementation plan to ${PLAN_DIR}/ as a Markdown file for user review`,
231
- promptGuidelines: [`Use ${PLAN_TOOL} in plan mode after repository exploration; do not use edit/write for implementation until the user approves the plan.`],
317
+ promptGuidelines: [
318
+ `Use ${PLAN_TOOL} in plan mode after repository exploration; do not use edit/write for implementation until the user approves the plan.`,
319
+ `Do not call ${PLAN_TOOL} while blocking user-answerable questions remain; use ${PLAN_QUESTION_TOOL} first.`,
320
+ ],
232
321
  parameters: Type.Object({
233
322
  title: Type.String({ description: "Short human-readable title for the plan" }),
234
323
  content: Type.String({ description: "Complete Markdown plan content" }),
@@ -246,22 +335,107 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
246
335
  lastPlanTitle = typedParams.title.trim() || "Plan";
247
336
  lastPlanStatus = isPlanStatus(typedParams.status) ? typedParams.status : "draft";
248
337
  persistState();
338
+ const warning = hasOpenQuestionWarning(content)
339
+ ? ` If the plan contains blocking user-answerable open questions, call ${PLAN_QUESTION_TOOL} before requesting approval.`
340
+ : "";
249
341
  return {
250
- content: [{ type: "text", text: `Plan written to ${relativeToCwd(ctx.cwd, destination)}. Ask the user to approve, refine, or keep planning.` }],
342
+ content: [{ type: "text", text: `Plan written to ${relativeToCwd(ctx.cwd, destination)}. If no blocking user-answerable questions remain, ask the user to approve, refine, execute in current session, execute in a new session, or keep planning.${warning}` }],
251
343
  details: { path: destination, title: lastPlanTitle, status: lastPlanStatus },
252
344
  };
253
345
  },
254
346
  });
255
347
 
256
- pi.registerCommand("plan", {
257
- description: "Toggle pi-plan mode; subcommands: on, off, levels, status, or a thinking level",
258
- getArgumentCompletions: (prefix) => {
259
- const values = ["on", "off", "levels", "status", ...THINKING_LEVELS].filter((value) => value.startsWith(prefix));
260
- return values.length ? values.map((value) => ({ label: value, value })) : null;
348
+ pi.registerTool({
349
+ name: PLAN_QUESTION_TOOL,
350
+ label: "Ask Plan Question",
351
+ description: "Ask the user a consequential planning question with selectable options and optional free-form input.",
352
+ promptSnippet: "Ask the user a concise planning question with 2-4 concrete options when an open decision materially affects the plan.",
353
+ promptGuidelines: [
354
+ "Use only after repository research leaves a consequential ambiguity that affects the plan.",
355
+ "Prefer 2-4 concrete options with short labels and useful descriptions.",
356
+ "Do not ask about details that can be discovered from the repository.",
357
+ "If the user already gave a preference, incorporate it instead of asking again.",
358
+ ],
359
+ parameters: Type.Object({
360
+ question: Type.String({ description: "The planning question to ask the user" }),
361
+ options: Type.Array(
362
+ Type.Object({
363
+ label: Type.String({ description: "Short selectable option label" }),
364
+ description: Type.Optional(Type.String({ description: "Optional explanation shown with the option" })),
365
+ }),
366
+ { description: "Concrete options for the user to choose from" },
367
+ ),
368
+ allowOther: Type.Optional(Type.Boolean({ description: "Whether to allow a free-form user answer; defaults to true" })),
369
+ }),
370
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
371
+ const typedParams = params as PlanQuestionParams;
372
+ const options = typedParams.options ?? [];
373
+ if (options.length === 0) {
374
+ return {
375
+ content: [{ type: "text", text: "Error: ask_plan_question requires at least one option." }],
376
+ details: { question: typedParams.question, answer: null },
377
+ };
378
+ }
379
+ if (!ctx.hasUI) {
380
+ return {
381
+ content: [{ type: "text", text: "UI is not available. Ask this planning question directly in chat and wait for the user's answer." }],
382
+ details: { question: typedParams.question, options, answer: null },
383
+ };
384
+ }
385
+
386
+ const allowOther = typedParams.allowOther !== false;
387
+ const labels = options.map((option) =>
388
+ option.description ? `${option.label} — ${option.description}` : option.label,
389
+ );
390
+ const otherLabel = "Other / type my answer";
391
+ const choice = await ctx.ui.select(typedParams.question, allowOther ? [...labels, otherLabel] : labels);
392
+ if (!choice) {
393
+ return {
394
+ content: [{ type: "text", text: "User cancelled the planning question." }],
395
+ details: { question: typedParams.question, options, answer: null, cancelled: true },
396
+ };
397
+ }
398
+
399
+ if (choice === otherLabel) {
400
+ const answer = (await ctx.ui.editor("Your answer", ""))?.trim();
401
+ if (!answer) {
402
+ return {
403
+ content: [{ type: "text", text: "User cancelled the planning question." }],
404
+ details: { question: typedParams.question, options, answer: null, cancelled: true },
405
+ };
406
+ }
407
+ return {
408
+ content: [{ type: "text", text: `User wrote: ${answer}` }],
409
+ details: { question: typedParams.question, options, answer, wasCustom: true },
410
+ };
411
+ }
412
+
413
+ const selectedIndex = labels.indexOf(choice);
414
+ const selected = options[selectedIndex];
415
+ const answer = selected?.label ?? choice;
416
+ return {
417
+ content: [{ type: "text", text: `User selected: ${answer}` }],
418
+ details: { question: typedParams.question, options, answer, selectedIndex: selectedIndex + 1, wasCustom: false },
419
+ };
261
420
  },
421
+ });
422
+
423
+ pi.registerCommand("plan", {
424
+ description: "Toggle pi-plan mode",
262
425
  handler: async (args, ctx) => handlePlanCommand(args, ctx),
263
426
  });
264
427
 
428
+ pi.registerCommand(PLAN_EXECUTE_COMMAND, {
429
+ description: "Internal pi-plan execution bridge",
430
+ handler: async (args, ctx) => {
431
+ if (args.trim() !== "new") {
432
+ ctx.ui.notify(`Usage: /${PLAN_EXECUTE_COMMAND} new`, "warning");
433
+ return;
434
+ }
435
+ await beginNewSessionExecution(ctx);
436
+ },
437
+ });
438
+
265
439
  pi.registerShortcut("ctrl+alt+p", {
266
440
  description: "Toggle pi-plan mode",
267
441
  handler: async (ctx) => {
@@ -271,27 +445,41 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
271
445
  });
272
446
 
273
447
  pi.on("session_start", async (_event, ctx) => {
448
+ const preferences = await loadPreferences();
274
449
  const entries = ctx.sessionManager.getEntries();
275
450
  const saved = entries
276
451
  .filter((entry: { type: string; customType?: string }) => entry.type === "custom" && entry.customType === "pi-plan")
277
452
  .pop() as { data?: PlanState } | undefined;
453
+ if (preferences) {
454
+ planThinking = preferences.planThinking;
455
+ normalThinking = preferences.normalThinking;
456
+ }
278
457
  if (saved?.data) {
279
458
  planModeEnabled = saved.data.enabled ?? planModeEnabled;
280
459
  executionMode = saved.data.executing ?? executionMode;
281
- planThinking = saved.data.planThinking ?? planThinking;
282
- normalThinking = saved.data.normalThinking ?? normalThinking;
460
+ if (!preferences && isThinkingLevel(saved.data.planThinking)) planThinking = saved.data.planThinking;
461
+ if (!preferences && isThinkingLevel(saved.data.normalThinking)) normalThinking = saved.data.normalThinking;
283
462
  toolsBeforePlan = saved.data.toolsBeforePlan ?? toolsBeforePlan;
284
463
  lastPlanPath = saved.data.lastPlanPath ?? lastPlanPath;
285
464
  lastPlanTitle = saved.data.lastPlanTitle ?? lastPlanTitle;
286
465
  lastPlanStatus = saved.data.lastPlanStatus ?? lastPlanStatus;
287
466
  }
467
+ if (!preferences) persistPreferences();
288
468
  if (pi.getFlag("plan") === true) planModeEnabled = true;
289
469
  if (planModeEnabled) {
290
470
  enablePlanTools();
291
471
  applyThinking(planThinking);
472
+ } else if (executionMode) {
473
+ applyThinking(normalThinking);
292
474
  }
293
475
  setStatus(ctx);
294
- setPlanWidget(ctx);
476
+ clearPlanWidget(ctx);
477
+ });
478
+
479
+ pi.on("thinking_level_select", async (event, ctx) => {
480
+ if (applyingStoredThinking) return;
481
+ if (!isThinkingLevel(event.level)) return;
482
+ recordActiveThinkingLevel(event.level, ctx);
295
483
  });
296
484
 
297
485
  pi.on("tool_call", async (event) => {
@@ -311,7 +499,7 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
311
499
  return {
312
500
  message: {
313
501
  customType: "pi-plan-context",
314
- content: `[PI-PLAN MODE ACTIVE]\nYou are in read-only planning mode. Research the codebase and produce a reviewable implementation plan before making changes.\n\nRules:\n- Do not edit source files, configs, lockfiles, or git state.\n- You may read files, search, inspect git state, and run read-only shell commands.\n- Ask concise clarifying questions if requirements are ambiguous.\n- When the plan is ready, call ${PLAN_TOOL} with a complete Markdown plan.\n- The plan file must live in ${PLAN_DIR}/. Current/next plan path: ${relativePlan}\n\nPlan content should include:\n1. Goal and assumptions.\n2. Key findings with durable file/symbol paths.\n3. Proposed implementation steps.\n4. Verification plan.\n5. Risks, open questions, and rejected alternatives if relevant.`,
502
+ content: `[PI-PLAN MODE ACTIVE]\nYou are in read-only planning mode. Research the codebase and produce a reviewable implementation plan before making changes.\n\nRules:\n- Do not edit source files, configs, lockfiles, or git state.\n- You may read files, search, inspect git state, and run read-only shell commands.\n- Ask concise clarifying questions if requirements are ambiguous. Use ${PLAN_QUESTION_TOOL} for consequential open decisions with 2-4 clear options and an Other/user-opinion path.\n- Do not ask about details you can discover from repository evidence. If the user already gave an opinion, incorporate it instead of asking again.\n- Before calling ${PLAN_TOOL}, if any consequential, user-answerable decision remains, call ${PLAN_QUESTION_TOOL} and wait for the answer. Do not place blocking user decisions in the final plan as open questions.\n- When the plan is ready, call ${PLAN_TOOL} with a complete Markdown plan.\n- The plan file must live in ${PLAN_DIR}/. Current/next plan path: ${relativePlan}\n\nPlan content should include:\n1. Goal and assumptions.\n2. Key findings with durable file/symbol paths.\n3. Proposed implementation steps.\n4. Verification plan.\n5. Risks, non-blocking open questions, and rejected alternatives if relevant.`,
315
503
  display: false,
316
504
  },
317
505
  };
@@ -337,22 +525,29 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
337
525
  pi.on("agent_end", async (_event, ctx) => {
338
526
  if (!planModeEnabled || !lastPlanPath || !ctx.hasUI) return;
339
527
  const relativePlan = relativeToCwd(ctx.cwd, lastPlanPath);
528
+ const currentSessionChoice = `Execute in current session (${formatContextUsage(ctx)})`;
529
+ const newSessionChoice = "Execute in new session (fresh context)";
340
530
  const choice = await ctx.ui.select(`Plan written: ${relativePlan}`, [
341
- "Approve and execute",
531
+ currentSessionChoice,
532
+ newSessionChoice,
342
533
  "Approve only (exit plan mode)",
343
534
  "Keep planning",
344
535
  "Refine with feedback",
345
536
  ]);
346
- if (choice === "Approve and execute") {
537
+ if (choice === currentSessionChoice) {
538
+ beginCurrentSessionExecution(ctx, relativePlan);
539
+ } else if (choice === newSessionChoice) {
347
540
  planModeEnabled = false;
348
- executionMode = true;
541
+ executionMode = false;
349
542
  lastPlanStatus = "approved";
350
543
  restoreTools();
351
544
  applyThinking(normalThinking);
352
545
  setStatus(ctx);
353
- setPlanWidget(ctx);
546
+ clearPlanWidget(ctx);
354
547
  persistState();
355
- pi.sendUserMessage(`Execute the approved plan in ${relativePlan}. Keep the implementation scoped to the plan, update the plan if reality differs materially, and run the verification described there.`, { deliverAs: "followUp" });
548
+ persistPreferences();
549
+ ctx.ui.setEditorText(`/${PLAN_EXECUTE_COMMAND} new`);
550
+ ctx.ui.notify("Press Enter to start execution in a new session.", "info");
356
551
  } else if (choice === "Approve only (exit plan mode)") {
357
552
  lastPlanStatus = "approved";
358
553
  leavePlanMode(ctx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-plan",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Pi extension that adds a plan mode with workspace markdown plans and thinking-level presets.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {