@astralyn/pi 0.82.5 → 0.82.7

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 (223) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +1 -1
  3. package/dist/cli/project-trust.d.ts.map +1 -1
  4. package/dist/cli/project-trust.js +4 -1
  5. package/dist/cli/project-trust.js.map +1 -1
  6. package/dist/cli/session-picker.d.ts.map +1 -1
  7. package/dist/cli/session-picker.js +11 -13
  8. package/dist/cli/session-picker.js.map +1 -1
  9. package/dist/core/bash-executor.d.ts.map +1 -1
  10. package/dist/core/bash-executor.js +4 -22
  11. package/dist/core/bash-executor.js.map +1 -1
  12. package/dist/core/extensions/index.d.ts +1 -1
  13. package/dist/core/extensions/index.d.ts.map +1 -1
  14. package/dist/core/extensions/index.js.map +1 -1
  15. package/dist/core/extensions/types.d.ts +32 -5
  16. package/dist/core/extensions/types.d.ts.map +1 -1
  17. package/dist/core/extensions/types.js.map +1 -1
  18. package/dist/core/session-manager.d.ts.map +1 -1
  19. package/dist/core/session-manager.js +2 -2
  20. package/dist/core/session-manager.js.map +1 -1
  21. package/dist/core/tools/bash.d.ts +1 -5
  22. package/dist/core/tools/bash.d.ts.map +1 -1
  23. package/dist/core/tools/bash.js +16 -41
  24. package/dist/core/tools/bash.js.map +1 -1
  25. package/dist/core/tools/edit.d.ts.map +1 -1
  26. package/dist/core/tools/edit.js +42 -11
  27. package/dist/core/tools/edit.js.map +1 -1
  28. package/dist/core/tools/find.d.ts.map +1 -1
  29. package/dist/core/tools/find.js +6 -7
  30. package/dist/core/tools/find.js.map +1 -1
  31. package/dist/core/tools/grep.d.ts.map +1 -1
  32. package/dist/core/tools/grep.js +17 -8
  33. package/dist/core/tools/grep.js.map +1 -1
  34. package/dist/core/tools/ls.d.ts.map +1 -1
  35. package/dist/core/tools/ls.js +3 -3
  36. package/dist/core/tools/ls.js.map +1 -1
  37. package/dist/core/tools/output-accumulator.d.ts +3 -13
  38. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  39. package/dist/core/tools/output-accumulator.js +6 -36
  40. package/dist/core/tools/output-accumulator.js.map +1 -1
  41. package/dist/core/tools/read.d.ts.map +1 -1
  42. package/dist/core/tools/read.js +4 -4
  43. package/dist/core/tools/read.js.map +1 -1
  44. package/dist/core/tools/render-utils.d.ts +7 -0
  45. package/dist/core/tools/render-utils.d.ts.map +1 -1
  46. package/dist/core/tools/render-utils.js +11 -0
  47. package/dist/core/tools/render-utils.js.map +1 -1
  48. package/dist/core/tools/write.d.ts.map +1 -1
  49. package/dist/core/tools/write.js +2 -3
  50. package/dist/core/tools/write.js.map +1 -1
  51. package/dist/extensions/deepwiki/render.d.ts.map +1 -1
  52. package/dist/extensions/deepwiki/render.js +34 -22
  53. package/dist/extensions/deepwiki/render.js.map +1 -1
  54. package/dist/extensions/plan/constants.d.ts +43 -6
  55. package/dist/extensions/plan/constants.d.ts.map +1 -1
  56. package/dist/extensions/plan/constants.js +51 -8
  57. package/dist/extensions/plan/constants.js.map +1 -1
  58. package/dist/extensions/plan/index.d.ts +12 -3
  59. package/dist/extensions/plan/index.d.ts.map +1 -1
  60. package/dist/extensions/plan/index.js +173 -35
  61. package/dist/extensions/plan/index.js.map +1 -1
  62. package/dist/extensions/plan/schema.d.ts +5 -0
  63. package/dist/extensions/plan/schema.d.ts.map +1 -1
  64. package/dist/extensions/plan/schema.js.map +1 -1
  65. package/dist/extensions/plan/state.d.ts +11 -2
  66. package/dist/extensions/plan/state.d.ts.map +1 -1
  67. package/dist/extensions/plan/state.js +31 -9
  68. package/dist/extensions/plan/state.js.map +1 -1
  69. package/dist/extensions/plan/storage.d.ts +11 -3
  70. package/dist/extensions/plan/storage.d.ts.map +1 -1
  71. package/dist/extensions/plan/storage.js +45 -20
  72. package/dist/extensions/plan/storage.js.map +1 -1
  73. package/dist/extensions/plan/view.d.ts +44 -0
  74. package/dist/extensions/plan/view.d.ts.map +1 -0
  75. package/dist/extensions/plan/view.js +149 -0
  76. package/dist/extensions/plan/view.js.map +1 -0
  77. package/dist/extensions/question/dialog.d.ts.map +1 -1
  78. package/dist/extensions/question/dialog.js +29 -10
  79. package/dist/extensions/question/dialog.js.map +1 -1
  80. package/dist/extensions/question/index.d.ts.map +1 -1
  81. package/dist/extensions/question/index.js +7 -2
  82. package/dist/extensions/question/index.js.map +1 -1
  83. package/dist/extensions/question/render.d.ts +4 -8
  84. package/dist/extensions/question/render.d.ts.map +1 -1
  85. package/dist/extensions/question/render.js +202 -29
  86. package/dist/extensions/question/render.js.map +1 -1
  87. package/dist/extensions/question/results.d.ts.map +1 -1
  88. package/dist/extensions/question/results.js +1 -0
  89. package/dist/extensions/question/results.js.map +1 -1
  90. package/dist/extensions/question/schema.d.ts.map +1 -1
  91. package/dist/extensions/question/schema.js +2 -2
  92. package/dist/extensions/question/schema.js.map +1 -1
  93. package/dist/extensions/question/types.d.ts +1 -0
  94. package/dist/extensions/question/types.d.ts.map +1 -1
  95. package/dist/extensions/question/types.js +1 -1
  96. package/dist/extensions/question/types.js.map +1 -1
  97. package/dist/extensions/router/stream.d.ts +1 -0
  98. package/dist/extensions/router/stream.d.ts.map +1 -1
  99. package/dist/extensions/router/stream.js +75 -0
  100. package/dist/extensions/router/stream.js.map +1 -1
  101. package/dist/extensions/subagent/activity.d.ts.map +1 -1
  102. package/dist/extensions/subagent/activity.js +25 -4
  103. package/dist/extensions/subagent/activity.js.map +1 -1
  104. package/dist/extensions/subagent/constants.d.ts +1 -0
  105. package/dist/extensions/subagent/constants.d.ts.map +1 -1
  106. package/dist/extensions/subagent/constants.js +1 -0
  107. package/dist/extensions/subagent/constants.js.map +1 -1
  108. package/dist/extensions/subagent/index.d.ts.map +1 -1
  109. package/dist/extensions/subagent/index.js +16 -0
  110. package/dist/extensions/subagent/index.js.map +1 -1
  111. package/dist/extensions/subagent/render.d.ts.map +1 -1
  112. package/dist/extensions/subagent/render.js +352 -152
  113. package/dist/extensions/subagent/render.js.map +1 -1
  114. package/dist/extensions/subagent/resolve.d.ts.map +1 -1
  115. package/dist/extensions/subagent/resolve.js +13 -9
  116. package/dist/extensions/subagent/resolve.js.map +1 -1
  117. package/dist/extensions/subagent/retry.d.ts +17 -0
  118. package/dist/extensions/subagent/retry.d.ts.map +1 -0
  119. package/dist/extensions/subagent/retry.js +41 -0
  120. package/dist/extensions/subagent/retry.js.map +1 -0
  121. package/dist/extensions/subagent/runner.d.ts.map +1 -1
  122. package/dist/extensions/subagent/runner.js +37 -7
  123. package/dist/extensions/subagent/runner.js.map +1 -1
  124. package/dist/extensions/subagent/schema.d.ts.map +1 -1
  125. package/dist/extensions/subagent/schema.js +2 -2
  126. package/dist/extensions/subagent/schema.js.map +1 -1
  127. package/dist/extensions/subagent/sdk-runner.d.ts +6 -0
  128. package/dist/extensions/subagent/sdk-runner.d.ts.map +1 -1
  129. package/dist/extensions/subagent/sdk-runner.js +46 -24
  130. package/dist/extensions/subagent/sdk-runner.js.map +1 -1
  131. package/dist/extensions/subagent/types.d.ts +7 -0
  132. package/dist/extensions/subagent/types.d.ts.map +1 -1
  133. package/dist/extensions/subagent/types.js.map +1 -1
  134. package/dist/extensions/todo/constants.d.ts +17 -6
  135. package/dist/extensions/todo/constants.d.ts.map +1 -1
  136. package/dist/extensions/todo/constants.js +22 -9
  137. package/dist/extensions/todo/constants.js.map +1 -1
  138. package/dist/extensions/todo/index.d.ts +0 -14
  139. package/dist/extensions/todo/index.d.ts.map +1 -1
  140. package/dist/extensions/todo/index.js +13 -1
  141. package/dist/extensions/todo/index.js.map +1 -1
  142. package/dist/extensions/todo/schema.d.ts +53 -2
  143. package/dist/extensions/todo/schema.d.ts.map +1 -1
  144. package/dist/extensions/todo/schema.js +57 -8
  145. package/dist/extensions/todo/schema.js.map +1 -1
  146. package/dist/extensions/todo/state.d.ts +16 -5
  147. package/dist/extensions/todo/state.d.ts.map +1 -1
  148. package/dist/extensions/todo/state.js +874 -130
  149. package/dist/extensions/todo/state.js.map +1 -1
  150. package/dist/extensions/todo/view.d.ts +16 -1
  151. package/dist/extensions/todo/view.d.ts.map +1 -1
  152. package/dist/extensions/todo/view.js +428 -1
  153. package/dist/extensions/todo/view.js.map +1 -1
  154. package/dist/migrations.d.ts.map +1 -1
  155. package/dist/migrations.js +2 -1
  156. package/dist/migrations.js.map +1 -1
  157. package/dist/modes/interactive/components/armin.d.ts.map +1 -1
  158. package/dist/modes/interactive/components/armin.js +5 -0
  159. package/dist/modes/interactive/components/armin.js.map +1 -1
  160. package/dist/modes/interactive/components/bash-execution.d.ts +1 -0
  161. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  162. package/dist/modes/interactive/components/bash-execution.js +4 -2
  163. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  164. package/dist/modes/interactive/components/countdown-timer.d.ts +13 -6
  165. package/dist/modes/interactive/components/countdown-timer.d.ts.map +1 -1
  166. package/dist/modes/interactive/components/countdown-timer.js +54 -17
  167. package/dist/modes/interactive/components/countdown-timer.js.map +1 -1
  168. package/dist/modes/interactive/components/extension-selector.d.ts +6 -1
  169. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  170. package/dist/modes/interactive/components/extension-selector.js +23 -11
  171. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  172. package/dist/modes/interactive/components/keybinding-hints.d.ts +11 -1
  173. package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
  174. package/dist/modes/interactive/components/keybinding-hints.js +56 -0
  175. package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
  176. package/dist/modes/interactive/components/session-selector.d.ts +3 -0
  177. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  178. package/dist/modes/interactive/components/session-selector.js +52 -2
  179. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  180. package/dist/modes/interactive/components/tool-execution.d.ts +10 -0
  181. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  182. package/dist/modes/interactive/components/tool-execution.js +139 -18
  183. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  184. package/dist/modes/interactive/components/tool-group.d.ts +1 -0
  185. package/dist/modes/interactive/components/tool-group.d.ts.map +1 -1
  186. package/dist/modes/interactive/components/tool-group.js +5 -0
  187. package/dist/modes/interactive/components/tool-group.js.map +1 -1
  188. package/dist/modes/interactive/components/tree-selector.d.ts +10 -1
  189. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  190. package/dist/modes/interactive/components/tree-selector.js +55 -2
  191. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  192. package/dist/modes/interactive/interactive-mode.d.ts +5 -0
  193. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  194. package/dist/modes/interactive/interactive-mode.js +79 -19
  195. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  196. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  197. package/dist/modes/interactive/theme/theme.js +3 -2
  198. package/dist/modes/interactive/theme/theme.js.map +1 -1
  199. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  200. package/dist/modes/rpc/rpc-mode.js +8 -1
  201. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  202. package/dist/utils/paths.d.ts +6 -0
  203. package/dist/utils/paths.d.ts.map +1 -1
  204. package/dist/utils/paths.js +8 -0
  205. package/dist/utils/paths.js.map +1 -1
  206. package/docs/bundled/README.md +1 -0
  207. package/docs/bundled/extensions/deepwiki.md +1 -1
  208. package/docs/bundled/extensions/plan.md +13 -10
  209. package/docs/bundled/extensions/question.md +7 -5
  210. package/docs/bundled/extensions/router.md +3 -4
  211. package/docs/bundled/extensions/statusline.md +0 -1
  212. package/docs/bundled/extensions/subagent.md +5 -4
  213. package/docs/bundled/extensions/todo.md +9 -7
  214. package/docs/bundled/tool-presentation.md +17 -16
  215. package/docs/extensions.md +34 -5
  216. package/docs/rpc.md +1 -1
  217. package/docs/usage.md +1 -1
  218. package/npm-shrinkwrap.json +6 -6
  219. package/package.json +1 -1
  220. package/dist/utils/output-decoder.d.ts +0 -52
  221. package/dist/utils/output-decoder.d.ts.map +0 -1
  222. package/dist/utils/output-decoder.js +0 -134
  223. package/dist/utils/output-decoder.js.map +0 -1
