@plannotator/pi-extension 0.8.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/index.ts ADDED
@@ -0,0 +1,668 @@
1
+ /**
2
+ * Plannotator Pi Extension — File-based plan mode with visual browser review.
3
+ *
4
+ * Plans are written to PLAN.md on disk (git-trackable, editor-visible).
5
+ * The agent calls exit_plan_mode to request approval; the user reviews
6
+ * the plan in the Plannotator browser UI and can approve, deny with
7
+ * annotations, or request changes.
8
+ *
9
+ * Features:
10
+ * - /plannotator command or Ctrl+Alt+P to toggle
11
+ * - --plan flag to start in planning mode
12
+ * - --plan-file flag to customize the plan file path
13
+ * - Bash restricted to read-only commands during planning
14
+ * - Write restricted to plan file only during planning
15
+ * - exit_plan_mode tool with browser-based visual approval
16
+ * - [DONE:n] markers for execution progress tracking
17
+ * - /plannotator-review command for code review
18
+ * - /plannotator-annotate command for markdown annotation
19
+ */
20
+
21
+ import { readFileSync, existsSync } from "node:fs";
22
+ import { resolve, dirname } from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+ import type { AgentMessage } from "@mariozechner/pi-agent-core";
25
+ import type { AssistantMessage, TextContent } from "@mariozechner/pi-ai";
26
+ import { Type } from "@mariozechner/pi-ai";
27
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
28
+ import { Key } from "@mariozechner/pi-tui";
29
+ import { isSafeCommand, markCompletedSteps, parseChecklist, type ChecklistItem } from "./utils.js";
30
+ import {
31
+ startPlanReviewServer,
32
+ startReviewServer,
33
+ startAnnotateServer,
34
+ getGitContext,
35
+ runGitDiff,
36
+ openBrowser,
37
+ } from "./server.js";
38
+
39
+ // Load HTML at runtime (jiti doesn't support import attributes)
40
+ const __dirname = dirname(fileURLToPath(import.meta.url));
41
+ let planHtmlContent = "";
42
+ let reviewHtmlContent = "";
43
+ try {
44
+ planHtmlContent = readFileSync(resolve(__dirname, "plannotator.html"), "utf-8");
45
+ } catch {
46
+ // HTML not built yet — browser features will be unavailable
47
+ }
48
+ try {
49
+ reviewHtmlContent = readFileSync(resolve(__dirname, "review-editor.html"), "utf-8");
50
+ } catch {
51
+ // HTML not built yet — review feature will be unavailable
52
+ }
53
+
54
+ // Tool sets by phase
55
+ const PLANNING_TOOLS = ["read", "bash", "grep", "find", "ls", "write", "edit", "exit_plan_mode"];
56
+ const EXECUTION_TOOLS = ["read", "bash", "edit", "write"];
57
+ const NORMAL_TOOLS = ["read", "bash", "edit", "write"];
58
+
59
+ type Phase = "idle" | "planning" | "executing";
60
+
61
+ function isAssistantMessage(m: AgentMessage): m is AssistantMessage {
62
+ return m.role === "assistant" && Array.isArray(m.content);
63
+ }
64
+
65
+ function getTextContent(message: AssistantMessage): string {
66
+ return message.content
67
+ .filter((block): block is TextContent => block.type === "text")
68
+ .map((block) => block.text)
69
+ .join("\n");
70
+ }
71
+
72
+ export default function plannotator(pi: ExtensionAPI): void {
73
+ let phase: Phase = "idle";
74
+ let planFilePath = "PLAN.md";
75
+ let checklistItems: ChecklistItem[] = [];
76
+
77
+ // ── Flags ────────────────────────────────────────────────────────────
78
+
79
+ pi.registerFlag("plan", {
80
+ description: "Start in plan mode (read-only exploration)",
81
+ type: "boolean",
82
+ default: false,
83
+ });
84
+
85
+ pi.registerFlag("plan-file", {
86
+ description: "Plan file path (default: PLAN.md)",
87
+ type: "string",
88
+ default: "PLAN.md",
89
+ });
90
+
91
+ // ── Helpers ──────────────────────────────────────────────────────────
92
+
93
+ function resolvePlanPath(cwd: string): string {
94
+ return resolve(cwd, planFilePath);
95
+ }
96
+
97
+ function updateStatus(ctx: ExtensionContext): void {
98
+ if (phase === "executing" && checklistItems.length > 0) {
99
+ const completed = checklistItems.filter((t) => t.completed).length;
100
+ ctx.ui.setStatus("plannotator", ctx.ui.theme.fg("accent", `📋 ${completed}/${checklistItems.length}`));
101
+ } else if (phase === "planning") {
102
+ ctx.ui.setStatus("plannotator", ctx.ui.theme.fg("warning", "⏸ plan"));
103
+ } else {
104
+ ctx.ui.setStatus("plannotator", undefined);
105
+ }
106
+ }
107
+
108
+ function updateWidget(ctx: ExtensionContext): void {
109
+ if (phase === "executing" && checklistItems.length > 0) {
110
+ const lines = checklistItems.map((item) => {
111
+ if (item.completed) {
112
+ return (
113
+ ctx.ui.theme.fg("success", "☑ ") +
114
+ ctx.ui.theme.fg("muted", ctx.ui.theme.strikethrough(item.text))
115
+ );
116
+ }
117
+ return `${ctx.ui.theme.fg("muted", "☐ ")}${item.text}`;
118
+ });
119
+ ctx.ui.setWidget("plannotator-progress", lines);
120
+ } else {
121
+ ctx.ui.setWidget("plannotator-progress", undefined);
122
+ }
123
+ }
124
+
125
+ function persistState(): void {
126
+ pi.appendEntry("plannotator", { phase, planFilePath });
127
+ }
128
+
129
+ function enterPlanning(ctx: ExtensionContext): void {
130
+ phase = "planning";
131
+ checklistItems = [];
132
+ pi.setActiveTools(PLANNING_TOOLS);
133
+ updateStatus(ctx);
134
+ updateWidget(ctx);
135
+ persistState();
136
+ ctx.ui.notify(`Plannotator: planning mode enabled. Write your plan to ${planFilePath}.`);
137
+ }
138
+
139
+ function exitToIdle(ctx: ExtensionContext): void {
140
+ phase = "idle";
141
+ checklistItems = [];
142
+ pi.setActiveTools(NORMAL_TOOLS);
143
+ updateStatus(ctx);
144
+ updateWidget(ctx);
145
+ persistState();
146
+ ctx.ui.notify("Plannotator: disabled. Full access restored.");
147
+ }
148
+
149
+ function togglePlanMode(ctx: ExtensionContext): void {
150
+ if (phase === "idle") {
151
+ enterPlanning(ctx);
152
+ } else {
153
+ exitToIdle(ctx);
154
+ }
155
+ }
156
+
157
+ // ── Commands & Shortcuts ─────────────────────────────────────────────
158
+
159
+ pi.registerCommand("plannotator", {
160
+ description: "Toggle plannotator (file-based plan mode)",
161
+ handler: async (_args, ctx) => togglePlanMode(ctx),
162
+ });
163
+
164
+ pi.registerCommand("plannotator-status", {
165
+ description: "Show plannotator status",
166
+ handler: async (_args, ctx) => {
167
+ const parts = [`Phase: ${phase}`, `Plan file: ${planFilePath}`];
168
+ if (checklistItems.length > 0) {
169
+ const done = checklistItems.filter((t) => t.completed).length;
170
+ parts.push(`Progress: ${done}/${checklistItems.length}`);
171
+ }
172
+ ctx.ui.notify(parts.join("\n"), "info");
173
+ },
174
+ });
175
+
176
+ pi.registerCommand("plannotator-review", {
177
+ description: "Open interactive code review for current changes",
178
+ handler: async (_args, ctx) => {
179
+ if (!reviewHtmlContent) {
180
+ ctx.ui.notify("Review UI not available. Run 'bun run build' in the pi-extension directory.", "error");
181
+ return;
182
+ }
183
+
184
+ ctx.ui.notify("Opening code review UI...", "info");
185
+
186
+ const gitCtx = getGitContext();
187
+ const { patch: rawPatch, label: gitRef } = runGitDiff("uncommitted", gitCtx.defaultBranch);
188
+
189
+ const server = startReviewServer({
190
+ rawPatch,
191
+ gitRef,
192
+ origin: "pi",
193
+ diffType: "uncommitted",
194
+ gitContext: gitCtx,
195
+ htmlContent: reviewHtmlContent,
196
+ });
197
+
198
+ openBrowser(server.url);
199
+
200
+ const result = await server.waitForDecision();
201
+ await new Promise((r) => setTimeout(r, 1500));
202
+ server.stop();
203
+
204
+ if (result.feedback) {
205
+ pi.sendUserMessage(`# Code Review Feedback\n\n${result.feedback}\n\nPlease address this feedback.`);
206
+ } else {
207
+ ctx.ui.notify("Code review closed (no feedback).", "info");
208
+ }
209
+ },
210
+ });
211
+
212
+ pi.registerCommand("plannotator-annotate", {
213
+ description: "Open markdown file in annotation UI",
214
+ handler: async (args, ctx) => {
215
+ const filePath = args?.trim();
216
+ if (!filePath) {
217
+ ctx.ui.notify("Usage: /plannotator-annotate <file.md>", "error");
218
+ return;
219
+ }
220
+ if (!planHtmlContent) {
221
+ ctx.ui.notify("Annotation UI not available. Run 'bun run build' in the pi-extension directory.", "error");
222
+ return;
223
+ }
224
+
225
+ const absolutePath = resolve(ctx.cwd, filePath);
226
+ if (!existsSync(absolutePath)) {
227
+ ctx.ui.notify(`File not found: ${absolutePath}`, "error");
228
+ return;
229
+ }
230
+
231
+ ctx.ui.notify(`Opening annotation UI for ${filePath}...`, "info");
232
+
233
+ const markdown = readFileSync(absolutePath, "utf-8");
234
+ const server = startAnnotateServer({
235
+ markdown,
236
+ filePath: absolutePath,
237
+ origin: "pi",
238
+ htmlContent: planHtmlContent,
239
+ });
240
+
241
+ openBrowser(server.url);
242
+
243
+ const result = await server.waitForDecision();
244
+ await new Promise((r) => setTimeout(r, 1500));
245
+ server.stop();
246
+
247
+ if (result.feedback) {
248
+ pi.sendUserMessage(
249
+ `# Markdown Annotations\n\nFile: ${absolutePath}\n\n${result.feedback}\n\nPlease address the annotation feedback above.`,
250
+ );
251
+ } else {
252
+ ctx.ui.notify("Annotation closed (no feedback).", "info");
253
+ }
254
+ },
255
+ });
256
+
257
+ pi.registerShortcut(Key.ctrlAlt("p"), {
258
+ description: "Toggle plannotator",
259
+ handler: async (ctx) => togglePlanMode(ctx),
260
+ });
261
+
262
+ // ── exit_plan_mode Tool ──────────────────────────────────────────────
263
+
264
+ pi.registerTool({
265
+ name: "exit_plan_mode",
266
+ label: "Exit Plan Mode",
267
+ description:
268
+ "Submit your plan for user review. " +
269
+ "Call this after drafting or revising your plan in PLAN.md. " +
270
+ "The user will review the plan in a visual browser UI and can approve, deny with feedback, or annotate it. " +
271
+ "If denied, use the edit tool to make targeted revisions (not write), then call this again.",
272
+ parameters: Type.Object({
273
+ summary: Type.Optional(
274
+ Type.String({ description: "Brief summary of the plan for the user's review" }),
275
+ ),
276
+ }),
277
+
278
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
279
+ // Guard: must be in planning phase
280
+ if (phase !== "planning") {
281
+ return {
282
+ content: [{ type: "text", text: "Error: Not in plan mode. Use /plannotator to enter planning mode first." }],
283
+ details: { approved: false },
284
+ };
285
+ }
286
+
287
+ // Read plan file
288
+ const fullPath = resolvePlanPath(ctx.cwd);
289
+ let planContent: string;
290
+ try {
291
+ planContent = readFileSync(fullPath, "utf-8");
292
+ } catch {
293
+ return {
294
+ content: [
295
+ {
296
+ type: "text",
297
+ text: `Error: ${planFilePath} does not exist. Write your plan using the write tool first, then call exit_plan_mode again.`,
298
+ },
299
+ ],
300
+ details: { approved: false },
301
+ };
302
+ }
303
+
304
+ if (planContent.trim().length === 0) {
305
+ return {
306
+ content: [
307
+ {
308
+ type: "text",
309
+ text: `Error: ${planFilePath} is empty. Write your plan first, then call exit_plan_mode again.`,
310
+ },
311
+ ],
312
+ details: { approved: false },
313
+ };
314
+ }
315
+
316
+ // Parse checklist items
317
+ checklistItems = parseChecklist(planContent);
318
+
319
+ // Non-interactive or no HTML: auto-approve
320
+ if (!ctx.hasUI || !planHtmlContent) {
321
+ phase = "executing";
322
+ pi.setActiveTools(EXECUTION_TOOLS);
323
+ persistState();
324
+ return {
325
+ content: [
326
+ {
327
+ type: "text",
328
+ text: "Plan auto-approved (non-interactive mode). Execute the plan now.",
329
+ },
330
+ ],
331
+ details: { approved: true },
332
+ };
333
+ }
334
+
335
+ // Start browser-based plan review server
336
+ const server = startPlanReviewServer({
337
+ plan: planContent,
338
+ htmlContent: planHtmlContent,
339
+ origin: "pi",
340
+ });
341
+
342
+ openBrowser(server.url);
343
+
344
+ // Wait for user decision in the browser
345
+ const result = await server.waitForDecision();
346
+ await new Promise((r) => setTimeout(r, 1500));
347
+ server.stop();
348
+
349
+ if (result.approved) {
350
+ phase = "executing";
351
+ pi.setActiveTools(EXECUTION_TOOLS);
352
+ updateStatus(ctx);
353
+ updateWidget(ctx);
354
+ persistState();
355
+
356
+ pi.appendEntry("plannotator-execute", { planFilePath });
357
+
358
+ const doneMsg = checklistItems.length > 0
359
+ ? `After completing each step, include [DONE:n] in your response where n is the step number.`
360
+ : "";
361
+
362
+ if (result.feedback) {
363
+ return {
364
+ content: [
365
+ {
366
+ type: "text",
367
+ text: `Plan approved with notes! You now have full tool access (read, bash, edit, write). Execute the plan in ${planFilePath}. ${doneMsg}\n\n## Implementation Notes\n\nThe user approved your plan but added the following notes to consider during implementation:\n\n${result.feedback}\n\nProceed with implementation, incorporating these notes where applicable.`,
368
+ },
369
+ ],
370
+ details: { approved: true, feedback: result.feedback },
371
+ };
372
+ }
373
+
374
+ return {
375
+ content: [
376
+ {
377
+ type: "text",
378
+ text: `Plan approved. You now have full tool access (read, bash, edit, write). Execute the plan in ${planFilePath}. ${doneMsg}`,
379
+ },
380
+ ],
381
+ details: { approved: true },
382
+ };
383
+ }
384
+
385
+ // Denied
386
+ const feedbackText = result.feedback || "Plan rejected. Please revise.";
387
+ return {
388
+ content: [
389
+ {
390
+ type: "text",
391
+ text: `Plan not approved.\n\nUser feedback: ${feedbackText}\n\nRevise the plan:\n1. Read ${planFilePath} to see the current plan.\n2. Use the edit tool to make targeted changes addressing the feedback above — do not rewrite the entire file.\n3. Call exit_plan_mode again when ready.`,
392
+ },
393
+ ],
394
+ details: { approved: false, feedback: feedbackText },
395
+ };
396
+ },
397
+ });
398
+
399
+ // ── Event Handlers ───────────────────────────────────────────────────
400
+
401
+ // Gate writes and bash during planning
402
+ pi.on("tool_call", async (event, ctx) => {
403
+ if (phase !== "planning") return;
404
+
405
+ if (event.toolName === "bash") {
406
+ const command = event.input.command as string;
407
+ if (!isSafeCommand(command)) {
408
+ return {
409
+ block: true,
410
+ reason: `Plannotator: command blocked (not in read-only allowlist).\nCommand: ${command}`,
411
+ };
412
+ }
413
+ }
414
+
415
+ if (event.toolName === "write") {
416
+ const targetPath = resolve(ctx.cwd, event.input.path as string);
417
+ const allowedPath = resolvePlanPath(ctx.cwd);
418
+ if (targetPath !== allowedPath) {
419
+ return {
420
+ block: true,
421
+ reason: `Plannotator: writes are restricted to ${planFilePath} during planning. Blocked: ${event.input.path}`,
422
+ };
423
+ }
424
+ }
425
+
426
+ if (event.toolName === "edit") {
427
+ const targetPath = resolve(ctx.cwd, event.input.path as string);
428
+ const allowedPath = resolvePlanPath(ctx.cwd);
429
+ if (targetPath !== allowedPath) {
430
+ return {
431
+ block: true,
432
+ reason: `Plannotator: edits are restricted to ${planFilePath} during planning. Blocked: ${event.input.path}`,
433
+ };
434
+ }
435
+ }
436
+ });
437
+
438
+ // Inject phase-specific context
439
+ pi.on("before_agent_start", async (_event, ctx) => {
440
+ if (phase === "planning") {
441
+ return {
442
+ message: {
443
+ customType: "plannotator-context",
444
+ content: `[PLANNOTATOR - PLANNING PHASE]
445
+ You are in plan mode. You MUST NOT make any changes to the codebase — no edits, no commits, no installs, no destructive commands. The ONLY file you may write to or edit is the plan file: ${planFilePath}.
446
+
447
+ Available tools: read, bash (read-only commands only), grep, find, ls, write (${planFilePath} only), edit (${planFilePath} only), exit_plan_mode
448
+
449
+ ## Iterative Planning Workflow
450
+
451
+ You are pair-planning with the user. Explore the code to build context, then write your findings into ${planFilePath} as you go. The plan starts as a rough skeleton and gradually becomes the final plan.
452
+
453
+ ### The Loop
454
+
455
+ Repeat this cycle until the plan is complete:
456
+
457
+ 1. **Explore** — Use read, grep, find, ls, and bash to understand the codebase. Actively search for existing functions, utilities, and patterns that can be reused — avoid proposing new code when suitable implementations already exist.
458
+ 2. **Update the plan file** — After each discovery, immediately capture what you learned in ${planFilePath}. Don't wait until the end. Use write for the initial draft, then edit for all subsequent updates.
459
+ 3. **Ask the user** — When you hit an ambiguity or decision you can't resolve from code alone, ask. Then go back to step 1.
460
+
461
+ ### First Turn
462
+
463
+ Start by quickly scanning key files to form an initial understanding of the task scope. Then write a skeleton plan (headers and rough notes) and ask the user your first round of questions. Don't explore exhaustively before engaging the user.
464
+
465
+ ### Asking Good Questions
466
+
467
+ - Never ask what you could find out by reading the code.
468
+ - Batch related questions together.
469
+ - Focus on things only the user can answer: requirements, preferences, tradeoffs, edge-case priorities.
470
+ - Scale depth to the task — a vague feature request needs many rounds; a focused bug fix may need one or none.
471
+
472
+ ### Plan File Structure
473
+
474
+ Your plan file should use markdown with clear sections:
475
+ - **Context** — Why this change is being made: the problem, what prompted it, the intended outcome.
476
+ - **Approach** — Your recommended approach only, not all alternatives considered.
477
+ - **Files to modify** — List the critical file paths that will be changed.
478
+ - **Reuse** — Reference existing functions and utilities you found, with their file paths.
479
+ - **Steps** — Implementation checklist:
480
+ - [ ] Step 1 description
481
+ - [ ] Step 2 description
482
+ - **Verification** — How to test the changes end-to-end (run the code, run tests, manual checks).
483
+
484
+ Keep the plan concise enough to scan quickly, but detailed enough to execute effectively.
485
+
486
+ ### When to Submit
487
+
488
+ Your plan is ready when you've addressed all ambiguities and it covers: what to change, which files to modify, what existing code to reuse, and how to verify. Call exit_plan_mode to submit for review.
489
+
490
+ ### Revising After Feedback
491
+
492
+ When the user denies a plan with feedback:
493
+ 1. Read ${planFilePath} to see the current plan.
494
+ 2. Use the edit tool to make targeted changes addressing the feedback — do NOT rewrite the entire file.
495
+ 3. Call exit_plan_mode again to resubmit.
496
+
497
+ ### Ending Your Turn
498
+
499
+ Your turn should only end by either:
500
+ - Asking the user a question to gather more information.
501
+ - Calling exit_plan_mode when the plan is ready for review.
502
+
503
+ Do not end your turn without doing one of these two things.`,
504
+ display: false,
505
+ },
506
+ };
507
+ }
508
+
509
+ if (phase === "executing" && checklistItems.length > 0) {
510
+ // Re-read from disk each turn to stay current
511
+ const fullPath = resolvePlanPath(ctx.cwd);
512
+ let planContent = "";
513
+ try {
514
+ planContent = readFileSync(fullPath, "utf-8");
515
+ checklistItems = parseChecklist(planContent);
516
+ } catch {
517
+ // File deleted during execution — degrade gracefully
518
+ }
519
+
520
+ const remaining = checklistItems.filter((t) => !t.completed);
521
+ if (remaining.length > 0) {
522
+ const todoList = remaining.map((t) => `- [ ] ${t.step}. ${t.text}`).join("\n");
523
+ return {
524
+ message: {
525
+ customType: "plannotator-context",
526
+ content: `[PLANNOTATOR - EXECUTING PLAN]
527
+ Full tool access is enabled. Execute the plan from ${planFilePath}.
528
+
529
+ Remaining steps:
530
+ ${todoList}
531
+
532
+ Execute each step in order. After completing a step, include [DONE:n] in your response where n is the step number.`,
533
+ display: false,
534
+ },
535
+ };
536
+ }
537
+ }
538
+ });
539
+
540
+ // Filter stale context when idle
541
+ pi.on("context", async (event) => {
542
+ if (phase !== "idle") return;
543
+
544
+ return {
545
+ messages: event.messages.filter((m) => {
546
+ const msg = m as AgentMessage & { customType?: string };
547
+ if (msg.customType === "plannotator-context") return false;
548
+ if (msg.role !== "user") return true;
549
+
550
+ const content = msg.content;
551
+ if (typeof content === "string") {
552
+ return !content.includes("[PLANNOTATOR -");
553
+ }
554
+ if (Array.isArray(content)) {
555
+ return !content.some(
556
+ (c) => c.type === "text" && (c as TextContent).text?.includes("[PLANNOTATOR -"),
557
+ );
558
+ }
559
+ return true;
560
+ }),
561
+ };
562
+ });
563
+
564
+ // Track execution progress
565
+ pi.on("turn_end", async (event, ctx) => {
566
+ if (phase !== "executing" || checklistItems.length === 0) return;
567
+ if (!isAssistantMessage(event.message)) return;
568
+
569
+ const text = getTextContent(event.message);
570
+ if (markCompletedSteps(text, checklistItems) > 0) {
571
+ updateStatus(ctx);
572
+ updateWidget(ctx);
573
+ }
574
+ persistState();
575
+ });
576
+
577
+ // Detect execution completion
578
+ pi.on("agent_end", async (_event, ctx) => {
579
+ if (phase !== "executing" || checklistItems.length === 0) return;
580
+
581
+ if (checklistItems.every((t) => t.completed)) {
582
+ const completedList = checklistItems.map((t) => `- [x] ~~${t.text}~~`).join("\n");
583
+ pi.sendMessage(
584
+ {
585
+ customType: "plannotator-complete",
586
+ content: `**Plan Complete!** ✓\n\n${completedList}`,
587
+ display: true,
588
+ },
589
+ { triggerTurn: false },
590
+ );
591
+ phase = "idle";
592
+ checklistItems = [];
593
+ pi.setActiveTools(NORMAL_TOOLS);
594
+ updateStatus(ctx);
595
+ updateWidget(ctx);
596
+ persistState();
597
+ }
598
+ });
599
+
600
+ // Restore state on session start/resume
601
+ pi.on("session_start", async (_event, ctx) => {
602
+ // Resolve plan file path from flag
603
+ const flagPlanFile = pi.getFlag("plan-file") as string;
604
+ if (flagPlanFile) {
605
+ planFilePath = flagPlanFile;
606
+ }
607
+
608
+ // Check --plan flag
609
+ if (pi.getFlag("plan") === true) {
610
+ phase = "planning";
611
+ }
612
+
613
+ // Restore persisted state
614
+ const entries = ctx.sessionManager.getEntries();
615
+ const stateEntry = entries
616
+ .filter((e: { type: string; customType?: string }) => e.type === "custom" && e.customType === "plannotator")
617
+ .pop() as { data?: { phase: Phase; planFilePath?: string } } | undefined;
618
+
619
+ if (stateEntry?.data) {
620
+ phase = stateEntry.data.phase ?? phase;
621
+ planFilePath = stateEntry.data.planFilePath ?? planFilePath;
622
+ }
623
+
624
+ // Rebuild execution state from disk + session messages
625
+ if (phase === "executing") {
626
+ const fullPath = resolvePlanPath(ctx.cwd);
627
+ if (existsSync(fullPath)) {
628
+ const content = readFileSync(fullPath, "utf-8");
629
+ checklistItems = parseChecklist(content);
630
+
631
+ // Find last execution marker and scan messages after it for [DONE:n]
632
+ let executeIndex = -1;
633
+ for (let i = entries.length - 1; i >= 0; i--) {
634
+ const entry = entries[i] as { type: string; customType?: string };
635
+ if (entry.customType === "plannotator-execute") {
636
+ executeIndex = i;
637
+ break;
638
+ }
639
+ }
640
+
641
+ for (let i = executeIndex + 1; i < entries.length; i++) {
642
+ const entry = entries[i];
643
+ if (
644
+ entry.type === "message" &&
645
+ "message" in entry &&
646
+ isAssistantMessage(entry.message as AgentMessage)
647
+ ) {
648
+ const text = getTextContent(entry.message as AssistantMessage);
649
+ markCompletedSteps(text, checklistItems);
650
+ }
651
+ }
652
+ } else {
653
+ // Plan file gone — fall back to idle
654
+ phase = "idle";
655
+ }
656
+ }
657
+
658
+ // Apply tool restrictions for current phase
659
+ if (phase === "planning") {
660
+ pi.setActiveTools(PLANNING_TOOLS);
661
+ } else if (phase === "executing") {
662
+ pi.setActiveTools(EXECUTION_TOOLS);
663
+ }
664
+
665
+ updateStatus(ctx);
666
+ updateWidget(ctx);
667
+ });
668
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@plannotator/pi-extension",
3
+ "version": "0.8.3",
4
+ "type": "module",
5
+ "description": "Plannotator extension for Pi coding agent - interactive plan review with visual annotation",
6
+ "author": "backnotprop",
7
+ "license": "MIT OR Apache-2.0",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/backnotprop/plannotator.git",
11
+ "directory": "apps/pi-extension"
12
+ },
13
+ "homepage": "https://github.com/backnotprop/plannotator",
14
+ "bugs": {
15
+ "url": "https://github.com/backnotprop/plannotator/issues"
16
+ },
17
+ "keywords": ["pi-package", "plannotator", "plan-review", "ai-agent", "coding-agent"],
18
+ "pi": {
19
+ "extensions": ["./"]
20
+ },
21
+ "files": [
22
+ "index.ts",
23
+ "server.ts",
24
+ "utils.ts",
25
+ "README.md",
26
+ "plannotator.html",
27
+ "review-editor.html"
28
+ ],
29
+ "scripts": {
30
+ "build": "cp ../hook/dist/index.html plannotator.html && cp ../hook/dist/review.html review-editor.html",
31
+ "prepublishOnly": "cd ../.. && bun run build:pi"
32
+ },
33
+ "peerDependencies": {
34
+ "@mariozechner/pi-coding-agent": ">=0.53.0"
35
+ }
36
+ }