@hank-warren/pi-plan-mode 1.2.1 → 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 CHANGED
@@ -1,5 +1,11 @@
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
+
3
9
  ## 1.2.1
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -78,20 +78,17 @@ The file is read at session start and **re-read whenever it changes**, so a hand
78
78
 
79
79
  ```json
80
80
  {
81
- "thinkingLevel": "inherit",
82
81
  "defaultPlanExportPath": "PLAN.md"
83
82
  }
84
83
  ```
85
84
 
86
- ### Plan thinking
87
-
88
- `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.
89
-
90
85
  ### Export destination
91
86
 
92
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.
93
88
 
94
- 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.
95
92
 
96
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.
97
94
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hank-warren/pi-plan-mode",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Plan mode for Pi: research and design with a durable plan file that survives compaction.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -1,5 +1,5 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import type { PlanModeFixedThinkingLevel } from "./settings.js";
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
- export function setPlanThinkingLevel(pi: ExtensionAPI, level: PlanModeFixedThinkingLevel) {
15
- (pi.setThinkingLevel as unknown as (level: PlanModeFixedThinkingLevel) => void)(level);
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-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
  /**
@@ -128,7 +127,7 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
128
127
  return interactiveUiPromise;
129
128
  };
130
129
  let state: PlanModeState = { enabled: false, awaitingAction: false };
131
- let settings: PlanModeSettings = { thinkingLevel: "inherit" };
130
+ let settings: PlanModeSettings = {};
132
131
  let sessionPlanPath: string | undefined;
133
132
  let readyPresentationNonce = 0;
134
133
  let pendingReadyNonce: number | undefined;
@@ -334,7 +333,7 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
334
333
  const loaded = await readRuntimeSettings();
335
334
  if (generation !== menuGeneration || menuController.signal.aborted) return;
336
335
  if (loaded.kind === "invalid" && !ctx) return;
337
- settings = loaded.kind === "loaded" ? loaded.settings : { thinkingLevel: "inherit" };
336
+ settings = loaded.kind === "loaded" ? loaded.settings : {};
338
337
  if (!ctx) return;
339
338
  if (loaded.kind === "invalid") {
340
339
  ctx.ui.notify(`pi-plan-mode settings ignored: ${loaded.reason}`, "warning");
@@ -390,9 +389,10 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
390
389
  menuController = new AbortController();
391
390
  pendingReadyNonce = undefined;
392
391
  latestCommandContext = undefined;
393
- settings = { thinkingLevel: "inherit" };
392
+ settings = {};
394
393
  sessionPlanPath = resolveSessionPlanPath(ctx);
395
394
  restoreState(ctx);
395
+ repairLegacyThinkingLevel(ctx);
396
396
  await loadPlanModeSettings(generation, ctx);
397
397
  if (generation !== menuGeneration || menuController.signal.aborted) return;
398
398
  startPlanModeSettingsWatch(generation);
@@ -400,24 +400,10 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
400
400
  if (persistFlagActivation) {
401
401
  state = { ...state, enabled: true, awaitingAction: state.planPath !== undefined };
402
402
  }
403
- const capturedThinkingLevel = state.enabled ? applyPlanThinkingLevel() : false;
404
- if (persistFlagActivation || capturedThinkingLevel) persistState();
403
+ if (persistFlagActivation) persistState();
405
404
  updateUi(ctx);
406
405
  });
407
406
 
408
- pi.on("thinking_level_select", (event) => {
409
- if (!state.enabled || !state.appliedThinkingLevel) return;
410
- if (event.level !== state.appliedThinkingLevel) {
411
- state = {
412
- ...state,
413
- manualThinkingLevel: event.level,
414
- previousThinkingLevel: undefined,
415
- appliedThinkingLevel: undefined,
416
- };
417
- persistState();
418
- }
419
- });
420
-
421
407
  pi.on("session_shutdown", async (_event, ctx) => {
422
408
  menuGeneration += 1;
423
409
  stopPlanModeSettingsWatch();
@@ -426,9 +412,7 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
426
412
  latestCommandContext = undefined;
427
413
  refreshStateBeforeFirstAgentStart = false;
428
414
  await awaitPlanModeSettingsWrites(dependencies.settingsPath);
429
- captureManualThinkingLevel();
430
415
  persistState();
431
- if (state.enabled) restoreThinkingLevel();
432
416
  clearUi(ctx);
433
417
  });
434
418
 
@@ -449,7 +433,6 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
449
433
  if (refreshStateBeforeFirstAgentStart) {
450
434
  refreshStateBeforeFirstAgentStart = false;
451
435
  restoreState(ctx);
452
- if (state.enabled && applyPlanThinkingLevel()) persistState();
453
436
  updateUi(ctx);
454
437
  }
455
438
  if (state.enabled && state.awaitingAction) {
@@ -494,7 +477,6 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
494
477
  function enterPlanMode(ctx: ExtensionContext) {
495
478
  workflowGeneration += 1;
496
479
  state = { ...state, enabled: true, awaitingAction: false };
497
- applyPlanThinkingLevel();
498
480
  persistState();
499
481
  updateUi(ctx);
500
482
  }
@@ -507,7 +489,6 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
507
489
  ctx.ui.notify("Plan mode enabled. I will explore and plan, but not modify files.", "info");
508
490
  }
509
491
  if (sendPlanModeUserMessage(prompt, ctx)) return;
510
- if (!wasEnabled) restoreThinkingLevel();
511
492
  state = previousState;
512
493
  persistState();
513
494
  updateUi(ctx);
@@ -515,7 +496,6 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
515
496
 
516
497
  async function exitPlanMode(ctx: ExtensionContext, options: { keepPlanFile?: boolean } = {}) {
517
498
  workflowGeneration += 1;
518
- const wasEnabled = state.enabled;
519
499
  const planPath = state.planPath;
520
500
  pendingReadyNonce = undefined;
521
501
  state = {
@@ -523,12 +503,7 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
523
503
  enabled: false,
524
504
  planPath: undefined,
525
505
  awaitingAction: false,
526
- manualThinkingLevel: undefined,
527
506
  };
528
- if (wasEnabled) {
529
- restoreThinkingLevel();
530
- state = { ...state, manualThinkingLevel: undefined };
531
- }
532
507
  persistState();
533
508
  updateUi(ctx);
534
509
  if (planPath && !options.keepPlanFile) await deletePlanFile(planPath);
@@ -600,25 +575,18 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
600
575
 
601
576
  workflowGeneration += 1;
602
577
  const previousState = state;
603
- const wasEnabled = state.enabled;
604
578
  pendingReadyNonce = undefined;
605
579
  state = {
606
580
  ...state,
607
581
  enabled: false,
608
582
  awaitingAction: false,
609
583
  planPath,
610
- manualThinkingLevel: undefined,
611
584
  };
612
- if (wasEnabled) {
613
- restoreThinkingLevel();
614
- state = { ...state, manualThinkingLevel: undefined };
615
- }
616
585
  persistState();
617
586
  updateUi(ctx);
618
587
 
619
588
  if (!sendPlanModeUserMessage(formatImplementationHandoff(planPath), ctx)) {
620
589
  state = previousState;
621
- if (wasEnabled) applyPlanThinkingLevel();
622
590
  persistState();
623
591
  updateUi(ctx);
624
592
  }
@@ -707,66 +675,21 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
707
675
  }
708
676
 
709
677
  /**
710
- * Returns true when the captured thinking levels changed, so the restore
711
- * paths can persist that capture. It is the only record of the user's level:
712
- * `setPlanThinkingLevel` goes through Pi's `setThinkingLevel`, which writes
713
- * the new level into the user's settings, so a capture left only in memory is
714
- * lost if the session ends without `session_shutdown`.
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
715
687
  */
716
- function applyPlanThinkingLevel(): boolean {
717
- if (state.manualThinkingLevel) {
718
- if (pi.getThinkingLevel() !== state.manualThinkingLevel) {
719
- setPlanThinkingLevel(pi, state.manualThinkingLevel);
720
- }
721
- return false;
722
- }
723
- const configured = configuredThinkingLevel(settings);
724
- if (!configured) {
725
- if (state.previousThinkingLevel === undefined && state.appliedThinkingLevel === undefined) {
726
- return false;
727
- }
728
- state = {
729
- ...state,
730
- previousThinkingLevel: undefined,
731
- appliedThinkingLevel: undefined,
732
- };
733
- return true;
734
- }
735
- const current = pi.getThinkingLevel();
736
- const capturedPrevious = state.previousThinkingLevel;
737
- const capturedApplied = state.appliedThinkingLevel;
738
- if (!state.appliedThinkingLevel) state.previousThinkingLevel = current;
739
- if (current !== configured) setPlanThinkingLevel(pi, configured);
740
- state.appliedThinkingLevel = pi.getThinkingLevel();
741
- return (
742
- state.previousThinkingLevel !== capturedPrevious ||
743
- state.appliedThinkingLevel !== capturedApplied
744
- );
745
- }
746
-
747
- function captureManualThinkingLevel() {
748
- if (!state.appliedThinkingLevel) return;
749
- const current = pi.getThinkingLevel();
750
- if (current === state.appliedThinkingLevel) return;
751
- state = {
752
- ...state,
753
- manualThinkingLevel: current,
754
- previousThinkingLevel: undefined,
755
- appliedThinkingLevel: undefined,
756
- };
757
- }
758
-
759
- function restoreThinkingLevel() {
760
- captureManualThinkingLevel();
761
- const { appliedThinkingLevel, previousThinkingLevel } = state;
762
- if (
763
- appliedThinkingLevel &&
764
- previousThinkingLevel &&
765
- pi.getThinkingLevel() === appliedThinkingLevel
766
- ) {
767
- setPlanThinkingLevel(pi, previousThinkingLevel);
768
- }
769
- state = { ...state, appliedThinkingLevel: undefined, previousThinkingLevel: undefined };
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();
770
693
  }
771
694
 
772
695
  function resolveSessionPlanPath(ctx: ExtensionContext) {
@@ -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 = "set-thinking" | "open-export" | "set-export";
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: { thinkingLevel: "inherit" },
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 : { thinkingLevel: "inherit" },
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.notice),
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, notice: string | undefined) {
156
+ function settingsLines(settingsPath: string, state: SettingsMenuState) {
175
157
  return [
176
158
  `User settings · ${safeTerminalText(settingsPath)}`,
177
- "Plan thinking applies to the next workflow; the export destination applies to its next action.",
178
- ...(notice ? [safeTerminalText(notice)] : []),
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
- * in the plan-file rewrite (defaultPlanTools, bashPolicy, safeSubcommands,
63
- * implementationPlanRetention) therefore keep an existing file valid instead of
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 thinkingLevel = Object.hasOwn(value, "thinkingLevel")
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 plus
5
- * the thinking-level bookkeeping needed to restore the user's level on exit.
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 branch = entries as SessionEntry[];
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
- function fixedThinkingLevel(value: unknown): PlanModeFixedThinkingLevel | undefined {
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 !== "inherit" &&
64
- PLAN_MODE_THINKING_LEVELS.includes(value as (typeof PLAN_MODE_THINKING_LEVELS)[number])
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