@narumitw/pi-plan-mode 0.1.27 → 0.1.29
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 +4 -4
- package/package.json +1 -1
- package/src/plan-mode.ts +80 -17
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Pi core intentionally does not ship a built-in plan mode; this package provides
|
|
|
14
14
|
- Disables extension and custom tools by default, with a `/plan tools` selector for explicit user-risk opt-in.
|
|
15
15
|
- Blocks mutating built-in tools and bash commands such as `rm`, `git commit`, dependency installs, redirects, and editor launches.
|
|
16
16
|
- Injects Codex-like Plan mode instructions: explore first, ask only non-discoverable questions, do not mutate files, and finish with `<proposed_plan>`.
|
|
17
|
-
- Detects proposed plan blocks and prompts you to implement, revise,
|
|
17
|
+
- Detects proposed plan blocks and prompts you to implement, revise, stay in Plan mode, or exit and discard the plan.
|
|
18
18
|
- Shows Plan mode state in Pi's statusline as `📝 plan active` or `📝 plan ready`.
|
|
19
19
|
- Persists Plan mode state in the Pi session so resume restores the mode.
|
|
20
20
|
|
|
@@ -72,14 +72,14 @@ A complete Plan mode answer should include exactly one block like this:
|
|
|
72
72
|
</proposed_plan>
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
After a proposed plan is detected, `/plan` lets you choose whether to implement the plan, revise it, stay in Plan mode, or exit Plan mode. Choosing implementation disables Plan mode, restores full tool access, and immediately starts an implementation turn with the proposed plan.
|
|
75
|
+
After a proposed plan is detected, `/plan` lets you choose whether to implement the plan, revise it, stay in Plan mode, or exit Plan mode. Choosing implementation disables Plan mode, restores full tool access, and immediately starts an implementation turn with the proposed plan. Choosing exit/off disables Plan mode and discards the proposed plan so it is not carried into later non-plan turns.
|
|
76
76
|
|
|
77
77
|
While Plan mode is enabled, the extension also publishes a compact status for Pi statuslines. With `@narumitw/pi-statusline`, this appears in the extension status area:
|
|
78
78
|
|
|
79
79
|
- `📝 plan active`: Plan mode is enabled and still gathering context or drafting a plan.
|
|
80
80
|
- `📝 plan ready`: A `<proposed_plan>` was detected and is waiting for your next `/plan` action.
|
|
81
81
|
|
|
82
|
-
You can also exit directly:
|
|
82
|
+
You can also exit directly. Direct exit discards the latest proposed plan instead of treating it as an implementation request:
|
|
83
83
|
|
|
84
84
|
```text
|
|
85
85
|
/plan exit
|
|
@@ -92,7 +92,7 @@ This extension maps Codex's `ModeKind::Plan` behavior onto Pi's extension API:
|
|
|
92
92
|
- Plan mode is a conversational collaboration mode, not TODO/progress tracking.
|
|
93
93
|
- `/plan <prompt>` follows Codex behavior by switching to Plan mode before submitting the inline prompt.
|
|
94
94
|
- `update_plan`-style checklist use is discouraged while Plan mode is active.
|
|
95
|
-
- The implementation boundary is explicit: Plan mode restores tools before starting implementation,
|
|
95
|
+
- The implementation boundary is explicit: Plan mode restores tools before starting implementation, choosing implementation immediately triggers a normal agent turn with full tool access, and plain exit/off discards the proposed plan.
|
|
96
96
|
- Pi extension safety is approximated with built-in tool restriction plus bash filtering; non-built-in tools are user-selected at user risk because Plan mode does not classify extension/custom tool behavior.
|
|
97
97
|
|
|
98
98
|
## 🗂️ Package layout
|
package/package.json
CHANGED
package/src/plan-mode.ts
CHANGED
|
@@ -3,12 +3,16 @@ import type { ExtensionAPI, ExtensionContext, ToolInfo } from "@mariozechner/pi-
|
|
|
3
3
|
const STATE_ENTRY_TYPE = "plan-mode-state";
|
|
4
4
|
const STATUS_KEY = "plan-mode";
|
|
5
5
|
const PLAN_WIDGET_KEY = "plan-mode-plan";
|
|
6
|
+
const PLAN_CONTEXT_MESSAGE_TYPE = "plan-mode-context";
|
|
7
|
+
const PROPOSED_PLAN_MESSAGE_TYPE = "proposed-plan";
|
|
8
|
+
const PLAN_IMPLEMENTATION_MESSAGE_TYPE = "plan-mode-implementation";
|
|
6
9
|
const PLAN_CONTEXT_MARKER = "[CODEX-LIKE PLAN MODE ACTIVE]";
|
|
7
10
|
const SAFE_BUILTIN_PLAN_TOOLS = new Set(["read", "bash", "grep", "find", "ls"]);
|
|
8
11
|
const BLOCKED_BUILTIN_TOOLS = new Set(["edit", "write"]);
|
|
9
12
|
const DEFAULT_TOOLS = ["read", "bash", "edit", "write"];
|
|
10
13
|
const TOOL_SELECTOR_PAGE_SIZE = 10;
|
|
11
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;
|
|
12
16
|
|
|
13
17
|
interface PlanModeState {
|
|
14
18
|
enabled: boolean;
|
|
@@ -95,7 +99,7 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
95
99
|
const command = prompt.toLowerCase();
|
|
96
100
|
if (command === "exit" || command === "off") {
|
|
97
101
|
exitPlanMode(ctx);
|
|
98
|
-
ctx.ui.notify("Plan mode disabled.
|
|
102
|
+
ctx.ui.notify("Plan mode disabled. Proposed plan discarded.", "info");
|
|
99
103
|
return;
|
|
100
104
|
}
|
|
101
105
|
if (command === "tools") {
|
|
@@ -150,7 +154,9 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
150
154
|
pi.on("context", async (event) => {
|
|
151
155
|
if (state.enabled) return;
|
|
152
156
|
return {
|
|
153
|
-
messages: event.messages
|
|
157
|
+
messages: event.messages
|
|
158
|
+
.filter((message: unknown) => !messageContainsInactivePlanModeArtifact(message))
|
|
159
|
+
.map(stripProposedPlanBlocksFromMessage),
|
|
154
160
|
};
|
|
155
161
|
});
|
|
156
162
|
|
|
@@ -159,7 +165,7 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
159
165
|
applyPlanModeTools();
|
|
160
166
|
return {
|
|
161
167
|
message: {
|
|
162
|
-
customType:
|
|
168
|
+
customType: PLAN_CONTEXT_MESSAGE_TYPE,
|
|
163
169
|
content: buildPlanModePrompt(),
|
|
164
170
|
display: false,
|
|
165
171
|
},
|
|
@@ -180,16 +186,18 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
180
186
|
state = { ...state, latestPlan: proposedPlan, awaitingAction: true };
|
|
181
187
|
persistState();
|
|
182
188
|
updateUi(ctx);
|
|
189
|
+
|
|
190
|
+
if (ctx.hasUI) await showPlanReadyMenu(ctx);
|
|
191
|
+
if (!state.enabled || !state.latestPlan) return;
|
|
192
|
+
|
|
183
193
|
pi.sendMessage(
|
|
184
194
|
{
|
|
185
|
-
customType:
|
|
195
|
+
customType: PROPOSED_PLAN_MESSAGE_TYPE,
|
|
186
196
|
content: `**Proposed Plan**\n\n${proposedPlan}`,
|
|
187
197
|
display: true,
|
|
188
198
|
},
|
|
189
199
|
{ triggerTurn: false },
|
|
190
200
|
);
|
|
191
|
-
|
|
192
|
-
if (ctx.hasUI) await showPlanReadyMenu(ctx);
|
|
193
201
|
});
|
|
194
202
|
|
|
195
203
|
function enterPlanMode(ctx: ExtensionContext) {
|
|
@@ -211,8 +219,9 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
211
219
|
}
|
|
212
220
|
|
|
213
221
|
function exitPlanMode(ctx: ExtensionContext) {
|
|
214
|
-
|
|
215
|
-
|
|
222
|
+
const wasEnabled = state.enabled;
|
|
223
|
+
state = { ...state, enabled: false, latestPlan: undefined, awaitingAction: false };
|
|
224
|
+
if (wasEnabled) restoreTools();
|
|
216
225
|
persistState();
|
|
217
226
|
updateUi(ctx);
|
|
218
227
|
}
|
|
@@ -228,7 +237,7 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
228
237
|
|
|
229
238
|
pi.sendMessage(
|
|
230
239
|
{
|
|
231
|
-
customType:
|
|
240
|
+
customType: PLAN_IMPLEMENTATION_MESSAGE_TYPE,
|
|
232
241
|
content: `Plan mode is now disabled. Full tool access is restored. Implement this proposed plan now:\n\n${plan}`,
|
|
233
242
|
display: true,
|
|
234
243
|
},
|
|
@@ -266,7 +275,7 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
266
275
|
}
|
|
267
276
|
if (choice === "Exit Plan mode") {
|
|
268
277
|
exitPlanMode(ctx);
|
|
269
|
-
ctx.ui.notify("Plan mode disabled.
|
|
278
|
+
ctx.ui.notify("Plan mode disabled. Proposed plan discarded.", "info");
|
|
270
279
|
return;
|
|
271
280
|
}
|
|
272
281
|
updateUi(ctx);
|
|
@@ -278,6 +287,7 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
278
287
|
"Revise plan",
|
|
279
288
|
"Configure Plan-mode tools",
|
|
280
289
|
"Stay in Plan mode",
|
|
290
|
+
"Exit Plan mode",
|
|
281
291
|
]);
|
|
282
292
|
if (choice === "Implement this plan") {
|
|
283
293
|
startImplementation(ctx);
|
|
@@ -290,6 +300,11 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
290
300
|
if (choice === "Revise plan") {
|
|
291
301
|
const refinement = await ctx.ui.editor("Revise the plan", "");
|
|
292
302
|
if (refinement?.trim()) pi.sendUserMessage(refinement.trim());
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (choice === "Exit Plan mode") {
|
|
306
|
+
exitPlanMode(ctx);
|
|
307
|
+
ctx.ui.notify("Plan mode disabled. Proposed plan discarded.", "info");
|
|
293
308
|
}
|
|
294
309
|
}
|
|
295
310
|
|
|
@@ -446,10 +461,11 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
446
461
|
.filter((candidate) => candidate.type === "custom" && candidate.customType === STATE_ENTRY_TYPE)
|
|
447
462
|
.pop();
|
|
448
463
|
if (!entry?.data) return;
|
|
464
|
+
const enabled = entry.data.enabled ?? false;
|
|
449
465
|
state = {
|
|
450
|
-
enabled
|
|
451
|
-
latestPlan: entry.data.latestPlan,
|
|
452
|
-
awaitingAction: entry.data.awaitingAction ?? false,
|
|
466
|
+
enabled,
|
|
467
|
+
latestPlan: enabled ? entry.data.latestPlan : undefined,
|
|
468
|
+
awaitingAction: enabled ? (entry.data.awaitingAction ?? false) : false,
|
|
453
469
|
selectedToolNames: entry.data.selectedToolNames,
|
|
454
470
|
selectedToolKeys: entry.data.selectedToolKeys,
|
|
455
471
|
};
|
|
@@ -616,10 +632,57 @@ function latestAssistantText(messages: unknown) {
|
|
|
616
632
|
return "";
|
|
617
633
|
}
|
|
618
634
|
|
|
619
|
-
function
|
|
620
|
-
const candidate = message
|
|
621
|
-
|
|
622
|
-
|
|
635
|
+
function messageContainsInactivePlanModeArtifact(message: unknown) {
|
|
636
|
+
const candidate = unwrapSessionMessage(message);
|
|
637
|
+
return (
|
|
638
|
+
candidate.customType === PLAN_CONTEXT_MESSAGE_TYPE ||
|
|
639
|
+
candidate.customType === PROPOSED_PLAN_MESSAGE_TYPE ||
|
|
640
|
+
contentText(candidate.content).includes(PLAN_CONTEXT_MARKER)
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function stripProposedPlanBlocksFromMessage<T>(message: T): T {
|
|
645
|
+
const candidate = unwrapSessionMessage(message);
|
|
646
|
+
if (candidate.role !== "assistant") return message;
|
|
647
|
+
|
|
648
|
+
const content = stripProposedPlanBlocksFromContent(candidate.content);
|
|
649
|
+
if (content === candidate.content) return message;
|
|
650
|
+
|
|
651
|
+
if (isSessionMessageEntry(message)) {
|
|
652
|
+
return { ...message, message: { ...candidate, content } };
|
|
653
|
+
}
|
|
654
|
+
return { ...candidate, content } as T;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function unwrapSessionMessage(message: unknown) {
|
|
658
|
+
const entry = message as { message?: unknown };
|
|
659
|
+
return (entry.message ?? message) as { role?: string; customType?: string; content?: unknown };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function isSessionMessageEntry<T>(message: T): message is T & { message: SessionMessage } {
|
|
663
|
+
return typeof message === "object" && message !== null && "message" in message;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function stripProposedPlanBlocksFromContent(content: unknown) {
|
|
667
|
+
if (typeof content === "string") return stripProposedPlanBlocks(content);
|
|
668
|
+
if (!Array.isArray(content)) return content;
|
|
669
|
+
|
|
670
|
+
let changed = false;
|
|
671
|
+
const nextContent = content.map((block) => {
|
|
672
|
+
const textBlock = block as TextBlock;
|
|
673
|
+
if (textBlock.type !== "text" || typeof textBlock.text !== "string") return block;
|
|
674
|
+
|
|
675
|
+
const text = stripProposedPlanBlocks(textBlock.text);
|
|
676
|
+
if (text === textBlock.text) return block;
|
|
677
|
+
|
|
678
|
+
changed = true;
|
|
679
|
+
return { ...textBlock, text };
|
|
680
|
+
});
|
|
681
|
+
return changed ? nextContent : content;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function stripProposedPlanBlocks(text: string) {
|
|
685
|
+
return text.replace(PROPOSED_PLAN_BLOCK_PATTERN, "");
|
|
623
686
|
}
|
|
624
687
|
|
|
625
688
|
function messageText(message: SessionMessage) {
|