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/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 checkFreshness, 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-B_2J2Wn9.js";
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 three flags that say what is being acted on, and the field each becomes. */
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 = lineageKeyFor(target.originRepoPath, target.branch, target.ref);
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 code = (await checkFreshness(workspace)).code;
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 subject = followedRefName(existing.branch, existing.headResolvedRef) ?? existing.branch;
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 checkFreshness, 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-B_2J2Wn9.js";
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"><svg viewBox="0 0 24 24" focusable="false" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">${PATHS[icon]}</svg></span>`;
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\">&rarr;</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
- <strong>${escapeHtml$1(changeSet.headRef)}</strong>
3675
- <span class="prb-arrow" aria-label="compared against">&rarr;</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, sections);
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 badges = [...refs?.issues ? [`<span class="prb-file-ref prb-file-ref-issue">${refs.issues} issue${refs.issues === 1 ? "" : "s"}</span>`] : [], ...refs?.questions ? [`<span class="prb-file-ref">${refs.questions} question${refs.questions === 1 ? "" : "s"}</span>`] : []].join("");
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
- ${badges ? `<span class="prb-file-badges">${badges}</span>` : ""}
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, sections) {
4174
+ function collectReferences(view) {
4124
4175
  const result = /* @__PURE__ */ new Map();
4125
- const owner = /* @__PURE__ */ new Map();
4126
- for (const section of sections) for (const file of section.files) if (!owner.has(file.path)) owner.set(file.path, section.id);
4127
- const titles = new Map(sections.map((section) => [section.id, `${section.order}. ${section.title}`]));
4128
- const entry = (path) => {
4129
- const existing = result.get(path);
4130
- if (existing) return existing;
4131
- const created = {
4132
- issues: 0,
4133
- questions: 0,
4134
- otherSections: []
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, issue.sectionId, "issues");
4151
- noteRelated(issue.relatedFiles, "issues");
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, question.sectionId, "questions");
4155
- noteRelated(question.relatedFiles, "questions");
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 badges = [
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
- ${badges ? `<span class="prb-file-badges">${badges}</span>` : ""}
4220
+ ${counts}
4187
4221
  <span class="prb-file-stat"><span class="prb-add">+${file.additions}</span> <span class="prb-del">&minus;${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 ? "&#9733;" : "&#9734;"}</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 freshness = await freshnessOrNull(workspace);
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 report = await freshnessOrNull(workspace);
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 freshnessOrNull(workspace) {
6937
+ async function standingOrNull(workspace) {
6899
6938
  try {
6900
- return await checkFreshness(workspace);
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.30.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
- /* The pills are a column of their own, sitting between the buttons and the line counts rather
850
- than trailing the path. Read down the list they answer one question at a glance: which files
851
- have something to say about them. Trailing the path they moved with every filename length. */
852
- .prb-file-badges {
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
- flex-wrap: wrap;
856
- align-items: center;
862
+ display: flex;
857
863
  justify-content: flex-end;
858
- gap: 6px;
859
- min-width: 0;
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-ref {
871
- flex: none;
872
- padding: 1px 6px;
873
- border-radius: 999px;
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 {