@narumitw/pi-plan-mode 0.13.0 → 0.14.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/README.md +25 -7
- package/package.json +2 -2
- package/src/message-transform.ts +120 -0
- package/src/plan-mode.ts +228 -472
- package/src/prompt.ts +58 -0
- package/src/question-tool.ts +207 -0
- package/src/settings.ts +68 -0
- package/src/tool-policy.ts +356 -0
package/src/plan-mode.ts
CHANGED
|
@@ -1,18 +1,48 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
ToolInfo,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import {
|
|
7
|
+
extractProposedPlan,
|
|
8
|
+
latestAssistantText,
|
|
9
|
+
parseProposedPlan,
|
|
10
|
+
messageContainsInactivePlanModeArtifact,
|
|
11
|
+
messageContainsLegacyPlanModeContextArtifact,
|
|
12
|
+
stripProposedPlanBlocksFromMessage,
|
|
13
|
+
} from "./message-transform.js";
|
|
14
|
+
import { buildPlanModePrompt } from "./prompt.js";
|
|
15
|
+
import {
|
|
16
|
+
askPlanModeQuestions,
|
|
17
|
+
normalizePlanModeQuestionParams,
|
|
18
|
+
PLAN_MODE_QUESTION_PARAMS,
|
|
19
|
+
PLAN_MODE_QUESTION_TOOL_NAME,
|
|
20
|
+
planModeQuestionAnswered,
|
|
21
|
+
planModeQuestionCancelled,
|
|
22
|
+
} from "./question-tool.js";
|
|
23
|
+
import {
|
|
24
|
+
canSelectToolInPlanMode,
|
|
25
|
+
classifyPlanModeTool,
|
|
26
|
+
isBuiltinTool,
|
|
27
|
+
isSafeCommand,
|
|
28
|
+
readCommand,
|
|
29
|
+
SAFE_BUILTIN_PLAN_TOOLS,
|
|
30
|
+
} from "./tool-policy.js";
|
|
31
|
+
import {
|
|
32
|
+
configuredThinkingLevel,
|
|
33
|
+
PLAN_MODE_THINKING_LEVELS,
|
|
34
|
+
readPlanModeSettings,
|
|
35
|
+
type PlanModeFixedThinkingLevel,
|
|
36
|
+
type PlanModeSettings,
|
|
37
|
+
} from "./settings.js";
|
|
2
38
|
|
|
3
39
|
const STATE_ENTRY_TYPE = "plan-mode-state";
|
|
4
40
|
const STATUS_KEY = "plan-mode";
|
|
5
41
|
const PLAN_WIDGET_KEY = "plan-mode-plan";
|
|
6
|
-
const PLAN_CONTEXT_MESSAGE_TYPE = "plan-mode-context";
|
|
7
42
|
const PROPOSED_PLAN_MESSAGE_TYPE = "proposed-plan";
|
|
8
|
-
const PLAN_MODE_QUESTION_TOOL_NAME = "plan_mode_question";
|
|
9
|
-
const PLAN_CONTEXT_MARKER = "[CODEX-LIKE PLAN MODE ACTIVE]";
|
|
10
|
-
const SAFE_BUILTIN_PLAN_TOOLS = new Set(["read", "bash", "grep", "find", "ls"]);
|
|
11
43
|
const BLOCKED_BUILTIN_TOOLS = new Set(["edit", "write"]);
|
|
12
44
|
const DEFAULT_TOOLS = ["read", "bash", "edit", "write"];
|
|
13
45
|
const TOOL_SELECTOR_PAGE_SIZE = 10;
|
|
14
|
-
const PROPOSED_PLAN_PATTERN = /<proposed_plan>\s*([\s\S]*?)\s*<\/proposed_plan>/i;
|
|
15
|
-
const PROPOSED_PLAN_BLOCK_PATTERN = /<proposed_plan>\s*[\s\S]*?\s*<\/proposed_plan>/gi;
|
|
16
46
|
|
|
17
47
|
interface CommandArgumentCompletion {
|
|
18
48
|
value: string;
|
|
@@ -26,6 +56,9 @@ interface PlanModeState {
|
|
|
26
56
|
awaitingAction: boolean;
|
|
27
57
|
selectedToolNames?: string[];
|
|
28
58
|
selectedToolKeys?: string[];
|
|
59
|
+
previousThinkingLevel?: PlanModeFixedThinkingLevel;
|
|
60
|
+
appliedThinkingLevel?: PlanModeFixedThinkingLevel;
|
|
61
|
+
manualThinkingLevel?: PlanModeFixedThinkingLevel;
|
|
29
62
|
}
|
|
30
63
|
|
|
31
64
|
type SessionEntry = {
|
|
@@ -45,146 +78,15 @@ type TextBlock = {
|
|
|
45
78
|
text?: string;
|
|
46
79
|
};
|
|
47
80
|
|
|
48
|
-
type PlanModeQuestionOption = {
|
|
49
|
-
label: string;
|
|
50
|
-
description?: string;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
type PlanModeQuestion = {
|
|
54
|
-
id: string;
|
|
55
|
-
header: string;
|
|
56
|
-
question: string;
|
|
57
|
-
options: PlanModeQuestionOption[];
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
type PlanModeQuestionParams = {
|
|
61
|
-
questions: PlanModeQuestion[];
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
type PlanModeQuestionAnswer = {
|
|
65
|
-
id: string;
|
|
66
|
-
header: string;
|
|
67
|
-
question: string;
|
|
68
|
-
answer: string;
|
|
69
|
-
wasCustom: boolean;
|
|
70
|
-
optionIndex?: number;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
type PlanModeQuestionReason = "cancelled" | "ui_unavailable" | "plan_mode_inactive" | "invalid_input";
|
|
74
|
-
|
|
75
|
-
type PlanModeQuestionDetails = {
|
|
76
|
-
cancelled: boolean;
|
|
77
|
-
reason?: PlanModeQuestionReason;
|
|
78
|
-
questions: PlanModeQuestion[];
|
|
79
|
-
answers?: PlanModeQuestionAnswer[];
|
|
80
|
-
};
|
|
81
|
-
|
|
82
81
|
const PLAN_COMMAND_COMPLETIONS: readonly CommandArgumentCompletion[] = [
|
|
83
82
|
{ value: "exit", label: "exit", description: "Leave Plan mode" },
|
|
84
83
|
{ value: "off", label: "off", description: "Leave Plan mode" },
|
|
85
84
|
{ value: "tools", label: "tools", description: "Select tools allowed in Plan mode" },
|
|
86
85
|
];
|
|
87
86
|
|
|
88
|
-
const PLAN_MODE_QUESTION_PARAMS = {
|
|
89
|
-
type: "object",
|
|
90
|
-
additionalProperties: false,
|
|
91
|
-
required: ["questions"],
|
|
92
|
-
properties: {
|
|
93
|
-
questions: {
|
|
94
|
-
type: "array",
|
|
95
|
-
minItems: 1,
|
|
96
|
-
maxItems: 3,
|
|
97
|
-
description: "Questions to show the user. Prefer 1 and do not exceed 3.",
|
|
98
|
-
items: {
|
|
99
|
-
type: "object",
|
|
100
|
-
additionalProperties: false,
|
|
101
|
-
required: ["id", "header", "question", "options"],
|
|
102
|
-
properties: {
|
|
103
|
-
id: {
|
|
104
|
-
type: "string",
|
|
105
|
-
description: "Stable identifier for mapping answers (snake_case).",
|
|
106
|
-
},
|
|
107
|
-
header: {
|
|
108
|
-
type: "string",
|
|
109
|
-
description: "Short header label shown in the UI (12 or fewer chars).",
|
|
110
|
-
},
|
|
111
|
-
question: {
|
|
112
|
-
type: "string",
|
|
113
|
-
description: "Single-sentence prompt shown to the user.",
|
|
114
|
-
},
|
|
115
|
-
options: {
|
|
116
|
-
type: "array",
|
|
117
|
-
minItems: 2,
|
|
118
|
-
maxItems: 4,
|
|
119
|
-
description:
|
|
120
|
-
"Provide 2-4 mutually exclusive choices. Put the recommended option first when there is a clear default.",
|
|
121
|
-
items: {
|
|
122
|
-
type: "object",
|
|
123
|
-
additionalProperties: false,
|
|
124
|
-
required: ["label", "description"],
|
|
125
|
-
properties: {
|
|
126
|
-
label: {
|
|
127
|
-
type: "string",
|
|
128
|
-
description: "User-facing label (1-5 words).",
|
|
129
|
-
},
|
|
130
|
-
description: {
|
|
131
|
-
type: "string",
|
|
132
|
-
description: "One short sentence explaining impact/tradeoff if selected.",
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
} as const;
|
|
142
|
-
|
|
143
|
-
const MUTATING_BASH_PATTERNS = [
|
|
144
|
-
/\brm\b/i,
|
|
145
|
-
/\brmdir\b/i,
|
|
146
|
-
/\bmv\b/i,
|
|
147
|
-
/\bcp\b/i,
|
|
148
|
-
/\bmkdir\b/i,
|
|
149
|
-
/\btouch\b/i,
|
|
150
|
-
/\bchmod\b/i,
|
|
151
|
-
/\bchown\b/i,
|
|
152
|
-
/\bchgrp\b/i,
|
|
153
|
-
/\bln\b/i,
|
|
154
|
-
/\btee\b/i,
|
|
155
|
-
/\btruncate\b/i,
|
|
156
|
-
/\bdd\b/i,
|
|
157
|
-
/(^|[^<])>(?!>)/,
|
|
158
|
-
/>>/,
|
|
159
|
-
/\bnpm\s+(install|uninstall|update|ci|link|publish|version)\b/i,
|
|
160
|
-
/\byarn\s+(add|remove|install|publish|upgrade)\b/i,
|
|
161
|
-
/\bpnpm\s+(add|remove|install|publish|update)\b/i,
|
|
162
|
-
/\bbun\s+(add|remove|install|update|publish)\b/i,
|
|
163
|
-
/\bpip\s+(install|uninstall)\b/i,
|
|
164
|
-
/\buv\s+(add|remove|sync|lock|pip\s+install)\b/i,
|
|
165
|
-
/\bgit\s+(add|commit|push|pull|merge|rebase|reset|checkout|switch|stash|cherry-pick|revert|tag|init|clone)\b/i,
|
|
166
|
-
/\bsudo\b/i,
|
|
167
|
-
/\bsu\b/i,
|
|
168
|
-
/\bkill\b/i,
|
|
169
|
-
/\bpkill\b/i,
|
|
170
|
-
/\bkillall\b/i,
|
|
171
|
-
/\breboot\b/i,
|
|
172
|
-
/\bshutdown\b/i,
|
|
173
|
-
/\bsystemctl\s+(start|stop|restart|enable|disable)\b/i,
|
|
174
|
-
/\bservice\s+\S+\s+(start|stop|restart)\b/i,
|
|
175
|
-
/\b(vim?|nano|emacs|code|subl)\b/i,
|
|
176
|
-
];
|
|
177
|
-
|
|
178
|
-
const SAFE_BASH_PATTERNS = [
|
|
179
|
-
/^\s*(cat|head|tail|less|more|grep|find|ls|pwd|echo|printf|wc|sort|uniq|diff|file|stat|du|df|tree|which|whereis|type|env|printenv|uname|whoami|id|date|uptime|ps|jq|awk|rg|fd|bat|eza)\b/i,
|
|
180
|
-
/^\s*sed\s+-n\b/i,
|
|
181
|
-
/^\s*git\s+(status|log|diff|show|branch|remote|config\s+--get|ls-files|grep)\b/i,
|
|
182
|
-
/^\s*npm\s+(list|ls|view|info|search|outdated|audit)\b/i,
|
|
183
|
-
/^\s*(node|python|python3|npm|tsc|biome|ruff|ty)\s+--version\b/i,
|
|
184
|
-
];
|
|
185
|
-
|
|
186
87
|
export default function planMode(pi: ExtensionAPI) {
|
|
187
88
|
let state: PlanModeState = { enabled: false, awaitingAction: false };
|
|
89
|
+
let settings: PlanModeSettings = { thinkingLevel: "inherit" };
|
|
188
90
|
let previousTools: string[] | undefined;
|
|
189
91
|
|
|
190
92
|
pi.registerFlag("plan", {
|
|
@@ -267,25 +169,65 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
267
169
|
},
|
|
268
170
|
});
|
|
269
171
|
|
|
270
|
-
pi.on("session_start", (_event, ctx) => {
|
|
172
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
173
|
+
settings = { thinkingLevel: "inherit" };
|
|
174
|
+
const loadedSettings = await readPlanModeSettings();
|
|
175
|
+
if (loadedSettings.kind === "loaded") settings = loadedSettings.settings;
|
|
176
|
+
else if (loadedSettings.kind === "invalid") {
|
|
177
|
+
ctx.ui.notify(`pi-plan-mode settings ignored: ${loadedSettings.reason}`, "warning");
|
|
178
|
+
}
|
|
271
179
|
restoreState(ctx);
|
|
272
180
|
if (pi.getFlag("plan") === true) state.enabled = true;
|
|
273
|
-
if (state.enabled)
|
|
274
|
-
|
|
181
|
+
if (state.enabled) {
|
|
182
|
+
activatePlanModeTools();
|
|
183
|
+
applyPlanThinkingLevel();
|
|
184
|
+
} else deactivatePlanModeQuestionTool();
|
|
275
185
|
updateUi(ctx);
|
|
276
186
|
});
|
|
277
187
|
|
|
188
|
+
pi.on("thinking_level_select", (event) => {
|
|
189
|
+
if (!state.enabled || !state.appliedThinkingLevel) return;
|
|
190
|
+
if (event.level !== state.appliedThinkingLevel) {
|
|
191
|
+
state = {
|
|
192
|
+
...state,
|
|
193
|
+
manualThinkingLevel: event.level,
|
|
194
|
+
previousThinkingLevel: undefined,
|
|
195
|
+
appliedThinkingLevel: undefined,
|
|
196
|
+
};
|
|
197
|
+
persistState();
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
278
201
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
202
|
+
captureManualThinkingLevel();
|
|
279
203
|
persistState();
|
|
204
|
+
if (state.enabled) {
|
|
205
|
+
restoreTools();
|
|
206
|
+
restoreThinkingLevel();
|
|
207
|
+
}
|
|
280
208
|
clearUi(ctx);
|
|
281
209
|
});
|
|
282
210
|
|
|
283
211
|
pi.on("tool_call", async (event) => {
|
|
284
212
|
if (!state.enabled) return;
|
|
285
|
-
if (
|
|
213
|
+
if (event.toolName === "update_plan") {
|
|
214
|
+
return {
|
|
215
|
+
block: true,
|
|
216
|
+
reason:
|
|
217
|
+
"Plan mode blocks update_plan because it tracks execution progress rather than conversational planning.",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const calledTool = toolByName(event.toolName);
|
|
221
|
+
if (calledTool && classifyPlanModeTool(calledTool) === "blocked") {
|
|
286
222
|
return {
|
|
287
223
|
block: true,
|
|
288
|
-
reason: `Plan mode blocks built-in
|
|
224
|
+
reason: `Plan mode blocks built-in tool '${event.toolName}' because its policy class is blocked.`,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (!calledTool && BLOCKED_BUILTIN_TOOLS.has(event.toolName)) {
|
|
228
|
+
return {
|
|
229
|
+
block: true,
|
|
230
|
+
reason: `Plan mode blocks built-in tool '${event.toolName}' because its metadata is unavailable.`,
|
|
289
231
|
};
|
|
290
232
|
}
|
|
291
233
|
if (event.toolName !== "bash" || !isBuiltinToolName(event.toolName)) return;
|
|
@@ -328,12 +270,16 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
328
270
|
if (!state.enabled) return;
|
|
329
271
|
|
|
330
272
|
const text = latestAssistantText(event.messages);
|
|
331
|
-
const
|
|
332
|
-
if (
|
|
273
|
+
const parsedPlan = parseProposedPlan(text);
|
|
274
|
+
if (parsedPlan.kind !== "valid") {
|
|
275
|
+
if (parsedPlan.kind !== "absent") {
|
|
276
|
+
ctx.ui.notify(invalidPlanMessage(parsedPlan.kind), "warning");
|
|
277
|
+
}
|
|
333
278
|
persistState();
|
|
334
279
|
updateUi(ctx);
|
|
335
280
|
return;
|
|
336
281
|
}
|
|
282
|
+
const proposedPlan = parsedPlan.plan;
|
|
337
283
|
|
|
338
284
|
state = { ...state, latestPlan: proposedPlan, awaitingAction: true };
|
|
339
285
|
persistState();
|
|
@@ -359,6 +305,7 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
359
305
|
if (!state.enabled) previousTools = withoutPlanModeQuestionTool(safeGetActiveTools());
|
|
360
306
|
state = { ...state, enabled: true, awaitingAction: false };
|
|
361
307
|
activatePlanModeTools();
|
|
308
|
+
applyPlanThinkingLevel();
|
|
362
309
|
persistState();
|
|
363
310
|
updateUi(ctx);
|
|
364
311
|
}
|
|
@@ -369,20 +316,37 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
369
316
|
if (!wasEnabled) {
|
|
370
317
|
ctx.ui.notify("Plan mode enabled. I will explore and plan, but not modify files.", "info");
|
|
371
318
|
}
|
|
372
|
-
sendPlanModeUserMessage(prompt, ctx);
|
|
319
|
+
if (!sendPlanModeUserMessage(prompt, ctx) && !wasEnabled) exitPlanMode(ctx);
|
|
373
320
|
}
|
|
374
321
|
|
|
375
322
|
function exitPlanMode(ctx: ExtensionContext) {
|
|
376
323
|
const wasEnabled = state.enabled;
|
|
377
|
-
state = {
|
|
378
|
-
|
|
324
|
+
state = {
|
|
325
|
+
...state,
|
|
326
|
+
enabled: false,
|
|
327
|
+
latestPlan: undefined,
|
|
328
|
+
awaitingAction: false,
|
|
329
|
+
manualThinkingLevel: undefined,
|
|
330
|
+
};
|
|
331
|
+
if (wasEnabled) {
|
|
332
|
+
restoreTools();
|
|
333
|
+
restoreThinkingLevel();
|
|
334
|
+
state = { ...state, manualThinkingLevel: undefined };
|
|
335
|
+
}
|
|
379
336
|
persistState();
|
|
380
337
|
updateUi(ctx);
|
|
381
338
|
}
|
|
382
339
|
|
|
383
340
|
function sendPlanModeUserMessage(message: string, ctx: ExtensionContext) {
|
|
384
|
-
|
|
385
|
-
|
|
341
|
+
try {
|
|
342
|
+
if (ctx.isIdle()) pi.sendUserMessage(message);
|
|
343
|
+
else pi.sendUserMessage(message, { deliverAs: "followUp" });
|
|
344
|
+
return true;
|
|
345
|
+
} catch (error: unknown) {
|
|
346
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
347
|
+
ctx.ui.notify(`Unable to send Plan-mode message: ${detail}`, "error");
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
386
350
|
}
|
|
387
351
|
|
|
388
352
|
function scheduleAfterCurrentAgentRun(task: () => Promise<void> | void) {
|
|
@@ -403,10 +367,16 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
403
367
|
return;
|
|
404
368
|
}
|
|
405
369
|
|
|
406
|
-
sendPlanModeUserMessage(
|
|
370
|
+
const sent = sendPlanModeUserMessage(
|
|
407
371
|
`Plan mode is now disabled. Full tool access is restored. Implement this proposed plan now:\n\n${plan}`,
|
|
408
372
|
ctx,
|
|
409
373
|
);
|
|
374
|
+
if (!sent) {
|
|
375
|
+
enterPlanMode(ctx);
|
|
376
|
+
state = { ...state, latestPlan: plan, awaitingAction: true };
|
|
377
|
+
persistState();
|
|
378
|
+
updateUi(ctx);
|
|
379
|
+
}
|
|
410
380
|
}
|
|
411
381
|
|
|
412
382
|
async function showPlanMenu(ctx: ExtensionContext) {
|
|
@@ -596,11 +566,58 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
596
566
|
}
|
|
597
567
|
|
|
598
568
|
function restoreTools() {
|
|
599
|
-
const restoredTools = previousTools
|
|
569
|
+
const restoredTools = previousTools ?? DEFAULT_TOOLS;
|
|
600
570
|
pi.setActiveTools(withoutPlanModeQuestionTool(restoredTools));
|
|
601
571
|
previousTools = undefined;
|
|
602
572
|
}
|
|
603
573
|
|
|
574
|
+
function applyPlanThinkingLevel() {
|
|
575
|
+
if (state.manualThinkingLevel) {
|
|
576
|
+
if (pi.getThinkingLevel() !== state.manualThinkingLevel) {
|
|
577
|
+
pi.setThinkingLevel(state.manualThinkingLevel);
|
|
578
|
+
}
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const configured = configuredThinkingLevel(settings);
|
|
582
|
+
if (!configured) {
|
|
583
|
+
state = {
|
|
584
|
+
...state,
|
|
585
|
+
previousThinkingLevel: undefined,
|
|
586
|
+
appliedThinkingLevel: undefined,
|
|
587
|
+
};
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const current = pi.getThinkingLevel();
|
|
591
|
+
if (!state.appliedThinkingLevel) state.previousThinkingLevel = current;
|
|
592
|
+
if (current !== configured) pi.setThinkingLevel(configured);
|
|
593
|
+
state.appliedThinkingLevel = pi.getThinkingLevel();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function captureManualThinkingLevel() {
|
|
597
|
+
if (!state.appliedThinkingLevel) return;
|
|
598
|
+
const current = pi.getThinkingLevel();
|
|
599
|
+
if (current === state.appliedThinkingLevel) return;
|
|
600
|
+
state = {
|
|
601
|
+
...state,
|
|
602
|
+
manualThinkingLevel: current,
|
|
603
|
+
previousThinkingLevel: undefined,
|
|
604
|
+
appliedThinkingLevel: undefined,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function restoreThinkingLevel() {
|
|
609
|
+
captureManualThinkingLevel();
|
|
610
|
+
const { appliedThinkingLevel, previousThinkingLevel } = state;
|
|
611
|
+
if (
|
|
612
|
+
appliedThinkingLevel &&
|
|
613
|
+
previousThinkingLevel &&
|
|
614
|
+
pi.getThinkingLevel() === appliedThinkingLevel
|
|
615
|
+
) {
|
|
616
|
+
pi.setThinkingLevel(previousThinkingLevel);
|
|
617
|
+
}
|
|
618
|
+
state = { ...state, appliedThinkingLevel: undefined, previousThinkingLevel: undefined };
|
|
619
|
+
}
|
|
620
|
+
|
|
604
621
|
function deactivatePlanModeQuestionTool() {
|
|
605
622
|
const activeTools = safeGetActiveTools();
|
|
606
623
|
const filteredTools = withoutPlanModeQuestionTool(activeTools);
|
|
@@ -622,18 +639,29 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
622
639
|
}
|
|
623
640
|
|
|
624
641
|
function restoreState(ctx: ExtensionContext) {
|
|
642
|
+
state = { enabled: false, awaitingAction: false };
|
|
625
643
|
const entries = ctx.sessionManager.getEntries() as SessionEntry[];
|
|
626
644
|
const entry = entries
|
|
627
645
|
.filter((candidate) => candidate.type === "custom" && candidate.customType === STATE_ENTRY_TYPE)
|
|
628
646
|
.pop();
|
|
629
|
-
if (!entry?.data) return;
|
|
630
|
-
const enabled = entry.data.enabled
|
|
647
|
+
if (!isRecord(entry?.data)) return;
|
|
648
|
+
const enabled = entry.data.enabled === true;
|
|
631
649
|
state = {
|
|
632
650
|
enabled,
|
|
633
|
-
latestPlan:
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
651
|
+
latestPlan:
|
|
652
|
+
enabled && typeof entry.data.latestPlan === "string" ? entry.data.latestPlan : undefined,
|
|
653
|
+
awaitingAction: enabled && entry.data.awaitingAction === true,
|
|
654
|
+
selectedToolNames: stringArray(entry.data.selectedToolNames),
|
|
655
|
+
selectedToolKeys: stringArray(entry.data.selectedToolKeys),
|
|
656
|
+
previousThinkingLevel: enabled
|
|
657
|
+
? fixedThinkingLevel(entry.data.previousThinkingLevel)
|
|
658
|
+
: undefined,
|
|
659
|
+
appliedThinkingLevel: enabled
|
|
660
|
+
? fixedThinkingLevel(entry.data.appliedThinkingLevel)
|
|
661
|
+
: undefined,
|
|
662
|
+
manualThinkingLevel: enabled
|
|
663
|
+
? fixedThinkingLevel(entry.data.manualThinkingLevel)
|
|
664
|
+
: undefined,
|
|
637
665
|
};
|
|
638
666
|
}
|
|
639
667
|
|
|
@@ -677,12 +705,6 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
677
705
|
return `Tools: ${names.length > 0 ? names.join(", ") : "none"}`;
|
|
678
706
|
}
|
|
679
707
|
|
|
680
|
-
function isBlockedBuiltinToolName(toolName: string) {
|
|
681
|
-
if (!BLOCKED_BUILTIN_TOOLS.has(toolName)) return false;
|
|
682
|
-
const tool = toolByName(toolName);
|
|
683
|
-
return tool ? isBuiltinTool(tool) : true;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
708
|
function isBuiltinToolName(toolName: string) {
|
|
687
709
|
const tool = toolByName(toolName);
|
|
688
710
|
return tool ? isBuiltinTool(tool) : toolName === "bash";
|
|
@@ -693,10 +715,6 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
693
715
|
}
|
|
694
716
|
}
|
|
695
717
|
|
|
696
|
-
function isBuiltinTool(tool: ToolInfo) {
|
|
697
|
-
return tool.sourceInfo.source === "builtin";
|
|
698
|
-
}
|
|
699
|
-
|
|
700
718
|
export function completePlanArguments(argumentPrefix: string): CommandArgumentCompletion[] | null {
|
|
701
719
|
const prefix = argumentPrefix.trimStart().toLowerCase();
|
|
702
720
|
if (prefix === "") return [...PLAN_COMMAND_COMPLETIONS];
|
|
@@ -706,11 +724,6 @@ export function completePlanArguments(argumentPrefix: string): CommandArgumentCo
|
|
|
706
724
|
return matches.length > 0 ? [...matches] : null;
|
|
707
725
|
}
|
|
708
726
|
|
|
709
|
-
export function canSelectToolInPlanMode(tool: ToolInfo) {
|
|
710
|
-
if (isBuiltinTool(tool)) return SAFE_BUILTIN_PLAN_TOOLS.has(tool.name);
|
|
711
|
-
return true;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
727
|
function toolNameFromLegacyKey(key: string, tools: ToolInfo[]) {
|
|
715
728
|
const directName = tools.find((tool) => tool.name === key)?.name;
|
|
716
729
|
if (directName) return directName;
|
|
@@ -731,11 +744,11 @@ function formatToolChoice(tool: ToolInfo, selected: boolean, index: number) {
|
|
|
731
744
|
}
|
|
732
745
|
|
|
733
746
|
function toolPolicyLabel(tool: ToolInfo) {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
return `user
|
|
747
|
+
const policy = classifyPlanModeTool(tool);
|
|
748
|
+
if (policy === "read-only") return "built-in read-only";
|
|
749
|
+
if (policy === "limited") return "built-in limited";
|
|
750
|
+
if (policy === "blocked") return "built-in blocked";
|
|
751
|
+
return `user opt-in: ${toolSourceLabel(tool)}`;
|
|
739
752
|
}
|
|
740
753
|
|
|
741
754
|
function toolSourceLabel(tool: ToolInfo) {
|
|
@@ -748,307 +761,50 @@ function unique(values: string[]) {
|
|
|
748
761
|
return Array.from(new Set(values));
|
|
749
762
|
}
|
|
750
763
|
|
|
751
|
-
export function withRequiredPlanModeTools(toolNames: string[]) {
|
|
752
|
-
return unique([...withoutPlanModeQuestionTool(toolNames), PLAN_MODE_QUESTION_TOOL_NAME]);
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
export function withoutPlanModeQuestionTool(toolNames: string[]) {
|
|
756
|
-
return toolNames.filter((toolName) => toolName !== PLAN_MODE_QUESTION_TOOL_NAME);
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
type NormalizePlanModeQuestionParamsResult =
|
|
760
|
-
| { ok: true; questions: PlanModeQuestion[] }
|
|
761
|
-
| { ok: false; error: string };
|
|
762
|
-
|
|
763
|
-
export function normalizePlanModeQuestionParams(input: unknown): NormalizePlanModeQuestionParamsResult {
|
|
764
|
-
if (!isRecord(input) || !Array.isArray(input.questions)) {
|
|
765
|
-
return { ok: false, error: "questions must be an array" };
|
|
766
|
-
}
|
|
767
|
-
if (input.questions.length < 1 || input.questions.length > 3) {
|
|
768
|
-
return { ok: false, error: "questions must contain 1-3 items" };
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
const questions: PlanModeQuestion[] = [];
|
|
772
|
-
for (const [questionIndex, rawQuestion] of input.questions.entries()) {
|
|
773
|
-
if (!isRecord(rawQuestion)) {
|
|
774
|
-
return { ok: false, error: `question ${questionIndex + 1} must be an object` };
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
const id = stringField(rawQuestion.id);
|
|
778
|
-
const header = stringField(rawQuestion.header);
|
|
779
|
-
const question = stringField(rawQuestion.question);
|
|
780
|
-
if (!id || !header || !question) {
|
|
781
|
-
return {
|
|
782
|
-
ok: false,
|
|
783
|
-
error: `question ${questionIndex + 1} requires non-empty id, header, and question`,
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
if (!Array.isArray(rawQuestion.options)) {
|
|
788
|
-
return { ok: false, error: `question ${questionIndex + 1} options must be an array` };
|
|
789
|
-
}
|
|
790
|
-
if (rawQuestion.options.length < 2 || rawQuestion.options.length > 4) {
|
|
791
|
-
return { ok: false, error: `question ${questionIndex + 1} options must contain 2-4 items` };
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
const options: PlanModeQuestionOption[] = [];
|
|
795
|
-
for (const [optionIndex, rawOption] of rawQuestion.options.entries()) {
|
|
796
|
-
if (!isRecord(rawOption)) {
|
|
797
|
-
return {
|
|
798
|
-
ok: false,
|
|
799
|
-
error: `question ${questionIndex + 1} option ${optionIndex + 1} must be an object`,
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
const label = stringField(rawOption.label);
|
|
804
|
-
if (!label) {
|
|
805
|
-
return {
|
|
806
|
-
ok: false,
|
|
807
|
-
error: `question ${questionIndex + 1} option ${optionIndex + 1} requires a label`,
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
const description = stringField(rawOption.description);
|
|
811
|
-
if (!description) {
|
|
812
|
-
return {
|
|
813
|
-
ok: false,
|
|
814
|
-
error: `question ${questionIndex + 1} option ${optionIndex + 1} requires a description`,
|
|
815
|
-
};
|
|
816
|
-
}
|
|
817
|
-
options.push({ label, description });
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
questions.push({ id, header, question, options });
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
return { ok: true, questions };
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
async function askPlanModeQuestions(
|
|
827
|
-
questions: PlanModeQuestion[],
|
|
828
|
-
ctx: ExtensionContext,
|
|
829
|
-
): Promise<PlanModeQuestionAnswer[] | undefined> {
|
|
830
|
-
const answers: PlanModeQuestionAnswer[] = [];
|
|
831
|
-
for (const question of questions) {
|
|
832
|
-
const choices = question.options.map(formatPlanModeQuestionChoice);
|
|
833
|
-
const otherChoice = `${question.options.length + 1}. Other (free-form)`;
|
|
834
|
-
const choice = await ctx.ui.select(`${question.header}: ${question.question}`, [...choices, otherChoice]);
|
|
835
|
-
if (!choice) return undefined;
|
|
836
|
-
|
|
837
|
-
if (choice === otherChoice) {
|
|
838
|
-
const customAnswer = (await ctx.ui.editor(question.question, ""))?.trim();
|
|
839
|
-
if (!customAnswer) return undefined;
|
|
840
|
-
answers.push({
|
|
841
|
-
id: question.id,
|
|
842
|
-
header: question.header,
|
|
843
|
-
question: question.question,
|
|
844
|
-
answer: customAnswer,
|
|
845
|
-
wasCustom: true,
|
|
846
|
-
});
|
|
847
|
-
continue;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
const optionIndex = choices.indexOf(choice);
|
|
851
|
-
const option = question.options[optionIndex];
|
|
852
|
-
if (!option) return undefined;
|
|
853
|
-
answers.push({
|
|
854
|
-
id: question.id,
|
|
855
|
-
header: question.header,
|
|
856
|
-
question: question.question,
|
|
857
|
-
answer: option.label,
|
|
858
|
-
wasCustom: false,
|
|
859
|
-
optionIndex: optionIndex + 1,
|
|
860
|
-
});
|
|
861
|
-
}
|
|
862
|
-
return answers;
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
function formatPlanModeQuestionChoice(option: PlanModeQuestionOption, index: number) {
|
|
866
|
-
return `${index + 1}. ${option.label}${option.description ? ` — ${option.description}` : ""}`;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
function planModeQuestionAnswered(questions: PlanModeQuestion[], answers: PlanModeQuestionAnswer[]) {
|
|
870
|
-
return {
|
|
871
|
-
content: [{ type: "text" as const, text: formatPlanModeQuestionPayload({ cancelled: false, answers }) }],
|
|
872
|
-
details: { cancelled: false, questions, answers } satisfies PlanModeQuestionDetails,
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
function planModeQuestionCancelled(
|
|
877
|
-
questions: PlanModeQuestion[],
|
|
878
|
-
reason: PlanModeQuestionReason,
|
|
879
|
-
message: string,
|
|
880
|
-
) {
|
|
881
|
-
return {
|
|
882
|
-
content: [{ type: "text" as const, text: formatPlanModeQuestionPayload({ cancelled: true, reason, message }) }],
|
|
883
|
-
details: { cancelled: true, reason, questions } satisfies PlanModeQuestionDetails,
|
|
884
|
-
};
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
function formatPlanModeQuestionPayload(payload: {
|
|
888
|
-
cancelled: boolean;
|
|
889
|
-
reason?: PlanModeQuestionReason;
|
|
890
|
-
message?: string;
|
|
891
|
-
answers?: PlanModeQuestionAnswer[];
|
|
892
|
-
}) {
|
|
893
|
-
return JSON.stringify(payload, null, 2);
|
|
894
|
-
}
|
|
895
|
-
|
|
896
764
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
897
|
-
return typeof value === "object" && value !== null;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
function stringField(value: unknown) {
|
|
901
|
-
return typeof value === "string" ? value.trim() : undefined;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
function buildPlanModePrompt() {
|
|
905
|
-
return `${PLAN_CONTEXT_MARKER}
|
|
906
|
-
# Plan Mode (Conversational)
|
|
907
|
-
|
|
908
|
-
You are in Plan Mode, a Codex-like collaboration mode for producing a decision-complete implementation plan. Chat your way to the plan before finalizing it. A final plan must leave no implementation decisions unresolved.
|
|
909
|
-
|
|
910
|
-
## Mode rules
|
|
911
|
-
|
|
912
|
-
- Stay in Plan Mode until a developer or extension explicitly exits it.
|
|
913
|
-
- Treat requests to implement as requests to plan the implementation; do not edit files or carry out the plan.
|
|
914
|
-
- Do not use update_plan/TODO tooling in Plan Mode; Plan Mode is conversational planning, not execution progress tracking.
|
|
915
|
-
- Plan Mode manages built-in tool safety only. Non-built-in tools are disabled by default and may be enabled by the user at their own risk.
|
|
916
|
-
- Do not perform mutating actions: no edit/write tools, no patching, no formatting that rewrites files, no dependency installation, no commits, no migrations.
|
|
917
|
-
|
|
918
|
-
## Phase 1 — Ground in the environment
|
|
919
|
-
|
|
920
|
-
- Explore first and ask second. Use non-mutating exploration to read files, search, inspect configuration, run read-only checks, and resolve discoverable facts.
|
|
921
|
-
- Before asking the user any question, perform at least one targeted non-mutating exploration pass unless no local environment or repository is available.
|
|
922
|
-
- Do not ask questions that can be answered from repository or system truth. Ask only when multiple plausible choices remain, a needed identifier/context is missing, or the ambiguity is product intent.
|
|
923
|
-
|
|
924
|
-
## Phase 2 — Intent chat
|
|
925
|
-
|
|
926
|
-
- Keep asking until you can clearly state the goal, success criteria, in/out of scope, constraints, current state, and key preferences/tradeoffs.
|
|
927
|
-
- Bias toward questions over guessing: if a high-impact ambiguity remains, do not produce a proposed plan yet.
|
|
928
|
-
|
|
929
|
-
## Phase 3 — Implementation chat
|
|
930
|
-
|
|
931
|
-
- Once intent is stable, keep asking until the spec is decision-complete: approach, interfaces, data flow, edge cases/failure modes, testing and acceptance criteria, and any migration or compatibility constraints.
|
|
932
|
-
- Use plan_mode_question for important preferences, tradeoffs, or assumption locks that cannot be discovered by non-mutating exploration. Ask 1-3 concise questions with 2-4 meaningful options. Do not include filler options.
|
|
933
|
-
- If plan_mode_question returns cancelled or ui_unavailable, do not jump straight to a final plan when the missing answer is high impact. Ask one concise plain-text question or proceed only with a clearly stated low-risk assumption.
|
|
934
|
-
|
|
935
|
-
## Finalization rule
|
|
936
|
-
|
|
937
|
-
Only output the final plan when it is decision-complete and leaves no decisions to the implementer. When presenting the official plan, output exactly one proposed plan block and keep the tags exactly as shown:
|
|
938
|
-
|
|
939
|
-
<proposed_plan>
|
|
940
|
-
# Title
|
|
941
|
-
|
|
942
|
-
## Summary
|
|
943
|
-
...
|
|
944
|
-
|
|
945
|
-
## Key Changes
|
|
946
|
-
...
|
|
947
|
-
|
|
948
|
-
## Test Plan
|
|
949
|
-
...
|
|
950
|
-
|
|
951
|
-
## Assumptions
|
|
952
|
-
...
|
|
953
|
-
</proposed_plan>
|
|
954
|
-
|
|
955
|
-
Keep the proposed plan concise, human and agent digestible, and free of open decisions. Do not ask "should I proceed?" in the final output; the Plan-mode ready menu handles implementation, staying in Plan mode, or exit.`;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
function readCommand(input: unknown) {
|
|
959
|
-
const command = input as { command?: unknown } | undefined;
|
|
960
|
-
return typeof command?.command === "string" ? command.command : "";
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
export function isSafeCommand(command: string) {
|
|
964
|
-
const trimmed = command.trim();
|
|
965
|
-
if (!trimmed) return false;
|
|
966
|
-
if (MUTATING_BASH_PATTERNS.some((pattern) => pattern.test(trimmed))) return false;
|
|
967
|
-
return SAFE_BASH_PATTERNS.some((pattern) => pattern.test(trimmed));
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
export function extractProposedPlan(text: string) {
|
|
971
|
-
const match = PROPOSED_PLAN_PATTERN.exec(text);
|
|
972
|
-
return match?.[1]?.trim();
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
export function latestAssistantText(messages: unknown) {
|
|
976
|
-
if (!Array.isArray(messages)) return "";
|
|
977
|
-
for (const entry of [...messages].reverse()) {
|
|
978
|
-
const message = (entry as { message?: SessionMessage })?.message ?? (entry as SessionMessage);
|
|
979
|
-
if (message?.role !== "assistant") continue;
|
|
980
|
-
const text = messageText(message);
|
|
981
|
-
if (text) return text;
|
|
982
|
-
}
|
|
983
|
-
return "";
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
function messageContainsLegacyPlanModeContextArtifact(message: unknown) {
|
|
987
|
-
const candidate = unwrapSessionMessage(message);
|
|
988
|
-
return candidate.customType === PLAN_CONTEXT_MESSAGE_TYPE;
|
|
765
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
989
766
|
}
|
|
990
767
|
|
|
991
|
-
function
|
|
992
|
-
|
|
993
|
-
|
|
768
|
+
function stringArray(value: unknown) {
|
|
769
|
+
return Array.isArray(value) && value.every((item): item is string => typeof item === "string")
|
|
770
|
+
? unique(value)
|
|
771
|
+
: undefined;
|
|
994
772
|
}
|
|
995
773
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
if (isSessionMessageEntry(message)) {
|
|
1004
|
-
return { ...message, message: { ...candidate, content } };
|
|
1005
|
-
}
|
|
1006
|
-
return { ...candidate, content } as T;
|
|
774
|
+
function fixedThinkingLevel(value: unknown): PlanModeFixedThinkingLevel | undefined {
|
|
775
|
+
return typeof value === "string" &&
|
|
776
|
+
value !== "inherit" &&
|
|
777
|
+
PLAN_MODE_THINKING_LEVELS.includes(value as (typeof PLAN_MODE_THINKING_LEVELS)[number])
|
|
778
|
+
? (value as PlanModeFixedThinkingLevel)
|
|
779
|
+
: undefined;
|
|
1007
780
|
}
|
|
1008
781
|
|
|
1009
|
-
function
|
|
1010
|
-
|
|
1011
|
-
return (entry.message ?? message) as { role?: string; customType?: string; content?: unknown };
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
function isSessionMessageEntry<T>(message: T): message is T & { message: SessionMessage } {
|
|
1015
|
-
return typeof message === "object" && message !== null && "message" in message;
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
function stripProposedPlanBlocksFromContent(content: unknown) {
|
|
1019
|
-
if (typeof content === "string") return stripProposedPlanBlocks(content);
|
|
1020
|
-
if (!Array.isArray(content)) return content;
|
|
1021
|
-
|
|
1022
|
-
let changed = false;
|
|
1023
|
-
const nextContent = content.map((block) => {
|
|
1024
|
-
const textBlock = block as TextBlock;
|
|
1025
|
-
if (textBlock.type !== "text" || typeof textBlock.text !== "string") return block;
|
|
1026
|
-
|
|
1027
|
-
const text = stripProposedPlanBlocks(textBlock.text);
|
|
1028
|
-
if (text === textBlock.text) return block;
|
|
1029
|
-
|
|
1030
|
-
changed = true;
|
|
1031
|
-
return { ...textBlock, text };
|
|
1032
|
-
});
|
|
1033
|
-
return changed ? nextContent : content;
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
export function stripProposedPlanBlocks(text: string) {
|
|
1037
|
-
return text.replace(PROPOSED_PLAN_BLOCK_PATTERN, "");
|
|
782
|
+
export function withRequiredPlanModeTools(toolNames: string[]) {
|
|
783
|
+
return unique([...withoutPlanModeQuestionTool(toolNames), PLAN_MODE_QUESTION_TOOL_NAME]);
|
|
1038
784
|
}
|
|
1039
785
|
|
|
1040
|
-
function
|
|
1041
|
-
return
|
|
786
|
+
export function withoutPlanModeQuestionTool(toolNames: string[]) {
|
|
787
|
+
return toolNames.filter((toolName) => toolName !== PLAN_MODE_QUESTION_TOOL_NAME);
|
|
1042
788
|
}
|
|
1043
789
|
|
|
1044
|
-
function
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
790
|
+
function invalidPlanMessage(kind: "empty" | "multiple" | "malformed" | "unclosed") {
|
|
791
|
+
const detail = {
|
|
792
|
+
empty: "the block is empty",
|
|
793
|
+
multiple: "more than one plan block was produced",
|
|
794
|
+
malformed: "the tags must be on their own lines",
|
|
795
|
+
unclosed: "the closing tag is missing",
|
|
796
|
+
}[kind];
|
|
797
|
+
return `Proposed plan is not ready: ${detail}. Continue Plan mode and produce one complete non-empty <proposed_plan> block.`;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export {
|
|
801
|
+
extractProposedPlan,
|
|
802
|
+
latestAssistantText,
|
|
803
|
+
parseProposedPlan,
|
|
804
|
+
stripProposedPlanBlocks,
|
|
805
|
+
stripProposedPlanBlocksFromMessage,
|
|
806
|
+
} from "./message-transform.js";
|
|
807
|
+
export { buildPlanModePrompt } from "./prompt.js";
|
|
808
|
+
export { normalizePlanModeQuestionParams } from "./question-tool.js";
|
|
809
|
+
export { normalizePlanModeSettings, readPlanModeSettings } from "./settings.js";
|
|
810
|
+
export { canSelectToolInPlanMode, classifyPlanModeTool, isSafeCommand } from "./tool-policy.js";
|