@hank-warren/pi-plan-mode 1.2.0 → 1.3.0
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.
- package/CHANGELOG.md +12 -0
- package/README.md +8 -9
- package/package.json +1 -1
- package/src/extension-runtime.ts +9 -3
- package/src/plan-action-menus.ts +1 -0
- package/src/plan-launch-menu.ts +1 -1
- package/src/plan-mode.ts +35 -89
- package/src/presentation.ts +11 -2
- package/src/prompt.ts +1 -1
- package/src/settings-menu.ts +32 -29
- package/src/settings.ts +4 -33
- package/src/state.ts +53 -25
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @hank-warren/pi-plan-mode
|
|
2
2
|
|
|
3
|
+
## 1.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 9dd9833: Remove the plan-mode thinking level. Plan mode no longer mutates session-global state: `pi.setThinkingLevel` writes through to the user's real settings, so the "temporary" level was a durable change that needed three shadow state fields, a `thinking_level_select` listener and restore logic on every exit path to undo. Thinking level and model are session settings now, and whatever you choose while planning carries into implementation. The `thinkingLevel` setting is gone and `/plan settings` drops its row; an existing key in `pi-plan-mode.json` is ignored rather than rejected — preserved verbatim on save, and no longer validated, so even a garbage value keeps the file loading. A one-shot migration restores the level an interrupted pre-1.3.0 session left raised, but only while the live level still matches what plan mode applied.
|
|
8
|
+
|
|
9
|
+
## 1.2.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 3fbf632: Persist the thinking-level capture taken while restoring plan mode. The two restore paths applied the configured level without writing the captured previous one, and Pi's `setThinkingLevel` writes the new level into the user's settings — so a session that ended without `session_shutdown` lost the only record of the original level and the next restore captured the plan level as "previous", permanently raising the user's default. An unchanged restore still writes nothing.
|
|
14
|
+
|
|
3
15
|
## 1.2.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
`@hank-warren/pi-plan-mode` adds a `/plan` mode to Pi for research and design. You gather information, ask questions, and land on a plan — then implement it, either in the same conversation or in a fresh one.
|
|
6
6
|
|
|
7
|
-
**Plan mode is a mode of intent, not a permission system.** It blocks `edit
|
|
7
|
+
**Plan mode is a mode of intent, not a permission system.** It blocks `edit` and `write` while planning and leaves every other tool exactly as you configured it. Command safety stays with your permission extension (for example [`@hank-warren/pi-auto-permissions`](../pi-auto-permissions)), which already reviews Bash. The only tool Plan mode ever removes from the active set is its own `plan_mode_question`, and only when a better questionnaire is installed (see below), so it cannot break other extensions.
|
|
8
8
|
|
|
9
9
|
The plan is written to a **durable file** that survives compaction, survives resume, and can be hand-edited.
|
|
10
10
|
|
|
@@ -45,7 +45,9 @@ pi -e npm:@hank-warren/pi-plan-mode
|
|
|
45
45
|
|
|
46
46
|
`--plan` starts a session directly in Plan mode.
|
|
47
47
|
|
|
48
|
-
While Plan mode is active, ask the agent to design the change. It can read, search, and run commands, but `edit
|
|
48
|
+
While Plan mode is active, ask the agent to design the change. It can read, search, and run commands, but `edit` and `write` are blocked. When the plan is decision-complete, the agent calls `plan_mode_complete` and the plan is written to disk.
|
|
49
|
+
|
|
50
|
+
A completed plan is not final until you act on it: just type feedback to revise — the next planning turn supersedes the proposed plan, and the next `plan_mode_complete` replaces it.
|
|
49
51
|
|
|
50
52
|
From a completed plan you can:
|
|
51
53
|
|
|
@@ -76,26 +78,23 @@ The file is read at session start and **re-read whenever it changes**, so a hand
|
|
|
76
78
|
|
|
77
79
|
```json
|
|
78
80
|
{
|
|
79
|
-
"thinkingLevel": "inherit",
|
|
80
81
|
"defaultPlanExportPath": "PLAN.md"
|
|
81
82
|
}
|
|
82
83
|
```
|
|
83
84
|
|
|
84
|
-
### Plan thinking
|
|
85
|
-
|
|
86
|
-
`thinkingLevel` requests a fixed thinking level while Plan mode is active. Supported values are `inherit`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. The extension restores your previous level on exit unless you changed it manually during Plan mode. Saving applies to the next Plan workflow, never one already running.
|
|
87
|
-
|
|
88
85
|
### Export destination
|
|
89
86
|
|
|
90
87
|
`defaultPlanExportPath` controls only exports that omit a path, and defaults to `PLAN.md`. Relative values resolve against the current working directory at export time. An explicit `/plan export <path>` always wins. Export never overwrites an existing file, directory, or symbolic link.
|
|
91
88
|
|
|
92
|
-
Unknown keys are preserved. Settings removed in 1.0 (`defaultPlanTools`, `bashPolicy`, `safeSubcommands`, `implementationPlanRetention`) are ignored rather than treated as errors, so an existing settings file keeps working.
|
|
89
|
+
Unknown keys are preserved. Settings removed in 1.0 (`defaultPlanTools`, `bashPolicy`, `safeSubcommands`, `implementationPlanRetention`) and in 1.3 (`thinkingLevel`) are ignored rather than treated as errors, so an existing settings file keeps working.
|
|
90
|
+
|
|
91
|
+
Thinking level and model are **session** settings, and Plan mode never changes either one. Set them with Pi's own controls; whatever you choose while planning carries into implementation, because that is what session state does.
|
|
93
92
|
|
|
94
93
|
A settings file that does not parse is reported at session start and the defaults are used. Mid-session it is ignored instead, leaving the last good settings in place: an edit is seen the moment your editor touches the file, so an unreadable one is usually a half-finished save rather than what you meant.
|
|
95
94
|
|
|
96
95
|
## 🔐 What Plan mode does and does not enforce
|
|
97
96
|
|
|
98
|
-
Plan mode blocks exactly
|
|
97
|
+
Plan mode blocks exactly two tools while planning: `edit` and `write`. That is the whole enforcement surface. Checklist tools (a `todo` extension, for example) are deliberately not blocked — a task list is ephemeral planning scratch, and the planning prompt steers the model away from execution-progress tracking.
|
|
99
98
|
|
|
100
99
|
It deliberately does **not** police Bash, subagents, MCP tools, or any other extension tool. Those decisions belong to your permission layer, which can see the whole session and judge each call. Pair Plan mode with a permission extension such as `@hank-warren/pi-auto-permissions` if you want command review during planning.
|
|
101
100
|
|
package/package.json
CHANGED
package/src/extension-runtime.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type {
|
|
2
|
+
import type { LegacyThinkingCapture } from "./state.js";
|
|
3
3
|
|
|
4
4
|
type AgentSettledHandler = (event: unknown, ctx: ExtensionContext) => unknown;
|
|
5
5
|
|
|
@@ -11,8 +11,14 @@ export function onAgentSettled(pi: ExtensionAPI, handler: AgentSettledHandler) {
|
|
|
11
11
|
).on("agent_settled", handler);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Only the one-shot migration that undoes a pre-1.3.0 thinking-level change
|
|
16
|
+
* still calls this. Plan mode never sets the thinking level otherwise.
|
|
17
|
+
*
|
|
18
|
+
* legacy: delete in 1.4.0
|
|
19
|
+
*/
|
|
20
|
+
export function setPlanThinkingLevel(pi: ExtensionAPI, level: LegacyThinkingCapture["previous"]) {
|
|
21
|
+
(pi.setThinkingLevel as unknown as (level: string) => void)(level);
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
export function isStaleExtensionContextError(error: unknown) {
|
package/src/plan-action-menus.ts
CHANGED
|
@@ -10,6 +10,7 @@ interface MenuLifecycle {
|
|
|
10
10
|
const IMPLEMENTATION_CONTEXT_LINES = [
|
|
11
11
|
"Implement here keeps this planning conversation.",
|
|
12
12
|
"Start fresh opens a new session that reads the same plan file.",
|
|
13
|
+
"Or just type feedback to revise — the next completed plan supersedes this one.",
|
|
13
14
|
] as const;
|
|
14
15
|
|
|
15
16
|
interface PlanMenuOptions extends MenuLifecycle {
|
package/src/plan-launch-menu.ts
CHANGED
|
@@ -13,7 +13,7 @@ const HOW_IT_WORKS_LINES = [
|
|
|
13
13
|
"Plan mode is for research and design, not implementation.",
|
|
14
14
|
"Explore the codebase, ask decision questions, then finish with plan_mode_complete.",
|
|
15
15
|
"The plan is written to a durable file that survives compaction and can be hand-edited.",
|
|
16
|
-
"Built-in edit
|
|
16
|
+
"Built-in edit and write are blocked while planning.",
|
|
17
17
|
"All other tools stay exactly as configured; command safety is left to your permission extension.",
|
|
18
18
|
"When the plan is ready: implement here, or start a fresh session that reads the same file.",
|
|
19
19
|
] as const;
|
package/src/plan-mode.ts
CHANGED
|
@@ -46,12 +46,11 @@ import {
|
|
|
46
46
|
} from "./question-tool.js";
|
|
47
47
|
import {
|
|
48
48
|
awaitPlanModeSettingsWrites,
|
|
49
|
-
configuredThinkingLevel,
|
|
50
49
|
type PlanModeSettings,
|
|
51
50
|
planModeSettingsPath,
|
|
52
51
|
readPlanModeSettings,
|
|
53
52
|
} from "./settings.js";
|
|
54
|
-
import { type PlanModeState, restorePlanModeState } from "./state.js";
|
|
53
|
+
import { type PlanModeState, readLegacyThinkingCapture, restorePlanModeState } from "./state.js";
|
|
55
54
|
|
|
56
55
|
const STATE_ENTRY_TYPE = "plan-mode-state";
|
|
57
56
|
/**
|
|
@@ -59,8 +58,12 @@ const STATE_ENTRY_TYPE = "plan-mode-state";
|
|
|
59
58
|
* MCP, and other extension tools — is left to the session's normal permission
|
|
60
59
|
* layer (for example @hank-warren/pi-auto-permissions), so Plan mode never
|
|
61
60
|
* mutates the active tool set and never fights other extensions for it.
|
|
61
|
+
* Checklist tools (a `todo` extension, for example) are deliberately not
|
|
62
|
+
* blocked: a task list is ephemeral planning scratch, and the planning prompt
|
|
63
|
+
* already steers the model away from execution-progress tooling. (`update_plan`
|
|
64
|
+
* was once listed here; it was a pre-1.0 upstream tool that no longer exists.)
|
|
62
65
|
*/
|
|
63
|
-
const BLOCKED_TOOLS = new Set(["edit", "write"
|
|
66
|
+
const BLOCKED_TOOLS = new Set(["edit", "write"]);
|
|
64
67
|
/**
|
|
65
68
|
* One hand-edit or menu save fans out into several filesystem events (temp file
|
|
66
69
|
* created, renamed into place). Collapsing them into one re-read keeps a save
|
|
@@ -124,7 +127,7 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
124
127
|
return interactiveUiPromise;
|
|
125
128
|
};
|
|
126
129
|
let state: PlanModeState = { enabled: false, awaitingAction: false };
|
|
127
|
-
let settings: PlanModeSettings = {
|
|
130
|
+
let settings: PlanModeSettings = {};
|
|
128
131
|
let sessionPlanPath: string | undefined;
|
|
129
132
|
let readyPresentationNonce = 0;
|
|
130
133
|
let pendingReadyNonce: number | undefined;
|
|
@@ -157,8 +160,14 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
157
160
|
exportPlan: (ctx, path, signal, isCurrent) => planExports.export(path, ctx, signal, isCurrent),
|
|
158
161
|
stay: updateUi,
|
|
159
162
|
exitReady: (ctx) => {
|
|
163
|
+
// Same had-plan branching as the /plan exit command: the menu must not
|
|
164
|
+
// claim a plan was discarded when none was ever completed.
|
|
165
|
+
const hadPlan = state.planPath !== undefined;
|
|
160
166
|
void exitPlanMode(ctx).then(() => {
|
|
161
|
-
ctx.ui.notify(
|
|
167
|
+
ctx.ui.notify(
|
|
168
|
+
hadPlan ? "Plan mode disabled. Proposed plan discarded." : "Plan mode disabled.",
|
|
169
|
+
"info",
|
|
170
|
+
);
|
|
162
171
|
});
|
|
163
172
|
},
|
|
164
173
|
});
|
|
@@ -324,7 +333,7 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
324
333
|
const loaded = await readRuntimeSettings();
|
|
325
334
|
if (generation !== menuGeneration || menuController.signal.aborted) return;
|
|
326
335
|
if (loaded.kind === "invalid" && !ctx) return;
|
|
327
|
-
settings = loaded.kind === "loaded" ? loaded.settings : {
|
|
336
|
+
settings = loaded.kind === "loaded" ? loaded.settings : {};
|
|
328
337
|
if (!ctx) return;
|
|
329
338
|
if (loaded.kind === "invalid") {
|
|
330
339
|
ctx.ui.notify(`pi-plan-mode settings ignored: ${loaded.reason}`, "warning");
|
|
@@ -380,9 +389,10 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
380
389
|
menuController = new AbortController();
|
|
381
390
|
pendingReadyNonce = undefined;
|
|
382
391
|
latestCommandContext = undefined;
|
|
383
|
-
settings = {
|
|
392
|
+
settings = {};
|
|
384
393
|
sessionPlanPath = resolveSessionPlanPath(ctx);
|
|
385
394
|
restoreState(ctx);
|
|
395
|
+
repairLegacyThinkingLevel(ctx);
|
|
386
396
|
await loadPlanModeSettings(generation, ctx);
|
|
387
397
|
if (generation !== menuGeneration || menuController.signal.aborted) return;
|
|
388
398
|
startPlanModeSettingsWatch(generation);
|
|
@@ -390,24 +400,10 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
390
400
|
if (persistFlagActivation) {
|
|
391
401
|
state = { ...state, enabled: true, awaitingAction: state.planPath !== undefined };
|
|
392
402
|
}
|
|
393
|
-
if (state.enabled) applyPlanThinkingLevel();
|
|
394
403
|
if (persistFlagActivation) persistState();
|
|
395
404
|
updateUi(ctx);
|
|
396
405
|
});
|
|
397
406
|
|
|
398
|
-
pi.on("thinking_level_select", (event) => {
|
|
399
|
-
if (!state.enabled || !state.appliedThinkingLevel) return;
|
|
400
|
-
if (event.level !== state.appliedThinkingLevel) {
|
|
401
|
-
state = {
|
|
402
|
-
...state,
|
|
403
|
-
manualThinkingLevel: event.level,
|
|
404
|
-
previousThinkingLevel: undefined,
|
|
405
|
-
appliedThinkingLevel: undefined,
|
|
406
|
-
};
|
|
407
|
-
persistState();
|
|
408
|
-
}
|
|
409
|
-
});
|
|
410
|
-
|
|
411
407
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
412
408
|
menuGeneration += 1;
|
|
413
409
|
stopPlanModeSettingsWatch();
|
|
@@ -416,14 +412,12 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
416
412
|
latestCommandContext = undefined;
|
|
417
413
|
refreshStateBeforeFirstAgentStart = false;
|
|
418
414
|
await awaitPlanModeSettingsWrites(dependencies.settingsPath);
|
|
419
|
-
captureManualThinkingLevel();
|
|
420
415
|
persistState();
|
|
421
|
-
if (state.enabled) restoreThinkingLevel();
|
|
422
416
|
clearUi(ctx);
|
|
423
417
|
});
|
|
424
418
|
|
|
425
419
|
/**
|
|
426
|
-
* The complete enforcement surface:
|
|
420
|
+
* The complete enforcement surface: two static built-in names. Plan mode
|
|
427
421
|
* does not classify, inspect, or filter any other tool.
|
|
428
422
|
*/
|
|
429
423
|
pi.on("tool_call", async (event) => {
|
|
@@ -431,10 +425,7 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
431
425
|
if (!BLOCKED_TOOLS.has(event.toolName)) return;
|
|
432
426
|
return {
|
|
433
427
|
block: true,
|
|
434
|
-
reason:
|
|
435
|
-
event.toolName === "update_plan"
|
|
436
|
-
? "Plan mode blocks update_plan because it tracks execution progress rather than conversational planning."
|
|
437
|
-
: `Plan mode blocks '${event.toolName}' because planning must not mutate files. Finish the plan with plan_mode_complete, then implement.`,
|
|
428
|
+
reason: `Plan mode blocks '${event.toolName}' because planning must not mutate files. Finish the plan with plan_mode_complete, then implement.`,
|
|
438
429
|
};
|
|
439
430
|
});
|
|
440
431
|
|
|
@@ -442,7 +433,6 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
442
433
|
if (refreshStateBeforeFirstAgentStart) {
|
|
443
434
|
refreshStateBeforeFirstAgentStart = false;
|
|
444
435
|
restoreState(ctx);
|
|
445
|
-
if (state.enabled) applyPlanThinkingLevel();
|
|
446
436
|
updateUi(ctx);
|
|
447
437
|
}
|
|
448
438
|
if (state.enabled && state.awaitingAction) {
|
|
@@ -487,7 +477,6 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
487
477
|
function enterPlanMode(ctx: ExtensionContext) {
|
|
488
478
|
workflowGeneration += 1;
|
|
489
479
|
state = { ...state, enabled: true, awaitingAction: false };
|
|
490
|
-
applyPlanThinkingLevel();
|
|
491
480
|
persistState();
|
|
492
481
|
updateUi(ctx);
|
|
493
482
|
}
|
|
@@ -500,7 +489,6 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
500
489
|
ctx.ui.notify("Plan mode enabled. I will explore and plan, but not modify files.", "info");
|
|
501
490
|
}
|
|
502
491
|
if (sendPlanModeUserMessage(prompt, ctx)) return;
|
|
503
|
-
if (!wasEnabled) restoreThinkingLevel();
|
|
504
492
|
state = previousState;
|
|
505
493
|
persistState();
|
|
506
494
|
updateUi(ctx);
|
|
@@ -508,7 +496,6 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
508
496
|
|
|
509
497
|
async function exitPlanMode(ctx: ExtensionContext, options: { keepPlanFile?: boolean } = {}) {
|
|
510
498
|
workflowGeneration += 1;
|
|
511
|
-
const wasEnabled = state.enabled;
|
|
512
499
|
const planPath = state.planPath;
|
|
513
500
|
pendingReadyNonce = undefined;
|
|
514
501
|
state = {
|
|
@@ -516,12 +503,7 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
516
503
|
enabled: false,
|
|
517
504
|
planPath: undefined,
|
|
518
505
|
awaitingAction: false,
|
|
519
|
-
manualThinkingLevel: undefined,
|
|
520
506
|
};
|
|
521
|
-
if (wasEnabled) {
|
|
522
|
-
restoreThinkingLevel();
|
|
523
|
-
state = { ...state, manualThinkingLevel: undefined };
|
|
524
|
-
}
|
|
525
507
|
persistState();
|
|
526
508
|
updateUi(ctx);
|
|
527
509
|
if (planPath && !options.keepPlanFile) await deletePlanFile(planPath);
|
|
@@ -593,25 +575,18 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
593
575
|
|
|
594
576
|
workflowGeneration += 1;
|
|
595
577
|
const previousState = state;
|
|
596
|
-
const wasEnabled = state.enabled;
|
|
597
578
|
pendingReadyNonce = undefined;
|
|
598
579
|
state = {
|
|
599
580
|
...state,
|
|
600
581
|
enabled: false,
|
|
601
582
|
awaitingAction: false,
|
|
602
583
|
planPath,
|
|
603
|
-
manualThinkingLevel: undefined,
|
|
604
584
|
};
|
|
605
|
-
if (wasEnabled) {
|
|
606
|
-
restoreThinkingLevel();
|
|
607
|
-
state = { ...state, manualThinkingLevel: undefined };
|
|
608
|
-
}
|
|
609
585
|
persistState();
|
|
610
586
|
updateUi(ctx);
|
|
611
587
|
|
|
612
588
|
if (!sendPlanModeUserMessage(formatImplementationHandoff(planPath), ctx)) {
|
|
613
589
|
state = previousState;
|
|
614
|
-
if (wasEnabled) applyPlanThinkingLevel();
|
|
615
590
|
persistState();
|
|
616
591
|
updateUi(ctx);
|
|
617
592
|
}
|
|
@@ -699,51 +674,22 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
699
674
|
};
|
|
700
675
|
}
|
|
701
676
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
const current = pi.getThinkingLevel();
|
|
719
|
-
if (!state.appliedThinkingLevel) state.previousThinkingLevel = current;
|
|
720
|
-
if (current !== configured) setPlanThinkingLevel(pi, configured);
|
|
721
|
-
state.appliedThinkingLevel = pi.getThinkingLevel();
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
function captureManualThinkingLevel() {
|
|
725
|
-
if (!state.appliedThinkingLevel) return;
|
|
726
|
-
const current = pi.getThinkingLevel();
|
|
727
|
-
if (current === state.appliedThinkingLevel) return;
|
|
728
|
-
state = {
|
|
729
|
-
...state,
|
|
730
|
-
manualThinkingLevel: current,
|
|
731
|
-
previousThinkingLevel: undefined,
|
|
732
|
-
appliedThinkingLevel: undefined,
|
|
733
|
-
};
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
function restoreThinkingLevel() {
|
|
737
|
-
captureManualThinkingLevel();
|
|
738
|
-
const { appliedThinkingLevel, previousThinkingLevel } = state;
|
|
739
|
-
if (
|
|
740
|
-
appliedThinkingLevel &&
|
|
741
|
-
previousThinkingLevel &&
|
|
742
|
-
pi.getThinkingLevel() === appliedThinkingLevel
|
|
743
|
-
) {
|
|
744
|
-
setPlanThinkingLevel(pi, previousThinkingLevel);
|
|
745
|
-
}
|
|
746
|
-
state = { ...state, appliedThinkingLevel: undefined, previousThinkingLevel: undefined };
|
|
677
|
+
/**
|
|
678
|
+
* pi-plan-mode <= 1.2.1 raised the thinking level while planning, and because
|
|
679
|
+
* `pi.setThinkingLevel` writes through to the user's real settings, a session
|
|
680
|
+
* that died before its restore left that change durable. If the newest state
|
|
681
|
+
* entry still carries the capture and the live level still equals what Plan
|
|
682
|
+
* mode applied, put the user's level back — once. Persisting state in the new
|
|
683
|
+
* shape drops the capture, so the next session finds nothing to repair. A
|
|
684
|
+
* user who has already moved the level themselves is left alone.
|
|
685
|
+
*
|
|
686
|
+
* legacy: delete in 1.4.0
|
|
687
|
+
*/
|
|
688
|
+
function repairLegacyThinkingLevel(ctx: ExtensionContext) {
|
|
689
|
+
const legacy = readLegacyThinkingCapture(ctx.sessionManager.getBranch(), STATE_ENTRY_TYPE);
|
|
690
|
+
if (!legacy || pi.getThinkingLevel() !== legacy.applied) return;
|
|
691
|
+
setPlanThinkingLevel(pi, legacy.previous);
|
|
692
|
+
persistState();
|
|
747
693
|
}
|
|
748
694
|
|
|
749
695
|
function resolveSessionPlanPath(ctx: ExtensionContext) {
|
package/src/presentation.ts
CHANGED
|
@@ -10,7 +10,7 @@ export function updatePlanModeUi(ctx: ExtensionContext, state: PlanModeState) {
|
|
|
10
10
|
if (state.enabled && state.awaitingAction) {
|
|
11
11
|
ctx.ui.setWidget(PLAN_WIDGET_KEY, [
|
|
12
12
|
"Proposed plan ready",
|
|
13
|
-
"Use /plan to implement, export,
|
|
13
|
+
"Use /plan to implement, export, or exit — or type feedback to revise.",
|
|
14
14
|
]);
|
|
15
15
|
} else if (state.enabled) {
|
|
16
16
|
ctx.ui.setWidget(PLAN_WIDGET_KEY, [
|
|
@@ -48,7 +48,13 @@ export async function showStoredPlan(
|
|
|
48
48
|
);
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
// enabled without awaitingAction but with a stored plan means revision
|
|
52
|
+
// feedback superseded the completed plan: show it, but never as current.
|
|
53
|
+
const title = state.enabled
|
|
54
|
+
? state.awaitingAction
|
|
55
|
+
? "Proposed Plan"
|
|
56
|
+
: "Superseded Proposed Plan (revision in progress — awaiting a new plan_mode_complete)"
|
|
57
|
+
: "Active Implementation Plan";
|
|
52
58
|
showPlanModePlan(pi, ctx, title, plan);
|
|
53
59
|
}
|
|
54
60
|
|
|
@@ -76,6 +82,9 @@ export function showPlanModePlan(
|
|
|
76
82
|
export function planModeStatusText(state: PlanModeState) {
|
|
77
83
|
if (state.enabled) {
|
|
78
84
|
if (state.awaitingAction) return "Plan mode is active and a proposed plan is ready.";
|
|
85
|
+
if (state.planPath) {
|
|
86
|
+
return "Plan mode is active; revision in progress. The stored plan is superseded until the next plan_mode_complete.";
|
|
87
|
+
}
|
|
79
88
|
return "Plan mode is active. Explore, ask, and finish with plan_mode_complete when decision-ready.";
|
|
80
89
|
}
|
|
81
90
|
if (state.planPath) return "An implementation plan is active.";
|
package/src/prompt.ts
CHANGED
|
@@ -53,7 +53,7 @@ You are in Plan Mode, a collaboration mode for producing a decision-complete imp
|
|
|
53
53
|
|
|
54
54
|
- Stay in Plan Mode until a developer or extension explicitly exits it.
|
|
55
55
|
- Treat requests to implement as requests to plan the implementation; do not edit files or carry out the plan.
|
|
56
|
-
- Do not use
|
|
56
|
+
- Do not use todo/checklist tooling to track execution progress in Plan Mode; Plan Mode is conversational planning, and the plan itself belongs in plan_mode_complete.
|
|
57
57
|
- Do not perform mutating actions: no edit/write tools, no patching, no formatting that rewrites files, no dependency installation, no commits, no migrations.
|
|
58
58
|
- Gather information freely: read files, search, inspect configuration, and run read-only commands.
|
|
59
59
|
|
package/src/settings-menu.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
1
2
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { defineMenu, type RunMenuResult, runMenu } from "@narumitw/pi-tui-kit";
|
|
3
4
|
import { planExportDestination } from "./plan-export.js";
|
|
4
5
|
import {
|
|
5
6
|
configuredPlanExportPath,
|
|
6
|
-
PLAN_MODE_THINKING_LEVELS,
|
|
7
7
|
type PlanModeSettings,
|
|
8
8
|
type PlanModeSettingsLoadResult,
|
|
9
9
|
type PlanModeSettingsPatch,
|
|
@@ -18,6 +18,8 @@ interface SettingsMenuState {
|
|
|
18
18
|
settings: PlanModeSettings;
|
|
19
19
|
notice?: string;
|
|
20
20
|
reason?: string;
|
|
21
|
+
/** The removed `thinkingLevel` key is still in the file. legacy: delete in 1.4.0 */
|
|
22
|
+
hasLegacyThinkingLevel?: boolean;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export interface PlanModeSettingsMenuOptions {
|
|
@@ -34,7 +36,7 @@ export interface PlanModeSettingsMenuOptions {
|
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
type Screen = "settings" | "export";
|
|
37
|
-
type Action = "
|
|
39
|
+
type Action = "open-export" | "set-export";
|
|
38
40
|
|
|
39
41
|
export async function showPlanModeSettings(
|
|
40
42
|
ctx: ExtensionContext,
|
|
@@ -49,15 +51,16 @@ export async function showPlanModeSettings(
|
|
|
49
51
|
if (loaded.kind === "invalid") {
|
|
50
52
|
return {
|
|
51
53
|
kind: "invalid",
|
|
52
|
-
settings: {
|
|
54
|
+
settings: {},
|
|
53
55
|
notice: loaded.notice,
|
|
54
56
|
reason: loaded.reason,
|
|
55
57
|
};
|
|
56
58
|
}
|
|
57
59
|
return {
|
|
58
60
|
kind: "valid",
|
|
59
|
-
settings: loaded.kind === "loaded" ? loaded.settings : {
|
|
61
|
+
settings: loaded.kind === "loaded" ? loaded.settings : {},
|
|
60
62
|
notice: loaded.notice,
|
|
63
|
+
hasLegacyThinkingLevel: await hasLegacyThinkingLevel(settingsPath),
|
|
61
64
|
};
|
|
62
65
|
};
|
|
63
66
|
|
|
@@ -70,16 +73,8 @@ export async function showPlanModeSettings(
|
|
|
70
73
|
: {
|
|
71
74
|
kind: "settings",
|
|
72
75
|
title: "Plan Mode Settings",
|
|
73
|
-
lines: settingsLines(settingsPath, state
|
|
76
|
+
lines: settingsLines(settingsPath, state),
|
|
74
77
|
items: [
|
|
75
|
-
{
|
|
76
|
-
id: "thinkingLevel",
|
|
77
|
-
label: "Plan thinking",
|
|
78
|
-
description: "Set the thinking level when the next Plan workflow starts.",
|
|
79
|
-
currentValue: state.settings.thinkingLevel,
|
|
80
|
-
values: PLAN_MODE_THINKING_LEVELS,
|
|
81
|
-
action: "set-thinking",
|
|
82
|
-
},
|
|
83
78
|
{
|
|
84
79
|
id: "defaultPlanExportPath",
|
|
85
80
|
label: "Export destination",
|
|
@@ -107,19 +102,6 @@ export async function showPlanModeSettings(
|
|
|
107
102
|
},
|
|
108
103
|
},
|
|
109
104
|
actions: {
|
|
110
|
-
"set-thinking": async ({ ctx: actionCtx, value, signal }) => {
|
|
111
|
-
if (
|
|
112
|
-
!PLAN_MODE_THINKING_LEVELS.includes(value as (typeof PLAN_MODE_THINKING_LEVELS)[number])
|
|
113
|
-
) {
|
|
114
|
-
return { kind: "rejected" };
|
|
115
|
-
}
|
|
116
|
-
return savePatch(
|
|
117
|
-
actionCtx,
|
|
118
|
-
{ thinkingLevel: value as PlanModeSettings["thinkingLevel"] },
|
|
119
|
-
signal,
|
|
120
|
-
`Plan mode thinking level: ${value}. Applies to the next Plan workflow.`,
|
|
121
|
-
);
|
|
122
|
-
},
|
|
123
105
|
"open-export": async () => ({ kind: "to", screen: "export" }),
|
|
124
106
|
"set-export": async ({ ctx: actionCtx, value, signal }) => {
|
|
125
107
|
const defaultPlanExportPath = value?.trim() || null;
|
|
@@ -171,14 +153,35 @@ export async function showPlanModeSettings(
|
|
|
171
153
|
}
|
|
172
154
|
}
|
|
173
155
|
|
|
174
|
-
function settingsLines(settingsPath: string,
|
|
156
|
+
function settingsLines(settingsPath: string, state: SettingsMenuState) {
|
|
175
157
|
return [
|
|
176
158
|
`User settings · ${safeTerminalText(settingsPath)}`,
|
|
177
|
-
"
|
|
178
|
-
|
|
159
|
+
"The export destination applies to its next action.",
|
|
160
|
+
// legacy: delete in 1.4.0
|
|
161
|
+
...(state.hasLegacyThinkingLevel
|
|
162
|
+
? [
|
|
163
|
+
"thinkingLevel is no longer used — thinking is a session setting and Plan mode never changes it.",
|
|
164
|
+
]
|
|
165
|
+
: []),
|
|
166
|
+
...(state.notice ? [safeTerminalText(state.notice)] : []),
|
|
179
167
|
];
|
|
180
168
|
}
|
|
181
169
|
|
|
170
|
+
/**
|
|
171
|
+
* The removed key is preserved verbatim on save, so the only way to know it is
|
|
172
|
+
* still there is to look at the file. A read failure simply hides the notice.
|
|
173
|
+
*
|
|
174
|
+
* legacy: delete in 1.4.0
|
|
175
|
+
*/
|
|
176
|
+
async function hasLegacyThinkingLevel(settingsPath: string) {
|
|
177
|
+
try {
|
|
178
|
+
const parsed: unknown = JSON.parse(await readFile(settingsPath, "utf8"));
|
|
179
|
+
return typeof parsed === "object" && parsed !== null && Object.hasOwn(parsed, "thinkingLevel");
|
|
180
|
+
} catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
182
185
|
function invalidScreen(settingsPath: string, state: SettingsMenuState) {
|
|
183
186
|
return {
|
|
184
187
|
kind: "detail" as const,
|
package/src/settings.ts
CHANGED
|
@@ -7,27 +7,13 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
7
7
|
export const PLAN_MODE_SETTINGS_FILE = "pi-plan-mode.json";
|
|
8
8
|
const LEGACY_PLAN_MODE_SETTINGS_FILE = "plan-mode.json";
|
|
9
9
|
const MAX_SETTINGS_BYTES = 64 * 1024;
|
|
10
|
-
export const PLAN_MODE_THINKING_LEVELS = [
|
|
11
|
-
"inherit",
|
|
12
|
-
"off",
|
|
13
|
-
"minimal",
|
|
14
|
-
"low",
|
|
15
|
-
"medium",
|
|
16
|
-
"high",
|
|
17
|
-
"xhigh",
|
|
18
|
-
"max",
|
|
19
|
-
] as const;
|
|
20
10
|
export const DEFAULT_PLAN_EXPORT_PATH = "PLAN.md";
|
|
21
11
|
const MAX_PLAN_EXPORT_PATH_LENGTH = 4096;
|
|
22
12
|
|
|
23
|
-
export type PlanModeThinkingLevel = (typeof PLAN_MODE_THINKING_LEVELS)[number];
|
|
24
|
-
export type PlanModeFixedThinkingLevel = Exclude<PlanModeThinkingLevel, "inherit">;
|
|
25
13
|
export interface PlanModeSettings {
|
|
26
|
-
thinkingLevel: PlanModeThinkingLevel;
|
|
27
14
|
defaultPlanExportPath?: string;
|
|
28
15
|
}
|
|
29
16
|
export interface PlanModeSettingsPatch {
|
|
30
|
-
thinkingLevel?: PlanModeThinkingLevel;
|
|
31
17
|
defaultPlanExportPath?: string | null;
|
|
32
18
|
}
|
|
33
19
|
export interface UpdatePlanModeSettingsOptions {
|
|
@@ -59,21 +45,13 @@ function legacyPlanModeSettingsPath() {
|
|
|
59
45
|
|
|
60
46
|
/**
|
|
61
47
|
* Unknown top-level keys are tolerated and preserved on save. Settings removed
|
|
62
|
-
*
|
|
63
|
-
* implementationPlanRetention) therefore keep an existing file
|
|
64
|
-
* failing it closed on upgrade.
|
|
48
|
+
* over time (defaultPlanTools, bashPolicy, safeSubcommands,
|
|
49
|
+
* implementationPlanRetention, thinkingLevel) therefore keep an existing file
|
|
50
|
+
* valid instead of failing it closed on upgrade.
|
|
65
51
|
*/
|
|
66
52
|
export function normalizePlanModeSettings(value: unknown): PlanModeSettings | undefined {
|
|
67
53
|
if (!isSettingsDocument(value)) return undefined;
|
|
68
|
-
const
|
|
69
|
-
? Reflect.get(value, "thinkingLevel")
|
|
70
|
-
: "inherit";
|
|
71
|
-
if (!PLAN_MODE_THINKING_LEVELS.includes(thinkingLevel as PlanModeThinkingLevel)) {
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
const settings: PlanModeSettings = {
|
|
75
|
-
thinkingLevel: thinkingLevel as PlanModeThinkingLevel,
|
|
76
|
-
};
|
|
54
|
+
const settings: PlanModeSettings = {};
|
|
77
55
|
if (Object.hasOwn(value, "defaultPlanExportPath")) {
|
|
78
56
|
const defaultPlanExportPath = normalizePlanExportPath(
|
|
79
57
|
Reflect.get(value, "defaultPlanExportPath"),
|
|
@@ -143,7 +121,6 @@ export function updatePlanModeSettings(
|
|
|
143
121
|
options.signal?.throwIfAborted();
|
|
144
122
|
const current = await readSettingsDocumentForUpdate(settingsPath, legacySettingsPath);
|
|
145
123
|
const updated: SettingsDocument = { ...current };
|
|
146
|
-
if (patch.thinkingLevel !== undefined) updated.thinkingLevel = patch.thinkingLevel;
|
|
147
124
|
if (patch.defaultPlanExportPath === null) delete updated.defaultPlanExportPath;
|
|
148
125
|
else if (patch.defaultPlanExportPath !== undefined) {
|
|
149
126
|
updated.defaultPlanExportPath = patch.defaultPlanExportPath;
|
|
@@ -310,12 +287,6 @@ function safeReadError(error: unknown) {
|
|
|
310
287
|
return error instanceof Error ? error.message : String(error);
|
|
311
288
|
}
|
|
312
289
|
|
|
313
|
-
export function configuredThinkingLevel(
|
|
314
|
-
settings: PlanModeSettings,
|
|
315
|
-
): PlanModeFixedThinkingLevel | undefined {
|
|
316
|
-
return settings.thinkingLevel === "inherit" ? undefined : settings.thinkingLevel;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
290
|
export function configuredPlanExportPath(settings: PlanModeSettings) {
|
|
320
291
|
return settings.defaultPlanExportPath ?? DEFAULT_PLAN_EXPORT_PATH;
|
|
321
292
|
}
|
package/src/state.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { PLAN_MODE_THINKING_LEVELS, type PlanModeFixedThinkingLevel } from "./settings.js";
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
* The plan lives on disk, so session state carries only a pointer to it
|
|
5
|
-
* the
|
|
2
|
+
* The plan lives on disk, so session state carries only a pointer to it and
|
|
3
|
+
* the ready-for-action flag. Plan mode holds no session-global state of its
|
|
4
|
+
* own: thinking level and model are session settings it never touches.
|
|
6
5
|
*/
|
|
7
6
|
export interface PlanModeState {
|
|
8
7
|
enabled: boolean;
|
|
@@ -10,9 +9,6 @@ export interface PlanModeState {
|
|
|
10
9
|
planPath?: string;
|
|
11
10
|
/** A completed plan is waiting for the user to choose how to proceed. */
|
|
12
11
|
awaitingAction: boolean;
|
|
13
|
-
previousThinkingLevel?: PlanModeFixedThinkingLevel;
|
|
14
|
-
appliedThinkingLevel?: PlanModeFixedThinkingLevel;
|
|
15
|
-
manualThinkingLevel?: PlanModeFixedThinkingLevel;
|
|
16
12
|
}
|
|
17
13
|
|
|
18
14
|
type SessionEntry = {
|
|
@@ -22,15 +18,7 @@ type SessionEntry = {
|
|
|
22
18
|
};
|
|
23
19
|
|
|
24
20
|
export function restorePlanModeState(entries: unknown[], stateEntryType: string): PlanModeState {
|
|
25
|
-
const
|
|
26
|
-
let entry: SessionEntry | undefined;
|
|
27
|
-
for (let index = branch.length - 1; index >= 0; index -= 1) {
|
|
28
|
-
const candidate = branch[index];
|
|
29
|
-
if (candidate?.type === "custom" && candidate.customType === stateEntryType) {
|
|
30
|
-
entry = candidate;
|
|
31
|
-
break;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
21
|
+
const entry = newestStateEntry(entries, stateEntryType);
|
|
34
22
|
if (!isRecord(entry?.data)) return { enabled: false, awaitingAction: false };
|
|
35
23
|
|
|
36
24
|
const enabled = entry.data.enabled === true;
|
|
@@ -39,14 +27,54 @@ export function restorePlanModeState(entries: unknown[], stateEntryType: string)
|
|
|
39
27
|
enabled,
|
|
40
28
|
planPath,
|
|
41
29
|
awaitingAction: enabled && entry.data.awaitingAction === true && planPath !== undefined,
|
|
42
|
-
previousThinkingLevel: enabled
|
|
43
|
-
? fixedThinkingLevel(entry.data.previousThinkingLevel)
|
|
44
|
-
: undefined,
|
|
45
|
-
appliedThinkingLevel: enabled ? fixedThinkingLevel(entry.data.appliedThinkingLevel) : undefined,
|
|
46
|
-
manualThinkingLevel: enabled ? fixedThinkingLevel(entry.data.manualThinkingLevel) : undefined,
|
|
47
30
|
};
|
|
48
31
|
}
|
|
49
32
|
|
|
33
|
+
function newestStateEntry(entries: unknown[], stateEntryType: string): SessionEntry | undefined {
|
|
34
|
+
const branch = entries as SessionEntry[];
|
|
35
|
+
for (let index = branch.length - 1; index >= 0; index -= 1) {
|
|
36
|
+
const candidate = branch[index];
|
|
37
|
+
if (candidate?.type === "custom" && candidate.customType === stateEntryType) return candidate;
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// legacy: delete in 1.4.0
|
|
43
|
+
const LEGACY_THINKING_LEVELS = [
|
|
44
|
+
"off",
|
|
45
|
+
"minimal",
|
|
46
|
+
"low",
|
|
47
|
+
"medium",
|
|
48
|
+
"high",
|
|
49
|
+
"xhigh",
|
|
50
|
+
"max",
|
|
51
|
+
] as const;
|
|
52
|
+
|
|
53
|
+
// legacy: delete in 1.4.0
|
|
54
|
+
export type LegacyThinkingCapture = {
|
|
55
|
+
previous: (typeof LEGACY_THINKING_LEVELS)[number];
|
|
56
|
+
applied: (typeof LEGACY_THINKING_LEVELS)[number];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Reads the thinking-level capture written by pi-plan-mode <= 1.2.1, so a
|
|
61
|
+
* session interrupted while Plan mode held a raised level can have the user's
|
|
62
|
+
* level put back once. Both halves must be present and valid: a partial or
|
|
63
|
+
* absent capture is nothing to repair.
|
|
64
|
+
*
|
|
65
|
+
* legacy: delete in 1.4.0
|
|
66
|
+
*/
|
|
67
|
+
export function readLegacyThinkingCapture(
|
|
68
|
+
entries: unknown[],
|
|
69
|
+
stateEntryType: string,
|
|
70
|
+
): LegacyThinkingCapture | undefined {
|
|
71
|
+
const entry = newestStateEntry(entries, stateEntryType);
|
|
72
|
+
if (!isRecord(entry?.data)) return undefined;
|
|
73
|
+
const previous = legacyThinkingLevel(entry.data.previousThinkingLevel);
|
|
74
|
+
const applied = legacyThinkingLevel(entry.data.appliedThinkingLevel);
|
|
75
|
+
return previous && applied ? { previous, applied } : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
50
78
|
/**
|
|
51
79
|
* Persisted paths are only trusted when they are absolute and free of NUL, so
|
|
52
80
|
* malformed state can never redirect a read or a delete to a relative target.
|
|
@@ -58,11 +86,11 @@ function absolutePath(value: unknown) {
|
|
|
58
86
|
return normalized;
|
|
59
87
|
}
|
|
60
88
|
|
|
61
|
-
|
|
89
|
+
// legacy: delete in 1.4.0
|
|
90
|
+
function legacyThinkingLevel(value: unknown): (typeof LEGACY_THINKING_LEVELS)[number] | undefined {
|
|
62
91
|
return typeof value === "string" &&
|
|
63
|
-
value
|
|
64
|
-
|
|
65
|
-
? (value as PlanModeFixedThinkingLevel)
|
|
92
|
+
LEGACY_THINKING_LEVELS.includes(value as (typeof LEGACY_THINKING_LEVELS)[number])
|
|
93
|
+
? (value as (typeof LEGACY_THINKING_LEVELS)[number])
|
|
66
94
|
: undefined;
|
|
67
95
|
}
|
|
68
96
|
|