@zhushanwen/pi-plan 0.3.15 → 0.4.1
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/package.json +3 -3
- package/src/command.ts +6 -27
- package/src/index.ts +2 -9
- package/src/tool.ts +187 -114
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-plan",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Lightweight plan mode for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"@earendil-works/pi-coding-agent": ">=0.73.0",
|
|
22
22
|
"typebox": "*",
|
|
23
23
|
"@earendil-works/pi-ai": "^0.84.4",
|
|
24
|
-
"@zhushanwen/pi-goal": "0.
|
|
24
|
+
"@zhushanwen/pi-goal": "0.12.1"
|
|
25
25
|
},
|
|
26
26
|
"peerDependenciesMeta": {
|
|
27
27
|
"@earendil-works/pi-ai": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"templates/"
|
|
39
39
|
],
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@zhushanwen/pi-extension-logger": "0.
|
|
41
|
+
"@zhushanwen/pi-extension-logger": "0.4.1"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"typecheck": "npx tsc --noEmit",
|
package/src/command.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
|
|
6
|
-
import type { PlanSessionMap } from "./state.js";
|
|
6
|
+
import type { PlanSessionMap, PlanState } from "./state.js";
|
|
7
7
|
import { getPlanState, persistPlanState, resetPlanState } from "./state.js";
|
|
8
8
|
import { updatePlanWidget } from "./widget.js";
|
|
9
9
|
|
|
@@ -88,7 +88,7 @@ async function handleAbort(
|
|
|
88
88
|
sessions: PlanSessionMap,
|
|
89
89
|
sessionId: string,
|
|
90
90
|
ctx: ExtensionContext,
|
|
91
|
-
state:
|
|
91
|
+
state: PlanState,
|
|
92
92
|
): Promise<void> {
|
|
93
93
|
if (!state.isActive) {
|
|
94
94
|
ctx.ui.notify("No active plan mode.", "info");
|
|
@@ -104,7 +104,7 @@ async function handleAbort(
|
|
|
104
104
|
/** Handle /plan status subcommand */
|
|
105
105
|
function handleStatus(
|
|
106
106
|
ctx: ExtensionContext,
|
|
107
|
-
state:
|
|
107
|
+
state: PlanState,
|
|
108
108
|
): void {
|
|
109
109
|
if (!state.isActive) {
|
|
110
110
|
ctx.ui.notify("No active plan mode.", "info");
|
|
@@ -136,7 +136,7 @@ function handleEnterPlanMode(
|
|
|
136
136
|
sessions: PlanSessionMap,
|
|
137
137
|
sessionId: string,
|
|
138
138
|
ctx: ExtensionContext,
|
|
139
|
-
state:
|
|
139
|
+
state: PlanState,
|
|
140
140
|
requirement: string,
|
|
141
141
|
): void {
|
|
142
142
|
const slug = requirement
|
|
@@ -157,7 +157,7 @@ function handleEnterPlanMode(
|
|
|
157
157
|
persistPlanState(pi, state);
|
|
158
158
|
updatePlanWidget(ctx, state);
|
|
159
159
|
|
|
160
|
-
// Restrict tools to
|
|
160
|
+
// Restrict tools to the plan-mode set (includes bash — file-write constraints come from the injected plan mode prompt below)
|
|
161
161
|
pi.setActiveTools(["read", "bash", "grep", "find", "ls", "plan"]);
|
|
162
162
|
|
|
163
163
|
// Inject plan mode system prompt inline
|
|
@@ -183,27 +183,6 @@ function handleEnterPlanMode(
|
|
|
183
183
|
`## Phase D: Completion\n` +
|
|
184
184
|
`1. Ask user to review the complete plan.\n` +
|
|
185
185
|
`2. Call plan tool (complete) with isolation method (compact/tree/direct).\n` +
|
|
186
|
-
`3. After plan complete:
|
|
186
|
+
`3. After plan complete: the user picks an execution path (subagent-driven / goal-driven / single-agent) via the completion dialog.`,
|
|
187
187
|
);
|
|
188
188
|
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Programmatic entry to start plan mode (for `pi.__planStart`).
|
|
192
|
-
*
|
|
193
|
-
* Mirrors the `/plan` command's enter logic. Returns false if plan mode is
|
|
194
|
-
* already active (caller can then surface a message or wait).
|
|
195
|
-
*
|
|
196
|
-
* @returns true if plan mode started; false if already active
|
|
197
|
-
*/
|
|
198
|
-
export function startPlanMode(
|
|
199
|
-
pi: ExtensionAPI,
|
|
200
|
-
sessions: PlanSessionMap,
|
|
201
|
-
ctx: ExtensionContext,
|
|
202
|
-
requirement: string,
|
|
203
|
-
): boolean {
|
|
204
|
-
const sessionId = ctx.sessionManager.getSessionId();
|
|
205
|
-
const state = getPlanState(sessions, sessionId, ctx);
|
|
206
|
-
if (state.isActive) return false;
|
|
207
|
-
handleEnterPlanMode(pi, sessions, sessionId, ctx, state, requirement);
|
|
208
|
-
return true;
|
|
209
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
3
3
|
|
|
4
|
-
import { registerPlanCommand
|
|
4
|
+
import { registerPlanCommand } from "./command.js";
|
|
5
5
|
import { type PlanSessionMap, reconstructPlanState } from "./state.js";
|
|
6
6
|
import { registerPlanTool } from "./tool.js";
|
|
7
7
|
import { updatePlanWidget } from "./widget.js";
|
|
@@ -16,13 +16,6 @@ export default function planExtension(pi: ExtensionAPI) {
|
|
|
16
16
|
registerPlanTool(pi, sessions);
|
|
17
17
|
registerPlanCommand(pi, sessions);
|
|
18
18
|
|
|
19
|
-
// External API: __planStart(allow other extensions to start plan mode programmatically, #9)
|
|
20
|
-
// 交叉类型单步断言(ExtensionAPI 可赋给 ExtensionAPI & { __planStart? },无需 unknown 中转)
|
|
21
|
-
const api = pi as ExtensionAPI & { __planStart?: (requirement: string, ctx: ExtensionContext) => boolean };
|
|
22
|
-
api.__planStart = (requirement: string, ctx: ExtensionContext): boolean => {
|
|
23
|
-
return startPlanMode(pi, sessions, ctx, requirement);
|
|
24
|
-
};
|
|
25
|
-
|
|
26
19
|
// Dynamic import compact handlers — avoids cross-group static import
|
|
27
20
|
import("./compact.js").then(({ registerPlanEventHandlers }) => {
|
|
28
21
|
registerPlanEventHandlers(pi, sessions);
|
|
@@ -36,7 +29,7 @@ export default function planExtension(pi: ExtensionAPI) {
|
|
|
36
29
|
const state = reconstructPlanState(ctx);
|
|
37
30
|
sessions.set(sessionId, state);
|
|
38
31
|
updatePlanWidget(ctx, state);
|
|
39
|
-
// If plan mode was active, re-restrict tools to
|
|
32
|
+
// If plan mode was active, re-restrict tools to the plan-mode set (includes bash — file-write constraints come from the injected plan mode prompt)
|
|
40
33
|
if (state.isActive) {
|
|
41
34
|
pi.setActiveTools(["read", "bash", "grep", "find", "ls", "plan"]);
|
|
42
35
|
}
|
package/src/tool.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { ExtensionAPI, ExtensionContext, Theme, ThemeColor } from "@earendi
|
|
|
6
6
|
import { Text } from "@earendil-works/pi-tui";
|
|
7
7
|
import { Type } from "typebox";
|
|
8
8
|
|
|
9
|
-
import type { PlanSessionMap } from "./state.js";
|
|
9
|
+
import type { PlanSessionMap, PlanState } from "./state.js";
|
|
10
10
|
import { getPlanState, persistPlanState, resetPlanState } from "./state.js";
|
|
11
11
|
import { listTemplates, loadTemplate } from "./templates.js";
|
|
12
12
|
import { updatePlanWidget } from "./widget.js";
|
|
@@ -171,6 +171,172 @@ function renderPlanResult(
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
// ── Action executors (one per switch case) ─────────────────────────
|
|
175
|
+
|
|
176
|
+
/** Execute result envelope (shared shape returned by every action). */
|
|
177
|
+
interface ActionResult {
|
|
178
|
+
content: Array<{ type: "text"; text: string }>;
|
|
179
|
+
details: PlanDetails;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function executeListTemplate(projectDir: string): ActionResult {
|
|
183
|
+
const templates = listTemplates(projectDir);
|
|
184
|
+
return {
|
|
185
|
+
content: [{ type: "text" as const, text: `${templates.length} templates available` }],
|
|
186
|
+
details: { action: "list-template", templates },
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function executeSelectTemplate(
|
|
191
|
+
pi: ExtensionAPI,
|
|
192
|
+
params: Record<string, unknown>,
|
|
193
|
+
state: PlanState,
|
|
194
|
+
projectDir: string,
|
|
195
|
+
): ActionResult {
|
|
196
|
+
const templateName = params.templateName as string;
|
|
197
|
+
if (!templateName) {
|
|
198
|
+
throw new Error("templateName is required for select-template");
|
|
199
|
+
}
|
|
200
|
+
const content = loadTemplate(templateName, projectDir);
|
|
201
|
+
if (!content) {
|
|
202
|
+
throw new Error(`Template not found: ${templateName}`);
|
|
203
|
+
}
|
|
204
|
+
state.templateName = templateName;
|
|
205
|
+
state.phase = "writing";
|
|
206
|
+
persistPlanState(pi, state);
|
|
207
|
+
return {
|
|
208
|
+
content: [{ type: "text" as const, text: `Template selected: ${templateName}` }],
|
|
209
|
+
details: { action: "select-template", templateName, content, phase: state.phase },
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function executeCreateTemplate(params: Record<string, unknown>, projectDir: string): ActionResult {
|
|
214
|
+
const templateName = params.templateName as string;
|
|
215
|
+
const templateContent = params.templateContent as string;
|
|
216
|
+
if (!templateName || !templateContent) {
|
|
217
|
+
throw new Error("templateName and templateContent are required for create-template");
|
|
218
|
+
}
|
|
219
|
+
const sanitizedName = templateName.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
220
|
+
if (!sanitizedName) {
|
|
221
|
+
throw new Error("Invalid template name: must contain alphanumeric characters");
|
|
222
|
+
}
|
|
223
|
+
const templateDir = path.join(projectDir, ".pi", "plan-templates");
|
|
224
|
+
fs.mkdirSync(templateDir, { recursive: true });
|
|
225
|
+
const filePath = path.join(templateDir, `${sanitizedName}.md`);
|
|
226
|
+
fs.writeFileSync(filePath, templateContent);
|
|
227
|
+
return {
|
|
228
|
+
content: [{ type: "text" as const, text: `Template created: ${sanitizedName}` }],
|
|
229
|
+
details: {
|
|
230
|
+
action: "create-template",
|
|
231
|
+
templateName: sanitizedName,
|
|
232
|
+
templateDir: relativePath(filePath, projectDir),
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function executeAbort(
|
|
238
|
+
pi: ExtensionAPI,
|
|
239
|
+
sessions: PlanSessionMap,
|
|
240
|
+
sessionId: string,
|
|
241
|
+
ctx: ExtensionContext,
|
|
242
|
+
): ActionResult {
|
|
243
|
+
const updatedState = resetPlanState(pi, sessions, sessionId, ctx);
|
|
244
|
+
updatePlanWidget(ctx, updatedState);
|
|
245
|
+
restoreFullToolSet(pi);
|
|
246
|
+
return {
|
|
247
|
+
content: [{ type: "text" as const, text: "Plan mode aborted. Full tool access restored." }],
|
|
248
|
+
details: { action: "abort" },
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Build execution options filtered by available capabilities. */
|
|
253
|
+
async function buildExecOptions(pi: ExtensionAPI): Promise<string[]> {
|
|
254
|
+
const execOptions = ["Subagent-driven execution"];
|
|
255
|
+
const hasGoal = (await import("./compact.js")).detectGoalCapability(pi);
|
|
256
|
+
if (hasGoal) execOptions.push("Goal-driven execution (/goal)");
|
|
257
|
+
execOptions.push("Single-agent (current session)");
|
|
258
|
+
execOptions.push("Modify the plan first", "Save for later");
|
|
259
|
+
return execOptions;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Map the user's execution-method choice to the chosenMode string. */
|
|
263
|
+
function chosenModeFromChoice(choice: string): string {
|
|
264
|
+
if (choice === "Subagent-driven execution") return "subagent";
|
|
265
|
+
if (choice === "Goal-driven execution (/goal)") return "goal";
|
|
266
|
+
return "single-agent";
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Outcome of the complete-action execution-method prompt. */
|
|
270
|
+
type CompleteChoiceOutcome =
|
|
271
|
+
| { kind: "cancelled"; result: ActionResult }
|
|
272
|
+
| { kind: "mode"; chosenMode: string };
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Prompt the user for an execution method (no-op selection when headless).
|
|
276
|
+
* Cancel / "Modify the plan first" / "Save for later" → cancelled with a
|
|
277
|
+
* complete-cancelled result; otherwise the mapped chosenMode.
|
|
278
|
+
*/
|
|
279
|
+
async function resolveCompleteChoice(ctx: ExtensionContext, pi: ExtensionAPI): Promise<CompleteChoiceOutcome> {
|
|
280
|
+
const execOptions = await buildExecOptions(pi);
|
|
281
|
+
|
|
282
|
+
if (typeof ctx.ui.select !== "function") {
|
|
283
|
+
return { kind: "mode", chosenMode: "single-agent" };
|
|
284
|
+
}
|
|
285
|
+
const choice = await ctx.ui.select("Plan is ready. Choose execution method:", execOptions);
|
|
286
|
+
if (!choice || choice === "Modify the plan first" || choice === "Save for later") {
|
|
287
|
+
return {
|
|
288
|
+
kind: "cancelled",
|
|
289
|
+
result: {
|
|
290
|
+
content: [
|
|
291
|
+
{ type: "text" as const, text: `User chose: ${choice ?? "cancelled"}. Staying in plan mode.` },
|
|
292
|
+
],
|
|
293
|
+
details: { action: "complete-cancelled", reason: choice ?? "cancelled" },
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
return { kind: "mode", chosenMode: chosenModeFromChoice(choice) };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** complete action: prompt for execution mode, persist final phase, restore tools, reset state. */
|
|
301
|
+
async function executeComplete(
|
|
302
|
+
pi: ExtensionAPI,
|
|
303
|
+
ctx: ExtensionContext,
|
|
304
|
+
params: Record<string, unknown>,
|
|
305
|
+
state: PlanState,
|
|
306
|
+
sessions: PlanSessionMap,
|
|
307
|
+
sessionId: string,
|
|
308
|
+
projectDir: string,
|
|
309
|
+
): Promise<ActionResult> {
|
|
310
|
+
const choice = await resolveCompleteChoice(ctx, pi);
|
|
311
|
+
if (choice.kind === "cancelled") {
|
|
312
|
+
return choice.result;
|
|
313
|
+
}
|
|
314
|
+
const chosenMode = choice.chosenMode;
|
|
315
|
+
|
|
316
|
+
// Persist final phase before cleanup
|
|
317
|
+
const planFilePath = state.planFilePath;
|
|
318
|
+
const isolation = (params.isolation as string) ?? "direct";
|
|
319
|
+
state.phase = "complete";
|
|
320
|
+
persistPlanState(pi, state);
|
|
321
|
+
|
|
322
|
+
// Restore full tool set
|
|
323
|
+
restoreFullToolSet(pi);
|
|
324
|
+
|
|
325
|
+
// Execute completion handler (compact/tree setup)
|
|
326
|
+
const { handlePlanComplete } = await import("./compact.js");
|
|
327
|
+
handlePlanComplete(pi, ctx, state, isolation, chosenMode);
|
|
328
|
+
|
|
329
|
+
// Reset state and clear widget — same as abort
|
|
330
|
+
const updatedState = resetPlanState(pi, sessions, sessionId, ctx);
|
|
331
|
+
updatePlanWidget(ctx, updatedState);
|
|
332
|
+
|
|
333
|
+
const displayPath = relativePath(planFilePath, projectDir);
|
|
334
|
+
return {
|
|
335
|
+
content: [{ type: "text" as const, text: `Plan approved. File: ${displayPath}` }],
|
|
336
|
+
details: { action: "complete", planFilePath: displayPath, isolation, execMode: chosenMode },
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
174
340
|
// ── Register tool ──────────────────────────────────────────────────
|
|
175
341
|
|
|
176
342
|
export function registerPlanTool(
|
|
@@ -182,7 +348,7 @@ export function registerPlanTool(
|
|
|
182
348
|
label: "Plan Mode",
|
|
183
349
|
description:
|
|
184
350
|
"Manages plan mode lifecycle (template selection, state transitions, completion). " +
|
|
185
|
-
"NOT for writing plan content —
|
|
351
|
+
"NOT for writing plan content — write plan.md via the bash tool (e.g. cat heredoc). " +
|
|
186
352
|
"Actions: list-template, select-template, create-template, complete, abort.",
|
|
187
353
|
parameters: Type.Object({
|
|
188
354
|
action: StringEnum(PLAN_ACTIONS, { description: "Action to perform" }),
|
|
@@ -195,25 +361,25 @@ export function registerPlanTool(
|
|
|
195
361
|
),
|
|
196
362
|
}),
|
|
197
363
|
promptSnippet:
|
|
198
|
-
"## When to use this tool vs
|
|
364
|
+
"## When to use this tool vs the bash tool\n" +
|
|
199
365
|
"Use 'plan' tool ONLY for plan mode state management:\n" +
|
|
200
366
|
"- list-template / select-template / create-template — template operations\n" +
|
|
201
367
|
"- complete — user approved plan, exit plan mode\n" +
|
|
202
368
|
"- abort — cancel plan mode\n" +
|
|
203
369
|
"\n" +
|
|
204
|
-
"Use
|
|
370
|
+
"Use the bash tool for ALL plan content: writing plan.md, updating plan chapters (e.g. cat heredoc).\n" +
|
|
205
371
|
"\n" +
|
|
206
372
|
"## End-to-end workflow example\n" +
|
|
207
373
|
"1. /plan 'add dark mode' — user enters plan mode\n" +
|
|
208
374
|
"2. AI explores codebase (read, grep, bash) — brainstorming\n" +
|
|
209
375
|
"3. plan(action='list-template') — show available templates\n" +
|
|
210
376
|
"4. User picks template → plan(action='select-template', templateName='feature-plan')\n" +
|
|
211
|
-
"5.
|
|
377
|
+
"5. bash: cat > \"$PLAN_FILE\" <<'EOF' ... EOF — write plan content\n" +
|
|
212
378
|
"6. User reviews → plan(action='complete', isolation='compact') — exit plan mode\n" +
|
|
213
379
|
"\n" +
|
|
214
380
|
"## Common mistakes\n" +
|
|
215
|
-
"❌ plan(action='complete') to 'write the plan' — WRONG,
|
|
216
|
-
"❌ Calling plan tool when user says 'write plan to file' — use
|
|
381
|
+
"❌ plan(action='complete') to 'write the plan' — WRONG, write plan.md via the bash tool\n" +
|
|
382
|
+
"❌ Calling plan tool when user says 'write plan to file' — use the bash tool\n" +
|
|
217
383
|
"✅ plan(action='list-template') to discover templates\n" +
|
|
218
384
|
"✅ plan(action='complete') AFTER plan.md is written AND user approves",
|
|
219
385
|
renderResult(
|
|
@@ -240,113 +406,20 @@ export function registerPlanTool(
|
|
|
240
406
|
const projectDir = ctx.cwd;
|
|
241
407
|
|
|
242
408
|
switch (action) {
|
|
243
|
-
case "list-template":
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (!content) {
|
|
258
|
-
throw new Error(`Template not found: ${templateName}`);
|
|
259
|
-
}
|
|
260
|
-
state.templateName = templateName;
|
|
261
|
-
state.phase = "writing";
|
|
262
|
-
persistPlanState(pi, state);
|
|
263
|
-
return {
|
|
264
|
-
content: [{ type: "text" as const, text: `Template selected: ${templateName}` }],
|
|
265
|
-
details: { action: "select-template", templateName, content, phase: state.phase },
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
case "create-template": {
|
|
270
|
-
const templateName = params.templateName as string;
|
|
271
|
-
const templateContent = params.templateContent as string;
|
|
272
|
-
if (!templateName || !templateContent) {
|
|
273
|
-
throw new Error("templateName and templateContent are required for create-template");
|
|
274
|
-
}
|
|
275
|
-
const sanitizedName = templateName.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
276
|
-
if (!sanitizedName) {
|
|
277
|
-
throw new Error("Invalid template name: must contain alphanumeric characters");
|
|
278
|
-
}
|
|
279
|
-
const templateDir = path.join(projectDir, ".pi", "plan-templates");
|
|
280
|
-
fs.mkdirSync(templateDir, { recursive: true });
|
|
281
|
-
const filePath = path.join(templateDir, `${sanitizedName}.md`);
|
|
282
|
-
fs.writeFileSync(filePath, templateContent);
|
|
283
|
-
return {
|
|
284
|
-
content: [{ type: "text" as const, text: `Template created: ${sanitizedName}` }],
|
|
285
|
-
details: {
|
|
286
|
-
action: "create-template",
|
|
287
|
-
templateName: sanitizedName,
|
|
288
|
-
templateDir: relativePath(filePath, projectDir),
|
|
289
|
-
},
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
case "complete": {
|
|
294
|
-
// Build execution options filtered by available capabilities
|
|
295
|
-
const execOptions = ["Subagent-driven execution"];
|
|
296
|
-
const hasGoal = (await import("./compact.js")).detectGoalCapability(pi);
|
|
297
|
-
if (hasGoal) execOptions.push("Goal-driven execution (/goal)");
|
|
298
|
-
execOptions.push("Single-agent (current session)");
|
|
299
|
-
execOptions.push("Modify the plan first", "Save for later");
|
|
300
|
-
|
|
301
|
-
let chosenMode = "single-agent";
|
|
302
|
-
if (typeof ctx.ui.select === "function") {
|
|
303
|
-
const choice = await ctx.ui.select("Plan is ready. Choose execution method:", execOptions);
|
|
304
|
-
if (!choice || choice === "Modify the plan first" || choice === "Save for later") {
|
|
305
|
-
return {
|
|
306
|
-
content: [
|
|
307
|
-
{ type: "text" as const, text: `User chose: ${choice ?? "cancelled"}. Staying in plan mode.` },
|
|
308
|
-
],
|
|
309
|
-
details: { action: "complete-cancelled", reason: choice ?? "cancelled" },
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
if (choice === "Subagent-driven execution") chosenMode = "subagent";
|
|
313
|
-
else if (choice === "Goal-driven execution (/goal)") chosenMode = "goal";
|
|
314
|
-
else chosenMode = "single-agent";
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Persist final phase before cleanup
|
|
318
|
-
const planFilePath = state.planFilePath;
|
|
319
|
-
const isolation = (params.isolation as string) ?? "direct";
|
|
320
|
-
state.phase = "complete";
|
|
321
|
-
persistPlanState(pi, state);
|
|
322
|
-
|
|
323
|
-
// Restore full tool set
|
|
324
|
-
restoreFullToolSet(pi);
|
|
325
|
-
|
|
326
|
-
// Execute completion handler (compact/tree setup)
|
|
327
|
-
const { handlePlanComplete } = await import("./compact.js");
|
|
328
|
-
handlePlanComplete(pi, ctx, state, isolation, chosenMode);
|
|
329
|
-
|
|
330
|
-
// Reset state and clear widget — same as abort
|
|
331
|
-
const updatedState = resetPlanState(pi, sessions, sessionId, ctx);
|
|
332
|
-
updatePlanWidget(ctx, updatedState);
|
|
333
|
-
|
|
334
|
-
const displayPath = relativePath(planFilePath, projectDir);
|
|
335
|
-
return {
|
|
336
|
-
content: [{ type: "text" as const, text: `Plan approved. File: ${displayPath}` }],
|
|
337
|
-
details: { action: "complete", planFilePath: displayPath, isolation, execMode: chosenMode },
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
case "abort": {
|
|
342
|
-
const updatedState = resetPlanState(pi, sessions, sessionId, ctx);
|
|
343
|
-
updatePlanWidget(ctx, updatedState);
|
|
344
|
-
restoreFullToolSet(pi);
|
|
345
|
-
return {
|
|
346
|
-
content: [{ type: "text" as const, text: "Plan mode aborted. Full tool access restored." }],
|
|
347
|
-
details: { action: "abort" },
|
|
348
|
-
};
|
|
349
|
-
}
|
|
409
|
+
case "list-template":
|
|
410
|
+
return executeListTemplate(projectDir);
|
|
411
|
+
|
|
412
|
+
case "select-template":
|
|
413
|
+
return executeSelectTemplate(pi, params, state, projectDir);
|
|
414
|
+
|
|
415
|
+
case "create-template":
|
|
416
|
+
return executeCreateTemplate(params, projectDir);
|
|
417
|
+
|
|
418
|
+
case "complete":
|
|
419
|
+
return await executeComplete(pi, ctx, params, state, sessions, sessionId, projectDir);
|
|
420
|
+
|
|
421
|
+
case "abort":
|
|
422
|
+
return executeAbort(pi, sessions, sessionId, ctx);
|
|
350
423
|
}
|
|
351
424
|
},
|
|
352
425
|
});
|