@hank-warren/pi-plan-mode 0.1.0 → 1.0.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/NOTICE.md +4 -0
- package/README.md +60 -246
- package/package.json +3 -4
- package/src/active-implementation-menu.ts +4 -3
- package/src/command.ts +4 -10
- package/src/completion-tool.ts +3 -1
- package/src/fresh-implementation.ts +19 -26
- package/src/interactive-ui.ts +0 -1
- package/src/plan-action-controller.ts +5 -29
- package/src/plan-action-menus.ts +16 -23
- package/src/plan-export.ts +9 -7
- package/src/plan-file.ts +85 -0
- package/src/plan-launch-menu.ts +18 -78
- package/src/plan-mode.ts +126 -505
- package/src/presentation.ts +21 -40
- package/src/prompt.ts +15 -12
- package/src/settings-menu.ts +4 -181
- package/src/settings.ts +6 -105
- package/src/state.ts +22 -118
- package/src/auto-permissions-delegation.ts +0 -122
- package/src/implementation-retention.ts +0 -122
- package/src/message-transform.ts +0 -232
- package/src/required-tools.ts +0 -22
- package/src/saved-plan-menu.ts +0 -93
- package/src/saved-plan-preflight.ts +0 -39
- package/src/tool-policy.ts +0 -563
- package/src/tool-selection.ts +0 -98
package/src/plan-mode.ts
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import type {
|
|
3
2
|
ExtensionAPI,
|
|
4
3
|
ExtensionCommandContext,
|
|
5
4
|
ExtensionContext,
|
|
6
5
|
} from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import {
|
|
8
|
-
autoPermissionsIsLoaded,
|
|
9
|
-
shouldDelegateBashToAutoPermissions,
|
|
10
|
-
snapshotAutoPermissionsTrustedGroups,
|
|
11
|
-
} from "./auto-permissions-delegation.js";
|
|
12
6
|
import { completePlanArguments } from "./command.js";
|
|
13
7
|
import {
|
|
14
8
|
normalizePlanModeCompletion,
|
|
@@ -26,11 +20,7 @@ import {
|
|
|
26
20
|
formatImplementationHandoff,
|
|
27
21
|
startFreshImplementationFromState,
|
|
28
22
|
} from "./fresh-implementation.js";
|
|
29
|
-
import {
|
|
30
|
-
createImplementationRetentionCoordinator,
|
|
31
|
-
implementationRetentionPreview,
|
|
32
|
-
} from "./implementation-retention.js";
|
|
33
|
-
import { invalidPlanMessage, latestAssistantText, parseProposedPlan } from "./message-transform.js";
|
|
23
|
+
import { deletePlanFile, planFilePathForSession, readPlanFile, writePlanFile } from "./plan-file.js";
|
|
34
24
|
import { createPlanActionController } from "./plan-action-controller.js";
|
|
35
25
|
import { createPlanExportController } from "./plan-export-controller.js";
|
|
36
26
|
import {
|
|
@@ -39,7 +29,7 @@ import {
|
|
|
39
29
|
showStoredPlan,
|
|
40
30
|
updatePlanModeUi,
|
|
41
31
|
} from "./presentation.js";
|
|
42
|
-
import { buildPlanModePrompt } from "./prompt.js";
|
|
32
|
+
import { buildActivePlanPointer, buildPlanModePrompt } from "./prompt.js";
|
|
43
33
|
import {
|
|
44
34
|
answerPlanModeQuestions,
|
|
45
35
|
normalizePlanModeQuestionParams,
|
|
@@ -47,43 +37,23 @@ import {
|
|
|
47
37
|
PLAN_MODE_QUESTION_TOOL_NAME,
|
|
48
38
|
planModeQuestionCancelled,
|
|
49
39
|
} from "./question-tool.js";
|
|
50
|
-
import { withoutRequiredPlanModeTools, withRequiredPlanModeTools } from "./required-tools.js";
|
|
51
|
-
import {
|
|
52
|
-
preflightSavedPlanImplementation,
|
|
53
|
-
savedPlanBlocksNewWorkflow,
|
|
54
|
-
} from "./saved-plan-preflight.js";
|
|
55
40
|
import {
|
|
56
41
|
awaitPlanModeSettingsWrites,
|
|
57
|
-
configuredBashPolicy,
|
|
58
|
-
configuredImplementationPlanRetention,
|
|
59
42
|
configuredThinkingLevel,
|
|
60
43
|
type PlanModeSettings,
|
|
61
44
|
readPlanModeSettings,
|
|
62
45
|
} from "./settings.js";
|
|
63
|
-
import { type
|
|
64
|
-
import {
|
|
65
|
-
canSelectToolInPlanMode,
|
|
66
|
-
classifyPlanModeTool,
|
|
67
|
-
findBlockedCommandSegment,
|
|
68
|
-
readCommand,
|
|
69
|
-
} from "./tool-policy.js";
|
|
70
|
-
import {
|
|
71
|
-
compareTools,
|
|
72
|
-
filterAvailableSelectedToolNames,
|
|
73
|
-
snapshotPlanModeSelectedNames,
|
|
74
|
-
snapshotPlanModeToolNames,
|
|
75
|
-
toolPolicyLabel,
|
|
76
|
-
} from "./tool-selection.js";
|
|
46
|
+
import { type PlanModeState, restorePlanModeState } from "./state.js";
|
|
77
47
|
|
|
78
48
|
const STATE_ENTRY_TYPE = "plan-mode-state";
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Plan mode's entire enforcement surface. Everything else — bash, subagents,
|
|
51
|
+
* MCP, and other extension tools — is left to the session's normal permission
|
|
52
|
+
* layer (for example @hank-warren/pi-auto-permissions), so Plan mode never
|
|
53
|
+
* mutates the active tool set and never fights other extensions for it.
|
|
54
|
+
*/
|
|
55
|
+
const BLOCKED_TOOLS = new Set(["edit", "write", "update_plan"]);
|
|
56
|
+
|
|
87
57
|
type InteractiveUi = typeof import("./interactive-ui.js");
|
|
88
58
|
|
|
89
59
|
interface PlanModeDependencies {
|
|
@@ -91,6 +61,7 @@ interface PlanModeDependencies {
|
|
|
91
61
|
settingsPath?: string;
|
|
92
62
|
loadInteractiveUi?(): Promise<InteractiveUi>;
|
|
93
63
|
}
|
|
64
|
+
|
|
94
65
|
export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDependencies = {}) {
|
|
95
66
|
let interactiveUiPromise: Promise<InteractiveUi> | undefined;
|
|
96
67
|
const loadInteractiveUi = () => {
|
|
@@ -105,49 +76,44 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
105
76
|
};
|
|
106
77
|
let state: PlanModeState = { enabled: false, awaitingAction: false };
|
|
107
78
|
let settings: PlanModeSettings = { thinkingLevel: "inherit" };
|
|
108
|
-
let
|
|
109
|
-
let
|
|
110
|
-
let
|
|
79
|
+
let sessionPlanPath: string | undefined;
|
|
80
|
+
let readyPresentationNonce = 0;
|
|
81
|
+
let pendingReadyNonce: number | undefined;
|
|
111
82
|
let latestCommandContext: ExtensionCommandContext | undefined;
|
|
112
|
-
let nextReadyPresentationNonce = 0;
|
|
113
83
|
let menuGeneration = 0;
|
|
114
84
|
let workflowGeneration = 0;
|
|
115
85
|
let refreshStateBeforeFirstAgentStart = false;
|
|
116
86
|
let menuController = new AbortController();
|
|
117
|
-
const implementationRetention = createImplementationRetentionCoordinator();
|
|
118
87
|
const persistState = () => pi.appendEntry<PlanModeState>(STATE_ENTRY_TYPE, state);
|
|
119
88
|
const planExports = createPlanExportController({
|
|
120
89
|
getState: () => state,
|
|
121
90
|
getSettings: () => settings,
|
|
122
|
-
finishReady: (ctx) =>
|
|
91
|
+
finishReady: (ctx) => {
|
|
92
|
+
void exitPlanMode(ctx, { keepPlanFile: true });
|
|
93
|
+
},
|
|
123
94
|
});
|
|
124
95
|
const planActions = createPlanActionController({
|
|
125
96
|
loadInteractiveUi,
|
|
126
97
|
getState: () => state,
|
|
127
98
|
captureLifecycle: captureMenuLifecycle,
|
|
128
99
|
statusText: planStatusText,
|
|
129
|
-
|
|
100
|
+
planPathLine: () => (state.planPath ? `Plan file: ${state.planPath}` : undefined),
|
|
130
101
|
getExportDestination: (ctx) => planExports.getDestination(ctx),
|
|
131
102
|
show: (ctx) => showStoredPlan(pi, ctx, state),
|
|
132
103
|
finalize: requestFinalPlan,
|
|
133
104
|
implementHere: startImplementation,
|
|
134
105
|
implementFresh: startFreshImplementation,
|
|
135
106
|
exportPlan: (ctx, path, signal, isCurrent) => planExports.export(path, ctx, signal, isCurrent),
|
|
136
|
-
settings: showSettings,
|
|
137
|
-
save: savePlanForLater,
|
|
138
107
|
stay: updateUi,
|
|
139
108
|
exitReady: (ctx) => {
|
|
140
|
-
exitPlanMode(ctx)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
clearSaved: (ctx) => {
|
|
144
|
-
exitPlanMode(ctx);
|
|
145
|
-
ctx.ui.notify("Saved plan cleared.", "info");
|
|
109
|
+
void exitPlanMode(ctx).then(() => {
|
|
110
|
+
ctx.ui.notify("Plan mode disabled. Proposed plan discarded.", "info");
|
|
111
|
+
});
|
|
146
112
|
},
|
|
147
113
|
});
|
|
148
114
|
|
|
149
115
|
pi.registerFlag("plan", {
|
|
150
|
-
description: "Start in
|
|
116
|
+
description: "Start in Plan mode",
|
|
151
117
|
type: "boolean",
|
|
152
118
|
default: false,
|
|
153
119
|
});
|
|
@@ -212,21 +178,19 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
212
178
|
const parsed = normalizePlanModeCompletion(params);
|
|
213
179
|
if (!parsed.ok) throw new Error(parsed.error);
|
|
214
180
|
|
|
215
|
-
acceptCompletedPlan(parsed.plan,
|
|
216
|
-
return planModeCompleted(parsed.plan);
|
|
181
|
+
const planPath = await acceptCompletedPlan(parsed.plan, ctx);
|
|
182
|
+
return planModeCompleted(parsed.plan, planPath);
|
|
217
183
|
},
|
|
218
184
|
});
|
|
219
185
|
|
|
220
186
|
pi.registerCommand("plan", {
|
|
221
|
-
description: "Enter or manage
|
|
187
|
+
description: "Enter or manage Plan mode",
|
|
222
188
|
getArgumentCompletions: completePlanArguments,
|
|
223
189
|
handler: async (args, ctx) => {
|
|
224
190
|
latestCommandContext = ctx;
|
|
225
191
|
const prompt = args.trim();
|
|
226
192
|
const command = prompt.toLowerCase();
|
|
227
193
|
if (command === "start") {
|
|
228
|
-
if (savedPlanBlocksNewWorkflow(ctx, state.savedPlan !== undefined && !state.enabled))
|
|
229
|
-
return;
|
|
230
194
|
if (state.enabled) {
|
|
231
195
|
ctx.ui.notify("Plan mode is already active.", "info");
|
|
232
196
|
return;
|
|
@@ -236,7 +200,7 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
236
200
|
return;
|
|
237
201
|
}
|
|
238
202
|
if (command === "show") {
|
|
239
|
-
showStoredPlan(pi, ctx, state);
|
|
203
|
+
await showStoredPlan(pi, ctx, state);
|
|
240
204
|
return;
|
|
241
205
|
}
|
|
242
206
|
if (command === "finalize") {
|
|
@@ -244,17 +208,13 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
244
208
|
return;
|
|
245
209
|
}
|
|
246
210
|
if (command === "implement") {
|
|
247
|
-
if (!(
|
|
211
|
+
if (!(await currentPlan())) {
|
|
248
212
|
ctx.ui.notify("No completed plan is available to implement.", "warning");
|
|
249
213
|
return;
|
|
250
214
|
}
|
|
251
215
|
await startImplementation(ctx);
|
|
252
216
|
return;
|
|
253
217
|
}
|
|
254
|
-
if (command === "save") {
|
|
255
|
-
savePlanForLater(ctx);
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
218
|
const exportMatch = /^export(?:\s+([\s\S]+))?$/iu.exec(prompt);
|
|
259
219
|
if (exportMatch) {
|
|
260
220
|
const lifecycle = captureMenuLifecycle();
|
|
@@ -262,36 +222,19 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
262
222
|
return;
|
|
263
223
|
}
|
|
264
224
|
if (command === "exit" || command === "off") {
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
? "
|
|
269
|
-
:
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
225
|
+
const hadPlan = state.planPath !== undefined;
|
|
226
|
+
const notification = state.enabled
|
|
227
|
+
? hadPlan
|
|
228
|
+
? "Plan mode disabled. Proposed plan discarded."
|
|
229
|
+
: "Plan mode disabled."
|
|
230
|
+
: hadPlan
|
|
231
|
+
? "Active implementation plan cleared."
|
|
232
|
+
: "Plan mode disabled.";
|
|
233
|
+
await exitPlanMode(ctx);
|
|
273
234
|
ctx.ui.notify(notification, "info");
|
|
274
235
|
return;
|
|
275
236
|
}
|
|
276
|
-
if (command === "tools") {
|
|
277
|
-
if (savedPlanBlocksNewWorkflow(ctx, state.savedPlan !== undefined && !state.enabled))
|
|
278
|
-
return;
|
|
279
|
-
if (state.enabled) {
|
|
280
|
-
const message =
|
|
281
|
-
"Plan-mode tools are locked while Planning is active. Exit Plan mode and choose tools before starting again.";
|
|
282
|
-
if (!ctx.hasUI) throw new Error(message);
|
|
283
|
-
ctx.ui.notify(message, "warning");
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
if (!ctx.hasUI) {
|
|
287
|
-
throw new Error("/plan tools requires TUI or RPC mode and is unavailable here.");
|
|
288
|
-
}
|
|
289
|
-
await showLaunchMenu(ctx, "tools");
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
237
|
if (prompt) {
|
|
293
|
-
if (savedPlanBlocksNewWorkflow(ctx, state.savedPlan !== undefined && !state.enabled))
|
|
294
|
-
return;
|
|
295
238
|
enterPlanModeWithPrompt(prompt, ctx);
|
|
296
239
|
return;
|
|
297
240
|
}
|
|
@@ -300,15 +243,11 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
300
243
|
"The interactive /plan menu is unavailable in print and JSON modes. Use /plan start or /plan <prompt>.",
|
|
301
244
|
);
|
|
302
245
|
}
|
|
246
|
+
if (!state.enabled && state.planPath) {
|
|
247
|
+
await showActivePlanMenu(ctx);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
303
250
|
if (!state.enabled) {
|
|
304
|
-
if (state.activeImplementation && ctx.hasUI) {
|
|
305
|
-
await showActivePlanMenu(ctx);
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
if (state.savedPlan) {
|
|
309
|
-
await planActions.showSaved(ctx);
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
251
|
await showLaunchMenu(ctx);
|
|
313
252
|
return;
|
|
314
253
|
}
|
|
@@ -321,16 +260,11 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
321
260
|
refreshStateBeforeFirstAgentStart = event.reason === "new";
|
|
322
261
|
menuController.abort(new DOMException("Plan-mode session replaced", "AbortError"));
|
|
323
262
|
menuController = new AbortController();
|
|
324
|
-
|
|
263
|
+
pendingReadyNonce = undefined;
|
|
325
264
|
latestCommandContext = undefined;
|
|
326
|
-
implementationRetention.reset();
|
|
327
265
|
settings = { thinkingLevel: "inherit" };
|
|
328
|
-
|
|
329
|
-
ctx.cwd,
|
|
330
|
-
ctx.isProjectTrusted(),
|
|
331
|
-
);
|
|
266
|
+
sessionPlanPath = resolveSessionPlanPath(ctx);
|
|
332
267
|
restoreState(ctx);
|
|
333
|
-
implementationRetention.restore(state.activeImplementation);
|
|
334
268
|
const loadedSettings = await (dependencies.readSettings?.() ?? readPlanModeSettings());
|
|
335
269
|
if (generation !== menuGeneration || menuController.signal.aborted) return;
|
|
336
270
|
if (loadedSettings.kind === "loaded") settings = loadedSettings.settings;
|
|
@@ -338,33 +272,11 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
338
272
|
ctx.ui.notify(`pi-plan-mode settings ignored: ${loadedSettings.reason}`, "warning");
|
|
339
273
|
}
|
|
340
274
|
if (loadedSettings.notice) ctx.ui.notify(loadedSettings.notice, "warning");
|
|
341
|
-
if (
|
|
342
|
-
configuredBashPolicy(settings) === "auto-permissions" &&
|
|
343
|
-
!autoPermissionsIsLoaded(safeGetAllTools())
|
|
344
|
-
) {
|
|
345
|
-
ctx.ui.notify(
|
|
346
|
-
"Plan mode bash policy is 'auto-permissions', but the Auto Permissions extension is not loaded; guarded commands stay blocked by Plan mode.",
|
|
347
|
-
"warning",
|
|
348
|
-
);
|
|
349
|
-
}
|
|
350
275
|
const persistFlagActivation = pi.getFlag("plan") === true && !state.enabled;
|
|
351
276
|
if (persistFlagActivation) {
|
|
352
|
-
state = state.
|
|
353
|
-
? {
|
|
354
|
-
...state,
|
|
355
|
-
enabled: true,
|
|
356
|
-
latestPlan: state.savedPlan.plan,
|
|
357
|
-
latestPlanSource: state.savedPlan.source,
|
|
358
|
-
awaitingAction: true,
|
|
359
|
-
savedPlan: undefined,
|
|
360
|
-
activeImplementation: undefined,
|
|
361
|
-
}
|
|
362
|
-
: { ...state, enabled: true, activeImplementation: undefined };
|
|
277
|
+
state = { ...state, enabled: true, awaitingAction: state.planPath !== undefined };
|
|
363
278
|
}
|
|
364
|
-
if (state.enabled)
|
|
365
|
-
activatePlanModeTools();
|
|
366
|
-
applyPlanThinkingLevel();
|
|
367
|
-
} else deactivatePlanModeQuestionTool();
|
|
279
|
+
if (state.enabled) applyPlanThinkingLevel();
|
|
368
280
|
if (persistFlagActivation) persistState();
|
|
369
281
|
updateUi(ctx);
|
|
370
282
|
});
|
|
@@ -385,144 +297,68 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
385
297
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
386
298
|
menuGeneration += 1;
|
|
387
299
|
menuController.abort(new DOMException("Plan-mode session shut down", "AbortError"));
|
|
388
|
-
|
|
300
|
+
pendingReadyNonce = undefined;
|
|
389
301
|
latestCommandContext = undefined;
|
|
390
302
|
refreshStateBeforeFirstAgentStart = false;
|
|
391
|
-
autoPermissionsTrustedGroups = new Set<string>();
|
|
392
|
-
implementationRetention.reset();
|
|
393
303
|
await awaitPlanModeSettingsWrites(dependencies.settingsPath);
|
|
394
304
|
captureManualThinkingLevel();
|
|
395
305
|
persistState();
|
|
396
|
-
if (state.enabled)
|
|
397
|
-
restoreTools();
|
|
398
|
-
restoreThinkingLevel();
|
|
399
|
-
}
|
|
306
|
+
if (state.enabled) restoreThinkingLevel();
|
|
400
307
|
clearUi(ctx);
|
|
401
308
|
});
|
|
402
309
|
|
|
310
|
+
/**
|
|
311
|
+
* The complete enforcement surface: three static built-in names. Plan mode
|
|
312
|
+
* does not classify, inspect, or filter any other tool.
|
|
313
|
+
*/
|
|
403
314
|
pi.on("tool_call", async (event) => {
|
|
404
315
|
if (!state.enabled) return;
|
|
405
|
-
if (event.toolName
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
if (calledTool && classifyPlanModeTool(calledTool) === "blocked") {
|
|
414
|
-
return {
|
|
415
|
-
block: true,
|
|
416
|
-
reason: `Plan mode blocks built-in tool '${event.toolName}' because its policy class is blocked.`,
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
if (!calledTool && BLOCKED_BUILTIN_TOOLS.has(event.toolName)) {
|
|
420
|
-
return {
|
|
421
|
-
block: true,
|
|
422
|
-
reason: `Plan mode blocks built-in tool '${event.toolName}' because its metadata is unavailable.`,
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
// Built-in-compatible overrides retain the canonical name but replace its source metadata.
|
|
426
|
-
if (event.toolName !== "bash") return;
|
|
427
|
-
|
|
428
|
-
const command = readCommand(event.input);
|
|
429
|
-
const blocked = findBlockedCommandSegment(command, settings.safeSubcommands);
|
|
430
|
-
if (blocked !== undefined) {
|
|
431
|
-
if (
|
|
432
|
-
configuredBashPolicy(settings) === "auto-permissions" &&
|
|
433
|
-
shouldDelegateBashToAutoPermissions(command, {
|
|
434
|
-
tools: safeGetAllTools(),
|
|
435
|
-
trustedGroups: autoPermissionsTrustedGroups,
|
|
436
|
-
})
|
|
437
|
-
) {
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
return {
|
|
441
|
-
block: true,
|
|
442
|
-
reason: `Plan mode blocks bash commands outside its reviewed inspection policy or containing explicitly unsafe arguments.\nBlocked command: ${blocked}`,
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
pi.on("context", async (event, ctx) => {
|
|
448
|
-
const result = implementationRetention.transformContext(event.messages, state);
|
|
449
|
-
if (result.clearActiveImplementationId) {
|
|
450
|
-
clearActiveImplementation(result.clearActiveImplementationId, ctx);
|
|
451
|
-
}
|
|
452
|
-
return { messages: result.messages as typeof event.messages };
|
|
316
|
+
if (!BLOCKED_TOOLS.has(event.toolName)) return;
|
|
317
|
+
return {
|
|
318
|
+
block: true,
|
|
319
|
+
reason:
|
|
320
|
+
event.toolName === "update_plan"
|
|
321
|
+
? "Plan mode blocks update_plan because it tracks execution progress rather than conversational planning."
|
|
322
|
+
: `Plan mode blocks '${event.toolName}' because planning must not mutate files. Finish the plan with plan_mode_complete, then implement.`,
|
|
323
|
+
};
|
|
453
324
|
});
|
|
454
325
|
|
|
455
326
|
pi.on("before_agent_start", (event, ctx) => {
|
|
456
327
|
if (refreshStateBeforeFirstAgentStart) {
|
|
457
328
|
refreshStateBeforeFirstAgentStart = false;
|
|
458
329
|
restoreState(ctx);
|
|
459
|
-
|
|
460
|
-
implementationRetention.restore(state.activeImplementation);
|
|
461
|
-
if (state.enabled) {
|
|
462
|
-
activatePlanModeTools();
|
|
463
|
-
applyPlanThinkingLevel();
|
|
464
|
-
} else deactivatePlanModeQuestionTool();
|
|
330
|
+
if (state.enabled) applyPlanThinkingLevel();
|
|
465
331
|
updateUi(ctx);
|
|
466
332
|
}
|
|
467
|
-
if (
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
latestPlan: undefined,
|
|
473
|
-
latestPlanSource: undefined,
|
|
474
|
-
awaitingAction: false,
|
|
475
|
-
};
|
|
333
|
+
if (state.enabled && state.awaitingAction) {
|
|
334
|
+
// A new turn supersedes the previous ready plan: revision feedback
|
|
335
|
+
// re-opens planning until another plan_mode_complete arrives.
|
|
336
|
+
pendingReadyNonce = undefined;
|
|
337
|
+
state = { ...state, awaitingAction: false };
|
|
476
338
|
persistState();
|
|
477
339
|
updateUi(ctx);
|
|
478
340
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
const text = latestAssistantText(event.messages);
|
|
489
|
-
const parsedPlan = parseProposedPlan(text);
|
|
490
|
-
if (parsedPlan.kind !== "valid") {
|
|
491
|
-
if (parsedPlan.kind !== "absent") {
|
|
492
|
-
ctx.ui.notify(invalidPlanMessage(parsedPlan.kind), "warning");
|
|
493
|
-
}
|
|
494
|
-
persistState();
|
|
495
|
-
updateUi(ctx);
|
|
496
|
-
return;
|
|
341
|
+
if (state.enabled) {
|
|
342
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${buildPlanModePrompt()}` };
|
|
343
|
+
}
|
|
344
|
+
// Pointer, not payload: an active plan costs one line of context no matter
|
|
345
|
+
// how large the plan is, and survives compaction for free.
|
|
346
|
+
if (state.planPath) {
|
|
347
|
+
return {
|
|
348
|
+
systemPrompt: `${event.systemPrompt}\n\n${buildActivePlanPointer(state.planPath)}`,
|
|
349
|
+
};
|
|
497
350
|
}
|
|
498
|
-
acceptCompletedPlan(parsedPlan.plan, "legacy_proposed_plan", ctx);
|
|
499
351
|
});
|
|
500
352
|
|
|
501
353
|
onAgentSettled(pi, async (_event, ctx) => {
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
);
|
|
505
|
-
if (settledImplementationId) clearActiveImplementation(settledImplementationId, ctx);
|
|
506
|
-
|
|
507
|
-
const intent = readyPresentationIntent;
|
|
508
|
-
if (!intent || !readyPresentationIsCurrent(intent)) return;
|
|
354
|
+
const nonce = pendingReadyNonce;
|
|
355
|
+
if (nonce === undefined || nonce !== readyPresentationNonce) return;
|
|
356
|
+
if (!state.enabled || !state.awaitingAction) return;
|
|
509
357
|
if (!ctx.isIdle() || ctx.hasPendingMessages()) return;
|
|
510
358
|
|
|
511
|
-
|
|
359
|
+
pendingReadyNonce = undefined;
|
|
512
360
|
try {
|
|
513
|
-
if (
|
|
514
|
-
pi.sendMessage(
|
|
515
|
-
{
|
|
516
|
-
customType: PROPOSED_PLAN_MESSAGE_TYPE,
|
|
517
|
-
content: `**Proposed Plan**\n\n${intent.plan}`,
|
|
518
|
-
display: true,
|
|
519
|
-
},
|
|
520
|
-
{ triggerTurn: false },
|
|
521
|
-
);
|
|
522
|
-
}
|
|
523
|
-
if (ctx.hasUI && completedPlanIsCurrent(intent)) {
|
|
524
|
-
await planActions.showReady(latestCommandContext ?? ctx);
|
|
525
|
-
}
|
|
361
|
+
if (ctx.hasUI) await planActions.showReady(latestCommandContext ?? ctx);
|
|
526
362
|
} catch (error: unknown) {
|
|
527
363
|
if (!isStaleExtensionContextError(error)) throw error;
|
|
528
364
|
}
|
|
@@ -530,15 +366,7 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
530
366
|
|
|
531
367
|
function enterPlanMode(ctx: ExtensionContext) {
|
|
532
368
|
workflowGeneration += 1;
|
|
533
|
-
|
|
534
|
-
state = {
|
|
535
|
-
...state,
|
|
536
|
-
enabled: true,
|
|
537
|
-
awaitingAction: false,
|
|
538
|
-
savedPlan: undefined,
|
|
539
|
-
activeImplementation: undefined,
|
|
540
|
-
};
|
|
541
|
-
activatePlanModeTools();
|
|
369
|
+
state = { ...state, enabled: true, awaitingAction: false };
|
|
542
370
|
applyPlanThinkingLevel();
|
|
543
371
|
persistState();
|
|
544
372
|
updateUi(ctx);
|
|
@@ -552,36 +380,31 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
552
380
|
ctx.ui.notify("Plan mode enabled. I will explore and plan, but not modify files.", "info");
|
|
553
381
|
}
|
|
554
382
|
if (sendPlanModeUserMessage(prompt, ctx)) return;
|
|
555
|
-
if (!
|
|
556
|
-
restoreTools();
|
|
557
|
-
restoreThinkingLevel();
|
|
558
|
-
}
|
|
383
|
+
if (!wasEnabled) restoreThinkingLevel();
|
|
559
384
|
state = previousState;
|
|
560
385
|
persistState();
|
|
561
386
|
updateUi(ctx);
|
|
562
387
|
}
|
|
563
388
|
|
|
564
|
-
function exitPlanMode(ctx: ExtensionContext) {
|
|
389
|
+
async function exitPlanMode(ctx: ExtensionContext, options: { keepPlanFile?: boolean } = {}) {
|
|
565
390
|
workflowGeneration += 1;
|
|
566
391
|
const wasEnabled = state.enabled;
|
|
567
|
-
|
|
392
|
+
const planPath = state.planPath;
|
|
393
|
+
pendingReadyNonce = undefined;
|
|
568
394
|
state = {
|
|
569
395
|
...state,
|
|
570
396
|
enabled: false,
|
|
571
|
-
|
|
572
|
-
latestPlanSource: undefined,
|
|
397
|
+
planPath: undefined,
|
|
573
398
|
awaitingAction: false,
|
|
574
|
-
savedPlan: undefined,
|
|
575
|
-
activeImplementation: undefined,
|
|
576
399
|
manualThinkingLevel: undefined,
|
|
577
400
|
};
|
|
578
401
|
if (wasEnabled) {
|
|
579
|
-
restoreTools();
|
|
580
402
|
restoreThinkingLevel();
|
|
581
403
|
state = { ...state, manualThinkingLevel: undefined };
|
|
582
404
|
}
|
|
583
405
|
persistState();
|
|
584
406
|
updateUi(ctx);
|
|
407
|
+
if (planPath && !options.keepPlanFile) await deletePlanFile(planPath);
|
|
585
408
|
}
|
|
586
409
|
|
|
587
410
|
function sendPlanModeUserMessage(message: string, ctx: ExtensionContext) {
|
|
@@ -596,48 +419,29 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
596
419
|
}
|
|
597
420
|
}
|
|
598
421
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
) {
|
|
613
|
-
return;
|
|
422
|
+
/**
|
|
423
|
+
* Writes the durable plan file and marks the plan ready. A write failure
|
|
424
|
+
* keeps Plan mode active rather than silently losing the plan.
|
|
425
|
+
*/
|
|
426
|
+
async function acceptCompletedPlan(plan: string, ctx: ExtensionContext) {
|
|
427
|
+
const planPath = sessionPlanPath ?? resolveSessionPlanPath(ctx);
|
|
428
|
+
sessionPlanPath = planPath;
|
|
429
|
+
try {
|
|
430
|
+
await writePlanFile(planPath, plan);
|
|
431
|
+
} catch (error: unknown) {
|
|
432
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
433
|
+
ctx.ui.notify(`Unable to save the plan to ${planPath}: ${detail}`, "error");
|
|
434
|
+
return undefined;
|
|
614
435
|
}
|
|
615
|
-
state = {
|
|
616
|
-
|
|
617
|
-
latestPlan: normalized.plan,
|
|
618
|
-
latestPlanSource: source,
|
|
619
|
-
awaitingAction: true,
|
|
620
|
-
};
|
|
621
|
-
readyPresentationIntent = {
|
|
622
|
-
nonce: ++nextReadyPresentationNonce,
|
|
623
|
-
plan: normalized.plan,
|
|
624
|
-
source,
|
|
625
|
-
};
|
|
436
|
+
state = { ...state, planPath, awaitingAction: true };
|
|
437
|
+
pendingReadyNonce = ++readyPresentationNonce;
|
|
626
438
|
persistState();
|
|
627
439
|
updateUi(ctx);
|
|
440
|
+
return planPath;
|
|
628
441
|
}
|
|
629
442
|
|
|
630
|
-
function
|
|
631
|
-
return (
|
|
632
|
-
state.enabled &&
|
|
633
|
-
state.awaitingAction &&
|
|
634
|
-
state.latestPlan === intent.plan &&
|
|
635
|
-
state.latestPlanSource === intent.source
|
|
636
|
-
);
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
function readyPresentationIsCurrent(intent: ReadyPresentationIntent) {
|
|
640
|
-
return completedPlanIsCurrent(intent) && readyPresentationIntent?.nonce === intent.nonce;
|
|
443
|
+
async function currentPlan() {
|
|
444
|
+
return state.planPath ? await readPlanFile(state.planPath) : undefined;
|
|
641
445
|
}
|
|
642
446
|
|
|
643
447
|
function requestFinalPlan(ctx: ExtensionContext) {
|
|
@@ -651,157 +455,61 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
651
455
|
);
|
|
652
456
|
}
|
|
653
457
|
|
|
654
|
-
function savePlanForLater(ctx: ExtensionContext) {
|
|
655
|
-
const plan = state.enabled ? state.latestPlan?.trim() : undefined;
|
|
656
|
-
if (!plan) {
|
|
657
|
-
const message = "No completed plan is available to save.";
|
|
658
|
-
if (!ctx.hasUI) throw new Error(message);
|
|
659
|
-
ctx.ui.notify(message, "warning");
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
const source = state.latestPlanSource ?? "legacy_proposed_plan";
|
|
663
|
-
|
|
664
|
-
workflowGeneration += 1;
|
|
665
|
-
readyPresentationIntent = undefined;
|
|
666
|
-
state = {
|
|
667
|
-
...state,
|
|
668
|
-
enabled: false,
|
|
669
|
-
latestPlan: undefined,
|
|
670
|
-
latestPlanSource: undefined,
|
|
671
|
-
awaitingAction: false,
|
|
672
|
-
savedPlan: { plan, source },
|
|
673
|
-
activeImplementation: undefined,
|
|
674
|
-
manualThinkingLevel: undefined,
|
|
675
|
-
};
|
|
676
|
-
restoreTools();
|
|
677
|
-
restoreThinkingLevel();
|
|
678
|
-
state = { ...state, manualThinkingLevel: undefined };
|
|
679
|
-
persistState();
|
|
680
|
-
updateUi(ctx);
|
|
681
|
-
ctx.ui.notify("Plan saved for later. Plan mode disabled.", "info");
|
|
682
|
-
}
|
|
683
|
-
|
|
684
458
|
async function startFreshImplementation(ctx: ExtensionContext, menuIsCurrent: () => boolean) {
|
|
685
459
|
await startFreshImplementationFromState(ctx, {
|
|
686
460
|
getState: () => state,
|
|
687
461
|
menuIsCurrent,
|
|
688
|
-
retention: configuredImplementationPlanRetention(settings),
|
|
689
462
|
stateEntryType: STATE_ENTRY_TYPE,
|
|
690
463
|
});
|
|
691
464
|
}
|
|
692
465
|
|
|
693
466
|
async function startImplementation(ctx: ExtensionContext) {
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
const isCurrent = () =>
|
|
699
|
-
sessionGeneration === menuGeneration &&
|
|
700
|
-
planWorkflowGeneration === workflowGeneration &&
|
|
701
|
-
!menuController.signal.aborted &&
|
|
702
|
-
!state.enabled &&
|
|
703
|
-
state.savedPlan === savedPlan;
|
|
704
|
-
if (!(await preflightSavedPlanImplementation(ctx, isCurrent))) return;
|
|
705
|
-
}
|
|
706
|
-
const plan = (state.enabled ? state.latestPlan : savedPlan?.plan)?.trim();
|
|
707
|
-
const source =
|
|
708
|
-
(state.enabled ? state.latestPlanSource : savedPlan?.source) ?? "legacy_proposed_plan";
|
|
709
|
-
if (!plan) {
|
|
710
|
-
ctx.ui.notify("Plan mode disabled. No proposed plan is available to implement.", "warning");
|
|
467
|
+
const planPath = state.planPath;
|
|
468
|
+
const plan = await currentPlan();
|
|
469
|
+
if (!planPath || !plan) {
|
|
470
|
+
ctx.ui.notify("No completed plan is available to implement.", "warning");
|
|
711
471
|
return;
|
|
712
472
|
}
|
|
713
473
|
|
|
714
474
|
workflowGeneration += 1;
|
|
715
475
|
const previousState = state;
|
|
716
476
|
const wasEnabled = state.enabled;
|
|
717
|
-
|
|
477
|
+
pendingReadyNonce = undefined;
|
|
718
478
|
state = {
|
|
719
479
|
...state,
|
|
720
480
|
enabled: false,
|
|
721
|
-
latestPlan: undefined,
|
|
722
|
-
latestPlanSource: undefined,
|
|
723
481
|
awaitingAction: false,
|
|
724
|
-
|
|
725
|
-
activeImplementation: {
|
|
726
|
-
id: randomUUID(),
|
|
727
|
-
plan,
|
|
728
|
-
source,
|
|
729
|
-
startedAt: Date.now(),
|
|
730
|
-
retention: configuredImplementationPlanRetention(settings),
|
|
731
|
-
},
|
|
482
|
+
planPath,
|
|
732
483
|
manualThinkingLevel: undefined,
|
|
733
484
|
};
|
|
734
485
|
if (wasEnabled) {
|
|
735
|
-
restoreTools();
|
|
736
486
|
restoreThinkingLevel();
|
|
737
487
|
state = { ...state, manualThinkingLevel: undefined };
|
|
738
488
|
}
|
|
739
489
|
persistState();
|
|
740
490
|
updateUi(ctx);
|
|
741
491
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
if (
|
|
745
|
-
state = previousState;
|
|
746
|
-
} else {
|
|
747
|
-
enterPlanMode(ctx);
|
|
748
|
-
state = previousState;
|
|
749
|
-
applyPlanThinkingLevel();
|
|
750
|
-
}
|
|
492
|
+
if (!sendPlanModeUserMessage(formatImplementationHandoff(planPath), ctx)) {
|
|
493
|
+
state = previousState;
|
|
494
|
+
if (wasEnabled) applyPlanThinkingLevel();
|
|
751
495
|
persistState();
|
|
752
496
|
updateUi(ctx);
|
|
753
497
|
}
|
|
754
498
|
}
|
|
755
499
|
|
|
756
|
-
function
|
|
757
|
-
if (state.activeImplementation?.id !== id) return false;
|
|
758
|
-
workflowGeneration += 1;
|
|
759
|
-
state = { ...state, activeImplementation: undefined };
|
|
760
|
-
persistState();
|
|
761
|
-
updateUi(ctx);
|
|
762
|
-
return true;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
async function showLaunchMenu(ctx: ExtensionContext, initialScreen: "main" | "tools" = "main") {
|
|
500
|
+
async function showLaunchMenu(ctx: ExtensionContext) {
|
|
766
501
|
const lifecycle = captureMenuLifecycle();
|
|
767
502
|
if (!lifecycle.isCurrent() || lifecycle.signal.aborted) return;
|
|
768
503
|
const ui = await loadInteractiveUi();
|
|
769
504
|
if (!lifecycle.isCurrent() || lifecycle.signal.aborted) return;
|
|
770
|
-
const tools = selectableTools();
|
|
771
505
|
await ui.showPlanLaunchMenu(ctx, {
|
|
772
|
-
statusText: "Status: Off
|
|
773
|
-
initialScreen,
|
|
774
|
-
getSelectedNames: () => snapshotPlanModeSelectedNames(tools, toolSelectionSnapshot()),
|
|
775
|
-
toolSummary: (selectedNames) =>
|
|
776
|
-
`When started: ${snapshotPlanModeToolNames(tools, selectedNames, toolSelectionSnapshot()).join(", ")}`,
|
|
777
|
-
tools: tools.map((tool) => {
|
|
778
|
-
const selectable = canSelectToolInPlanMode(tool);
|
|
779
|
-
const policy = toolPolicyLabel(tool);
|
|
780
|
-
const description = tool.description ?? "No description available";
|
|
781
|
-
return {
|
|
782
|
-
name: tool.name,
|
|
783
|
-
description: `${policy} · ${description}`,
|
|
784
|
-
searchText: [policy, description].join(" "),
|
|
785
|
-
disabled: !selectable,
|
|
786
|
-
disabledReason: selectable ? undefined : "Blocked by Plan-mode policy",
|
|
787
|
-
};
|
|
788
|
-
}),
|
|
506
|
+
statusText: "Status: Off.",
|
|
789
507
|
...lifecycle,
|
|
790
508
|
start: (signal) => {
|
|
791
509
|
if (signal.aborted || !lifecycle.isCurrent()) return;
|
|
792
510
|
enterPlanMode(ctx);
|
|
793
511
|
ctx.ui.notify("Plan mode enabled. I will explore and plan, but not modify files.", "info");
|
|
794
512
|
},
|
|
795
|
-
startWithTools: (names, signal) => {
|
|
796
|
-
if (signal.aborted || !lifecycle.isCurrent()) return;
|
|
797
|
-
state = {
|
|
798
|
-
...state,
|
|
799
|
-
selectedToolNames: filterAvailableSelectedToolNames(names, tools),
|
|
800
|
-
selectedToolKeys: undefined,
|
|
801
|
-
};
|
|
802
|
-
enterPlanMode(ctx);
|
|
803
|
-
ctx.ui.notify("Plan mode enabled with the selected tools.", "info");
|
|
804
|
-
},
|
|
805
513
|
settings: (signal) => showSettings(ctx, signal, lifecycle.isCurrent),
|
|
806
514
|
});
|
|
807
515
|
}
|
|
@@ -817,6 +525,7 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
817
525
|
if (!lifecycle.isCurrent() || lifecycle.signal.aborted) return;
|
|
818
526
|
await ui.showActiveImplementationMenu(ctx, {
|
|
819
527
|
statusText: planStatusText(),
|
|
528
|
+
...(state.planPath ? { planPathLine: `Plan file: ${state.planPath}` } : {}),
|
|
820
529
|
getExportDestination: () => planExports.getDestination(ctx),
|
|
821
530
|
signal: lifecycle.signal,
|
|
822
531
|
isCurrent: lifecycle.isCurrent,
|
|
@@ -828,8 +537,9 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
828
537
|
ctx.ui.notify("Plan mode enabled. I will explore and plan, but not modify files.", "info");
|
|
829
538
|
},
|
|
830
539
|
clear: () => {
|
|
831
|
-
exitPlanMode(ctx)
|
|
832
|
-
|
|
540
|
+
void exitPlanMode(ctx).then(() => {
|
|
541
|
+
ctx.ui.notify("Active implementation plan cleared.", "info");
|
|
542
|
+
});
|
|
833
543
|
},
|
|
834
544
|
});
|
|
835
545
|
}
|
|
@@ -843,7 +553,6 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
843
553
|
const ui = await loadInteractiveUi();
|
|
844
554
|
if (!isCurrent() || signal.aborted) return false;
|
|
845
555
|
const result = await ui.showPlanModeSettings(ctx, {
|
|
846
|
-
tools: selectableTools(),
|
|
847
556
|
signal,
|
|
848
557
|
isCurrent,
|
|
849
558
|
settingsPath: dependencies.settingsPath,
|
|
@@ -870,65 +579,6 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
870
579
|
};
|
|
871
580
|
}
|
|
872
581
|
|
|
873
|
-
function activatePlanModeTools() {
|
|
874
|
-
previousTools ??= withoutRequiredPlanModeTools(safeGetActiveTools());
|
|
875
|
-
applyPlanModeTools();
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
function applyPlanModeTools() {
|
|
879
|
-
pi.setActiveTools(planModeToolNames());
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
function planModeToolNames() {
|
|
883
|
-
const tools = selectableTools();
|
|
884
|
-
if (
|
|
885
|
-
tools.length === 0 &&
|
|
886
|
-
state.selectedToolNames === undefined &&
|
|
887
|
-
state.selectedToolKeys === undefined &&
|
|
888
|
-
settings.defaultPlanTools === undefined
|
|
889
|
-
) {
|
|
890
|
-
return ["read", "bash", PLAN_MODE_QUESTION_TOOL_NAME, PLAN_MODE_COMPLETE_TOOL_NAME];
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
const selectedNames = snapshotPlanModeSelectedNames(tools, toolSelectionSnapshot());
|
|
894
|
-
return withRequiredPlanModeTools(
|
|
895
|
-
tools
|
|
896
|
-
.filter((tool) => selectedNames.has(tool.name) && canSelectToolInPlanMode(tool))
|
|
897
|
-
.map((tool) => tool.name),
|
|
898
|
-
);
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
function toolSelectionSnapshot() {
|
|
902
|
-
return {
|
|
903
|
-
selectedToolNames: state.selectedToolNames,
|
|
904
|
-
selectedToolKeys: state.selectedToolKeys,
|
|
905
|
-
defaultPlanTools: settings.defaultPlanTools,
|
|
906
|
-
};
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
function selectableTools() {
|
|
910
|
-
return safeGetAllTools()
|
|
911
|
-
.filter(
|
|
912
|
-
(tool) =>
|
|
913
|
-
tool.name !== PLAN_MODE_QUESTION_TOOL_NAME && tool.name !== PLAN_MODE_COMPLETE_TOOL_NAME,
|
|
914
|
-
)
|
|
915
|
-
.sort(compareTools);
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
function safeGetAllTools() {
|
|
919
|
-
try {
|
|
920
|
-
return pi.getAllTools();
|
|
921
|
-
} catch {
|
|
922
|
-
return [];
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
function restoreTools() {
|
|
927
|
-
const restoredTools = previousTools ?? DEFAULT_TOOLS;
|
|
928
|
-
pi.setActiveTools(withoutRequiredPlanModeTools(restoredTools));
|
|
929
|
-
previousTools = undefined;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
582
|
function applyPlanThinkingLevel() {
|
|
933
583
|
if (state.manualThinkingLevel) {
|
|
934
584
|
if (pi.getThinkingLevel() !== state.manualThinkingLevel) {
|
|
@@ -976,19 +626,11 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
976
626
|
state = { ...state, appliedThinkingLevel: undefined, previousThinkingLevel: undefined };
|
|
977
627
|
}
|
|
978
628
|
|
|
979
|
-
function
|
|
980
|
-
const activeTools = safeGetActiveTools();
|
|
981
|
-
const filteredTools = withoutRequiredPlanModeTools(activeTools);
|
|
982
|
-
if (filteredTools.length !== activeTools.length) {
|
|
983
|
-
pi.setActiveTools(filteredTools);
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
function safeGetActiveTools() {
|
|
629
|
+
function resolveSessionPlanPath(ctx: ExtensionContext) {
|
|
988
630
|
try {
|
|
989
|
-
return
|
|
631
|
+
return planFilePathForSession(ctx.sessionManager.getSessionId());
|
|
990
632
|
} catch {
|
|
991
|
-
return
|
|
633
|
+
return planFilePathForSession(undefined);
|
|
992
634
|
}
|
|
993
635
|
}
|
|
994
636
|
|
|
@@ -997,7 +639,7 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
997
639
|
}
|
|
998
640
|
|
|
999
641
|
function updateUi(ctx: ExtensionContext) {
|
|
1000
|
-
updatePlanModeUi(ctx, state
|
|
642
|
+
updatePlanModeUi(ctx, state);
|
|
1001
643
|
}
|
|
1002
644
|
|
|
1003
645
|
function clearUi(ctx: ExtensionContext) {
|
|
@@ -1005,33 +647,12 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
1005
647
|
}
|
|
1006
648
|
|
|
1007
649
|
function planStatusText() {
|
|
1008
|
-
return formatPlanModeStatusText(state
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
function implementationOutcome() {
|
|
1012
|
-
return implementationRetentionPreview(configuredImplementationPlanRetention(settings));
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
function formatToolSummary() {
|
|
1016
|
-
const names = planModeToolNames();
|
|
1017
|
-
return `Tools: ${names.length > 0 ? names.join(", ") : "none"}`;
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
function toolByName(toolName: string) {
|
|
1021
|
-
return safeGetAllTools().find((candidate) => candidate.name === toolName);
|
|
650
|
+
return formatPlanModeStatusText(state);
|
|
1022
651
|
}
|
|
1023
652
|
}
|
|
1024
653
|
|
|
1025
654
|
export { completePlanArguments } from "./command.js";
|
|
1026
|
-
export {
|
|
1027
|
-
|
|
1028
|
-
latestAssistantText,
|
|
1029
|
-
parseProposedPlan,
|
|
1030
|
-
stripProposedPlanBlocks,
|
|
1031
|
-
stripProposedPlanBlocksFromMessage,
|
|
1032
|
-
} from "./message-transform.js";
|
|
1033
|
-
export { buildPlanModePrompt } from "./prompt.js";
|
|
655
|
+
export { planFilePathForSession, plansDirectory } from "./plan-file.js";
|
|
656
|
+
export { buildActivePlanPointer, buildPlanModePrompt } from "./prompt.js";
|
|
1034
657
|
export { normalizePlanModeQuestionParams } from "./question-tool.js";
|
|
1035
|
-
export { withoutPlanModeQuestionTool, withRequiredPlanModeTools } from "./required-tools.js";
|
|
1036
658
|
export { normalizePlanModeSettings, readPlanModeSettings } from "./settings.js";
|
|
1037
|
-
export { canSelectToolInPlanMode, classifyPlanModeTool, isSafeCommand } from "./tool-policy.js";
|