@@ -1,7 +1,30 @@
1
- import { buildPlanModePrompt, EXIT_PLAN_PROMPT_GUIDELINES, EXIT_PLAN_PROMPT_SNIPPET, EXIT_PLAN_TOOL_DESCRIPTION, EXIT_PLAN_TOOL_LABEL, EXIT_PLAN_TOOL_NAME, MENU_COMPACT_EXECUTE, MENU_EXECUTE, MENU_KEEP_PLANNING, PLAN_BLOCKED_TOOLS, PLAN_COMMAND_NAME, PLAN_EMBED_MAX_CHARS, PLAN_ENTRY_TYPE, PLAN_FLAG_NAME, PLAN_STATUS_KEY, } from "./constants.js";
1
+ /**
2
+ * plan — Claude-Code-style plan mode: read-only exploration and interview,
3
+ * then a user-approved exit that saves the plan and optionally compacts.
4
+ *
5
+ * Shape:
6
+ * - Entry (/plan or --plan) snapshots the active tool set and swaps to the
7
+ * exploration set (read/grep/find/ls/bash + exit_plan; edit/write removed,
8
+ * bash prompt-constrained to read-only, subagent guarded to the explorer
9
+ * profile); a tool_call guard covers requests already in flight before the
10
+ * tool-set change lands.
11
+ * - The plan text travels as an exit_plan parameter, so the model needs no
12
+ * write access to produce the plan file.
13
+ * - "Compact, then execute" keeps plan mode active until compaction completes
14
+ * (tools are restored in onComplete) and terminates the run via the tool
15
+ * result, so there is no window where the model holds full context plus
16
+ * write tools. The kickoff message embeds the plan itself — continuation
17
+ * never depends on the compaction summary preserving a path.
18
+ * - State is conversation-backed via custom entries and branch replay (same
19
+ * pattern as todo): /reload, /tree, resume, and fork all restore.
20
+ */
21
+ import { basename } from "node:path";
22
+ import { Text } from "@earendil-works/pi-tui";
23
+ import { buildPlanModePrompt, EXIT_PLAN_CANCEL_HINT, EXIT_PLAN_MENU_OPTIONS, EXIT_PLAN_PROMPT_GUIDELINES, EXIT_PLAN_PROMPT_SNIPPET, EXIT_PLAN_TOOL_DESCRIPTION, EXIT_PLAN_TOOL_LABEL, EXIT_PLAN_TOOL_NAME, MENU_COMPACT_EXECUTE, MENU_EXECUTE, MENU_KEEP_PLANNING, PLAN_ALLOWED_SUBAGENT, PLAN_BLOCKED_TOOLS, PLAN_COMMAND_NAME, PLAN_EMBED_MAX_CHARS, PLAN_ENTRY_TYPE, PLAN_EXPLORE_TOOLS, PLAN_FLAG_NAME, PLAN_KICKOFF_MESSAGE_TYPE, PLAN_PANEL_CANCEL_HINT, PLAN_PANEL_EXIT, PLAN_PANEL_EXIT_DESCRIPTION, PLAN_PANEL_MAX_FILES, PLAN_PANEL_TITLE, PLAN_STATUS_KEY, PLAN_SUBAGENT_TOOL_NAME, } from "./constants.js";
2
24
  import { ExitPlanParamsSchema } from "./schema.js";
3
25
  import { clonePlanState, disposePlanSession, EMPTY_PLAN_STATE, getPlanState, replacePlanState, replayPlanFromBranch, setActivePlanSession, } from "./state.js";
