@plannotator/pi-extension 0.15.0 → 0.15.2

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 CHANGED
@@ -2,9 +2,9 @@
2
2
  * Plannotator Pi Extension — File-based plan mode with visual browser review.
3
3
  *
4
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.
5
+ * The agent calls plannotator_submit_plan to request approval; the user
6
+ * reviews the plan in the Plannotator browser UI and can approve, deny
7
+ * with annotations, or request changes.
8
8
  *
9
9
  * Features:
10
10
  * - /plannotator command or Ctrl+Alt+P to toggle
@@ -12,13 +12,13 @@
12
12
  * - --plan-file flag to customize the plan file path
13
13
  * - Bash unrestricted during planning (prompt-guided)
14
14
  * - Write restricted to plan file only during planning
15
- * - exit_plan_mode tool with browser-based visual approval
15
+ * - plannotator_submit_plan tool with browser-based visual approval
16
16
  * - [DONE:n] markers for execution progress tracking
17
17
  * - /plannotator-review command for code review
18
18
  * - /plannotator-annotate command for markdown annotation
19
19
  */
20
20
 
21
- import { existsSync, readFileSync } from "node:fs";
21
+ import { existsSync, readFileSync, statSync } from "node:fs";
22
22
  import { dirname, resolve } from "node:path";
23
23
  import { fileURLToPath } from "node:url";
24
24
  import type { AgentMessage } from "@mariozechner/pi-agent-core";
@@ -35,6 +35,8 @@ import {
35
35
  parseChecklist,
36
36
  } from "./generated/checklist.js";
37
37
  import { planDenyFeedback } from "./generated/feedback-templates.js";
38
+ import { hasMarkdownFiles } from "./generated/resolve-file.js";
39
+ import { FILE_BROWSER_EXCLUDED } from "./generated/reference-common.js";
38
40
  import { openBrowser } from "./server/network.js";
