@dungle-scrubs/tallow 0.8.28 → 0.9.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/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +2 -9
- package/dist/install.js.map +1 -1
- package/dist/interactive-mode-patch.d.ts.map +1 -1
- package/dist/interactive-mode-patch.js +20 -9
- package/dist/interactive-mode-patch.js.map +1 -1
- package/extensions/_icons/__tests__/icons.test.ts +0 -1
- package/extensions/_icons/index.ts +0 -2
- package/extensions/context-fork/__tests__/context-fork.test.ts +9 -0
- package/extensions/health/index.ts +1 -1
- package/extensions/render-stabilizer/__tests__/render-stabilizer.test.ts +42 -0
- package/extensions/render-stabilizer/extension.json +5 -0
- package/extensions/render-stabilizer/index.ts +66 -0
- package/extensions/subagent-tool/__tests__/auto-cheap-model.test.ts +66 -6
- package/extensions/subagent-tool/__tests__/model-router-explicit-resolution.test.ts +79 -5
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts +47 -0
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.js +139 -5
- package/node_modules/@mariozechner/pi-tui/dist/tui.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/src/tui.ts +142 -5
- package/package.json +1 -1
- package/schemas/settings.schema.json +0 -5
- package/extensions/plan-mode-tool/__tests__/e2e.mjs +0 -350
- package/extensions/plan-mode-tool/__tests__/index.test.ts +0 -213
- package/extensions/plan-mode-tool/__tests__/utils.test.ts +0 -381
- package/extensions/plan-mode-tool/extension.json +0 -22
- package/extensions/plan-mode-tool/index.ts +0 -583
- package/extensions/plan-mode-tool/utils.ts +0 -257
|
@@ -1,583 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plan Mode Extension
|
|
3
|
-
*
|
|
4
|
-
* Read-only exploration mode for safe code analysis.
|
|
5
|
-
* When enabled, only read-only tools are available.
|
|
6
|
-
*
|
|
7
|
-
* Features:
|
|
8
|
-
* - /plan-mode command or Ctrl+Alt+P to toggle
|
|
9
|
-
* - Strict fail-closed tool allowlist while plan mode is active
|
|
10
|
-
* - Bash restricted to allowlisted read-only commands
|
|
11
|
-
* - Extracts numbered plan steps from "Plan:" sections
|
|
12
|
-
* - Delegates execution tracking to the tasks extension
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
16
|
-
import type { AssistantMessage, TextContent } from "@mariozechner/pi-ai";
|
|
17
|
-
import type { KeybindingsManager } from "@mariozechner/pi-coding-agent";
|
|
18
|
-
import {
|
|
19
|
-
CustomEditor,
|
|
20
|
-
type ExtensionAPI,
|
|
21
|
-
type ExtensionContext,
|
|
22
|
-
} from "@mariozechner/pi-coding-agent";
|
|
23
|
-
import {
|
|
24
|
-
type EditorTheme,
|
|
25
|
-
Key,
|
|
26
|
-
Loader,
|
|
27
|
-
type TUI,
|
|
28
|
-
truncateToWidth,
|
|
29
|
-
visibleWidth,
|
|
30
|
-
} from "@mariozechner/pi-tui";
|
|
31
|
-
import { Type } from "@sinclair/typebox";
|
|
32
|
-
import { getIcon } from "../_icons/index.js";
|
|
33
|
-
import { renderBorderedBox } from "../_shared/bordered-box.js";
|
|
34
|
-
import {
|
|
35
|
-
detectPlanIntent,
|
|
36
|
-
extractTodoItems,
|
|
37
|
-
isPlanModeToolAllowed,
|
|
38
|
-
isSafeCommand,
|
|
39
|
-
PLAN_MODE_ALLOWED_TOOLS,
|
|
40
|
-
stripPlanIntent,
|
|
41
|
-
type TodoItem,
|
|
42
|
-
} from "./utils.js";
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Type guard to check if a message is an assistant message.
|
|
46
|
-
* @param m - The message to check
|
|
47
|
-
* @returns true if the message is from the assistant
|
|
48
|
-
*/
|
|
49
|
-
function isAssistantMessage(m: AgentMessage): m is AssistantMessage {
|
|
50
|
-
return m.role === "assistant" && Array.isArray(m.content);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Extracts all text content from an assistant message.
|
|
55
|
-
* @param message - The assistant message to extract text from
|
|
56
|
-
* @returns Concatenated text content
|
|
57
|
-
*/
|
|
58
|
-
function getTextContent(message: AssistantMessage): string {
|
|
59
|
-
return message.content
|
|
60
|
-
.filter((block): block is TextContent => block.type === "text")
|
|
61
|
-
.map((block) => block.text)
|
|
62
|
-
.join("\n");
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Plan mode label shown in the editor border */
|
|
66
|
-
const PLAN_LABEL = ` ${getIcon("plan_mode")} PLAN `;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Custom editor that renders a warning-colored border in plan mode.
|
|
70
|
-
* Extends CustomEditor to preserve all app keybindings.
|
|
71
|
-
*/
|
|
72
|
-
class PlanModeEditor extends CustomEditor {
|
|
73
|
-
constructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) {
|
|
74
|
-
super(tui, { ...theme, borderColor: (s: string) => `\x1b[33m${s}\x1b[39m` }, keybindings);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Renders the editor with a PLAN label in the top border.
|
|
79
|
-
* @param width - Available width
|
|
80
|
-
* @returns Array of rendered lines
|
|
81
|
-
*/
|
|
82
|
-
override render(width: number): string[] {
|
|
83
|
-
const lines = super.render(width);
|
|
84
|
-
if (lines.length > 0) {
|
|
85
|
-
const label = `\x1b[33;1m${PLAN_LABEL}\x1b[22;39m`;
|
|
86
|
-
const first = lines[0];
|
|
87
|
-
const vis = visibleWidth(first);
|
|
88
|
-
const labelVis = visibleWidth(PLAN_LABEL);
|
|
89
|
-
if (vis >= labelVis + 4) {
|
|
90
|
-
lines[0] = truncateToWidth(first, width - labelVis, "") + label;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return lines;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Registers the plan mode extension with Pi.
|
|
99
|
-
* Provides read-only exploration mode with progress tracking.
|
|
100
|
-
* @param pi - The Pi extension API
|
|
101
|
-
*/
|
|
102
|
-
export default function planModeExtension(pi: ExtensionAPI): void {
|
|
103
|
-
let planModeEnabled = false;
|
|
104
|
-
let todoItems: TodoItem[] = [];
|
|
105
|
-
let normalModeTools: string[] = [];
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Capture the active tools used outside plan mode.
|
|
109
|
-
*
|
|
110
|
-
* @returns Snapshot of normal-mode tools
|
|
111
|
-
*/
|
|
112
|
-
function captureNormalModeTools(): string[] {
|
|
113
|
-
const activeTools = pi.getActiveTools();
|
|
114
|
-
normalModeTools =
|
|
115
|
-
activeTools.length > 0 ? [...activeTools] : pi.getAllTools().map((t) => t.name);
|
|
116
|
-
return normalModeTools;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Resolve allowlisted tools that exist in the current session.
|
|
121
|
-
*
|
|
122
|
-
* @returns Plan-mode tool list constrained to the strict allowlist
|
|
123
|
-
*/
|
|
124
|
-
function getPlanModeTools(): string[] {
|
|
125
|
-
const availableTools = new Set(pi.getAllTools().map((t) => t.name));
|
|
126
|
-
return PLAN_MODE_ALLOWED_TOOLS.filter((name) => availableTools.has(name));
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Apply strict read-only tool policy for plan mode.
|
|
131
|
-
*
|
|
132
|
-
* @returns The active allowlisted tool names
|
|
133
|
-
*/
|
|
134
|
-
function applyPlanModeTools(): string[] {
|
|
135
|
-
const tools = getPlanModeTools();
|
|
136
|
-
pi.setActiveTools(tools);
|
|
137
|
-
return tools;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Restore the normal tool set captured before plan mode was enabled.
|
|
142
|
-
*
|
|
143
|
-
* @returns Restored normal-mode tool names
|
|
144
|
-
*/
|
|
145
|
-
function restoreNormalModeTools(): string[] {
|
|
146
|
-
if (normalModeTools.length === 0) {
|
|
147
|
-
captureNormalModeTools();
|
|
148
|
-
}
|
|
149
|
-
pi.setActiveTools(normalModeTools);
|
|
150
|
-
return normalModeTools;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
pi.registerFlag("plan", {
|
|
154
|
-
description: "Start in plan mode (read-only exploration)",
|
|
155
|
-
type: "boolean",
|
|
156
|
-
default: false,
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Updates visual indicators: footer status, editor border, and widgets.
|
|
161
|
-
* Plan mode gets a warning-colored editor border with PLAN label,
|
|
162
|
-
* a custom footer bar, and the todo widget when executing.
|
|
163
|
-
* @param ctx - The extension context
|
|
164
|
-
*/
|
|
165
|
-
function updateStatus(ctx: ExtensionContext): void {
|
|
166
|
-
// Footer status — plan mode only
|
|
167
|
-
if (planModeEnabled) {
|
|
168
|
-
ctx.ui.setStatus(
|
|
169
|
-
"plan-mode",
|
|
170
|
-
ctx.ui.theme.fg("warning", `${getIcon("plan_mode")} PLAN MODE — read-only`)
|
|
171
|
-
);
|
|
172
|
-
} else {
|
|
173
|
-
ctx.ui.setStatus("plan-mode", undefined);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Editor border: warning-colored in plan mode, default otherwise
|
|
177
|
-
if (planModeEnabled) {
|
|
178
|
-
ctx.ui.setEditorComponent(
|
|
179
|
-
(tui, theme, keybindings) => new PlanModeEditor(tui, theme, keybindings)
|
|
180
|
-
);
|
|
181
|
-
} else {
|
|
182
|
-
ctx.ui.setEditorComponent(undefined);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Full-width banner above editor — plan mode only.
|
|
186
|
-
if (planModeEnabled) {
|
|
187
|
-
ctx.ui.setWidget("plan-banner", (_tui, theme) => {
|
|
188
|
-
const label = " PLAN MODE — READ ONLY ";
|
|
189
|
-
return {
|
|
190
|
-
render: (width: number) => [
|
|
191
|
-
theme.bg("customMessageBg", theme.fg("customMessageLabel", label.padEnd(width))),
|
|
192
|
-
],
|
|
193
|
-
invalidate() {},
|
|
194
|
-
};
|
|
195
|
-
});
|
|
196
|
-
} else {
|
|
197
|
-
ctx.ui.setWidget("plan-banner", undefined);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
ctx.ui.setWidget("plan-todos", undefined);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Toggles plan mode on or off.
|
|
205
|
-
* @param ctx - The extension context
|
|
206
|
-
*/
|
|
207
|
-
function togglePlanMode(ctx: ExtensionContext): void {
|
|
208
|
-
planModeEnabled = !planModeEnabled;
|
|
209
|
-
todoItems = [];
|
|
210
|
-
|
|
211
|
-
if (planModeEnabled) {
|
|
212
|
-
captureNormalModeTools();
|
|
213
|
-
const tools = applyPlanModeTools();
|
|
214
|
-
ctx.ui.notify(`Plan mode enabled. Strict read-only tools: ${tools.join(", ")}`);
|
|
215
|
-
} else {
|
|
216
|
-
restoreNormalModeTools();
|
|
217
|
-
ctx.ui.notify("Plan mode disabled. Previous tool access restored.");
|
|
218
|
-
}
|
|
219
|
-
updateStatus(ctx);
|
|
220
|
-
persistState();
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Persists the current plan mode state to the session.
|
|
225
|
-
*/
|
|
226
|
-
function persistState(): void {
|
|
227
|
-
pi.appendEntry("plan-mode", {
|
|
228
|
-
enabled: planModeEnabled,
|
|
229
|
-
normalTools: normalModeTools,
|
|
230
|
-
todos: todoItems,
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
pi.registerCommand("plan-mode", {
|
|
235
|
-
description: "Toggle plan mode (read-only exploration)",
|
|
236
|
-
handler: async (_args, ctx) => togglePlanMode(ctx),
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
pi.registerCommand("todos", {
|
|
240
|
-
description: "Show current plan todo list",
|
|
241
|
-
handler: async (_args, ctx) => {
|
|
242
|
-
if (todoItems.length === 0) {
|
|
243
|
-
ctx.ui.notify("No todos. Create a plan first with /plan-mode", "info");
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
const list = todoItems
|
|
247
|
-
.map(
|
|
248
|
-
(item, i) =>
|
|
249
|
-
`${i + 1}. ${item.completed ? getIcon("success") : getIcon("idle")} ${item.text}`
|
|
250
|
-
)
|
|
251
|
-
.join("\n");
|
|
252
|
-
ctx.ui.notify(`Plan Progress:\n${list}`, "info");
|
|
253
|
-
},
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
pi.registerShortcut(Key.ctrlAlt("p"), {
|
|
257
|
-
description: "Toggle plan mode",
|
|
258
|
-
handler: async (ctx) => togglePlanMode(ctx),
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
// Tool for the agent to toggle plan mode programmatically
|
|
262
|
-
pi.registerTool({
|
|
263
|
-
name: "plan_mode",
|
|
264
|
-
label: "plan_mode",
|
|
265
|
-
description: `Toggle plan mode on or off. Plan mode is a strict read-only exploration mode for safe code analysis.
|
|
266
|
-
|
|
267
|
-
When enabled:
|
|
268
|
-
- Only allowlisted read-only tools are available (read, bash, grep, find, ls, questionnaire, plan_mode)
|
|
269
|
-
- All other tools are blocked fail-closed (including extension tools)
|
|
270
|
-
- Bash is additionally restricted to safe read-only commands
|
|
271
|
-
|
|
272
|
-
Use action "enable" to enter plan mode, "disable" to exit, or "status" to check current state.`,
|
|
273
|
-
parameters: Type.Object({
|
|
274
|
-
action: Type.Union(
|
|
275
|
-
[Type.Literal("enable"), Type.Literal("disable"), Type.Literal("status")],
|
|
276
|
-
{
|
|
277
|
-
description: "Whether to enable, disable, or check plan mode status",
|
|
278
|
-
}
|
|
279
|
-
),
|
|
280
|
-
}),
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Toggles plan mode or reports current status.
|
|
284
|
-
* @param _toolCallId - Unique identifier for this tool call
|
|
285
|
-
* @param params - Action to perform (enable/disable/status)
|
|
286
|
-
* @param _signal - Abort signal
|
|
287
|
-
* @param _onUpdate - Update callback
|
|
288
|
-
* @param ctx - Extension context
|
|
289
|
-
* @returns Tool result with the new state
|
|
290
|
-
*/
|
|
291
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
292
|
-
const { action } = params;
|
|
293
|
-
|
|
294
|
-
if (action === "status") {
|
|
295
|
-
const mode = planModeEnabled ? "planning" : "normal";
|
|
296
|
-
const tools = planModeEnabled ? getPlanModeTools() : pi.getActiveTools();
|
|
297
|
-
return {
|
|
298
|
-
content: [
|
|
299
|
-
{
|
|
300
|
-
type: "text",
|
|
301
|
-
text: `Plan mode: ${mode}\nActive tools: ${tools.join(", ")}${
|
|
302
|
-
todoItems.length > 0
|
|
303
|
-
? `\nTodos: ${todoItems.filter((t) => t.completed).length}/${todoItems.length} completed`
|
|
304
|
-
: ""
|
|
305
|
-
}`,
|
|
306
|
-
},
|
|
307
|
-
],
|
|
308
|
-
details: {},
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const shouldEnable = action === "enable";
|
|
313
|
-
|
|
314
|
-
if (shouldEnable === planModeEnabled) {
|
|
315
|
-
return {
|
|
316
|
-
content: [
|
|
317
|
-
{
|
|
318
|
-
type: "text",
|
|
319
|
-
text: `Plan mode is already ${shouldEnable ? "enabled" : "disabled"}.`,
|
|
320
|
-
},
|
|
321
|
-
],
|
|
322
|
-
details: {},
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
planModeEnabled = shouldEnable;
|
|
327
|
-
todoItems = [];
|
|
328
|
-
|
|
329
|
-
let activeTools: string[];
|
|
330
|
-
if (planModeEnabled) {
|
|
331
|
-
captureNormalModeTools();
|
|
332
|
-
activeTools = applyPlanModeTools();
|
|
333
|
-
} else {
|
|
334
|
-
activeTools = restoreNormalModeTools();
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (ctx.hasUI) {
|
|
338
|
-
updateStatus(ctx);
|
|
339
|
-
}
|
|
340
|
-
persistState();
|
|
341
|
-
|
|
342
|
-
return {
|
|
343
|
-
content: [
|
|
344
|
-
{
|
|
345
|
-
type: "text",
|
|
346
|
-
text: planModeEnabled
|
|
347
|
-
? `Plan mode enabled. Strict allowlist active: ${activeTools.join(", ")}. All other tools are blocked.`
|
|
348
|
-
: "Plan mode disabled. Previous tool access restored.",
|
|
349
|
-
},
|
|
350
|
-
],
|
|
351
|
-
details: {},
|
|
352
|
-
};
|
|
353
|
-
},
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
// Enforce strict plan-mode allowlist and safe bash commands
|
|
357
|
-
pi.on("tool_call", async (event, ctx) => {
|
|
358
|
-
if (!planModeEnabled) return;
|
|
359
|
-
|
|
360
|
-
if (!isPlanModeToolAllowed(event.toolName)) {
|
|
361
|
-
const reason =
|
|
362
|
-
`Plan mode: tool "${event.toolName}" blocked (not in strict read-only allowlist). ` +
|
|
363
|
-
"Disable plan mode first to use this tool.";
|
|
364
|
-
ctx.ui?.notify(`⛔ ${reason}`, "error");
|
|
365
|
-
return { block: true, reason };
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (event.toolName !== "bash") return;
|
|
369
|
-
|
|
370
|
-
const command =
|
|
371
|
-
typeof event.input.command === "string" ? event.input.command : String(event.input.command);
|
|
372
|
-
if (!isSafeCommand(command)) {
|
|
373
|
-
const reason =
|
|
374
|
-
"Plan mode: bash command blocked (not in read-only command allowlist). " +
|
|
375
|
-
`Disable plan mode first to run it.\nCommand: ${command}`;
|
|
376
|
-
ctx.ui?.notify(`⛔ ${reason}`, "error");
|
|
377
|
-
return { block: true, reason };
|
|
378
|
-
}
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
// Auto-enable plan mode when a human interactive session explicitly signals planning intent.
|
|
382
|
-
pi.on("input", async (event, ctx) => {
|
|
383
|
-
// No-op if already in plan mode
|
|
384
|
-
if (planModeEnabled) {
|
|
385
|
-
return { action: "continue" as const };
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Headless/orchestrated prompts should never toggle workflow modes via string matching.
|
|
389
|
-
if (!ctx.hasUI || event.source !== "interactive") {
|
|
390
|
-
return { action: "continue" as const };
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (!detectPlanIntent(event.text)) {
|
|
394
|
-
return { action: "continue" as const };
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Auto-enable plan mode
|
|
398
|
-
planModeEnabled = true;
|
|
399
|
-
captureNormalModeTools();
|
|
400
|
-
applyPlanModeTools();
|
|
401
|
-
updateStatus(ctx);
|
|
402
|
-
persistState();
|
|
403
|
-
|
|
404
|
-
ctx.ui?.notify(
|
|
405
|
-
"Plan mode auto-enabled (detected planning intent). Use /plan-mode or Ctrl+Alt+P to disable.",
|
|
406
|
-
"info"
|
|
407
|
-
);
|
|
408
|
-
|
|
409
|
-
// Strip the plan-intent phrase, keep the actual request
|
|
410
|
-
const stripped = stripPlanIntent(event.text);
|
|
411
|
-
if (stripped !== event.text) {
|
|
412
|
-
return { action: "transform" as const, text: stripped };
|
|
413
|
-
}
|
|
414
|
-
return { action: "continue" as const };
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
// Filter out stale plan mode context when not in plan mode
|
|
418
|
-
pi.on("context", async (event) => {
|
|
419
|
-
if (planModeEnabled) return;
|
|
420
|
-
|
|
421
|
-
return {
|
|
422
|
-
messages: event.messages.filter((m) => {
|
|
423
|
-
const msg = m as AgentMessage & { customType?: string };
|
|
424
|
-
if (msg.customType === "plan-mode-context") return false;
|
|
425
|
-
if (msg.role !== "user") return true;
|
|
426
|
-
|
|
427
|
-
const content = msg.content;
|
|
428
|
-
if (typeof content === "string") {
|
|
429
|
-
return !content.includes("[PLAN MODE ACTIVE]");
|
|
430
|
-
}
|
|
431
|
-
if (Array.isArray(content)) {
|
|
432
|
-
return !content.some(
|
|
433
|
-
(c) => c.type === "text" && (c as TextContent).text?.includes("[PLAN MODE ACTIVE]")
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
return true;
|
|
437
|
-
}),
|
|
438
|
-
};
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
// Inject plan/execution context before agent starts
|
|
442
|
-
pi.on("before_agent_start", async () => {
|
|
443
|
-
if (planModeEnabled) {
|
|
444
|
-
return {
|
|
445
|
-
message: {
|
|
446
|
-
customType: "plan-mode-context",
|
|
447
|
-
content: `[PLAN MODE ACTIVE]
|
|
448
|
-
You are in plan mode - a read-only exploration mode for safe code analysis.
|
|
449
|
-
|
|
450
|
-
Restrictions:
|
|
451
|
-
- You can only use strict allowlisted read-only tools: read, bash, grep, find, ls, questionnaire, plan_mode
|
|
452
|
-
- All other tools are blocked fail-closed (including edit, write, bg_bash, subagent, and mcp__* tools)
|
|
453
|
-
- Bash is additionally restricted to an allowlist of read-only commands
|
|
454
|
-
|
|
455
|
-
Ask clarifying questions using the questionnaire tool.
|
|
456
|
-
Use bash only for safe inspection commands.
|
|
457
|
-
|
|
458
|
-
Create a detailed numbered plan under a "Plan:" header:
|
|
459
|
-
|
|
460
|
-
Plan:
|
|
461
|
-
1. First step description
|
|
462
|
-
2. Second step description
|
|
463
|
-
...
|
|
464
|
-
|
|
465
|
-
Do NOT attempt to make changes - just describe what you would do.`,
|
|
466
|
-
display: false,
|
|
467
|
-
},
|
|
468
|
-
};
|
|
469
|
-
}
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
// Handle plan completion and plan mode UI
|
|
473
|
-
pi.on("agent_end", async (event, ctx) => {
|
|
474
|
-
if (!(planModeEnabled && ctx.hasUI)) return;
|
|
475
|
-
|
|
476
|
-
// Extract todos from last assistant message
|
|
477
|
-
const lastAssistant = [...event.messages].reverse().find(isAssistantMessage);
|
|
478
|
-
if (lastAssistant) {
|
|
479
|
-
const extracted = extractTodoItems(getTextContent(lastAssistant));
|
|
480
|
-
if (extracted.length > 0) {
|
|
481
|
-
todoItems = extracted;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Show plan steps in a bordered widget above the editor
|
|
486
|
-
if (todoItems.length > 0) {
|
|
487
|
-
ctx.ui.setWidget("plan-steps", (_tui, theme) => ({
|
|
488
|
-
render(width: number): string[] {
|
|
489
|
-
const stepLines = todoItems.map(
|
|
490
|
-
(t) => `${theme.fg("muted", `${getIcon("pending")} `)}${t.text}`
|
|
491
|
-
);
|
|
492
|
-
return renderBorderedBox(stepLines, width, {
|
|
493
|
-
title: `PLAN (${todoItems.length} steps)`,
|
|
494
|
-
style: "rounded",
|
|
495
|
-
borderColorFn: (s: string) => theme.fg("warning", s),
|
|
496
|
-
titleColorFn: (s: string) => theme.fg("warning", s),
|
|
497
|
-
});
|
|
498
|
-
},
|
|
499
|
-
invalidate() {},
|
|
500
|
-
}));
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
ctx.ui.setWorkingMessage(Loader.HIDE);
|
|
504
|
-
|
|
505
|
-
const choice = await ctx.ui.select("Plan mode - what next?", [
|
|
506
|
-
"Execute the plan",
|
|
507
|
-
"Stay in plan mode",
|
|
508
|
-
"Refine the plan",
|
|
509
|
-
]);
|
|
510
|
-
|
|
511
|
-
// Clear the plan steps widget after user makes a choice
|
|
512
|
-
ctx.ui.setWidget("plan-steps", undefined);
|
|
513
|
-
|
|
514
|
-
if (choice?.startsWith("Execute")) {
|
|
515
|
-
const steps = [...todoItems];
|
|
516
|
-
planModeEnabled = false;
|
|
517
|
-
todoItems = [];
|
|
518
|
-
restoreNormalModeTools();
|
|
519
|
-
updateStatus(ctx);
|
|
520
|
-
persistState();
|
|
521
|
-
|
|
522
|
-
const stepList = steps.map((t) => `${t.step}. ${t.text}`).join("\n");
|
|
523
|
-
const execMessage =
|
|
524
|
-
steps.length > 0
|
|
525
|
-
? `Execute this plan. Create tasks to track each step, then work through them:\n\n${stepList}`
|
|
526
|
-
: "Execute the plan you just created.";
|
|
527
|
-
pi.sendMessage(
|
|
528
|
-
{ customType: "plan-mode-execute", content: execMessage, display: true },
|
|
529
|
-
{ triggerTurn: true }
|
|
530
|
-
);
|
|
531
|
-
} else if (choice === "Stay in plan mode") {
|
|
532
|
-
ctx.ui.notify("Staying in plan mode. Continue refining or ask follow-up questions.", "info");
|
|
533
|
-
} else if (choice === "Refine the plan") {
|
|
534
|
-
const refinement = await ctx.ui.editor("Refine the plan:", "");
|
|
535
|
-
if (refinement?.trim()) {
|
|
536
|
-
pi.sendUserMessage(refinement.trim());
|
|
537
|
-
} else {
|
|
538
|
-
ctx.ui.notify("No refinement provided. Plan unchanged.", "info");
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
// Restore state on session start/resume
|
|
544
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
545
|
-
if (pi.getFlag("plan") === true) {
|
|
546
|
-
planModeEnabled = true;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
const entries = ctx.sessionManager.getEntries();
|
|
550
|
-
|
|
551
|
-
// Restore persisted state
|
|
552
|
-
const planModeEntry = entries
|
|
553
|
-
.filter(
|
|
554
|
-
(e: { type: string; customType?: string }) =>
|
|
555
|
-
e.type === "custom" && e.customType === "plan-mode"
|
|
556
|
-
)
|
|
557
|
-
.pop() as
|
|
558
|
-
| {
|
|
559
|
-
data?: {
|
|
560
|
-
enabled?: boolean;
|
|
561
|
-
normalTools?: string[];
|
|
562
|
-
todos?: TodoItem[];
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
| undefined;
|
|
566
|
-
|
|
567
|
-
if (planModeEntry?.data) {
|
|
568
|
-
planModeEnabled = planModeEntry.data.enabled ?? planModeEnabled;
|
|
569
|
-
normalModeTools = planModeEntry.data.normalTools ?? normalModeTools;
|
|
570
|
-
todoItems = planModeEntry.data.todos ?? todoItems;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
if (normalModeTools.length === 0) {
|
|
574
|
-
captureNormalModeTools();
|
|
575
|
-
}
|
|
576
|
-
if (planModeEnabled) {
|
|
577
|
-
applyPlanModeTools();
|
|
578
|
-
} else {
|
|
579
|
-
restoreNormalModeTools();
|
|
580
|
-
}
|
|
581
|
-
updateStatus(ctx);
|
|
582
|
-
});
|
|
583
|
-
}
|