4
- import { savePlanFile } from "./storage.js";
26
+ import { listProjectPlanFiles, savePlanFile } from "./storage.js";
27
+ import { formatApprovalSubtitle, formatExitPlanResult, renderExitPlanCall, renderPlanKickoffMessage, shortenPlanPath, } from "./view.js";
5
28
  function buildKickoffMessage(pending) {
6
29
  const truncated = pending.plan.length > PLAN_EMBED_MAX_CHARS;
7
30
  const body = truncated
@@ -13,47 +36,98 @@ ${body}
13
36
 
14
37
  Start now. Track multi-step work with the todo tool, and report back instead of improvising if the plan turns out to need revision.`;
15
38
  }
39
+ /**
40
+ * First subagent profile in the call that is not allowed in plan mode, or
41
+ * undefined when every profile is the read-only explorer. An omitted or null
42
+ * agent resolves to the default "general" profile, so it is disallowed too.
43
+ * Args arrive off the wire, so every field is read defensively.
44
+ */
45
+ export function findDisallowedSubagentProfile(input) {
46
+ const record = input && typeof input === "object" ? input : {};
47
+ const requested = Array.isArray(record.tasks)
48
+ ? record.tasks.map((task) => task && typeof task === "object" ? task.agent : undefined)
49
+ : [record.agent];
50
+ for (const profile of requested) {
51
+ const name = typeof profile === "string" && profile.trim() ? profile.trim() : "general";
52
+ if (name !== PLAN_ALLOWED_SUBAGENT)
53
+ return name;
54
+ }
55
+ return undefined;
56
+ }
16
57
  export default function plan(pi) {
17
58
  let menuOpen = false;
18
59
  let pendingCompact;
60
+ pi.registerMessageRenderer(PLAN_KICKOFF_MESSAGE_TYPE, renderPlanKickoffMessage);
61
+ /**
62
+ * Kickoff as a custom message, not a user message: the LLM still receives
63
+ * the full kickoff text (custom messages convert to user-role context),
64
+ * while the TUI renders the registered collapsed card instead of echoing
65
+ * the whole plan again.
66
+ */
67
+ function sendKickoff(pending) {
68
+ pi.sendMessage({
69
+ customType: PLAN_KICKOFF_MESSAGE_TYPE,
70
+ content: buildKickoffMessage(pending),
71
+ display: true,
72
+ details: { title: pending.title, planPath: pending.planPath },
73
+ }, { triggerTurn: true });
74
+ }
19
75
  pi.registerFlag(PLAN_FLAG_NAME, {
20
76
  description: "Start in plan mode (read-only planning)",
21
77
  type: "boolean",
22
78
  default: false,
23
79
  });
24
- /** Remove blocked tools, activate exit_plan, and return what was actually removed. */
80
+ /**
81
+ * Swap to the exploration tool set and return the pre-entry snapshot.
82
+ * Snapshot + delta rather than a fixed allowlist: edit/write are dropped,
83
+ * the explore tools (grep/find/ls are registered but not active by
84
+ * default) and exit_plan are added, and every other active tool — question,
85
+ * todo, deepwiki, subagent, ... — stays active.
86
+ */
25
87
  function applyPlanTools() {
26
- const active = pi.getActiveTools();
27
- const removed = active.filter((name) => PLAN_BLOCKED_TOOLS.has(name));
28
- pi.setActiveTools([...new Set([...active.filter((name) => !PLAN_BLOCKED_TOOLS.has(name)), EXIT_PLAN_TOOL_NAME])]);
29
- return removed;
88
+ const snapshot = pi.getActiveTools();
89
+ pi.setActiveTools([
90
+ ...new Set([
91
+ ...snapshot.filter((name) => !PLAN_BLOCKED_TOOLS.has(name)),
92
+ ...PLAN_EXPLORE_TOOLS,
93
+ EXIT_PLAN_TOOL_NAME,
94
+ ]),
95
+ ]);
96
+ return snapshot;
30
97
  }
31
- /** Inverse of applyPlanTools: drop exit_plan, re-add exactly what was removed. */
32
- function restoreTools(removedTools) {
33
- const active = pi.getActiveTools().filter((name) => name !== EXIT_PLAN_TOOL_NAME);
34
- pi.setActiveTools([...new Set([...active, ...removedTools])]);
98
+ /**
99
+ * Inverse of applyPlanTools: restore the snapshot verbatim. An empty
100
+ * snapshot (legacy entries) only drops exit_plan — there is nothing
101
+ * recorded to restore to.
102
+ */
103
+ function restoreTools(toolSnapshot) {
104
+ if (toolSnapshot.length > 0) {
105
+ pi.setActiveTools([...new Set(toolSnapshot)].filter((name) => name !== EXIT_PLAN_TOOL_NAME));
106
+ return;
107
+ }
108
+ pi.setActiveTools(pi.getActiveTools().filter((name) => name !== EXIT_PLAN_TOOL_NAME));
35
109
  }
36
110
  function persist() {
37
111
  pi.appendEntry(PLAN_ENTRY_TYPE, getPlanState());
38
112
  }
39
113
  function updateStatus(ctx) {
40
114
  const state = getPlanState();
41
- ctx.ui.setStatus(PLAN_STATUS_KEY, state.planning ? ctx.ui.theme.fg("warning", "plan") : undefined);
115
+ ctx.ui.setStatus(PLAN_STATUS_KEY, state.planning ? ctx.ui.theme.fg("accent", "Plan Mode") : undefined);
42
116
  }
43
117
  function enterPlanMode(ctx) {
44
118
  const state = getPlanState();
45
119
  if (state.planning)
46
120
  return;
47
- const removedTools = applyPlanTools();
48
- replacePlanState({ planning: true, removedTools, awaitingCompact: false, planFiles: state.planFiles });
121
+ const toolSnapshot = applyPlanTools();
122
+ replacePlanState({ planning: true, toolSnapshot, awaitingCompact: false, planFiles: state.planFiles });
49
123
  persist();
50
124
  updateStatus(ctx);
51
- ctx.ui.notify("Plan mode enabled: edit, write, bash, and subagent are disabled.", "info");
125
+ ctx.ui.notify("Plan mode: read-only exploration — edit/write disabled, bash constrained to read-only, subagent limited to explorer.", "info");
52
126
  }
53
127
  function exitPlanMode(ctx, notifyText) {
54
128
  const state = getPlanState();
55
- restoreTools(state.removedTools);
56
- replacePlanState({ ...state, planning: false, awaitingCompact: false, removedTools: [] });
129
+ restoreTools(state.toolSnapshot);
130
+ replacePlanState({ ...state, planning: false, awaitingCompact: false, toolSnapshot: [] });
57
131
  persist();
58
132
  updateStatus(ctx);
59
133
  if (notifyText)
@@ -95,15 +169,19 @@ export default function plan(pi) {
95
169
  pendingCompact = undefined;
96
170
  }
97
171
  if (next.planning) {
98
- // The fresh removal set is authoritative for the eventual restore:
99
- // the live tool set may differ from what the entry recorded.
100
- next.removedTools = applyPlanTools();
172
+ // Re-applying is idempotent, but the returned live snapshot is only
173
+ // authoritative when the live set was NOT already restricted: on a
174
+ // branch switch while planning, re-snapshotting would capture the
175
+ // plan-mode set, so the in-memory snapshot wins there.
176
+ const liveSnapshot = applyPlanTools();
177
+ next.toolSnapshot =
178
+ before.planning && before.toolSnapshot.length > 0 ? [...before.toolSnapshot] : liveSnapshot;
101
179
  }
102
180
  else {
103
181
  if (pi.getActiveTools().includes(EXIT_PLAN_TOOL_NAME)) {
104
- restoreTools(before.planning ? before.removedTools : next.removedTools);
182
+ restoreTools(before.planning ? before.toolSnapshot : next.toolSnapshot);
105
183
  }
106
- next.removedTools = [];
184
+ next.toolSnapshot = [];
107
185
  }
108
186
  replacePlanState(next);
109
187
  if (interruptedCompact) {
@@ -120,6 +198,7 @@ export default function plan(pi) {
120
198
  promptGuidelines: EXIT_PLAN_PROMPT_GUIDELINES,
121
199
  parameters: ExitPlanParamsSchema,
122
200
  executionMode: "sequential",
201
+ rendersOwnProgress: true,
123
202
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
124
203
  setActivePlanSession(ctx.sessionManager.getSessionId());
125
204
  const state = getPlanState();
@@ -150,8 +229,10 @@ export default function plan(pi) {
150
229
  let choice;
151
230
  menuOpen = true;
152
231
  try {
153
- choice = await ctx.ui.select(`Approve plan "${title}"?`, [MENU_EXECUTE, MENU_COMPACT_EXECUTE, MENU_KEEP_PLANNING], {
232
+ choice = await ctx.ui.select(`Approve plan "${title}"?`, EXIT_PLAN_MENU_OPTIONS, {
154
233
  signal,
234
+ subtitle: formatApprovalSubtitle(ctx),
235
+ cancelHint: EXIT_PLAN_CANCEL_HINT,
155
236
  });
156
237
  }
157
238
  finally {
@@ -214,9 +295,51 @@ export default function plan(pi) {
214
295
  details: { decision: "keepPlanning", title },
215
296
  };
216
297
  },
298
+ renderCall(args, theme, context) {
299
+ return renderExitPlanCall(args, theme, context.expanded);
300
+ },
301
+ renderResult(result, _options, theme) {
302
+ return new Text(formatExitPlanResult(result, theme), 0, 0);
303
+ },
217
304
  });
305
+ /**
306
+ * Plans offered in the /plan panel: this branch's plans first (newest
307
+ * first), then the rest of the project's, deduped, bounded. Legacy plans
308
+ * from other sessions' directories only appear via planFiles entries.
309
+ */
310
+ function collectPanelPlanFiles(ctx) {
311
+ const state = getPlanState();
312
+ const seen = new Set();
313
+ const files = [];
314
+ for (const path of [...[...state.planFiles].reverse(), ...listProjectPlanFiles(ctx.cwd)]) {
315
+ const key = path.replace(/\\/g, "/").toLowerCase();
316
+ if (seen.has(key))
317
+ continue;
318
+ seen.add(key);
319
+ files.push(path);
320
+ if (files.length >= PLAN_PANEL_MAX_FILES)
321
+ break;
322
+ }
323
+ return files;
324
+ }
325
+ /** Panel behind /plan while planning: pick a plan file or exit the mode. */
326
+ async function openPlanPanel(ctx) {
327
+ const files = collectPanelPlanFiles(ctx);
328
+ const fileOptions = files.map((path) => ({ label: basename(path), description: shortenPlanPath(path) }));
329
+ const options = [{ label: PLAN_PANEL_EXIT, description: PLAN_PANEL_EXIT_DESCRIPTION }, ...fileOptions];
330
+ const choice = await ctx.ui.select(PLAN_PANEL_TITLE, options, { cancelHint: PLAN_PANEL_CANCEL_HINT });
331
+ if (choice === PLAN_PANEL_EXIT) {
332
+ exitPlanMode(ctx, "Plan mode disabled: full tool access restored.");
333
+ return;
334
+ }
335
+ if (!choice)
336
+ return; // Esc: keep planning, no side effects.
337
+ const selected = files.find((path) => basename(path) === choice);
338
+ if (selected)
339
+ ctx.ui.pasteToEditor(selected);
340
+ }
218
341
  pi.registerCommand(PLAN_COMMAND_NAME, {
219
- description: "Toggle plan mode (read-only planning with user-approved exit)",
342
+ description: "Enter plan mode; while planning, open the plan panel (pick a plan file or exit)",
220
343
  handler: async (_args, ctx) => {
221
344
  setActivePlanSession(ctx.sessionManager.getSessionId());
222
345
  const state = getPlanState();
@@ -228,12 +351,16 @@ export default function plan(pi) {
228
351
  ctx.ui.notify("Waiting for compaction to finish before leaving plan mode.", "warning");
229
352
  return;
230
353
  }
231
- if (state.planning) {
232
- exitPlanMode(ctx, "Plan mode disabled: full tool access restored.");
233
- }
234
- else {
354
+ if (!state.planning) {
235
355
  enterPlanMode(ctx);
356
+ return;
357
+ }
358
+ if (ctx.mode !== "tui") {
359
+ // No panel affordances outside the TUI: keep the toggle semantic.
360
+ exitPlanMode(ctx, "Plan mode disabled: full tool access restored.");
361
+ return;
236
362
  }
363
+ await openPlanPanel(ctx);
237
364
  },
238
365
  });
239
366
  // Defense in depth: the active-tools change lands on the next LLM request,
@@ -242,12 +369,23 @@ export default function plan(pi) {
242
369
  pi.on("tool_call", async (event, ctx) => {
243
370
  setActivePlanSession(ctx.sessionManager.getSessionId());
244
371
  const state = getPlanState();
245
- if (!state.planning || !PLAN_BLOCKED_TOOLS.has(event.toolName))
372
+ if (!state.planning)
246
373
  return;
247
- return {
248
- block: true,
249
- reason: `Plan mode: ${event.toolName} is disabled. Keep exploring with read-only tools, or call ${EXIT_PLAN_TOOL_NAME} to submit the plan for approval.`,
250
- };
374
+ if (PLAN_BLOCKED_TOOLS.has(event.toolName)) {
375
+ return {
376
+ block: true,
377
+ reason: `Plan mode: ${event.toolName} is disabled. Keep exploring with read-only tools, or call ${EXIT_PLAN_TOOL_NAME} to submit the plan for approval.`,
378
+ };
379
+ }
380
+ if (event.toolName === PLAN_SUBAGENT_TOOL_NAME) {
381
+ const disallowed = findDisallowedSubagentProfile(event.input);
382
+ if (disallowed !== undefined) {
383
+ return {
384
+ block: true,
385
+ reason: `Plan mode: subagent profile "${disallowed}" is blocked while planning. Delegate read-only exploration with agent: "${PLAN_ALLOWED_SUBAGENT}" (set it on every task in tasks mode).`,
386
+ };
387
+ }
388
+ }
251
389
  });
252
390
  pi.on("before_agent_start", async (event, ctx) => {
253
391
  setActivePlanSession(ctx.sessionManager.getSessionId());
@@ -268,12 +406,12 @@ export default function plan(pi) {
268
406
  customInstructions: `The user approved an implementation plan saved at ${pending.planPath}. Preserve decisions, constraints, and rejected alternatives from the planning discussion that are not already written in the plan file.`,
269
407
  onComplete: () => {
270
408
  exitPlanMode(ctx, "Context compacted; executing the plan.");
271
- pi.sendUserMessage(buildKickoffMessage(pending));
409
+ sendKickoff(pending);
272
410
  },
273
411
  onError: (error) => {
274
412
  // Never leave the user stuck read-only: degrade to keep-context execution.
275
413
  exitPlanMode(ctx, `Compaction failed (${error.message}); executing with full context instead.`);
276
- pi.sendUserMessage(buildKickoffMessage(pending));
414
+ sendKickoff(pending);
277
415
  },
278
416
  });
279
417
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/extensions/plan/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * plan — Claude-Code-style plan mode: read-only exploration and interview,\n * then a user-approved exit that saves the plan and optionally compacts.\n *\n * Shape:\n * - Entry (/plan or --plan) removes write-capable tools (edit/write/bash/\n * subagent) and activates exit_plan; a tool_call guard covers requests\n * already in flight before the tool-set change lands.\n * - The plan text travels as an exit_plan parameter, so the model needs no\n * write access to produce the plan file.\n * - \"Compact, then execute\" keeps plan mode active until compaction completes\n * (tools are restored in onComplete) and terminates the run via the tool\n * result, so there is no window where the model holds full context plus\n * write tools. The kickoff message embeds the plan itself — continuation\n * never depends on the compaction summary preserving a path.\n * - State is conversation-backed via custom entries and branch replay (same\n * pattern as todo): /reload, /tree, resume, and fork all restore.\n */\nimport type {\n\tAgentToolResult,\n\tExtensionAPI,\n\tExtensionContext,\n\tToolExecutionMode,\n} from \"../../core/extensions/types.ts\";\nimport {\n\tbuildPlanModePrompt,\n\tEXIT_PLAN_PROMPT_GUIDELINES,\n\tEXIT_PLAN_PROMPT_SNIPPET,\n\tEXIT_PLAN_TOOL_DESCRIPTION,\n\tEXIT_PLAN_TOOL_LABEL,\n\tEXIT_PLAN_TOOL_NAME,\n\tMENU_COMPACT_EXECUTE,\n\tMENU_EXECUTE,\n\tMENU_KEEP_PLANNING,\n\tPLAN_BLOCKED_TOOLS,\n\tPLAN_COMMAND_NAME,\n\tPLAN_EMBED_MAX_CHARS,\n\tPLAN_ENTRY_TYPE,\n\tPLAN_FLAG_NAME,\n\tPLAN_STATUS_KEY,\n} from \"./constants.ts\";\nimport { type ExitPlanDetails, type ExitPlanParams, ExitPlanParamsSchema } from \"./schema.ts\";\nimport {\n\tclonePlanState,\n\tdisposePlanSession,\n\tEMPTY_PLAN_STATE,\n\tgetPlanState,\n\treplacePlanState,\n\treplayPlanFromBranch,\n\tsetActivePlanSession,\n} from \"./state.ts\";\nimport { savePlanFile } from \"./storage.ts\";\n\ninterface PendingCompact {\n\ttitle: string;\n\tplan: string;\n\tplanPath: string;\n}\n\nfunction buildKickoffMessage(pending: PendingCompact): string {\n\tconst truncated = pending.plan.length > PLAN_EMBED_MAX_CHARS;\n\tconst body = truncated\n\t\t? `${pending.plan.slice(0, PLAN_EMBED_MAX_CHARS)}\\n\\n[Plan truncated — read the full plan at ${pending.planPath}]`\n\t\t: pending.plan;\n\treturn `Execute the approved plan \"${pending.title}\" (saved at ${pending.planPath}).\n\n${body}\n\nStart now. Track multi-step work with the todo tool, and report back instead of improvising if the plan turns out to need revision.`;\n}\n\nexport default function plan(pi: ExtensionAPI): void {\n\tlet menuOpen = false;\n\tlet pendingCompact: PendingCompact | undefined;\n\n\tpi.registerFlag(PLAN_FLAG_NAME, {\n\t\tdescription: \"Start in plan mode (read-only planning)\",\n\t\ttype: \"boolean\",\n\t\tdefault: false,\n\t});\n\n\t/** Remove blocked tools, activate exit_plan, and return what was actually removed. */\n\tfunction applyPlanTools(): string[] {\n\t\tconst active = pi.getActiveTools();\n\t\tconst removed = active.filter((name) => PLAN_BLOCKED_TOOLS.has(name));\n\t\tpi.setActiveTools([...new Set([...active.filter((name) => !PLAN_BLOCKED_TOOLS.has(name)), EXIT_PLAN_TOOL_NAME])]);\n\t\treturn removed;\n\t}\n\n\t/** Inverse of applyPlanTools: drop exit_plan, re-add exactly what was removed. */\n\tfunction restoreTools(removedTools: string[]): void {\n\t\tconst active = pi.getActiveTools().filter((name) => name !== EXIT_PLAN_TOOL_NAME);\n\t\tpi.setActiveTools([...new Set([...active, ...removedTools])]);\n\t}\n\n\tfunction persist(): void {\n\t\tpi.appendEntry(PLAN_ENTRY_TYPE, getPlanState());\n\t}\n\n\tfunction updateStatus(ctx: ExtensionContext): void {\n\t\tconst state = getPlanState();\n\t\tctx.ui.setStatus(PLAN_STATUS_KEY, state.planning ? ctx.ui.theme.fg(\"warning\", \"plan\") : undefined);\n\t}\n\n\tfunction enterPlanMode(ctx: ExtensionContext): void {\n\t\tconst state = getPlanState();\n\t\tif (state.planning) return;\n\t\tconst removedTools = applyPlanTools();\n\t\treplacePlanState({ planning: true, removedTools, awaitingCompact: false, planFiles: state.planFiles });\n\t\tpersist();\n\t\tupdateStatus(ctx);\n\t\tctx.ui.notify(\"Plan mode enabled: edit, write, bash, and subagent are disabled.\", \"info\");\n\t}\n\n\tfunction exitPlanMode(ctx: ExtensionContext, notifyText?: string): void {\n\t\tconst state = getPlanState();\n\t\trestoreTools(state.removedTools);\n\t\treplacePlanState({ ...state, planning: false, awaitingCompact: false, removedTools: [] });\n\t\tpersist();\n\t\tupdateStatus(ctx);\n\t\tif (notifyText) ctx.ui.notify(notifyText, \"info\");\n\t}\n\n\t/** Save the approved plan, index it in state, and name the session if unnamed. */\n\tfunction saveApprovedPlan(ctx: ExtensionContext, params: ExitPlanParams, title: string): string {\n\t\tconst planPath = savePlanFile({\n\t\t\tsessionId: ctx.sessionManager.getSessionId(),\n\t\t\ttitle,\n\t\t\tplan: params.plan,\n\t\t\tcwd: ctx.cwd,\n\t\t\trevises: params.revises,\n\t\t});\n\t\tconst state = getPlanState();\n\t\treplacePlanState({ ...state, planFiles: [...state.planFiles, planPath] });\n\t\tif (!pi.getSessionName()) pi.setSessionName(title);\n\t\treturn planPath;\n\t}\n\n\t/**\n\t * Rebuild state from the current branch and reconcile the live tool set.\n\t * An interrupted compact-and-execute approval (restart, resume, branch\n\t * switch) degrades to the keep-context exit so the approval is honored.\n\t */\n\tfunction syncFromBranch(ctx: ExtensionContext, options?: { applyFlag?: boolean }): void {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst before = getPlanState();\n\t\tconst replayed = replayPlanFromBranch(ctx);\n\t\tconst next = replayed ?? clonePlanState(EMPTY_PLAN_STATE);\n\t\tif (!replayed && options?.applyFlag && pi.getFlag(PLAN_FLAG_NAME) === true) {\n\t\t\tnext.planning = true;\n\t\t}\n\n\t\tlet interruptedCompact = false;\n\t\tif (next.awaitingCompact) {\n\t\t\tnext.planning = false;\n\t\t\tnext.awaitingCompact = false;\n\t\t\tinterruptedCompact = true;\n\t\t\tpendingCompact = undefined;\n\t\t}\n\n\t\tif (next.planning) {\n\t\t\t// The fresh removal set is authoritative for the eventual restore:\n\t\t\t// the live tool set may differ from what the entry recorded.\n\t\t\tnext.removedTools = applyPlanTools();\n\t\t} else {\n\t\t\tif (pi.getActiveTools().includes(EXIT_PLAN_TOOL_NAME)) {\n\t\t\t\trestoreTools(before.planning ? before.removedTools : next.removedTools);\n\t\t\t}\n\t\t\tnext.removedTools = [];\n\t\t}\n\n\t\treplacePlanState(next);\n\t\tif (interruptedCompact) {\n\t\t\tpersist();\n\t\t\tctx.ui.notify(\"Pending compaction was interrupted; plan mode exited with context kept.\", \"warning\");\n\t\t}\n\t\tupdateStatus(ctx);\n\t}\n\n\tpi.registerTool({\n\t\tname: EXIT_PLAN_TOOL_NAME,\n\t\tlabel: EXIT_PLAN_TOOL_LABEL,\n\t\tdescription: EXIT_PLAN_TOOL_DESCRIPTION,\n\t\tpromptSnippet: EXIT_PLAN_PROMPT_SNIPPET,\n\t\tpromptGuidelines: EXIT_PLAN_PROMPT_GUIDELINES,\n\t\tparameters: ExitPlanParamsSchema,\n\t\texecutionMode: \"sequential\" as ToolExecutionMode,\n\n\t\tasync execute(_toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult<ExitPlanDetails>> {\n\t\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\t\tconst state = getPlanState();\n\t\t\tconst title = params.title.trim() || \"plan\";\n\n\t\t\tif (!state.planning || state.awaitingCompact) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{ type: \"text\", text: \"Plan mode is not awaiting an exit; continue with your current instructions.\" },\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"cancelled\", title },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!ctx.hasUI) {\n\t\t\t\t// json/print: no way to ask, so honor the least surprising default —\n\t\t\t\t// exit with context kept. Documented as a headless-mode semantic.\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\texitPlanMode(ctx);\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan saved to ${planPath}. No interactive UI, so plan mode exited with context kept (authoritative: write tools are restored). Begin executing the plan now.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"execute\", title, planPath },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tlet choice: string | undefined;\n\t\t\tmenuOpen = true;\n\t\t\ttry {\n\t\t\t\tchoice = await ctx.ui.select(\n\t\t\t\t\t`Approve plan \"${title}\"?`,\n\t\t\t\t\t[MENU_EXECUTE, MENU_COMPACT_EXECUTE, MENU_KEEP_PLANNING],\n\t\t\t\t\t{\n\t\t\t\t\t\tsignal,\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t} finally {\n\t\t\t\tmenuOpen = false;\n\t\t\t}\n\n\t\t\tif (signal?.aborted) {\n\t\t\t\t// Throwing lets Pi mark the result as an error, matching the todo convention.\n\t\t\t\tthrow new Error(\"Plan approval was interrupted; plan mode is still active.\");\n\t\t\t}\n\n\t\t\tif (choice === MENU_EXECUTE) {\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\texitPlanMode(ctx, \"Plan approved: executing with full context.\");\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan approved and saved to ${planPath}. Plan mode has ended and write tools are restored (authoritative — overrides the plan-mode system prompt block). Begin executing the plan now; track multi-step work with the todo tool.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"execute\", title, planPath },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (choice === MENU_COMPACT_EXECUTE) {\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\t// Stay in plan mode (tools restricted) until compaction completes;\n\t\t\t\t// terminate ends the run so agent_settled can start the compaction.\n\t\t\t\treplacePlanState({ ...getPlanState(), awaitingCompact: true });\n\t\t\t\tpersist();\n\t\t\t\tpendingCompact = { title, plan: params.plan, planPath };\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan approved and saved to ${planPath}. The context will now be compacted; stop here — execution restarts automatically after compaction.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"compactAndExecute\", title, planPath },\n\t\t\t\t\tterminate: true,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Explicit \"Keep planning\", Esc, or timeout: the only side-effect-free\n\t\t\t// choice, so any dismissal maps here.\n\t\t\tlet feedback: string | undefined;\n\t\t\tif (choice === MENU_KEEP_PLANNING) {\n\t\t\t\tfeedback =\n\t\t\t\t\tctx.mode === \"tui\"\n\t\t\t\t\t\t? await ctx.ui.editor(\"Feedback for the plan (optional):\", \"\")\n\t\t\t\t\t\t: await ctx.ui.input(\"Feedback for the plan (optional):\");\n\t\t\t}\n\t\t\tconst trimmedFeedback = feedback?.trim();\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\ttext: trimmedFeedback\n\t\t\t\t\t\t\t? `The user wants to keep planning. Feedback:\\n${trimmedFeedback}`\n\t\t\t\t\t\t\t: \"The user wants to keep planning. Ask what should change, or keep refining the open points.\",\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdetails: { decision: \"keepPlanning\", title },\n\t\t\t};\n\t\t},\n\t});\n\n\tpi.registerCommand(PLAN_COMMAND_NAME, {\n\t\tdescription: \"Toggle plan mode (read-only planning with user-approved exit)\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\t\tconst state = getPlanState();\n\t\t\tif (menuOpen) {\n\t\t\t\tctx.ui.notify(\"The exit_plan approval menu is open — answer it first.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (state.awaitingCompact) {\n\t\t\t\tctx.ui.notify(\"Waiting for compaction to finish before leaving plan mode.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (state.planning) {\n\t\t\t\texitPlanMode(ctx, \"Plan mode disabled: full tool access restored.\");\n\t\t\t} else {\n\t\t\t\tenterPlanMode(ctx);\n\t\t\t}\n\t\t},\n\t});\n\n\t// Defense in depth: the active-tools change lands on the next LLM request,\n\t// so a request already in flight when plan mode toggled can still carry\n\t// blocked tool calls.\n\tpi.on(\"tool_call\", async (event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst state = getPlanState();\n\t\tif (!state.planning || !PLAN_BLOCKED_TOOLS.has(event.toolName)) return;\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `Plan mode: ${event.toolName} is disabled. Keep exploring with read-only tools, or call ${EXIT_PLAN_TOOL_NAME} to submit the plan for approval.`,\n\t\t};\n\t});\n\n\tpi.on(\"before_agent_start\", async (event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst state = getPlanState();\n\t\tif (!state.planning) return;\n\t\treturn { systemPrompt: `${event.systemPrompt}\\n\\n${buildPlanModePrompt(state.planFiles)}` };\n\t});\n\n\t// agent_settled fires once per fully settled run (agent_end can repeat on\n\t// retries and queued continuations), so the approved compaction starts here.\n\tpi.on(\"agent_settled\", async (_event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tif (!getPlanState().awaitingCompact || !pendingCompact) return;\n\t\tconst pending = pendingCompact;\n\t\tpendingCompact = undefined;\n\t\tctx.compact({\n\t\t\tcustomInstructions: `The user approved an implementation plan saved at ${pending.planPath}. Preserve decisions, constraints, and rejected alternatives from the planning discussion that are not already written in the plan file.`,\n\t\t\tonComplete: () => {\n\t\t\t\texitPlanMode(ctx, \"Context compacted; executing the plan.\");\n\t\t\t\tpi.sendUserMessage(buildKickoffMessage(pending));\n\t\t\t},\n\t\t\tonError: (error) => {\n\t\t\t\t// Never leave the user stuck read-only: degrade to keep-context execution.\n\t\t\t\texitPlanMode(ctx, `Compaction failed (${error.message}); executing with full context instead.`);\n\t\t\t\tpi.sendUserMessage(buildKickoffMessage(pending));\n\t\t\t},\n\t\t});\n\t});\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tsyncFromBranch(ctx, { applyFlag: true });\n\t});\n\n\tpi.on(\"session_tree\", async (_event, ctx) => {\n\t\tsyncFromBranch(ctx);\n\t});\n\n\tpi.on(\"session_shutdown\", async (_event, ctx) => {\n\t\tpendingCompact = undefined;\n\t\tdisposePlanSession(ctx.sessionManager.getSessionId());\n\t});\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/extensions/plan/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * plan — Claude-Code-style plan mode: read-only exploration and interview,\n * then a user-approved exit that saves the plan and optionally compacts.\n *\n * Shape:\n * - Entry (/plan or --plan) snapshots the active tool set and swaps to the\n * exploration set (read/grep/find/ls/bash + exit_plan; edit/write removed,\n * bash prompt-constrained to read-only, subagent guarded to the explorer\n * profile); a tool_call guard covers requests already in flight before the\n * tool-set change lands.\n * - The plan text travels as an exit_plan parameter, so the model needs no\n * write access to produce the plan file.\n * - \"Compact, then execute\" keeps plan mode active until compaction completes\n * (tools are restored in onComplete) and terminates the run via the tool\n * result, so there is no window where the model holds full context plus\n * write tools. The kickoff message embeds the plan itself — continuation\n * never depends on the compaction summary preserving a path.\n * - State is conversation-backed via custom entries and branch replay (same\n * pattern as todo): /reload, /tree, resume, and fork all restore.\n */\n\nimport { basename } from \"node:path\";\nimport { Text } from \"@earendil-works/pi-tui\";\nimport type {\n\tAgentToolResult,\n\tExtensionAPI,\n\tExtensionContext,\n\tToolExecutionMode,\n} from \"../../core/extensions/types.ts\";\nimport {\n\tbuildPlanModePrompt,\n\tEXIT_PLAN_CANCEL_HINT,\n\tEXIT_PLAN_MENU_OPTIONS,\n\tEXIT_PLAN_PROMPT_GUIDELINES,\n\tEXIT_PLAN_PROMPT_SNIPPET,\n\tEXIT_PLAN_TOOL_DESCRIPTION,\n\tEXIT_PLAN_TOOL_LABEL,\n\tEXIT_PLAN_TOOL_NAME,\n\tMENU_COMPACT_EXECUTE,\n\tMENU_EXECUTE,\n\tMENU_KEEP_PLANNING,\n\tPLAN_ALLOWED_SUBAGENT,\n\tPLAN_BLOCKED_TOOLS,\n\tPLAN_COMMAND_NAME,\n\tPLAN_EMBED_MAX_CHARS,\n\tPLAN_ENTRY_TYPE,\n\tPLAN_EXPLORE_TOOLS,\n\tPLAN_FLAG_NAME,\n\tPLAN_KICKOFF_MESSAGE_TYPE,\n\tPLAN_PANEL_CANCEL_HINT,\n\tPLAN_PANEL_EXIT,\n\tPLAN_PANEL_EXIT_DESCRIPTION,\n\tPLAN_PANEL_MAX_FILES,\n\tPLAN_PANEL_TITLE,\n\tPLAN_STATUS_KEY,\n\tPLAN_SUBAGENT_TOOL_NAME,\n} from \"./constants.ts\";\nimport { type ExitPlanDetails, type ExitPlanParams, ExitPlanParamsSchema, type PlanKickoffDetails } from \"./schema.ts\";\nimport {\n\tclonePlanState,\n\tdisposePlanSession,\n\tEMPTY_PLAN_STATE,\n\tgetPlanState,\n\treplacePlanState,\n\treplayPlanFromBranch,\n\tsetActivePlanSession,\n} from \"./state.ts\";\nimport { listProjectPlanFiles, savePlanFile } from \"./storage.ts\";\nimport {\n\tformatApprovalSubtitle,\n\tformatExitPlanResult,\n\trenderExitPlanCall,\n\trenderPlanKickoffMessage,\n\tshortenPlanPath,\n} from \"./view.ts\";\n\ninterface PendingCompact {\n\ttitle: string;\n\tplan: string;\n\tplanPath: string;\n}\n\nfunction buildKickoffMessage(pending: PendingCompact): string {\n\tconst truncated = pending.plan.length > PLAN_EMBED_MAX_CHARS;\n\tconst body = truncated\n\t\t? `${pending.plan.slice(0, PLAN_EMBED_MAX_CHARS)}\\n\\n[Plan truncated — read the full plan at ${pending.planPath}]`\n\t\t: pending.plan;\n\treturn `Execute the approved plan \"${pending.title}\" (saved at ${pending.planPath}).\n\n${body}\n\nStart now. Track multi-step work with the todo tool, and report back instead of improvising if the plan turns out to need revision.`;\n}\n\n/**\n * First subagent profile in the call that is not allowed in plan mode, or\n * undefined when every profile is the read-only explorer. An omitted or null\n * agent resolves to the default \"general\" profile, so it is disallowed too.\n * Args arrive off the wire, so every field is read defensively.\n */\nexport function findDisallowedSubagentProfile(input: unknown): string | undefined {\n\tconst record = input && typeof input === \"object\" ? (input as Record<string, unknown>) : {};\n\tconst requested: unknown[] = Array.isArray(record.tasks)\n\t\t? record.tasks.map((task) =>\n\t\t\t\ttask && typeof task === \"object\" ? (task as Record<string, unknown>).agent : undefined,\n\t\t\t)\n\t\t: [record.agent];\n\tfor (const profile of requested) {\n\t\tconst name = typeof profile === \"string\" && profile.trim() ? profile.trim() : \"general\";\n\t\tif (name !== PLAN_ALLOWED_SUBAGENT) return name;\n\t}\n\treturn undefined;\n}\n\nexport default function plan(pi: ExtensionAPI): void {\n\tlet menuOpen = false;\n\tlet pendingCompact: PendingCompact | undefined;\n\n\tpi.registerMessageRenderer<PlanKickoffDetails>(PLAN_KICKOFF_MESSAGE_TYPE, renderPlanKickoffMessage);\n\n\t/**\n\t * Kickoff as a custom message, not a user message: the LLM still receives\n\t * the full kickoff text (custom messages convert to user-role context),\n\t * while the TUI renders the registered collapsed card instead of echoing\n\t * the whole plan again.\n\t */\n\tfunction sendKickoff(pending: PendingCompact): void {\n\t\tpi.sendMessage(\n\t\t\t{\n\t\t\t\tcustomType: PLAN_KICKOFF_MESSAGE_TYPE,\n\t\t\t\tcontent: buildKickoffMessage(pending),\n\t\t\t\tdisplay: true,\n\t\t\t\tdetails: { title: pending.title, planPath: pending.planPath },\n\t\t\t},\n\t\t\t{ triggerTurn: true },\n\t\t);\n\t}\n\n\tpi.registerFlag(PLAN_FLAG_NAME, {\n\t\tdescription: \"Start in plan mode (read-only planning)\",\n\t\ttype: \"boolean\",\n\t\tdefault: false,\n\t});\n\n\t/**\n\t * Swap to the exploration tool set and return the pre-entry snapshot.\n\t * Snapshot + delta rather than a fixed allowlist: edit/write are dropped,\n\t * the explore tools (grep/find/ls are registered but not active by\n\t * default) and exit_plan are added, and every other active tool — question,\n\t * todo, deepwiki, subagent, ... — stays active.\n\t */\n\tfunction applyPlanTools(): string[] {\n\t\tconst snapshot = pi.getActiveTools();\n\t\tpi.setActiveTools([\n\t\t\t...new Set([\n\t\t\t\t...snapshot.filter((name) => !PLAN_BLOCKED_TOOLS.has(name)),\n\t\t\t\t...PLAN_EXPLORE_TOOLS,\n\t\t\t\tEXIT_PLAN_TOOL_NAME,\n\t\t\t]),\n\t\t]);\n\t\treturn snapshot;\n\t}\n\n\t/**\n\t * Inverse of applyPlanTools: restore the snapshot verbatim. An empty\n\t * snapshot (legacy entries) only drops exit_plan — there is nothing\n\t * recorded to restore to.\n\t */\n\tfunction restoreTools(toolSnapshot: string[]): void {\n\t\tif (toolSnapshot.length > 0) {\n\t\t\tpi.setActiveTools([...new Set(toolSnapshot)].filter((name) => name !== EXIT_PLAN_TOOL_NAME));\n\t\t\treturn;\n\t\t}\n\t\tpi.setActiveTools(pi.getActiveTools().filter((name) => name !== EXIT_PLAN_TOOL_NAME));\n\t}\n\n\tfunction persist(): void {\n\t\tpi.appendEntry(PLAN_ENTRY_TYPE, getPlanState());\n\t}\n\n\tfunction updateStatus(ctx: ExtensionContext): void {\n\t\tconst state = getPlanState();\n\t\tctx.ui.setStatus(PLAN_STATUS_KEY, state.planning ? ctx.ui.theme.fg(\"accent\", \"Plan Mode\") : undefined);\n\t}\n\n\tfunction enterPlanMode(ctx: ExtensionContext): void {\n\t\tconst state = getPlanState();\n\t\tif (state.planning) return;\n\t\tconst toolSnapshot = applyPlanTools();\n\t\treplacePlanState({ planning: true, toolSnapshot, awaitingCompact: false, planFiles: state.planFiles });\n\t\tpersist();\n\t\tupdateStatus(ctx);\n\t\tctx.ui.notify(\n\t\t\t\"Plan mode: read-only exploration — edit/write disabled, bash constrained to read-only, subagent limited to explorer.\",\n\t\t\t\"info\",\n\t\t);\n\t}\n\n\tfunction exitPlanMode(ctx: ExtensionContext, notifyText?: string): void {\n\t\tconst state = getPlanState();\n\t\trestoreTools(state.toolSnapshot);\n\t\treplacePlanState({ ...state, planning: false, awaitingCompact: false, toolSnapshot: [] });\n\t\tpersist();\n\t\tupdateStatus(ctx);\n\t\tif (notifyText) ctx.ui.notify(notifyText, \"info\");\n\t}\n\n\t/** Save the approved plan, index it in state, and name the session if unnamed. */\n\tfunction saveApprovedPlan(ctx: ExtensionContext, params: ExitPlanParams, title: string): string {\n\t\tconst planPath = savePlanFile({\n\t\t\tsessionId: ctx.sessionManager.getSessionId(),\n\t\t\ttitle,\n\t\t\tplan: params.plan,\n\t\t\tcwd: ctx.cwd,\n\t\t\trevises: params.revises,\n\t\t});\n\t\tconst state = getPlanState();\n\t\treplacePlanState({ ...state, planFiles: [...state.planFiles, planPath] });\n\t\tif (!pi.getSessionName()) pi.setSessionName(title);\n\t\treturn planPath;\n\t}\n\n\t/**\n\t * Rebuild state from the current branch and reconcile the live tool set.\n\t * An interrupted compact-and-execute approval (restart, resume, branch\n\t * switch) degrades to the keep-context exit so the approval is honored.\n\t */\n\tfunction syncFromBranch(ctx: ExtensionContext, options?: { applyFlag?: boolean }): void {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst before = getPlanState();\n\t\tconst replayed = replayPlanFromBranch(ctx);\n\t\tconst next = replayed ?? clonePlanState(EMPTY_PLAN_STATE);\n\t\tif (!replayed && options?.applyFlag && pi.getFlag(PLAN_FLAG_NAME) === true) {\n\t\t\tnext.planning = true;\n\t\t}\n\n\t\tlet interruptedCompact = false;\n\t\tif (next.awaitingCompact) {\n\t\t\tnext.planning = false;\n\t\t\tnext.awaitingCompact = false;\n\t\t\tinterruptedCompact = true;\n\t\t\tpendingCompact = undefined;\n\t\t}\n\n\t\tif (next.planning) {\n\t\t\t// Re-applying is idempotent, but the returned live snapshot is only\n\t\t\t// authoritative when the live set was NOT already restricted: on a\n\t\t\t// branch switch while planning, re-snapshotting would capture the\n\t\t\t// plan-mode set, so the in-memory snapshot wins there.\n\t\t\tconst liveSnapshot = applyPlanTools();\n\t\t\tnext.toolSnapshot =\n\t\t\t\tbefore.planning && before.toolSnapshot.length > 0 ? [...before.toolSnapshot] : liveSnapshot;\n\t\t} else {\n\t\t\tif (pi.getActiveTools().includes(EXIT_PLAN_TOOL_NAME)) {\n\t\t\t\trestoreTools(before.planning ? before.toolSnapshot : next.toolSnapshot);\n\t\t\t}\n\t\t\tnext.toolSnapshot = [];\n\t\t}\n\n\t\treplacePlanState(next);\n\t\tif (interruptedCompact) {\n\t\t\tpersist();\n\t\t\tctx.ui.notify(\"Pending compaction was interrupted; plan mode exited with context kept.\", \"warning\");\n\t\t}\n\t\tupdateStatus(ctx);\n\t}\n\n\tpi.registerTool<typeof ExitPlanParamsSchema, ExitPlanDetails>({\n\t\tname: EXIT_PLAN_TOOL_NAME,\n\t\tlabel: EXIT_PLAN_TOOL_LABEL,\n\t\tdescription: EXIT_PLAN_TOOL_DESCRIPTION,\n\t\tpromptSnippet: EXIT_PLAN_PROMPT_SNIPPET,\n\t\tpromptGuidelines: EXIT_PLAN_PROMPT_GUIDELINES,\n\t\tparameters: ExitPlanParamsSchema,\n\t\texecutionMode: \"sequential\" as ToolExecutionMode,\n\t\trendersOwnProgress: true,\n\n\t\tasync execute(_toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult<ExitPlanDetails>> {\n\t\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\t\tconst state = getPlanState();\n\t\t\tconst title = params.title.trim() || \"plan\";\n\n\t\t\tif (!state.planning || state.awaitingCompact) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{ type: \"text\", text: \"Plan mode is not awaiting an exit; continue with your current instructions.\" },\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"cancelled\", title },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!ctx.hasUI) {\n\t\t\t\t// json/print: no way to ask, so honor the least surprising default —\n\t\t\t\t// exit with context kept. Documented as a headless-mode semantic.\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\texitPlanMode(ctx);\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan saved to ${planPath}. No interactive UI, so plan mode exited with context kept (authoritative: write tools are restored). Begin executing the plan now.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"execute\", title, planPath },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tlet choice: string | undefined;\n\t\t\tmenuOpen = true;\n\t\t\ttry {\n\t\t\t\tchoice = await ctx.ui.select(`Approve plan \"${title}\"?`, EXIT_PLAN_MENU_OPTIONS, {\n\t\t\t\t\tsignal,\n\t\t\t\t\tsubtitle: formatApprovalSubtitle(ctx),\n\t\t\t\t\tcancelHint: EXIT_PLAN_CANCEL_HINT,\n\t\t\t\t});\n\t\t\t} finally {\n\t\t\t\tmenuOpen = false;\n\t\t\t}\n\n\t\t\tif (signal?.aborted) {\n\t\t\t\t// Throwing lets Pi mark the result as an error, matching the todo convention.\n\t\t\t\tthrow new Error(\"Plan approval was interrupted; plan mode is still active.\");\n\t\t\t}\n\n\t\t\tif (choice === MENU_EXECUTE) {\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\texitPlanMode(ctx, \"Plan approved: executing with full context.\");\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan approved and saved to ${planPath}. Plan mode has ended and write tools are restored (authoritative — overrides the plan-mode system prompt block). Begin executing the plan now; track multi-step work with the todo tool.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"execute\", title, planPath },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (choice === MENU_COMPACT_EXECUTE) {\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\t// Stay in plan mode (tools restricted) until compaction completes;\n\t\t\t\t// terminate ends the run so agent_settled can start the compaction.\n\t\t\t\treplacePlanState({ ...getPlanState(), awaitingCompact: true });\n\t\t\t\tpersist();\n\t\t\t\tpendingCompact = { title, plan: params.plan, planPath };\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan approved and saved to ${planPath}. The context will now be compacted; stop here — execution restarts automatically after compaction.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"compactAndExecute\", title, planPath },\n\t\t\t\t\tterminate: true,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Explicit \"Keep planning\", Esc, or timeout: the only side-effect-free\n\t\t\t// choice, so any dismissal maps here.\n\t\t\tlet feedback: string | undefined;\n\t\t\tif (choice === MENU_KEEP_PLANNING) {\n\t\t\t\tfeedback =\n\t\t\t\t\tctx.mode === \"tui\"\n\t\t\t\t\t\t? await ctx.ui.editor(\"Feedback for the plan (optional):\", \"\")\n\t\t\t\t\t\t: await ctx.ui.input(\"Feedback for the plan (optional):\");\n\t\t\t}\n\t\t\tconst trimmedFeedback = feedback?.trim();\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\ttext: trimmedFeedback\n\t\t\t\t\t\t\t? `The user wants to keep planning. Feedback:\\n${trimmedFeedback}`\n\t\t\t\t\t\t\t: \"The user wants to keep planning. Ask what should change, or keep refining the open points.\",\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdetails: { decision: \"keepPlanning\", title },\n\t\t\t};\n\t\t},\n\n\t\trenderCall(args, theme, context) {\n\t\t\treturn renderExitPlanCall(args, theme, context.expanded);\n\t\t},\n\n\t\trenderResult(result, _options, theme) {\n\t\t\treturn new Text(formatExitPlanResult(result, theme), 0, 0);\n\t\t},\n\t});\n\n\t/**\n\t * Plans offered in the /plan panel: this branch's plans first (newest\n\t * first), then the rest of the project's, deduped, bounded. Legacy plans\n\t * from other sessions' directories only appear via planFiles entries.\n\t */\n\tfunction collectPanelPlanFiles(ctx: ExtensionContext): string[] {\n\t\tconst state = getPlanState();\n\t\tconst seen = new Set<string>();\n\t\tconst files: string[] = [];\n\t\tfor (const path of [...[...state.planFiles].reverse(), ...listProjectPlanFiles(ctx.cwd)]) {\n\t\t\tconst key = path.replace(/\\\\/g, \"/\").toLowerCase();\n\t\t\tif (seen.has(key)) continue;\n\t\t\tseen.add(key);\n\t\t\tfiles.push(path);\n\t\t\tif (files.length >= PLAN_PANEL_MAX_FILES) break;\n\t\t}\n\t\treturn files;\n\t}\n\n\t/** Panel behind /plan while planning: pick a plan file or exit the mode. */\n\tasync function openPlanPanel(ctx: ExtensionContext): Promise<void> {\n\t\tconst files = collectPanelPlanFiles(ctx);\n\t\tconst fileOptions = files.map((path) => ({ label: basename(path), description: shortenPlanPath(path) }));\n\t\tconst options = [{ label: PLAN_PANEL_EXIT, description: PLAN_PANEL_EXIT_DESCRIPTION }, ...fileOptions];\n\t\tconst choice = await ctx.ui.select(PLAN_PANEL_TITLE, options, { cancelHint: PLAN_PANEL_CANCEL_HINT });\n\t\tif (choice === PLAN_PANEL_EXIT) {\n\t\t\texitPlanMode(ctx, \"Plan mode disabled: full tool access restored.\");\n\t\t\treturn;\n\t\t}\n\t\tif (!choice) return; // Esc: keep planning, no side effects.\n\t\tconst selected = files.find((path) => basename(path) === choice);\n\t\tif (selected) ctx.ui.pasteToEditor(selected);\n\t}\n\n\tpi.registerCommand(PLAN_COMMAND_NAME, {\n\t\tdescription: \"Enter plan mode; while planning, open the plan panel (pick a plan file or exit)\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\t\tconst state = getPlanState();\n\t\t\tif (menuOpen) {\n\t\t\t\tctx.ui.notify(\"The exit_plan approval menu is open — answer it first.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (state.awaitingCompact) {\n\t\t\t\tctx.ui.notify(\"Waiting for compaction to finish before leaving plan mode.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!state.planning) {\n\t\t\t\tenterPlanMode(ctx);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (ctx.mode !== \"tui\") {\n\t\t\t\t// No panel affordances outside the TUI: keep the toggle semantic.\n\t\t\t\texitPlanMode(ctx, \"Plan mode disabled: full tool access restored.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait openPlanPanel(ctx);\n\t\t},\n\t});\n\n\t// Defense in depth: the active-tools change lands on the next LLM request,\n\t// so a request already in flight when plan mode toggled can still carry\n\t// blocked tool calls.\n\tpi.on(\"tool_call\", async (event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst state = getPlanState();\n\t\tif (!state.planning) return;\n\t\tif (PLAN_BLOCKED_TOOLS.has(event.toolName)) {\n\t\t\treturn {\n\t\t\t\tblock: true,\n\t\t\t\treason: `Plan mode: ${event.toolName} is disabled. Keep exploring with read-only tools, or call ${EXIT_PLAN_TOOL_NAME} to submit the plan for approval.`,\n\t\t\t};\n\t\t}\n\t\tif (event.toolName === PLAN_SUBAGENT_TOOL_NAME) {\n\t\t\tconst disallowed = findDisallowedSubagentProfile(event.input);\n\t\t\tif (disallowed !== undefined) {\n\t\t\t\treturn {\n\t\t\t\t\tblock: true,\n\t\t\t\t\treason: `Plan mode: subagent profile \"${disallowed}\" is blocked while planning. Delegate read-only exploration with agent: \"${PLAN_ALLOWED_SUBAGENT}\" (set it on every task in tasks mode).`,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t});\n\n\tpi.on(\"before_agent_start\", async (event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst state = getPlanState();\n\t\tif (!state.planning) return;\n\t\treturn { systemPrompt: `${event.systemPrompt}\\n\\n${buildPlanModePrompt(state.planFiles)}` };\n\t});\n\n\t// agent_settled fires once per fully settled run (agent_end can repeat on\n\t// retries and queued continuations), so the approved compaction starts here.\n\tpi.on(\"agent_settled\", async (_event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tif (!getPlanState().awaitingCompact || !pendingCompact) return;\n\t\tconst pending = pendingCompact;\n\t\tpendingCompact = undefined;\n\t\tctx.compact({\n\t\t\tcustomInstructions: `The user approved an implementation plan saved at ${pending.planPath}. Preserve decisions, constraints, and rejected alternatives from the planning discussion that are not already written in the plan file.`,\n\t\t\tonComplete: () => {\n\t\t\t\texitPlanMode(ctx, \"Context compacted; executing the plan.\");\n\t\t\t\tsendKickoff(pending);\n\t\t\t},\n\t\t\tonError: (error) => {\n\t\t\t\t// Never leave the user stuck read-only: degrade to keep-context execution.\n\t\t\t\texitPlanMode(ctx, `Compaction failed (${error.message}); executing with full context instead.`);\n\t\t\t\tsendKickoff(pending);\n\t\t\t},\n\t\t});\n\t});\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tsyncFromBranch(ctx, { applyFlag: true });\n\t});\n\n\tpi.on(\"session_tree\", async (_event, ctx) => {\n\t\tsyncFromBranch(ctx);\n\t});\n\n\tpi.on(\"session_shutdown\", async (_event, ctx) => {\n\t\tpendingCompact = undefined;\n\t\tdisposePlanSession(ctx.sessionManager.getSessionId());\n\t});\n}\n"]}
@@ -12,4 +12,9 @@ export interface ExitPlanDetails {
12
12
  /** Absolute path of the saved plan file. Present for both execute decisions. */
13
13
  planPath?: string;
14
14
  }
