@plannotator/pi-extension 0.16.4 → 0.16.6

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.
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { createServer } from "node:http";
2
3
 
3
4
  import { contentHash, deleteDraft } from "../generated/draft.js";
@@ -45,11 +46,21 @@ import {
45
46
  handleObsidianVaultsRequest,
46
47
  } from "./reference.js";
47
48
 
49
+ export interface PlanReviewDecision {
50
+ approved: boolean;
51
+ feedback?: string;
52
+ savedPath?: string;
53
+ agentSwitch?: string;
54
+ permissionMode?: string;
55
+ }
56
+
48
57
  export interface PlanServerResult {
58
+ reviewId: string;
49
59
  port: number;
50
60
  portSource: "env" | "remote-default" | "random";
51
61
  url: string;
52
- waitForDecision: () => Promise<{ approved: boolean; feedback?: string; savedPath?: string; agentSwitch?: string; permissionMode?: string }>;
62
+ waitForDecision: () => Promise<PlanReviewDecision>;
63
+ onDecision: (listener: (result: PlanReviewDecision) => void | Promise<void>) => () => void;
53
64
  waitForDone?: () => Promise<void>;
54
65
  stop: () => void;
55
66
  }
@@ -114,22 +125,24 @@ export async function startPlanReviewServer(options: {
114
125
  }
115
126
  : null;
116
127
 
117
- let resolveDecision!: (result: {
118
- approved: boolean;
119
- feedback?: string;
120
- savedPath?: string;
121
- agentSwitch?: string;
122
- permissionMode?: string;
123
- }) => void;
124
- const decisionPromise = new Promise<{
125
- approved: boolean;
126
- feedback?: string;
127
- savedPath?: string;
128
- agentSwitch?: string;
129
- permissionMode?: string;
130
- }>((r) => {
128
+ const reviewId = randomUUID();
129
+ let resolveDecision!: (result: PlanReviewDecision) => void;
130
+ const decisionListeners = new Set<(result: PlanReviewDecision) => void | Promise<void>>();
131
+ let decisionSettled = false;
132
+ const decisionPromise = new Promise<PlanReviewDecision>((r) => {
131
133
  resolveDecision = r;
132
134
  });
135
+ const publishDecision = (result: PlanReviewDecision): boolean => {
136
+ if (decisionSettled) return false;
137
+ decisionSettled = true;
138
+ resolveDecision(result);
139
+ for (const listener of decisionListeners) {
140
+ Promise.resolve(listener(result)).catch((error) => {
141
+ console.error("[Plan Review] Decision listener failed:", error);
142
+ });
143
+ }
144
+ return true;
145
+ };
133
146
 
134
147
  // Draft key for annotation persistence
135
148
  const draftKey = options.mode !== "archive" ? contentHash(options.plan) : "";
@@ -324,6 +337,10 @@ export async function startPlanReviewServer(options: {
324
337
  }
325
338
  json(res, { ok: true, results });
326
339
  } else if (url.pathname === "/api/approve" && req.method === "POST") {
340
+ if (decisionSettled) {
341
+ json(res, { ok: true, duplicate: true });
342
+ return;
343
+ }
327
344
  let feedback: string | undefined;
328
345
  let agentSwitch: string | undefined;
329
346
  let requestedPermissionMode: string | undefined;
@@ -390,7 +407,7 @@ export async function startPlanReviewServer(options: {
390
407
  }
391
408
  deleteDraft(draftKey);
392
409
  const effectivePermissionMode = requestedPermissionMode || options.permissionMode;
393
- resolveDecision({
410
+ publishDecision({
394
411
  approved: true,
395
412
  feedback,
396
413
  savedPath,
@@ -399,6 +416,10 @@ export async function startPlanReviewServer(options: {
399
416
  });
400
417
  json(res, { ok: true, savedPath });
401
418
  } else if (url.pathname === "/api/deny" && req.method === "POST") {
419
+ if (decisionSettled) {
420
+ json(res, { ok: true, duplicate: true });
421
+ return;
422
+ }
402
423
  let feedback = "Plan rejected by user";
403
424
  let planSaveEnabled = true;
404
425
  let planSaveCustomPath: string | undefined;
@@ -425,7 +446,7 @@ export async function startPlanReviewServer(options: {
425
446
  );
426
447
  }
427
448
  deleteDraft(draftKey);
428
- resolveDecision({ approved: false, feedback, savedPath });
449
+ publishDecision({ approved: false, feedback, savedPath });
429
450
  json(res, { ok: true, savedPath });
430
451
  } else {
431
452
  html(res, options.htmlContent);
@@ -435,10 +456,17 @@ export async function startPlanReviewServer(options: {
435
456
  const { port, portSource } = await listenOnPort(server);
436
457
 
437
458
  return {
459
+ reviewId,
438
460
  port,
439
461
  portSource,
440
462
  url: `http://localhost:${port}`,
441
463
  waitForDecision: () => decisionPromise,
464
+ onDecision: (listener) => {
465
+ decisionListeners.add(listener);
466
+ return () => {
467
+ decisionListeners.delete(listener);
468
+ };
469
+ },
442
470
  ...(donePromise && { waitForDone: () => donePromise }),
443
471
  stop: () => server.close(),
444
472
  };
@@ -95,8 +95,8 @@ const reviewRuntime: ReviewGitRuntime = {
95
95
  },
96
96
  };
97
97
 
98
- export function getGitContext(): Promise<GitContext> {
99
- return getGitContextCore(reviewRuntime);
98
+ export function getGitContext(cwd?: string): Promise<GitContext> {
99
+ return getGitContextCore(reviewRuntime, cwd);
100
100
  }
101
101
 
102
102
  export function runGitDiff(