prreviewbuddy 0.42.0 → 0.44.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/CHANGELOG.md +41 -0
- package/dist/{delete_review-DPRL5IPt.js → delete_review-BWMmO81Q.js} +257 -145
- package/dist/main.js +95 -14
- package/dist/server.js +63 -28
- package/package.json +1 -1
- package/static/shell.css +4 -0
- package/static/sidebar.css +58 -14
- package/static/workspace.js +32 -32
package/dist/main.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as
|
|
2
|
+
import { $ as bootstrapUrl, $t as recentWorkspaces, An as STORE_ROOT, Bt as driverGone, Cn as git, Dn as readAgentPreference, E as telemetryUploadConsent, Fn as detectAgents, G as MANAGED_ROOT, Ht as followedRefName, J as modelHelpLines, Jt as loadWorkspace, K as readMarker, Kt as lineageKeyFor, Lt as progressSteps, Mn as DEFAULT_AGENT_ID, Mt as loadJob, Nn as agentById, On as writeAgentPreference, Ot as allJobs, Pt as PHASES, Q as readIndexToken, Rt as purposeOf, S as startJob, T as recordTelemetryUploadConsent, Tn as resolveModel, Tt as checkReview, Ut as groupByLineage, Vt as isClaimed, Xt as mutateWorkspace, Y as relativeTime, Yt as matchingWorkspaceIds, Z as queueForReview, Zt as positionInLineage, _t as statedWorkspacePort, a as wasBlocked, b as reanalyseReview, bn as resolveTarget, bt as forgeResolver, ft as DEFAULT_WORKSPACE_PORT, g as runningJobs, h as liveUpdateFor, ht as parseWorkspacePort, i as liveJobsFor, jn as AGENT_IDS, jt as isTerminal, kn as CONFIG_PATH, l as mergeUpdateInto, lt as BUILD_VERSION, m as UpdateAlreadyRunningError, mt as MIN_WORKSPACE_PORT, n as deleteReview$1, nn as summarise, ot as stopServer, pt as MAX_WORKSPACE_PORT, q as removeWorktree, rt as reviewsUrl, st as workspaceUrl, tn as reviewedRepositories, tt as ensureServer, u as updateReview, ut as PACKAGE_NAME, v as recordKissRun, vt as writePortPreference, w as record$1, wn as resolveAgent, x as runJob, xn as displayRef, zt as clearClaim } from "./delete_review-BWMmO81Q.js";
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { existsSync, mkdirSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -33,7 +33,8 @@ var BARE = {
|
|
|
33
33
|
"--update",
|
|
34
34
|
"--reanalyse",
|
|
35
35
|
"--delete",
|
|
36
|
-
"--yes"
|
|
36
|
+
"--yes",
|
|
37
|
+
"--queue"
|
|
37
38
|
],
|
|
38
39
|
open: [],
|
|
39
40
|
agents: [],
|
|
@@ -117,16 +118,21 @@ function parse(argv) {
|
|
|
117
118
|
const values = {};
|
|
118
119
|
const flags = /* @__PURE__ */ new Set();
|
|
119
120
|
const positional = [];
|
|
121
|
+
/** The flag each positional came after, as typed, so a stray URL can be told where it went wrong. */
|
|
122
|
+
const after = /* @__PURE__ */ new Map();
|
|
123
|
+
let lastFlag;
|
|
120
124
|
/** Every URL `--pr` took. `review` has no positionals, so what follows `--pr` up to the next flag is its. */
|
|
121
125
|
const prs = [];
|
|
122
126
|
for (let index = 0; index < rest.length; index += 1) {
|
|
123
127
|
const argument = rest[index];
|
|
124
128
|
if (!argument.startsWith("--")) {
|
|
125
129
|
positional.push(argument);
|
|
130
|
+
if (lastFlag && !after.has(argument)) after.set(argument, lastFlag);
|
|
126
131
|
continue;
|
|
127
132
|
}
|
|
128
133
|
const equals = argument.indexOf("=");
|
|
129
134
|
const name = equals === -1 ? argument : argument.slice(0, equals);
|
|
135
|
+
lastFlag = name;
|
|
130
136
|
if (VALUED[head]?.includes(name)) {
|
|
131
137
|
const value = equals === -1 ? rest[index + 1] : argument.slice(equals + 1);
|
|
132
138
|
if (!value) return {
|
|
@@ -153,10 +159,18 @@ function parse(argv) {
|
|
|
153
159
|
message: `\`${head}\` does not take ${name}. Run \`prreviewbuddy --help\` to see what it takes.`
|
|
154
160
|
};
|
|
155
161
|
}
|
|
156
|
-
if (head === "review" && positional[0])
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
162
|
+
if (head === "review" && positional[0]) {
|
|
163
|
+
const stray = positional[0];
|
|
164
|
+
const flag = after.get(stray);
|
|
165
|
+
if (prs.length > 0 && flag && LOOKS_LIKE_A_REQUEST.test(stray)) return {
|
|
166
|
+
name: "error",
|
|
167
|
+
message: `\`${stray}\` comes after \`${flag}\`, so \`--pr\` did not take it: \`--pr\` takes the URLs straight after it, up to the next flag. Put all the URLs together after \`--pr\`, and the other flags before or after them.`
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
name: "error",
|
|
171
|
+
message: strayArgument(stray)
|
|
172
|
+
};
|
|
173
|
+
}
|
|
160
174
|
if (head === "uninstall" && positional[0]) return {
|
|
161
175
|
name: "error",
|
|
162
176
|
message: `\`uninstall\` removes everything on this machine, so it takes no target and \`${positional[0]}\` means nothing to it. It takes \`--keep-reviews\`, \`--yes\` and \`--force\`.`
|
|
@@ -184,7 +198,8 @@ function parse(argv) {
|
|
|
184
198
|
...values["--concurrency"] ? { concurrency: Number(values["--concurrency"]) } : {},
|
|
185
199
|
...values["--agent"] ? { agentId: values["--agent"] } : {},
|
|
186
200
|
...values["--model"] ? { requestedModel: values["--model"] } : {},
|
|
187
|
-
...flags.has("--fresh") ? { operation: "fresh" } : {}
|
|
201
|
+
...flags.has("--fresh") ? { operation: "fresh" } : {},
|
|
202
|
+
...flags.has("--queue") ? { queue: true } : {}
|
|
188
203
|
};
|
|
189
204
|
const target = Object.keys(TARGETS).find((flag) => values[flag]);
|
|
190
205
|
const operation = Object.keys(OPERATIONS).find((flag) => flags.has(flag));
|
|
@@ -197,7 +212,8 @@ function parse(argv) {
|
|
|
197
212
|
...operation ? { operation: OPERATIONS[operation][0] } : {},
|
|
198
213
|
...values["--lens"] ? { lens: values["--lens"] } : {},
|
|
199
214
|
...values["--audience"] ? { audience: values["--audience"] } : {},
|
|
200
|
-
...flags.has("--yes") ? { yes: true } : {}
|
|
215
|
+
...flags.has("--yes") ? { yes: true } : {},
|
|
216
|
+
...flags.has("--queue") ? { queue: true } : {}
|
|
201
217
|
};
|
|
202
218
|
}
|
|
203
219
|
if (head === "uninstall") return {
|
|
@@ -313,6 +329,7 @@ function refuseCombination(values, flags) {
|
|
|
313
329
|
}
|
|
314
330
|
if (values["--agent"]) return "`--agent` chooses who analyses a change, and `--delete` analyses nothing. Drop it, or drop `--delete` if you meant to review something.";
|
|
315
331
|
if (values["--model"]) return "`--model` chooses which model analyses a change, and `--delete` analyses nothing. Drop it, or drop `--delete` if you meant to review something.";
|
|
332
|
+
if (flags.has("--queue")) return "`--queue` puts a review on your review queue to work through, and `--delete` removes the review, so there would be nothing left to queue. Ask for one of them.";
|
|
316
333
|
}
|
|
317
334
|
if (flags.has("--yes") && !flags.has("--delete")) return "`--yes` answers the confirmation that `--delete` asks, and nothing here is asking one. Add `--delete` if you meant to remove a review.";
|
|
318
335
|
if (values["--review"]) {
|
|
@@ -980,6 +997,8 @@ your own checkout is never read or written while it works.
|
|
|
980
997
|
prreviewbuddy review --pr <url> <url> ...
|
|
981
998
|
review several pull requests, each on its own, two at a
|
|
982
999
|
time; --concurrency <n> changes how many
|
|
1000
|
+
prreviewbuddy review ... --queue also put each review on your review queue, as soon as
|
|
1001
|
+
it exists rather than when it is ready
|
|
983
1002
|
prreviewbuddy review --base <ref> compare against a base of your choosing
|
|
984
1003
|
prreviewbuddy review --agent <agent> review with ${AGENT_NAMES}
|
|
985
1004
|
prreviewbuddy review --model <model> ask that agent for a particular model. Reviews keep
|
|
@@ -1408,11 +1427,52 @@ function batchFinished(rows, index) {
|
|
|
1408
1427
|
lines.push(`${lead}${row.detail ?? row.url ?? ""}`.trimEnd());
|
|
1409
1428
|
if (row.detail && row.url) lines.push(`${" ".repeat(lead.length)}${row.url}`);
|
|
1410
1429
|
}
|
|
1411
|
-
|
|
1430
|
+
const unfinished = rows.some((row) => row.outcome === "stopped" || row.outcome === "not-started" || row.outcome === "failed" && row.url);
|
|
1431
|
+
lines.push(...batchQueueLines(rows));
|
|
1432
|
+
if (unfinished) lines.push("", "Run the same command again to carry on with the reviews that did not finish, from where each got to.");
|
|
1412
1433
|
const all = indexLinks(index);
|
|
1413
1434
|
if (all) lines.push("", all);
|
|
1414
1435
|
return lines.join("\n");
|
|
1415
1436
|
}
|
|
1437
|
+
/**
|
|
1438
|
+
* What `--queue` did across a batch, or nothing at all when it was not asked for.
|
|
1439
|
+
*
|
|
1440
|
+
* One line for what is in the queue, rather than a word on every row: the rows say how each review
|
|
1441
|
+
* ended, and the queue is where the reviewer goes next, so it is said once, together. What was left
|
|
1442
|
+
* out is named with its reason, because each reason has a different way round it.
|
|
1443
|
+
*/
|
|
1444
|
+
function batchQueueLines(rows) {
|
|
1445
|
+
const asked = rows.filter((row) => row.queue !== void 0);
|
|
1446
|
+
if (asked.length === 0) return [];
|
|
1447
|
+
const lines = [""];
|
|
1448
|
+
const queued = asked.filter((row) => row.queue === "added" || row.queue === "already");
|
|
1449
|
+
if (queued.length > 0) lines.push(`In your review queue: ${queued.map((row) => row.label).join(", ")}`);
|
|
1450
|
+
for (const row of asked) {
|
|
1451
|
+
if (row.queue === "added" || row.queue === "already") continue;
|
|
1452
|
+
lines.push(`Not added to your review queue: ${notQueuedBecause(row.label, row.queue)}`);
|
|
1453
|
+
}
|
|
1454
|
+
return lines;
|
|
1455
|
+
}
|
|
1456
|
+
function notQueuedBecause(subject, result) {
|
|
1457
|
+
switch (result) {
|
|
1458
|
+
case "complete": return `${subject} is marked complete. Move it back to In progress in the workspace to queue it.`;
|
|
1459
|
+
case "full": return `${subject}, because the review queue is full. It holds 50 reviews at most.`;
|
|
1460
|
+
default: return `${subject}, because the review queue could not be saved.`;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* What `--queue` did with the one review a single command acted on. To stderr, like every line that
|
|
1465
|
+
* is not the review's own link.
|
|
1466
|
+
*/
|
|
1467
|
+
function reviewQueued(result) {
|
|
1468
|
+
switch (result) {
|
|
1469
|
+
case "added": return "Added to review queue.";
|
|
1470
|
+
case "already": return "Already in your review queue.";
|
|
1471
|
+
case "complete": return "Not added to your review queue: this review is marked complete. Move it back to In progress in the workspace to queue it.";
|
|
1472
|
+
case "full": return `Not added to your review queue: it is full, and holds 50 reviews at most.`;
|
|
1473
|
+
default: return "Not added to your review queue: the queue could not be saved.";
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1416
1476
|
function noReviews() {
|
|
1417
1477
|
return "No reviews stored yet. Run `prreviewbuddy review` in a repository to make one.";
|
|
1418
1478
|
}
|
|
@@ -1908,16 +1968,16 @@ async function reviewBatch(command, repoPath, deps = realDeps()) {
|
|
|
1908
1968
|
}
|
|
1909
1969
|
claimed.set(identity, index);
|
|
1910
1970
|
const prepared = await deps.prepare(target, agent.id, command.operation === "fresh", command.requestedModel);
|
|
1971
|
+
const workspaceId = prepared.kind === "new" ? prepared.workspaceId : prepared.existing.id;
|
|
1972
|
+
row.workspaceId = workspaceId;
|
|
1973
|
+
if (command.queue) row.reviewQueue = deps.enqueue(workspaceId);
|
|
1911
1974
|
if (prepared.kind === "existing") {
|
|
1912
|
-
row.workspaceId = prepared.existing.id;
|
|
1913
1975
|
row.state = {
|
|
1914
1976
|
kind: "existing",
|
|
1915
1977
|
inProgress: prepared.existing.inProgress
|
|
1916
1978
|
};
|
|
1917
1979
|
return;
|
|
1918
1980
|
}
|
|
1919
|
-
const workspaceId = prepared.kind === "resume" ? prepared.existing.id : prepared.workspaceId;
|
|
1920
|
-
row.workspaceId = workspaceId;
|
|
1921
1981
|
row.state = {
|
|
1922
1982
|
kind: "running",
|
|
1923
1983
|
jobId: deps.jobIdOf(workspaceId) ?? ""
|
|
@@ -1949,7 +2009,11 @@ async function reviewBatch(command, repoPath, deps = realDeps()) {
|
|
|
1949
2009
|
}
|
|
1950
2010
|
function summaryRow(row, link) {
|
|
1951
2011
|
const url = row.workspaceId ? link(row.workspaceId) : void 0;
|
|
1952
|
-
const
|
|
2012
|
+
const queued = row.reviewQueue ? { queue: row.reviewQueue } : {};
|
|
2013
|
+
const withUrl = {
|
|
2014
|
+
...url ? { url } : {},
|
|
2015
|
+
...queued
|
|
2016
|
+
};
|
|
1953
2017
|
switch (row.state.kind) {
|
|
1954
2018
|
case "ready": return {
|
|
1955
2019
|
label: row.label,
|
|
@@ -1979,7 +2043,8 @@ function summaryRow(row, link) {
|
|
|
1979
2043
|
};
|
|
1980
2044
|
default: return {
|
|
1981
2045
|
label: row.label,
|
|
1982
|
-
outcome: "not-started"
|
|
2046
|
+
outcome: "not-started",
|
|
2047
|
+
...queued
|
|
1983
2048
|
};
|
|
1984
2049
|
}
|
|
1985
2050
|
}
|
|
@@ -1994,6 +2059,7 @@ function realDeps() {
|
|
|
1994
2059
|
prepare: prepareReview,
|
|
1995
2060
|
settle: (workspaceId, signal) => settleReview(workspaceId, "standard", signal),
|
|
1996
2061
|
jobIdOf: (workspaceId) => loadWorkspace(workspaceId)?.jobId,
|
|
2062
|
+
enqueue: queueForReview,
|
|
1997
2063
|
links: async () => {
|
|
1998
2064
|
try {
|
|
1999
2065
|
const port = await ensureServer();
|
|
@@ -2257,6 +2323,7 @@ async function review(command, repoPath) {
|
|
|
2257
2323
|
const agent = await chooseFor(command);
|
|
2258
2324
|
if (!agent) return 1;
|
|
2259
2325
|
const prepared = await prepareReview(target, agent.id, command.operation === "fresh", command.requestedModel);
|
|
2326
|
+
enqueue(command, prepared.kind === "new" ? prepared.workspaceId : prepared.existing.id);
|
|
2260
2327
|
if (prepared.kind === "existing") {
|
|
2261
2328
|
const url = linkTo(await ensureServer(), prepared.existing.id, prepared.existing.token);
|
|
2262
2329
|
out(existingReview(prepared.existing, url));
|
|
@@ -2311,6 +2378,7 @@ async function kiss(command, target) {
|
|
|
2311
2378
|
...workspace ? { review: { session: workspace.session } } : {}
|
|
2312
2379
|
}) ?? void 0;
|
|
2313
2380
|
if (workspace) {
|
|
2381
|
+
enqueue(command, workspace.id);
|
|
2314
2382
|
const job = await recordKissRun({
|
|
2315
2383
|
workspaceId: workspace.id,
|
|
2316
2384
|
audience,
|
|
@@ -2330,6 +2398,7 @@ async function kiss(command, target) {
|
|
|
2330
2398
|
agentId: agent.id,
|
|
2331
2399
|
...requestedModel ? { requestedModel } : {}
|
|
2332
2400
|
});
|
|
2401
|
+
enqueue(command, workspaceId);
|
|
2333
2402
|
const fresh = loadWorkspace(workspaceId);
|
|
2334
2403
|
const run = (fresh.kissRuns ?? [])[0];
|
|
2335
2404
|
if (!run) {
|
|
@@ -2390,6 +2459,7 @@ async function operate(command, workspace) {
|
|
|
2390
2459
|
err(commitReviewCannotUpdate(workspace.changeSet.data.pullRequest.head.sha));
|
|
2391
2460
|
return 1;
|
|
2392
2461
|
}
|
|
2462
|
+
enqueue(command, workspace.id);
|
|
2393
2463
|
if (command.operation === "reanalyse") {
|
|
2394
2464
|
const agent = await chooseFor(command, workspace);
|
|
2395
2465
|
if (!agent) return 1;
|
|
@@ -2443,6 +2513,17 @@ async function operate(command, workspace) {
|
|
|
2443
2513
|
return 0;
|
|
2444
2514
|
}
|
|
2445
2515
|
/**
|
|
2516
|
+
* `--queue`: put the review on the reviewer's review queue, now, and say how that went.
|
|
2517
|
+
*
|
|
2518
|
+
* Called the moment a review exists and before its analysis is awaited, so the card is in the
|
|
2519
|
+
* workspace's Queue while the review is still being made. Never a reason to fail the command: the
|
|
2520
|
+
* review is what was asked for, and the queue is bookkeeping around it. To stderr, so a piped link
|
|
2521
|
+
* is still only a link.
|
|
2522
|
+
*/
|
|
2523
|
+
function enqueue(command, workspaceId) {
|
|
2524
|
+
if (command.queue) err(reviewQueued(queueForReview(workspaceId)));
|
|
2525
|
+
}
|
|
2526
|
+
/**
|
|
2446
2527
|
* Draw a job's progress block, from what the job itself records about its target.
|
|
2447
2528
|
*
|
|
2448
2529
|
* From the record rather than the caller, for the reason `drive` gives: the job knows what it is
|
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as createPrState, An as
|
|
1
|
+
import { $t as recentWorkspaces, A as createPrState, An as STORE_ROOT, At as fail, B as toneOf, C as readEvents, Cn as git, Ct as fillFileUrlTemplate, D as announceJob, Dn as readAgentPreference, Dt as reviewedShaOf, E as telemetryUploadConsent, En as clearAgentPreference, Et as followedBranch, F as markAllRead, Ft as isLiveJob, G as MANAGED_ROOT, Gt as latestKissRun, H as withUsageRecorded, Ht as followedRefName, I as markRead, In as AgentCancelledError, It as phasesFor, Jt as loadWorkspace, K as readMarker, L as pruneNotifications, Ln as AgentUnavailableError, Lt as progressSteps, M as eventName, Mt as loadJob, N as headline, Nt as saveJob, O as branchOf, On as writeAgentPreference, Ot as allJobs, P as listNotifications, Pn as agentFor, Q as readIndexToken, Qt as previousKissResult, R as readNotification, Rt as purposeOf, Sn as isCommitReview, St as remoteHost, T as recordTelemetryUploadConsent, Tn as resolveModel, Tt as checkReview, U as EXPLAIN_SIMPLY_PROMPT, V as unreadCount$1, Vt as isClaimed, W as ReviewBeingDeletedError, Wt as indexReviewGroups, X as compactQueue, Xt as mutateWorkspace, Y as relativeTime$1, _ as agentEnvOf, _n as reviewerDispositions, _t as statedWorkspacePort, a as wasBlocked, an as workspaceStamps, at as spawnServer, c as RefreshUnavailableError, cn as queueAddMany, ct as writeServerRecord, d as refreshPrContext, dn as readQueue, dt as feedbackUrl, en as reviewedCommit, et as clearServerRecord, f as isUnchanged, fn as doneVerb, ft as DEFAULT_WORKSPACE_PORT, gn as questionsOutstanding, gt as resolveWorkspacePort, h as liveUpdateFor, hn as issuesOutstanding, ht as parseWorkspacePort, i as liveJobsFor, in as workspaceRevision, it as serverEntryExists, j as createPrUpdate, jn as AGENT_IDS, jt as isTerminal, k as subjectOf, kt as endedAt, l as mergeUpdateInto, ln as queueRemove, lt as BUILD_VERSION, m as UpdateAlreadyRunningError, mn as isQuestionOutstanding, mt as MIN_WORKSPACE_PORT, n as deleteReview, nt as readServerRecord, o as discardJob, on as dequeueOnComplete, p as askCheckout, pn as isIssueOutstanding, pt as MAX_WORKSPACE_PORT, q as removeWorktree, qt as lineagePosition, r as lineageIds, rn as touchWorkspace, s as FETCH_TIMEOUT_MS, sn as queueAdd, t as deleteLineage, u as updateReview, un as queueStamp, vn as processDiscussion, vt as writePortPreference, w as record, wt as checkCodeFreshness, x as runJob, xt as forgeFor, y as startKissJob, yn as describeAuthorship, yt as claimRequestUrl, z as removeNotification } from "./delete_review-BWMmO81Q.js";
|
|
2
2
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { createHash, timingSafeEqual } from "node:crypto";
|
|
@@ -744,19 +744,30 @@ var STATES$1 = {
|
|
|
744
744
|
function renderMessageCopySource(source) {
|
|
745
745
|
return button(`${MESSAGE_SOURCE_ATTR}="${escapeHtml$1(source)}"`);
|
|
746
746
|
}
|
|
747
|
+
/** The class that places a finding's copy control in its card's heading, and gives it its tooltip. */
|
|
748
|
+
var FINDING_COPY_CLASS = "pr-helper-finding-copy";
|
|
747
749
|
/**
|
|
748
750
|
* The same control on an issue or question card, carrying that finding as Markdown.
|
|
749
751
|
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
753
|
-
*
|
|
754
|
-
*
|
|
752
|
+
* It sits in the card's heading, not its footer. It copies the whole finding, so the heading is
|
|
753
|
+
* the thing it acts on. And it is a different kind of act from Investigate, Dismiss and Mark
|
|
754
|
+
* answered, which change the review: those keep the footer to themselves, so the footer holds
|
|
755
|
+
* only actions that change something.
|
|
756
|
+
*
|
|
757
|
+
* Icon only at rest, like a chat reply's. It used to show a word because it sat in a row of worded
|
|
758
|
+
* buttons, where an icon alone read as decoration. In the heading it is on its own, and a word
|
|
759
|
+
* would give a utility the weight of the title beside it. What it does is said by a hover
|
|
760
|
+
* tooltip (`data-tip`, the same CSS tooltip the pills use, not a native `title`) and by its
|
|
761
|
+
* accessible name. "Copied" and "Copy failed" still show as words, because a picture cannot say
|
|
762
|
+
* that the click did something. The resting name is kept on the button so the reset returns to
|
|
763
|
+
* "Copy finding" rather than to a chat reply's "Copy message".
|
|
755
764
|
*/
|
|
756
765
|
function renderFindingCopySource(source, kind) {
|
|
766
|
+
const name = kind === "issue" ? "Copy finding" : "Copy question";
|
|
757
767
|
return button(`${MESSAGE_SOURCE_ATTR}="${escapeHtml$1(source)}"`, {
|
|
758
|
-
label: "
|
|
759
|
-
aria:
|
|
768
|
+
label: "",
|
|
769
|
+
aria: name,
|
|
770
|
+
tip: name
|
|
760
771
|
});
|
|
761
772
|
}
|
|
762
773
|
/** Where a finding's button keeps its resting word and name. Absent on a chat reply's. */
|
|
@@ -766,7 +777,8 @@ function button(handle, idle) {
|
|
|
766
777
|
const { icon } = STATES$1.idle;
|
|
767
778
|
const label = idle?.label ?? STATES$1.idle.label;
|
|
768
779
|
const aria = idle?.aria ?? STATES$1.idle.aria;
|
|
769
|
-
const
|
|
780
|
+
const remembered = idle ? ` ${IDLE_LABEL_ATTR}="${escapeHtml$1(label)}" ${IDLE_ARIA_ATTR}="${escapeHtml$1(aria)}"` : "";
|
|
781
|
+
const control = `<button type="button" class="${idle ? `${MESSAGE_COPY_CLASS} ${FINDING_COPY_CLASS}` : MESSAGE_COPY_CLASS}" ${handle}${remembered}${idle ? ` data-tip="${escapeHtml$1(idle.tip)}"` : ""} aria-label="${escapeHtml$1(aria)}"><span class="${MESSAGE_COPY_CLASS}-icon" aria-hidden="true">${svg(icon)}</span><span class="${MESSAGE_COPY_CLASS}-label"${label ? "" : " hidden"}>${escapeHtml$1(label)}</span></button>`;
|
|
770
782
|
return idle ? control : `<div class="${MESSAGE_ACTIONS_CLASS}">${control}</div>`;
|
|
771
783
|
}
|
|
772
784
|
function svg(paths) {
|
|
@@ -2027,7 +2039,7 @@ function renderReconciliationNote(note) {
|
|
|
2027
2039
|
* stuck. Outlined rather than filled — see sidebar.css.
|
|
2028
2040
|
*
|
|
2029
2041
|
* Text only, no leading icon. Every other action in the sidebar (Jump to file, Mark answered,
|
|
2030
|
-
* Explain, Analyse
|
|
2042
|
+
* Explain, Analyse) is a bare label, and an emoji here read as decoration on a control
|
|
2031
2043
|
* whose neighbours have none rather than as emphasis.
|
|
2032
2044
|
*/
|
|
2033
2045
|
function renderInvestigateButton(attribute, id, label) {
|
|
@@ -2147,7 +2159,10 @@ function renderIssuesTab(session, investigatingId) {
|
|
|
2147
2159
|
return `
|
|
2148
2160
|
<div class="pr-helper-issue-card pr-helper-severity-${issue.severity}${issue.reconciliationStatus ? ` pr-helper-issue-${issue.reconciliationStatus}` : ""}${issue.investigationImpact ? ` pr-helper-issue-investigation-${issue.investigationImpact}` : ""}${isDismissed && !stale ? " pr-helper-issue-card-dismissed" : ""}" data-finding-card="${escapeHtml$1(issue.id)}">
|
|
2149
2161
|
<div class="pr-helper-issue-header">
|
|
2150
|
-
<
|
|
2162
|
+
<div class="pr-helper-issue-title-row">
|
|
2163
|
+
<p class="pr-helper-issue-title">${renderInlineText(issue.title)}</p>
|
|
2164
|
+
${renderFindingCopySource(issueMarkdown(issue), "issue")}
|
|
2165
|
+
</div>
|
|
2151
2166
|
<div class="pr-helper-issue-badges">
|
|
2152
2167
|
${renderFindingStatus(issue.reconciliationStatus)}
|
|
2153
2168
|
${renderDismissedBadge(isDismissed)}
|
|
@@ -2167,7 +2182,6 @@ function renderIssuesTab(session, investigatingId) {
|
|
|
2167
2182
|
${renderRelatedFiles(issue.relatedFiles)}
|
|
2168
2183
|
${renderInvestigationProvenance("issue", issue.id, records, now)}
|
|
2169
2184
|
<div class="pr-helper-issue-actions">
|
|
2170
|
-
${renderFindingCopySource(issueMarkdown(issue), "issue")}
|
|
2171
2185
|
${renderInvestigationActions("issue", issue.id, records, investigatingId)}
|
|
2172
2186
|
${renderDismissToggle(issue, isDismissed, stale, dismissals.get(issue.id))}
|
|
2173
2187
|
</div>
|
|
@@ -2236,7 +2250,10 @@ function renderQuestionCard(q, sections, answered, records, now = Date.now(), in
|
|
|
2236
2250
|
return `
|
|
2237
2251
|
<div class="pr-helper-question-card${answered ? " pr-helper-question-card-answered" : ""}${q.investigationImpact ? ` pr-helper-issue-investigation-${q.investigationImpact}` : ""}" data-finding-card="${escapeHtml$1(q.id)}"${q.sectionId ? ` data-section-id="${escapeHtml$1(q.sectionId)}"` : ""}>
|
|
2238
2252
|
<div class="pr-helper-issue-header">
|
|
2239
|
-
<
|
|
2253
|
+
<div class="pr-helper-issue-title-row">
|
|
2254
|
+
<p class="pr-helper-issue-title">${renderInlineText(q.question)}</p>
|
|
2255
|
+
${renderFindingCopySource(questionMarkdown(q), "question")}
|
|
2256
|
+
</div>
|
|
2240
2257
|
<div class="pr-helper-issue-badges">
|
|
2241
2258
|
${renderQuestionStatus(q.reconciliationStatus)}
|
|
2242
2259
|
${renderInvestigationImpact(q.investigationImpact)}
|
|
@@ -2259,7 +2276,6 @@ function renderQuestionCard(q, sections, answered, records, now = Date.now(), in
|
|
|
2259
2276
|
<div class="pr-helper-question-actions">
|
|
2260
2277
|
${jumpLink}
|
|
2261
2278
|
<div class="pr-helper-question-actions-group">
|
|
2262
|
-
${renderFindingCopySource(questionMarkdown(q), "question")}
|
|
2263
2279
|
${renderInvestigationActions("question", q.id, records, investigatingId)}
|
|
2264
2280
|
${answerToggle}
|
|
2265
2281
|
</div>
|
|
@@ -4015,6 +4031,20 @@ function renderUpdateProgress(job) {
|
|
|
4015
4031
|
</div>`;
|
|
4016
4032
|
}
|
|
4017
4033
|
//#endregion
|
|
4034
|
+
//#region ../../packages/review-harness/src/workspace/pr_state_view.ts
|
|
4035
|
+
/**
|
|
4036
|
+
* The pull request's state, beside the review's status and never instead of it: `Reviewed` is the
|
|
4037
|
+
* reviewer's decision, `Merged` is the upstream's. Open says nothing, because it is what a review
|
|
4038
|
+
* of a pull request usually is, and a label that never varies teaches nothing.
|
|
4039
|
+
*
|
|
4040
|
+
* `besideNumber` is whether the pull request's number sits right before the pill. There, "Closed"
|
|
4041
|
+
* is enough; anywhere else it would read as the branch closing, so the pill names the PR itself.
|
|
4042
|
+
*/
|
|
4043
|
+
function renderPrState(state, besideNumber = false) {
|
|
4044
|
+
if (state !== "merged" && state !== "closed") return "";
|
|
4045
|
+
return `<span class="prb-card-pr-state" data-pr-state="${state}">${state === "merged" ? "Merged" : besideNumber ? "Closed" : "PR closed"}</span>`;
|
|
4046
|
+
}
|
|
4047
|
+
//#endregion
|
|
4018
4048
|
//#region ../../packages/review-harness/src/workspace/page.ts
|
|
4019
4049
|
function renderWorkspace(view) {
|
|
4020
4050
|
const { session, changeSet } = view;
|
|
@@ -4394,6 +4424,7 @@ function renderReviewBar(view, freshness) {
|
|
|
4394
4424
|
<span class="prb-change-ref" title="${escapeHtml$1(changeSet.repoPath)}">
|
|
4395
4425
|
${changeSet.isLinkedWorktree ? `<span class="prb-checkout">${escapeHtml$1(changeSet.checkoutName)}</span>` : ""}
|
|
4396
4426
|
${view.prContext ? `<a class="prb-pr-chip" href="${escapeHtml$1(view.prContext.prContext.pullRequest.url)}" target="_blank" rel="noopener noreferrer" title="${escapeHtml$1(view.prContext.prContext.pullRequest.title ?? "")}">#${view.prContext.prContext.pullRequest.number}</a>` : ""}
|
|
4427
|
+
${view.prState ? `<span class="prb-pr-state-slot" data-pr-state-slot${view.prContext ? " data-beside-number" : ""}>${renderPrState(view.prState.state, view.prContext !== void 0)}</span>` : ""}
|
|
4397
4428
|
${renderSubject(changeSet)}
|
|
4398
4429
|
${changeSet.includesUncommitted ? "<span class=\"prb-file-status\">+ working tree</span>" : ""}
|
|
4399
4430
|
${renderAuthor(view)}
|
|
@@ -5912,6 +5943,20 @@ function storedRequestKeys() {
|
|
|
5912
5943
|
function prStatesByRequest(states = readPrStates()) {
|
|
5913
5944
|
return new Map(Object.entries(states).map(([key, entry]) => [key, entry.state]));
|
|
5914
5945
|
}
|
|
5946
|
+
/**
|
|
5947
|
+
* One review's pull request state, for its page, filed under the same key the list looks up. Null
|
|
5948
|
+
* for a review with no request or one never observed: unknown, not open. Only ever what the watcher
|
|
5949
|
+
* last saw; nothing here reaches the forge.
|
|
5950
|
+
*/
|
|
5951
|
+
function prStateOf$1(workspace, states = readPrStates()) {
|
|
5952
|
+
const request = requestOf(workspace);
|
|
5953
|
+
if (!request) return null;
|
|
5954
|
+
return states[requestKey(workspace.repoPath, request.number)]?.state ?? null;
|
|
5955
|
+
}
|
|
5956
|
+
/** Whether a review is of a pull request at all, whatever its state. See `requestOf`. */
|
|
5957
|
+
function hasRequest(workspace) {
|
|
5958
|
+
return requestOf(workspace) !== null;
|
|
5959
|
+
}
|
|
5915
5960
|
function defaultDeps() {
|
|
5916
5961
|
return {
|
|
5917
5962
|
candidates: watchedCandidates,
|
|
@@ -6417,15 +6462,6 @@ function statusOf(review) {
|
|
|
6417
6462
|
if (review.completedAt) return "complete";
|
|
6418
6463
|
return review.lastViewedAt ? "in-progress" : "new";
|
|
6419
6464
|
}
|
|
6420
|
-
/**
|
|
6421
|
-
* The pull request's state, beside the review's status and never instead of it: `Reviewed` is the
|
|
6422
|
-
* reviewer's decision, `Merged` is the upstream's. Open says nothing, because it is what a review
|
|
6423
|
-
* of a pull request usually is, and a label that never varies teaches nothing.
|
|
6424
|
-
*/
|
|
6425
|
-
function renderPrState(state) {
|
|
6426
|
-
if (state !== "merged" && state !== "closed") return "";
|
|
6427
|
-
return `<span class="prb-card-pr-state" data-pr-state="${state}">${state === "merged" ? "Merged" : "PR closed"}</span>`;
|
|
6428
|
-
}
|
|
6429
6465
|
function renderStatus(status) {
|
|
6430
6466
|
return `<span class="prb-card-status" data-status="${status}">${statusLabel(status)}</span>`;
|
|
6431
6467
|
}
|
|
@@ -8636,11 +8672,6 @@ function addMany(res, ids) {
|
|
|
8636
8672
|
refused
|
|
8637
8673
|
});
|
|
8638
8674
|
}
|
|
8639
|
-
/** Compacted here rather than in `queue.ts`, which must not learn to read the review store. */
|
|
8640
|
-
function compactQueue(stored) {
|
|
8641
|
-
const live = stored.filter((queued) => loadWorkspace(queued) !== null);
|
|
8642
|
-
return live.length === stored.length ? stored : writeQueue(live) ?? stored;
|
|
8643
|
-
}
|
|
8644
8675
|
/**
|
|
8645
8676
|
* Delete one review, or everything on one card.
|
|
8646
8677
|
*
|
|
@@ -8881,6 +8912,7 @@ async function sendWorkspacePage(res, workspace) {
|
|
|
8881
8912
|
freshness: freshness ?? void 0,
|
|
8882
8913
|
availability: standing?.availability ?? null,
|
|
8883
8914
|
remote: await remoteOrNull(workspace),
|
|
8915
|
+
...hasRequest(workspace) ? { prState: { state: prStateOf$1(workspace) } } : {},
|
|
8884
8916
|
dismissedFreshnessSha: workspace.dismissedFreshnessSha,
|
|
8885
8917
|
lastPrRefresh: workspace.lastPrRefresh,
|
|
8886
8918
|
lastUpdate: notice,
|
|
@@ -8999,10 +9031,12 @@ async function handleFreshness(res, workspace, quiet) {
|
|
|
8999
9031
|
const standing = await standingOrNull(workspace);
|
|
9000
9032
|
const report = standing?.freshness ?? null;
|
|
9001
9033
|
const remote = await remoteOrNull(workspace);
|
|
9034
|
+
const prState = prStateOf$1(workspace);
|
|
9002
9035
|
const revision = workspaceRevision(workspace) ?? null;
|
|
9003
9036
|
if (!report) return send(res, 200, {
|
|
9004
9037
|
report: null,
|
|
9005
9038
|
remote,
|
|
9039
|
+
prState,
|
|
9006
9040
|
kiss,
|
|
9007
9041
|
kissDismissedSha: workspace.dismissedKissFreshnessSha ?? null,
|
|
9008
9042
|
revision
|
|
@@ -9018,6 +9052,7 @@ async function handleFreshness(res, workspace, quiet) {
|
|
|
9018
9052
|
report,
|
|
9019
9053
|
availability: standing?.availability ?? null,
|
|
9020
9054
|
remote,
|
|
9055
|
+
prState,
|
|
9021
9056
|
dismissedSha: workspace.dismissedFreshnessSha ?? null,
|
|
9022
9057
|
kiss,
|
|
9023
9058
|
kissDismissedSha: workspace.dismissedKissFreshnessSha ?? null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prreviewbuddy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"description": "Review a branch or pull request in an isolated checkout, from your terminal, with the coding agent you already have installed.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://prreviewbuddy.com",
|
package/static/shell.css
CHANGED
|
@@ -562,6 +562,10 @@ body[data-server-stopped] .prb-shell { opacity: 0.55; pointer-events: none; }
|
|
|
562
562
|
text-decoration: none;
|
|
563
563
|
}
|
|
564
564
|
.prb-pr-chip:hover { background: var(--brand); color: #fff; }
|
|
565
|
+
/* Where the pull request's Merged or Closed pill goes, right after the chip. No box of its own, so an
|
|
566
|
+
open pull request's empty slot leaves no extra gap in the row and a filled one lays the pill out
|
|
567
|
+
as the chip's sibling. The pill is the Reviews card's, `.prb-card-pr-state`. */
|
|
568
|
+
.prb-pr-state-slot { display: contents; }
|
|
565
569
|
|
|
566
570
|
/* Beside the branch, after it, because the branch is what the reviewer came for and this is who
|
|
567
571
|
it belongs to. One line, ellipsed rather than wrapped: the header is a single row and a long
|
package/static/sidebar.css
CHANGED
|
@@ -876,6 +876,64 @@
|
|
|
876
876
|
|
|
877
877
|
.pr-helper-issue-title { font-size: 14px; font-weight: 600; margin: 0; text-align: left; width: 100%; }
|
|
878
878
|
|
|
879
|
+
/* The title and the one control that acts on the whole finding, side by side. The title takes the
|
|
880
|
+
room and wraps; the copy icon keeps its size at the top-right, level with the title's first line
|
|
881
|
+
however many lines the title wraps to. */
|
|
882
|
+
.pr-helper-issue-title-row {
|
|
883
|
+
display: flex;
|
|
884
|
+
align-items: flex-start;
|
|
885
|
+
gap: 8px;
|
|
886
|
+
width: 100%;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.pr-helper-issue-title-row > .pr-helper-issue-title { flex: 1; min-width: 0; }
|
|
890
|
+
|
|
891
|
+
/* Full strength rather than a chat reply's 0.65: at 0.65 its grey falls under 3:1 on the white card.
|
|
892
|
+
Pulled up a pixel so the 14px glyph sits on the 14px title's first line rather than below it.
|
|
893
|
+
Positioned so its tooltip can anchor to it: unlike a pill's, which spans its whole row, this one
|
|
894
|
+
belongs to one small control at the card's edge. */
|
|
895
|
+
.pr-helper-finding-copy {
|
|
896
|
+
position: relative;
|
|
897
|
+
flex-shrink: 0;
|
|
898
|
+
margin-top: -1px;
|
|
899
|
+
opacity: 1;
|
|
900
|
+
font-size: 12px;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/* The tooltip grows leftward from the right edge, back over the card rather than out of the pane,
|
|
904
|
+
and stays on one line. The same look, delay and text reset as `.pr-helper-tip`, which it cannot
|
|
905
|
+
reuse: that one pins itself to its positioned row, and this control is its own anchor. */
|
|
906
|
+
.pr-helper-finding-copy::after {
|
|
907
|
+
content: attr(data-tip);
|
|
908
|
+
position: absolute;
|
|
909
|
+
top: calc(100% + 6px);
|
|
910
|
+
right: 0;
|
|
911
|
+
z-index: 3;
|
|
912
|
+
padding: 5px 8px;
|
|
913
|
+
border-radius: 6px;
|
|
914
|
+
background: #24292f;
|
|
915
|
+
color: #ffffff;
|
|
916
|
+
font-size: 11px;
|
|
917
|
+
font-weight: 400;
|
|
918
|
+
font-family: inherit;
|
|
919
|
+
line-height: 1.4;
|
|
920
|
+
white-space: nowrap;
|
|
921
|
+
opacity: 0;
|
|
922
|
+
pointer-events: none;
|
|
923
|
+
transition: opacity 0.1s;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
.pr-helper-finding-copy:hover::after,
|
|
927
|
+
.pr-helper-finding-copy:focus-visible::after {
|
|
928
|
+
opacity: 1;
|
|
929
|
+
transition-delay: 0.35s;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/* The button is saying "Copied" or "Copy failed" in words right now; a tooltip offering to copy
|
|
933
|
+
would argue with it. */
|
|
934
|
+
.pr-helper-finding-copy.copied::after,
|
|
935
|
+
.pr-helper-finding-copy.failed::after { display: none; }
|
|
936
|
+
|
|
879
937
|
.pr-helper-issue-desc {
|
|
880
938
|
font-size: 13px;
|
|
881
939
|
line-height: 1.5;
|
|
@@ -4187,20 +4245,6 @@
|
|
|
4187
4245
|
margin-top: 12px;
|
|
4188
4246
|
}
|
|
4189
4247
|
|
|
4190
|
-
/* Copying the finding is a different kind of act from deciding about it, so it takes the far edge
|
|
4191
|
-
rather than joining Investigate and Dismiss. On a question card it sits in the group instead,
|
|
4192
|
-
because the far edge there already belongs to Jump to file. */
|
|
4193
|
-
.pr-helper-issue-actions > .pr-helper-msg-copy { margin-right: auto; }
|
|
4194
|
-
|
|
4195
|
-
/* Full strength on a card. Under a chat reply the control is a quiet afterthought to the text, and
|
|
4196
|
-
its 0.65 is right there; here it is a worded button among worded buttons, and at 0.65 its grey
|
|
4197
|
-
falls under 3:1 on the white card. The size is its neighbours'. */
|
|
4198
|
-
.pr-helper-issue-actions .pr-helper-msg-copy,
|
|
4199
|
-
.pr-helper-question-actions .pr-helper-msg-copy {
|
|
4200
|
-
opacity: 1;
|
|
4201
|
-
font-size: 12px;
|
|
4202
|
-
}
|
|
4203
|
-
|
|
4204
4248
|
/* margin-left:auto rather than relying on the row's space-between: the jump link beside it is
|
|
4205
4249
|
conditional (a question without a file has none), and space-between alone would swing this
|
|
4206
4250
|
group to the left edge whenever it is the row's only child. Wraps because three controls do
|