@plannotator/pi-extension 0.19.2 → 0.19.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.
@@ -32,9 +32,70 @@ export interface CCLabelConfig {
32
32
  blocking: boolean;
33
33
  }
34
34
 
35
+ export type PromptSectionOverrides = Record<string, string | undefined>;
36
+
37
+ export type PromptRuntime =
38
+ | "claude-code"
39
+ | "opencode"
40
+ | "copilot-cli"
41
+ | "pi"
42
+ | "codex"
43
+ | "gemini-cli";
44
+
45
+ interface PromptSectionConfig {
46
+ [key: string]: string | Partial<Record<PromptRuntime, PromptSectionOverrides>> | undefined;
47
+ runtimes?: Partial<Record<PromptRuntime, PromptSectionOverrides>>;
48
+ }
49
+
50
+ export interface PromptConfig {
51
+ review?: PromptSectionConfig & {
52
+ approved?: string;
53
+ denied?: string;
54
+ };
55
+ plan?: PromptSectionConfig & {
56
+ approved?: string;
57
+ approvedWithNotes?: string;
58
+ autoApproved?: string;
59
+ denied?: string;
60
+ };
61
+ annotate?: PromptSectionConfig & {
62
+ fileFeedback?: string;
63
+ messageFeedback?: string;
64
+ approved?: string;
65
+ };
66
+ }
67
+
68
+ const PROMPT_SECTIONS = ["review", "plan", "annotate"] as const;
69
+
70
+ export function mergePromptConfig(
71
+ current?: PromptConfig,
72
+ partial?: PromptConfig,
73
+ ): PromptConfig | undefined {
74
+ if (!current && !partial) return undefined;
75
+
76
+ const result: Record<string, any> = { ...current, ...partial };
77
+
78
+ for (const section of PROMPT_SECTIONS) {
79
+ const cur = current?.[section];
80
+ const par = partial?.[section];
81
+ if (cur || par) {
82
+ result[section] = {
83
+ ...cur,
84
+ ...par,
85
+ runtimes: (cur?.runtimes || par?.runtimes)
86
+ ? { ...cur?.runtimes, ...par?.runtimes }
87
+ : undefined,
88
+ };
89
+ }
90
+ }
91
+
92
+ return result as PromptConfig;
93
+ }
94
+
35
95
  export interface PlannotatorConfig {
36
96
  displayName?: string;
37
97
  diffOptions?: DiffOptions;
98
+ prompts?: PromptConfig;
38
99
  conventionalComments?: boolean;
39
100
  /** null = explicitly cleared (use defaults), undefined = not set */
40
101
  conventionalLabels?: CCLabelConfig[] | null;
@@ -84,7 +145,13 @@ export function saveConfig(partial: Partial<PlannotatorConfig>): void {
84
145
  const mergedDiffOptions = (current.diffOptions || partial.diffOptions)
85
146
  ? { ...current.diffOptions, ...partial.diffOptions }
86
147
  : undefined;
87
- const merged = { ...current, ...partial, diffOptions: mergedDiffOptions };
148
+ const mergedPrompts = mergePromptConfig(current.prompts, partial.prompts);
149
+ const merged = {
150
+ ...current,
151
+ ...partial,
152
+ diffOptions: mergedDiffOptions,
153
+ prompts: mergedPrompts,
154
+ };
88
155
  mkdirSync(CONFIG_DIR, { recursive: true });
89
156
  writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2) + "\n", "utf-8");
