@plannotator/pi-extension 0.17.0 → 0.17.1
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/generated/repo.ts +13 -5
- package/index.ts +9 -3
- package/package.json +1 -1
- package/plannotator-browser.ts +193 -9
- package/plannotator-events.ts +2 -0
- package/plannotator.html +2 -2
- package/review-editor.html +122 -116
- package/server/serverReview.ts +91 -49
- package/server.ts +1 -0
package/server/serverReview.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execSync, spawnSync } from "node:child_process";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
2
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
|
+
import os from "node:os";
|
|
4
5
|
|
|
5
6
|
import { Readable } from "node:stream";
|
|
6
7
|
|
|
@@ -46,7 +47,7 @@ import {
|
|
|
46
47
|
} from "./handlers.js";
|
|
47
48
|
import { html, json, parseBody, requestUrl, toWebRequest } from "./helpers.js";
|
|
48
49
|
|
|
49
|
-
import { listenOnPort } from "./network.js";
|
|
50
|
+
import { isRemoteSession, listenOnPort } from "./network.js";
|
|
50
51
|
import {
|
|
51
52
|
fetchPRContext,
|
|
52
53
|
fetchPRFileContent,
|
|
@@ -71,10 +72,24 @@ import {
|
|
|
71
72
|
transformClaudeFindings,
|
|
72
73
|
} from "../generated/claude-review.js";
|
|
73
74
|
|
|
75
|
+
/** Detect if running inside WSL (Windows Subsystem for Linux) */
|
|
76
|
+
function detectWSL(): boolean {
|
|
77
|
+
if (process.platform !== "linux") return false;
|
|
78
|
+
if (os.release().toLowerCase().includes("microsoft")) return true;
|
|
79
|
+
try {
|
|
80
|
+
if (existsSync("/proc/version")) {
|
|
81
|
+
const content = readFileSync("/proc/version", "utf-8").toLowerCase();
|
|
82
|
+
return content.includes("wsl") || content.includes("microsoft");
|
|
83
|
+
}
|
|
84
|
+
} catch { /* ignore */ }
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
74
88
|
export interface ReviewServerResult {
|
|
75
89
|
port: number;
|
|
76
90
|
portSource: "env" | "remote-default" | "random";
|
|
77
91
|
url: string;
|
|
92
|
+
isRemote: boolean;
|
|
78
93
|
waitForDecision: () => Promise<{
|
|
79
94
|
approved: boolean;
|
|
80
95
|
feedback: string;
|
|
@@ -84,7 +99,7 @@ export interface ReviewServerResult {
|
|
|
84
99
|
stop: () => void;
|
|
85
100
|
}
|
|
86
101
|
|
|
87
|
-
const reviewRuntime: ReviewGitRuntime = {
|
|
102
|
+
export const reviewRuntime: ReviewGitRuntime = {
|
|
88
103
|
async runGit(
|
|
89
104
|
args: string[],
|
|
90
105
|
options?: { cwd?: string },
|
|
@@ -132,11 +147,20 @@ export async function startReviewServer(options: {
|
|
|
132
147
|
sharingEnabled?: boolean;
|
|
133
148
|
shareBaseUrl?: string;
|
|
134
149
|
prMetadata?: PRMetadata;
|
|
150
|
+
/** Working directory for agent processes (e.g., --local worktree). Independent of diff pipeline. */
|
|
151
|
+
agentCwd?: string;
|
|
152
|
+
/** Cleanup callback invoked when server stops (e.g., remove temp worktree) */
|
|
153
|
+
onCleanup?: () => void | Promise<void>;
|
|
154
|
+
/** Called when server starts with the URL, remote status, and port */
|
|
155
|
+
onReady?: (url: string, isRemote: boolean, port: number) => void;
|
|
135
156
|
}): Promise<ReviewServerResult> {
|
|
136
157
|
const gitUser = detectGitUser();
|
|
137
158
|
const draftKey = contentHash(options.rawPatch);
|
|
138
159
|
const prMeta = options.prMetadata;
|
|
139
160
|
const isPRMode = !!prMeta;
|
|
161
|
+
const hasLocalAccess = !!options.gitContext;
|
|
162
|
+
const isRemote = isRemoteSession();
|
|
163
|
+
const wslFlag = detectWSL();
|
|
140
164
|
const prRef = prMeta ? prRefFromMetadata(prMeta) : null;
|
|
141
165
|
const platformUser = prRef ? await getPRUser(prRef) : null;
|
|
142
166
|
|
|
@@ -170,6 +194,7 @@ export async function startReviewServer(options: {
|
|
|
170
194
|
let serverUrl = "";
|
|
171
195
|
// Worktree-aware cwd resolver — shared by getCwd, buildCommand, and onJobComplete
|
|
172
196
|
function resolveAgentCwd(): string {
|
|
197
|
+
if (options.agentCwd) return options.agentCwd;
|
|
173
198
|
if (currentDiffType.startsWith("worktree:")) {
|
|
174
199
|
const parsed = parseWorktreeDiffType(currentDiffType);
|
|
175
200
|
if (parsed) return parsed.path;
|
|
@@ -183,11 +208,11 @@ export async function startReviewServer(options: {
|
|
|
183
208
|
|
|
184
209
|
async buildCommand(provider) {
|
|
185
210
|
const cwd = resolveAgentCwd();
|
|
186
|
-
const
|
|
211
|
+
const hasAgentLocalAccess = !!options.agentCwd || !!options.gitContext;
|
|
187
212
|
const userMessage = buildCodexReviewUserMessage(
|
|
188
213
|
currentPatch,
|
|
189
214
|
currentDiffType,
|
|
190
|
-
{ defaultBranch: options.gitContext?.defaultBranch, hasLocalAccess },
|
|
215
|
+
{ defaultBranch: options.gitContext?.defaultBranch, hasLocalAccess: hasAgentLocalAccess },
|
|
191
216
|
options.prMetadata,
|
|
192
217
|
);
|
|
193
218
|
|
|
@@ -214,8 +239,11 @@ export async function startReviewServer(options: {
|
|
|
214
239
|
const output = await parseCodexOutput(meta.outputPath);
|
|
215
240
|
if (!output) return;
|
|
216
241
|
|
|
242
|
+
// Override verdict if there are blocking findings (P0/P1) — Codex's
|
|
243
|
+
// freeform correctness string can say "mostly correct" with real bugs.
|
|
244
|
+
const hasBlockingFindings = output.findings.some((f: any) => f.priority !== null && f.priority <= 1);
|
|
217
245
|
job.summary = {
|
|
218
|
-
correctness: output.overall_correctness,
|
|
246
|
+
correctness: hasBlockingFindings ? "Issues Found" : output.overall_correctness,
|
|
219
247
|
explanation: output.overall_explanation,
|
|
220
248
|
confidence: output.overall_confidence_score,
|
|
221
249
|
};
|
|
@@ -369,13 +397,7 @@ export async function startReviewServer(options: {
|
|
|
369
397
|
aiEndpoints = ai.createAIEndpoints({
|
|
370
398
|
registry,
|
|
371
399
|
sessionManager,
|
|
372
|
-
getCwd:
|
|
373
|
-
if (currentDiffType.startsWith("worktree:")) {
|
|
374
|
-
const parsed = parseWorktreeDiffType(currentDiffType);
|
|
375
|
-
if (parsed) return parsed.path;
|
|
376
|
-
}
|
|
377
|
-
return options.gitContext?.cwd ?? process.cwd();
|
|
378
|
-
},
|
|
400
|
+
getCwd: resolveAgentCwd,
|
|
379
401
|
});
|
|
380
402
|
aiSessionManager = sessionManager;
|
|
381
403
|
aiRegistry = registry;
|
|
@@ -392,19 +414,21 @@ export async function startReviewServer(options: {
|
|
|
392
414
|
rawPatch: currentPatch,
|
|
393
415
|
gitRef: currentGitRef,
|
|
394
416
|
origin: options.origin ?? "pi",
|
|
395
|
-
diffType:
|
|
396
|
-
gitContext:
|
|
417
|
+
diffType: hasLocalAccess ? currentDiffType : undefined,
|
|
418
|
+
gitContext: hasLocalAccess ? options.gitContext : undefined,
|
|
397
419
|
sharingEnabled,
|
|
398
420
|
shareBaseUrl,
|
|
399
421
|
repoInfo,
|
|
422
|
+
isWSL: wslFlag,
|
|
423
|
+
...(options.agentCwd && { agentCwd: options.agentCwd }),
|
|
400
424
|
...(isPRMode && { prMetadata: prMeta, platformUser }),
|
|
401
425
|
...(isPRMode && initialViewedFiles.length > 0 && { viewedFiles: initialViewedFiles }),
|
|
402
|
-
...(currentError
|
|
426
|
+
...(currentError && { error: currentError }),
|
|
403
427
|
serverConfig: getServerConfig(gitUser),
|
|
404
428
|
});
|
|
405
429
|
} else if (url.pathname === "/api/diff/switch" && req.method === "POST") {
|
|
406
|
-
if (
|
|
407
|
-
json(res, { error: "Not available
|
|
430
|
+
if (!hasLocalAccess) {
|
|
431
|
+
json(res, { error: "Not available without local file access" }, 400);
|
|
408
432
|
return;
|
|
409
433
|
}
|
|
410
434
|
try {
|
|
@@ -456,23 +480,21 @@ export async function startReviewServer(options: {
|
|
|
456
480
|
}
|
|
457
481
|
try {
|
|
458
482
|
const body = await parseBody(req);
|
|
483
|
+
const fileComments = (body.fileComments as PRReviewFileComment[]) || [];
|
|
484
|
+
console.error(`[pr-action] ${body.action} with ${fileComments.length} file comment(s), headSha=${prMeta.headSha}`);
|
|
459
485
|
await submitPRReview(
|
|
460
486
|
prRef,
|
|
461
487
|
prMeta.headSha,
|
|
462
488
|
body.action as "approve" | "comment",
|
|
463
489
|
body.body as string,
|
|
464
|
-
|
|
490
|
+
fileComments,
|
|
465
491
|
);
|
|
492
|
+
console.error(`[pr-action] Success`);
|
|
466
493
|
json(res, { ok: true, prUrl: prMeta.url });
|
|
467
494
|
} catch (err) {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
error:
|
|
472
|
-
err instanceof Error ? err.message : "Failed to submit PR review",
|
|
473
|
-
},
|
|
474
|
-
500,
|
|
475
|
-
);
|
|
495
|
+
const message = err instanceof Error ? err.message : "Failed to submit PR review";
|
|
496
|
+
console.error(`[pr-action] Failed: ${message}`);
|
|
497
|
+
json(res, { error: message }, 500);
|
|
476
498
|
}
|
|
477
499
|
} else if (url.pathname === "/api/pr-viewed" && req.method === "POST") {
|
|
478
500
|
if (!isPRMode || !prMeta || !prRef) {
|
|
@@ -498,14 +520,9 @@ export async function startReviewServer(options: {
|
|
|
498
520
|
);
|
|
499
521
|
json(res, { ok: true });
|
|
500
522
|
} catch (err) {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
error:
|
|
505
|
-
err instanceof Error ? err.message : "Failed to update viewed state",
|
|
506
|
-
},
|
|
507
|
-
500,
|
|
508
|
-
);
|
|
523
|
+
const message = err instanceof Error ? err.message : "Failed to update viewed state";
|
|
524
|
+
console.error("[plannotator] /api/pr-viewed error:", message);
|
|
525
|
+
json(res, { error: message }, 500);
|
|
509
526
|
}
|
|
510
527
|
} else if (url.pathname === "/api/file-content" && req.method === "GET") {
|
|
511
528
|
const filePath = url.searchParams.get("path");
|
|
@@ -529,6 +546,23 @@ export async function startReviewServer(options: {
|
|
|
529
546
|
}
|
|
530
547
|
}
|
|
531
548
|
|
|
549
|
+
// Local mode first (matches Bun server priority)
|
|
550
|
+
if (hasLocalAccess && !isPRMode) {
|
|
551
|
+
const defaultBranch = options.gitContext?.defaultBranch || "main";
|
|
552
|
+
const defaultCwd = options.gitContext?.cwd;
|
|
553
|
+
const result = await getFileContentsForDiffCore(
|
|
554
|
+
reviewRuntime,
|
|
555
|
+
currentDiffType,
|
|
556
|
+
defaultBranch,
|
|
557
|
+
filePath,
|
|
558
|
+
oldPath,
|
|
559
|
+
defaultCwd,
|
|
560
|
+
);
|
|
561
|
+
json(res, result);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// PR mode: fetch from platform API using merge-base/head SHAs
|
|
532
566
|
if (isPRMode && prRef && prMeta) {
|
|
533
567
|
try {
|
|
534
568
|
const oldSha = prMeta.mergeBaseSha ?? prMeta.baseSha;
|
|
@@ -552,17 +586,7 @@ export async function startReviewServer(options: {
|
|
|
552
586
|
return;
|
|
553
587
|
}
|
|
554
588
|
|
|
555
|
-
|
|
556
|
-
const defaultCwd = options.gitContext?.cwd;
|
|
557
|
-
const result = await getFileContentsForDiffCore(
|
|
558
|
-
reviewRuntime,
|
|
559
|
-
currentDiffType,
|
|
560
|
-
defaultBranch,
|
|
561
|
-
filePath,
|
|
562
|
-
oldPath,
|
|
563
|
-
defaultCwd,
|
|
564
|
-
);
|
|
565
|
-
json(res, result);
|
|
589
|
+
json(res, { error: "No file access available" }, 400);
|
|
566
590
|
} else if (url.pathname === "/api/config" && req.method === "POST") {
|
|
567
591
|
try {
|
|
568
592
|
const body = (await parseBody(req)) as { displayName?: string; diffOptions?: Record<string, unknown> };
|
|
@@ -581,8 +605,14 @@ export async function startReviewServer(options: {
|
|
|
581
605
|
} else if (url.pathname === "/api/agents" && req.method === "GET") {
|
|
582
606
|
json(res, { agents: [] });
|
|
583
607
|
} else if (url.pathname === "/api/git-add" && req.method === "POST") {
|
|
584
|
-
|
|
585
|
-
|
|
608
|
+
// Staging only available for local diff types that support it (not PR mode, not branch diffs).
|
|
609
|
+
// Worktree diff types use composite format "worktree:/path:uncommitted" — extract the base type.
|
|
610
|
+
const baseDiffType = currentDiffType.startsWith("worktree:")
|
|
611
|
+
? (parseWorktreeDiffType(currentDiffType)?.subType ?? currentDiffType)
|
|
612
|
+
: currentDiffType;
|
|
613
|
+
const canStage = baseDiffType === "uncommitted" || baseDiffType === "unstaged";
|
|
614
|
+
if (isPRMode || !canStage) {
|
|
615
|
+
json(res, { error: "Staging not available" }, 400);
|
|
586
616
|
return;
|
|
587
617
|
}
|
|
588
618
|
try {
|
|
@@ -608,7 +638,7 @@ export async function startReviewServer(options: {
|
|
|
608
638
|
json(res, { ok: true });
|
|
609
639
|
} catch (err) {
|
|
610
640
|
const message =
|
|
611
|
-
err instanceof Error ? err.message : "Failed to
|
|
641
|
+
err instanceof Error ? err.message : "Failed to stage file";
|
|
612
642
|
json(res, { error: message }, 500);
|
|
613
643
|
}
|
|
614
644
|
} else if (url.pathname === "/api/draft") {
|
|
@@ -674,10 +704,15 @@ export async function startReviewServer(options: {
|
|
|
674
704
|
const exitHandler = () => agentJobs.killAll();
|
|
675
705
|
process.once("exit", exitHandler);
|
|
676
706
|
|
|
707
|
+
if (options.onReady) {
|
|
708
|
+
options.onReady(serverUrl, isRemote, port);
|
|
709
|
+
}
|
|
710
|
+
|
|
677
711
|
return {
|
|
678
712
|
port,
|
|
679
713
|
portSource,
|
|
680
714
|
url: serverUrl,
|
|
715
|
+
isRemote,
|
|
681
716
|
waitForDecision: () => decisionPromise,
|
|
682
717
|
stop: () => {
|
|
683
718
|
process.removeListener("exit", exitHandler);
|
|
@@ -685,6 +720,13 @@ export async function startReviewServer(options: {
|
|
|
685
720
|
aiSessionManager?.disposeAll();
|
|
686
721
|
aiRegistry?.disposeAll();
|
|
687
722
|
server.close();
|
|
723
|
+
// Invoke cleanup callback (e.g., remove temp worktree)
|
|
724
|
+
if (options.onCleanup) {
|
|
725
|
+
try {
|
|
726
|
+
const result = options.onCleanup();
|
|
727
|
+
if (result instanceof Promise) result.catch(() => {});
|
|
728
|
+
} catch { /* best effort */ }
|
|
729
|
+
}
|
|
688
730
|
},
|
|
689
731
|
};
|
|
690
732
|
}
|