15
+ /** details of the post-compaction kickoff custom message. */
16
+ export interface PlanKickoffDetails {
17
+ title: string;
18
+ planPath: string;
19
+ }
15
20
  //# sourceMappingURL=schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/extensions/plan/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,eAAO,MAAM,oBAAoB;;;;EAY/B,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAEjE,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,mBAAmB,GAAG,cAAc,GAAG,WAAW,CAAC;AAE9F,MAAM,WAAW,eAAe;IAC/B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB","sourcesContent":["import { type Static, Type } from \"typebox\";\n\nexport const ExitPlanParamsSchema = Type.Object({\n\ttitle: Type.String({\n\t\tdescription: \"Short plan title. Used for the plan file name and, when the session is unnamed, the session name.\",\n\t}),\n\tplan: Type.String({\n\t\tdescription: \"The complete plan as markdown: goal, steps, and verification criteria.\",\n\t}),\n\trevises: Type.Optional(\n\t\tType.String({\n\t\t\tdescription: \"File name of a previously saved plan this plan revises, e.g. `01-add-cache.md`.\",\n\t\t}),\n\t),\n});\n\nexport type ExitPlanParams = Static<typeof ExitPlanParamsSchema>;\n\nexport type ExitPlanDecision = \"execute\" | \"compactAndExecute\" | \"keepPlanning\" | \"cancelled\";\n\nexport interface ExitPlanDetails {\n\tdecision: ExitPlanDecision;\n\ttitle: string;\n\t/** Absolute path of the saved plan file. Present for both execute decisions. */\n\tplanPath?: string;\n}\n"]}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/extensions/plan/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,eAAO,MAAM,oBAAoB;;;;EAY/B,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAEjE,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,mBAAmB,GAAG,cAAc,GAAG,WAAW,CAAC;AAE9F,MAAM,WAAW,eAAe;IAC/B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,6DAA6D;AAC7D,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CACjB","sourcesContent":["import { type Static, Type } from \"typebox\";\n\nexport const ExitPlanParamsSchema = Type.Object({\n\ttitle: Type.String({\n\t\tdescription: \"Short plan title. Used for the plan file name and, when the session is unnamed, the session name.\",\n\t}),\n\tplan: Type.String({\n\t\tdescription: \"The complete plan as markdown: goal, steps, and verification criteria.\",\n\t}),\n\trevises: Type.Optional(\n\t\tType.String({\n\t\t\tdescription: \"File name of a previously saved plan this plan revises, e.g. `01-add-cache.md`.\",\n\t\t}),\n\t),\n});\n\nexport type ExitPlanParams = Static<typeof ExitPlanParamsSchema>;\n\nexport type ExitPlanDecision = \"execute\" | \"compactAndExecute\" | \"keepPlanning\" | \"cancelled\";\n\nexport interface ExitPlanDetails {\n\tdecision: ExitPlanDecision;\n\ttitle: string;\n\t/** Absolute path of the saved plan file. Present for both execute decisions. */\n\tplanPath?: string;\n}\n\n/** details of the post-compaction kickoff custom message. */\nexport interface PlanKickoffDetails {\n\ttitle: string;\n\tplanPath: string;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/extensions/plan/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAC,MAAM,CAAC;IAC/C,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC;QAClB,WAAW,EAAE,mGAAmG;KAChH,CAAC;IACF,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC;QACjB,WAAW,EAAE,wEAAwE;KACrF,CAAC;IACF,OAAO,EAAE,IAAI,CAAC,QAAQ,CACrB,IAAI,CAAC,MAAM,CAAC;QACX,WAAW,EAAE,iFAAiF;KAC9F,CAAC,CACF;CACD,CAAC,CAAC","sourcesContent":["import { type Static, Type } from \"typebox\";\n\nexport const ExitPlanParamsSchema = Type.Object({\n\ttitle: Type.String({\n\t\tdescription: \"Short plan title. Used for the plan file name and, when the session is unnamed, the session name.\",\n\t}),\n\tplan: Type.String({\n\t\tdescription: \"The complete plan as markdown: goal, steps, and verification criteria.\",\n\t}),\n\trevises: Type.Optional(\n\t\tType.String({\n\t\t\tdescription: \"File name of a previously saved plan this plan revises, e.g. `01-add-cache.md`.\",\n\t\t}),\n\t),\n});\n\nexport type ExitPlanParams = Static<typeof ExitPlanParamsSchema>;\n\nexport type ExitPlanDecision = \"execute\" | \"compactAndExecute\" | \"keepPlanning\" | \"cancelled\";\n\nexport interface ExitPlanDetails {\n\tdecision: ExitPlanDecision;\n\ttitle: string;\n\t/** Absolute path of the saved plan file. Present for both execute decisions. */\n\tplanPath?: string;\n}\n"]}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/extensions/plan/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAC,MAAM,CAAC;IAC/C,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC;QAClB,WAAW,EAAE,mGAAmG;KAChH,CAAC;IACF,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC;QACjB,WAAW,EAAE,wEAAwE;KACrF,CAAC;IACF,OAAO,EAAE,IAAI,CAAC,QAAQ,CACrB,IAAI,CAAC,MAAM,CAAC;QACX,WAAW,EAAE,iFAAiF;KAC9F,CAAC,CACF;CACD,CAAC,CAAC","sourcesContent":["import { type Static, Type } from \"typebox\";\n\nexport const ExitPlanParamsSchema = Type.Object({\n\ttitle: Type.String({\n\t\tdescription: \"Short plan title. Used for the plan file name and, when the session is unnamed, the session name.\",\n\t}),\n\tplan: Type.String({\n\t\tdescription: \"The complete plan as markdown: goal, steps, and verification criteria.\",\n\t}),\n\trevises: Type.Optional(\n\t\tType.String({\n\t\t\tdescription: \"File name of a previously saved plan this plan revises, e.g. `01-add-cache.md`.\",\n\t\t}),\n\t),\n});\n\nexport type ExitPlanParams = Static<typeof ExitPlanParamsSchema>;\n\nexport type ExitPlanDecision = \"execute\" | \"compactAndExecute\" | \"keepPlanning\" | \"cancelled\";\n\nexport interface ExitPlanDetails {\n\tdecision: ExitPlanDecision;\n\ttitle: string;\n\t/** Absolute path of the saved plan file. Present for both execute decisions. */\n\tplanPath?: string;\n}\n\n/** details of the post-compaction kickoff custom message. */\nexport interface PlanKickoffDetails {\n\ttitle: string;\n\tplanPath: string;\n}\n"]}
