@janvitos/pi-plan-build 0.1.31 → 0.1.33
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 +34 -4
- package/index.ts +273 -6
- package/package.json +4 -2
- package/plan-execution.ts +185 -0
- package/plan-panel.ts +76 -0
- package/prompts.ts +26 -0
package/README.md
CHANGED
|
@@ -15,10 +15,11 @@ A global [Pi coding agent](https://github.com/badlogic/pi-mono) extension that a
|
|
|
15
15
|
- Interactive `question`, `plan_enter`, and `plan_exit` tools.
|
|
16
16
|
- Plan mode supports read-only conversation and research across multiple turns, then persists the final plan when it is ready for approval.
|
|
17
17
|
- The complete saved plan is rendered in the transcript before approval—without the built-in write preview's truncation.
|
|
18
|
-
-
|
|
18
|
+
- Existing approval actions remain available:
|
|
19
19
|
- **Switch to Build and implement here**
|
|
20
20
|
- **Start fresh and implement**
|
|
21
21
|
- **Stay in Plan mode**
|
|
22
|
+
- **Experimental:** In fullscreen TUI, valid checklist plans also offer **Implement step by step**: a passive, non-overlapping docked right panel keeps the plan visible while natural-language prompts gate steps and return completed work for review. The panel is visual-only and never captures keyboard input. This feature is still under active development.
|
|
22
23
|
- Staying in Plan mode—or pressing Escape in the approval dialog—produces a durable acknowledgement and stops the run until the user responds.
|
|
23
24
|
- Mode state survives reloads, resumes, and forks.
|
|
24
25
|
- When Pi recreates the custom editor, the latest 100 user prompts from the active session branch are restored for Up/Down history navigation.
|
|
@@ -28,6 +29,7 @@ A global [Pi coding agent](https://github.com/badlogic/pi-mono) extension that a
|
|
|
28
29
|
- Pi `0.84.2` or newer
|
|
29
30
|
- Node.js `22.6` or newer for the test command
|
|
30
31
|
- TUI or RPC UI support for interactive questions and approval dialogs
|
|
32
|
+
- Pi fullscreen TUI mode for the optional docked step-by-step plan panel; regular mode and all existing workflows remain supported
|
|
31
33
|
|
|
32
34
|
## Install
|
|
33
35
|
|
|
@@ -71,8 +73,9 @@ The agent may also enter Plan mode with `plan_enter` when planning or investigat
|
|
|
71
73
|
When planning is complete, `plan_exit` displays the entire persisted plan and asks whether to:
|
|
72
74
|
|
|
73
75
|
1. implement in the current session;
|
|
74
|
-
2. start a clean linked implementation session;
|
|
75
|
-
3. stay in Plan mode
|
|
76
|
+
2. start a clean linked implementation session;
|
|
77
|
+
3. stay in Plan mode; or
|
|
78
|
+
4. **experimentally implement step by step** in fullscreen TUI when the plan contains a valid checklist.
|
|
76
79
|
|
|
77
80
|
Selecting **Start fresh and implement** stops the current run and automatically dispatches `/build-fresh`. Pi 0.84.2 or newer is required for extension command dispatch from an injected user message. The command creates a linked child session, copies the approved plan to its canonical plan file, preserves the model and thinking level selected for the action, switches it to Build, and starts implementation without transferring the planning conversation.
|
|
78
81
|
|
|
@@ -82,6 +85,33 @@ Selecting **Stay in Plan mode**, or pressing Escape while the approval dialog is
|
|
|
82
85
|
|
|
83
86
|
Both actions leave Plan mode active, stop the agent, and wait for the next user message.
|
|
84
87
|
|
|
88
|
+
### Step-by-step execution panel (Experimental)
|
|
89
|
+
|
|
90
|
+
> **Experimental feature:** Step-by-step execution is still being developed. Expect UI and workflow changes, and please report issues or unexpected behavior.
|
|
91
|
+
|
|
92
|
+
When Pi uses `"tuiMode": "fullscreen"` and the saved plan contains top-level `- [ ]` items under `## Implementation Steps`, `plan_exit` offers the additional **Implement step by step** approval action. This is opt-in per plan; it does not replace either one-shot implementation option or **Stay in Plan mode**.
|
|
93
|
+
|
|
94
|
+
The passive 64-column right panel reserves terminal columns, so the transcript and editor reflow instead of being covered. Long step instructions wrap across aligned continuation rows rather than being clipped. It never accepts focus or keyboard input and collapses below 132 terminal columns. The panel is a visual status aid only; all control happens through ordinary prompts. The agent interprets intent contextually, so these are examples rather than required commands:
|
|
95
|
+
|
|
96
|
+
- “Implement the next step” or “Start step 2.”
|
|
97
|
+
- “Step 1 is complete,” “I verified that one,” or “I already handled this.”
|
|
98
|
+
- “Change step 3 to …” or “Skip this step.”
|
|
99
|
+
- “Accept this result” or “Correct it by …”
|
|
100
|
+
- “Pause the plan,” “hide the plan,” or “show the plan.”
|
|
101
|
+
- “Cancel this plan” at any point to end step-by-step execution immediately.
|
|
102
|
+
|
|
103
|
+
The extension exposes these actions to the agent through `plan_step_control`; project mutations remain blocked until the user clearly approves a ready step or explicitly indicates that it is already complete. A ready step may be marked complete without implementation when the user says they already handled or verified it. A reviewed implementation may be accepted, while ambiguous feedback prompts clarification. The agent interprets intent contextually rather than requiring exact phrases, while the extension validates every resulting state transition. Step tool results identify the affected step, for example `Step 2: complete`, rather than presenting a plan-wide completion label. Cancelling removes the panel and execution guards immediately, restores the full-width layout, and preserves the saved plan file for reference. The agent implements only that step, calls `plan_step_complete`, and waits for the user's next prompt. Accepting a result makes the next step ready but never starts it automatically. Progress, revisions, summaries, and panel visibility survive reload/resume. If such a session is opened in regular mode, progress is retained but cannot advance until fullscreen mode is restored; no overlay fallback is used.
|
|
104
|
+
|
|
105
|
+
Enable fullscreen in `~/.pi/agent/settings.json` and restart Pi:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"tuiMode": "fullscreen"
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The integration uses Pi 0.84.2's public fullscreen layout primitives plus a guarded read of its runtime layout root because the current extension API exposes `setLayoutRoot()` but not a corresponding getter.
|
|
114
|
+
|
|
85
115
|
## Plan-mode permissions
|
|
86
116
|
|
|
87
117
|
Normal tools remain visible so the model can inspect the project. While a Plan run is active:
|
|
@@ -112,7 +142,7 @@ npm test
|
|
|
112
142
|
npm pack --dry-run
|
|
113
143
|
```
|
|
114
144
|
|
|
115
|
-
The tests cover state decoding, safe plan paths, mutation restrictions, deferred transitions, mode and provider rendering, conversational Plan guidance, session-based prompt history restoration, complete plan rendering, approval decisions, stop behavior, fresh-session settings and handoff content,
|
|
145
|
+
The tests cover state decoding, safe plan paths, mutation restrictions, deferred transitions, mode and provider rendering, conversational Plan guidance, session-based prompt history restoration, complete plan rendering, approval decisions, stop behavior, fresh-session settings and handoff content, question formatting and cancellation, structured checklist parsing, step state transitions, safe instruction revisions, and responsive panel rendering.
|
|
116
146
|
|
|
117
147
|
### Publishing
|
|
118
148
|
|
package/index.ts
CHANGED
|
@@ -2,15 +2,34 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { CustomEditor, getAgentDir, getMarkdownTheme, type EntryRenderer, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { Key, Markdown, matchesKey, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
5
|
+
import { HStack, Key, Markdown, matchesKey, Text, truncateToWidth, visibleWidth, isViewportTUI, type Component, type TUI, type ViewportTUI } from "@earendil-works/pi-tui";
|
|
6
6
|
import { Type } from "typebox";
|
|
7
7
|
import { registerQuestionTool } from "./question-ui.ts";
|
|
8
8
|
import {
|
|
9
9
|
buildPlanReminder,
|
|
10
|
+
buildPlanStepReminder,
|
|
11
|
+
buildPlanStepWaitingReminder,
|
|
10
12
|
PLAN_ENTER_DESCRIPTION,
|
|
11
13
|
PLAN_EXIT_DESCRIPTION,
|
|
14
|
+
PLAN_STEP_COMPLETE_DESCRIPTION,
|
|
12
15
|
PLAN_TO_BUILD_REMINDER,
|
|
13
16
|
} from "./prompts.ts";
|
|
17
|
+
import {
|
|
18
|
+
acceptPlanStep,
|
|
19
|
+
activePlanStep,
|
|
20
|
+
completePlanStep,
|
|
21
|
+
createPlanExecution,
|
|
22
|
+
decodePlanExecution,
|
|
23
|
+
pausePlanExecution,
|
|
24
|
+
requestPlanStepCorrections,
|
|
25
|
+
revisePlanStep,
|
|
26
|
+
skipPlanStep,
|
|
27
|
+
startPlanStep,
|
|
28
|
+
submitPlanStepForReview,
|
|
29
|
+
updatePlanChecklistStep,
|
|
30
|
+
type PlanExecutionState,
|
|
31
|
+
} from "./plan-execution.ts";
|
|
32
|
+
import { PlanPanel } from "./plan-panel.ts";
|
|
14
33
|
import {
|
|
15
34
|
applyManualSelection,
|
|
16
35
|
buildFreshImplementationHandoff,
|
|
@@ -48,7 +67,10 @@ const LEGACY_PLAN_REVIEW_ENTRY_TYPE = "opencode-plan-review";
|
|
|
48
67
|
const MODE_NOTICE_ENTRY_TYPE = "pi-plan-build-notice";
|
|
49
68
|
const LEGACY_MODE_NOTICE_ENTRY_TYPE = "opencode-mode-notice";
|
|
50
69
|
const STATUS_KEY = "pi-plan-build-mode";
|
|
51
|
-
const
|
|
70
|
+
const PLAN_STEP_CHOICE = "Implement step by step";
|
|
71
|
+
const PANEL_WIDTH = 64;
|
|
72
|
+
const PANEL_MIN_TERMINAL_WIDTH = 132;
|
|
73
|
+
const MANAGED_TOOLS = new Set(["question", "plan_enter", "plan_exit", "plan_step_control", "plan_step_complete"]);
|
|
52
74
|
const MODE_ADDED_TOOLS = new Set([...MANAGED_TOOLS, "edit", "write"]);
|
|
53
75
|
const EMPTY_PARAMETERS = Type.Object({});
|
|
54
76
|
|
|
@@ -58,6 +80,7 @@ interface StoredState {
|
|
|
58
80
|
selectedMode: Mode;
|
|
59
81
|
pendingReminder?: "plan" | "build";
|
|
60
82
|
toolsBeforeModes?: string[];
|
|
83
|
+
execution?: PlanExecutionState;
|
|
61
84
|
}
|
|
62
85
|
|
|
63
86
|
function shorten(filePath: string, cwd: string): string {
|
|
@@ -76,6 +99,12 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
76
99
|
let currentContext: ExtensionContext | undefined;
|
|
77
100
|
let requestEditorRender: (() => void) | undefined;
|
|
78
101
|
let freshImplementationRequest: FreshImplementationRequest | undefined;
|
|
102
|
+
let execution: PlanExecutionState | undefined;
|
|
103
|
+
let panel: PlanPanel | undefined;
|
|
104
|
+
let panelTui: (TUI & Partial<ViewportTUI>) | undefined;
|
|
105
|
+
let originalLayoutRoot: Component | undefined;
|
|
106
|
+
let panelLayoutRoot: Component | undefined;
|
|
107
|
+
let fullscreenPanelCapable = false;
|
|
79
108
|
|
|
80
109
|
pi.registerFlag("plan", {
|
|
81
110
|
description: "Start in Plan mode",
|
|
@@ -98,13 +127,57 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
98
127
|
pi.registerEntryRenderer<{ message: string }>(LEGACY_MODE_NOTICE_ENTRY_TYPE, renderModeNotice);
|
|
99
128
|
|
|
100
129
|
function stateData(): StoredState {
|
|
101
|
-
return { version: 1, selectedMode, pendingReminder, toolsBeforeModes };
|
|
130
|
+
return { version: 1, selectedMode, pendingReminder, toolsBeforeModes, ...(execution ? { execution } : {}) };
|
|
102
131
|
}
|
|
103
132
|
|
|
104
133
|
function persist(): void {
|
|
105
134
|
pi.appendEntry(STATE_TYPE, stateData());
|
|
106
135
|
}
|
|
107
136
|
|
|
137
|
+
function updateExecution(next: PlanExecutionState): void {
|
|
138
|
+
execution = next;
|
|
139
|
+
panel?.setState(next);
|
|
140
|
+
persist();
|
|
141
|
+
panelTui?.requestRender();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function removePanelLayout(): void {
|
|
145
|
+
if (panelLayoutRoot && panelTui && originalLayoutRoot) panelTui.setLayoutRoot?.(originalLayoutRoot);
|
|
146
|
+
panelLayoutRoot = undefined;
|
|
147
|
+
panel = undefined;
|
|
148
|
+
panelTui?.requestRender();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function cancelPlanExecution(): void {
|
|
152
|
+
execution = undefined;
|
|
153
|
+
removePanelLayout();
|
|
154
|
+
persist();
|
|
155
|
+
applyTools("build");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function ensurePanelLayout(): boolean {
|
|
159
|
+
if (!execution || !fullscreenPanelCapable || !panelTui || !originalLayoutRoot || !currentContext) return false;
|
|
160
|
+
if (!panel) panel = new PlanPanel(execution, currentContext.ui.theme);
|
|
161
|
+
else panel.setState(execution);
|
|
162
|
+
if (!panelLayoutRoot) {
|
|
163
|
+
panelLayoutRoot = new HStack([
|
|
164
|
+
{ component: originalLayoutRoot, basis: 0, grow: 1, shrink: 1, minSize: 58 },
|
|
165
|
+
{
|
|
166
|
+
component: panel,
|
|
167
|
+
basis: PANEL_WIDTH,
|
|
168
|
+
grow: 0,
|
|
169
|
+
shrink: 0,
|
|
170
|
+
minSize: PANEL_WIDTH,
|
|
171
|
+
maxSize: PANEL_WIDTH,
|
|
172
|
+
visible: (viewport) => execution?.panelVisible !== false && viewport.width >= PANEL_MIN_TERMINAL_WIDTH,
|
|
173
|
+
},
|
|
174
|
+
]);
|
|
175
|
+
panelTui.setLayoutRoot?.(panelLayoutRoot);
|
|
176
|
+
}
|
|
177
|
+
panelTui.requestRender();
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
108
181
|
function updateModeIndicator(ctx: ExtensionContext): void {
|
|
109
182
|
// Clear the legacy footer status; the mode is now rendered inside the editor.
|
|
110
183
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
@@ -122,7 +195,13 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
122
195
|
if (mode === "plan") {
|
|
123
196
|
pi.setActiveTools(unique([...base, "edit", "write", "question", "plan_exit"]));
|
|
124
197
|
} else {
|
|
125
|
-
pi.setActiveTools(unique([
|
|
198
|
+
pi.setActiveTools(unique([
|
|
199
|
+
...base,
|
|
200
|
+
"question",
|
|
201
|
+
"plan_enter",
|
|
202
|
+
...(execution ? ["plan_step_control"] : []),
|
|
203
|
+
...(activePlanStep(execution) ? ["plan_step_complete"] : []),
|
|
204
|
+
]));
|
|
126
205
|
}
|
|
127
206
|
}
|
|
128
207
|
|
|
@@ -297,6 +376,128 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
297
376
|
},
|
|
298
377
|
});
|
|
299
378
|
|
|
379
|
+
pi.registerTool({
|
|
380
|
+
name: "plan_step_control",
|
|
381
|
+
label: "Control Plan Execution",
|
|
382
|
+
description: `Use this tool to translate the user's natural-language instructions into one step-by-step plan action. Available actions: start a ready step, complete a clearly finished ready or reviewed step, accept a reviewed step, request corrections, skip a ready step, revise an unimplemented instruction, pause/resume or cancel execution, or hide/show the visual plan panel. Interpret clear user intent semantically, including direct completion statements, but do not advance based on hypothetical, uncertain, or unrelated conversation.`,
|
|
383
|
+
parameters: Type.Object({
|
|
384
|
+
action: Type.Union([
|
|
385
|
+
Type.Literal("start"),
|
|
386
|
+
Type.Literal("complete"),
|
|
387
|
+
Type.Literal("accept"),
|
|
388
|
+
Type.Literal("correct"),
|
|
389
|
+
Type.Literal("skip"),
|
|
390
|
+
Type.Literal("revise"),
|
|
391
|
+
Type.Literal("pause"),
|
|
392
|
+
Type.Literal("resume"),
|
|
393
|
+
Type.Literal("cancel"),
|
|
394
|
+
Type.Literal("hide"),
|
|
395
|
+
Type.Literal("show"),
|
|
396
|
+
]),
|
|
397
|
+
step: Type.Optional(Type.Number({ description: "One-based step number; defaults to the current ready/review step", minimum: 1 })),
|
|
398
|
+
instruction: Type.Optional(Type.String({ description: "Replacement instruction required for revise" })),
|
|
399
|
+
}),
|
|
400
|
+
executionMode: "sequential",
|
|
401
|
+
async execute(_toolCallId, params) {
|
|
402
|
+
if (!execution) throw new Error("No step-by-step plan is active");
|
|
403
|
+
const target = params.step === undefined
|
|
404
|
+
? execution.steps.find((step) => step.status === "ready" || step.status === "review")
|
|
405
|
+
: execution.steps[Math.floor(params.step) - 1];
|
|
406
|
+
const finish = (message: string) => ({
|
|
407
|
+
content: [{ type: "text" as const, text: message }],
|
|
408
|
+
details: { action: params.action, stepId: target?.id },
|
|
409
|
+
terminate: true,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
if (params.action === "cancel") {
|
|
413
|
+
cancelPlanExecution();
|
|
414
|
+
return finish("Step-by-step execution was cancelled. The panel and execution guards were removed; the saved plan file remains available.");
|
|
415
|
+
}
|
|
416
|
+
if (params.action === "hide" || params.action === "show") {
|
|
417
|
+
updateExecution({ ...execution, panelVisible: params.action === "show" });
|
|
418
|
+
if (params.action === "show") ensurePanelLayout();
|
|
419
|
+
return finish(`The visual plan panel is now ${params.action === "show" ? "visible" : "hidden"}. Progress is unchanged.`);
|
|
420
|
+
}
|
|
421
|
+
if (execution.status === "completed") throw new Error("The plan is complete; only hide or show actions remain available");
|
|
422
|
+
if (params.action === "pause" || params.action === "resume") {
|
|
423
|
+
if ((params.action === "pause") === (execution.status === "paused")) return finish(`Plan execution is already ${params.action === "pause" ? "paused" : "running"}.`);
|
|
424
|
+
updateExecution(pausePlanExecution(execution));
|
|
425
|
+
return finish(`Plan execution is now ${params.action === "pause" ? "paused" : "running"}.`);
|
|
426
|
+
}
|
|
427
|
+
if (!target) throw new Error("No matching plan step is available for that action");
|
|
428
|
+
if (params.action === "start") {
|
|
429
|
+
if (execution.status === "paused") throw new Error("Resume plan execution before starting a step");
|
|
430
|
+
updateExecution(startPlanStep(execution, target.id));
|
|
431
|
+
applyTools("build");
|
|
432
|
+
pi.sendUserMessage(`Implement plan step ${execution.steps.findIndex((step) => step.id === target.id) + 1}: ${target.text}`, { deliverAs: "followUp" });
|
|
433
|
+
return finish("The requested step is approved. Its implementation is starting in a follow-up turn.");
|
|
434
|
+
}
|
|
435
|
+
if (params.action === "complete") {
|
|
436
|
+
updateExecution(completePlanStep(execution, target.id));
|
|
437
|
+
return finish(execution.status === "completed" ? "The step was marked complete and the plan is complete." : "The step was marked complete. The next step is ready and awaits user instruction.");
|
|
438
|
+
}
|
|
439
|
+
if (params.action === "accept") {
|
|
440
|
+
updateExecution(acceptPlanStep(execution, target.id));
|
|
441
|
+
return finish(execution.status === "completed" ? "The reviewed step was accepted and the plan is complete." : "The reviewed step was accepted. The next step is ready and awaits user instruction.");
|
|
442
|
+
}
|
|
443
|
+
if (params.action === "correct") {
|
|
444
|
+
updateExecution(requestPlanStepCorrections(execution, target.id));
|
|
445
|
+
applyTools("build");
|
|
446
|
+
pi.sendUserMessage(`Apply the corrections requested in the user's preceding message to plan step ${execution.steps.findIndex((step) => step.id === target.id) + 1}.`, { deliverAs: "followUp" });
|
|
447
|
+
return finish("The reviewed step was reopened. The requested corrections are starting in a follow-up turn.");
|
|
448
|
+
}
|
|
449
|
+
if (params.action === "skip") {
|
|
450
|
+
updateExecution(skipPlanStep(execution, target.id));
|
|
451
|
+
return finish(execution.status === "completed" ? "The step was skipped and the plan is complete." : "The step was skipped. The next step awaits user instruction.");
|
|
452
|
+
}
|
|
453
|
+
if (!params.instruction?.trim()) throw new Error("Revising a step requires a replacement instruction");
|
|
454
|
+
const plan = await fs.promises.readFile(planPath, "utf8");
|
|
455
|
+
const updatedPlan = updatePlanChecklistStep(plan, target.sourceLine, params.instruction);
|
|
456
|
+
await fs.promises.writeFile(planPath, updatedPlan, "utf8");
|
|
457
|
+
updateExecution(revisePlanStep(execution, target.id, params.instruction, updatedPlan));
|
|
458
|
+
return finish("The plan step instruction was revised and is awaiting user approval.");
|
|
459
|
+
},
|
|
460
|
+
renderCall(args, theme) {
|
|
461
|
+
const requestedStep = typeof args.step === "number" && Number.isFinite(args.step) ? Math.max(1, Math.floor(args.step)) : undefined;
|
|
462
|
+
const inferredStep = requestedStep ?? (execution
|
|
463
|
+
? execution.steps.findIndex((step) => step.status === "ready" || step.status === "review") + 1
|
|
464
|
+
: 0);
|
|
465
|
+
const label = inferredStep > 0 ? `Step ${inferredStep}: ${args.action}` : `Plan: ${args.action}`;
|
|
466
|
+
return new Text(theme.fg("toolTitle", theme.bold(label)), 0, 0);
|
|
467
|
+
},
|
|
468
|
+
renderResult(result, _options, theme, context) {
|
|
469
|
+
const text = result.content.find((item) => item.type === "text")?.text ?? "Plan state updated";
|
|
470
|
+
return new Text(theme.fg(context.isError ? "error" : "success", text), 0, 0);
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
pi.registerTool({
|
|
475
|
+
name: "plan_step_complete",
|
|
476
|
+
label: "Complete Plan Step",
|
|
477
|
+
description: PLAN_STEP_COMPLETE_DESCRIPTION,
|
|
478
|
+
parameters: Type.Object({
|
|
479
|
+
summary: Type.String({ description: "Concise summary of what was implemented and verified" }),
|
|
480
|
+
}),
|
|
481
|
+
executionMode: "sequential",
|
|
482
|
+
async execute(_toolCallId, params) {
|
|
483
|
+
const step = activePlanStep(execution);
|
|
484
|
+
if (!execution || !step) throw new Error("No plan step is currently active");
|
|
485
|
+
updateExecution(submitPlanStepForReview(execution, step.id, params.summary));
|
|
486
|
+
applyTools("build");
|
|
487
|
+
return {
|
|
488
|
+
content: [{ type: "text", text: "The active plan step is awaiting user review. Stop now and do not begin another step." }],
|
|
489
|
+
details: { stepId: step.id, review: true },
|
|
490
|
+
terminate: true,
|
|
491
|
+
};
|
|
492
|
+
},
|
|
493
|
+
renderCall(_args, theme) {
|
|
494
|
+
return new Text(theme.fg("toolTitle", theme.bold("Submit plan step for review")), 0, 0);
|
|
495
|
+
},
|
|
496
|
+
renderResult(_result, _options, theme, context) {
|
|
497
|
+
return new Text(theme.fg(context.isError ? "error" : "success", context.isError ? "Plan step submission failed" : "Plan step ready for review"), 0, 0);
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
|
|
300
501
|
pi.registerTool({
|
|
301
502
|
name: "plan_exit",
|
|
302
503
|
label: "Exit Plan Mode",
|
|
@@ -315,10 +516,43 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
315
516
|
if (!plan.trim()) throw new Error("Cannot request plan approval because the plan file is empty");
|
|
316
517
|
pi.appendEntry(PLAN_REVIEW_ENTRY_TYPE, { plan, planPath });
|
|
317
518
|
const displayPath = shorten(planPath, ctx.cwd);
|
|
519
|
+
let stepExecution: PlanExecutionState | undefined;
|
|
520
|
+
let checklistError: string | undefined;
|
|
521
|
+
const panelAvailable = fullscreenPanelCapable && (panelTui?.terminal.columns ?? 0) >= PANEL_MIN_TERMINAL_WIDTH;
|
|
522
|
+
if (panelAvailable) {
|
|
523
|
+
try {
|
|
524
|
+
stepExecution = createPlanExecution(plan);
|
|
525
|
+
} catch (error: unknown) {
|
|
526
|
+
checklistError = error instanceof Error ? error.message : String(error);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const choices = [
|
|
530
|
+
PLAN_EXIT_APPROVE_CHOICE,
|
|
531
|
+
...(stepExecution ? [PLAN_STEP_CHOICE] : []),
|
|
532
|
+
PLAN_EXIT_FRESH_CHOICE,
|
|
533
|
+
PLAN_EXIT_STAY_CHOICE,
|
|
534
|
+
];
|
|
318
535
|
const selection = normalizePlanExitChoice(await ctx.ui.select(
|
|
319
536
|
`Build Agent: Plan at ${displayPath} is complete. What would you like to do?`,
|
|
320
|
-
|
|
537
|
+
choices,
|
|
321
538
|
));
|
|
539
|
+
if (selection.choice === PLAN_STEP_CHOICE && stepExecution) {
|
|
540
|
+
freshImplementationRequest = undefined;
|
|
541
|
+
execution = stepExecution;
|
|
542
|
+
await selectMode("build", ctx, "tool");
|
|
543
|
+
updateExecution(stepExecution);
|
|
544
|
+
ensurePanelLayout();
|
|
545
|
+
return {
|
|
546
|
+
content: [{ type: "text", text: "Step-by-step execution is ready. Stop now and wait for the user's natural-language instruction in the composer; the plan panel is visual-only." }],
|
|
547
|
+
details: { approved: true, action: "step-by-step", mode: "build", planPath },
|
|
548
|
+
terminate: true,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
if (panelAvailable && !stepExecution && checklistError) {
|
|
552
|
+
ctx.ui.notify(`Step-by-step execution is unavailable: ${checklistError}.`, "warning");
|
|
553
|
+
} else if (fullscreenPanelCapable && !panelAvailable) {
|
|
554
|
+
ctx.ui.notify(`Step-by-step execution requires a terminal at least ${PANEL_MIN_TERMINAL_WIDTH} columns wide.`, "warning");
|
|
555
|
+
}
|
|
322
556
|
const decision = classifyPlanExitChoice(selection.choice);
|
|
323
557
|
if (decision === "stay") {
|
|
324
558
|
freshImplementationRequest = undefined;
|
|
@@ -354,6 +588,9 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
354
588
|
},
|
|
355
589
|
renderResult(result, _options, theme, context) {
|
|
356
590
|
const details = result.details as { approved?: boolean; action?: string } | undefined;
|
|
591
|
+
if (details?.action === "step-by-step" && !context.isError) {
|
|
592
|
+
return new Text(theme.fg("success", "Step-by-step execution ready — waiting for your instruction."), 0, 0);
|
|
593
|
+
}
|
|
357
594
|
if (details?.action === "implement-fresh" && !context.isError) {
|
|
358
595
|
return new Text(
|
|
359
596
|
theme.fg("success", "Clean-session implementation selected — starting automatically."),
|
|
@@ -369,6 +606,12 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
369
606
|
});
|
|
370
607
|
|
|
371
608
|
pi.on("tool_call", async (event, ctx) => {
|
|
609
|
+
if ((runMode ?? selectedMode) === "build" && execution && execution.status !== "completed" && !activePlanStep(execution) && (event.toolName === "edit" || event.toolName === "write" || event.toolName === "bash")) {
|
|
610
|
+
return {
|
|
611
|
+
block: true,
|
|
612
|
+
reason: "Step-by-step execution is waiting for an explicit natural-language instruction from the user; no step is approved for project mutations.",
|
|
613
|
+
};
|
|
614
|
+
}
|
|
372
615
|
if (runMode !== "plan" || (event.toolName !== "edit" && event.toolName !== "write")) return;
|
|
373
616
|
const inputPath = (event.input as { path?: unknown }).path;
|
|
374
617
|
if (isAllowedPlanMutation(ctx.cwd, inputPath, planPath)) return;
|
|
@@ -385,6 +628,11 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
385
628
|
if (runMode === "plan") {
|
|
386
629
|
await ensurePlanDirectory();
|
|
387
630
|
content = buildPlanReminder(describePlanFile());
|
|
631
|
+
} else if (activePlanStep(execution)) {
|
|
632
|
+
const step = activePlanStep(execution)!;
|
|
633
|
+
content = buildPlanStepReminder(planPath, execution!.steps.findIndex((item) => item.id === step.id) + 1, execution!.steps.length, step.text);
|
|
634
|
+
} else if (execution && execution.status !== "completed") {
|
|
635
|
+
content = buildPlanStepWaitingReminder(execution.steps.map((step, index) => `${index + 1}. [${step.status}] ${step.text}`).join("\n"));
|
|
388
636
|
} else if (pendingReminder === "build") {
|
|
389
637
|
content = PLAN_TO_BUILD_REMINDER;
|
|
390
638
|
if (fs.existsSync(planPath)) content += `\n\nA plan file exists at ${planPath}. You should execute the plan defined within it.`;
|
|
@@ -399,6 +647,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
399
647
|
runMode = undefined;
|
|
400
648
|
applyTools(selectedMode);
|
|
401
649
|
updateModeIndicator(ctx);
|
|
650
|
+
if (execution) ensurePanelLayout();
|
|
402
651
|
});
|
|
403
652
|
|
|
404
653
|
pi.on("session_start", async (event, ctx) => {
|
|
@@ -413,13 +662,15 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
413
662
|
.pop() as { data?: unknown } | undefined;
|
|
414
663
|
const decoded = decodeModeState(latest?.data);
|
|
415
664
|
const raw = latest?.data as StoredState | undefined;
|
|
665
|
+
execution = decodePlanExecution(raw?.execution);
|
|
416
666
|
selectedMode = decoded?.selectedMode ?? (pi.getFlag("plan") === true ? "plan" : "build");
|
|
417
667
|
pendingReminder = raw?.pendingReminder ?? (decoded ? undefined : pi.getFlag("plan") === true ? "plan" : undefined);
|
|
418
668
|
toolsBeforeModes = Array.isArray(raw?.toolsBeforeModes)
|
|
419
669
|
? raw.toolsBeforeModes.filter((name): name is string => typeof name === "string" && !MANAGED_TOOLS.has(name))
|
|
420
670
|
: pi.getActiveTools().filter((name) => !MANAGED_TOOLS.has(name));
|
|
421
671
|
planPath = makePlanPath(path.join(getAgentDir(), "plans"), ctx.sessionManager.getSessionId());
|
|
422
|
-
if (selectedMode === "plan") await ensurePlanDirectory();
|
|
672
|
+
if (selectedMode === "plan" || execution) await ensurePlanDirectory();
|
|
673
|
+
if (execution && !fs.existsSync(planPath)) await fs.promises.writeFile(planPath, execution.planMarkdown, "utf8");
|
|
423
674
|
applyTools(selectedMode);
|
|
424
675
|
updateModeIndicator(ctx);
|
|
425
676
|
|
|
@@ -569,6 +820,13 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
569
820
|
const editor = new ModeEditor(tui, theme, keybindings);
|
|
570
821
|
for (const prompt of promptHistory) editor.addToHistory(prompt);
|
|
571
822
|
requestEditorRender = () => editor.requestModeRender();
|
|
823
|
+
panelTui = tui;
|
|
824
|
+
fullscreenPanelCapable = isViewportTUI(tui) && typeof (tui as ViewportTUI).setLayoutRoot === "function";
|
|
825
|
+
if (fullscreenPanelCapable && !originalLayoutRoot) {
|
|
826
|
+
originalLayoutRoot = (tui as TUI & { layoutRoot?: Component }).layoutRoot;
|
|
827
|
+
fullscreenPanelCapable = originalLayoutRoot !== undefined;
|
|
828
|
+
}
|
|
829
|
+
if (execution) ensurePanelLayout();
|
|
572
830
|
editor.onCycle = () => {
|
|
573
831
|
if (currentContext) void selectMode(nextMode(selectedMode), currentContext, "manual");
|
|
574
832
|
};
|
|
@@ -583,14 +841,23 @@ export default function planBuildModes(pi: ExtensionAPI): void {
|
|
|
583
841
|
};
|
|
584
842
|
return editor;
|
|
585
843
|
});
|
|
844
|
+
if (execution && !fullscreenPanelCapable) {
|
|
845
|
+
ctx.ui.notify("Step-by-step progress was restored, but its plan panel requires fullscreen TUI mode. Progress is preserved.", "warning");
|
|
846
|
+
}
|
|
586
847
|
}
|
|
587
848
|
});
|
|
588
849
|
|
|
589
850
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
851
|
+
removePanelLayout();
|
|
590
852
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
591
853
|
ctx.ui.setFooter(undefined);
|
|
592
854
|
ctx.ui.setEditorComponent(undefined);
|
|
593
855
|
requestEditorRender = undefined;
|
|
856
|
+
panel = undefined;
|
|
857
|
+
panelTui = undefined;
|
|
858
|
+
panelLayoutRoot = undefined;
|
|
859
|
+
originalLayoutRoot = undefined;
|
|
860
|
+
fullscreenPanelCapable = false;
|
|
594
861
|
currentContext = undefined;
|
|
595
862
|
});
|
|
596
863
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@janvitos/pi-plan-build",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"description": "Plan safely, approve explicitly, then implement here or in a clean session.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,13 +23,15 @@
|
|
|
23
23
|
"index.ts",
|
|
24
24
|
"prompts.ts",
|
|
25
25
|
"question-ui.ts",
|
|
26
|
+
"plan-execution.ts",
|
|
27
|
+
"plan-panel.ts",
|
|
26
28
|
"utils.ts"
|
|
27
29
|
],
|
|
28
30
|
"publishConfig": {
|
|
29
31
|
"access": "public"
|
|
30
32
|
},
|
|
31
33
|
"scripts": {
|
|
32
|
-
"test": "node --experimental-strip-types --test utils.test.ts question-ui.test.ts",
|
|
34
|
+
"test": "node --experimental-strip-types --test utils.test.ts question-ui.test.ts plan-execution.test.ts plan-panel.test.ts",
|
|
33
35
|
"prepublishOnly": "npm test"
|
|
34
36
|
},
|
|
35
37
|
"peerDependencies": {
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
export type PlanStepStatus = "pending" | "ready" | "active" | "review" | "completed" | "skipped";
|
|
2
|
+
|
|
3
|
+
export interface PlanStep {
|
|
4
|
+
id: string;
|
|
5
|
+
text: string;
|
|
6
|
+
status: PlanStepStatus;
|
|
7
|
+
sourceLine: number;
|
|
8
|
+
summary?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PlanExecutionState {
|
|
12
|
+
version: 1;
|
|
13
|
+
status: "running" | "paused" | "completed";
|
|
14
|
+
steps: PlanStep[];
|
|
15
|
+
planMarkdown: string;
|
|
16
|
+
selectedStepId?: string;
|
|
17
|
+
panelVisible: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const IMPLEMENTATION_HEADING = /^##\s+Implementation Steps\s*$/i;
|
|
21
|
+
const NEXT_H2 = /^##\s+/;
|
|
22
|
+
const CHECKLIST_ITEM = /^- \[ \]\s+(.+?)\s*$/;
|
|
23
|
+
|
|
24
|
+
export function parseImplementationSteps(plan: string): PlanStep[] {
|
|
25
|
+
const lines = plan.replace(/\r\n?/g, "\n").split("\n");
|
|
26
|
+
const heading = lines.findIndex((line) => IMPLEMENTATION_HEADING.test(line.trim()));
|
|
27
|
+
if (heading < 0) throw new Error("The plan needs a ‘## Implementation Steps’ section");
|
|
28
|
+
|
|
29
|
+
const steps: PlanStep[] = [];
|
|
30
|
+
const seen = new Set<string>();
|
|
31
|
+
for (let index = heading + 1; index < lines.length; index++) {
|
|
32
|
+
const line = lines[index]!;
|
|
33
|
+
if (NEXT_H2.test(line.trim())) break;
|
|
34
|
+
const match = CHECKLIST_ITEM.exec(line);
|
|
35
|
+
if (!match) continue;
|
|
36
|
+
const text = match[1]!.trim();
|
|
37
|
+
const normalized = text.toLocaleLowerCase();
|
|
38
|
+
if (!text) throw new Error(`Implementation step on line ${index + 1} is empty`);
|
|
39
|
+
if (seen.has(normalized)) throw new Error(`Duplicate implementation step: ${text}`);
|
|
40
|
+
seen.add(normalized);
|
|
41
|
+
steps.push({ id: `step-${steps.length + 1}`, text, status: "pending", sourceLine: index });
|
|
42
|
+
}
|
|
43
|
+
if (steps.length === 0) throw new Error("The Implementation Steps section has no top-level ‘- [ ]’ items");
|
|
44
|
+
return steps;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createPlanExecution(plan: string): PlanExecutionState {
|
|
48
|
+
const steps = parseImplementationSteps(plan);
|
|
49
|
+
steps[0]!.status = "ready";
|
|
50
|
+
return { version: 1, status: "running", steps, planMarkdown: plan, selectedStepId: steps[0]!.id, panelVisible: true };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function decodePlanExecution(value: unknown): PlanExecutionState | undefined {
|
|
54
|
+
if (!value || typeof value !== "object") return undefined;
|
|
55
|
+
const candidate = value as Partial<PlanExecutionState>;
|
|
56
|
+
if (candidate.version !== 1 || !["running", "paused", "completed"].includes(candidate.status ?? "")) return undefined;
|
|
57
|
+
if (!Array.isArray(candidate.steps) || candidate.steps.length === 0 || typeof candidate.planMarkdown !== "string") return undefined;
|
|
58
|
+
const validStatuses = new Set<PlanStepStatus>(["pending", "ready", "active", "review", "completed", "skipped"]);
|
|
59
|
+
const steps: PlanStep[] = [];
|
|
60
|
+
for (const raw of candidate.steps) {
|
|
61
|
+
if (!raw || typeof raw !== "object") return undefined;
|
|
62
|
+
const step = raw as Partial<PlanStep>;
|
|
63
|
+
if (typeof step.id !== "string" || typeof step.text !== "string" || !validStatuses.has(step.status as PlanStepStatus)) return undefined;
|
|
64
|
+
if (!Number.isInteger(step.sourceLine) || (step.sourceLine ?? -1) < 0) return undefined;
|
|
65
|
+
steps.push({ id: step.id, text: step.text, status: step.status as PlanStepStatus, sourceLine: step.sourceLine!, ...(typeof step.summary === "string" ? { summary: step.summary } : {}) });
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
version: 1,
|
|
69
|
+
status: candidate.status!,
|
|
70
|
+
steps,
|
|
71
|
+
planMarkdown: candidate.planMarkdown,
|
|
72
|
+
selectedStepId: typeof candidate.selectedStepId === "string" ? candidate.selectedStepId : undefined,
|
|
73
|
+
panelVisible: candidate.panelVisible !== false,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function clone(state: PlanExecutionState): PlanExecutionState {
|
|
78
|
+
return { ...state, steps: state.steps.map((step) => ({ ...step })) };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function findStep(state: PlanExecutionState, id: string): PlanStep {
|
|
82
|
+
const step = state.steps.find((candidate) => candidate.id === id);
|
|
83
|
+
if (!step) throw new Error(`Unknown plan step: ${id}`);
|
|
84
|
+
return step;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function makeNextReady(state: PlanExecutionState, afterId: string): void {
|
|
88
|
+
const index = state.steps.findIndex((step) => step.id === afterId);
|
|
89
|
+
const next = state.steps.slice(index + 1).find((step) => step.status === "pending");
|
|
90
|
+
if (next) {
|
|
91
|
+
next.status = "ready";
|
|
92
|
+
state.selectedStepId = next.id;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (state.steps.every((step) => step.status === "completed" || step.status === "skipped")) {
|
|
96
|
+
state.status = "completed";
|
|
97
|
+
state.selectedStepId = undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function startPlanStep(state: PlanExecutionState, id: string): PlanExecutionState {
|
|
102
|
+
const next = clone(state);
|
|
103
|
+
if (next.status === "completed") throw new Error("The plan is already complete");
|
|
104
|
+
const step = findStep(next, id);
|
|
105
|
+
if (step.status !== "ready") throw new Error("Only a ready step can be implemented");
|
|
106
|
+
step.status = "active";
|
|
107
|
+
next.status = "running";
|
|
108
|
+
next.selectedStepId = id;
|
|
109
|
+
return next;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function submitPlanStepForReview(state: PlanExecutionState, id: string, summary: string): PlanExecutionState {
|
|
113
|
+
const next = clone(state);
|
|
114
|
+
const step = findStep(next, id);
|
|
115
|
+
if (step.status !== "active") throw new Error("Only the active step can be submitted for review");
|
|
116
|
+
step.status = "review";
|
|
117
|
+
step.summary = summary.trim();
|
|
118
|
+
next.selectedStepId = id;
|
|
119
|
+
return next;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function completePlanStep(state: PlanExecutionState, id: string): PlanExecutionState {
|
|
123
|
+
const next = clone(state);
|
|
124
|
+
const step = findStep(next, id);
|
|
125
|
+
if (step.status !== "ready" && step.status !== "review") {
|
|
126
|
+
throw new Error("Only a ready or reviewed step can be marked complete");
|
|
127
|
+
}
|
|
128
|
+
step.status = "completed";
|
|
129
|
+
makeNextReady(next, id);
|
|
130
|
+
return next;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function acceptPlanStep(state: PlanExecutionState, id: string): PlanExecutionState {
|
|
134
|
+
const step = findStep(state, id);
|
|
135
|
+
if (step.status !== "review") throw new Error("Only a step awaiting review can be accepted");
|
|
136
|
+
return completePlanStep(state, id);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function requestPlanStepCorrections(state: PlanExecutionState, id: string): PlanExecutionState {
|
|
140
|
+
const next = clone(state);
|
|
141
|
+
const step = findStep(next, id);
|
|
142
|
+
if (step.status !== "review") throw new Error("Only a step awaiting review can receive corrections");
|
|
143
|
+
step.status = "active";
|
|
144
|
+
next.selectedStepId = id;
|
|
145
|
+
return next;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function skipPlanStep(state: PlanExecutionState, id: string): PlanExecutionState {
|
|
149
|
+
const next = clone(state);
|
|
150
|
+
const step = findStep(next, id);
|
|
151
|
+
if (step.status !== "ready") throw new Error("Only a ready step can be skipped");
|
|
152
|
+
step.status = "skipped";
|
|
153
|
+
makeNextReady(next, id);
|
|
154
|
+
return next;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function revisePlanStep(state: PlanExecutionState, id: string, text: string, planMarkdown = state.planMarkdown): PlanExecutionState {
|
|
158
|
+
const revised = text.trim();
|
|
159
|
+
if (!revised) throw new Error("A plan step cannot be empty");
|
|
160
|
+
const next = clone(state);
|
|
161
|
+
const step = findStep(next, id);
|
|
162
|
+
if (step.status !== "pending" && step.status !== "ready") throw new Error("Only an unimplemented step can be edited");
|
|
163
|
+
step.text = revised;
|
|
164
|
+
next.planMarkdown = planMarkdown;
|
|
165
|
+
return next;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function pausePlanExecution(state: PlanExecutionState): PlanExecutionState {
|
|
169
|
+
if (state.status === "completed") return state;
|
|
170
|
+
return { ...clone(state), status: state.status === "paused" ? "running" : "paused" };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function updatePlanChecklistStep(plan: string, sourceLine: number, text: string): string {
|
|
174
|
+
const newline = plan.includes("\r\n") ? "\r\n" : "\n";
|
|
175
|
+
const lines = plan.replace(/\r\n?/g, "\n").split("\n");
|
|
176
|
+
if (!Number.isInteger(sourceLine) || sourceLine < 0 || sourceLine >= lines.length || !CHECKLIST_ITEM.test(lines[sourceLine]!)) {
|
|
177
|
+
throw new Error("The saved plan changed and the selected checklist item can no longer be updated safely");
|
|
178
|
+
}
|
|
179
|
+
lines[sourceLine] = `- [ ] ${text.trim()}`;
|
|
180
|
+
return lines.join(newline);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function activePlanStep(state: PlanExecutionState | undefined): PlanStep | undefined {
|
|
184
|
+
return state?.steps.find((step) => step.status === "active");
|
|
185
|
+
}
|
package/plan-panel.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { PlanExecutionState } from "./plan-execution.ts";
|
|
4
|
+
|
|
5
|
+
const GLYPHS = {
|
|
6
|
+
pending: "○",
|
|
7
|
+
ready: "▷",
|
|
8
|
+
active: "▶",
|
|
9
|
+
review: "◆",
|
|
10
|
+
completed: "✓",
|
|
11
|
+
skipped: "–",
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
export class PlanPanel implements Component {
|
|
15
|
+
private state: PlanExecutionState;
|
|
16
|
+
private readonly theme: Theme;
|
|
17
|
+
|
|
18
|
+
constructor(state: PlanExecutionState, theme: Theme) {
|
|
19
|
+
this.state = state;
|
|
20
|
+
this.theme = theme;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
setState(state: PlanExecutionState): void {
|
|
24
|
+
this.state = state;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
invalidate(): void {}
|
|
28
|
+
|
|
29
|
+
render(width: number): string[] {
|
|
30
|
+
const safeWidth = Math.max(12, width);
|
|
31
|
+
const inner = Math.max(1, safeWidth - 2);
|
|
32
|
+
const contentWidth = Math.max(1, safeWidth - 4);
|
|
33
|
+
const border = (text: string) => this.theme.fg("borderMuted", text);
|
|
34
|
+
const pad = (content = "") => {
|
|
35
|
+
const clipped = truncateToWidth(content, contentWidth, "");
|
|
36
|
+
return `${border("│")} ${clipped}${" ".repeat(Math.max(0, contentWidth - visibleWidth(clipped)))} ${border("│")}`;
|
|
37
|
+
};
|
|
38
|
+
const done = this.state.steps.filter((step) => step.status === "completed" || step.status === "skipped").length;
|
|
39
|
+
const status = this.state.status === "completed" ? "complete" : this.state.status;
|
|
40
|
+
const currentIndex = this.state.steps.findIndex((step) => step.id === this.state.selectedStepId);
|
|
41
|
+
const lines = [
|
|
42
|
+
border(`╭${"─".repeat(inner)}╮`),
|
|
43
|
+
pad(`${this.theme.bold(this.theme.fg("accent", "Plan"))} ${this.theme.fg("dim", `${done}/${this.state.steps.length}`)}`),
|
|
44
|
+
pad(this.theme.fg(this.state.status === "paused" ? "warning" : "muted", status)),
|
|
45
|
+
border(`├${"─".repeat(inner)}┤`),
|
|
46
|
+
];
|
|
47
|
+
for (let index = 0; index < this.state.steps.length; index++) {
|
|
48
|
+
const step = this.state.steps[index]!;
|
|
49
|
+
const glyphColor = step.status === "completed" ? "success" : step.status === "review" ? "warning" : step.status === "active" || step.status === "ready" ? "accent" : "muted";
|
|
50
|
+
const prefix = `${this.theme.fg(glyphColor, GLYPHS[step.status])} ${index + 1}. `;
|
|
51
|
+
const text = step.status === "completed" || step.status === "skipped" ? this.theme.fg("muted", step.text) : step.text;
|
|
52
|
+
const wrapped = wrapTextWithAnsi(text, Math.max(1, contentWidth - visibleWidth(prefix)));
|
|
53
|
+
lines.push(pad(`${prefix}${wrapped[0] ?? ""}`));
|
|
54
|
+
const continuationIndent = " ".repeat(visibleWidth(prefix));
|
|
55
|
+
for (const continuation of wrapped.slice(1)) lines.push(pad(`${continuationIndent}${continuation}`));
|
|
56
|
+
if (index < this.state.steps.length - 1) lines.push(pad());
|
|
57
|
+
}
|
|
58
|
+
const current = currentIndex >= 0 ? this.state.steps[currentIndex] : undefined;
|
|
59
|
+
if (current) {
|
|
60
|
+
lines.push(border(`├${"─".repeat(inner)}┤`));
|
|
61
|
+
for (const detail of wrapTextWithAnsi(current.text, contentWidth).slice(0, 5)) lines.push(pad(detail));
|
|
62
|
+
if (current.summary) {
|
|
63
|
+
lines.push(pad(this.theme.fg("dim", "Result:")));
|
|
64
|
+
for (const summary of wrapTextWithAnsi(current.summary, contentWidth).slice(0, 4)) lines.push(pad(summary));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
lines.push(border(`├${"─".repeat(inner)}┤`));
|
|
68
|
+
if (this.state.status === "completed") lines.push(pad(this.theme.fg("success", "Plan complete")));
|
|
69
|
+
else if (this.state.status === "paused") lines.push(pad("Tell the agent to resume, cancel, or hide the plan."));
|
|
70
|
+
else if (current?.status === "ready") lines.push(pad("Tell the agent to implement, complete, edit, skip, cancel, or hide this step."));
|
|
71
|
+
else if (current?.status === "review") lines.push(pad("Tell the agent to accept, correct, or cancel the plan."));
|
|
72
|
+
else lines.push(pad("Plan progress updates automatically from your prompts."));
|
|
73
|
+
lines.push(border(`╰${"─".repeat(inner)}╯`));
|
|
74
|
+
return lines.map((line) => truncateToWidth(line, safeWidth, ""));
|
|
75
|
+
}
|
|
76
|
+
}
|
package/prompts.ts
CHANGED
|
@@ -44,6 +44,7 @@ The plan file is the only file you may edit, and only while finalizing the plan
|
|
|
44
44
|
- Be concise enough to scan quickly but detailed enough to implement.
|
|
45
45
|
- Identify the critical files that need modification.
|
|
46
46
|
- Include verification steps for testing the change end-to-end.
|
|
47
|
+
- End with a \`## Implementation Steps\` section containing the executable top-level steps as \`- [ ] ...\` checklist items. Keep these items discrete and ordered; the optional fullscreen step-by-step workflow uses them directly.
|
|
47
48
|
|
|
48
49
|
After writing the complete plan, call plan_exit to request approval. Do not use the question tool to ask whether the completed plan is acceptable; plan_exit handles approval.
|
|
49
50
|
</system-reminder>`;
|
|
@@ -51,6 +52,31 @@ After writing the complete plan, call plan_exit to request approval. Do not use
|
|
|
51
52
|
|
|
52
53
|
export const PLAN_ENTER_DESCRIPTION = `Use this tool when the user asks you to plan, when a request needs investigation before implementation, or when switching to the plan agent is the safest next step. The tool changes the current continuation to Plan mode.`;
|
|
53
54
|
|
|
55
|
+
export function buildPlanStepReminder(planPath: string, stepNumber: number, totalSteps: number, step: string): string {
|
|
56
|
+
return `<system-reminder>
|
|
57
|
+
# Step-by-Step Plan Execution
|
|
58
|
+
|
|
59
|
+
The approved plan is at ${planPath}. Implement only step ${stepNumber} of ${totalSteps}:
|
|
60
|
+
|
|
61
|
+
${step}
|
|
62
|
+
|
|
63
|
+
Do not begin any later plan step. Complete and verify this step, then call plan_step_complete with a concise result summary. If the user requests corrections, continue working only on this same step and submit it for review again.
|
|
64
|
+
</system-reminder>`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function buildPlanStepWaitingReminder(progress: string): string {
|
|
68
|
+
return `<system-reminder>
|
|
69
|
+
Step-by-step execution is waiting for the user's natural-language instruction. No plan step is currently approved for implementation. Do not modify the project or begin a pending step directly.
|
|
70
|
+
|
|
71
|
+
Current progress:
|
|
72
|
+
${progress}
|
|
73
|
+
|
|
74
|
+
Interpret the user's intent contextually rather than requiring exact phrases. Clear statements, confirmations, and feedback may request a state transition even when phrased non-imperatively. For example, a statement that a ready step is finished can use the complete action, while a positive review of agent work can use accept. If multiple materially different actions are plausible, ask a brief clarification. Cancellation is always available when the user clearly wants to stop. Do not advance based on hypothetical, uncertain, or unrelated discussion. The sidebar is a passive visual aid and cannot receive input.
|
|
75
|
+
</system-reminder>`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const PLAN_STEP_COMPLETE_DESCRIPTION = `Call this tool after implementing and verifying the currently active plan step. It stops execution and returns control to the user for review. Do not call it before the active step is complete, and never begin the next step yourself.`;
|
|
79
|
+
|
|
54
80
|
export const PLAN_EXIT_DESCRIPTION = `Use this tool when you have completed the planning phase and are ready to exit plan agent.
|
|
55
81
|
|
|
56
82
|
This tool displays the complete plan and asks the user whether to implement it in this session, prepare a clean-session implementation, or stay in Plan mode.
|