39
41
  import {
40
42
  type AnnotateServerResult,
@@ -46,6 +48,12 @@ import {
46
48
  startPlanReviewServer,
47
49
  startReviewServer,
48
50
  } from "./server.js";
51
+ import {
52
+ getToolsForPhase,
53
+ PLAN_SUBMIT_TOOL,
54
+ type Phase,
55
+ stripPlanningOnlyTools,
56
+ } from "./tool-scope.js";
49
57
 
50
58
  // ── Types ──────────────────────────────────────────────────────────────
51
59
 
@@ -77,11 +85,6 @@ try {
77
85
  // HTML not built yet — review feature will be unavailable
78
86
  }
79
87
 
80
- /** Extra tools to ensure are available during planning (on top of whatever is already active). */
81
- const PLANNING_EXTRA_TOOLS = ["grep", "find", "ls", "exit_plan_mode"];
82
-
83
- type Phase = "idle" | "planning" | "executing";
84
-
85
88
  function isAssistantMessage(m: AgentMessage): m is AssistantMessage {
86
89
  return m.role === "assistant" && Array.isArray(m.content);
87
90
  }
@@ -177,28 +180,19 @@ export default function plannotator(pi: ExtensionAPI): void {
177
180
  }
178
181
 
179
182
  function persistState(): void {
180
- pi.appendEntry("plannotator", { phase, planFilePath });
183
+ pi.appendEntry("plannotator", { phase, planFilePath, preplanTools });
181
184
  }
182
185
 
183
186
  /** Apply tool visibility for the current phase, preserving tools from other extensions. */
184
187
  function applyToolsForPhase(): void {
185
- if (phase === "planning") {
186
- const base = preplanTools ?? pi.getActiveTools();
187
- const toolSet = new Set(base);
188
- for (const t of PLANNING_EXTRA_TOOLS) toolSet.add(t);
189
- pi.setActiveTools([...toolSet]);
190
- } else if (preplanTools) {
191
- // Restore pre-plan tool set (removes exit_plan_mode, etc.)
192
- pi.setActiveTools(preplanTools);
193
- preplanTools = null;
194
- }
195
- // If no preplanTools (e.g. session restore to executing/idle), leave tools as-is
188
+ const baseTools = stripPlanningOnlyTools(preplanTools ?? pi.getActiveTools());
189
+ pi.setActiveTools(getToolsForPhase(baseTools, phase));
196
190
  }
197
191
 
198
192
  function enterPlanning(ctx: ExtensionContext): void {
199
193
  phase = "planning";
200
194
  checklistItems = [];
201
- preplanTools = pi.getActiveTools();
195
+ preplanTools = stripPlanningOnlyTools(pi.getActiveTools());
202
196
  applyToolsForPhase();
203
197
  updateStatus(ctx);
204
198
  updateWidget(ctx);
@@ -212,6 +206,7 @@ export default function plannotator(pi: ExtensionAPI): void {
212
206
  phase = "idle";
213
207
  checklistItems = [];
214
208
  applyToolsForPhase();
209
+ preplanTools = null;
215
210
  updateStatus(ctx);
216
211
  updateWidget(ctx);
217
212
  persistState();
@@ -343,11 +338,11 @@ export default function plannotator(pi: ExtensionAPI): void {
343
338
  });
344
339
 
345
340
  pi.registerCommand("plannotator-annotate", {
346
- description: "Open markdown file in annotation UI",
341
+ description: "Open markdown file or folder in annotation UI",
347
342
  handler: async (args, ctx) => {
348
343
  const filePath = args?.trim();
349
344
  if (!filePath) {
350
- ctx.ui.notify("Usage: /plannotator-annotate <file.md>", "error");
345
+ ctx.ui.notify("Usage: /plannotator-annotate <file.md | folder/>", "error");
351
346
  return;
352
347
  }
353
348
  if (!planHtmlContent) {
@@ -364,15 +359,41 @@ export default function plannotator(pi: ExtensionAPI): void {
364
359
  return;
365
360
  }
366
361
 
367
- ctx.ui.notify(`Opening annotation UI for ${filePath}...`, "info");
362
+ // Check if the argument is a directory (folder annotation mode)
363
+ let isFolder = false;
364
+ try {
365
+ isFolder = statSync(absolutePath).isDirectory();
366
+ } catch {
367
+ ctx.ui.notify(`Cannot access: ${absolutePath}`, "error");
368
+ return;
369
+ }
370
+
371
+ let markdown: string;
372
+ let folderPath: string | undefined;
373
+ let mode: string | undefined;
374
+
375
+ if (isFolder) {
376
+ if (!hasMarkdownFiles(absolutePath, FILE_BROWSER_EXCLUDED)) {
377
+ ctx.ui.notify(`No markdown files found in ${absolutePath}`, "error");
378
+ return;
379
+ }
380
+ markdown = "";
381
+ folderPath = absolutePath;
382
+ mode = "annotate-folder";
383
+ ctx.ui.notify(`Opening annotation UI for folder ${filePath}...`, "info");
384
+ } else {
385
+ markdown = readFileSync(absolutePath, "utf-8");
386
+ ctx.ui.notify(`Opening annotation UI for ${filePath}...`, "info");
387
+ }
368
388
 
369
- const markdown = readFileSync(absolutePath, "utf-8");
370
389
  let server: AnnotateServerResult;
371
390
  try {
372
391
  server = await startAnnotateServer({
373
392
  markdown,
374
393
  filePath: absolutePath,
375
394
  origin: "pi",
395
+ mode,
396
+ folderPath,
376
397
  htmlContent: planHtmlContent,
377
398
  sharingEnabled: process.env.PLANNOTATOR_SHARE !== "disabled",
378
399
  shareBaseUrl: process.env.PLANNOTATOR_SHARE_URL || undefined,
@@ -389,8 +410,11 @@ export default function plannotator(pi: ExtensionAPI): void {
389
410
  const result = await runBrowserReview(server, ctx);
390
411
 
391
412
  if (result.feedback) {
413
+ const header = isFolder
414
+ ? `# Markdown Annotations\n\nFolder: ${absolutePath}\n\n`
415
+ : `# Markdown Annotations\n\nFile: ${absolutePath}\n\n`;
392
416
  pi.sendUserMessage(
393
- `# Markdown Annotations\n\nFile: ${absolutePath}\n\n${result.feedback}\n\nPlease address the annotation feedback above.`,
417
+ `${header}${result.feedback}\n\nPlease address the annotation feedback above.`,
394
418
  );
395
419
  } else {
396
420
  ctx.ui.notify("Annotation closed (no feedback).", "info");
@@ -519,14 +543,14 @@ export default function plannotator(pi: ExtensionAPI): void {
519
543
  handler: async (ctx) => togglePlanMode(ctx),
520
544
  });
521
545
 
522
- // ── exit_plan_mode Tool ──────────────────────────────────────────────
546
+ // ── plannotator_submit_plan Tool ────────────────────────────────────
523
547
 
524
548
  pi.registerTool({
525
- name: "exit_plan_mode",
526
- label: "Exit Plan Mode",
549
+ name: PLAN_SUBMIT_TOOL,
550
+ label: "Submit Plan",
527
551
  description:
528
- "Submit your plan for user review. " +
529
- "Call this after drafting or revising your plan file. " +
552
+ "Submit your Plannotator plan for user review. " +
553
+ "Call this only while Plannotator planning mode is active, after drafting or revising your plan file. " +
530
554
  "The user will review the plan in a visual browser UI and can approve, deny with feedback, or annotate it. " +
531
555
  "If denied, use the edit tool to make targeted revisions (not write), then call this again.",
532
556
  parameters: Type.Object({
@@ -561,7 +585,7 @@ export default function plannotator(pi: ExtensionAPI): void {
561
585
  content: [
562
586
  {
563
587
  type: "text",
564
- text: `Error: ${planFilePath} does not exist. Write your plan using the write tool first, then call exit_plan_mode again.`,
588
+ text: `Error: ${planFilePath} does not exist. Write your plan using the write tool first, then call ${PLAN_SUBMIT_TOOL} again.`,
565
589
  },
566
590
  ],
567
591
  details: { approved: false },
@@ -573,7 +597,7 @@ export default function plannotator(pi: ExtensionAPI): void {
573
597
  content: [
574
598
  {
575
599
  type: "text",
576
- text: `Error: ${planFilePath} is empty. Write your plan first, then call exit_plan_mode again.`,
600
+ text: `Error: ${planFilePath} is empty. Write your plan first, then call ${PLAN_SUBMIT_TOOL} again.`,
577
601
  },
578
602
  ],
579
603
  details: { approved: false },
@@ -587,6 +611,7 @@ export default function plannotator(pi: ExtensionAPI): void {
587
611
  if (!ctx.hasUI || !planHtmlContent) {
588
612
  phase = "executing";
589
613
  applyToolsForPhase();
614
+ preplanTools = null;
590
615
  persistState();
591
616
  return {
592
617
  content: [
@@ -624,6 +649,7 @@ export default function plannotator(pi: ExtensionAPI): void {
624
649
  if (result.approved) {
625
650
  phase = "executing";
626
651
  applyToolsForPhase();
652
+ preplanTools = null;
627
653
  updateStatus(ctx);
628
654
  updateWidget(ctx);
629
655
  persistState();
@@ -664,7 +690,7 @@ export default function plannotator(pi: ExtensionAPI): void {
664
690
  content: [
665
691
  {
666
692
  type: "text",
667
- text: planDenyFeedback(feedbackText, "exit_plan_mode", {
693
+ text: planDenyFeedback(feedbackText, PLAN_SUBMIT_TOOL, {
668
694
  planFilePath,
669
695
  }),
670
696
  },
@@ -712,7 +738,7 @@ export default function plannotator(pi: ExtensionAPI): void {
712
738
  content: `[PLANNOTATOR - PLANNING PHASE]
713
739
  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}.
714
740
 
715
- Available tools: read, bash, grep, find, ls, write (${planFilePath} only), edit (${planFilePath} only), exit_plan_mode
741
+ Available tools: read, bash, grep, find, ls, write (${planFilePath} only), edit (${planFilePath} only), ${PLAN_SUBMIT_TOOL}
716
742
 
717
743
  Do not run destructive bash commands (rm, git push, npm install, etc.) — focus on reading and exploring the codebase. Web fetching (curl, wget) is fine.
718
744
 
@@ -755,20 +781,20 @@ Keep the plan concise enough to scan quickly, but detailed enough to execute eff
755
781
 
756
782
  ### When to Submit
757
783
 
758
- 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.
784
+ 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 ${PLAN_SUBMIT_TOOL} to submit for review.
759
785
 
760
786
  ### Revising After Feedback
761
787
 
762
788
  When the user denies a plan with feedback:
763
789
  1. Read ${planFilePath} to see the current plan.
764
790
  2. Use the edit tool to make targeted changes addressing the feedback — do NOT rewrite the entire file.
765
- 3. Call exit_plan_mode again to resubmit.
791
+ 3. Call ${PLAN_SUBMIT_TOOL} again to resubmit.
766
792
 
767
793
  ### Ending Your Turn
768
794
 
769
795
  Your turn should only end by either:
770
796
  - Asking the user a question to gather more information.
771
- - Calling exit_plan_mode when the plan is ready for review.
797
+ - Calling ${PLAN_SUBMIT_TOOL} when the plan is ready for review.
772
798
 
773
799
  Do not end your turn without doing one of these two things.`,
774
800
  display: false,
@@ -867,6 +893,7 @@ Execute each step in order. After completing a step, include [DONE:n] in your re
867
893
  phase = "idle";
868
894
  checklistItems = [];
869
895
  applyToolsForPhase();
896
+ preplanTools = null;
870
897
  updateStatus(ctx);
871
898
  updateWidget(ctx);
872
899
  persistState();
@@ -893,11 +920,14 @@ Execute each step in order. After completing a step, include [DONE:n] in your re
893
920
  (e: { type: string; customType?: string }) =>
894
921
  e.type === "custom" && e.customType === "plannotator",
895
922
  )
896
- .pop() as { data?: { phase: Phase; planFilePath?: string } } | undefined;
923
+ .pop() as {
924
+ data?: { phase: Phase; planFilePath?: string; preplanTools?: string[] | null };
925
+ } | undefined;
897
926
 
898
927
  if (stateEntry?.data) {
899
928
  phase = stateEntry.data.phase ?? phase;
900
929
  planFilePath = stateEntry.data.planFilePath ?? planFilePath;
930
+ preplanTools = stateEntry.data.preplanTools ?? preplanTools;
901
931
  }
902
932
 
903
933
  // Rebuild execution state from disk + session messages
@@ -934,10 +964,9 @@ Execute each step in order. After completing a step, include [DONE:n] in your re
934
964
  }
935
965
  }
936
966
 
937
- // Apply tool restrictions for current phase
938
- if (phase === "planning") {
939
- applyToolsForPhase();
940
- }
967
+ // Re-apply tool visibility on startup/resume so planning-only tools stay hidden
968
+ // outside planning and pre-plan tool state is restored after approvals.
969
+ applyToolsForPhase();
941
970
 
942
971
  updateStatus(ctx);
943
972
  updateWidget(ctx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plannotator/pi-extension",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "type": "module",
5
5
  "description": "Plannotator extension for Pi coding agent - interactive plan review with visual annotation",
6
6
  "author": "backnotprop",
@@ -28,7 +28,7 @@
28
28
  "review-editor.html"
29
29
  ],
30
30
  "scripts": {
31
- "build": "cp ../hook/dist/index.html plannotator.html && cp ../hook/dist/review.html review-editor.html && mkdir -p generated && for f in feedback-templates review-core storage draft project pr-provider pr-github pr-gitlab checklist integrations-common repo reference-common favicon resolve-file; do src=\"../../packages/shared/$f.ts\"; printf '// @generated — DO NOT EDIT. Source: packages/shared/%s.ts\\n' \"$f\" | cat - \"$src\" > \"generated/$f.ts\"; done",
31
+ "build": "cp ../hook/dist/index.html plannotator.html && cp ../hook/dist/review.html review-editor.html && mkdir -p generated && for f in feedback-templates review-core storage draft project pr-provider pr-github pr-gitlab checklist integrations-common repo reference-common favicon resolve-file; do src=\"../../packages/shared/$f.ts\"; printf '// @generated — DO NOT EDIT. Source: packages/shared/%s.ts\\n' \"$f\" | cat - \"$src\" > \"generated/$f.ts\"; done && mkdir -p generated/ai/providers && for f in index types provider session-manager endpoints context base-session; do src=\"../../packages/ai/$f.ts\"; printf '// @generated — DO NOT EDIT. Source: packages/ai/%s.ts\\n' \"$f\" | cat - \"$src\" > \"generated/ai/$f.ts\"; done && for f in claude-agent-sdk codex-sdk opencode-sdk pi-sdk pi-sdk-node pi-events; do src=\"../../packages/ai/providers/$f.ts\"; printf '// @generated — DO NOT EDIT. Source: packages/ai/providers/%s.ts\\n' \"$f\" | cat - \"$src\" > \"generated/ai/providers/$f.ts\"; done",
32
32
  "prepublishOnly": "cd ../.. && bun run build:pi"
33
33
  },
34
34
  "peerDependencies": {