@@ -1,8 +1,13 @@
1
1
  export interface PlanSessionState {
2
2
  /** True from entry until the exit is fully applied (including a pending compaction). */
3
3
  planning: boolean;
4
- /** Tools actually removed on entry; restored verbatim on exit. */
5
- removedTools: string[];
4
+ /**
5
+ * Full active-tool set snapshotted on entry; restored verbatim on exit.
6
+ * Empty for legacy entries (pre-snapshot `removedTools` shape) — replay
7
+ * re-derives a fresh snapshot for active planning, and an inactive legacy
8
+ * entry needs no restore beyond dropping exit_plan.
9
+ */
10
+ toolSnapshot: string[];
6
11
  /** A compact-and-execute exit was approved and is waiting for agent_settled + compaction. */
7
12
  awaitingCompact: boolean;
8
13
  /** Absolute paths of plan files saved on this conversation branch. */
@@ -16,6 +21,10 @@ export declare function replacePlanState(next: PlanSessionState): void;
16
21
  /** Drop in-memory state for a session (call from session_shutdown). */
17
22
  export declare function disposePlanSession(sid: string): void;
18
23
  export declare function clonePlanState(source: PlanSessionState): PlanSessionState;
24
+ /**
25
+ * Accepts both the current shape (`toolSnapshot`) and the legacy one
26
+ * (`removedTools`), so branches recorded before the snapshot change replay.
27
+ */
19
28
  export declare function isPlanState(value: unknown): value is PlanSessionState;
20
29
  /**
21
30
  * Latest plan-mode entry on the current branch, or undefined when the branch
@@ -1 +1 @@
1
- {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/extensions/plan/state.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,gBAAgB;IAChC,wFAAwF;IACxF,QAAQ,EAAE,OAAO,CAAC;IAClB,kEAAkE;IAClE,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,6FAA6F;IAC7F,eAAe,EAAE,OAAO,CAAC;IACzB,sEAAsE;IACtE,SAAS,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,eAAO,MAAM,gBAAgB,EAAE,gBAK9B,CAAC;AAKF,kFAAkF;AAClF,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEtD;AAED,wBAAgB,YAAY,IAAI,gBAAgB,CAE/C;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAE7D;AAED,uEAAuE;AACvE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAIpD;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,CAOzE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CASrE;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE;IACzC,cAAc,EAAE;QAAE,SAAS,IAAI,QAAQ,CAAC,OAAO,CAAC,CAAA;KAAE,CAAC;CACnD,GAAG,gBAAgB,GAAG,SAAS,CAS/B","sourcesContent":["/**\n * plan/state.ts — per-session plan-mode state, persisted as custom entries.\n *\n * State is conversation-backed like the todo extension: every transition is\n * appended as a `plan-mode` custom entry (never sent to the LLM), and lifecycle\n * handlers replay the current branch tail → head to restore. Custom entries are\n * copied on fork/clone, so `planFiles` keeps pointing at the right plan files\n * even though the forked session gets a new session id.\n *\n * State is keyed per session id: one Pi process can host several sessions over\n * its lifetime (resume, /tree switches), so callers re-point the active bucket\n * wherever ctx is in hand.\n */\nimport { PLAN_ENTRY_TYPE } from \"./constants.ts\";\n\nexport interface PlanSessionState {\n\t/** True from entry until the exit is fully applied (including a pending compaction). */\n\tplanning: boolean;\n\t/** Tools actually removed on entry; restored verbatim on exit. */\n\tremovedTools: string[];\n\t/** A compact-and-execute exit was approved and is waiting for agent_settled + compaction. */\n\tawaitingCompact: boolean;\n\t/** Absolute paths of plan files saved on this conversation branch. */\n\tplanFiles: string[];\n}\n\nexport const EMPTY_PLAN_STATE: PlanSessionState = {\n\tplanning: false,\n\tremovedTools: [],\n\tawaitingCompact: false,\n\tplanFiles: [],\n};\n\nconst states = new Map<string, PlanSessionState>();\nlet activeSid = \"\";\n\n/** Point the module at a session's state bucket. Call wherever ctx is in hand. */\nexport function setActivePlanSession(sid: string): void {\n\tactiveSid = sid;\n}\n\nexport function getPlanState(): PlanSessionState {\n\treturn states.get(activeSid) ?? clonePlanState(EMPTY_PLAN_STATE);\n}\n\nexport function replacePlanState(next: PlanSessionState): void {\n\tstates.set(activeSid, clonePlanState(next));\n}\n\n/** Drop in-memory state for a session (call from session_shutdown). */\nexport function disposePlanSession(sid: string): void {\n\tif (!sid) return;\n\tstates.delete(sid);\n\tif (activeSid === sid) activeSid = \"\";\n}\n\nexport function clonePlanState(source: PlanSessionState): PlanSessionState {\n\treturn {\n\t\tplanning: source.planning,\n\t\tremovedTools: [...source.removedTools],\n\t\tawaitingCompact: source.awaitingCompact,\n\t\tplanFiles: [...source.planFiles],\n\t};\n}\n\nexport function isPlanState(value: unknown): value is PlanSessionState {\n\tif (!value || typeof value !== \"object\") return false;\n\tconst record = value as Record<string, unknown>;\n\treturn (\n\t\ttypeof record.planning === \"boolean\" &&\n\t\ttypeof record.awaitingCompact === \"boolean\" &&\n\t\tArray.isArray(record.removedTools) &&\n\t\tArray.isArray(record.planFiles)\n\t);\n}\n\n/**\n * Latest plan-mode entry on the current branch, or undefined when the branch\n * has none (lets callers distinguish \"never used\" from \"explicitly exited\").\n */\nexport function replayPlanFromBranch(ctx: {\n\tsessionManager: { getBranch(): Iterable<unknown> };\n}): PlanSessionState | undefined {\n\tconst branch = Array.from(ctx.sessionManager.getBranch());\n\tfor (let i = branch.length - 1; i >= 0; i--) {\n\t\tconst item = branch[i] as { type?: string; customType?: string; data?: unknown };\n\t\tif (item.type !== \"custom\" || item.customType !== PLAN_ENTRY_TYPE) continue;\n\t\tif (!isPlanState(item.data)) continue;\n\t\treturn clonePlanState(item.data);\n\t}\n\treturn undefined;\n}\n"]}
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/extensions/plan/state.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,gBAAgB;IAChC,wFAAwF;IACxF,QAAQ,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,6FAA6F;IAC7F,eAAe,EAAE,OAAO,CAAC;IACzB,sEAAsE;IACtE,SAAS,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,eAAO,MAAM,gBAAgB,EAAE,gBAK9B,CAAC;AAKF,kFAAkF;AAClF,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEtD;AAED,wBAAgB,YAAY,IAAI,gBAAgB,CAE/C;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAE7D;AAED,uEAAuE;AACvE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAIpD;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,CAOzE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAGrE;AAqBD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE;IACzC,cAAc,EAAE;QAAE,SAAS,IAAI,QAAQ,CAAC,OAAO,CAAC,CAAA;KAAE,CAAC;CACnD,GAAG,gBAAgB,GAAG,SAAS,CAU/B","sourcesContent":["/**\n * plan/state.ts — per-session plan-mode state, persisted as custom entries.\n *\n * State is conversation-backed like the todo extension: every transition is\n * appended as a `plan-mode` custom entry (never sent to the LLM), and lifecycle\n * handlers replay the current branch tail → head to restore. Custom entries are\n * copied on fork/clone, so `planFiles` keeps pointing at the right plan files\n * even though the forked session gets a new session id.\n *\n * State is keyed per session id: one Pi process can host several sessions over\n * its lifetime (resume, /tree switches), so callers re-point the active bucket\n * wherever ctx is in hand.\n */\nimport { PLAN_ENTRY_TYPE } from \"./constants.ts\";\n\nexport interface PlanSessionState {\n\t/** True from entry until the exit is fully applied (including a pending compaction). */\n\tplanning: boolean;\n\t/**\n\t * Full active-tool set snapshotted on entry; restored verbatim on exit.\n\t * Empty for legacy entries (pre-snapshot `removedTools` shape) — replay\n\t * re-derives a fresh snapshot for active planning, and an inactive legacy\n\t * entry needs no restore beyond dropping exit_plan.\n\t */\n\ttoolSnapshot: string[];\n\t/** A compact-and-execute exit was approved and is waiting for agent_settled + compaction. */\n\tawaitingCompact: boolean;\n\t/** Absolute paths of plan files saved on this conversation branch. */\n\tplanFiles: string[];\n}\n\nexport const EMPTY_PLAN_STATE: PlanSessionState = {\n\tplanning: false,\n\ttoolSnapshot: [],\n\tawaitingCompact: false,\n\tplanFiles: [],\n};\n\nconst states = new Map<string, PlanSessionState>();\nlet activeSid = \"\";\n\n/** Point the module at a session's state bucket. Call wherever ctx is in hand. */\nexport function setActivePlanSession(sid: string): void {\n\tactiveSid = sid;\n}\n\nexport function getPlanState(): PlanSessionState {\n\treturn states.get(activeSid) ?? clonePlanState(EMPTY_PLAN_STATE);\n}\n\nexport function replacePlanState(next: PlanSessionState): void {\n\tstates.set(activeSid, clonePlanState(next));\n}\n\n/** Drop in-memory state for a session (call from session_shutdown). */\nexport function disposePlanSession(sid: string): void {\n\tif (!sid) return;\n\tstates.delete(sid);\n\tif (activeSid === sid) activeSid = \"\";\n}\n\nexport function clonePlanState(source: PlanSessionState): PlanSessionState {\n\treturn {\n\t\tplanning: source.planning,\n\t\ttoolSnapshot: [...source.toolSnapshot],\n\t\tawaitingCompact: source.awaitingCompact,\n\t\tplanFiles: [...source.planFiles],\n\t};\n}\n\n/**\n * Accepts both the current shape (`toolSnapshot`) and the legacy one\n * (`removedTools`), so branches recorded before the snapshot change replay.\n */\nexport function isPlanState(value: unknown): value is PlanSessionState {\n\tconst parsed = parsePlanState(value);\n\treturn parsed !== undefined;\n}\n\nfunction parsePlanState(value: unknown): PlanSessionState | undefined {\n\tif (!value || typeof value !== \"object\") return undefined;\n\tconst record = value as Record<string, unknown>;\n\tif (typeof record.planning !== \"boolean\" || typeof record.awaitingCompact !== \"boolean\") return undefined;\n\tif (!Array.isArray(record.planFiles)) return undefined;\n\tconst snapshot = Array.isArray(record.toolSnapshot)\n\t\t? record.toolSnapshot\n\t\t: Array.isArray(record.removedTools)\n\t\t\t? [] // Legacy entries recorded removals, not a snapshot; nothing to restore from.\n\t\t\t: undefined;\n\tif (!snapshot) return undefined;\n\treturn {\n\t\tplanning: record.planning,\n\t\ttoolSnapshot: snapshot.filter((tool): tool is string => typeof tool === \"string\"),\n\t\tawaitingCompact: record.awaitingCompact,\n\t\tplanFiles: record.planFiles.filter((path): path is string => typeof path === \"string\"),\n\t};\n}\n\n/**\n * Latest plan-mode entry on the current branch, or undefined when the branch\n * has none (lets callers distinguish \"never used\" from \"explicitly exited\").\n */\nexport function replayPlanFromBranch(ctx: {\n\tsessionManager: { getBranch(): Iterable<unknown> };\n}): PlanSessionState | undefined {\n\tconst branch = Array.from(ctx.sessionManager.getBranch());\n\tfor (let i = branch.length - 1; i >= 0; i--) {\n\t\tconst item = branch[i] as { type?: string; customType?: string; data?: unknown };\n\t\tif (item.type !== \"custom\" || item.customType !== PLAN_ENTRY_TYPE) continue;\n\t\tconst parsed = parsePlanState(item.data);\n\t\tif (!parsed) continue;\n\t\treturn parsed;\n\t}\n\treturn undefined;\n}\n"]}
@@ -14,7 +14,7 @@
14
14
  import { PLAN_ENTRY_TYPE } from "./constants.js";