90
157
  } catch (e) {
@@ -4,6 +4,13 @@
4
4
  *
5
5
  * The plan deny template was tuned in #224 / commit 3dca977 to use strong
6
6
  * directive framing — Claude was ignoring softer phrasing.
7
+ *
8
+ * IMPORTANT: This module is imported by packages/ui/utils/parser.ts which is
9
+ * bundled into the browser SPA. It must NOT import from ./prompts or ./config
10
+ * (which depend on node:fs, node:os, node:child_process). Keep it self-contained.
11
+ *
12
+ * Server-side call sites use getPlanDeniedPrompt() from ./prompts directly.
13
+ * This module is only kept for the browser's wrapFeedbackForAgent clipboard feature.
7
14
  */
8
15
 
9
16
  export interface PlanDenyFeedbackOptions {
@@ -506,7 +506,7 @@ export async function submitGhPRReview(
506
506
 
507
507
  // --- Stack Tree (GraphQL) ---
508
508
 
509
- type StackPRNode = { number: number; title: string; url: string; baseRefName: string; headRefName: string };
509
+ type StackPRNode = { number: number; title: string; url: string; baseRefName: string; headRefName: string; state: string };
510
510
 
511
511
  function stackPRQuery(kind: "head" | "base"): string {
512
512
  const varName = kind === "head" ? "headRefName" : "baseRefName";
@@ -515,7 +515,7 @@ function stackPRQuery(kind: "head" | "base"): string {
515
515
  query($owner: String!, $repo: String!, $${varName}: String!) {
516
516
  repository(owner: $owner, name: $repo) {
517
517
  pullRequests(first: ${first}, ${varName}: $${varName}, states: [OPEN, MERGED]) {
518
- nodes { number title url baseRefName headRefName }
518
+ nodes { number title url baseRefName headRefName state }
519
519
  }
520
520
  }
521
521
  }`;
@@ -588,6 +588,7 @@ export async function fetchGhPRStack(
588
588
  url: pr.url,
589
589
  isCurrent: false,
590
590
  isDefaultBranch: false,
591
+ state: (pr.state === 'MERGED' ? 'merged' : pr.state === 'CLOSED' ? 'closed' : 'open') as PRStackNode['state'],
591
592
  });
592
593
  nextHead = pr.baseRefName;
593
594
  }
@@ -608,6 +609,7 @@ export async function fetchGhPRStack(
608
609
  url: pr.url,
609
610
  isCurrent: false,
610
611
  isDefaultBranch: false,
612
+ state: (pr.state === 'MERGED' ? 'merged' : pr.state === 'CLOSED' ? 'closed' : 'open') as PRStackNode['state'],
611
613
  });
612
614
  nextBase = pr.headRefName;
613
615
  }
@@ -201,6 +201,7 @@ export interface PRStackNode {
201
201
  url?: string;
202
202
  isCurrent: boolean;
203
203
  isDefaultBranch: boolean;
204
+ state?: 'open' | 'merged' | 'closed';
204
205
  }
205
206
 
206
207
  export interface PRStackTree {
@@ -0,0 +1,245 @@
1
+ // @generated — DO NOT EDIT. Source: packages/shared/prompts.ts
2
+ import { loadConfig, type PlannotatorConfig, type PromptRuntime } from "./config";
3
+
4
+ // ─── Template engine ─────────────────────────────────────────────────────────
5
+
6
+ export function resolveTemplate(
7
+ template: string,
8
+ vars: Record<string, string | undefined>,
9
+ ): string {
10
+ return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
11
+ const val = vars[key];
12
+ return val !== undefined ? val : match;
13
+ });
14
+ }
15
+
16
+ // ─── Tool name map ───────────────────────────────────────────────────────────
17
+
18
+ export const PLAN_TOOL_NAMES: Record<PromptRuntime, string> = {
19
+ "claude-code": "ExitPlanMode",
20
+ opencode: "submit_plan",
21
+ "copilot-cli": "exit_plan_mode",
22
+ pi: "plannotator_submit_plan",
23
+ codex: "ExitPlanMode",
24
+ "gemini-cli": "exit_plan_mode",
25
+ };
26
+
27
+ export function getPlanToolName(runtime?: PromptRuntime | null): string {
28
+ return (runtime && PLAN_TOOL_NAMES[runtime]) || "ExitPlanMode";
29
+ }
30
+
31
+ export function buildPlanFileRule(toolName: string, planFilePath?: string): string {
32
+ if (!planFilePath) return "";
33
+ return `- Your plan is saved at: ${planFilePath}\n You can edit this file to make targeted changes, then pass its path to ${toolName}.\n`;
34
+ }
35
+
36
+ // ─── Default constants ───────────────────────────────────────────────────────
37
+
38
+ export const DEFAULT_REVIEW_APPROVED_PROMPT = "# Code Review\n\nCode review completed — no changes requested.";
39
+
40
+ export const DEFAULT_REVIEW_DENIED_SUFFIX = "\nThe reviewer has identified issues above. You must address all of them.";
41
+
42
+ export const DEFAULT_PLAN_DENIED_PROMPT =
43
+ "YOUR PLAN WAS NOT APPROVED.\n\nYou MUST revise the plan to address ALL of the feedback below before calling {{toolName}} again.\n\nRules:\n{{planFileRule}}- Do not resubmit the same plan unchanged.\n- Do NOT change the plan title (first # heading) unless the user explicitly asks you to.\n\n{{feedback}}";
44
+
45
+ export const DEFAULT_PLAN_APPROVED_PROMPT =
46
+ "Plan approved. You now have full tool access (read, bash, edit, write). Execute the plan in {{planFilePath}}. {{doneMsg}}";
47
+
48
+ export const DEFAULT_PLAN_APPROVED_WITH_NOTES_PROMPT =
49
+ "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{{feedback}}\n\nProceed with implementation, incorporating these notes where applicable.";
50
+
51
+ export const DEFAULT_PLAN_AUTO_APPROVED_PROMPT =
52
+ "Plan auto-approved (non-interactive mode). Execute the plan now.";
53
+
54
+ export const DEFAULT_ANNOTATE_FILE_FEEDBACK_PROMPT =
55
+ "# Markdown Annotations\n\n{{fileHeader}}: {{filePath}}\n\n{{feedback}}\n\nPlease address the annotation feedback above.";
56
+
57
+ export const DEFAULT_ANNOTATE_MESSAGE_FEEDBACK_PROMPT =
58
+ "# Message Annotations\n\n{{feedback}}\n\nPlease address the annotation feedback above.";
59
+
60
+ export const DEFAULT_ANNOTATE_APPROVED_PROMPT = "The user approved.";
61
+
62
+ // ─── Core resolver ───────────────────────────────────────────────────────────
63
+
64
+ type PromptSection = "review" | "plan" | "annotate";
65
+ type PromptKey = "approved" | "approvedWithNotes" | "autoApproved" | "denied"
66
+ | "fileFeedback" | "messageFeedback";
67
+
68
+ interface PromptLookupOptions {
69
+ section: PromptSection;
70
+ key: PromptKey;
71
+ runtime?: PromptRuntime | null;
72
+ config?: PlannotatorConfig;
73
+ fallback: string;
74
+ runtimeFallbacks?: Partial<Record<PromptRuntime, string>>;
75
+ }
76
+
77
+ function normalizePrompt(prompt: unknown): string | undefined {
78
+ if (typeof prompt !== "string") return undefined;
79
+ return prompt.trim() ? prompt : undefined;
80
+ }
81
+
82
+ export function getConfiguredPrompt(options: PromptLookupOptions): string {
83
+ const resolvedConfig = options.config ?? loadConfig();
84
+ const section = resolvedConfig.prompts?.[options.section];
85
+ const runtimePrompt = options.runtime
86
+ ? normalizePrompt(section?.runtimes?.[options.runtime]?.[options.key])
87
+ : undefined;
88
+ const genericPrompt = normalizePrompt(section?.[options.key]);
89
+ const runtimeFallback = options.runtime
90
+ ? options.runtimeFallbacks?.[options.runtime]
91
+ : undefined;
92
+
93
+ return runtimePrompt ?? genericPrompt ?? runtimeFallback ?? options.fallback;
94
+ }
95
+
96
+ type FeedbackVars = Record<string, string | undefined>;
97
+
98
+ // ─── Review wrappers ─────────────────────────────────────────────────────────
99
+
100
+ export function getReviewApprovedPrompt(
101
+ runtime?: PromptRuntime | null,
102
+ config?: PlannotatorConfig,
103
+ ): string {
104
+ return getConfiguredPrompt({
105
+ section: "review",
106
+ key: "approved",
107
+ runtime,
108
+ config,
109
+ fallback: DEFAULT_REVIEW_APPROVED_PROMPT,
110
+ });
111
+ }
112
+
113
+ const REVIEW_DENIED_RUNTIME_DEFAULTS: Partial<Record<PromptRuntime, string>> = {
114
+ opencode: "\n\nPlease address this feedback.",
115
+ pi: "\n\nPlease address this feedback.",
116
+ };
117
+
118
+ export function getReviewDeniedSuffix(
119
+ runtime?: PromptRuntime | null,
120
+ config?: PlannotatorConfig,
121
+ ): string {
122
+ return getConfiguredPrompt({
123
+ section: "review",
124
+ key: "denied",
125
+ runtime,
126
+ config,
127
+ fallback: DEFAULT_REVIEW_DENIED_SUFFIX,
128
+ runtimeFallbacks: REVIEW_DENIED_RUNTIME_DEFAULTS,
129
+ });
130
+ }
131
+
132
+ // ─── Plan wrappers ───────────────────────────────────────────────────────────
133
+
134
+ export function getPlanDeniedPrompt(
135
+ runtime?: PromptRuntime | null,
136
+ config?: PlannotatorConfig,
137
+ vars?: FeedbackVars,
138
+ ): string {
139
+ const template = getConfiguredPrompt({
140
+ section: "plan",
141
+ key: "denied",
142
+ runtime,
143
+ config,
144
+ fallback: DEFAULT_PLAN_DENIED_PROMPT,
145
+ });
146
+ return resolveTemplate(template, vars ?? {});
147
+ }
148
+
149
+ const PLAN_APPROVED_RUNTIME_DEFAULTS: Partial<Record<PromptRuntime, string>> = {
150
+ opencode: "Plan approved!{{doneMsg}}",
151
+ };
152
+
153
+ export function getPlanApprovedPrompt(
154
+ runtime?: PromptRuntime | null,
155
+ config?: PlannotatorConfig,
156
+ vars?: FeedbackVars,
157
+ ): string {
158
+ const template = getConfiguredPrompt({
159
+ section: "plan",
160
+ key: "approved",
161
+ runtime,
162
+ config,
163
+ fallback: DEFAULT_PLAN_APPROVED_PROMPT,
164
+ runtimeFallbacks: PLAN_APPROVED_RUNTIME_DEFAULTS,
165
+ });
166
+ return resolveTemplate(template, vars ?? {});
167
+ }
168
+
169
+ const PLAN_APPROVED_WITH_NOTES_RUNTIME_DEFAULTS: Partial<Record<PromptRuntime, string>> = {
170
+ opencode: "Plan approved with notes!\n{{doneMsg}}\n\n## Implementation Notes\n\nThe user approved your plan but added the following notes to consider during implementation:\n\n{{feedback}}\n\nProceed with implementation, incorporating these notes where applicable.",
171
+ };
172
+
173
+ export function getPlanApprovedWithNotesPrompt(
174
+ runtime?: PromptRuntime | null,
175
+ config?: PlannotatorConfig,
176
+ vars?: FeedbackVars,
177
+ ): string {
178
+ const template = getConfiguredPrompt({
179
+ section: "plan",
180
+ key: "approvedWithNotes",
181
+ runtime,
182
+ config,
183
+ fallback: DEFAULT_PLAN_APPROVED_WITH_NOTES_PROMPT,
184
+ runtimeFallbacks: PLAN_APPROVED_WITH_NOTES_RUNTIME_DEFAULTS,
185
+ });
186
+ return resolveTemplate(template, vars ?? {});
187
+ }
188
+
189
+ export function getPlanAutoApprovedPrompt(
190
+ runtime?: PromptRuntime | null,
191
+ config?: PlannotatorConfig,
192
+ ): string {
193
+ return getConfiguredPrompt({
194
+ section: "plan",
195
+ key: "autoApproved",
196
+ runtime,
197
+ config,
198
+ fallback: DEFAULT_PLAN_AUTO_APPROVED_PROMPT,
199
+ });
200
+ }
201
+
202
+ // ─── Annotate wrappers ──────────────────────────────────────────────────────
203
+
204
+ export function getAnnotateFileFeedbackPrompt(
205
+ runtime?: PromptRuntime | null,
206
+ config?: PlannotatorConfig,
207
+ vars?: FeedbackVars,
208
+ ): string {
209
+ const template = getConfiguredPrompt({
210
+ section: "annotate",
211
+ key: "fileFeedback",
212
+ runtime,
213
+ config,
214
+ fallback: DEFAULT_ANNOTATE_FILE_FEEDBACK_PROMPT,
215
+ });
216
+ return resolveTemplate(template, vars ?? {});
217
+ }
218
+
219
+ export function getAnnotateMessageFeedbackPrompt(
220
+ runtime?: PromptRuntime | null,
221
+ config?: PlannotatorConfig,
222
+ vars?: FeedbackVars,
223
+ ): string {
224
+ const template = getConfiguredPrompt({
225
+ section: "annotate",
226
+ key: "messageFeedback",
227
+ runtime,
228
+ config,
229
+ fallback: DEFAULT_ANNOTATE_MESSAGE_FEEDBACK_PROMPT,
230
+ });
231
+ return resolveTemplate(template, vars ?? {});
232
+ }
233
+
234
+ export function getAnnotateApprovedPrompt(
235
+ runtime?: PromptRuntime | null,
236
+ config?: PlannotatorConfig,
237
+ ): string {
238
+ return getConfiguredPrompt({
239
+ section: "annotate",
240
+ key: "approved",
241
+ runtime,
242
+ config,
243
+ fallback: DEFAULT_ANNOTATE_APPROVED_PROMPT,
244
+ });
245
+ }
package/index.ts CHANGED
@@ -31,12 +31,23 @@ import {
31
31
  markCompletedSteps,
32
32
  parseChecklist,
33
33
  } from "./generated/checklist.js";
34
- import { planDenyFeedback } from "./generated/feedback-templates.js";
35
34
  import { hasMarkdownFiles, resolveUserPath } from "./generated/resolve-file.js";
36
35
  import { FILE_BROWSER_EXCLUDED } from "./generated/reference-common.js";
37
36
  import { htmlToMarkdown } from "./generated/html-to-markdown.js";
38
37
  import { urlToMarkdown, isConvertedSource } from "./generated/url-to-markdown.js";
39
38
  import { loadConfig, resolveUseJina } from "./generated/config.js";
39
+ import {
40
+ getReviewApprovedPrompt,
41
+ getReviewDeniedSuffix,
42
+ getPlanDeniedPrompt,
43
+ getPlanApprovedPrompt,
44
+ getPlanApprovedWithNotesPrompt,
45
+ getPlanAutoApprovedPrompt,
46
+ getPlanToolName,
47
+ buildPlanFileRule,
48
+ getAnnotateFileFeedbackPrompt,
49
+ getAnnotateMessageFeedbackPrompt,
50
+ } from "./generated/prompts.js";
40
51
  import { parseAnnotateArgs } from "./generated/annotate-args.js";
41
52
  import { resolveAtReference } from "./generated/at-reference.js";
42
53
  import {
@@ -313,7 +324,7 @@ export default function plannotator(pi: ExtensionAPI): void {
313
324
  } else if (result.feedback) {
314
325
  if (result.approved) {
315
326
  pi.sendUserMessage(
316
- `# Code Review\n\nCode review completed — no changes requested.`,
327
+ getReviewApprovedPrompt("pi", loadConfig()),
317
328
  );
318
329
  } else if (isPRReview) {
319
330
  // Platform PR actions (approve/comment) return approved:false with a
@@ -321,7 +332,7 @@ export default function plannotator(pi: ExtensionAPI): void {
321
332
  pi.sendUserMessage(result.feedback);
322
333
  } else {
323
334
  pi.sendUserMessage(
324
- `${result.feedback}\n\nPlease address this feedback.`,
335
+ `${result.feedback}${getReviewDeniedSuffix("pi", loadConfig())}`,
325
336
  );
326
337
  }
327
338
  } else {
@@ -437,12 +448,11 @@ export default function plannotator(pi: ExtensionAPI): void {
437
448
  } else if (result.exit) {
438
449
  ctx.ui.notify("Annotation session closed.", "info");
439
450
  } else if (result.feedback) {
440
- const header = isFolder
441
- ? `# Markdown Annotations\n\nFolder: ${absolutePath}\n\n`
442
- : `# Markdown Annotations\n\nFile: ${absolutePath}\n\n`;
443
- pi.sendUserMessage(
444
- `${header}${result.feedback}\n\nPlease address the annotation feedback above.`,
445
- );
451
+ pi.sendUserMessage(getAnnotateFileFeedbackPrompt("pi", loadConfig(), {
452
+ fileHeader: isFolder ? "Folder" : "File",
453
+ filePath: absolutePath,
454
+ feedback: result.feedback,
455
+ }));
446
456
  } else {
447
457
  ctx.ui.notify("Annotation closed (no feedback).", "info");
448
458
  }
@@ -484,9 +494,9 @@ export default function plannotator(pi: ExtensionAPI): void {
484
494
  } else if (result.exit) {
485
495
  ctx.ui.notify("Annotation session closed.", "info");
486
496
  } else if (result.feedback) {
487
- pi.sendUserMessage(
488
- `# Message Annotations\n\n${result.feedback}\n\nPlease address the annotation feedback above.`,
489
- );
497
+ pi.sendUserMessage(getAnnotateMessageFeedbackPrompt("pi", loadConfig(), {
498
+ feedback: result.feedback,
499
+ }));
490
500
  } else {
491
501
  ctx.ui.notify("Annotation closed (no feedback).", "info");
492
502
  }
@@ -654,7 +664,7 @@ export default function plannotator(pi: ExtensionAPI): void {
654
664
  content: [
655
665
  {
656
666
  type: "text",
657
- text: "Plan auto-approved (non-interactive mode). Execute the plan now.",
667
+ text: getPlanAutoApprovedPrompt("pi", loadConfig()),
658
668
  },
659
669
  ],
660
670
  details: { approved: true },
@@ -689,7 +699,11 @@ export default function plannotator(pi: ExtensionAPI): void {
689
699
  content: [
690
700
  {
691
701
  type: "text",
692
- text: `Plan approved with notes! You now have full tool access (read, bash, edit, write). Execute the plan in ${inputPath}. ${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.`,
702
+ text: getPlanApprovedWithNotesPrompt("pi", loadConfig(), {
703
+ planFilePath: inputPath,
704
+ doneMsg,
705
+ feedback: result.feedback,
706
+ }),
693
707
  },
694
708
  ],
695
709
  details: { approved: true, feedback: result.feedback },
@@ -700,7 +714,10 @@ export default function plannotator(pi: ExtensionAPI): void {
700
714
  content: [
701
715
  {
702
716
  type: "text",
703
- text: `Plan approved. You now have full tool access (read, bash, edit, write). Execute the plan in ${inputPath}. ${doneMsg}`,
717
+ text: getPlanApprovedPrompt("pi", loadConfig(), {
718
+ planFilePath: inputPath,
719
+ doneMsg,
720
+ }),
704
721
  },
705
722
  ],
706
723
  details: { approved: true },
@@ -714,8 +731,10 @@ export default function plannotator(pi: ExtensionAPI): void {
714
731
  content: [
715
732
  {
716
733
  type: "text",
717
- text: planDenyFeedback(feedbackText, PLAN_SUBMIT_TOOL, {
718
- planFilePath: inputPath,
734
+ text: getPlanDeniedPrompt("pi", loadConfig(), {
735
+ toolName: getPlanToolName("pi"),
736
+ planFileRule: buildPlanFileRule(getPlanToolName("pi"), inputPath),
737
+ feedback: feedbackText,
719
738
  }),
720
739
  },
721
740
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plannotator/pi-extension",
3
- "version": "0.19.2",
3
+ "version": "0.19.3",
4
4
  "type": "module",
5
5
  "description": "Plannotator extension for Pi coding agent - interactive plan review with visual annotation",
6
6
  "author": "backnotprop",