@plannotator/pi-extension 0.16.2 → 0.16.3
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 +81 -0
- package/config.ts +318 -0
- package/generated/review-core.ts +13 -2
- package/index.ts +202 -39
- package/package.json +3 -1
- package/plannotator.html +2 -2
- package/plannotator.json +12 -0
- package/review-editor.html +29 -29
package/index.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
22
22
|
import { dirname, resolve } from "node:path";
|
|
23
23
|
import { fileURLToPath } from "node:url";
|
|
24
|
-
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
24
|
+
import type { AgentMessage, ThinkingLevel } from "@mariozechner/pi-agent-core";
|
|
25
25
|
import type { AssistantMessage, TextContent } from "@mariozechner/pi-ai";
|
|
26
26
|
import { Type } from "@mariozechner/pi-ai";
|
|
27
27
|
import type {
|
|
@@ -29,6 +29,7 @@ import type {
|
|
|
29
29
|
ExtensionContext,
|
|
30
30
|
} from "@mariozechner/pi-coding-agent";
|
|
31
31
|
import { Key } from "@mariozechner/pi-tui";
|
|
32
|
+
import { buildPromptVariables, formatTodoList, loadPlannotatorConfig, renderTemplate, resolvePhaseProfile } from "./config.js";
|
|
32
33
|
import {
|
|
33
34
|
type ChecklistItem,
|
|
34
35
|
markCompletedSteps,
|
|
@@ -85,6 +86,20 @@ try {
|
|
|
85
86
|
// HTML not built yet — review feature will be unavailable
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
type SavedPhaseState = {
|
|
92
|
+
activeTools: string[];
|
|
93
|
+
model?: { provider: string; id: string };
|
|
94
|
+
thinkingLevel: ThinkingLevel;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
type PersistedPlannotatorState = {
|
|
98
|
+
phase: Phase;
|
|
99
|
+
planFilePath?: string;
|
|
100
|
+
savedState?: SavedPhaseState;
|
|
101
|
+
};
|
|
102
|
+
|
|
88
103
|
function isAssistantMessage(m: AgentMessage): m is AssistantMessage {
|
|
89
104
|
return m.role === "assistant" && Array.isArray(m.content);
|
|
90
105
|
}
|
|
@@ -122,11 +137,24 @@ function getStartupErrorMessage(err: unknown): string {
|
|
|
122
137
|
return err instanceof Error ? err.message : "Unknown error";
|
|
123
138
|
}
|
|
124
139
|
|
|
140
|
+
function getPlanReviewAvailabilityWarning(options: { hasUI: boolean; hasPlanHtml: boolean }): string | null {
|
|
141
|
+
const { hasUI, hasPlanHtml } = options;
|
|
142
|
+
if (hasUI && hasPlanHtml) return null;
|
|
143
|
+
if (!hasUI && !hasPlanHtml) {
|
|
144
|
+
return "Plannotator: interactive plan review is unavailable in this session (no UI support and missing built assets). Plans will auto-approve on exit_plan_mode.";
|
|
145
|
+
}
|
|
146
|
+
if (!hasUI) {
|
|
147
|
+
return "Plannotator: interactive plan review is unavailable in this session (no UI support). Plans will auto-approve on exit_plan_mode.";
|
|
148
|
+
}
|
|
149
|
+
return "Plannotator: interactive plan review assets are missing. Rebuild the extension to restore the browser UI. Plans will auto-approve on exit_plan_mode.";
|
|
150
|
+
}
|
|
151
|
+
|
|
125
152
|
export default function plannotator(pi: ExtensionAPI): void {
|
|
126
153
|
let phase: Phase = "idle";
|
|
127
154
|
let planFilePath = "PLAN.md";
|
|
128
155
|
let checklistItems: ChecklistItem[] = [];
|
|
129
|
-
let
|
|
156
|
+
let savedState: SavedPhaseState | null = null;
|
|
157
|
+
let plannotatorConfig = {};
|
|
130
158
|
|
|
131
159
|
// ── Flags ────────────────────────────────────────────────────────────
|
|
132
160
|
|
|
@@ -148,15 +176,25 @@ export default function plannotator(pi: ExtensionAPI): void {
|
|
|
148
176
|
return resolve(cwd, planFilePath);
|
|
149
177
|
}
|
|
150
178
|
|
|
179
|
+
function getPhaseProfile(): ReturnType<typeof resolvePhaseProfile> | undefined {
|
|
180
|
+
if (phase === "planning" || phase === "executing") {
|
|
181
|
+
return resolvePhaseProfile(plannotatorConfig, phase);
|
|
182
|
+
}
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
151
186
|
function updateStatus(ctx: ExtensionContext): void {
|
|
187
|
+
const profile = getPhaseProfile();
|
|
152
188
|
if (phase === "executing" && checklistItems.length > 0) {
|
|
153
189
|
const completed = checklistItems.filter((t) => t.completed).length;
|
|
154
190
|
ctx.ui.setStatus(
|
|
155
191
|
"plannotator",
|
|
156
192
|
ctx.ui.theme.fg("accent", `📋 ${completed}/${checklistItems.length}`),
|
|
157
193
|
);
|
|
158
|
-
} else if (phase === "planning") {
|
|
159
|
-
ctx.ui.setStatus("plannotator", ctx.ui.theme.fg("warning",
|
|
194
|
+
} else if (phase === "planning" && profile?.statusLabel) {
|
|
195
|
+
ctx.ui.setStatus("plannotator", ctx.ui.theme.fg("warning", profile.statusLabel));
|
|
196
|
+
} else if (phase === "executing" && profile?.statusLabel) {
|
|
197
|
+
ctx.ui.setStatus("plannotator", ctx.ui.theme.fg("accent", profile.statusLabel));
|
|
160
198
|
} else {
|
|
161
199
|
ctx.ui.setStatus("plannotator", undefined);
|
|
162
200
|
}
|
|
@@ -179,45 +217,109 @@ export default function plannotator(pi: ExtensionAPI): void {
|
|
|
179
217
|
}
|
|
180
218
|
}
|
|
181
219
|
|
|
220
|
+
function captureSavedState(ctx: ExtensionContext): void {
|
|
221
|
+
savedState = {
|
|
222
|
+
activeTools: pi.getActiveTools(),
|
|
223
|
+
model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined,
|
|
224
|
+
thinkingLevel: pi.getThinkingLevel(),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
182
228
|
function persistState(): void {
|
|
183
|
-
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
pi.appendEntry("plannotator", { phase, planFilePath, savedState });
|
|
184
232
|
}
|
|
185
233
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
234
|
+
async function applyModelRef(
|
|
235
|
+
ref: { provider: string; id: string },
|
|
236
|
+
ctx: ExtensionContext,
|
|
237
|
+
reason: string,
|
|
238
|
+
): Promise<void> {
|
|
239
|
+
const model = ctx.modelRegistry.find(ref.provider, ref.id);
|
|
240
|
+
if (!model) {
|
|
241
|
+
ctx.ui.notify(`Plannotator: ${reason} model ${ref.provider}/${ref.id} not found.`, "warning");
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const success = await pi.setModel(model);
|
|
246
|
+
if (!success) {
|
|
247
|
+
ctx.ui.notify(`Plannotator: no API key for ${ref.provider}/${ref.id}.`, "warning");
|
|
248
|
+
}
|
|
190
249
|
}
|
|
191
250
|
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
251
|
+
async function restoreSavedState(ctx: ExtensionContext): Promise<void> {
|
|
252
|
+
if (!savedState) return;
|
|
253
|
+
|
|
254
|
+
pi.setActiveTools(savedState.activeTools);
|
|
255
|
+
if (savedState.model) {
|
|
256
|
+
await applyModelRef(savedState.model, ctx, "restore");
|
|
257
|
+
}
|
|
258
|
+
pi.setThinkingLevel(savedState.thinkingLevel);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function applyPhaseConfig(ctx: ExtensionContext, opts: { restoreSavedState?: boolean } = {}): Promise<void> {
|
|
262
|
+
const profile = getPhaseProfile();
|
|
263
|
+
if (opts.restoreSavedState !== false && savedState) {
|
|
264
|
+
await restoreSavedState(ctx);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (phase === "planning" || phase === "executing") {
|
|
268
|
+
const baseTools = stripPlanningOnlyTools(savedState?.activeTools ?? pi.getActiveTools());
|
|
269
|
+
const toolSet = new Set(baseTools);
|
|
270
|
+
for (const tool of profile?.activeTools ?? []) toolSet.add(tool);
|
|
271
|
+
if (phase === "planning") {
|
|
272
|
+
pi.setActiveTools(getToolsForPhase([...toolSet], phase));
|
|
273
|
+
} else {
|
|
274
|
+
pi.setActiveTools([...toolSet]);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (profile?.model) {
|
|
279
|
+
await applyModelRef(profile.model, ctx, phase);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (profile?.thinking) {
|
|
283
|
+
pi.setThinkingLevel(profile.thinking);
|
|
284
|
+
}
|
|
285
|
+
|
|
197
286
|
updateStatus(ctx);
|
|
198
287
|
updateWidget(ctx);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function enterPlanning(ctx: ExtensionContext): Promise<void> {
|
|
291
|
+
phase = "planning";
|
|
292
|
+
checklistItems = [];
|
|
293
|
+
captureSavedState(ctx);
|
|
294
|
+
await applyPhaseConfig(ctx, { restoreSavedState: false });
|
|
199
295
|
persistState();
|
|
200
296
|
ctx.ui.notify(
|
|
201
297
|
`Plannotator: planning mode enabled. Write your plan to ${planFilePath}.`,
|
|
202
298
|
);
|
|
299
|
+
const warning = getPlanReviewAvailabilityWarning({ hasUI: ctx.hasUI, hasPlanHtml: Boolean(planHtmlContent) });
|
|
300
|
+
if (warning) {
|
|
301
|
+
ctx.ui.notify(warning, "warning");
|
|
302
|
+
}
|
|
203
303
|
}
|
|
204
304
|
|
|
205
|
-
function exitToIdle(ctx: ExtensionContext): void {
|
|
305
|
+
async function exitToIdle(ctx: ExtensionContext): Promise<void> {
|
|
206
306
|
phase = "idle";
|
|
207
307
|
checklistItems = [];
|
|
208
|
-
|
|
209
|
-
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
await restoreSavedState(ctx);
|
|
311
|
+
savedState = null;
|
|
210
312
|
updateStatus(ctx);
|
|
211
313
|
updateWidget(ctx);
|
|
212
314
|
persistState();
|
|
213
315
|
ctx.ui.notify("Plannotator: disabled. Full access restored.");
|
|
214
316
|
}
|
|
215
317
|
|
|
216
|
-
function togglePlanMode(ctx: ExtensionContext): void {
|
|
318
|
+
async function togglePlanMode(ctx: ExtensionContext): Promise<void> {
|
|
217
319
|
if (phase === "idle") {
|
|
218
|
-
enterPlanning(ctx);
|
|
320
|
+
await enterPlanning(ctx);
|
|
219
321
|
} else {
|
|
220
|
-
exitToIdle(ctx);
|
|
322
|
+
await exitToIdle(ctx);
|
|
221
323
|
}
|
|
222
324
|
}
|
|
223
325
|
|
|
@@ -227,7 +329,7 @@ export default function plannotator(pi: ExtensionAPI): void {
|
|
|
227
329
|
description: "Toggle plannotator (file-based plan mode)",
|
|
228
330
|
handler: async (args, ctx) => {
|
|
229
331
|
if (phase !== "idle") {
|
|
230
|
-
exitToIdle(ctx);
|
|
332
|
+
await exitToIdle(ctx);
|
|
231
333
|
return;
|
|
232
334
|
}
|
|
233
335
|
|
|
@@ -241,7 +343,7 @@ export default function plannotator(pi: ExtensionAPI): void {
|
|
|
241
343
|
}
|
|
242
344
|
|
|
243
345
|
if (targetPath) planFilePath = targetPath;
|
|
244
|
-
enterPlanning(ctx);
|
|
346
|
+
await enterPlanning(ctx);
|
|
245
347
|
},
|
|
246
348
|
});
|
|
247
349
|
|
|
@@ -540,7 +642,9 @@ export default function plannotator(pi: ExtensionAPI): void {
|
|
|
540
642
|
|
|
541
643
|
pi.registerShortcut(Key.ctrlAlt("p"), {
|
|
542
644
|
description: "Toggle plannotator",
|
|
543
|
-
handler: async (ctx) =>
|
|
645
|
+
handler: async (ctx) => {
|
|
646
|
+
await togglePlanMode(ctx);
|
|
647
|
+
},
|
|
544
648
|
});
|
|
545
649
|
|
|
546
650
|
// ── plannotator_submit_plan Tool ────────────────────────────────────
|
|
@@ -610,8 +714,10 @@ export default function plannotator(pi: ExtensionAPI): void {
|
|
|
610
714
|
// Non-interactive or no HTML: auto-approve
|
|
611
715
|
if (!ctx.hasUI || !planHtmlContent) {
|
|
612
716
|
phase = "executing";
|
|
613
|
-
|
|
614
|
-
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
await applyPhaseConfig(ctx, { restoreSavedState: true });
|
|
720
|
+
pi.appendEntry("plannotator-execute", { planFilePath });
|
|
615
721
|
persistState();
|
|
616
722
|
return {
|
|
617
723
|
content: [
|
|
@@ -648,13 +754,11 @@ export default function plannotator(pi: ExtensionAPI): void {
|
|
|
648
754
|
|
|
649
755
|
if (result.approved) {
|
|
650
756
|
phase = "executing";
|
|
651
|
-
applyToolsForPhase();
|
|
652
|
-
preplanTools = null;
|
|
653
|
-
updateStatus(ctx);
|
|
654
|
-
updateWidget(ctx);
|
|
655
|
-
persistState();
|
|
656
757
|
|
|
758
|
+
|
|
759
|
+
await applyPhaseConfig(ctx, { restoreSavedState: true });
|
|
657
760
|
pi.appendEntry("plannotator-execute", { planFilePath });
|
|
761
|
+
persistState();
|
|
658
762
|
|
|
659
763
|
const doneMsg =
|
|
660
764
|
checklistItems.length > 0
|
|
@@ -731,6 +835,41 @@ export default function plannotator(pi: ExtensionAPI): void {
|
|
|
731
835
|
|
|
732
836
|
// Inject phase-specific context
|
|
733
837
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
838
|
+
const profile = getPhaseProfile();
|
|
839
|
+
if (phase === "executing") {
|
|
840
|
+
// Re-read from disk each turn to stay current
|
|
841
|
+
const fullPath = resolvePlanPath(ctx.cwd);
|
|
842
|
+
try {
|
|
843
|
+
const planContent = readFileSync(fullPath, "utf-8");
|
|
844
|
+
checklistItems = parseChecklist(planContent);
|
|
845
|
+
} catch {
|
|
846
|
+
// File deleted during execution — degrade gracefully
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const todoStats = phase === "executing" ? formatTodoList(checklistItems) : formatTodoList([]);
|
|
851
|
+
if (profile?.systemPrompt) {
|
|
852
|
+
const rendered = renderTemplate(
|
|
853
|
+
profile.systemPrompt,
|
|
854
|
+
buildPromptVariables({
|
|
855
|
+
planFilePath,
|
|
856
|
+
phase,
|
|
857
|
+
todoList: todoStats.todoList,
|
|
858
|
+
completedCount: todoStats.completedCount,
|
|
859
|
+
totalCount: todoStats.totalCount,
|
|
860
|
+
remainingCount: todoStats.remainingCount,
|
|
861
|
+
}),
|
|
862
|
+
);
|
|
863
|
+
if (rendered.unknownVariables.length > 0) {
|
|
864
|
+
ctx.ui.notify(
|
|
865
|
+
"Plannotator: unknown template variables in " + phase + " prompt: " + rendered.unknownVariables.join(", "),
|
|
866
|
+
"warning",
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return { systemPrompt: rendered.text };
|
|
871
|
+
}
|
|
872
|
+
|
|
734
873
|
if (phase === "planning") {
|
|
735
874
|
return {
|
|
736
875
|
message: {
|
|
@@ -892,8 +1031,10 @@ Execute each step in order. After completing a step, include [DONE:n] in your re
|
|
|
892
1031
|
);
|
|
893
1032
|
phase = "idle";
|
|
894
1033
|
checklistItems = [];
|
|
895
|
-
|
|
896
|
-
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
await restoreSavedState(ctx);
|
|
1037
|
+
savedState = null;
|
|
897
1038
|
updateStatus(ctx);
|
|
898
1039
|
updateWidget(ctx);
|
|
899
1040
|
persistState();
|
|
@@ -908,6 +1049,12 @@ Execute each step in order. After completing a step, include [DONE:n] in your re
|
|
|
908
1049
|
planFilePath = flagPlanFile;
|
|
909
1050
|
}
|
|
910
1051
|
|
|
1052
|
+
const loadedConfig = loadPlannotatorConfig(ctx.cwd);
|
|
1053
|
+
plannotatorConfig = loadedConfig.config;
|
|
1054
|
+
for (const warning of loadedConfig.warnings) {
|
|
1055
|
+
ctx.ui.notify(`Plannotator config: ${warning}`, "warning");
|
|
1056
|
+
}
|
|
1057
|
+
|
|
911
1058
|
// Check --plan flag
|
|
912
1059
|
if (pi.getFlag("plan") === true) {
|
|
913
1060
|
phase = "planning";
|
|
@@ -920,14 +1067,16 @@ Execute each step in order. After completing a step, include [DONE:n] in your re
|
|
|
920
1067
|
(e: { type: string; customType?: string }) =>
|
|
921
1068
|
e.type === "custom" && e.customType === "plannotator",
|
|
922
1069
|
)
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
} | undefined;
|
|
1070
|
+
|
|
1071
|
+
|
|
1072
|
+
.pop() as { data?: PersistedPlannotatorState } | undefined;
|
|
926
1073
|
|
|
927
1074
|
if (stateEntry?.data) {
|
|
928
1075
|
phase = stateEntry.data.phase ?? phase;
|
|
929
1076
|
planFilePath = stateEntry.data.planFilePath ?? planFilePath;
|
|
930
|
-
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
savedState = stateEntry.data.savedState ?? savedState;
|
|
931
1080
|
}
|
|
932
1081
|
|
|
933
1082
|
// Rebuild execution state from disk + session messages
|
|
@@ -964,11 +1113,25 @@ Execute each step in order. After completing a step, include [DONE:n] in your re
|
|
|
964
1113
|
}
|
|
965
1114
|
}
|
|
966
1115
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
if (phase === "planning") {
|
|
1119
|
+
checklistItems = [];
|
|
1120
|
+
const warning = getPlanReviewAvailabilityWarning({ hasUI: ctx.hasUI, hasPlanHtml: Boolean(planHtmlContent) });
|
|
1121
|
+
if (warning) {
|
|
1122
|
+
ctx.ui.notify(warning, "warning");
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
if (phase === "idle") {
|
|
1127
|
+
await restoreSavedState(ctx);
|
|
1128
|
+
savedState = null;
|
|
1129
|
+
} else if (phase === "planning" || phase === "executing") {
|
|
1130
|
+
await applyPhaseConfig(ctx, { restoreSavedState: true });
|
|
1131
|
+
}
|
|
970
1132
|
|
|
971
1133
|
updateStatus(ctx);
|
|
972
1134
|
updateWidget(ctx);
|
|
1135
|
+
persistState();
|
|
973
1136
|
});
|
|
974
1137
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plannotator/pi-extension",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Plannotator extension for Pi coding agent - interactive plan review with visual annotation",
|
|
6
6
|
"author": "backnotprop",
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
"index.ts",
|
|
24
24
|
"server.ts",
|
|
25
25
|
"tool-scope.ts",
|
|
26
|
+
"config.ts",
|
|
27
|
+
"plannotator.json",
|
|
26
28
|
"server/",
|
|
27
29
|
"generated/",
|
|
28
30
|
"README.md",
|