prreviewbuddy 0.12.1 → 0.12.16
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/{log-PmeEgome.js → log-CFa7EzgZ.js} +465 -165
- package/dist/main.js +19 -7
- package/dist/server.js +852 -127
- package/package.json +1 -1
- package/static/chunk-5rwdhcVY.js +1 -0
- package/static/chunk-7inUl5Il.js +1 -0
- package/static/chunk-9-3VOpWX.js +24 -0
- package/static/chunk-B0xoFuVi.js +1 -0
- package/static/chunk-B80S6zbH.js +44 -0
- package/static/chunk-B8V5WPf1.js +292 -0
- package/static/chunk-BFRx3GcN.js +1 -0
- package/static/chunk-BFoDCU3G.js +1 -0
- package/static/chunk-BHBwEv5G.js +1 -0
- package/static/chunk-BOfOlrLQ.js +1 -0
- package/static/chunk-BSUrFSfn.js +2 -0
- package/static/chunk-BVDh-dl-.js +88 -0
- package/static/chunk-BVm3MgB2.js +139 -0
- package/static/chunk-BVqE4wlY.js +1 -0
- package/static/chunk-BZFc_oT5.js +1 -0
- package/static/chunk-Bd8oAEBF.js +62 -0
- package/static/chunk-Bdvr7G0w.js +1 -0
- package/static/chunk-BfpeM2_Y.js +1 -0
- package/static/chunk-BifhSqXX.js +1 -0
- package/static/chunk-Bk4d_k6M.js +62 -0
- package/static/chunk-BmkZxg03.js +1 -0
- package/static/chunk-BoTbRR8S.js +4 -0
- package/static/chunk-BokiwdgS.js +40 -0
- package/static/chunk-BqFMUyVw.js +168 -0
- package/static/chunk-Bs98n_vr.js +1 -0
- package/static/chunk-BvO9qrHb.js +1 -0
- package/static/chunk-BxpAJfRF.js +36 -0
- package/static/chunk-C-OQMol4.js +1 -0
- package/static/chunk-C26MXXwi.js +1 -0
- package/static/chunk-C6X32c5Y.js +1 -0
- package/static/chunk-CGd-uY3x.js +321 -0
- package/static/chunk-CMKpUUG0.js +1 -0
- package/static/chunk-CNXFALfL.js +89 -0
- package/static/chunk-CQfKfv79.js +1 -0
- package/static/chunk-CQhDWCuO.js +1 -0
- package/static/chunk-CU_yc7c0.js +7 -0
- package/static/chunk-CUsQ7lH1.js +34 -0
- package/static/chunk-CXMH3UgJ.js +257 -0
- package/static/chunk-CZus5tIC.js +1 -0
- package/static/chunk-CeiETtR3.js +127 -0
- package/static/chunk-ChQTsUB2.js +30 -0
- package/static/chunk-CiB0fz5M.js +1 -0
- package/static/chunk-CmOPaNov.js +1 -0
- package/static/chunk-CoIQ0XcM.js +8 -0
- package/static/chunk-Cp5EDur4.js +38 -0
- package/static/chunk-CqMjOmHM.js +1 -0
- package/static/chunk-CqbP8tav.js +231 -0
- package/static/chunk-Crk3W7xi.js +1 -0
- package/static/chunk-CyJ4bWgC.js +1 -0
- package/static/chunk-D289T3W2.js +39 -0
- package/static/chunk-D3k4QmBl.js +162 -0
- package/static/chunk-D5VxBvZv.js +7 -0
- package/static/chunk-D7FarRGf.js +1 -0
- package/static/chunk-D87EWWOh.js +206 -0
- package/static/chunk-DBVuY2lB.js +96 -0
- package/static/chunk-DEKSwylx.js +10 -0
- package/static/chunk-DHkjHDYG.js +1 -0
- package/static/chunk-DHwB1DNv.js +161 -0
- package/static/chunk-DQT7Z75O.js +99 -0
- package/static/chunk-DS17s2tU.js +1 -0
- package/static/chunk-DTTB1Rjf.js +1 -0
- package/static/chunk-DVXDUcQZ.js +1 -0
- package/static/chunk-DdmKGZXf.js +1 -0
- package/static/chunk-Dex9JiR1.js +3 -0
- package/static/chunk-DfMSIask.js +1 -0
- package/static/chunk-DfcNMhQb.js +41 -0
- package/static/chunk-Dh6xxjov.js +1 -0
- package/static/chunk-Dk6P4vVY.js +1 -0
- package/static/chunk-DlMt7LBR.js +1 -0
- package/static/chunk-DlPtUH2x.js +24 -0
- package/static/chunk-DmHr41SG.js +72 -0
- package/static/chunk-DpHRgEqx.js +1 -0
- package/static/chunk-DqZGGQx6.js +2 -0
- package/static/chunk-DsF7k-Jl.js +1 -0
- package/static/chunk-DtQPBrIL.js +129 -0
- package/static/chunk-DufL1VKq.js +1 -0
- package/static/chunk-DxSMoRAg.js +1 -0
- package/static/chunk-Dy-Kn_BL.js +1 -0
- package/static/chunk-H9AITnpH.js +1 -0
- package/static/chunk-IOULrYuo.js +7 -0
- package/static/chunk-IsizPYbz.js +70 -0
- package/static/chunk-LHD4D-55.js +1 -0
- package/static/chunk-MLIlWKCY.js +1 -0
- package/static/chunk-SWs44m7G.js +1 -0
- package/static/chunk-Wi1hfKnY.js +15 -0
- package/static/chunk-dQvmgBo8.js +106 -0
- package/static/chunk-fybaL0A-.js +1 -0
- package/static/chunk-lpcXIlNC.js +84 -0
- package/static/chunk-nKL3T2Rn.js +78 -0
- package/static/chunk-oZJp8_Oh.js +1 -0
- package/static/chunk-oxFaA6rD.js +120 -0
- package/static/chunk-pzXl7dPH.js +1 -0
- package/static/chunk-qMjAlJP9.js +1 -0
- package/static/chunk-qnagBFlM.js +1 -0
- package/static/shell.css +372 -41
- package/static/sidebar.css +140 -4
- package/static/workspace.js +0 -0
|
@@ -10,6 +10,13 @@ import { platform } from "node:process";
|
|
|
10
10
|
//#region ../../packages/review-harness/src/agents/agent.ts
|
|
11
11
|
/** The agent could not be run at all, as distinct from running and failing. */
|
|
12
12
|
var AgentUnavailableError = class extends Error {};
|
|
13
|
+
/**
|
|
14
|
+
* The caller stopped waiting, so the run was stopped.
|
|
15
|
+
*
|
|
16
|
+
* Its own class rather than a message, because every caller has to tell it from a failure: a run
|
|
17
|
+
* the reviewer cancelled is not an error to report to them, and it is not a turn to store.
|
|
18
|
+
*/
|
|
19
|
+
var AgentCancelledError = class extends Error {};
|
|
13
20
|
/** The run was refused before it started, because it would have breached isolation. */
|
|
14
21
|
var PromptIsolationError = class extends Error {};
|
|
15
22
|
/**
|
|
@@ -81,6 +88,10 @@ var CLAUDE_AGENT_ID = "claude";
|
|
|
81
88
|
* about sign-in to errors that have nothing to do with it, which is worse than staying quiet.
|
|
82
89
|
*/
|
|
83
90
|
var AUTHENTICATION = /\b(?:auth(?:enticat\w*)?|oauth|credential|session expired|not logged in|log ?in)\b/i;
|
|
91
|
+
/** Said to the caller that asked for the stop, and never shown to the reviewer who pressed it. */
|
|
92
|
+
var STOPPED = "The agent was stopped.";
|
|
93
|
+
/** How long a stopped agent gets to end on its own terms before it is ended for it. */
|
|
94
|
+
var SIGKILL_GRACE_MS = 2e3;
|
|
84
95
|
/** The two variables that decide which Claude answers, and nothing else. */
|
|
85
96
|
var CARRIED = ["CLAUDE_CONFIG_DIR", "PATH"];
|
|
86
97
|
/**
|
|
@@ -302,6 +313,7 @@ var claudeAgent = {
|
|
|
302
313
|
},
|
|
303
314
|
async run(input) {
|
|
304
315
|
guardPrompt(input);
|
|
316
|
+
if (input.signal?.aborted) throw new AgentCancelledError(STOPPED);
|
|
305
317
|
const reader = createStreamReader(input.onProgress ?? (() => {}));
|
|
306
318
|
return new Promise((resolvePromise, reject) => {
|
|
307
319
|
const child = spawn("claude", claudeArgs(input.capability), {
|
|
@@ -318,6 +330,14 @@ var claudeAgent = {
|
|
|
318
330
|
child.kill("SIGKILL");
|
|
319
331
|
reject(/* @__PURE__ */ new Error("The agent did not finish in time and was stopped."));
|
|
320
332
|
}, input.timeoutMs ?? DEFAULT_TIMEOUT_MS[input.capability]);
|
|
333
|
+
const stop = () => {
|
|
334
|
+
clearTimeout(timer);
|
|
335
|
+
child.kill("SIGTERM");
|
|
336
|
+
setTimeout(() => child.kill("SIGKILL"), SIGKILL_GRACE_MS).unref();
|
|
337
|
+
reject(new AgentCancelledError(STOPPED));
|
|
338
|
+
};
|
|
339
|
+
input.signal?.addEventListener("abort", stop, { once: true });
|
|
340
|
+
const release = () => input.signal?.removeEventListener("abort", stop);
|
|
321
341
|
child.stdout.setEncoding("utf8");
|
|
322
342
|
child.stdout.on("data", (chunk) => reader.push(chunk));
|
|
323
343
|
child.stderr.setEncoding("utf8");
|
|
@@ -326,11 +346,13 @@ var claudeAgent = {
|
|
|
326
346
|
});
|
|
327
347
|
child.on("error", (error) => {
|
|
328
348
|
clearTimeout(timer);
|
|
349
|
+
release();
|
|
329
350
|
if (error.code === "ENOENT") return reject(new ClaudeUnavailableError("The `claude` command is not on PATH, so this cannot be run from here. Install Claude Code, or start the workspace from a shell where `claude` runs."));
|
|
330
351
|
reject(error);
|
|
331
352
|
});
|
|
332
353
|
child.on("close", () => {
|
|
333
354
|
clearTimeout(timer);
|
|
355
|
+
release();
|
|
334
356
|
reader.end();
|
|
335
357
|
if (reader.overflowed()) return reject(/* @__PURE__ */ new Error("The agent produced more output than could be read, so it was stopped."));
|
|
336
358
|
const failure = reader.failed();
|
|
@@ -1193,6 +1215,73 @@ function inferLanguage(files) {
|
|
|
1193
1215
|
}[topExt] ?? topExt;
|
|
1194
1216
|
}
|
|
1195
1217
|
//#endregion
|
|
1218
|
+
//#region ../../packages/review-harness/src/domain/ai_review/outstanding.ts
|
|
1219
|
+
function reviewerDispositions(session) {
|
|
1220
|
+
const byId = new Map((session.dismissedIssues ?? []).map((entry) => [entry.id, entry]));
|
|
1221
|
+
return {
|
|
1222
|
+
isDismissed: (issue) => byId.has(issue.id),
|
|
1223
|
+
isStale: (issue) => issue.reconciliationStatus === "affected" && (byId.get(issue.id)?.sessionId ?? session.id) !== session.id
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* An issue is done when the commits fixed it, or when the reviewer dismissed it and that decision
|
|
1228
|
+
* still stands.
|
|
1229
|
+
*
|
|
1230
|
+
* `still-open` and `new` are outstanding unless dismissed. `affected` is the one worth stating: the
|
|
1231
|
+
* code under it moved without settling it, so it needs the reviewer's eyes *more* than an untouched
|
|
1232
|
+
* finding does — and that outranks a dismissal made before the code moved. The finding comes back
|
|
1233
|
+
* into the count **carrying its dismissal**, which the card shows: the reviewer's decision is not
|
|
1234
|
+
* deleted, its applicability has expired, and re-affirming it closes the finding again.
|
|
1235
|
+
*/
|
|
1236
|
+
function isIssueOutstanding(issue, dispositions) {
|
|
1237
|
+
if (issue.reconciliationStatus === "resolved") return false;
|
|
1238
|
+
if (dispositions.isDismissed(issue)) return dispositions.isStale(issue);
|
|
1239
|
+
return true;
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* A question is done when the reviewer has ticked it, or when the new commits answered it.
|
|
1243
|
+
*
|
|
1244
|
+
* Either alone is enough. The reviewer's tick is the older of the two and the only one the first
|
|
1245
|
+
* analysis can produce; `reconciliationStatus === 'answered'` arrives with an update and carries
|
|
1246
|
+
* `reconciliationAnswer` beside it, which is the answer itself. Requiring the tick as well would
|
|
1247
|
+
* make the reviewer hand-close seven questions the page had already answered in front of them.
|
|
1248
|
+
*
|
|
1249
|
+
* No `affected` expiry here, unlike an issue's dismissal. An issue is a claim about the code and a
|
|
1250
|
+
* dismissal is a bet on that code; a question the reviewer has answered was answered out of their
|
|
1251
|
+
* own head, and a commit somewhere near it does not un-know what they now know.
|
|
1252
|
+
*/
|
|
1253
|
+
function isQuestionOutstanding(question, answered) {
|
|
1254
|
+
return !answered.has(question.id) && question.reconciliationStatus !== "answered";
|
|
1255
|
+
}
|
|
1256
|
+
function tally(items, outstanding, byReviewer) {
|
|
1257
|
+
let open = 0;
|
|
1258
|
+
let reviewerClosed = 0;
|
|
1259
|
+
for (const item of items) if (outstanding(item)) open += 1;
|
|
1260
|
+
else if (byReviewer(item)) reviewerClosed += 1;
|
|
1261
|
+
return {
|
|
1262
|
+
total: items.length,
|
|
1263
|
+
open,
|
|
1264
|
+
done: items.length - open,
|
|
1265
|
+
reviewerClosed
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
function issuesOutstanding(issues, dispositions) {
|
|
1269
|
+
return tally(issues, (issue) => isIssueOutstanding(issue, dispositions), (issue) => issue.reconciliationStatus !== "resolved" && dispositions.isDismissed(issue));
|
|
1270
|
+
}
|
|
1271
|
+
function questionsOutstanding(questions, answered) {
|
|
1272
|
+
return tally(questions, (question) => isQuestionOutstanding(question, answered), (question) => question.reconciliationStatus !== "answered" && answered.has(question.id));
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* What to call the pile of findings nobody has to act on any more.
|
|
1276
|
+
*
|
|
1277
|
+
* "Resolved" is a claim about the code and is only true while the commits did all the closing. One
|
|
1278
|
+
* dismissal in there and the honest word is the vaguer one, because the reviewer deciding a finding
|
|
1279
|
+
* is acceptable did not resolve anything.
|
|
1280
|
+
*/
|
|
1281
|
+
function doneVerb(counts) {
|
|
1282
|
+
return counts.reviewerClosed > 0 ? "dealt with" : "resolved";
|
|
1283
|
+
}
|
|
1284
|
+
//#endregion
|
|
1196
1285
|
//#region ../../packages/review-harness/src/workspace/store.ts
|
|
1197
1286
|
var STORE_ROOT = join(homedir(), ".prreviewbuddy");
|
|
1198
1287
|
var WORKSPACES_DIR = join(STORE_ROOT, "workspaces");
|
|
@@ -1335,8 +1424,9 @@ function summarise(workspace) {
|
|
|
1335
1424
|
summary: result?.summary ?? "",
|
|
1336
1425
|
overallRisk: result?.overallRisk ?? "unknown",
|
|
1337
1426
|
issues: result?.issues.length ?? 0,
|
|
1427
|
+
openIssues: issuesOutstanding(result?.issues ?? [], reviewerDispositions(workspace.session)).open,
|
|
1338
1428
|
questions: questions.length,
|
|
1339
|
-
unansweredQuestions: questions
|
|
1429
|
+
unansweredQuestions: questionsOutstanding(questions, answered).open,
|
|
1340
1430
|
messages: workspace.transcript.length,
|
|
1341
1431
|
lastViewedAt: workspace.lastViewedAt,
|
|
1342
1432
|
completedAt: workspace.completedAt,
|
|
@@ -1348,6 +1438,16 @@ function summarise(workspace) {
|
|
|
1348
1438
|
};
|
|
1349
1439
|
}
|
|
1350
1440
|
/**
|
|
1441
|
+
* The commit a review was made from, however the record spells it. Null when it recorded none.
|
|
1442
|
+
*
|
|
1443
|
+
* Lives here rather than beside one of its callers because it is a reading of the record, and the
|
|
1444
|
+
* two things that need it are unrelated: pinning a checkout to answer questions against, and
|
|
1445
|
+
* reading a single file the reviewer's own checkout does not have.
|
|
1446
|
+
*/
|
|
1447
|
+
function reviewedCommit(workspace) {
|
|
1448
|
+
return workspace.session.metadata?.commitSha || workspace.changeSet.data.pullRequest.head.sha || null;
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1351
1451
|
* Record that a workspace was opened.
|
|
1352
1452
|
*
|
|
1353
1453
|
* Best-effort and deliberately silent: a review that cannot record having been looked at is still
|
|
@@ -1456,6 +1556,7 @@ async function buildChangeSet(options) {
|
|
|
1456
1556
|
},
|
|
1457
1557
|
repoPath,
|
|
1458
1558
|
baseRef: displayRef(baseRef),
|
|
1559
|
+
baseResolvedRef: baseRef,
|
|
1459
1560
|
headRef,
|
|
1460
1561
|
mergeBase,
|
|
1461
1562
|
includesUncommitted: false,
|
|
@@ -1849,6 +1950,90 @@ function resumeFrom(job) {
|
|
|
1849
1950
|
return WORK_PHASES.find((phase) => !job.completed.includes(phase)) ?? "analysing";
|
|
1850
1951
|
}
|
|
1851
1952
|
//#endregion
|
|
1953
|
+
//#region ../../packages/review-harness/src/workspace/request_branch.ts
|
|
1954
|
+
/**
|
|
1955
|
+
* Where the change under review is now.
|
|
1956
|
+
*
|
|
1957
|
+
* One question, asked by two things that must never answer it differently. `review_update.ts` asks
|
|
1958
|
+
* it to know what to reconcile against; `freshness.ts` asks it to know whether the findings still
|
|
1959
|
+
* describe the change. When those two disagree the product contradicts itself on one screen: the
|
|
1960
|
+
* strip says the code was rewritten and the button says the review is already current, and the
|
|
1961
|
+
* reviewer has no way to tell which of them is lying.
|
|
1962
|
+
*
|
|
1963
|
+
* The refs are tried in the order that best answers it, and the first that resolves wins:
|
|
1964
|
+
*
|
|
1965
|
+
* 1. `refs/remotes/origin/<the request's branch>` — the pull request's own branch, as the forge
|
|
1966
|
+
* named it. A plain `git fetch` writes this for every branch on origin, and the review path
|
|
1967
|
+
* already ran one to get the head commit here at all.
|
|
1968
|
+
* 2. `refs/remotes/<the request's branch>` — the same thing on a remote that is not called origin.
|
|
1969
|
+
* 3. `<the review's branch>@{upstream}` — whatever the local branch tracks, resolved without
|
|
1970
|
+
* reading HEAD, which matters because nothing here is entitled to care what is checked out.
|
|
1971
|
+
*
|
|
1972
|
+
* Nothing is guessed past that. A pull request from a fork has its branch on a remote this
|
|
1973
|
+
* repository does not have, and the honest answer there is that we cannot see where the change is,
|
|
1974
|
+
* not a comparison against some other branch that happens to be at hand.
|
|
1975
|
+
*
|
|
1976
|
+
* Read-only, and it has to stay that way: `freshness.ts` may reach nothing that writes or fetches,
|
|
1977
|
+
* and `freshness_read_only.test.ts` scans this file for the same reason it scans that one.
|
|
1978
|
+
*/
|
|
1979
|
+
function requestBranchRefs(branch, remoteHeadRef) {
|
|
1980
|
+
return remoteHeadRef ? [
|
|
1981
|
+
`refs/remotes/origin/${remoteHeadRef}`,
|
|
1982
|
+
`refs/remotes/${remoteHeadRef}`,
|
|
1983
|
+
`${branch}@{upstream}`
|
|
1984
|
+
] : [`${branch}@{upstream}`];
|
|
1985
|
+
}
|
|
1986
|
+
async function resolveRequestBranch(repoPath, branch, remoteHeadRef) {
|
|
1987
|
+
for (const ref of requestBranchRefs(branch, remoteHeadRef)) {
|
|
1988
|
+
const sha = await refSha(repoPath, ref);
|
|
1989
|
+
if (sha) return {
|
|
1990
|
+
sha,
|
|
1991
|
+
ref: await shortName(repoPath, ref) ?? ref,
|
|
1992
|
+
remote: true
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
return null;
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* The name to show for a ref that has already resolved.
|
|
1999
|
+
*
|
|
2000
|
+
* Asked of git rather than trimmed off the string, because the two candidate forms need opposite
|
|
2001
|
+
* treatments and only git knows the answer to the second: `refs/remotes/origin/x` is `origin/x` by
|
|
2002
|
+
* removing a prefix, and `x@{upstream}` is also `origin/x`, which no amount of trimming produces.
|
|
2003
|
+
*/
|
|
2004
|
+
async function shortName(repoPath, ref) {
|
|
2005
|
+
try {
|
|
2006
|
+
return (await git(repoPath, [
|
|
2007
|
+
"rev-parse",
|
|
2008
|
+
"--abbrev-ref",
|
|
2009
|
+
ref
|
|
2010
|
+
])).trim() || null;
|
|
2011
|
+
} catch {
|
|
2012
|
+
return null;
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
/** A local branch by its full path, so a ref that is really a sha cannot be mistaken for one. */
|
|
2016
|
+
async function localBranch(repoPath, branch) {
|
|
2017
|
+
const sha = await refSha(repoPath, `refs/heads/${branch}`);
|
|
2018
|
+
return sha ? {
|
|
2019
|
+
sha,
|
|
2020
|
+
ref: branch,
|
|
2021
|
+
remote: false
|
|
2022
|
+
} : null;
|
|
2023
|
+
}
|
|
2024
|
+
async function refSha(repoPath, ref) {
|
|
2025
|
+
try {
|
|
2026
|
+
return (await git(repoPath, [
|
|
2027
|
+
"rev-parse",
|
|
2028
|
+
"--verify",
|
|
2029
|
+
"--quiet",
|
|
2030
|
+
`${ref}^{commit}`
|
|
2031
|
+
])).trim() || null;
|
|
2032
|
+
} catch {
|
|
2033
|
+
return null;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
//#endregion
|
|
1852
2037
|
//#region ../../packages/review-harness/src/workspace/freshness.ts
|
|
1853
2038
|
/**
|
|
1854
2039
|
* Is this review still describing the code in front of you?
|
|
@@ -1862,6 +2047,10 @@ function resumeFrom(job) {
|
|
|
1862
2047
|
* come from three different places:
|
|
1863
2048
|
*
|
|
1864
2049
|
* 1. **Has the code moved?** Local git against the commit the review was made from. Free.
|
|
2050
|
+
* What it is compared *to* is the branch this review is of, never whatever happens to be
|
|
2051
|
+
* checked out. A pull request is reviewed in a checkout of its own and the reviewer never
|
|
2052
|
+
* stands on its branch, so HEAD is a different change entirely: measuring against it announced
|
|
2053
|
+
* "Code rewritten, 343 files differ" about a review that was perfectly current.
|
|
1865
2054
|
* 2. **Is the checkout what the pull request had?** The stored head sha against local HEAD.
|
|
1866
2055
|
* Answerable offline precisely because the sha was written down at import time.
|
|
1867
2056
|
* 3. **Has anyone commented since?** Not answerable here at all. The daemon reaches no forge,
|
|
@@ -1891,7 +2080,7 @@ async function checkFreshness(workspace) {
|
|
|
1891
2080
|
} catch {}
|
|
1892
2081
|
return {
|
|
1893
2082
|
checkedAt: Date.now(),
|
|
1894
|
-
code: await codeFreshness(workspace, reviewedSha, headSha),
|
|
2083
|
+
code: await codeFreshness(workspace, reviewedSha, headSha, await latest(workspace)),
|
|
1895
2084
|
checkout: await checkoutFreshness(workspace, headSha),
|
|
1896
2085
|
conversation: workspace.prContext ? {
|
|
1897
2086
|
attached: true,
|
|
@@ -1900,17 +2089,45 @@ async function checkFreshness(workspace) {
|
|
|
1900
2089
|
} : { attached: false }
|
|
1901
2090
|
};
|
|
1902
2091
|
}
|
|
1903
|
-
|
|
2092
|
+
/**
|
|
2093
|
+
* The branch this review is of, and where it currently is.
|
|
2094
|
+
*
|
|
2095
|
+
* The comparison the whole panel rests on, and the one that used to read `git rev-parse HEAD`. That
|
|
2096
|
+
* was right while every review was of the checkout you were standing in. It stopped being right the
|
|
2097
|
+
* day pull requests were reviewed in a worktree of their own: the reviewer never checks that branch
|
|
2098
|
+
* out, so HEAD is some unrelated branch and the distance to it is not a fact about this review.
|
|
2099
|
+
*
|
|
2100
|
+
* A pull request is measured against its own branch as this repository last saw it. A branch review
|
|
2101
|
+
* is measured against that branch by name, `refs/heads/<name>` rather than HEAD, so switching
|
|
2102
|
+
* branches to look at something else does not make the review look rewritten. Neither reads HEAD,
|
|
2103
|
+
* and that is deliberate: what is checked out is a fact about the reviewer, not about the change.
|
|
2104
|
+
*
|
|
2105
|
+
* Null when the branch is not in this repository at all, which is a real answer with two ordinary
|
|
2106
|
+
* causes: a pull request from a fork, whose branch lives on a remote this clone does not have, and
|
|
2107
|
+
* a branch deleted after it merged. Neither is a licence to compare against something else.
|
|
2108
|
+
*/
|
|
2109
|
+
async function latest(workspace) {
|
|
2110
|
+
const branch = workspace.changeSet.headRef;
|
|
2111
|
+
const requestRef = workspace.prContext?.pullRequest.headRef;
|
|
2112
|
+
if (requestRef) return resolveRequestBranch(workspace.repoPath, branch, requestRef);
|
|
2113
|
+
return await localBranch(workspace.repoPath, branch) ?? resolveRequestBranch(workspace.repoPath, branch);
|
|
2114
|
+
}
|
|
2115
|
+
async function codeFreshness(workspace, reviewedSha, readable, latest) {
|
|
2116
|
+
const latestSha = latest?.sha ?? null;
|
|
1904
2117
|
const base = {
|
|
1905
2118
|
state: "unknown",
|
|
1906
2119
|
reviewedSha,
|
|
1907
|
-
|
|
2120
|
+
latestSha,
|
|
2121
|
+
comparison: latest ? {
|
|
2122
|
+
ref: latest.ref,
|
|
2123
|
+
remote: latest.remote
|
|
2124
|
+
} : null,
|
|
1908
2125
|
newCommits: null,
|
|
1909
2126
|
changedFiles: null,
|
|
1910
2127
|
rewritten: false,
|
|
1911
2128
|
reason: null
|
|
1912
2129
|
};
|
|
1913
|
-
if (!
|
|
2130
|
+
if (!readable || !reviewedSha) return {
|
|
1914
2131
|
...base,
|
|
1915
2132
|
reason: "git-failed"
|
|
1916
2133
|
};
|
|
@@ -1919,14 +2136,18 @@ async function codeFreshness(workspace, reviewedSha, headSha) {
|
|
|
1919
2136
|
...base,
|
|
1920
2137
|
reason: "missing-commit"
|
|
1921
2138
|
};
|
|
1922
|
-
if (
|
|
1923
|
-
|
|
2139
|
+
if (!latestSha) return {
|
|
2140
|
+
...base,
|
|
2141
|
+
reason: "no-branch"
|
|
2142
|
+
};
|
|
2143
|
+
if (latestSha !== reviewedSha) {
|
|
2144
|
+
const rewritten = !await isAncestor(repoPath, reviewedSha, latestSha);
|
|
1924
2145
|
return {
|
|
1925
2146
|
...base,
|
|
1926
2147
|
state: "changed",
|
|
1927
2148
|
rewritten,
|
|
1928
|
-
newCommits: rewritten ? null : await countCommits(repoPath, reviewedSha,
|
|
1929
|
-
changedFiles: await countChangedFiles(repoPath, reviewedSha,
|
|
2149
|
+
newCommits: rewritten ? null : await countCommits(repoPath, reviewedSha, latestSha),
|
|
2150
|
+
changedFiles: await countChangedFiles(repoPath, reviewedSha, latestSha)
|
|
1930
2151
|
};
|
|
1931
2152
|
}
|
|
1932
2153
|
if (workspace.changeSet.includesUncommitted) return {
|
|
@@ -5154,113 +5375,6 @@ async function applyGuide(workspaceId, payload) {
|
|
|
5154
5375
|
});
|
|
5155
5376
|
}
|
|
5156
5377
|
//#endregion
|
|
5157
|
-
//#region ../../packages/review-harness/src/workspace/landing.ts
|
|
5158
|
-
/**
|
|
5159
|
-
* Long enough for a loopback round trip many times over. Short because nothing rests on it: when
|
|
5160
|
-
* it expires the reader gets a link rather than a verdict.
|
|
5161
|
-
*/
|
|
5162
|
-
var PROBE_TIMEOUT_MS = 1500;
|
|
5163
|
-
/**
|
|
5164
|
-
* The landing page, as a string.
|
|
5165
|
-
*
|
|
5166
|
-
* Pure, and separated from writing it for the usual reason: what this page says when the server is
|
|
5167
|
-
* down is the half that is hard to look at by hand, because reproducing it means killing a daemon.
|
|
5168
|
-
*/
|
|
5169
|
-
/**
|
|
5170
|
-
* A record with no token renders as stopped, and that is not a lie anyone can be told: nothing
|
|
5171
|
-
* writes such a record. `cli/server.ts` mints a token before it listens, and `healthyPort` stops a
|
|
5172
|
-
* server whose version does not match this build, which is the only way one could ever be seen.
|
|
5173
|
-
* The case is here because the field is optional in the type, not because it is a state.
|
|
5174
|
-
*/
|
|
5175
|
-
function renderLanding(record) {
|
|
5176
|
-
const url = record?.token ? indexUrl(record) : null;
|
|
5177
|
-
return url ? running(record, url) : stopped();
|
|
5178
|
-
}
|
|
5179
|
-
function indexUrl(record) {
|
|
5180
|
-
return `http://127.0.0.1:${record.port}/?t=${encodeURIComponent(record.token ?? "")}`;
|
|
5181
|
-
}
|
|
5182
|
-
function running(record, url) {
|
|
5183
|
-
return page("Opening your reviews", `<h1>Opening your reviews</h1>
|
|
5184
|
-
<p class="prb-quiet" id="waiting">Looking for the workspace server on port ${record.port}.</p>
|
|
5185
|
-
<div id="gone" hidden>
|
|
5186
|
-
<p><a class="prb-go" href="${escapeHtml(url)}">Open your reviews on port ${record.port}</a></p>
|
|
5187
|
-
<p>This page could not confirm the server was answering, so it has not sent you there on its
|
|
5188
|
-
own. Follow the link and see.</p>
|
|
5189
|
-
<p>If it does not open, the server has stopped since this page was written. Nothing is lost:
|
|
5190
|
-
every review is on disk in <code>~/.prreviewbuddy/workspaces</code>. Run
|
|
5191
|
-
<code>/prreviewbuddy:start-server</code> in Claude Code, and this page will point at the new
|
|
5192
|
-
server by the time you reload it.</p>
|
|
5193
|
-
</div>
|
|
5194
|
-
<script>
|
|
5195
|
-
// An image, not a fetch: a file:// page cannot read a cross-origin response, but it can be
|
|
5196
|
-
// told whether one arrived. /assets is the only route that answers without a credential.
|
|
5197
|
-
var settled = false;
|
|
5198
|
-
var probe = new Image();
|
|
5199
|
-
var give_up = setTimeout(function () { fail(); }, ${PROBE_TIMEOUT_MS});
|
|
5200
|
-
function fail() {
|
|
5201
|
-
if (settled) return;
|
|
5202
|
-
settled = true;
|
|
5203
|
-
clearTimeout(give_up);
|
|
5204
|
-
document.getElementById('waiting').hidden = true;
|
|
5205
|
-
document.getElementById('gone').hidden = false;
|
|
5206
|
-
}
|
|
5207
|
-
probe.onload = function () {
|
|
5208
|
-
if (settled) return;
|
|
5209
|
-
settled = true;
|
|
5210
|
-
clearTimeout(give_up);
|
|
5211
|
-
// replace, not assign: going back should leave the reviews, not bounce through here again.
|
|
5212
|
-
window.location.replace(${JSON.stringify(url)});
|
|
5213
|
-
};
|
|
5214
|
-
probe.onerror = fail;
|
|
5215
|
-
probe.src = ${JSON.stringify(`http://127.0.0.1:${record.port}/assets/icon128.png?probe=`)} + Date.now();
|
|
5216
|
-
<\/script>`);
|
|
5217
|
-
}
|
|
5218
|
-
function stopped() {
|
|
5219
|
-
return page("Your reviews", `<h1>The workspace server is not running</h1>
|
|
5220
|
-
<p>Every review is still on disk in <code>~/.prreviewbuddy/workspaces</code>, with its guide,
|
|
5221
|
-
its diff and its conversation.</p>
|
|
5222
|
-
<p>Run <code>/prreviewbuddy:start-server</code> in Claude Code to serve them again. This page
|
|
5223
|
-
is rewritten when it starts, so keep the bookmark and reload it then.</p>`);
|
|
5224
|
-
}
|
|
5225
|
-
function page(title, body) {
|
|
5226
|
-
return `<!doctype html>
|
|
5227
|
-
<html lang="en">
|
|
5228
|
-
<head>
|
|
5229
|
-
<meta charset="utf-8">
|
|
5230
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
5231
|
-
<title>${escapeHtml(title)} · PR Review Buddy</title>
|
|
5232
|
-
<style>
|
|
5233
|
-
:root { color-scheme: light dark; --prb-ink: #1b1b1f; --prb-paper: #fbfbfd; --prb-quiet: #5c5c66; }
|
|
5234
|
-
@media (prefers-color-scheme: dark) {
|
|
5235
|
-
:root { --prb-ink: #e8e8ec; --prb-paper: #17171b; --prb-quiet: #9a9aa4; }
|
|
5236
|
-
}
|
|
5237
|
-
body {
|
|
5238
|
-
margin: 0; min-height: 100vh; display: grid; place-items: center;
|
|
5239
|
-
background: var(--prb-paper); color: var(--prb-ink);
|
|
5240
|
-
font: 15px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
5241
|
-
}
|
|
5242
|
-
main { max-width: 34rem; padding: 2rem; }
|
|
5243
|
-
h1 { font-size: 1.25rem; margin: 0 0 0.75rem; }
|
|
5244
|
-
p { margin: 0 0 0.75rem; }
|
|
5245
|
-
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9em; }
|
|
5246
|
-
.prb-quiet { color: var(--prb-quiet); }
|
|
5247
|
-
.prb-go { font-weight: 600; }
|
|
5248
|
-
[hidden] { display: none; }
|
|
5249
|
-
</style>
|
|
5250
|
-
</head>
|
|
5251
|
-
<body><main>${body}</main></body>
|
|
5252
|
-
</html>
|
|
5253
|
-
`;
|
|
5254
|
-
}
|
|
5255
|
-
/**
|
|
5256
|
-
* The token is minted from `base64url` and cannot contain any of these, and it is escaped anyway.
|
|
5257
|
-
* A credential is the last value in this product to be interpolated on the strength of what it is
|
|
5258
|
-
* currently generated from.
|
|
5259
|
-
*/
|
|
5260
|
-
function escapeHtml(value) {
|
|
5261
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
5262
|
-
}
|
|
5263
|
-
//#endregion
|
|
5264
5378
|
//#region ../../packages/review-harness/src/domain/feedback/feedback_url.ts
|
|
5265
5379
|
/** How each client names itself in the prefilled message, which is the half a screenshot carries. */
|
|
5266
5380
|
var CLIENT_LABELS = {
|
|
@@ -5400,16 +5514,188 @@ function feedbackUrl() {
|
|
|
5400
5514
|
});
|
|
5401
5515
|
}
|
|
5402
5516
|
//#endregion
|
|
5517
|
+
//#region ../../packages/review-harness/src/workspace/way_back.ts
|
|
5518
|
+
/**
|
|
5519
|
+
* How a reviewer gets back to the list of their reviews.
|
|
5520
|
+
*
|
|
5521
|
+
* Three places have to answer this, and until now all three answered it for the plugin: the
|
|
5522
|
+
* refusal at `/`, and both states of the bookmark page. A reviewer who installed the CLI was told
|
|
5523
|
+
* to run `/prreviewbuddy:start-server`, which is a Claude Code slash command they do not have,
|
|
5524
|
+
* from a product that has never mentioned Claude Code to them. Every one of those readers is
|
|
5525
|
+
* already stuck, which is the worst moment to be handed a command that does not exist.
|
|
5526
|
+
*
|
|
5527
|
+
* The build knows which product it is. `SURFACE.client` is read from the `package.json` beside the
|
|
5528
|
+
* running code, and each app bundles its own copy of the workspace server, so the server that
|
|
5529
|
+
* refuses the request is the CLI's server or the plugin's server and can say so. A build that
|
|
5530
|
+
* cannot name itself names both, which is never wrong and is only slightly longer.
|
|
5531
|
+
*
|
|
5532
|
+
* The bookmark is here too, because it is the other half of the same answer and the durable half:
|
|
5533
|
+
* the command is what unsticks this browser now, and the bookmark is what stops the question being
|
|
5534
|
+
* asked again after the next restart.
|
|
5535
|
+
*/
|
|
5536
|
+
/**
|
|
5537
|
+
* The one address in this product that never changes.
|
|
5538
|
+
*
|
|
5539
|
+
* Rewritten by `daemon.ts` at both moments the truth changes. It lives here rather than there
|
|
5540
|
+
* because the pages that tell a reviewer about it must not have to import the daemon to do it.
|
|
5541
|
+
*/
|
|
5542
|
+
var LANDING_PAGE = join(STORE_ROOT, "reviews.html");
|
|
5543
|
+
var CLI = {
|
|
5544
|
+
command: "prreviewbuddy open",
|
|
5545
|
+
where: "your terminal"
|
|
5546
|
+
};
|
|
5547
|
+
var PLUGIN = {
|
|
5548
|
+
command: "/prreviewbuddy:start-server",
|
|
5549
|
+
where: "Claude Code"
|
|
5550
|
+
};
|
|
5551
|
+
function wayBack() {
|
|
5552
|
+
return {
|
|
5553
|
+
commands: SURFACE.client === "cli" ? [CLI] : SURFACE.client === "plugin" ? [PLUGIN] : [CLI, PLUGIN],
|
|
5554
|
+
bookmark: LANDING_PAGE
|
|
5555
|
+
};
|
|
5556
|
+
}
|
|
5557
|
+
/** The commands as one sentence, for somewhere with no room to make a list of them. */
|
|
5558
|
+
function wayBackSentence(way = wayBack()) {
|
|
5559
|
+
return way.commands.map(({ command, where }) => `${command} in ${where}`).join(", or ");
|
|
5560
|
+
}
|
|
5561
|
+
//#endregion
|
|
5562
|
+
//#region ../../packages/review-harness/src/workspace/landing.ts
|
|
5563
|
+
/**
|
|
5564
|
+
* Long enough for a loopback round trip many times over. Short because nothing rests on it: when
|
|
5565
|
+
* it expires the reader gets a link rather than a verdict.
|
|
5566
|
+
*/
|
|
5567
|
+
var PROBE_TIMEOUT_MS = 1500;
|
|
5568
|
+
/**
|
|
5569
|
+
* The landing page, as a string.
|
|
5570
|
+
*
|
|
5571
|
+
* Pure, and separated from writing it for the usual reason: what this page says when the server is
|
|
5572
|
+
* down is the half that is hard to look at by hand, because reproducing it means killing a daemon.
|
|
5573
|
+
*/
|
|
5574
|
+
/**
|
|
5575
|
+
* A record with no token renders as stopped, and that is not a lie anyone can be told: nothing
|
|
5576
|
+
* writes such a record. `cli/server.ts` mints a token before it listens, and `healthyPort` stops a
|
|
5577
|
+
* server whose version does not match this build, which is the only way one could ever be seen.
|
|
5578
|
+
* The case is here because the field is optional in the type, not because it is a state.
|
|
5579
|
+
*/
|
|
5580
|
+
function renderLanding(record) {
|
|
5581
|
+
const url = record?.token ? indexUrl(record) : null;
|
|
5582
|
+
return url ? running(record, url) : stopped();
|
|
5583
|
+
}
|
|
5584
|
+
function indexUrl(record) {
|
|
5585
|
+
return `http://127.0.0.1:${record.port}/?t=${encodeURIComponent(record.token ?? "")}`;
|
|
5586
|
+
}
|
|
5587
|
+
function running(record, url) {
|
|
5588
|
+
return page("Opening your reviews", `<h1>Opening your reviews</h1>
|
|
5589
|
+
<p class="prb-quiet" id="waiting">Looking for the workspace server on port ${record.port}.</p>
|
|
5590
|
+
<div id="gone" hidden>
|
|
5591
|
+
<p><a class="prb-go" href="${escapeHtml(url)}">Open your reviews on port ${record.port}</a></p>
|
|
5592
|
+
<p>This page could not confirm the server was answering, so it has not sent you there on its
|
|
5593
|
+
own. Follow the link and see.</p>
|
|
5594
|
+
<p>If it does not open, the server has stopped since this page was written. Nothing is lost:
|
|
5595
|
+
every review is on disk in <code>~/.prreviewbuddy/workspaces</code>. Start it again and this
|
|
5596
|
+
page will point at the new server by the time you reload it.</p>
|
|
5597
|
+
${commandList(wayBack())}
|
|
5598
|
+
</div>
|
|
5599
|
+
<script>
|
|
5600
|
+
// An image, not a fetch: a file:// page cannot read a cross-origin response, but it can be
|
|
5601
|
+
// told whether one arrived. /assets is the only route that answers without a credential.
|
|
5602
|
+
var settled = false;
|
|
5603
|
+
var probe = new Image();
|
|
5604
|
+
var give_up = setTimeout(function () { fail(); }, ${PROBE_TIMEOUT_MS});
|
|
5605
|
+
function fail() {
|
|
5606
|
+
if (settled) return;
|
|
5607
|
+
settled = true;
|
|
5608
|
+
clearTimeout(give_up);
|
|
5609
|
+
document.getElementById('waiting').hidden = true;
|
|
5610
|
+
document.getElementById('gone').hidden = false;
|
|
5611
|
+
}
|
|
5612
|
+
probe.onload = function () {
|
|
5613
|
+
if (settled) return;
|
|
5614
|
+
settled = true;
|
|
5615
|
+
clearTimeout(give_up);
|
|
5616
|
+
// replace, not assign: going back should leave the reviews, not bounce through here again.
|
|
5617
|
+
window.location.replace(${JSON.stringify(url)});
|
|
5618
|
+
};
|
|
5619
|
+
probe.onerror = fail;
|
|
5620
|
+
probe.src = ${JSON.stringify(`http://127.0.0.1:${record.port}/assets/icon128.png?probe=`)} + Date.now();
|
|
5621
|
+
<\/script>`);
|
|
5622
|
+
}
|
|
5623
|
+
function stopped() {
|
|
5624
|
+
return page("Your reviews", `<h1>The workspace server is not running</h1>
|
|
5625
|
+
<p>Every review is still on disk in <code>~/.prreviewbuddy/workspaces</code>, with its guide,
|
|
5626
|
+
its diff and its conversation.</p>
|
|
5627
|
+
<p>Serve them again with:</p>
|
|
5628
|
+
${commandList(wayBack())}
|
|
5629
|
+
<p>This page is rewritten when the server starts, so keep the bookmark and reload it then.</p>`);
|
|
5630
|
+
}
|
|
5631
|
+
/**
|
|
5632
|
+
* The commands that start a server, named for the product the reader actually installed.
|
|
5633
|
+
*
|
|
5634
|
+
* A list rather than a sentence because a build that cannot name itself offers both, and two
|
|
5635
|
+
* commands run together in prose read as one command with a typo in it.
|
|
5636
|
+
*/
|
|
5637
|
+
function commandList(way) {
|
|
5638
|
+
return `<ul class="prb-commands">${way.commands.map(({ command, where }) => `<li><code>${escapeHtml(command)}</code> in ${escapeHtml(where)}</li>`).join("")}</ul>`;
|
|
5639
|
+
}
|
|
5640
|
+
/**
|
|
5641
|
+
* The shell both of this file's states use, and the refusal at `/` uses too.
|
|
5642
|
+
*
|
|
5643
|
+
* Exported so that the three pages a stuck reviewer can land on look like one product rather than
|
|
5644
|
+
* like three accidents. It carries no script and no asset request, which is what lets it render
|
|
5645
|
+
* identically from `file://` and from the server.
|
|
5646
|
+
*/
|
|
5647
|
+
function page(title, body) {
|
|
5648
|
+
return `<!doctype html>
|
|
5649
|
+
<html lang="en">
|
|
5650
|
+
<head>
|
|
5651
|
+
<meta charset="utf-8">
|
|
5652
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
5653
|
+
<title>${escapeHtml(title)} · PR Review Buddy</title>
|
|
5654
|
+
<style>
|
|
5655
|
+
:root { color-scheme: light dark; --prb-ink: #1b1b1f; --prb-paper: #fbfbfd; --prb-quiet: #5c5c66; }
|
|
5656
|
+
@media (prefers-color-scheme: dark) {
|
|
5657
|
+
:root { --prb-ink: #e8e8ec; --prb-paper: #17171b; --prb-quiet: #9a9aa4; }
|
|
5658
|
+
}
|
|
5659
|
+
body {
|
|
5660
|
+
margin: 0; min-height: 100vh; display: grid; place-items: center;
|
|
5661
|
+
background: var(--prb-paper); color: var(--prb-ink);
|
|
5662
|
+
font: 15px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
5663
|
+
}
|
|
5664
|
+
main { max-width: 34rem; padding: 2rem; }
|
|
5665
|
+
h1 { font-size: 1.25rem; margin: 0 0 0.75rem; }
|
|
5666
|
+
p { margin: 0 0 0.75rem; }
|
|
5667
|
+
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9em; }
|
|
5668
|
+
.prb-quiet { color: var(--prb-quiet); }
|
|
5669
|
+
.prb-go { font-weight: 600; }
|
|
5670
|
+
.prb-commands { margin: 0 0 0.75rem; padding-left: 1.25rem; }
|
|
5671
|
+
.prb-commands li { margin: 0 0 0.25rem; }
|
|
5672
|
+
[hidden] { display: none; }
|
|
5673
|
+
</style>
|
|
5674
|
+
</head>
|
|
5675
|
+
<body><main>${body}</main></body>
|
|
5676
|
+
</html>
|
|
5677
|
+
`;
|
|
5678
|
+
}
|
|
5679
|
+
/**
|
|
5680
|
+
* The token is minted from `base64url` and cannot contain any of these, and it is escaped anyway.
|
|
5681
|
+
* A credential is the last value in this product to be interpolated on the strength of what it is
|
|
5682
|
+
* currently generated from.
|
|
5683
|
+
*/
|
|
5684
|
+
function escapeHtml(value) {
|
|
5685
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
5686
|
+
}
|
|
5687
|
+
//#endregion
|
|
5403
5688
|
//#region ../../packages/review-harness/src/workspace/daemon.ts
|
|
5404
|
-
var SERVER_RECORD = join(STORE_ROOT, "server.json");
|
|
5405
|
-
var SPAWN_LOCK = join(STORE_ROOT, "server.lock");
|
|
5406
5689
|
/**
|
|
5407
5690
|
* The one address in this product that never changes.
|
|
5408
5691
|
*
|
|
5409
|
-
*
|
|
5692
|
+
* Declared in `way_back.ts` with the rest of the answer to "how do I get back to my reviews", and
|
|
5693
|
+
* re-exported here because this is where callers already look for it. Rewritten by this file at
|
|
5694
|
+
* both moments the truth changes, which is why the writing is here rather than anywhere that
|
|
5410
5695
|
* merely knows the port: this file is where a server starts existing and stops existing.
|
|
5411
5696
|
*/
|
|
5412
|
-
var
|
|
5697
|
+
var SERVER_RECORD = join(STORE_ROOT, "server.json");
|
|
5698
|
+
var SPAWN_LOCK = join(STORE_ROOT, "server.lock");
|
|
5413
5699
|
var SERVER_ENTRY = process.env.PRB_SERVER_ENTRY ?? fileURLToPath(new URL("./server.js", import.meta.url));
|
|
5414
5700
|
var STARTUP_TIMEOUT_MS = 15e3;
|
|
5415
5701
|
var POLL_INTERVAL_MS = 100;
|
|
@@ -5613,6 +5899,29 @@ async function waitForHealthyServer() {
|
|
|
5613
5899
|
function workspaceUrl(port, id, token) {
|
|
5614
5900
|
return `http://127.0.0.1:${port}/r/${id}?t=${token}`;
|
|
5615
5901
|
}
|
|
5902
|
+
/**
|
|
5903
|
+
* A workspace link that also teaches the browser its way back to the list.
|
|
5904
|
+
*
|
|
5905
|
+
* The reviews index is opened by a credential no review page is allowed to carry, and the only
|
|
5906
|
+
* thing that puts that credential into a browser is arriving at the index with it once. Nothing on
|
|
5907
|
+
* the `prreviewbuddy review` path ever sent anyone there, so "All reviews" on the page it printed
|
|
5908
|
+
* refused every reviewer who had never separately run `prreviewbuddy open`. The bootstrap belongs
|
|
5909
|
+
* here, in the link, rather than as a step for the reviewer to discover after being refused.
|
|
5910
|
+
*
|
|
5911
|
+
* It goes through the index because that is the route that sets the cookie, and it carries `go` so
|
|
5912
|
+
* that the browser does not stop there: cookie set, then straight on to the review, leaving
|
|
5913
|
+
* `/r/<id>?t=<reviewToken>` in the address bar. The page still never receives the index credential,
|
|
5914
|
+
* which is the boundary that matters. What is in the address bar afterwards is safe to copy.
|
|
5915
|
+
*
|
|
5916
|
+
* Printing this rather than the plain link costs nothing when the credential is missing or stale:
|
|
5917
|
+
* `handleIndex` forwards to `go` either way, and only the cookie depends on the credential being
|
|
5918
|
+
* right. The worst case is exactly the link this used to print.
|
|
5919
|
+
*/
|
|
5920
|
+
function bootstrapUrl(port, id, token, indexToken) {
|
|
5921
|
+
const workspace = `/r/${id}?t=${encodeURIComponent(token)}`;
|
|
5922
|
+
if (!indexToken) return `http://127.0.0.1:${port}${workspace}`;
|
|
5923
|
+
return `http://127.0.0.1:${port}/?t=${encodeURIComponent(indexToken)}&go=${encodeURIComponent(workspace)}`;
|
|
5924
|
+
}
|
|
5616
5925
|
/** The reviews index. Null when the running server predates the index and has no token. */
|
|
5617
5926
|
function reviewsUrl(port, token) {
|
|
5618
5927
|
return token ? `http://127.0.0.1:${port}/?t=${token}` : null;
|
|
@@ -5984,10 +6293,6 @@ function countByStatus(issues) {
|
|
|
5984
6293
|
* same policy as every other managed directory and a crash between creating one and recording it
|
|
5985
6294
|
* leaves residue the orphan rule can still reach.
|
|
5986
6295
|
*/
|
|
5987
|
-
/** The commit a review was made from, however the record spells it. Null when it recorded none. */
|
|
5988
|
-
function reviewedCommit(workspace) {
|
|
5989
|
-
return workspace.session.metadata?.commitSha || workspace.changeSet.data.pullRequest.head.sha || null;
|
|
5990
|
-
}
|
|
5991
6296
|
var NoCheckoutError = class extends Error {};
|
|
5992
6297
|
/**
|
|
5993
6298
|
* A worktree detached at `sha`, and the job that owns it.
|
|
@@ -6360,31 +6665,21 @@ async function reconcile(workspace, runners, reviewedSha, remote, conversationOk
|
|
|
6360
6665
|
}
|
|
6361
6666
|
var BASELINE_GONE_MESSAGE = "The commit this review was made from is no longer in this repository, so there is nothing to measure the new code against. This one needs starting fresh: run /prreviewbuddy:fresh-review.";
|
|
6362
6667
|
/**
|
|
6363
|
-
*
|
|
6668
|
+
* Where the request's branch is, after a fetch.
|
|
6669
|
+
*
|
|
6670
|
+
* The candidates live in `request_branch.ts` rather than here because the freshness strip has to
|
|
6671
|
+
* ask the same question and get the same answer. Two answers means the panel says the code was
|
|
6672
|
+
* rewritten while this button says the review is already current, on one screen, with nothing to
|
|
6673
|
+
* tell the reviewer which is right.
|
|
6364
6674
|
*
|
|
6365
|
-
*
|
|
6366
|
-
*
|
|
6367
|
-
*
|
|
6675
|
+
* The fetch is the only difference between the two callers, and it is the honest one: this button
|
|
6676
|
+
* is allowed to reach the network and that panel is not, so it looks at a newer copy of the same
|
|
6677
|
+
* refs.
|
|
6368
6678
|
*/
|
|
6369
|
-
function candidateRefs(branch, remoteHeadRef) {
|
|
6370
|
-
return remoteHeadRef ? [
|
|
6371
|
-
`refs/remotes/origin/${remoteHeadRef}`,
|
|
6372
|
-
`refs/remotes/${remoteHeadRef}`,
|
|
6373
|
-
`${branch}@{upstream}`
|
|
6374
|
-
] : [`${branch}@{upstream}`];
|
|
6375
|
-
}
|
|
6376
6679
|
async function remoteHead(workspace) {
|
|
6377
6680
|
const fetchFailed = !await fetchOrigin(workspace.repoPath);
|
|
6378
|
-
const refs = candidateRefs(workspace.changeSet.headRef, workspace.prContext?.pullRequest.headRef);
|
|
6379
|
-
for (const ref of refs) {
|
|
6380
|
-
const sha = await refSha(workspace.repoPath, ref);
|
|
6381
|
-
if (sha) return {
|
|
6382
|
-
sha,
|
|
6383
|
-
fetchFailed
|
|
6384
|
-
};
|
|
6385
|
-
}
|
|
6386
6681
|
return {
|
|
6387
|
-
sha: null,
|
|
6682
|
+
sha: (await resolveRequestBranch(workspace.repoPath, workspace.changeSet.headRef, workspace.prContext?.pullRequest.headRef))?.sha ?? null,
|
|
6388
6683
|
fetchFailed
|
|
6389
6684
|
};
|
|
6390
6685
|
}
|
|
@@ -6405,18 +6700,6 @@ async function fetchOrigin(repoPath) {
|
|
|
6405
6700
|
return false;
|
|
6406
6701
|
}
|
|
6407
6702
|
}
|
|
6408
|
-
async function refSha(repoPath, ref) {
|
|
6409
|
-
try {
|
|
6410
|
-
return (await git(repoPath, [
|
|
6411
|
-
"rev-parse",
|
|
6412
|
-
"--verify",
|
|
6413
|
-
"--quiet",
|
|
6414
|
-
`${ref}^{commit}`
|
|
6415
|
-
])).trim() || null;
|
|
6416
|
-
} catch {
|
|
6417
|
-
return null;
|
|
6418
|
-
}
|
|
6419
|
-
}
|
|
6420
6703
|
/** Whether the object itself is still in the repository, whatever ref does or does not name it. */
|
|
6421
6704
|
async function commitPresent(repoPath, sha) {
|
|
6422
6705
|
try {
|
|
@@ -6465,7 +6748,7 @@ async function applyUpdate(workspace, result, replaced, newHead, worktreePath) {
|
|
|
6465
6748
|
try {
|
|
6466
6749
|
const plain = await buildReviewContext({
|
|
6467
6750
|
repoPath: worktreePath,
|
|
6468
|
-
base: await resolvableBase(worktreePath, workspace.changeSet
|
|
6751
|
+
base: await resolvableBase(worktreePath, workspace.changeSet)
|
|
6469
6752
|
});
|
|
6470
6753
|
whole = workspace.prContext ? await withPrContext(plain, workspace.prContext) : plain;
|
|
6471
6754
|
workspace.changeSet = whole.changeSet;
|
|
@@ -6476,15 +6759,32 @@ async function applyUpdate(workspace, result, replaced, newHead, worktreePath) {
|
|
|
6476
6759
|
delete workspace.dismissedFreshnessSha;
|
|
6477
6760
|
}
|
|
6478
6761
|
/**
|
|
6479
|
-
* The
|
|
6762
|
+
* The base this review was made against, turned back into something git will accept.
|
|
6763
|
+
*
|
|
6764
|
+
* `ChangeSet.baseResolvedRef` is the ref git actually resolved when the review was built, so a
|
|
6765
|
+
* review that has one needs no reasoning at all: it is the same comparison or it is nothing.
|
|
6480
6766
|
*
|
|
6481
|
-
*
|
|
6482
|
-
*
|
|
6483
|
-
*
|
|
6484
|
-
*
|
|
6767
|
+
* Everything else is a review made before that field existed, and all such a review kept is
|
|
6768
|
+
* `baseRef`, which is a *display* string: `origin/main` is stored as `main`, because that is what
|
|
6769
|
+
* a reviewer calls it. Both candidates have to be tried, and the order matters more than it looks.
|
|
6770
|
+
* A clone made for one branch has no local `main` at all, so the display name resolves to nothing
|
|
6771
|
+
* and `origin/main` is the only answer. An ordinary clone has both, `main` sits behind the remote
|
|
6772
|
+
* from the first day nobody pulls, and picking it silently widens the review to every upstream
|
|
6773
|
+
* commit that has not been fetched into the working branch. So the remote-tracking ref goes first:
|
|
6774
|
+
* it is what `resolveBase` itself prefers when nobody names a base, and no `prreviewbuddy review
|
|
6775
|
+
* <pr>` ever names one.
|
|
6776
|
+
*
|
|
6777
|
+
* Undefined is the last resort and means "work it out again", which is what the change set did in
|
|
6778
|
+
* the first place.
|
|
6485
6779
|
*/
|
|
6486
|
-
async function resolvableBase(repoPath,
|
|
6487
|
-
|
|
6780
|
+
async function resolvableBase(repoPath, changeSet) {
|
|
6781
|
+
const stored = changeSet.baseRef;
|
|
6782
|
+
const candidates = [
|
|
6783
|
+
changeSet.baseResolvedRef,
|
|
6784
|
+
`origin/${stored}`,
|
|
6785
|
+
stored
|
|
6786
|
+
];
|
|
6787
|
+
for (const candidate of candidates) if (candidate && await refExists(repoPath, candidate)) return candidate;
|
|
6488
6788
|
}
|
|
6489
6789
|
/** Null when there is no conversation attached, which is neither success nor failure. */
|
|
6490
6790
|
async function reReadConversation(workspace, fetch) {
|
|
@@ -6664,4 +6964,4 @@ function loadConfig() {
|
|
|
6664
6964
|
return config;
|
|
6665
6965
|
}
|
|
6666
6966
|
//#endregion
|
|
6667
|
-
export {
|
|
6967
|
+
export { touchWorkspace as $, MANAGED_ROOT as A, loadJob as B, runJob as C, fillFileUrlTemplate as D, forgeResolver as E, WORK_PHASES as F, lineagePosition as G, displayRef as H, allJobs as I, recentReviewGroups as J, loadWorkspace as K, deleteJob as L, removeWorktree as M, relativeTime as N, clearClaim as O, checkFreshness as P, summarise as Q, fail as R, feedbackUrl as S, EXPLAIN_SIMPLY_PROMPT as T, STORE_ROOT as U, saveJob as V, groupByLineage as W, reviewedCommit as X, recentWorkspaces as Y, saveWorkspace as Z, page as _, refreshPrContext as a, reviewerDispositions as at, wayBackSentence as b, agentEnvOf as c, resolveTarget as ct, ensureServer as d, AGENT_IDS as dt, doneVerb as et, readServerRecord as f, agentById as ft, commandList as g, AgentUnavailableError as gt, writeServerRecord as h, AgentCancelledError as ht, updateReview as i, questionsOutstanding as it, readMarker as j, isClaimed as k, bootstrapUrl as l, withBase as lt, workspaceUrl as m, defaultAgent as mt, record as n, isQuestionOutstanding as nt, isUnchanged as o, processDiscussion as ot, reviewsUrl as p, agentFor as pt, positionInLineage as q, RefreshUnavailableError as r, issuesOutstanding as rt, askCheckout as s, describeAuthorship as st, readEvents as t, isIssueOutstanding as tt, clearServerRecord as u, git as ut, LANDING_PAGE as v, startJob as w, BUILD_VERSION as x, wayBack as y, isTerminal as z };
|