diffprism 0.49.0 → 1.1.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/README.md +60 -20
- package/dist/bin.js +457 -358
- package/dist/{chunk-QGWYCEJN.js → chunk-3GMPE2ZR.js} +13 -0
- package/dist/{chunk-24B33UN6.js → chunk-EPU4F7WT.js} +6 -56
- package/dist/{chunk-7UQM4WBZ.js → chunk-FWPE5EA6.js} +2 -2
- package/dist/{chunk-EPUCA2N5.js → chunk-KRHZSUEK.js} +704 -287
- package/dist/{demo-VPKUCZT3.js → demo-Z2XUXEV3.js} +3 -3
- package/dist/mcp-server.js +432 -963
- package/dist/{src-KF5HRJPX.js → src-DNNNQ2RD.js} +2 -2
- package/dist/{src-AMCPIYDZ.js → src-OZQ32NDN.js} +3 -1
- package/package.json +1 -1
- package/ui-dist/assets/index-BulxhOr3.css +1 -0
- package/ui-dist/assets/index-Zw97bHGn.js +345 -0
- package/ui-dist/index.html +2 -2
- package/ui-dist/assets/index-DpPy4Hg4.css +0 -1
- package/ui-dist/assets/index-Dt04Ez08.js +0 -335
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getCurrentBranch,
|
|
3
3
|
getDiff,
|
|
4
|
+
getRepoRoot,
|
|
4
5
|
listBranches,
|
|
5
6
|
listCommits
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-3GMPE2ZR.js";
|
|
7
8
|
import {
|
|
8
9
|
analyze
|
|
9
10
|
} from "./chunk-DHCVZGHE.js";
|
|
@@ -78,24 +79,162 @@ async function isServerAlive() {
|
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
// packages/core/src/
|
|
82
|
-
|
|
82
|
+
// packages/core/src/diff-scope.ts
|
|
83
|
+
var DEFAULT_DIFF_REF = "working-copy";
|
|
84
|
+
var COMMIT_GATE_DIFF_REF = "staged";
|
|
85
|
+
var DIFF_REF_DESCRIPTION = 'Which changes to review. "working-copy" (the default): everything not yet committed, staged and unstaged shown as separate groups. "staged": only what the next commit would contain. "unstaged": only edits not yet staged. Or a ref range such as "HEAD~3..HEAD" or "main..feature".';
|
|
86
|
+
|
|
87
|
+
// packages/core/src/build-info.ts
|
|
83
88
|
import fs2 from "fs";
|
|
84
89
|
import path2 from "path";
|
|
85
|
-
import os2 from "os";
|
|
86
90
|
import { fileURLToPath } from "url";
|
|
91
|
+
function getBuildInfo() {
|
|
92
|
+
let dir = path2.dirname(fileURLToPath(import.meta.url));
|
|
93
|
+
while (dir !== path2.dirname(dir)) {
|
|
94
|
+
const manifest = path2.join(dir, "package.json");
|
|
95
|
+
if (fs2.existsSync(manifest) && isOwnManifest(manifest)) {
|
|
96
|
+
return fs2.existsSync(path2.join(dir, ".git")) ? { dev: true, root: dir } : { dev: false, root: null };
|
|
97
|
+
}
|
|
98
|
+
dir = path2.dirname(dir);
|
|
99
|
+
}
|
|
100
|
+
return { dev: false, root: null };
|
|
101
|
+
}
|
|
102
|
+
function isOwnManifest(manifestPath) {
|
|
103
|
+
try {
|
|
104
|
+
const manifest = JSON.parse(fs2.readFileSync(manifestPath, "utf8"));
|
|
105
|
+
return manifest.name === "diffprism";
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function describeVersion(version) {
|
|
111
|
+
const info = getBuildInfo();
|
|
112
|
+
return info.dev ? `${version} (dev build \u2014 ${info.root})` : version;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// packages/core/src/feedback.ts
|
|
116
|
+
import fs3 from "fs";
|
|
117
|
+
import os2 from "os";
|
|
118
|
+
import path3 from "path";
|
|
119
|
+
var ISSUES_NEW_URL = "https://github.com/CodeJonesW/diffprism/issues/new";
|
|
120
|
+
var MAX_ERROR_CHARS = 1500;
|
|
121
|
+
function currentVersion() {
|
|
122
|
+
return true ? "1.1.0" : "0.0.0-dev";
|
|
123
|
+
}
|
|
124
|
+
function describeEnvironment() {
|
|
125
|
+
return {
|
|
126
|
+
version: currentVersion(),
|
|
127
|
+
// Whether it is a dev build, but not where: the checkout path is private.
|
|
128
|
+
build: getBuildInfo().dev ? "dev build" : "release",
|
|
129
|
+
os: `${os2.type()} ${os2.release()} (${os2.arch()})`,
|
|
130
|
+
node: process.version
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function lastErrorFile() {
|
|
134
|
+
return path3.join(os2.homedir(), ".diffprism", "last-error.json");
|
|
135
|
+
}
|
|
136
|
+
function recordError(command, err) {
|
|
137
|
+
const report = {
|
|
138
|
+
command,
|
|
139
|
+
message: err instanceof Error ? err.message : String(err),
|
|
140
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
141
|
+
};
|
|
142
|
+
try {
|
|
143
|
+
fs3.mkdirSync(path3.dirname(lastErrorFile()), { recursive: true });
|
|
144
|
+
fs3.writeFileSync(lastErrorFile(), JSON.stringify(report, null, 2));
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function readLastError() {
|
|
149
|
+
try {
|
|
150
|
+
return JSON.parse(fs3.readFileSync(lastErrorFile(), "utf8"));
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
var REPORT_HINT = "Think this is a DiffPrism bug? Report it: diffprism feedback --bug";
|
|
156
|
+
function redactHome(text) {
|
|
157
|
+
const home = os2.homedir();
|
|
158
|
+
return home ? text.split(home).join("~") : text;
|
|
159
|
+
}
|
|
160
|
+
function buildFeedbackUrl(options) {
|
|
161
|
+
const env = options.environment ?? describeEnvironment();
|
|
162
|
+
const message = options.message?.trim();
|
|
163
|
+
const error = options.kind === "bug" ? options.error : null;
|
|
164
|
+
const firstLine = (text) => text.split("\n")[0].slice(0, 80);
|
|
165
|
+
const title = options.kind === "bug" ? `Bug: ${firstLine(message || (error ? redactHome(error.message) : "")) || "describe the problem"}` : `Feedback: ${message ? firstLine(message) : "your idea or experience"}`;
|
|
166
|
+
const sections = [];
|
|
167
|
+
if (options.kind === "bug") {
|
|
168
|
+
sections.push(`### What happened
|
|
169
|
+
|
|
170
|
+
${message || "<!-- What were you doing, and what went wrong? -->"}`);
|
|
171
|
+
sections.push("### What you expected\n\n<!-- What should have happened instead? -->");
|
|
172
|
+
} else {
|
|
173
|
+
sections.push(`### Feedback
|
|
174
|
+
|
|
175
|
+
${message || "<!-- What's working, what isn't, what you wish it did. -->"}`);
|
|
176
|
+
}
|
|
177
|
+
if (error) {
|
|
178
|
+
let errorText = redactHome(error.message);
|
|
179
|
+
if (errorText.length > MAX_ERROR_CHARS) {
|
|
180
|
+
errorText = `${errorText.slice(0, MAX_ERROR_CHARS)}
|
|
181
|
+
\u2026 (truncated)`;
|
|
182
|
+
}
|
|
183
|
+
sections.push(`### Last error
|
|
184
|
+
|
|
185
|
+
\`diffprism ${error.command}\` at ${error.at}
|
|
186
|
+
|
|
187
|
+
\`\`\`
|
|
188
|
+
${errorText}
|
|
189
|
+
\`\`\``);
|
|
190
|
+
}
|
|
191
|
+
sections.push(
|
|
192
|
+
[
|
|
193
|
+
"### Environment",
|
|
194
|
+
"",
|
|
195
|
+
"| | |",
|
|
196
|
+
"|---|---|",
|
|
197
|
+
`| DiffPrism | ${env.version} (${env.build}) |`,
|
|
198
|
+
`| OS | ${env.os} |`,
|
|
199
|
+
`| Node | ${env.node} |`
|
|
200
|
+
].join("\n")
|
|
201
|
+
);
|
|
202
|
+
sections.push("<!-- Everything above is visible before you submit. Remove anything you don't want to share publicly. -->");
|
|
203
|
+
const params = new URLSearchParams({
|
|
204
|
+
title,
|
|
205
|
+
body: sections.join("\n\n"),
|
|
206
|
+
labels: options.kind === "bug" ? "bug" : "feedback"
|
|
207
|
+
});
|
|
208
|
+
return `${ISSUES_NEW_URL}?${params.toString()}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// packages/core/src/threads.ts
|
|
212
|
+
function lastAuthor(annotation) {
|
|
213
|
+
const replies = annotation.replies ?? [];
|
|
214
|
+
return replies.length > 0 ? replies[replies.length - 1].author : annotation.author ?? "agent";
|
|
215
|
+
}
|
|
216
|
+
function awaitingAgent(annotation) {
|
|
217
|
+
return !annotation.dismissed && lastAuthor(annotation) === "reviewer";
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// packages/core/src/server-client.ts
|
|
221
|
+
import { spawn } from "child_process";
|
|
222
|
+
import fs4 from "fs";
|
|
223
|
+
import path4 from "path";
|
|
224
|
+
import os3 from "os";
|
|
225
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
87
226
|
async function ensureServer(options = {}) {
|
|
88
227
|
const existing = await isServerAlive();
|
|
89
228
|
if (existing) {
|
|
90
229
|
return existing;
|
|
91
230
|
}
|
|
92
231
|
const spawnArgs = options.spawnCommand ?? buildDefaultSpawnCommand(options);
|
|
93
|
-
const logDir =
|
|
94
|
-
if (!
|
|
95
|
-
|
|
232
|
+
const logDir = path4.join(os3.homedir(), ".diffprism");
|
|
233
|
+
if (!fs4.existsSync(logDir)) {
|
|
234
|
+
fs4.mkdirSync(logDir, { recursive: true });
|
|
96
235
|
}
|
|
97
|
-
const logPath =
|
|
98
|
-
const logFd =
|
|
236
|
+
const logPath = path4.join(logDir, "server.log");
|
|
237
|
+
const logFd = fs4.openSync(logPath, "a");
|
|
99
238
|
const [cmd, ...args] = spawnArgs;
|
|
100
239
|
const child = spawn(cmd, args, {
|
|
101
240
|
detached: true,
|
|
@@ -103,7 +242,7 @@ async function ensureServer(options = {}) {
|
|
|
103
242
|
env: { ...process.env }
|
|
104
243
|
});
|
|
105
244
|
child.unref();
|
|
106
|
-
|
|
245
|
+
fs4.closeSync(logFd);
|
|
107
246
|
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
108
247
|
const startTime = Date.now();
|
|
109
248
|
while (Date.now() - startTime < timeoutMs) {
|
|
@@ -118,15 +257,15 @@ async function ensureServer(options = {}) {
|
|
|
118
257
|
);
|
|
119
258
|
}
|
|
120
259
|
function buildDefaultSpawnCommand(options) {
|
|
121
|
-
const thisFile =
|
|
122
|
-
const thisDir =
|
|
123
|
-
const workspaceRoot =
|
|
124
|
-
const devBin =
|
|
125
|
-
if (
|
|
260
|
+
const thisFile = fileURLToPath2(import.meta.url);
|
|
261
|
+
const thisDir = path4.dirname(thisFile);
|
|
262
|
+
const workspaceRoot = path4.resolve(thisDir, "..", "..", "..");
|
|
263
|
+
const devBin = path4.join(workspaceRoot, "cli", "bin", "diffprism.mjs");
|
|
264
|
+
if (fs4.existsSync(devBin)) {
|
|
126
265
|
return withDevFlag([process.execPath, devBin, "server", "--_daemon"], options);
|
|
127
266
|
}
|
|
128
267
|
let searchDir = thisDir;
|
|
129
|
-
while (searchDir !==
|
|
268
|
+
while (searchDir !== path4.dirname(searchDir)) {
|
|
130
269
|
const ownBin = readOwnBinPath(searchDir);
|
|
131
270
|
if (ownBin) {
|
|
132
271
|
return withDevFlag(
|
|
@@ -134,11 +273,11 @@ function buildDefaultSpawnCommand(options) {
|
|
|
134
273
|
options
|
|
135
274
|
);
|
|
136
275
|
}
|
|
137
|
-
const shim =
|
|
138
|
-
if (
|
|
276
|
+
const shim = path4.join(searchDir, "node_modules", ".bin", "diffprism");
|
|
277
|
+
if (fs4.existsSync(shim)) {
|
|
139
278
|
return withDevFlag([shim, "server", "--_daemon"], options);
|
|
140
279
|
}
|
|
141
|
-
searchDir =
|
|
280
|
+
searchDir = path4.dirname(searchDir);
|
|
142
281
|
}
|
|
143
282
|
return withDevFlag(["diffprism", "server", "--_daemon"], options);
|
|
144
283
|
}
|
|
@@ -146,22 +285,71 @@ function withDevFlag(args, options) {
|
|
|
146
285
|
return options.dev ? [...args, "--dev"] : args;
|
|
147
286
|
}
|
|
148
287
|
function readOwnBinPath(dir) {
|
|
149
|
-
const manifestPath =
|
|
150
|
-
if (!
|
|
288
|
+
const manifestPath = path4.join(dir, "package.json");
|
|
289
|
+
if (!fs4.existsSync(manifestPath)) {
|
|
151
290
|
return null;
|
|
152
291
|
}
|
|
153
292
|
try {
|
|
154
|
-
const manifest = JSON.parse(
|
|
293
|
+
const manifest = JSON.parse(fs4.readFileSync(manifestPath, "utf8"));
|
|
155
294
|
const entry = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.diffprism;
|
|
156
295
|
if (!entry) {
|
|
157
296
|
return null;
|
|
158
297
|
}
|
|
159
|
-
const resolved =
|
|
160
|
-
return
|
|
298
|
+
const resolved = path4.resolve(dir, entry);
|
|
299
|
+
return fs4.existsSync(resolved) ? resolved : null;
|
|
161
300
|
} catch {
|
|
162
301
|
return null;
|
|
163
302
|
}
|
|
164
303
|
}
|
|
304
|
+
var ReviewTimeoutError = class extends Error {
|
|
305
|
+
sessionId;
|
|
306
|
+
waitedMs;
|
|
307
|
+
constructor(sessionId, waitedMs) {
|
|
308
|
+
super(
|
|
309
|
+
`Review ${sessionId} is still open after ${Math.round(waitedMs / 1e3)}s without a decision.`
|
|
310
|
+
);
|
|
311
|
+
this.name = "ReviewTimeoutError";
|
|
312
|
+
this.sessionId = sessionId;
|
|
313
|
+
this.waitedMs = waitedMs;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
var ReviewerAskedError = class extends Error {
|
|
317
|
+
sessionId;
|
|
318
|
+
threads;
|
|
319
|
+
constructor(sessionId, threads) {
|
|
320
|
+
super(
|
|
321
|
+
`The reviewer asked ${threads.length} question${threads.length === 1 ? "" : "s"} on review ${sessionId} before deciding.`
|
|
322
|
+
);
|
|
323
|
+
this.name = "ReviewerAskedError";
|
|
324
|
+
this.sessionId = sessionId;
|
|
325
|
+
this.threads = threads;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
async function waitForDecision(serverInfo, sessionId, maxWaitMs, pollIntervalMs = 2e3) {
|
|
329
|
+
const base = `http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}`;
|
|
330
|
+
const start = Date.now();
|
|
331
|
+
while (Date.now() - start < maxWaitMs) {
|
|
332
|
+
const resultResponse = await fetch(`${base}/result`);
|
|
333
|
+
if (!resultResponse.ok) {
|
|
334
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
335
|
+
}
|
|
336
|
+
const { result } = await resultResponse.json();
|
|
337
|
+
if (result) {
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
const annotationsResponse = await fetch(`${base}/annotations`);
|
|
341
|
+
if (!annotationsResponse.ok) {
|
|
342
|
+
throw new Error(`Could not read threads for ${sessionId}: server returned ${annotationsResponse.status}`);
|
|
343
|
+
}
|
|
344
|
+
const { annotations } = await annotationsResponse.json();
|
|
345
|
+
const asked = annotations.filter(awaitingAgent);
|
|
346
|
+
if (asked.length > 0) {
|
|
347
|
+
throw new ReviewerAskedError(sessionId, asked);
|
|
348
|
+
}
|
|
349
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
350
|
+
}
|
|
351
|
+
throw new ReviewTimeoutError(sessionId, maxWaitMs);
|
|
352
|
+
}
|
|
165
353
|
async function submitReviewToServer(serverInfo, diffRef, options = {}) {
|
|
166
354
|
const cwd = options.cwd ?? process.cwd();
|
|
167
355
|
const projectPath = options.projectPath ?? cwd;
|
|
@@ -169,7 +357,7 @@ async function submitReviewToServer(serverInfo, diffRef, options = {}) {
|
|
|
169
357
|
if (options.injectedPayload) {
|
|
170
358
|
payload = options.injectedPayload;
|
|
171
359
|
} else {
|
|
172
|
-
const { getDiff: getDiff2, getCurrentBranch: getCurrentBranch2, detectWorktree } = await import("./src-
|
|
360
|
+
const { getDiff: getDiff2, getCurrentBranch: getCurrentBranch2, detectWorktree } = await import("./src-OZQ32NDN.js");
|
|
173
361
|
const { analyze: analyze2 } = await import("./src-JMPTSU3P.js");
|
|
174
362
|
const { diffSet, rawDiff } = getDiff2(diffRef, { cwd });
|
|
175
363
|
if (diffSet.files.length === 0) {
|
|
@@ -249,49 +437,7 @@ async function submitReviewToServer(serverInfo, diffRef, options = {}) {
|
|
|
249
437
|
if (maxWaitMs <= 0) {
|
|
250
438
|
return { result: null, sessionId };
|
|
251
439
|
}
|
|
252
|
-
|
|
253
|
-
const start = Date.now();
|
|
254
|
-
while (Date.now() - start < maxWaitMs) {
|
|
255
|
-
const resultResponse = await fetch(
|
|
256
|
-
`http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/result`
|
|
257
|
-
);
|
|
258
|
-
if (resultResponse.ok) {
|
|
259
|
-
const data = await resultResponse.json();
|
|
260
|
-
if (data.result) {
|
|
261
|
-
return { result: data.result, sessionId };
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
265
|
-
}
|
|
266
|
-
throw new Error("Review timed out waiting for submission.");
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// packages/core/src/build-info.ts
|
|
270
|
-
import fs3 from "fs";
|
|
271
|
-
import path3 from "path";
|
|
272
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
273
|
-
function getBuildInfo() {
|
|
274
|
-
let dir = path3.dirname(fileURLToPath2(import.meta.url));
|
|
275
|
-
while (dir !== path3.dirname(dir)) {
|
|
276
|
-
const manifest = path3.join(dir, "package.json");
|
|
277
|
-
if (fs3.existsSync(manifest) && isOwnManifest(manifest)) {
|
|
278
|
-
return fs3.existsSync(path3.join(dir, ".git")) ? { dev: true, root: dir } : { dev: false, root: null };
|
|
279
|
-
}
|
|
280
|
-
dir = path3.dirname(dir);
|
|
281
|
-
}
|
|
282
|
-
return { dev: false, root: null };
|
|
283
|
-
}
|
|
284
|
-
function isOwnManifest(manifestPath) {
|
|
285
|
-
try {
|
|
286
|
-
const manifest = JSON.parse(fs3.readFileSync(manifestPath, "utf8"));
|
|
287
|
-
return manifest.name === "diffprism";
|
|
288
|
-
} catch {
|
|
289
|
-
return false;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
function describeVersion(version) {
|
|
293
|
-
const info = getBuildInfo();
|
|
294
|
-
return info.dev ? `${version} (dev build \u2014 ${info.root})` : version;
|
|
440
|
+
return { result: await waitForDecision(serverInfo, sessionId, maxWaitMs), sessionId };
|
|
295
441
|
}
|
|
296
442
|
|
|
297
443
|
// packages/core/src/diff-utils.ts
|
|
@@ -334,8 +480,20 @@ function createDiffPoller(options) {
|
|
|
334
480
|
let lastDiffHash = null;
|
|
335
481
|
let lastDiffSet = null;
|
|
336
482
|
let refreshRequested = false;
|
|
337
|
-
let
|
|
483
|
+
let timer = null;
|
|
338
484
|
let running = false;
|
|
485
|
+
let quietPolls = 0;
|
|
486
|
+
function nextDelay() {
|
|
487
|
+
return typeof pollInterval === "number" ? pollInterval : pollInterval({ quietPolls });
|
|
488
|
+
}
|
|
489
|
+
function schedule() {
|
|
490
|
+
if (!running) return;
|
|
491
|
+
timer = setTimeout(() => {
|
|
492
|
+
timer = null;
|
|
493
|
+
poll();
|
|
494
|
+
schedule();
|
|
495
|
+
}, nextDelay());
|
|
496
|
+
}
|
|
339
497
|
function poll() {
|
|
340
498
|
if (!running) return;
|
|
341
499
|
try {
|
|
@@ -343,6 +501,7 @@ function createDiffPoller(options) {
|
|
|
343
501
|
const newHash = hashDiff(newRawDiff);
|
|
344
502
|
if (newHash !== lastDiffHash || refreshRequested) {
|
|
345
503
|
refreshRequested = false;
|
|
504
|
+
quietPolls = 0;
|
|
346
505
|
const newBriefing = analyze(newDiffSet);
|
|
347
506
|
const changedFiles = detectChangedFiles(lastDiffSet, newDiffSet);
|
|
348
507
|
lastDiffHash = newHash;
|
|
@@ -355,8 +514,11 @@ function createDiffPoller(options) {
|
|
|
355
514
|
timestamp: Date.now()
|
|
356
515
|
};
|
|
357
516
|
onDiffChanged(updatePayload);
|
|
517
|
+
} else {
|
|
518
|
+
quietPolls++;
|
|
358
519
|
}
|
|
359
520
|
} catch (err) {
|
|
521
|
+
quietPolls++;
|
|
360
522
|
if (onError && err instanceof Error) {
|
|
361
523
|
onError(err);
|
|
362
524
|
}
|
|
@@ -372,13 +534,13 @@ function createDiffPoller(options) {
|
|
|
372
534
|
lastDiffSet = initialDiffSet;
|
|
373
535
|
} catch {
|
|
374
536
|
}
|
|
375
|
-
|
|
537
|
+
schedule();
|
|
376
538
|
},
|
|
377
539
|
stop() {
|
|
378
540
|
running = false;
|
|
379
|
-
if (
|
|
380
|
-
|
|
381
|
-
|
|
541
|
+
if (timer) {
|
|
542
|
+
clearTimeout(timer);
|
|
543
|
+
timer = null;
|
|
382
544
|
}
|
|
383
545
|
},
|
|
384
546
|
setDiffRef(newRef) {
|
|
@@ -388,6 +550,15 @@ function createDiffPoller(options) {
|
|
|
388
550
|
},
|
|
389
551
|
refresh() {
|
|
390
552
|
refreshRequested = true;
|
|
553
|
+
},
|
|
554
|
+
wake() {
|
|
555
|
+
if (!running) return;
|
|
556
|
+
if (timer) {
|
|
557
|
+
clearTimeout(timer);
|
|
558
|
+
timer = null;
|
|
559
|
+
}
|
|
560
|
+
poll();
|
|
561
|
+
schedule();
|
|
391
562
|
}
|
|
392
563
|
};
|
|
393
564
|
}
|
|
@@ -398,12 +569,13 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
398
569
|
import getPort from "get-port";
|
|
399
570
|
import open from "open";
|
|
400
571
|
import { WebSocketServer, WebSocket } from "ws";
|
|
401
|
-
import
|
|
572
|
+
import fs7 from "fs";
|
|
573
|
+
import path7 from "path";
|
|
402
574
|
|
|
403
575
|
// packages/core/src/ui-server.ts
|
|
404
576
|
import http from "http";
|
|
405
|
-
import
|
|
406
|
-
import
|
|
577
|
+
import fs5 from "fs";
|
|
578
|
+
import path5 from "path";
|
|
407
579
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
408
580
|
var MIME_TYPES = {
|
|
409
581
|
".html": "text/html",
|
|
@@ -418,14 +590,14 @@ var MIME_TYPES = {
|
|
|
418
590
|
};
|
|
419
591
|
function resolveUiDist() {
|
|
420
592
|
const thisFile = fileURLToPath3(import.meta.url);
|
|
421
|
-
const thisDir =
|
|
422
|
-
const publishedUiDist =
|
|
423
|
-
if (
|
|
593
|
+
const thisDir = path5.dirname(thisFile);
|
|
594
|
+
const publishedUiDist = path5.resolve(thisDir, "..", "ui-dist");
|
|
595
|
+
if (fs5.existsSync(path5.join(publishedUiDist, "index.html"))) {
|
|
424
596
|
return publishedUiDist;
|
|
425
597
|
}
|
|
426
|
-
const workspaceRoot =
|
|
427
|
-
const devUiDist =
|
|
428
|
-
if (
|
|
598
|
+
const workspaceRoot = path5.resolve(thisDir, "..", "..", "..");
|
|
599
|
+
const devUiDist = path5.join(workspaceRoot, "packages", "ui", "dist");
|
|
600
|
+
if (fs5.existsSync(path5.join(devUiDist, "index.html"))) {
|
|
429
601
|
return devUiDist;
|
|
430
602
|
}
|
|
431
603
|
throw new Error(
|
|
@@ -434,10 +606,10 @@ function resolveUiDist() {
|
|
|
434
606
|
}
|
|
435
607
|
function resolveUiRoot() {
|
|
436
608
|
const thisFile = fileURLToPath3(import.meta.url);
|
|
437
|
-
const thisDir =
|
|
438
|
-
const workspaceRoot =
|
|
439
|
-
const uiRoot =
|
|
440
|
-
if (
|
|
609
|
+
const thisDir = path5.dirname(thisFile);
|
|
610
|
+
const workspaceRoot = path5.resolve(thisDir, "..", "..", "..");
|
|
611
|
+
const uiRoot = path5.join(workspaceRoot, "packages", "ui");
|
|
612
|
+
if (fs5.existsSync(path5.join(uiRoot, "index.html"))) {
|
|
441
613
|
return uiRoot;
|
|
442
614
|
}
|
|
443
615
|
throw new Error(
|
|
@@ -457,14 +629,14 @@ async function startViteDevServer(uiRoot, port, silent) {
|
|
|
457
629
|
function createStaticServer(distPath, port) {
|
|
458
630
|
const server = http.createServer((req, res) => {
|
|
459
631
|
const urlPath = req.url?.split("?")[0] ?? "/";
|
|
460
|
-
let filePath =
|
|
461
|
-
if (!
|
|
462
|
-
filePath =
|
|
632
|
+
let filePath = path5.join(distPath, urlPath === "/" ? "index.html" : urlPath);
|
|
633
|
+
if (!fs5.existsSync(filePath)) {
|
|
634
|
+
filePath = path5.join(distPath, "index.html");
|
|
463
635
|
}
|
|
464
|
-
const ext =
|
|
636
|
+
const ext = path5.extname(filePath);
|
|
465
637
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
466
638
|
try {
|
|
467
|
-
const content =
|
|
639
|
+
const content = fs5.readFileSync(filePath);
|
|
468
640
|
res.writeHead(200, { "Content-Type": contentType });
|
|
469
641
|
res.end(content);
|
|
470
642
|
} catch {
|
|
@@ -478,23 +650,70 @@ function createStaticServer(distPath, port) {
|
|
|
478
650
|
});
|
|
479
651
|
}
|
|
480
652
|
|
|
653
|
+
// packages/core/src/pr-review.ts
|
|
654
|
+
var EVENTS = ["APPROVE", "REQUEST_CHANGES", "COMMENT"];
|
|
655
|
+
var PR_EVENT_DECISION = {
|
|
656
|
+
APPROVE: "approved",
|
|
657
|
+
REQUEST_CHANGES: "changes_requested",
|
|
658
|
+
// A GitHub "Comment" review neither approves nor blocks; this is the
|
|
659
|
+
// closest DiffPrism decision.
|
|
660
|
+
COMMENT: "approved_with_comments"
|
|
661
|
+
};
|
|
662
|
+
function buildGitHubReview(submission, annotations) {
|
|
663
|
+
if (!EVENTS.includes(submission.event)) {
|
|
664
|
+
return { error: `Unknown review event: ${String(submission.event)}` };
|
|
665
|
+
}
|
|
666
|
+
const body = submission.summary?.trim() ?? "";
|
|
667
|
+
if (!body && submission.event !== "APPROVE") {
|
|
668
|
+
return { error: "GitHub needs a summary to request changes or comment" };
|
|
669
|
+
}
|
|
670
|
+
const comments = [];
|
|
671
|
+
for (const id of submission.threadIds ?? []) {
|
|
672
|
+
const thread = annotations.find((a) => a.id === id);
|
|
673
|
+
if (!thread) return { error: `Thread not found: ${id}` };
|
|
674
|
+
if (thread.author !== "reviewer") return { error: `Only your own threads can be posted: ${id}` };
|
|
675
|
+
if (thread.dismissed) return { error: `Thread was dismissed: ${id}` };
|
|
676
|
+
comments.push({
|
|
677
|
+
path: thread.file,
|
|
678
|
+
line: thread.line,
|
|
679
|
+
side: thread.side === "old" ? "LEFT" : "RIGHT",
|
|
680
|
+
body: thread.body
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
return { review: { event: submission.event, body, comments } };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// packages/core/src/watch-schedule.ts
|
|
687
|
+
var DEFAULT_WATCH_SCHEDULE = {
|
|
688
|
+
viewedMs: 2e3,
|
|
689
|
+
unviewedMs: 3e4,
|
|
690
|
+
unviewedMaxMs: 5 * 6e4
|
|
691
|
+
};
|
|
692
|
+
function watcherPollDelay(state, options = DEFAULT_WATCH_SCHEDULE) {
|
|
693
|
+
if (state.viewed) {
|
|
694
|
+
return options.viewedMs;
|
|
695
|
+
}
|
|
696
|
+
const backedOff = options.unviewedMs * 2 ** Math.min(state.quietPolls, 20);
|
|
697
|
+
return Math.min(backedOff, options.unviewedMaxMs);
|
|
698
|
+
}
|
|
699
|
+
|
|
481
700
|
// packages/core/src/review-history.ts
|
|
482
|
-
import
|
|
483
|
-
import
|
|
701
|
+
import fs6 from "fs";
|
|
702
|
+
import path6 from "path";
|
|
484
703
|
import { randomUUID } from "crypto";
|
|
485
704
|
function generateEntryId() {
|
|
486
705
|
return randomUUID();
|
|
487
706
|
}
|
|
488
707
|
function getHistoryPath(projectDir) {
|
|
489
|
-
return
|
|
708
|
+
return path6.join(projectDir, ".diffprism", "history", "reviews.json");
|
|
490
709
|
}
|
|
491
710
|
function readHistory(projectDir) {
|
|
492
711
|
const filePath = getHistoryPath(projectDir);
|
|
493
|
-
if (!
|
|
712
|
+
if (!fs6.existsSync(filePath)) {
|
|
494
713
|
return { version: 1, entries: [] };
|
|
495
714
|
}
|
|
496
715
|
try {
|
|
497
|
-
const raw =
|
|
716
|
+
const raw = fs6.readFileSync(filePath, "utf-8");
|
|
498
717
|
const parsed = JSON.parse(raw);
|
|
499
718
|
return parsed;
|
|
500
719
|
} catch {
|
|
@@ -503,14 +722,14 @@ function readHistory(projectDir) {
|
|
|
503
722
|
}
|
|
504
723
|
function appendHistory(projectDir, entry) {
|
|
505
724
|
const filePath = getHistoryPath(projectDir);
|
|
506
|
-
const dir =
|
|
507
|
-
if (!
|
|
508
|
-
|
|
725
|
+
const dir = path6.dirname(filePath);
|
|
726
|
+
if (!fs6.existsSync(dir)) {
|
|
727
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
509
728
|
}
|
|
510
729
|
const history = readHistory(projectDir);
|
|
511
730
|
history.entries.push(entry);
|
|
512
731
|
history.entries.sort((a, b) => a.timestamp - b.timestamp);
|
|
513
|
-
|
|
732
|
+
fs6.writeFileSync(filePath, JSON.stringify(history, null, 2) + "\n");
|
|
514
733
|
}
|
|
515
734
|
function getRecentHistory(projectDir, limit = 50) {
|
|
516
735
|
const history = readHistory(projectDir);
|
|
@@ -521,10 +740,126 @@ function getRecentHistory(projectDir, limit = 50) {
|
|
|
521
740
|
var SUBMITTED_TTL_MS = 5 * 60 * 1e3;
|
|
522
741
|
var ABANDONED_TTL_MS = 60 * 60 * 1e3;
|
|
523
742
|
var CLEANUP_INTERVAL_MS = 60 * 1e3;
|
|
743
|
+
var IDLE_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
744
|
+
function touch(session) {
|
|
745
|
+
session.lastActivityAt = Date.now();
|
|
746
|
+
}
|
|
524
747
|
var sessions = /* @__PURE__ */ new Map();
|
|
748
|
+
function listedSummaries() {
|
|
749
|
+
return Array.from(sessions.values()).filter((session) => session.closedAt === void 0).map(toSummary);
|
|
750
|
+
}
|
|
751
|
+
function localIdentity(projectPath) {
|
|
752
|
+
const repoRoot = getRepoRoot({ cwd: projectPath }) ?? path7.resolve(projectPath);
|
|
753
|
+
return { key: `repo:${repoRoot}`, repoRoot };
|
|
754
|
+
}
|
|
755
|
+
function findSessionByKey(key) {
|
|
756
|
+
for (const session of sessions.values()) {
|
|
757
|
+
if (session.key === key) return session;
|
|
758
|
+
}
|
|
759
|
+
return void 0;
|
|
760
|
+
}
|
|
761
|
+
function openSession(request) {
|
|
762
|
+
const { payload, diffRef } = request;
|
|
763
|
+
if (diffRef) {
|
|
764
|
+
payload.watchMode = true;
|
|
765
|
+
}
|
|
766
|
+
const existing = findSessionByKey(request.key);
|
|
767
|
+
if (!existing) {
|
|
768
|
+
const id2 = `session-${randomUUID2().slice(0, 8)}`;
|
|
769
|
+
payload.reviewId = id2;
|
|
770
|
+
const session = {
|
|
771
|
+
id: id2,
|
|
772
|
+
key: request.key,
|
|
773
|
+
repoRoot: request.repoRoot,
|
|
774
|
+
payload,
|
|
775
|
+
projectPath: request.projectPath,
|
|
776
|
+
source: request.source,
|
|
777
|
+
status: "pending",
|
|
778
|
+
createdAt: Date.now(),
|
|
779
|
+
result: null,
|
|
780
|
+
diffRef,
|
|
781
|
+
openedDiffRef: diffRef,
|
|
782
|
+
lastDiffHash: diffRef ? hashDiff(payload.rawDiff) : void 0,
|
|
783
|
+
lastDiffSet: diffRef ? payload.diffSet : void 0,
|
|
784
|
+
hasNewChanges: false,
|
|
785
|
+
annotations: [],
|
|
786
|
+
attentionClearedAt: 0,
|
|
787
|
+
lastActivityAt: Date.now()
|
|
788
|
+
};
|
|
789
|
+
sessions.set(id2, session);
|
|
790
|
+
if (diffRef) {
|
|
791
|
+
startSessionWatcher(id2);
|
|
792
|
+
}
|
|
793
|
+
broadcastToAll({ type: "session:added", payload: toSummary(session) });
|
|
794
|
+
return { session, reused: false };
|
|
795
|
+
}
|
|
796
|
+
const id = existing.id;
|
|
797
|
+
payload.reviewId = id;
|
|
798
|
+
stopSessionWatcher(id);
|
|
799
|
+
const wasClosed = existing.closedAt !== void 0;
|
|
800
|
+
existing.closedAt = void 0;
|
|
801
|
+
const incomingHash = hashDiff(payload.rawDiff);
|
|
802
|
+
const contentChanged = hashDiff(existing.payload.rawDiff) !== incomingHash;
|
|
803
|
+
const stillAnswered = existing.result !== null && existing.result.decision !== "dismissed" && existing.verdictDiffHash === incomingHash;
|
|
804
|
+
if (existing.result !== null && !stillAnswered) {
|
|
805
|
+
existing.result = null;
|
|
806
|
+
existing.status = "pending";
|
|
807
|
+
existing.verdictDiffHash = void 0;
|
|
808
|
+
}
|
|
809
|
+
existing.payload = payload;
|
|
810
|
+
existing.projectPath = request.projectPath;
|
|
811
|
+
existing.repoRoot = request.repoRoot;
|
|
812
|
+
existing.diffRef = diffRef;
|
|
813
|
+
existing.openedDiffRef = diffRef;
|
|
814
|
+
existing.lastDiffHash = diffRef ? incomingHash : void 0;
|
|
815
|
+
existing.lastDiffSet = diffRef ? payload.diffSet : void 0;
|
|
816
|
+
existing.createdAt = Date.now();
|
|
817
|
+
touch(existing);
|
|
818
|
+
if (diffRef) {
|
|
819
|
+
startSessionWatcher(id);
|
|
820
|
+
}
|
|
821
|
+
if (hasViewersForSession(id)) {
|
|
822
|
+
if (existing.status === "pending") {
|
|
823
|
+
existing.status = "in_review";
|
|
824
|
+
}
|
|
825
|
+
existing.hasNewChanges = false;
|
|
826
|
+
sendToSessionClients(id, { type: "review:init", payload });
|
|
827
|
+
for (const annotation of existing.annotations) {
|
|
828
|
+
sendToSessionClients(id, { type: "annotation:added", payload: annotation });
|
|
829
|
+
}
|
|
830
|
+
} else if (contentChanged || wasClosed) {
|
|
831
|
+
existing.hasNewChanges = true;
|
|
832
|
+
}
|
|
833
|
+
if (wasClosed) {
|
|
834
|
+
broadcastToAll({ type: "session:added", payload: toSummary(existing) });
|
|
835
|
+
} else {
|
|
836
|
+
broadcastSessionUpdate(existing);
|
|
837
|
+
}
|
|
838
|
+
return { session: existing, reused: true };
|
|
839
|
+
}
|
|
840
|
+
function attachViewer(ws, session) {
|
|
841
|
+
clientSessions.set(ws, session.id);
|
|
842
|
+
session.status = "in_review";
|
|
843
|
+
session.hasNewChanges = false;
|
|
844
|
+
session.attentionClearedAt = Date.now();
|
|
845
|
+
touch(session);
|
|
846
|
+
const watcher = sessionWatchers.get(session.id);
|
|
847
|
+
startSessionWatcher(session.id);
|
|
848
|
+
broadcastSessionUpdate(session);
|
|
849
|
+
ws.send(JSON.stringify({ type: "review:init", payload: session.payload }));
|
|
850
|
+
for (const annotation of session.annotations) {
|
|
851
|
+
ws.send(JSON.stringify({ type: "annotation:added", payload: annotation }));
|
|
852
|
+
}
|
|
853
|
+
watcher?.wake();
|
|
854
|
+
}
|
|
855
|
+
function needsAttention(session) {
|
|
856
|
+
return session.annotations.some(
|
|
857
|
+
(a) => a.type === "warning" && !a.dismissed && a.createdAt > session.attentionClearedAt
|
|
858
|
+
);
|
|
859
|
+
}
|
|
525
860
|
var clientSessions = /* @__PURE__ */ new Map();
|
|
526
861
|
var sessionWatchers = /* @__PURE__ */ new Map();
|
|
527
|
-
var
|
|
862
|
+
var watchSchedule = DEFAULT_WATCH_SCHEDULE;
|
|
528
863
|
var reopenBrowserIfNeeded = null;
|
|
529
864
|
var serverUiUrl = null;
|
|
530
865
|
function toSummary(session) {
|
|
@@ -549,6 +884,8 @@ function toSummary(session) {
|
|
|
549
884
|
decision: session.result?.decision,
|
|
550
885
|
createdAt: session.createdAt,
|
|
551
886
|
hasNewChanges: session.hasNewChanges,
|
|
887
|
+
needsAttention: needsAttention(session),
|
|
888
|
+
diffRef: session.diffRef,
|
|
552
889
|
source: session.source
|
|
553
890
|
};
|
|
554
891
|
}
|
|
@@ -632,10 +969,13 @@ function startSessionWatcher(sessionId) {
|
|
|
632
969
|
const poller = createDiffPoller({
|
|
633
970
|
diffRef: session.diffRef,
|
|
634
971
|
cwd: session.projectPath,
|
|
635
|
-
|
|
972
|
+
// Asked before every poll, so a session speeds up the moment someone views
|
|
973
|
+
// it and backs off while nobody is — see watch-schedule.ts.
|
|
974
|
+
pollInterval: ({ quietPolls }) => watcherPollDelay({ viewed: hasViewersForSession(sessionId), quietPolls }, watchSchedule),
|
|
636
975
|
onDiffChanged: (updatePayload) => {
|
|
637
976
|
const s = sessions.get(sessionId);
|
|
638
977
|
if (!s) return;
|
|
978
|
+
touch(s);
|
|
639
979
|
s.payload = {
|
|
640
980
|
...s.payload,
|
|
641
981
|
diffSet: updatePayload.diffSet,
|
|
@@ -687,9 +1027,16 @@ function hasConnectedClients() {
|
|
|
687
1027
|
return false;
|
|
688
1028
|
}
|
|
689
1029
|
function broadcastSessionList() {
|
|
690
|
-
const summaries =
|
|
1030
|
+
const summaries = listedSummaries();
|
|
691
1031
|
broadcastToAll({ type: "session:list", payload: summaries });
|
|
692
1032
|
}
|
|
1033
|
+
function recordVerdict(session, result) {
|
|
1034
|
+
session.result = result;
|
|
1035
|
+
session.status = "submitted";
|
|
1036
|
+
session.verdictDiffHash = hashDiff(session.payload.rawDiff);
|
|
1037
|
+
touch(session);
|
|
1038
|
+
recordReviewHistory(session, result);
|
|
1039
|
+
}
|
|
693
1040
|
function recordReviewHistory(session, result) {
|
|
694
1041
|
if (session.projectPath.startsWith("github:")) return;
|
|
695
1042
|
try {
|
|
@@ -714,12 +1061,12 @@ function recordReviewHistory(session, result) {
|
|
|
714
1061
|
function isInsideGitRepo(dirPath) {
|
|
715
1062
|
let current = dirPath;
|
|
716
1063
|
while (current !== "/") {
|
|
717
|
-
if (
|
|
1064
|
+
if (fs7.existsSync(`${current}/.git`)) return true;
|
|
718
1065
|
const parent = current.replace(/\/[^/]+$/, "") || "/";
|
|
719
1066
|
if (parent === current) break;
|
|
720
1067
|
current = parent;
|
|
721
1068
|
}
|
|
722
|
-
return
|
|
1069
|
+
return fs7.existsSync(`${current}/.git`);
|
|
723
1070
|
}
|
|
724
1071
|
async function handleApiRequest(req, res) {
|
|
725
1072
|
const method = req.method ?? "GET";
|
|
@@ -742,7 +1089,18 @@ async function handleApiRequest(req, res) {
|
|
|
742
1089
|
sessions: sessions.size,
|
|
743
1090
|
uptime: process.uptime(),
|
|
744
1091
|
uiUrl: serverUiUrl,
|
|
745
|
-
cwd: process.cwd()
|
|
1092
|
+
cwd: process.cwd(),
|
|
1093
|
+
// The dashboard reads its default scope from here rather than keeping
|
|
1094
|
+
// its own copy — Vite can't import @diffprism/core, and a copy drifts.
|
|
1095
|
+
defaultDiffRef: DEFAULT_DIFF_REF
|
|
1096
|
+
});
|
|
1097
|
+
return true;
|
|
1098
|
+
}
|
|
1099
|
+
if (method === "GET" && url === "/api/feedback") {
|
|
1100
|
+
const parsed = new URL(req.url ?? "/", "http://localhost");
|
|
1101
|
+
const kind = parsed.searchParams.get("kind") === "bug" ? "bug" : "feedback";
|
|
1102
|
+
jsonResponse(res, 200, {
|
|
1103
|
+
url: buildFeedbackUrl({ kind, error: kind === "bug" ? readLastError() : null })
|
|
746
1104
|
});
|
|
747
1105
|
return true;
|
|
748
1106
|
}
|
|
@@ -750,72 +1108,17 @@ async function handleApiRequest(req, res) {
|
|
|
750
1108
|
try {
|
|
751
1109
|
const body = await readBody(req);
|
|
752
1110
|
const { payload, projectPath, diffRef } = JSON.parse(body);
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
payload.watchMode = true;
|
|
765
|
-
}
|
|
766
|
-
stopSessionWatcher(sessionId);
|
|
767
|
-
existingSession.payload = payload;
|
|
768
|
-
existingSession.status = "pending";
|
|
769
|
-
existingSession.result = null;
|
|
770
|
-
existingSession.createdAt = Date.now();
|
|
771
|
-
existingSession.diffRef = diffRef;
|
|
772
|
-
existingSession.lastDiffHash = diffRef ? hashDiff(payload.rawDiff) : void 0;
|
|
773
|
-
existingSession.lastDiffSet = diffRef ? payload.diffSet : void 0;
|
|
774
|
-
existingSession.hasNewChanges = false;
|
|
775
|
-
existingSession.annotations = [];
|
|
776
|
-
if (diffRef) {
|
|
777
|
-
startSessionWatcher(sessionId);
|
|
778
|
-
}
|
|
779
|
-
if (hasViewersForSession(sessionId)) {
|
|
780
|
-
sendToSessionClients(sessionId, {
|
|
781
|
-
type: "review:init",
|
|
782
|
-
payload
|
|
783
|
-
});
|
|
784
|
-
}
|
|
785
|
-
broadcastSessionUpdate(existingSession);
|
|
786
|
-
reopenBrowserIfNeeded?.();
|
|
787
|
-
jsonResponse(res, 200, { sessionId });
|
|
788
|
-
} else {
|
|
789
|
-
const sessionId = `session-${randomUUID2().slice(0, 8)}`;
|
|
790
|
-
payload.reviewId = sessionId;
|
|
791
|
-
if (diffRef) {
|
|
792
|
-
payload.watchMode = true;
|
|
793
|
-
}
|
|
794
|
-
const session = {
|
|
795
|
-
id: sessionId,
|
|
796
|
-
payload,
|
|
797
|
-
projectPath,
|
|
798
|
-
source: "agent",
|
|
799
|
-
status: "pending",
|
|
800
|
-
createdAt: Date.now(),
|
|
801
|
-
result: null,
|
|
802
|
-
diffRef,
|
|
803
|
-
lastDiffHash: diffRef ? hashDiff(payload.rawDiff) : void 0,
|
|
804
|
-
lastDiffSet: diffRef ? payload.diffSet : void 0,
|
|
805
|
-
hasNewChanges: false,
|
|
806
|
-
annotations: []
|
|
807
|
-
};
|
|
808
|
-
sessions.set(sessionId, session);
|
|
809
|
-
if (diffRef) {
|
|
810
|
-
startSessionWatcher(sessionId);
|
|
811
|
-
}
|
|
812
|
-
broadcastToAll({
|
|
813
|
-
type: "session:added",
|
|
814
|
-
payload: toSummary(session)
|
|
815
|
-
});
|
|
816
|
-
reopenBrowserIfNeeded?.();
|
|
817
|
-
jsonResponse(res, 201, { sessionId });
|
|
818
|
-
}
|
|
1111
|
+
const identity = localIdentity(projectPath);
|
|
1112
|
+
const { session, reused } = openSession({
|
|
1113
|
+
key: identity.key,
|
|
1114
|
+
repoRoot: identity.repoRoot,
|
|
1115
|
+
projectPath: identity.repoRoot,
|
|
1116
|
+
payload,
|
|
1117
|
+
diffRef,
|
|
1118
|
+
source: "agent"
|
|
1119
|
+
});
|
|
1120
|
+
reopenBrowserIfNeeded?.();
|
|
1121
|
+
jsonResponse(res, reused ? 200 : 201, { sessionId: session.id });
|
|
819
1122
|
} catch {
|
|
820
1123
|
jsonResponse(res, 400, { error: "Invalid request body" });
|
|
821
1124
|
}
|
|
@@ -824,13 +1127,13 @@ async function handleApiRequest(req, res) {
|
|
|
824
1127
|
if (method === "POST" && url === "/api/projects/open") {
|
|
825
1128
|
try {
|
|
826
1129
|
const body = await readBody(req);
|
|
827
|
-
const { projectPath, diffRef =
|
|
1130
|
+
const { projectPath, diffRef = DEFAULT_DIFF_REF } = JSON.parse(body);
|
|
828
1131
|
if (!projectPath) {
|
|
829
1132
|
jsonResponse(res, 400, { error: "Missing projectPath" });
|
|
830
1133
|
return true;
|
|
831
1134
|
}
|
|
832
1135
|
try {
|
|
833
|
-
const stat =
|
|
1136
|
+
const stat = fs7.statSync(projectPath);
|
|
834
1137
|
if (!stat.isDirectory()) {
|
|
835
1138
|
jsonResponse(res, 400, { error: "Path is not a directory" });
|
|
836
1139
|
return true;
|
|
@@ -855,40 +1158,30 @@ async function handleApiRequest(req, res) {
|
|
|
855
1158
|
currentBranch = getCurrentBranch({ cwd: projectPath });
|
|
856
1159
|
} catch {
|
|
857
1160
|
}
|
|
858
|
-
const
|
|
859
|
-
const projectName =
|
|
1161
|
+
const identity = localIdentity(projectPath);
|
|
1162
|
+
const projectName = identity.repoRoot.split("/").pop() || identity.repoRoot;
|
|
860
1163
|
const payload = {
|
|
861
|
-
reviewId:
|
|
1164
|
+
reviewId: "",
|
|
862
1165
|
diffSet,
|
|
863
1166
|
rawDiff,
|
|
864
1167
|
briefing,
|
|
865
1168
|
metadata: {
|
|
866
1169
|
title: projectName,
|
|
867
1170
|
currentBranch
|
|
868
|
-
}
|
|
869
|
-
watchMode: true
|
|
1171
|
+
}
|
|
870
1172
|
};
|
|
871
|
-
const session = {
|
|
872
|
-
|
|
1173
|
+
const { session, reused } = openSession({
|
|
1174
|
+
key: identity.key,
|
|
1175
|
+
repoRoot: identity.repoRoot,
|
|
1176
|
+
projectPath: identity.repoRoot,
|
|
873
1177
|
payload,
|
|
874
|
-
projectPath,
|
|
875
|
-
source: "manual",
|
|
876
|
-
status: "pending",
|
|
877
|
-
createdAt: Date.now(),
|
|
878
|
-
result: null,
|
|
879
1178
|
diffRef,
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
sessions.set(sessionId, session);
|
|
886
|
-
startSessionWatcher(sessionId);
|
|
887
|
-
broadcastToAll({
|
|
888
|
-
type: "session:added",
|
|
889
|
-
payload: toSummary(session)
|
|
1179
|
+
source: "manual"
|
|
1180
|
+
});
|
|
1181
|
+
jsonResponse(res, reused ? 200 : 201, {
|
|
1182
|
+
sessionId: session.id,
|
|
1183
|
+
fileCount: diffSet.files.length
|
|
890
1184
|
});
|
|
891
|
-
jsonResponse(res, 201, { sessionId, fileCount: diffSet.files.length });
|
|
892
1185
|
} catch {
|
|
893
1186
|
jsonResponse(res, 400, { error: "Invalid request body" });
|
|
894
1187
|
}
|
|
@@ -910,7 +1203,7 @@ async function handleApiRequest(req, res) {
|
|
|
910
1203
|
fetchPullRequest,
|
|
911
1204
|
fetchPullRequestDiff,
|
|
912
1205
|
normalizePr
|
|
913
|
-
} = await import("./src-
|
|
1206
|
+
} = await import("./src-DNNNQ2RD.js");
|
|
914
1207
|
if (!isPrRef(prUrl)) {
|
|
915
1208
|
jsonResponse(res, 400, {
|
|
916
1209
|
error: "Invalid PR URL. Expected https://github.com/owner/repo/pull/123 or owner/repo#123"
|
|
@@ -947,26 +1240,15 @@ async function handleApiRequest(req, res) {
|
|
|
947
1240
|
}
|
|
948
1241
|
} catch {
|
|
949
1242
|
}
|
|
950
|
-
const
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
id: sessionId,
|
|
954
|
-
payload: normalized.payload,
|
|
1243
|
+
const { session, reused } = openSession({
|
|
1244
|
+
key: `pr:${owner}/${repo}#${prNumber}`.toLowerCase(),
|
|
1245
|
+
repoRoot: localRepoPath ? getRepoRoot({ cwd: localRepoPath }) ?? localRepoPath : null,
|
|
955
1246
|
projectPath: localRepoPath ?? `github:${owner}/${repo}#${prNumber}`,
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
createdAt: Date.now(),
|
|
959
|
-
result: null,
|
|
960
|
-
hasNewChanges: false,
|
|
961
|
-
annotations: []
|
|
962
|
-
};
|
|
963
|
-
sessions.set(sessionId, session);
|
|
964
|
-
broadcastToAll({
|
|
965
|
-
type: "session:added",
|
|
966
|
-
payload: toSummary(session)
|
|
1247
|
+
payload: normalized.payload,
|
|
1248
|
+
source: "manual"
|
|
967
1249
|
});
|
|
968
|
-
jsonResponse(res, 201, {
|
|
969
|
-
sessionId,
|
|
1250
|
+
jsonResponse(res, reused ? 200 : 201, {
|
|
1251
|
+
sessionId: session.id,
|
|
970
1252
|
fileCount: normalized.diffSet.files.length,
|
|
971
1253
|
localRepoPath,
|
|
972
1254
|
pr: {
|
|
@@ -989,7 +1271,7 @@ async function handleApiRequest(req, res) {
|
|
|
989
1271
|
if (parsedUrl.pathname === "/api/fs/list") {
|
|
990
1272
|
const dirPath = parsedUrl.searchParams.get("path") || process.cwd();
|
|
991
1273
|
try {
|
|
992
|
-
const stat =
|
|
1274
|
+
const stat = fs7.statSync(dirPath);
|
|
993
1275
|
if (!stat.isDirectory()) {
|
|
994
1276
|
jsonResponse(res, 400, { error: "Not a directory" });
|
|
995
1277
|
return true;
|
|
@@ -999,13 +1281,13 @@ async function handleApiRequest(req, res) {
|
|
|
999
1281
|
return true;
|
|
1000
1282
|
}
|
|
1001
1283
|
try {
|
|
1002
|
-
const entries =
|
|
1284
|
+
const entries = fs7.readdirSync(dirPath, { withFileTypes: true });
|
|
1003
1285
|
const dirs = [];
|
|
1004
1286
|
for (const entry of entries) {
|
|
1005
1287
|
if (!entry.isDirectory()) continue;
|
|
1006
1288
|
if (entry.name.startsWith(".")) continue;
|
|
1007
1289
|
const fullPath = `${dirPath}/${entry.name}`;
|
|
1008
|
-
const isGitRepo2 =
|
|
1290
|
+
const isGitRepo2 = fs7.existsSync(`${fullPath}/.git`);
|
|
1009
1291
|
dirs.push({ name: entry.name, path: fullPath, isGitRepo: isGitRepo2 });
|
|
1010
1292
|
}
|
|
1011
1293
|
dirs.sort((a, b) => {
|
|
@@ -1022,10 +1304,22 @@ async function handleApiRequest(req, res) {
|
|
|
1022
1304
|
}
|
|
1023
1305
|
}
|
|
1024
1306
|
if (method === "GET" && url === "/api/reviews") {
|
|
1025
|
-
const summaries =
|
|
1307
|
+
const summaries = listedSummaries();
|
|
1026
1308
|
jsonResponse(res, 200, { sessions: summaries });
|
|
1027
1309
|
return true;
|
|
1028
1310
|
}
|
|
1311
|
+
if (method === "GET" && url === "/api/reviews/resolve") {
|
|
1312
|
+
const parsed = new URL(req.url ?? "/", "http://localhost");
|
|
1313
|
+
const rawPath = parsed.searchParams.get("path");
|
|
1314
|
+
if (!rawPath) {
|
|
1315
|
+
jsonResponse(res, 400, { error: "Missing path" });
|
|
1316
|
+
return true;
|
|
1317
|
+
}
|
|
1318
|
+
const repoRoot = getRepoRoot({ cwd: rawPath }) ?? path7.resolve(rawPath);
|
|
1319
|
+
const matches = Array.from(sessions.values()).filter((session) => session.closedAt === void 0 && session.repoRoot === repoRoot).map(toSummary);
|
|
1320
|
+
jsonResponse(res, 200, { repoRoot, sessions: matches });
|
|
1321
|
+
return true;
|
|
1322
|
+
}
|
|
1029
1323
|
const getReviewParams = matchRoute(method, url, "GET", "/api/reviews/:id");
|
|
1030
1324
|
if (getReviewParams) {
|
|
1031
1325
|
const session = sessions.get(getReviewParams.id);
|
|
@@ -1060,9 +1354,7 @@ async function handleApiRequest(req, res) {
|
|
|
1060
1354
|
try {
|
|
1061
1355
|
const body = await readBody(req);
|
|
1062
1356
|
const result = JSON.parse(body);
|
|
1063
|
-
session
|
|
1064
|
-
session.status = "submitted";
|
|
1065
|
-
recordReviewHistory(session, result);
|
|
1357
|
+
recordVerdict(session, result);
|
|
1066
1358
|
if (result.decision === "dismissed") {
|
|
1067
1359
|
broadcastSessionRemoved(postResultParams.id);
|
|
1068
1360
|
} else {
|
|
@@ -1081,6 +1373,7 @@ async function handleApiRequest(req, res) {
|
|
|
1081
1373
|
jsonResponse(res, 404, { error: "Session not found" });
|
|
1082
1374
|
return true;
|
|
1083
1375
|
}
|
|
1376
|
+
touch(session);
|
|
1084
1377
|
if (session.result) {
|
|
1085
1378
|
jsonResponse(res, 200, { result: session.result, status: "submitted" });
|
|
1086
1379
|
} else {
|
|
@@ -1126,24 +1419,44 @@ async function handleApiRequest(req, res) {
|
|
|
1126
1419
|
}
|
|
1127
1420
|
try {
|
|
1128
1421
|
const body = await readBody(req);
|
|
1129
|
-
const { file, line, body: annotationBody, type, confidence, category, source } = JSON.parse(body);
|
|
1422
|
+
const { file, line, side, body: annotationBody, type, confidence, category, source, author } = JSON.parse(body);
|
|
1423
|
+
if (author !== void 0 && author !== "agent" && author !== "reviewer") {
|
|
1424
|
+
jsonResponse(res, 400, { error: `Unknown author: ${String(author)}` });
|
|
1425
|
+
return true;
|
|
1426
|
+
}
|
|
1427
|
+
if (side !== void 0 && side !== "old" && side !== "new") {
|
|
1428
|
+
jsonResponse(res, 400, { error: `Unknown side: ${String(side)}` });
|
|
1429
|
+
return true;
|
|
1430
|
+
}
|
|
1130
1431
|
const annotation = {
|
|
1131
1432
|
id: randomUUID2(),
|
|
1132
1433
|
sessionId: session.id,
|
|
1133
1434
|
file,
|
|
1134
1435
|
line,
|
|
1436
|
+
// Agents annotate lines of the changed file; only the dashboard can
|
|
1437
|
+
// point at a deleted line, and it says so.
|
|
1438
|
+
side: side ?? "new",
|
|
1135
1439
|
body: annotationBody,
|
|
1136
1440
|
type,
|
|
1137
1441
|
confidence: confidence ?? 1,
|
|
1138
1442
|
category: category ?? "other",
|
|
1139
1443
|
source,
|
|
1140
|
-
createdAt: Date.now()
|
|
1444
|
+
createdAt: Date.now(),
|
|
1445
|
+
author: author ?? "agent",
|
|
1446
|
+
replies: []
|
|
1141
1447
|
};
|
|
1142
1448
|
session.annotations.push(annotation);
|
|
1449
|
+
touch(session);
|
|
1143
1450
|
sendToSessionClients(session.id, {
|
|
1144
1451
|
type: "annotation:added",
|
|
1145
1452
|
payload: annotation
|
|
1146
1453
|
});
|
|
1454
|
+
if (annotation.type === "warning") {
|
|
1455
|
+
if (hasViewersForSession(session.id)) {
|
|
1456
|
+
session.attentionClearedAt = annotation.createdAt;
|
|
1457
|
+
}
|
|
1458
|
+
broadcastSessionUpdate(session);
|
|
1459
|
+
}
|
|
1147
1460
|
jsonResponse(res, 200, { annotationId: annotation.id });
|
|
1148
1461
|
} catch {
|
|
1149
1462
|
jsonResponse(res, 400, { error: "Invalid request body" });
|
|
@@ -1160,6 +1473,94 @@ async function handleApiRequest(req, res) {
|
|
|
1160
1473
|
jsonResponse(res, 200, { annotations: session.annotations });
|
|
1161
1474
|
return true;
|
|
1162
1475
|
}
|
|
1476
|
+
const replyParams = matchRoute(method, url, "POST", "/api/reviews/:id/annotations/:annotationId/replies");
|
|
1477
|
+
if (replyParams) {
|
|
1478
|
+
const session = sessions.get(replyParams.id);
|
|
1479
|
+
if (!session) {
|
|
1480
|
+
jsonResponse(res, 404, { error: "Session not found" });
|
|
1481
|
+
return true;
|
|
1482
|
+
}
|
|
1483
|
+
const annotation = session.annotations.find((a) => a.id === replyParams.annotationId);
|
|
1484
|
+
if (!annotation) {
|
|
1485
|
+
jsonResponse(res, 404, { error: "Annotation not found" });
|
|
1486
|
+
return true;
|
|
1487
|
+
}
|
|
1488
|
+
try {
|
|
1489
|
+
const { author, agent, body: replyBody } = JSON.parse(await readBody(req));
|
|
1490
|
+
if (author !== "agent" && author !== "reviewer") {
|
|
1491
|
+
jsonResponse(res, 400, { error: 'author must be "agent" or "reviewer"' });
|
|
1492
|
+
return true;
|
|
1493
|
+
}
|
|
1494
|
+
if (!replyBody?.trim()) {
|
|
1495
|
+
jsonResponse(res, 400, { error: "A reply needs a body" });
|
|
1496
|
+
return true;
|
|
1497
|
+
}
|
|
1498
|
+
const reply = {
|
|
1499
|
+
id: randomUUID2(),
|
|
1500
|
+
author,
|
|
1501
|
+
...author === "agent" ? { agent: agent ?? "unknown" } : {},
|
|
1502
|
+
body: replyBody,
|
|
1503
|
+
createdAt: Date.now()
|
|
1504
|
+
};
|
|
1505
|
+
annotation.replies = [...annotation.replies ?? [], reply];
|
|
1506
|
+
touch(session);
|
|
1507
|
+
sendToSessionClients(session.id, { type: "annotation:updated", payload: annotation });
|
|
1508
|
+
jsonResponse(res, 200, { replyId: reply.id, annotation });
|
|
1509
|
+
} catch {
|
|
1510
|
+
jsonResponse(res, 400, { error: "Invalid request body" });
|
|
1511
|
+
}
|
|
1512
|
+
return true;
|
|
1513
|
+
}
|
|
1514
|
+
const githubReviewParams = matchRoute(method, url, "POST", "/api/reviews/:id/github-review");
|
|
1515
|
+
if (githubReviewParams) {
|
|
1516
|
+
const session = sessions.get(githubReviewParams.id);
|
|
1517
|
+
if (!session) {
|
|
1518
|
+
jsonResponse(res, 404, { error: "Session not found" });
|
|
1519
|
+
return true;
|
|
1520
|
+
}
|
|
1521
|
+
const pr = session.payload.metadata.githubPr;
|
|
1522
|
+
if (!pr) {
|
|
1523
|
+
jsonResponse(res, 400, { error: "Not a pull request review" });
|
|
1524
|
+
return true;
|
|
1525
|
+
}
|
|
1526
|
+
let submission;
|
|
1527
|
+
try {
|
|
1528
|
+
submission = JSON.parse(await readBody(req));
|
|
1529
|
+
} catch {
|
|
1530
|
+
jsonResponse(res, 400, { error: "Invalid request body" });
|
|
1531
|
+
return true;
|
|
1532
|
+
}
|
|
1533
|
+
const built = buildGitHubReview(submission, session.annotations);
|
|
1534
|
+
if ("error" in built) {
|
|
1535
|
+
jsonResponse(res, 400, { error: built.error });
|
|
1536
|
+
return true;
|
|
1537
|
+
}
|
|
1538
|
+
const { resolveGitHubToken, createGitHubClient, submitGitHubReview } = await import("./src-DNNNQ2RD.js");
|
|
1539
|
+
let token;
|
|
1540
|
+
try {
|
|
1541
|
+
token = resolveGitHubToken();
|
|
1542
|
+
} catch (err) {
|
|
1543
|
+
jsonResponse(res, 401, { error: err instanceof Error ? err.message : String(err) });
|
|
1544
|
+
return true;
|
|
1545
|
+
}
|
|
1546
|
+
let posted;
|
|
1547
|
+
try {
|
|
1548
|
+
posted = await submitGitHubReview(createGitHubClient(token), pr.owner, pr.repo, pr.number, built.review);
|
|
1549
|
+
} catch (err) {
|
|
1550
|
+
jsonResponse(res, 502, {
|
|
1551
|
+
error: `GitHub rejected the review: ${err instanceof Error ? err.message : String(err)}`
|
|
1552
|
+
});
|
|
1553
|
+
return true;
|
|
1554
|
+
}
|
|
1555
|
+
recordVerdict(session, {
|
|
1556
|
+
decision: PR_EVENT_DECISION[built.review.event],
|
|
1557
|
+
comments: [],
|
|
1558
|
+
summary: built.review.body || void 0
|
|
1559
|
+
});
|
|
1560
|
+
broadcastSessionUpdate(session);
|
|
1561
|
+
jsonResponse(res, 200, { url: posted.url });
|
|
1562
|
+
return true;
|
|
1563
|
+
}
|
|
1163
1564
|
const dismissAnnotationParams = matchRoute(method, url, "POST", "/api/reviews/:id/annotations/:annotationId/dismiss");
|
|
1164
1565
|
if (dismissAnnotationParams) {
|
|
1165
1566
|
const session = sessions.get(dismissAnnotationParams.id);
|
|
@@ -1177,6 +1578,9 @@ async function handleApiRequest(req, res) {
|
|
|
1177
1578
|
type: "annotation:dismissed",
|
|
1178
1579
|
payload: { annotationId: dismissAnnotationParams.annotationId }
|
|
1179
1580
|
});
|
|
1581
|
+
if (annotation.type === "warning") {
|
|
1582
|
+
broadcastSessionUpdate(session);
|
|
1583
|
+
}
|
|
1180
1584
|
jsonResponse(res, 200, { ok: true });
|
|
1181
1585
|
return true;
|
|
1182
1586
|
}
|
|
@@ -1257,7 +1661,8 @@ async function handleApiRequest(req, res) {
|
|
|
1257
1661
|
}
|
|
1258
1662
|
try {
|
|
1259
1663
|
const body = await readBody(req);
|
|
1260
|
-
const { ref } = JSON.parse(body);
|
|
1664
|
+
const { ref: requestedRef, reset } = JSON.parse(body);
|
|
1665
|
+
const ref = reset ? session.openedDiffRef ?? DEFAULT_DIFF_REF : requestedRef;
|
|
1261
1666
|
if (!ref) {
|
|
1262
1667
|
jsonResponse(res, 400, { error: "Missing ref in request body" });
|
|
1263
1668
|
return true;
|
|
@@ -1277,6 +1682,7 @@ async function handleApiRequest(req, res) {
|
|
|
1277
1682
|
session.lastDiffSet = newDiffSet;
|
|
1278
1683
|
stopSessionWatcher(session.id);
|
|
1279
1684
|
session.diffRef = ref;
|
|
1685
|
+
touch(session);
|
|
1280
1686
|
if (hasConnectedClients()) {
|
|
1281
1687
|
startSessionWatcher(session.id);
|
|
1282
1688
|
}
|
|
@@ -1337,10 +1743,18 @@ async function startGlobalServer(options = {}) {
|
|
|
1337
1743
|
wsPort: preferredWsPort = 24681,
|
|
1338
1744
|
silent = false,
|
|
1339
1745
|
dev = false,
|
|
1340
|
-
pollInterval =
|
|
1746
|
+
pollInterval = DEFAULT_WATCH_SCHEDULE.viewedMs,
|
|
1747
|
+
unviewedPollInterval = DEFAULT_WATCH_SCHEDULE.unviewedMs,
|
|
1748
|
+
unviewedPollMaxInterval = DEFAULT_WATCH_SCHEDULE.unviewedMaxMs,
|
|
1749
|
+
idleSessionTtl = IDLE_SESSION_TTL_MS,
|
|
1750
|
+
cleanupInterval = CLEANUP_INTERVAL_MS,
|
|
1341
1751
|
openBrowser = true
|
|
1342
1752
|
} = options;
|
|
1343
|
-
|
|
1753
|
+
watchSchedule = {
|
|
1754
|
+
viewedMs: pollInterval,
|
|
1755
|
+
unviewedMs: unviewedPollInterval,
|
|
1756
|
+
unviewedMaxMs: unviewedPollMaxInterval
|
|
1757
|
+
};
|
|
1344
1758
|
const [httpPort, wsPort] = await Promise.all([
|
|
1345
1759
|
getPort({ port: preferredHttpPort }),
|
|
1346
1760
|
getPort({ port: preferredWsPort })
|
|
@@ -1370,26 +1784,14 @@ async function startGlobalServer(options = {}) {
|
|
|
1370
1784
|
const url = new URL(req.url ?? "/", `http://localhost:${wsPort}`);
|
|
1371
1785
|
const sessionId = url.searchParams.get("sessionId");
|
|
1372
1786
|
if (sessionId) {
|
|
1373
|
-
clientSessions.set(ws, sessionId);
|
|
1374
1787
|
const session = sessions.get(sessionId);
|
|
1375
1788
|
if (session) {
|
|
1376
|
-
session
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
const msg = {
|
|
1380
|
-
type: "review:init",
|
|
1381
|
-
payload: session.payload
|
|
1382
|
-
};
|
|
1383
|
-
ws.send(JSON.stringify(msg));
|
|
1384
|
-
for (const annotation of session.annotations) {
|
|
1385
|
-
ws.send(JSON.stringify({
|
|
1386
|
-
type: "annotation:added",
|
|
1387
|
-
payload: annotation
|
|
1388
|
-
}));
|
|
1389
|
-
}
|
|
1789
|
+
attachViewer(ws, session);
|
|
1790
|
+
} else {
|
|
1791
|
+
clientSessions.set(ws, sessionId);
|
|
1390
1792
|
}
|
|
1391
1793
|
} else {
|
|
1392
|
-
const summaries =
|
|
1794
|
+
const summaries = listedSummaries();
|
|
1393
1795
|
const msg = {
|
|
1394
1796
|
type: "session:list",
|
|
1395
1797
|
payload: summaries
|
|
@@ -1398,20 +1800,7 @@ async function startGlobalServer(options = {}) {
|
|
|
1398
1800
|
if (summaries.length === 1) {
|
|
1399
1801
|
const session = sessions.get(summaries[0].id);
|
|
1400
1802
|
if (session) {
|
|
1401
|
-
|
|
1402
|
-
session.status = "in_review";
|
|
1403
|
-
session.hasNewChanges = false;
|
|
1404
|
-
broadcastSessionUpdate(session);
|
|
1405
|
-
ws.send(JSON.stringify({
|
|
1406
|
-
type: "review:init",
|
|
1407
|
-
payload: session.payload
|
|
1408
|
-
}));
|
|
1409
|
-
for (const annotation of session.annotations) {
|
|
1410
|
-
ws.send(JSON.stringify({
|
|
1411
|
-
type: "annotation:added",
|
|
1412
|
-
payload: annotation
|
|
1413
|
-
}));
|
|
1414
|
-
}
|
|
1803
|
+
attachViewer(ws, session);
|
|
1415
1804
|
}
|
|
1416
1805
|
}
|
|
1417
1806
|
}
|
|
@@ -1423,9 +1812,7 @@ async function startGlobalServer(options = {}) {
|
|
|
1423
1812
|
if (sid) {
|
|
1424
1813
|
const session = sessions.get(sid);
|
|
1425
1814
|
if (session) {
|
|
1426
|
-
session
|
|
1427
|
-
session.status = "submitted";
|
|
1428
|
-
recordReviewHistory(session, msg.payload);
|
|
1815
|
+
recordVerdict(session, msg.payload);
|
|
1429
1816
|
if (msg.payload.decision === "dismissed") {
|
|
1430
1817
|
broadcastSessionRemoved(sid);
|
|
1431
1818
|
} else {
|
|
@@ -1436,29 +1823,18 @@ async function startGlobalServer(options = {}) {
|
|
|
1436
1823
|
} else if (msg.type === "session:select") {
|
|
1437
1824
|
const session = sessions.get(msg.payload.sessionId);
|
|
1438
1825
|
if (session) {
|
|
1439
|
-
|
|
1440
|
-
session.status = "in_review";
|
|
1441
|
-
session.hasNewChanges = false;
|
|
1442
|
-
startSessionWatcher(session.id);
|
|
1443
|
-
broadcastSessionUpdate(session);
|
|
1444
|
-
ws.send(JSON.stringify({
|
|
1445
|
-
type: "review:init",
|
|
1446
|
-
payload: session.payload
|
|
1447
|
-
}));
|
|
1448
|
-
for (const annotation of session.annotations) {
|
|
1449
|
-
ws.send(JSON.stringify({
|
|
1450
|
-
type: "annotation:added",
|
|
1451
|
-
payload: annotation
|
|
1452
|
-
}));
|
|
1453
|
-
}
|
|
1826
|
+
attachViewer(ws, session);
|
|
1454
1827
|
}
|
|
1455
1828
|
} else if (msg.type === "session:close") {
|
|
1456
1829
|
const closedId = msg.payload.sessionId;
|
|
1457
1830
|
stopSessionWatcher(closedId);
|
|
1458
1831
|
const closedSession = sessions.get(closedId);
|
|
1459
|
-
if (closedSession
|
|
1460
|
-
closedSession.
|
|
1461
|
-
closedSession.
|
|
1832
|
+
if (closedSession) {
|
|
1833
|
+
closedSession.closedAt = Date.now();
|
|
1834
|
+
if (!closedSession.result) {
|
|
1835
|
+
closedSession.result = { decision: "dismissed", comments: [] };
|
|
1836
|
+
closedSession.status = "submitted";
|
|
1837
|
+
}
|
|
1462
1838
|
}
|
|
1463
1839
|
broadcastSessionRemoved(closedId);
|
|
1464
1840
|
} else if (msg.type === "diff:change_ref") {
|
|
@@ -1519,19 +1895,18 @@ async function startGlobalServer(options = {}) {
|
|
|
1519
1895
|
function cleanupExpiredSessions() {
|
|
1520
1896
|
const now = Date.now();
|
|
1521
1897
|
for (const [id, session] of sessions.entries()) {
|
|
1522
|
-
|
|
1523
|
-
continue;
|
|
1524
|
-
}
|
|
1898
|
+
const idle = session.status !== "submitted" && !hasViewersForSession(id) && now - session.lastActivityAt > idleSessionTtl;
|
|
1525
1899
|
const age = now - session.createdAt;
|
|
1526
|
-
const
|
|
1527
|
-
|
|
1900
|
+
const ageRulesApply = !(session.source === "manual" && session.status !== "submitted");
|
|
1901
|
+
const expiredByAge = ageRulesApply && (session.status === "submitted" && age > SUBMITTED_TTL_MS || session.status === "pending" && age > ABANDONED_TTL_MS);
|
|
1902
|
+
if (idle || expiredByAge) {
|
|
1528
1903
|
stopSessionWatcher(id);
|
|
1529
1904
|
sessions.delete(id);
|
|
1530
1905
|
broadcastSessionRemoved(id);
|
|
1531
1906
|
}
|
|
1532
1907
|
}
|
|
1533
1908
|
}
|
|
1534
|
-
const cleanupTimer = setInterval(cleanupExpiredSessions,
|
|
1909
|
+
const cleanupTimer = setInterval(cleanupExpiredSessions, cleanupInterval);
|
|
1535
1910
|
const serverInfo = {
|
|
1536
1911
|
httpPort,
|
|
1537
1912
|
wsPort,
|
|
@@ -1588,12 +1963,54 @@ Waiting for reviews...
|
|
|
1588
1963
|
return { httpPort, wsPort, stop };
|
|
1589
1964
|
}
|
|
1590
1965
|
|
|
1966
|
+
// packages/core/src/mcp-tools.ts
|
|
1967
|
+
var MCP_TOOL_NAMES = [
|
|
1968
|
+
"open_review",
|
|
1969
|
+
"get_review_result",
|
|
1970
|
+
"update_review_context",
|
|
1971
|
+
"get_diff",
|
|
1972
|
+
"analyze_diff",
|
|
1973
|
+
"annotate",
|
|
1974
|
+
"get_review_state",
|
|
1975
|
+
"get_review_comments",
|
|
1976
|
+
"reply",
|
|
1977
|
+
"wait_for_comments",
|
|
1978
|
+
"get_user_focus",
|
|
1979
|
+
"get_pr_context",
|
|
1980
|
+
"get_file_diff",
|
|
1981
|
+
"get_file_context"
|
|
1982
|
+
];
|
|
1983
|
+
var RETIRED_MCP_TOOL_NAMES = [
|
|
1984
|
+
"add_annotation",
|
|
1985
|
+
"add_review_comment",
|
|
1986
|
+
"flag_for_attention",
|
|
1987
|
+
"review_pr"
|
|
1988
|
+
];
|
|
1989
|
+
function mcpToolPermission(name) {
|
|
1990
|
+
return `mcp__diffprism__${name}`;
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1591
1993
|
export {
|
|
1592
1994
|
readServerFile,
|
|
1593
1995
|
isServerAlive,
|
|
1996
|
+
DEFAULT_DIFF_REF,
|
|
1997
|
+
COMMIT_GATE_DIFF_REF,
|
|
1998
|
+
DIFF_REF_DESCRIPTION,
|
|
1999
|
+
getBuildInfo,
|
|
2000
|
+
describeVersion,
|
|
2001
|
+
currentVersion,
|
|
2002
|
+
recordError,
|
|
2003
|
+
readLastError,
|
|
2004
|
+
REPORT_HINT,
|
|
2005
|
+
buildFeedbackUrl,
|
|
1594
2006
|
startGlobalServer,
|
|
2007
|
+
awaitingAgent,
|
|
1595
2008
|
ensureServer,
|
|
2009
|
+
ReviewTimeoutError,
|
|
2010
|
+
ReviewerAskedError,
|
|
2011
|
+
waitForDecision,
|
|
1596
2012
|
submitReviewToServer,
|
|
1597
|
-
|
|
1598
|
-
|
|
2013
|
+
MCP_TOOL_NAMES,
|
|
2014
|
+
RETIRED_MCP_TOOL_NAMES,
|
|
2015
|
+
mcpToolPermission
|
|
1599
2016
|
};
|