15
15
  export const EMPTY_PLAN_STATE = {
16
16
  planning: false,
17
- removedTools: [],
17
+ toolSnapshot: [],
18
18
  awaitingCompact: false,
19
19
  planFiles: [],
20
20
  };
@@ -41,19 +41,40 @@ export function disposePlanSession(sid) {
41
41
  export function clonePlanState(source) {
42
42
  return {
43
43
  planning: source.planning,
44
- removedTools: [...source.removedTools],
44
+ toolSnapshot: [...source.toolSnapshot],
45
45
  awaitingCompact: source.awaitingCompact,
46
46
  planFiles: [...source.planFiles],
47
47
  };
48
48
  }
49
+ /**
50
+ * Accepts both the current shape (`toolSnapshot`) and the legacy one
51
+ * (`removedTools`), so branches recorded before the snapshot change replay.
52
+ */
49
53
  export function isPlanState(value) {
54
+ const parsed = parsePlanState(value);
55
+ return parsed !== undefined;
56
+ }
57
+ function parsePlanState(value) {
50
58
  if (!value || typeof value !== "object")
51
- return false;
59
+ return undefined;
52
60
  const record = value;
53
- return (typeof record.planning === "boolean" &&
54
- typeof record.awaitingCompact === "boolean" &&
55
- Array.isArray(record.removedTools) &&
56
- Array.isArray(record.planFiles));
61
+ if (typeof record.planning !== "boolean" || typeof record.awaitingCompact !== "boolean")
62
+ return undefined;
63
+ if (!Array.isArray(record.planFiles))
64
+ return undefined;
65
+ const snapshot = Array.isArray(record.toolSnapshot)
66
+ ? record.toolSnapshot
67
+ : Array.isArray(record.removedTools)
68
+ ? [] // Legacy entries recorded removals, not a snapshot; nothing to restore from.
69
+ : undefined;
70
+ if (!snapshot)
71
+ return undefined;
72
+ return {
73
+ planning: record.planning,
74
+ toolSnapshot: snapshot.filter((tool) => typeof tool === "string"),
75
+ awaitingCompact: record.awaitingCompact,
76
+ planFiles: record.planFiles.filter((path) => typeof path === "string"),
77
+ };
57
78
  }
58
79
  /**
59
80
  * Latest plan-mode entry on the current branch, or undefined when the branch
@@ -65,9 +86,10 @@ export function replayPlanFromBranch(ctx) {
65
86
  const item = branch[i];
66
87
  if (item.type !== "custom" || item.customType !== PLAN_ENTRY_TYPE)
67
88
  continue;
68
- if (!isPlanState(item.data))
89
+ const parsed = parsePlanState(item.data);
90
+ if (!parsed)
69
91
  continue;
70
- return clonePlanState(item.data);
92
+ return parsed;
71
93
  }
72
94
  return undefined;
73
95
  }