prreviewbuddy 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/dist/{delete_review-B_2J2Wn9.js → delete_review-C2wax0ED.js} +256 -20
- package/dist/main.js +51 -10
- package/dist/server.js +92 -53
- package/package.json +1 -1
- package/static/shell.css +41 -23
- package/static/workspace.js +11 -11
package/dist/main.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $t as writeAgentPreference, A as removeWorktree, B as workspaceUrl, C as recordTelemetryUploadConsent, Dt as saveWorkspace, Et as reviewedRepositories, G as DEFAULT_WORKSPACE_PORT, H as BUILD_VERSION, I as ensureServer, J as parseWorkspacePort, Jt as git, K as MAX_WORKSPACE_PORT, Kt as resolveTarget, M as relativeTime, N as readIndexToken, O as MANAGED_ROOT, Ot as summarise, P as bootstrapUrl, Q as forgeResolver, Qt as readAgentPreference, R as reviewsUrl, S as record$1, St as positionInLineage, U as PACKAGE_NAME, X as statedWorkspacePort, Xt as resolveModel, Yt as resolveAgent, Z as writePortPreference, a as wasBlocked, at as isTerminal, b as startJob, bt as loadWorkspace, c as updateReview, ct as PHASES, dt as purposeOf, en as CONFIG_PATH, f as UpdateAlreadyRunningError, ft as clearClaim, g as recordKissRun, ht as groupByLineage, i as liveJobsFor, in as agentById, j as modelHelpLines, k as readMarker, m as runningJobs, mt as followedRefName, n as deleteReview$1, nn as AGENT_IDS, nt as allJobs, on as detectAgents, ot as loadJob, p as liveUpdateFor, pt as isClaimed, q as MIN_WORKSPACE_PORT, qt as displayRef, rn as DEFAULT_AGENT_ID, tn as STORE_ROOT, tt as
|
|
2
|
+
import { $t as writeAgentPreference, A as removeWorktree, B as workspaceUrl, C as recordTelemetryUploadConsent, Dt as saveWorkspace, Et as reviewedRepositories, G as DEFAULT_WORKSPACE_PORT, H as BUILD_VERSION, I as ensureServer, J as parseWorkspacePort, Jt as git, K as MAX_WORKSPACE_PORT, Kt as resolveTarget, M as relativeTime, N as readIndexToken, O as MANAGED_ROOT, Ot as summarise, P as bootstrapUrl, Q as forgeResolver, Qt as readAgentPreference, R as reviewsUrl, S as record$1, St as positionInLineage, U as PACKAGE_NAME, X as statedWorkspacePort, Xt as resolveModel, Yt as resolveAgent, Z as writePortPreference, a as wasBlocked, at as isTerminal, b as startJob, bt as loadWorkspace, c as updateReview, ct as PHASES, dt as purposeOf, en as CONFIG_PATH, f as UpdateAlreadyRunningError, ft as clearClaim, g as recordKissRun, ht as groupByLineage, i as liveJobsFor, in as agentById, j as modelHelpLines, k as readMarker, m as runningJobs, mt as followedRefName, n as deleteReview$1, nn as AGENT_IDS, nt as allJobs, on as detectAgents, ot as loadJob, p as liveUpdateFor, pt as isClaimed, q as MIN_WORKSPACE_PORT, qt as displayRef, rn as DEFAULT_AGENT_ID, tn as STORE_ROOT, tt as checkReview, ut as progressSteps, v as reanalyseReview, vt as lineageKeyFor, w as telemetryUploadConsent, wt as recentWorkspaces, xt as matchingWorkspaceIds, y as runJob, z as stopServer } from "./delete_review-C2wax0ED.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";
|
|
@@ -15,6 +15,7 @@ var VALUED = {
|
|
|
15
15
|
"--model",
|
|
16
16
|
"--branch",
|
|
17
17
|
"--pr",
|
|
18
|
+
"--commit",
|
|
18
19
|
"--review",
|
|
19
20
|
"--audience",
|
|
20
21
|
"--lens",
|
|
@@ -60,10 +61,11 @@ var POSITIONALS = {
|
|
|
60
61
|
config: 3,
|
|
61
62
|
uninstall: 0
|
|
62
63
|
};
|
|
63
|
-
/** The
|
|
64
|
+
/** The flags that say what is being acted on, and the field each becomes. */
|
|
64
65
|
var TARGETS = {
|
|
65
66
|
"--branch": "branchRef",
|
|
66
67
|
"--pr": "prUrl",
|
|
68
|
+
"--commit": "commitRef",
|
|
67
69
|
"--review": "reviewId"
|
|
68
70
|
};
|
|
69
71
|
/**
|
|
@@ -302,9 +304,11 @@ function refuseCombination(values, flags) {
|
|
|
302
304
|
}
|
|
303
305
|
}
|
|
304
306
|
if (values["--pr"] && values["--base"]) return "A pull request is compared against the branch it is asking to merge into, so `--base` would stop the diff being the pull request's. To compare its branch against something else, review the branch: `prreviewbuddy review --branch <branch> --base <ref>`.";
|
|
307
|
+
if (values["--commit"] && values["--base"]) return "A commit is reviewed as the change it made, which is its diff against its own parent, so `--base` would make the findings about a different change under a page still naming `--commit`. To compare a branch against a base of your choosing, review the branch: `prreviewbuddy review --branch <branch> --base <ref>`.";
|
|
308
|
+
if (values["--commit"] && flags.has("--update")) return `\`--update\` ${OPERATIONS["--update"][1]}, and \`--commit\` names one commit, which cannot move, so there is nothing to bring up to date. To analyse the same commit again, run \`prreviewbuddy review --commit ${values["--commit"]} --reanalyse\`.`;
|
|
305
309
|
if (flags.has("--delete")) {
|
|
306
310
|
if (!values["--review"]) {
|
|
307
|
-
const named = values["--branch"] ?? values["--pr"];
|
|
311
|
+
const named = values["--branch"] ?? values["--pr"] ?? values["--commit"];
|
|
308
312
|
return (named ? `\`--delete\` removes one review, and \`${named}\` can name several: reviewing a target twice with \`--fresh\` leaves two. ` : "`--delete` removes one review, and the branch you are standing on can have several: reviewing it twice with `--fresh` leaves two. ") + "Say which with `--review <review>`, which `prreviewbuddy open` prints beside each review.";
|
|
309
313
|
}
|
|
310
314
|
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.";
|
|
@@ -626,11 +630,20 @@ async function pick(ask, options) {
|
|
|
626
630
|
*/
|
|
627
631
|
async function findExistingReview(target) {
|
|
628
632
|
const groups = groupByLineage(recentWorkspaces(Number.MAX_SAFE_INTEGER));
|
|
629
|
-
const wanted =
|
|
633
|
+
const wanted = target.commit ? lineageKeyFor({
|
|
634
|
+
kind: "commit",
|
|
635
|
+
repoPath: target.originRepoPath,
|
|
636
|
+
sha: target.commit.sha
|
|
637
|
+
}) : lineageKeyFor({
|
|
638
|
+
kind: "branch",
|
|
639
|
+
repoPath: target.originRepoPath,
|
|
640
|
+
branch: target.branch,
|
|
641
|
+
followedRef: target.ref
|
|
642
|
+
});
|
|
630
643
|
const group = groups.find((entry) => entry.key === wanted);
|
|
631
644
|
if (!group) return null;
|
|
632
645
|
const requested = target.pullRequest?.number;
|
|
633
|
-
const history = group.history.filter((review) => target.base ? madeAgainst(target.base)(review) : true).filter((review) => requested ? review.prNumber === requested : true);
|
|
646
|
+
const history = target.commit ? group.history : group.history.filter((review) => target.base ? madeAgainst(target.base)(review) : true).filter((review) => requested ? review.prNumber === requested : true);
|
|
634
647
|
if (history.length === 0) return null;
|
|
635
648
|
const summary = history[0];
|
|
636
649
|
const found = {
|
|
@@ -640,6 +653,7 @@ async function findExistingReview(target) {
|
|
|
640
653
|
branch: summary.branch,
|
|
641
654
|
baseRef: summary.baseRef,
|
|
642
655
|
...summary.headResolvedRef ? { headResolvedRef: summary.headResolvedRef } : {},
|
|
656
|
+
...summary.targetType === "commit" ? { targetType: "commit" } : {},
|
|
643
657
|
createdAt: summary.createdAt,
|
|
644
658
|
commitSha: summary.commitSha ?? null,
|
|
645
659
|
commitsBehind: null,
|
|
@@ -651,7 +665,13 @@ async function findExistingReview(target) {
|
|
|
651
665
|
if (!workspace) return found;
|
|
652
666
|
const inProgress = isRunning(workspace.jobId);
|
|
653
667
|
try {
|
|
654
|
-
const
|
|
668
|
+
const { freshness, availability } = await checkReview(workspace);
|
|
669
|
+
if (availability) return {
|
|
670
|
+
...found,
|
|
671
|
+
inProgress,
|
|
672
|
+
commitAvailable: availability.state === "available"
|
|
673
|
+
};
|
|
674
|
+
const code = freshness.code;
|
|
655
675
|
const baselineGone = code.rewritten || code.reason === "missing-commit";
|
|
656
676
|
return {
|
|
657
677
|
...found,
|
|
@@ -722,6 +742,7 @@ function describeSubject(existing) {
|
|
|
722
742
|
* reviewer's own working tree.
|
|
723
743
|
*/
|
|
724
744
|
function describeMovement(existing) {
|
|
745
|
+
if (existing.targetType === "commit") return existing.commitAvailable === false ? "That commit is no longer in this repository, so the review can be read but not analysed again." : "A commit does not move, so the review still describes exactly that commit.";
|
|
725
746
|
const subject = followedRefName(existing.branch, existing.headResolvedRef) ?? existing.branch;
|
|
726
747
|
if (existing.baselineGone) return `${subject} has been rebuilt since, so that commit is no longer on it.`;
|
|
727
748
|
if (existing.commitsBehind === null) return `How far ${subject} has moved since could not be worked out.`;
|
|
@@ -803,7 +824,7 @@ var RESET = "\x1B[0m";
|
|
|
803
824
|
function header(target, colour, purpose = "review") {
|
|
804
825
|
const meta = `${target.sha.slice(0, 7)} · isolated checkout · ${target.originRepoPath}`;
|
|
805
826
|
return [
|
|
806
|
-
`${purpose === "update" ? "Updating" : "Reviewing"} ${target.branch}`,
|
|
827
|
+
`${purpose === "update" ? "Updating" : "Reviewing"} ${target.commit ? `commit ${target.branch}` : target.branch}`,
|
|
807
828
|
colour ? `${DIM}${meta}${RESET}` : meta,
|
|
808
829
|
""
|
|
809
830
|
];
|
|
@@ -948,6 +969,7 @@ your own checkout is never read or written while it works.
|
|
|
948
969
|
|
|
949
970
|
prreviewbuddy review review the current branch
|
|
950
971
|
prreviewbuddy review --branch <branch> review another branch, without checking it out
|
|
972
|
+
prreviewbuddy review --commit <commit> review the change one commit made, against its parent
|
|
951
973
|
prreviewbuddy review --pr <url> review a pull request
|
|
952
974
|
prreviewbuddy review --pr <url> <url> ...
|
|
953
975
|
review several pull requests, each on its own, two at a
|
|
@@ -1256,7 +1278,8 @@ function startedReview(url, _target) {
|
|
|
1256
1278
|
* never produced a result is offering to reconcile something against itself.
|
|
1257
1279
|
*/
|
|
1258
1280
|
function existingReview(existing, url) {
|
|
1259
|
-
const
|
|
1281
|
+
const commit = existing.targetType === "commit" ? (existing.commitSha ?? existing.branch).slice(0, 7) : null;
|
|
1282
|
+
const subject = commit ? `commit ${commit}` : followedRefName(existing.branch, existing.headResolvedRef) ?? existing.branch;
|
|
1260
1283
|
if (existing.inProgress) return [
|
|
1261
1284
|
url,
|
|
1262
1285
|
"",
|
|
@@ -1272,7 +1295,7 @@ function existingReview(existing, url) {
|
|
|
1272
1295
|
`A review of ${subject} already exists, so here it is rather than a second one.`,
|
|
1273
1296
|
made,
|
|
1274
1297
|
"",
|
|
1275
|
-
existing.baselineGone ? "This branch has been rebuilt since, so the commit the review was made from is no longer on it. There is nothing left to update it against, so this one needs starting fresh: `prreviewbuddy review --fresh`." : "To update the review in place, keeping your transcript and your place in it, run `prreviewbuddy review --update`. To start fresh alongside it, run `prreviewbuddy review --fresh`."
|
|
1298
|
+
commit ? `To analyse the same commit again, keeping this analysis beside the new one, run \`prreviewbuddy review --commit ${commit} --reanalyse\`. To start a separate review of it, run \`prreviewbuddy review --commit ${commit} --fresh\`.` : existing.baselineGone ? "This branch has been rebuilt since, so the commit the review was made from is no longer on it. There is nothing left to update it against, so this one needs starting fresh: `prreviewbuddy review --fresh`." : "To update the review in place, keeping your transcript and your place in it, run `prreviewbuddy review --update`. To start fresh alongside it, run `prreviewbuddy review --fresh`."
|
|
1276
1299
|
].join("\n");
|
|
1277
1300
|
}
|
|
1278
1301
|
function reviewFinished(url, summary, conversation) {
|
|
@@ -1478,6 +1501,14 @@ function noReviewOfTarget(name, make) {
|
|
|
1478
1501
|
return `There is no review of ${name} to act on. Run \`${make}\` to make one.`;
|
|
1479
1502
|
}
|
|
1480
1503
|
/**
|
|
1504
|
+
* `--update` reaching a commit review by id, where the parser could not see what the id names.
|
|
1505
|
+
* The `--commit --update` spelling is refused earlier, in `args.ts`, in the same words.
|
|
1506
|
+
*/
|
|
1507
|
+
function commitReviewCannotUpdate(sha) {
|
|
1508
|
+
const short = sha.slice(0, 7);
|
|
1509
|
+
return `This review is of commit ${short}, and a commit cannot move, so there is nothing to bring up to date. To analyse the same commit again, run \`prreviewbuddy review --commit ${short} --reanalyse\`.`;
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1481
1512
|
* The update's own sentence, which the harness already wrote for the workspace strip, plus the
|
|
1482
1513
|
* link. Not rephrased here: an update that stopped early has a specific reason and the reviewer
|
|
1483
1514
|
* gets the same reason whichever surface asked for it.
|
|
@@ -2200,6 +2231,7 @@ async function review(command, repoPath) {
|
|
|
2200
2231
|
repoPath,
|
|
2201
2232
|
...command.branchRef ? { branchRef: command.branchRef } : {},
|
|
2202
2233
|
...command.prUrl ? { prUrl: command.prUrl } : {},
|
|
2234
|
+
...command.commitRef ? { commitRef: command.commitRef } : {},
|
|
2203
2235
|
...command.base ? { base: command.base } : {},
|
|
2204
2236
|
resolveRequest: forgeResolver
|
|
2205
2237
|
});
|
|
@@ -2207,7 +2239,7 @@ async function review(command, repoPath) {
|
|
|
2207
2239
|
const existing = await findExistingReview(target);
|
|
2208
2240
|
const workspace = existing ? loadWorkspace(existing.id) : null;
|
|
2209
2241
|
if (!workspace) {
|
|
2210
|
-
err(noReviewOfTarget(target.branch, makeCommandFor(command)));
|
|
2242
|
+
err(noReviewOfTarget(target.commit ? `commit ${target.branch}` : target.branch, makeCommandFor(command)));
|
|
2211
2243
|
return 1;
|
|
2212
2244
|
}
|
|
2213
2245
|
return operate(command, workspace);
|
|
@@ -2345,6 +2377,10 @@ function selectionOf(command) {
|
|
|
2345
2377
|
* store, so the two lines after it here are the whole difference between this and the HTTP route.
|
|
2346
2378
|
*/
|
|
2347
2379
|
async function operate(command, workspace) {
|
|
2380
|
+
if (command.operation === "update" && workspace.changeSet.targetType === "commit") {
|
|
2381
|
+
err(commitReviewCannotUpdate(workspace.changeSet.data.pullRequest.head.sha));
|
|
2382
|
+
return 1;
|
|
2383
|
+
}
|
|
2348
2384
|
if (command.operation === "reanalyse") {
|
|
2349
2385
|
const agent = await chooseFor(command, workspace);
|
|
2350
2386
|
if (!agent) return 1;
|
|
@@ -2419,6 +2455,10 @@ function follow(jobId) {
|
|
|
2419
2455
|
* the whole of what there is to say.
|
|
2420
2456
|
*/
|
|
2421
2457
|
function actedOn(workspace) {
|
|
2458
|
+
if (workspace.changeSet.targetType === "commit") return {
|
|
2459
|
+
name: `commit ${workspace.changeSet.data.pullRequest.head.sha.slice(0, 7)}`,
|
|
2460
|
+
createdAt: workspace.createdAt
|
|
2461
|
+
};
|
|
2422
2462
|
const { headRef, headResolvedRef } = workspace.changeSet;
|
|
2423
2463
|
return {
|
|
2424
2464
|
name: followedRefName(headRef, headResolvedRef) ?? headRef,
|
|
@@ -2437,6 +2477,7 @@ function actedOn(workspace) {
|
|
|
2437
2477
|
function makeCommandFor(command) {
|
|
2438
2478
|
if (command.branchRef) return `prreviewbuddy review --branch ${command.branchRef}`;
|
|
2439
2479
|
if (command.prUrl) return `prreviewbuddy review --pr ${command.prUrl}`;
|
|
2480
|
+
if (command.commitRef) return `prreviewbuddy review --commit ${command.commitRef}`;
|
|
2440
2481
|
return "prreviewbuddy review";
|
|
2441
2482
|
}
|
|
2442
2483
|
function failureOf(workspaceId) {
|
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as fillFileUrlTemplate, $t as writeAgentPreference, A as removeWorktree, At as workspaceRevision, Bt as isQuestionOutstanding, C as recordTelemetryUploadConsent, Ct as previousKissResult, D as ReviewBeingDeletedError, Dt as saveWorkspace, E as EXPLAIN_SIMPLY_PROMPT, F as clearServerRecord, Ft as queueStamp, G as DEFAULT_WORKSPACE_PORT, Gt as describeAuthorship, H as BUILD_VERSION, Ht as questionsOutstanding, It as readQueue, J as parseWorkspacePort, Jt as git, K as MAX_WORKSPACE_PORT, L as readServerRecord, Lt as writeQueue, M as relativeTime$1, Mt as dequeueOnComplete, N as readIndexToken, Nt as queueAdd, O as MANAGED_ROOT, Pt as queueRemove, Qt as readAgentPreference, Rt as doneVerb, S as record, T as withUsageRecorded, Tt as reviewedCommit, Ut as reviewerDispositions, V as writeServerRecord, Vt as issuesOutstanding, W as feedbackUrl, Wt as processDiscussion, X as statedWorkspacePort, Xt as resolveModel, Y as resolveWorkspacePort, Z as writePortPreference, Zt as clearAgentPreference, _ as startKissJob, _t as latestKissRun, a as wasBlocked, an as agentFor, at as isTerminal, bt as loadWorkspace, c as updateReview, cn as AgentUnavailableError, d as askCheckout, dt as purposeOf, et as checkCodeFreshness, f as UpdateAlreadyRunningError, gt as indexReviewGroups, h as agentEnvOf, i as liveJobsFor, it as fail, jt as workspaceStamps, k as readMarker, kt as touchWorkspace, l as refreshPrContext, lt as phasesFor, mt as followedRefName, n as deleteReview, nn as AGENT_IDS, nt as allJobs, o as discardJob, ot as loadJob, p as liveUpdateFor, pt as isClaimed, q as MIN_WORKSPACE_PORT, r as lineageIds, rt as endedAt, s as RefreshUnavailableError, sn as AgentCancelledError, st as saveJob, t as deleteLineage, tn as STORE_ROOT, tt as
|
|
1
|
+
import { $ as fillFileUrlTemplate, $t as writeAgentPreference, A as removeWorktree, At as workspaceRevision, Bt as isQuestionOutstanding, C as recordTelemetryUploadConsent, Ct as previousKissResult, D as ReviewBeingDeletedError, Dt as saveWorkspace, E as EXPLAIN_SIMPLY_PROMPT, F as clearServerRecord, Ft as queueStamp, G as DEFAULT_WORKSPACE_PORT, Gt as describeAuthorship, H as BUILD_VERSION, Ht as questionsOutstanding, It as readQueue, J as parseWorkspacePort, Jt as git, K as MAX_WORKSPACE_PORT, L as readServerRecord, Lt as writeQueue, M as relativeTime$1, Mt as dequeueOnComplete, N as readIndexToken, Nt as queueAdd, O as MANAGED_ROOT, Pt as queueRemove, Qt as readAgentPreference, Rt as doneVerb, S as record, T as withUsageRecorded, Tt as reviewedCommit, Ut as reviewerDispositions, V as writeServerRecord, Vt as issuesOutstanding, W as feedbackUrl, Wt as processDiscussion, X as statedWorkspacePort, Xt as resolveModel, Y as resolveWorkspacePort, Z as writePortPreference, Zt as clearAgentPreference, _ as startKissJob, _t as latestKissRun, a as wasBlocked, an as agentFor, at as isTerminal, bt as loadWorkspace, c as updateReview, cn as AgentUnavailableError, d as askCheckout, dt as purposeOf, et as checkCodeFreshness, f as UpdateAlreadyRunningError, gt as indexReviewGroups, h as agentEnvOf, i as liveJobsFor, it as fail, jt as workspaceStamps, k as readMarker, kt as touchWorkspace, l as refreshPrContext, lt as phasesFor, mt as followedRefName, n as deleteReview, nn as AGENT_IDS, nt as allJobs, o as discardJob, ot as loadJob, p as liveUpdateFor, pt as isClaimed, q as MIN_WORKSPACE_PORT, r as lineageIds, rt as endedAt, s as RefreshUnavailableError, sn as AgentCancelledError, st as saveJob, t as deleteLineage, tn as STORE_ROOT, tt as checkReview, u as isUnchanged, ut as progressSteps, w as telemetryUploadConsent, wt as recentWorkspaces, x as readEvents, y as runJob, yt as lineagePosition, zt as isIssueOutstanding } from "./delete_review-C2wax0ED.js";
|
|
2
2
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { createHash, timingSafeEqual } from "node:crypto";
|
|
@@ -256,7 +256,16 @@ var PATHS = {
|
|
|
256
256
|
};
|
|
257
257
|
/** The icon slot for one rail row. Hidden from assistive technology: the label beside it says it. */
|
|
258
258
|
function railIcon(icon) {
|
|
259
|
-
return `<span class="prb-rail-icon" aria-hidden="true"
|
|
259
|
+
return `<span class="prb-rail-icon" aria-hidden="true">${iconSvg(icon)}</span>`;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* The same drawing without the rail's slot, for somewhere else that means the same thing.
|
|
263
|
+
*
|
|
264
|
+
* The Files tab counts a file's issues and questions with these, so the icon a reader learned from
|
|
265
|
+
* the rail's Issues and Questions rows is the icon they meet on the row. Sized by the caller's CSS.
|
|
266
|
+
*/
|
|
267
|
+
function iconSvg(icon) {
|
|
268
|
+
return `<svg viewBox="0 0 24 24" focusable="false" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">${PATHS[icon]}</svg>`;
|
|
260
269
|
}
|
|
261
270
|
//#endregion
|
|
262
271
|
//#region ../../packages/review-harness/src/domain/utils/file_path.ts
|
|
@@ -3024,6 +3033,10 @@ function freshnessLabel(report) {
|
|
|
3024
3033
|
label: "Check for updates",
|
|
3025
3034
|
state: "unknown"
|
|
3026
3035
|
};
|
|
3036
|
+
if (report.code.state === "fixed") return {
|
|
3037
|
+
label: "Freshness: Fixed",
|
|
3038
|
+
state: "fixed"
|
|
3039
|
+
};
|
|
3027
3040
|
if (report.code.state === "changed") return {
|
|
3028
3041
|
label: report.code.rewritten ? "Code rewritten" : "Code changed",
|
|
3029
3042
|
state: "changed"
|
|
@@ -3062,9 +3075,10 @@ function renderFreshness(report, options = {}) {
|
|
|
3062
3075
|
if (!report) return "";
|
|
3063
3076
|
const alarming = report.code.state === "changed";
|
|
3064
3077
|
const lost = report.code.reason === "missing-commit";
|
|
3078
|
+
const unavailable = options.availability?.state === "missing";
|
|
3065
3079
|
const dismissed = options.dismissedSha !== void 0 && options.dismissedSha === report.code.latestSha;
|
|
3066
|
-
if (!options.open && (!(alarming || lost) || dismissed)) return "";
|
|
3067
|
-
const status = codeStatus(report.code);
|
|
3080
|
+
if (!options.open && !unavailable && (!(alarming || lost) || dismissed)) return "";
|
|
3081
|
+
const status = codeStatus(report.code, options.availability ?? null);
|
|
3068
3082
|
const facts = [...status.facts, `checked ${escapeHtml$1(relativeTime$1(report.checkedAt))}`];
|
|
3069
3083
|
return `<div class="prb-fresh" data-state="${alarming || lost ? "changed" : "quiet"}" role="status">
|
|
3070
3084
|
<div class="prb-fresh-head">
|
|
@@ -3142,7 +3156,13 @@ function short(sha) {
|
|
|
3142
3156
|
function reviewed(sha) {
|
|
3143
3157
|
return sha ? [`reviewed ${short(sha)}`] : [];
|
|
3144
3158
|
}
|
|
3145
|
-
function codeStatus(code) {
|
|
3159
|
+
function codeStatus(code, availability) {
|
|
3160
|
+
if (code.state === "fixed") return {
|
|
3161
|
+
state: "fixed",
|
|
3162
|
+
title: "Freshness: Fixed",
|
|
3163
|
+
facts: reviewed(code.reviewedSha),
|
|
3164
|
+
consequence: availability?.state === "missing" ? "That commit is no longer in this repository. The review still reads in full, but it cannot be analysed again until the commit is back." : "This review is of one commit, and a commit does not change, so these findings keep describing it."
|
|
3165
|
+
};
|
|
3146
3166
|
if (code.state === "current") return {
|
|
3147
3167
|
state: "current",
|
|
3148
3168
|
title: "Review up to date",
|
|
@@ -3379,9 +3399,10 @@ function renderWorkspace(view) {
|
|
|
3379
3399
|
<body data-api-base="${escapeHtml$1(view.apiBase)}"${view.token ? ` data-token="${escapeHtml$1(view.token)}"` : ""}${view.editor ? ` data-editor-label="${escapeHtml$1(view.editor.label)}" data-editor-template="${escapeHtml$1(view.editor.template)}"` : ""}${view.repoPath ? ` data-repo-path="${escapeHtml$1(view.repoPath)}"` : ""}${view.revision === void 0 ? "" : ` data-workspace-revision="${escapeHtml$1(String(view.revision))}"`} data-session-id="${escapeHtml$1(view.session.id)}" data-explain-prompt="${escapeHtml$1(EXPLAIN_SIMPLY_PROMPT)}" data-way-back="${escapeHtml$1(wayBackSentence())}" data-default-view="${session.result ? "overview" : showsProgress(view) ? "progress" : "kiss"}">
|
|
3380
3400
|
${renderHeader(view, freshness)}
|
|
3381
3401
|
${showsProgress(view) ? "" : renderJobState(view.job ?? null)}
|
|
3382
|
-
<div class="prb-fresh-region" data-fresh-region${freshness ? ` data-report="${escapeHtml$1(JSON.stringify(freshness))}"` : ""}${view.dismissedFreshnessSha ? ` data-dismissed-sha="${escapeHtml$1(view.dismissedFreshnessSha)}"` : ""}>${renderFreshness(freshness ?? null, {
|
|
3402
|
+
<div class="prb-fresh-region" data-fresh-region${freshness ? ` data-report="${escapeHtml$1(JSON.stringify(freshness))}"` : ""}${view.availability ? ` data-availability="${escapeHtml$1(JSON.stringify(view.availability))}"` : ""}${view.dismissedFreshnessSha ? ` data-dismissed-sha="${escapeHtml$1(view.dismissedFreshnessSha)}"` : ""}>${renderFreshness(freshness ?? null, {
|
|
3383
3403
|
dismissedSha: view.dismissedFreshnessSha,
|
|
3384
|
-
actions: view.token !== void 0
|
|
3404
|
+
actions: view.token !== void 0,
|
|
3405
|
+
availability: view.availability ?? null
|
|
3385
3406
|
})}</div>
|
|
3386
3407
|
${renderUpdateProgress(view.updateJob)}
|
|
3387
3408
|
${renderUpdateOutcome(view.lastUpdate)}
|
|
@@ -3661,6 +3682,25 @@ function lineageLeft(review) {
|
|
|
3661
3682
|
if (review.unansweredQuestions > 0) parts.push(`${review.unansweredQuestions} unanswered question${review.unansweredQuestions === 1 ? "" : "s"}`);
|
|
3662
3683
|
return parts.length === 0 ? "" : `<span class="prb-lineage-left">${escapeHtml$1(parts.join(", "))}</span>`;
|
|
3663
3684
|
}
|
|
3685
|
+
/**
|
|
3686
|
+
* What the review is of, and what it was compared against.
|
|
3687
|
+
*
|
|
3688
|
+
* A commit review says `Commit` in words, because a short sha where a branch name usually sits
|
|
3689
|
+
* reads as a branch nobody has. A merge names the first parent, because which parent the diff was
|
|
3690
|
+
* taken against is the one fact about a merge's diff a reader cannot work out from the page.
|
|
3691
|
+
*/
|
|
3692
|
+
function renderSubject(changeSet) {
|
|
3693
|
+
const arrow = "<span class=\"prb-arrow\" aria-label=\"compared against\">→</span>";
|
|
3694
|
+
if (changeSet.targetType !== "commit") return `<strong>${escapeHtml$1(changeSet.headRef)}</strong>
|
|
3695
|
+
${arrow}
|
|
3696
|
+
<span>${escapeHtml$1(changeSet.baseRef)}</span>`;
|
|
3697
|
+
const sha = changeSet.data.pullRequest.head.sha;
|
|
3698
|
+
const parent = changeSet.parentSha;
|
|
3699
|
+
const against = !parent ? "the empty tree" : `${(changeSet.parentCount ?? 1) > 1 ? "first parent" : "parent"} <code>${escapeHtml$1(parent.slice(0, 7))}</code>`;
|
|
3700
|
+
return `<strong>Commit <code>${escapeHtml$1(sha.slice(0, 7))}</code></strong>
|
|
3701
|
+
${arrow}
|
|
3702
|
+
<span>${against}</span>`;
|
|
3703
|
+
}
|
|
3664
3704
|
function renderHeader(view, freshness) {
|
|
3665
3705
|
const { changeSet } = view;
|
|
3666
3706
|
const additions = changeSet.data.files.reduce((sum, f) => sum + f.additions, 0);
|
|
@@ -3671,9 +3711,7 @@ function renderHeader(view, freshness) {
|
|
|
3671
3711
|
<span class="prb-change-ref" title="${escapeHtml$1(changeSet.repoPath)}">
|
|
3672
3712
|
${changeSet.isLinkedWorktree ? `<span class="prb-checkout">${escapeHtml$1(changeSet.checkoutName)}</span>` : ""}
|
|
3673
3713
|
${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>` : ""}
|
|
3674
|
-
|
|
3675
|
-
<span class="prb-arrow" aria-label="compared against">→</span>
|
|
3676
|
-
<span>${escapeHtml$1(changeSet.baseRef)}</span>
|
|
3714
|
+
${renderSubject(changeSet)}
|
|
3677
3715
|
${changeSet.includesUncommitted ? "<span class=\"prb-file-status\">+ working tree</span>" : ""}
|
|
3678
3716
|
${renderAuthor(view)}
|
|
3679
3717
|
</span>
|
|
@@ -4046,7 +4084,7 @@ function renderDisclosure(view) {
|
|
|
4046
4084
|
function renderFiles(view, sections) {
|
|
4047
4085
|
const files = view.changeSet.data.files;
|
|
4048
4086
|
const byPath = new Map(files.map((file) => [file.name, file]));
|
|
4049
|
-
const references = collectReferences(view
|
|
4087
|
+
const references = collectReferences(view);
|
|
4050
4088
|
const claimed = /* @__PURE__ */ new Set();
|
|
4051
4089
|
const groups = sections.map((section) => {
|
|
4052
4090
|
const sectionFiles = section.files.map((file) => byPath.get(file.path)).filter((file) => {
|
|
@@ -4102,7 +4140,7 @@ function referencedGroup(view, sections, references) {
|
|
|
4102
4140
|
*/
|
|
4103
4141
|
function referencedRow(view, file, titles, refs) {
|
|
4104
4142
|
const { dir, name } = splitFilePath(file.path);
|
|
4105
|
-
const
|
|
4143
|
+
const counts = fileCounts(refs);
|
|
4106
4144
|
const named = file.sectionIds.map((id) => titles.get(id)).filter((title) => Boolean(title));
|
|
4107
4145
|
const by = named.length > 0 ? `<span class="prb-file-referenced-by">Referenced by: ${escapeHtml$1(named.join(", "))}</span>` : "";
|
|
4108
4146
|
return `<div class="prb-file prb-file-expandable prb-file-referenced" data-file="${escapeHtml$1(file.path)}" role="button" tabindex="0" aria-expanded="false" data-search="${escapeHtml$1(file.path.toLowerCase())}" title="${escapeHtml$1(file.path)}">
|
|
@@ -4111,48 +4149,48 @@ function referencedRow(view, file, titles, refs) {
|
|
|
4111
4149
|
<span class="pr-helper-path-name">${escapeHtml$1(name)}</span>
|
|
4112
4150
|
${by}
|
|
4113
4151
|
</span>
|
|
4114
|
-
${
|
|
4152
|
+
${counts}
|
|
4153
|
+
<span class="prb-file-spacer" aria-hidden="true"></span>
|
|
4115
4154
|
</div>`;
|
|
4116
4155
|
}
|
|
4117
4156
|
/**
|
|
4157
|
+
* How many findings point at a file, drawn the way the rail draws them: its issue and question
|
|
4158
|
+
* icons, each with a number.
|
|
4159
|
+
*
|
|
4160
|
+
* A column of its own before the line counts, rendered on every row whether or not it holds
|
|
4161
|
+
* anything, so the numbers line up down the list. Each count is announced as a sentence ("1 issue")
|
|
4162
|
+
* rather than a native tooltip, and the digits are hidden so they are not read twice.
|
|
4163
|
+
*/
|
|
4164
|
+
function fileCounts(refs) {
|
|
4165
|
+
const count = (n, icon, noun) => n > 0 ? `<span class="prb-file-count prb-file-count-${icon}" role="img" aria-label="${n} ${noun}${n === 1 ? "" : "s"}">${iconSvg(icon)}<span aria-hidden="true">${n}</span></span>` : "";
|
|
4166
|
+
return `<span class="prb-file-counts">${count(refs?.issues ?? 0, "issues", "issue")}${count(refs?.questions ?? 0, "questions", "question")}</span>`;
|
|
4167
|
+
}
|
|
4168
|
+
/**
|
|
4118
4169
|
* Which findings point at each file.
|
|
4119
4170
|
*
|
|
4120
4171
|
* Worth showing on the row because it is the one thing the file list cannot say for itself: a file
|
|
4121
4172
|
* with four issues against it and a file with none look identical by path, status and line count.
|
|
4122
4173
|
*/
|
|
4123
|
-
function collectReferences(view
|
|
4174
|
+
function collectReferences(view) {
|
|
4124
4175
|
const result = /* @__PURE__ */ new Map();
|
|
4125
|
-
const
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
};
|
|
4136
|
-
result.set(path, created);
|
|
4137
|
-
return created;
|
|
4138
|
-
};
|
|
4139
|
-
const note = (path, sectionId, kind) => {
|
|
4140
|
-
if (!path) return;
|
|
4141
|
-
const record = entry(path);
|
|
4142
|
-
record[kind] += 1;
|
|
4143
|
-
const title = sectionId && sectionId !== owner.get(path) ? titles.get(sectionId) : void 0;
|
|
4144
|
-
if (title && !record.otherSections.includes(title)) record.otherSections.push(title);
|
|
4145
|
-
};
|
|
4146
|
-
const noteRelated = (paths, kind) => {
|
|
4147
|
-
for (const path of paths ?? []) entry(path)[kind] += 1;
|
|
4176
|
+
const note = (paths, kind) => {
|
|
4177
|
+
for (const path of paths) {
|
|
4178
|
+
if (!path) continue;
|
|
4179
|
+
const record = result.get(path) ?? {
|
|
4180
|
+
issues: 0,
|
|
4181
|
+
questions: 0
|
|
4182
|
+
};
|
|
4183
|
+
record[kind] += 1;
|
|
4184
|
+
result.set(path, record);
|
|
4185
|
+
}
|
|
4148
4186
|
};
|
|
4149
4187
|
for (const issue of view.session.result?.issues ?? []) {
|
|
4150
|
-
note(issue.file,
|
|
4151
|
-
|
|
4188
|
+
note([issue.file], "issues");
|
|
4189
|
+
note(issue.relatedFiles ?? [], "issues");
|
|
4152
4190
|
}
|
|
4153
4191
|
for (const question of view.session.result?.questions ?? []) {
|
|
4154
|
-
note(question.file,
|
|
4155
|
-
|
|
4192
|
+
note([question.file], "questions");
|
|
4193
|
+
note(question.relatedFiles ?? [], "questions");
|
|
4156
4194
|
}
|
|
4157
4195
|
return result;
|
|
4158
4196
|
}
|
|
@@ -4165,25 +4203,21 @@ function fileGroup(view, heading, files, references) {
|
|
|
4165
4203
|
}
|
|
4166
4204
|
function fileRow(view, file, refs) {
|
|
4167
4205
|
const { dir, name } = splitFilePath(file.name);
|
|
4168
|
-
const
|
|
4169
|
-
...refs?.issues ? [`<span class="prb-file-ref prb-file-ref-issue">${refs.issues} issue${refs.issues === 1 ? "" : "s"}</span>`] : [],
|
|
4170
|
-
...refs?.questions ? [`<span class="prb-file-ref">${refs.questions} question${refs.questions === 1 ? "" : "s"}</span>`] : [],
|
|
4171
|
-
...(refs?.otherSections ?? []).map((title) => `<span class="prb-file-ref">also ${escapeHtml$1(title)}</span>`)
|
|
4172
|
-
].join("");
|
|
4206
|
+
const counts = fileCounts(refs);
|
|
4173
4207
|
const readable = !(file.additions === 0 && file.deletions === 0 && !file.patch);
|
|
4174
4208
|
const openEditor = !readable && view.editor ? `<button type="button" class="prb-file-open" data-open-editor="${escapeHtml$1(file.name)}" data-open-line="1" title="Open in ${escapeHtml$1(view.editor.label)}">Open in ${escapeHtml$1(view.editor.label)}</button>` : "";
|
|
4175
4209
|
const link = readable ? null : prFileLink(view, file.name);
|
|
4176
4210
|
const openPr = link ? `<a class="prb-file-open prb-file-pr" href="${escapeHtml$1(link.href)}" target="_blank" rel="noopener noreferrer" title="${escapeHtml$1(link.label)}">Pull request</a>` : "";
|
|
4177
4211
|
const attributes = readable ? ` data-file="${escapeHtml$1(file.name)}" role="button" tabindex="0" aria-expanded="false"` : "";
|
|
4178
4212
|
return `<div class="prb-file${readable ? " prb-file-expandable" : ""}"${attributes} data-search="${escapeHtml$1(file.name.toLowerCase())}" title="${escapeHtml$1(file.name)}">
|
|
4179
|
-
<span class="prb-file-status">${escapeHtml$1(file.status)}</span>
|
|
4213
|
+
<span class="prb-file-status" data-status="${escapeHtml$1(file.status)}">${escapeHtml$1(file.status)}</span>
|
|
4180
4214
|
<span class="prb-file-path">
|
|
4181
4215
|
${dir ? `<span class="pr-helper-path-dir">${escapeHtml$1(dir)}</span>` : ""}
|
|
4182
4216
|
<span class="pr-helper-path-name">${escapeHtml$1(name)}</span>
|
|
4183
4217
|
</span>
|
|
4184
4218
|
${openPr}
|
|
4185
4219
|
${openEditor}
|
|
4186
|
-
${
|
|
4220
|
+
${counts}
|
|
4187
4221
|
<span class="prb-file-stat"><span class="prb-add">+${file.additions}</span> <span class="prb-del">−${file.deletions}</span></span>
|
|
4188
4222
|
</div>`;
|
|
4189
4223
|
}
|
|
@@ -4561,7 +4595,7 @@ function renderCard(group, now, analysis, queued) {
|
|
|
4561
4595
|
const review = group.latest;
|
|
4562
4596
|
const status = statusOf(review);
|
|
4563
4597
|
const href = `/r/${encodeURIComponent(review.id)}?t=${encodeURIComponent(review.token)}`;
|
|
4564
|
-
const followed = followedRefName(review.branch, review.headResolvedRef) ?? review.branch;
|
|
4598
|
+
const followed = review.targetType === "commit" ? `commit ${(review.commitSha ?? review.branch).slice(0, 7)}` : followedRefName(review.branch, review.headResolvedRef) ?? review.branch;
|
|
4565
4599
|
return `<article class="prb-card" data-card data-id="${escapeHtml$1(review.id)}" data-status="${status}" data-pinned="${review.pinned}"${queued === void 0 ? "" : ` data-queue="${queued}"`}${review.authorHandle ? ` data-author="${escapeHtml$1(review.authorHandle)}"` : ""} data-reviews="${group.history.length}" data-search-text="${escapeHtml$1(groupSearchText(group))}">
|
|
4566
4600
|
<input type="checkbox" class="prb-card-select" data-select="${escapeHtml$1(review.id)}" aria-label="Select ${escapeHtml$1(followed)}">
|
|
4567
4601
|
<button type="button" class="prb-card-pin" data-pin="${escapeHtml$1(review.id)}" aria-pressed="${review.pinned}" aria-label="${review.pinned ? "Unsave" : "Save"} this review">${review.pinned ? "★" : "☆"}</button>
|
|
@@ -4824,6 +4858,7 @@ function searchText(review) {
|
|
|
4824
4858
|
review.repoPath,
|
|
4825
4859
|
review.branch,
|
|
4826
4860
|
followedRefName(review.branch, review.headResolvedRef) ?? "",
|
|
4861
|
+
review.targetType === "commit" ? review.commitSha ?? "" : "",
|
|
4827
4862
|
review.title,
|
|
4828
4863
|
review.summary,
|
|
4829
4864
|
review.author ?? "",
|
|
@@ -6571,7 +6606,8 @@ function recordWorkspaceOpened(workspace) {
|
|
|
6571
6606
|
if (event) record(event);
|
|
6572
6607
|
}
|
|
6573
6608
|
async function sendWorkspacePage(res, workspace) {
|
|
6574
|
-
const
|
|
6609
|
+
const standing = await standingOrNull(workspace);
|
|
6610
|
+
const freshness = standing?.freshness ?? null;
|
|
6575
6611
|
const kissFreshness = await kissFreshnessOrNull(workspace);
|
|
6576
6612
|
const kissRun = latestKissRun(workspace);
|
|
6577
6613
|
const notice = workspace.lastUpdate?.acknowledged ? void 0 : workspace.lastUpdate;
|
|
@@ -6600,6 +6636,7 @@ async function sendWorkspacePage(res, workspace) {
|
|
|
6600
6636
|
processed: processDiscussion(workspace.prContext.discussion)
|
|
6601
6637
|
} : void 0,
|
|
6602
6638
|
freshness: freshness ?? void 0,
|
|
6639
|
+
availability: standing?.availability ?? null,
|
|
6603
6640
|
dismissedFreshnessSha: workspace.dismissedFreshnessSha,
|
|
6604
6641
|
lastPrRefresh: workspace.lastPrRefresh,
|
|
6605
6642
|
lastUpdate: notice,
|
|
@@ -6710,7 +6747,8 @@ function jobReport(workspace) {
|
|
|
6710
6747
|
*/
|
|
6711
6748
|
async function handleFreshness(res, workspace) {
|
|
6712
6749
|
const kiss = await kissFreshnessOrNull(workspace);
|
|
6713
|
-
const
|
|
6750
|
+
const standing = await standingOrNull(workspace);
|
|
6751
|
+
const report = standing?.freshness ?? null;
|
|
6714
6752
|
const revision = workspaceRevision(workspace) ?? null;
|
|
6715
6753
|
if (!report) return send(res, 200, {
|
|
6716
6754
|
report: null,
|
|
@@ -6727,6 +6765,7 @@ async function handleFreshness(res, workspace) {
|
|
|
6727
6765
|
});
|
|
6728
6766
|
send(res, 200, {
|
|
6729
6767
|
report,
|
|
6768
|
+
availability: standing?.availability ?? null,
|
|
6730
6769
|
dismissedSha: workspace.dismissedFreshnessSha ?? null,
|
|
6731
6770
|
kiss,
|
|
6732
6771
|
kissDismissedSha: workspace.dismissedKissFreshnessSha ?? null,
|
|
@@ -6895,9 +6934,9 @@ async function handleKiss(req, res, workspace) {
|
|
|
6895
6934
|
* to serve a review that is sitting complete on disk. The page renders without the banner, which
|
|
6896
6935
|
* reads as "not checked" rather than as "nothing has changed".
|
|
6897
6936
|
*/
|
|
6898
|
-
async function
|
|
6937
|
+
async function standingOrNull(workspace) {
|
|
6899
6938
|
try {
|
|
6900
|
-
return await
|
|
6939
|
+
return await checkReview(workspace);
|
|
6901
6940
|
} catch {
|
|
6902
6941
|
return null;
|
|
6903
6942
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prreviewbuddy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.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
|
@@ -177,11 +177,16 @@ body > * { flex: 0 0 auto; }
|
|
|
177
177
|
border-bottom: 1px solid var(--border);
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/* One line, always. The header is a fixed height, and anything that wrapped inside it (`Commit
|
|
181
|
+
2e8bf41`, `parent 39ad3d3`) broke across two lines in a bar with room for one. On a narrow
|
|
182
|
+
window this clips rather than wraps; the author's name already ellipsizes inside it. */
|
|
180
183
|
.prb-change-ref {
|
|
181
184
|
display: flex;
|
|
182
185
|
align-items: center;
|
|
183
186
|
gap: 8px;
|
|
184
187
|
min-width: 0;
|
|
188
|
+
white-space: nowrap;
|
|
189
|
+
overflow: hidden;
|
|
185
190
|
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
|
|
186
191
|
font-size: 12.5px;
|
|
187
192
|
color: var(--text-secondary);
|
|
@@ -197,6 +202,9 @@ body > * { flex: 0 0 auto; }
|
|
|
197
202
|
font-size: 12.5px;
|
|
198
203
|
color: var(--text-secondary);
|
|
199
204
|
}
|
|
205
|
+
/* Each control on one line, for the same reason as the change ref. Not the `...` menu: its panel is
|
|
206
|
+
a fixed-width box hanging below the header, and its items have to wrap inside it. */
|
|
207
|
+
.prb-header-stats > :not(.prb-menu) { white-space: nowrap; }
|
|
200
208
|
.prb-header-stats .prb-add { color: var(--success); }
|
|
201
209
|
.prb-header-stats .prb-del { color: var(--danger); }
|
|
202
210
|
|
|
@@ -846,17 +854,18 @@ a.prb-rail-item { text-decoration: none; }
|
|
|
846
854
|
/* Directory above, filename below in purple, which is how every other file citation in the
|
|
847
855
|
product is drawn. The classes are the extension's own so the two cannot drift apart. */
|
|
848
856
|
.prb-file-path { flex: 1 1 auto; min-width: 0; line-height: 1.35; }
|
|
849
|
-
/*
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
.prb-file-
|
|
853
|
-
display: flex;
|
|
857
|
+
/* A file's issue and question counts: the rail's own icons, each with a number, in a column before
|
|
858
|
+
the line counts. A fixed width that is rendered on every row, empty or not, so the numbers read
|
|
859
|
+
down the list as a column and never take width from the filename. */
|
|
860
|
+
.prb-file-counts {
|
|
854
861
|
flex: none;
|
|
855
|
-
|
|
856
|
-
align-items: center;
|
|
862
|
+
display: flex;
|
|
857
863
|
justify-content: flex-end;
|
|
858
|
-
|
|
859
|
-
|
|
864
|
+
align-items: center;
|
|
865
|
+
gap: 10px;
|
|
866
|
+
min-width: 64px;
|
|
867
|
+
font-size: 11.5px;
|
|
868
|
+
font-variant-numeric: tabular-nums;
|
|
860
869
|
}
|
|
861
870
|
|
|
862
871
|
/* What the path alone cannot say: a file with four issues against it and one with none look
|
|
@@ -867,22 +876,14 @@ a.prb-rail-item { text-decoration: none; }
|
|
|
867
876
|
--surface / --border / --text-* tokens are the shell's, so a chip styled with them renders as a
|
|
868
877
|
dark box in the middle of a light list — legible in neither theme, because it belongs to the
|
|
869
878
|
other one. Use the pane's own literals, as .prb-file-status and .prb-file-name already do. */
|
|
870
|
-
.prb-file-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
border: 1px solid rgba(15, 23, 42, 0.15);
|
|
875
|
-
background: rgba(15, 23, 42, 0.04);
|
|
879
|
+
.prb-file-count {
|
|
880
|
+
display: inline-flex;
|
|
881
|
+
align-items: center;
|
|
882
|
+
gap: 3px;
|
|
876
883
|
color: #57606a;
|
|
877
|
-
font-family: inherit;
|
|
878
|
-
font-size: 10.5px;
|
|
879
|
-
letter-spacing: 0.01em;
|
|
880
|
-
}
|
|
881
|
-
.prb-file-ref-issue {
|
|
882
|
-
border-color: rgba(207, 34, 46, 0.3);
|
|
883
|
-
background: rgba(207, 34, 46, 0.07);
|
|
884
|
-
color: #cf222e;
|
|
885
884
|
}
|
|
885
|
+
.prb-file-count svg { width: 13px; height: 13px; flex: none; }
|
|
886
|
+
.prb-file-count-issues { color: #cf222e; }
|
|
886
887
|
|
|
887
888
|
/* Files the review leant on that this change does not touch. Pane literals throughout, per the
|
|
888
889
|
note above.
|
|
@@ -971,10 +972,17 @@ a.prb-rail-item { text-decoration: none; }
|
|
|
971
972
|
font-variant-numeric: tabular-nums;
|
|
972
973
|
font-size: 11.5px;
|
|
973
974
|
}
|
|
975
|
+
/* The line-count column's width, on a referenced row that has no line counts, so its finding counts
|
|
976
|
+
still line up with the changed files above it. Kept in step with `.prb-file-stat`'s min-width. */
|
|
977
|
+
.prb-file-spacer { flex: none; min-width: 74px; }
|
|
974
978
|
.prb-file-stat .prb-add { color: #1a7f37; }
|
|
975
979
|
.prb-file-stat .prb-del { color: #cf222e; }
|
|
980
|
+
/* One width for every status, so the path beside it starts at the same edge on every row. Sized for
|
|
981
|
+
the longest word, UNCHANGED, plus the padding, whichever box-sizing is in force. */
|
|
976
982
|
.prb-file-status {
|
|
977
983
|
flex: none;
|
|
984
|
+
min-width: calc(10ch + 10px);
|
|
985
|
+
text-align: center;
|
|
978
986
|
padding: 0 5px;
|
|
979
987
|
border-radius: 3px;
|
|
980
988
|
background: rgba(175, 184, 193, 0.24);
|
|
@@ -983,6 +991,16 @@ a.prb-rail-item { text-decoration: none; }
|
|
|
983
991
|
text-transform: uppercase;
|
|
984
992
|
letter-spacing: 0.04em;
|
|
985
993
|
}
|
|
994
|
+
/* One colour per kind of change, so a new file and an edited one are told apart without reading
|
|
995
|
+
the word. Added and removed take the pane's own `+` and `−` colours, which the row's line counts
|
|
996
|
+
already use for the same meaning. Pane literals, per the palette note above. `unchanged` keeps
|
|
997
|
+
the grey. */
|
|
998
|
+
.prb-file-status[data-status='added'] { background: rgba(26, 127, 55, 0.12); color: #1a7f37; }
|
|
999
|
+
.prb-file-status[data-status='removed'] { background: rgba(207, 34, 46, 0.1); color: #cf222e; }
|
|
1000
|
+
.prb-file-status[data-status='modified'],
|
|
1001
|
+
.prb-file-status[data-status='changed'] { background: rgba(212, 167, 44, 0.18); color: #9a6700; }
|
|
1002
|
+
.prb-file-status[data-status='renamed'],
|
|
1003
|
+
.prb-file-status[data-status='copied'] { background: rgba(9, 105, 218, 0.1); color: #0969da; }
|
|
986
1004
|
|
|
987
1005
|
/* ── Ask panel ── */
|
|
988
1006
|
.prb-ask {
|