diffprism 0.28.0 → 0.31.0
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/dist/bin.js +56 -5
- package/dist/{chunk-VPIL4KYB.js → chunk-OJ723D6Z.js} +252 -18
- package/dist/mcp-server.js +378 -3
- package/package.json +1 -1
- package/ui-dist/assets/index-BE8-SWvU.css +1 -0
- package/ui-dist/assets/index-BoUaIrQD.js +319 -0
- package/ui-dist/index.html +2 -2
- package/ui-dist/assets/index-D_PeJaM9.css +0 -1
- package/ui-dist/assets/index-swQ2C7w3.js +0 -304
package/dist/bin.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
startGlobalServer,
|
|
7
7
|
startReview,
|
|
8
8
|
startWatch
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-OJ723D6Z.js";
|
|
10
10
|
|
|
11
11
|
// cli/src/index.ts
|
|
12
12
|
import { Command } from "commander";
|
|
@@ -140,10 +140,46 @@ Use this pattern:
|
|
|
140
140
|
|
|
141
141
|
You can also check for feedback without blocking by calling \`get_review_result\` without \`wait\` at natural breakpoints (between tasks, before committing, etc.).
|
|
142
142
|
|
|
143
|
+
## Self-Review: Headless Analysis Tools
|
|
144
|
+
|
|
145
|
+
DiffPrism provides two headless tools that return analysis data as JSON without opening a browser. Use these to check your own work before requesting human review.
|
|
146
|
+
|
|
147
|
+
### Available Headless Tools
|
|
148
|
+
|
|
149
|
+
- **\`mcp__diffprism__get_diff\`** \u2014 Returns a structured \`DiffSet\` (files, hunks, additions/deletions) for a given diff ref. Use this to inspect exactly what changed.
|
|
150
|
+
- **\`mcp__diffprism__analyze_diff\`** \u2014 Returns a \`ReviewBriefing\` with summary, file triage, impact detection, complexity scores, test coverage gaps, and pattern flags (security issues, TODOs, console.logs left in, etc.).
|
|
151
|
+
|
|
152
|
+
Both accept a \`diff_ref\` parameter: \`"staged"\`, \`"unstaged"\`, \`"working-copy"\`, or a git range like \`"HEAD~3..HEAD"\`.
|
|
153
|
+
|
|
154
|
+
### Self-Review Loop
|
|
155
|
+
|
|
156
|
+
When you've finished writing code and before requesting human review, use this pattern:
|
|
157
|
+
|
|
158
|
+
1. **Analyze your changes:** Call \`mcp__diffprism__analyze_diff\` with \`diff_ref: "working-copy"\`
|
|
159
|
+
2. **Check the briefing for issues:**
|
|
160
|
+
- \`patterns\` \u2014 Look for console.logs, TODOs, security flags, disabled tests
|
|
161
|
+
- \`testCoverage\` \u2014 Check if changed source files have corresponding test changes
|
|
162
|
+
- \`complexity\` \u2014 Review high-complexity scores
|
|
163
|
+
- \`impact.newDependencies\` \u2014 Verify any new deps are intentional
|
|
164
|
+
- \`impact.breakingChanges\` \u2014 Confirm breaking changes are expected
|
|
165
|
+
3. **Fix any issues found** \u2014 Remove debug statements, add missing tests, address security flags
|
|
166
|
+
4. **Re-analyze** \u2014 Run \`analyze_diff\` again to confirm the issues are resolved
|
|
167
|
+
5. **Open for human review** \u2014 Once clean, use \`/review\` or \`open_review\` for final human sign-off
|
|
168
|
+
|
|
169
|
+
This loop catches common issues (leftover console.logs, missing tests, security anti-patterns) before the human reviewer sees them, making reviews faster and more focused.
|
|
170
|
+
|
|
171
|
+
### When to Use Headless Tools
|
|
172
|
+
|
|
173
|
+
- **After completing a coding task** \u2014 Self-check before requesting review
|
|
174
|
+
- **During implementation** \u2014 Periodically check for patterns and issues as you work
|
|
175
|
+
- **Before committing** \u2014 Quick sanity check on what's about to be committed
|
|
176
|
+
- **Do NOT use these as a replacement for human review** \u2014 They complement, not replace, \`/review\`
|
|
177
|
+
|
|
143
178
|
## Behavior Rules
|
|
144
179
|
|
|
145
180
|
- **IMPORTANT: Do NOT open reviews automatically.** Only open a review when the user explicitly invokes \`/review\` or directly asks for a review.
|
|
146
181
|
- Do NOT open reviews before commits, after code changes, or at any other time unless the user requests it.
|
|
182
|
+
- Headless tools (\`get_diff\`, \`analyze_diff\`) can be used proactively during development without explicit user request \u2014 they don't open a browser or interrupt the user.
|
|
147
183
|
- Power users can create \`diffprism.config.json\` manually to customize defaults (diff scope, reasoning).
|
|
148
184
|
`;
|
|
149
185
|
|
|
@@ -203,7 +239,12 @@ function setupClaudeSettings(baseDir, force) {
|
|
|
203
239
|
const toolNames = [
|
|
204
240
|
"mcp__diffprism__open_review",
|
|
205
241
|
"mcp__diffprism__update_review_context",
|
|
206
|
-
"mcp__diffprism__get_review_result"
|
|
242
|
+
"mcp__diffprism__get_review_result",
|
|
243
|
+
"mcp__diffprism__get_diff",
|
|
244
|
+
"mcp__diffprism__analyze_diff",
|
|
245
|
+
"mcp__diffprism__add_annotation",
|
|
246
|
+
"mcp__diffprism__get_review_state",
|
|
247
|
+
"mcp__diffprism__flag_for_attention"
|
|
207
248
|
];
|
|
208
249
|
const allPresent = toolNames.every((t) => allow.includes(t));
|
|
209
250
|
if (allPresent && !force) {
|
|
@@ -433,7 +474,12 @@ function isGlobalSetupDone() {
|
|
|
433
474
|
const toolNames = [
|
|
434
475
|
"mcp__diffprism__open_review",
|
|
435
476
|
"mcp__diffprism__update_review_context",
|
|
436
|
-
"mcp__diffprism__get_review_result"
|
|
477
|
+
"mcp__diffprism__get_review_result",
|
|
478
|
+
"mcp__diffprism__get_diff",
|
|
479
|
+
"mcp__diffprism__analyze_diff",
|
|
480
|
+
"mcp__diffprism__add_annotation",
|
|
481
|
+
"mcp__diffprism__get_review_state",
|
|
482
|
+
"mcp__diffprism__flag_for_attention"
|
|
437
483
|
];
|
|
438
484
|
return toolNames.every((t) => allow.includes(t));
|
|
439
485
|
}
|
|
@@ -476,7 +522,12 @@ function teardownClaudePermissions(baseDir) {
|
|
|
476
522
|
const toolNames = [
|
|
477
523
|
"mcp__diffprism__open_review",
|
|
478
524
|
"mcp__diffprism__update_review_context",
|
|
479
|
-
"mcp__diffprism__get_review_result"
|
|
525
|
+
"mcp__diffprism__get_review_result",
|
|
526
|
+
"mcp__diffprism__get_diff",
|
|
527
|
+
"mcp__diffprism__analyze_diff",
|
|
528
|
+
"mcp__diffprism__add_annotation",
|
|
529
|
+
"mcp__diffprism__get_review_state",
|
|
530
|
+
"mcp__diffprism__flag_for_attention"
|
|
480
531
|
];
|
|
481
532
|
const filtered = allow.filter((t) => !toolNames.includes(t));
|
|
482
533
|
if (filtered.length === allow.length) {
|
|
@@ -836,7 +887,7 @@ async function serverStop() {
|
|
|
836
887
|
|
|
837
888
|
// cli/src/index.ts
|
|
838
889
|
var program = new Command();
|
|
839
|
-
program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.
|
|
890
|
+
program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.31.0" : "0.0.0-dev");
|
|
840
891
|
program.command("review [ref]").description("Open a browser-based diff review").option("--staged", "Review staged changes").option("--unstaged", "Review unstaged changes").option("-t, --title <title>", "Review title").option("--dev", "Use Vite dev server with HMR instead of static files").action(review);
|
|
841
892
|
program.command("start [ref]").description("Set up DiffPrism and start watching for changes").option("--staged", "Watch staged changes").option("--unstaged", "Watch unstaged changes").option("-t, --title <title>", "Review title").option("--interval <ms>", "Poll interval in milliseconds (default: 1000)").option("--dev", "Use Vite dev server with HMR instead of static files").option("--global", "Install skill globally (~/.claude/skills/)").option("--force", "Overwrite existing configuration files").action(start);
|
|
842
893
|
program.command("watch [ref]").description("Start a persistent diff watcher with live-updating browser UI").option("--staged", "Watch staged changes").option("--unstaged", "Watch unstaged changes").option("-t, --title <title>", "Review title").option("--interval <ms>", "Poll interval in milliseconds (default: 1000)").option("--dev", "Use Vite dev server with HMR instead of static files").action(watch);
|
|
@@ -116,6 +116,46 @@ function listCommits(options) {
|
|
|
116
116
|
return [];
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
+
function detectWorktree(options) {
|
|
120
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
121
|
+
try {
|
|
122
|
+
const gitDir = execSync("git rev-parse --git-dir", {
|
|
123
|
+
cwd,
|
|
124
|
+
encoding: "utf-8",
|
|
125
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
126
|
+
}).trim();
|
|
127
|
+
const gitCommonDir = execSync("git rev-parse --git-common-dir", {
|
|
128
|
+
cwd,
|
|
129
|
+
encoding: "utf-8",
|
|
130
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
131
|
+
}).trim();
|
|
132
|
+
const resolvedGitDir = path.resolve(cwd, gitDir);
|
|
133
|
+
const resolvedCommonDir = path.resolve(cwd, gitCommonDir);
|
|
134
|
+
const isWorktree = resolvedGitDir !== resolvedCommonDir;
|
|
135
|
+
if (!isWorktree) {
|
|
136
|
+
return { isWorktree: false };
|
|
137
|
+
}
|
|
138
|
+
const worktreePath = execSync("git rev-parse --show-toplevel", {
|
|
139
|
+
cwd,
|
|
140
|
+
encoding: "utf-8",
|
|
141
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
142
|
+
}).trim();
|
|
143
|
+
const mainWorktreePath = path.dirname(resolvedCommonDir);
|
|
144
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
145
|
+
cwd,
|
|
146
|
+
encoding: "utf-8",
|
|
147
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
148
|
+
}).trim();
|
|
149
|
+
return {
|
|
150
|
+
isWorktree: true,
|
|
151
|
+
worktreePath,
|
|
152
|
+
mainWorktreePath,
|
|
153
|
+
branch: branch === "HEAD" ? void 0 : branch
|
|
154
|
+
};
|
|
155
|
+
} catch {
|
|
156
|
+
return { isWorktree: false };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
119
159
|
function getUntrackedDiffs(cwd) {
|
|
120
160
|
let untrackedList;
|
|
121
161
|
try {
|
|
@@ -398,17 +438,138 @@ function getWorkingCopyDiff(options) {
|
|
|
398
438
|
}
|
|
399
439
|
|
|
400
440
|
// packages/analysis/src/deterministic.ts
|
|
441
|
+
var MECHANICAL_CONFIG_PATTERNS = [
|
|
442
|
+
/\.config\./,
|
|
443
|
+
/\.eslintrc/,
|
|
444
|
+
/\.prettierrc/,
|
|
445
|
+
/tsconfig.*\.json$/,
|
|
446
|
+
/\.gitignore$/,
|
|
447
|
+
/\.lock$/
|
|
448
|
+
];
|
|
449
|
+
var API_SURFACE_PATTERNS = [
|
|
450
|
+
/\/api\//,
|
|
451
|
+
/\/routes\//
|
|
452
|
+
];
|
|
453
|
+
function isFormattingOnly(file) {
|
|
454
|
+
if (file.hunks.length === 0) return false;
|
|
455
|
+
for (const hunk of file.hunks) {
|
|
456
|
+
const adds = hunk.changes.filter((c) => c.type === "add").map((c) => c.content.replace(/\s/g, ""));
|
|
457
|
+
const deletes = hunk.changes.filter((c) => c.type === "delete").map((c) => c.content.replace(/\s/g, ""));
|
|
458
|
+
if (adds.length === 0 || deletes.length === 0) return false;
|
|
459
|
+
const deleteBag = [...deletes];
|
|
460
|
+
for (const add of adds) {
|
|
461
|
+
const idx = deleteBag.indexOf(add);
|
|
462
|
+
if (idx === -1) return false;
|
|
463
|
+
deleteBag.splice(idx, 1);
|
|
464
|
+
}
|
|
465
|
+
if (deleteBag.length > 0) return false;
|
|
466
|
+
}
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
function isImportOnly(file) {
|
|
470
|
+
if (file.hunks.length === 0) return false;
|
|
471
|
+
const importPattern = /^\s*(import\s|export\s.*from\s|const\s+\w+\s*=\s*require\(|require\()/;
|
|
472
|
+
for (const hunk of file.hunks) {
|
|
473
|
+
for (const change of hunk.changes) {
|
|
474
|
+
if (change.type === "context") continue;
|
|
475
|
+
const trimmed = change.content.trim();
|
|
476
|
+
if (trimmed === "") continue;
|
|
477
|
+
if (!importPattern.test(trimmed)) return false;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
function isMechanicalConfigFile(path7) {
|
|
483
|
+
return MECHANICAL_CONFIG_PATTERNS.some((re) => re.test(path7));
|
|
484
|
+
}
|
|
485
|
+
function isApiSurface(file) {
|
|
486
|
+
if (API_SURFACE_PATTERNS.some((re) => re.test(file.path))) return true;
|
|
487
|
+
const basename = file.path.slice(file.path.lastIndexOf("/") + 1);
|
|
488
|
+
if ((basename === "index.ts" || basename === "index.js") && file.additions >= 10) {
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
401
493
|
function categorizeFiles(files) {
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
494
|
+
const critical = [];
|
|
495
|
+
const notable = [];
|
|
496
|
+
const mechanical = [];
|
|
497
|
+
const securityFlags = detectSecurityPatterns(files);
|
|
498
|
+
const complexityScores = computeComplexityScores(files);
|
|
499
|
+
const securityByFile = /* @__PURE__ */ new Map();
|
|
500
|
+
for (const flag of securityFlags) {
|
|
501
|
+
const existing = securityByFile.get(flag.file) || [];
|
|
502
|
+
existing.push(flag);
|
|
503
|
+
securityByFile.set(flag.file, existing);
|
|
504
|
+
}
|
|
505
|
+
const complexityByFile = /* @__PURE__ */ new Map();
|
|
506
|
+
for (const score of complexityScores) {
|
|
507
|
+
complexityByFile.set(score.path, score);
|
|
508
|
+
}
|
|
509
|
+
for (const file of files) {
|
|
510
|
+
const description = `${file.status} (${file.language || "unknown"}) +${file.additions} -${file.deletions}`;
|
|
511
|
+
const fileSecurityFlags = securityByFile.get(file.path);
|
|
512
|
+
const fileComplexity = complexityByFile.get(file.path);
|
|
513
|
+
const criticalReasons = [];
|
|
514
|
+
if (fileSecurityFlags && fileSecurityFlags.length > 0) {
|
|
515
|
+
const patterns = fileSecurityFlags.map((f) => f.pattern);
|
|
516
|
+
const unique = [...new Set(patterns)];
|
|
517
|
+
criticalReasons.push(`security patterns detected: ${unique.join(", ")}`);
|
|
518
|
+
}
|
|
519
|
+
if (fileComplexity && fileComplexity.score >= 8) {
|
|
520
|
+
criticalReasons.push(`high complexity score (${fileComplexity.score}/10)`);
|
|
521
|
+
}
|
|
522
|
+
if (isApiSurface(file)) {
|
|
523
|
+
criticalReasons.push("modifies public API surface");
|
|
524
|
+
}
|
|
525
|
+
if (criticalReasons.length > 0) {
|
|
526
|
+
critical.push({
|
|
527
|
+
file: file.path,
|
|
528
|
+
description,
|
|
529
|
+
reason: `Critical: ${criticalReasons.join("; ")}`
|
|
530
|
+
});
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
const isPureRename = file.status === "renamed" && file.additions === 0 && file.deletions === 0;
|
|
534
|
+
if (isPureRename) {
|
|
535
|
+
mechanical.push({
|
|
536
|
+
file: file.path,
|
|
537
|
+
description,
|
|
538
|
+
reason: "Mechanical: pure rename with no content changes"
|
|
539
|
+
});
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
if (isFormattingOnly(file)) {
|
|
543
|
+
mechanical.push({
|
|
544
|
+
file: file.path,
|
|
545
|
+
description,
|
|
546
|
+
reason: "Mechanical: formatting/whitespace-only changes"
|
|
547
|
+
});
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
if (isMechanicalConfigFile(file.path)) {
|
|
551
|
+
mechanical.push({
|
|
552
|
+
file: file.path,
|
|
553
|
+
description,
|
|
554
|
+
reason: "Mechanical: config file change"
|
|
555
|
+
});
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (file.hunks.length > 0 && isImportOnly(file)) {
|
|
559
|
+
mechanical.push({
|
|
560
|
+
file: file.path,
|
|
561
|
+
description,
|
|
562
|
+
reason: "Mechanical: import/require-only changes"
|
|
563
|
+
});
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
notable.push({
|
|
567
|
+
file: file.path,
|
|
568
|
+
description,
|
|
569
|
+
reason: "Notable: requires review"
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
return { critical, notable, mechanical };
|
|
412
573
|
}
|
|
413
574
|
function computeFileStats(files) {
|
|
414
575
|
return files.map((f) => ({
|
|
@@ -584,15 +745,15 @@ var CONFIG_PATTERNS = [
|
|
|
584
745
|
/vite\.config/,
|
|
585
746
|
/vitest\.config/
|
|
586
747
|
];
|
|
587
|
-
function isTestFile(
|
|
588
|
-
return TEST_PATTERNS.some((re) => re.test(
|
|
748
|
+
function isTestFile(path7) {
|
|
749
|
+
return TEST_PATTERNS.some((re) => re.test(path7));
|
|
589
750
|
}
|
|
590
|
-
function isNonCodeFile(
|
|
591
|
-
const ext =
|
|
751
|
+
function isNonCodeFile(path7) {
|
|
752
|
+
const ext = path7.slice(path7.lastIndexOf("."));
|
|
592
753
|
return NON_CODE_EXTENSIONS.has(ext);
|
|
593
754
|
}
|
|
594
|
-
function isConfigFile(
|
|
595
|
-
return CONFIG_PATTERNS.some((re) => re.test(
|
|
755
|
+
function isConfigFile(path7) {
|
|
756
|
+
return CONFIG_PATTERNS.some((re) => re.test(path7));
|
|
596
757
|
}
|
|
597
758
|
function detectTestCoverageGaps(files) {
|
|
598
759
|
const filePaths = new Set(files.map((f) => f.path));
|
|
@@ -1289,11 +1450,17 @@ async function startReview(options) {
|
|
|
1289
1450
|
const briefing = analyze(diffSet);
|
|
1290
1451
|
const session = createSession(options);
|
|
1291
1452
|
updateSession(session.id, { status: "in_progress" });
|
|
1453
|
+
const worktreeInfo = detectWorktree({ cwd });
|
|
1292
1454
|
const metadata = {
|
|
1293
1455
|
title,
|
|
1294
1456
|
description,
|
|
1295
1457
|
reasoning,
|
|
1296
|
-
currentBranch
|
|
1458
|
+
currentBranch,
|
|
1459
|
+
worktree: worktreeInfo.isWorktree ? {
|
|
1460
|
+
isWorktree: true,
|
|
1461
|
+
worktreePath: worktreeInfo.worktreePath,
|
|
1462
|
+
mainWorktreePath: worktreeInfo.mainWorktreePath
|
|
1463
|
+
} : void 0
|
|
1297
1464
|
};
|
|
1298
1465
|
let poller = null;
|
|
1299
1466
|
const [bridgePort, httpPort] = await Promise.all([
|
|
@@ -1901,6 +2068,7 @@ async function handleApiRequest(req, res) {
|
|
|
1901
2068
|
existingSession.lastDiffHash = diffRef ? hashDiff(payload.rawDiff) : void 0;
|
|
1902
2069
|
existingSession.lastDiffSet = diffRef ? payload.diffSet : void 0;
|
|
1903
2070
|
existingSession.hasNewChanges = false;
|
|
2071
|
+
existingSession.annotations = [];
|
|
1904
2072
|
if (diffRef && hasConnectedClients()) {
|
|
1905
2073
|
startSessionWatcher(sessionId);
|
|
1906
2074
|
}
|
|
@@ -1929,7 +2097,8 @@ async function handleApiRequest(req, res) {
|
|
|
1929
2097
|
diffRef,
|
|
1930
2098
|
lastDiffHash: diffRef ? hashDiff(payload.rawDiff) : void 0,
|
|
1931
2099
|
lastDiffSet: diffRef ? payload.diffSet : void 0,
|
|
1932
|
-
hasNewChanges: false
|
|
2100
|
+
hasNewChanges: false,
|
|
2101
|
+
annotations: []
|
|
1933
2102
|
};
|
|
1934
2103
|
sessions2.set(sessionId, session);
|
|
1935
2104
|
if (diffRef && hasConnectedClients()) {
|
|
@@ -2028,6 +2197,65 @@ async function handleApiRequest(req, res) {
|
|
|
2028
2197
|
}
|
|
2029
2198
|
return true;
|
|
2030
2199
|
}
|
|
2200
|
+
const postAnnotationParams = matchRoute(method, url, "POST", "/api/reviews/:id/annotations");
|
|
2201
|
+
if (postAnnotationParams) {
|
|
2202
|
+
const session = sessions2.get(postAnnotationParams.id);
|
|
2203
|
+
if (!session) {
|
|
2204
|
+
jsonResponse(res, 404, { error: "Session not found" });
|
|
2205
|
+
return true;
|
|
2206
|
+
}
|
|
2207
|
+
try {
|
|
2208
|
+
const body = await readBody(req);
|
|
2209
|
+
const { file, line, body: annotationBody, type, confidence, category, source } = JSON.parse(body);
|
|
2210
|
+
const annotation = {
|
|
2211
|
+
id: randomUUID(),
|
|
2212
|
+
sessionId: session.id,
|
|
2213
|
+
file,
|
|
2214
|
+
line,
|
|
2215
|
+
body: annotationBody,
|
|
2216
|
+
type,
|
|
2217
|
+
confidence: confidence ?? 1,
|
|
2218
|
+
category: category ?? "other",
|
|
2219
|
+
source,
|
|
2220
|
+
createdAt: Date.now()
|
|
2221
|
+
};
|
|
2222
|
+
session.annotations.push(annotation);
|
|
2223
|
+
sendToSessionClients(session.id, {
|
|
2224
|
+
type: "annotation:added",
|
|
2225
|
+
payload: annotation
|
|
2226
|
+
});
|
|
2227
|
+
jsonResponse(res, 200, { annotationId: annotation.id });
|
|
2228
|
+
} catch {
|
|
2229
|
+
jsonResponse(res, 400, { error: "Invalid request body" });
|
|
2230
|
+
}
|
|
2231
|
+
return true;
|
|
2232
|
+
}
|
|
2233
|
+
const getAnnotationsParams = matchRoute(method, url, "GET", "/api/reviews/:id/annotations");
|
|
2234
|
+
if (getAnnotationsParams) {
|
|
2235
|
+
const session = sessions2.get(getAnnotationsParams.id);
|
|
2236
|
+
if (!session) {
|
|
2237
|
+
jsonResponse(res, 404, { error: "Session not found" });
|
|
2238
|
+
return true;
|
|
2239
|
+
}
|
|
2240
|
+
jsonResponse(res, 200, { annotations: session.annotations });
|
|
2241
|
+
return true;
|
|
2242
|
+
}
|
|
2243
|
+
const dismissAnnotationParams = matchRoute(method, url, "POST", "/api/reviews/:id/annotations/:annotationId/dismiss");
|
|
2244
|
+
if (dismissAnnotationParams) {
|
|
2245
|
+
const session = sessions2.get(dismissAnnotationParams.id);
|
|
2246
|
+
if (!session) {
|
|
2247
|
+
jsonResponse(res, 404, { error: "Session not found" });
|
|
2248
|
+
return true;
|
|
2249
|
+
}
|
|
2250
|
+
const annotation = session.annotations.find((a) => a.id === dismissAnnotationParams.annotationId);
|
|
2251
|
+
if (!annotation) {
|
|
2252
|
+
jsonResponse(res, 404, { error: "Annotation not found" });
|
|
2253
|
+
return true;
|
|
2254
|
+
}
|
|
2255
|
+
annotation.dismissed = true;
|
|
2256
|
+
jsonResponse(res, 200, { ok: true });
|
|
2257
|
+
return true;
|
|
2258
|
+
}
|
|
2031
2259
|
const deleteParams = matchRoute(method, url, "DELETE", "/api/reviews/:id");
|
|
2032
2260
|
if (deleteParams) {
|
|
2033
2261
|
stopSessionWatcher(deleteParams.id);
|
|
@@ -2340,8 +2568,14 @@ Waiting for reviews...
|
|
|
2340
2568
|
return { httpPort, wsPort, stop };
|
|
2341
2569
|
}
|
|
2342
2570
|
|
|
2571
|
+
// packages/core/src/review-history.ts
|
|
2572
|
+
import fs4 from "fs";
|
|
2573
|
+
import path6 from "path";
|
|
2574
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2575
|
+
|
|
2343
2576
|
export {
|
|
2344
2577
|
getCurrentBranch,
|
|
2578
|
+
detectWorktree,
|
|
2345
2579
|
getDiff,
|
|
2346
2580
|
analyze,
|
|
2347
2581
|
readWatchFile,
|