prreviewbuddy 0.69.1 → 0.71.1

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/server.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as MAX_WORKSPACE_PORT, $n as isClaimed, $r as resolveAgent, $t as markReadForReview, A as relativeTime$1, An as isWorktreeHeld, Ar as parseLifecycle, At as partFiles, B as readIndexToken, Bn as reviewedShaOf, Br as issuesOutstanding, Bt as isArchitectureId, C as track, Cn as originRemote, Cr as activityAttributes, Ct as mapRuntimes, D as endRun, Dn as MANAGED_ROOT, Dr as lifecycleItemLabel, Dt as ARCHITECTURE_DIR, E as completeRun, En as ReviewBeingDeletedError, Er as lifecycleChip, Et as describeRuntimes, F as runBatch, Fr as queueStamp, Ft as trackedFiles, Gn as loadJob, Gr as describeAuthorship, Gt as createArchitectureNotification, H as clearServerRecordOwnedBy, Hn as endedAt, Hr as reviewerDispositions, Ht as announceJob, I as activateQueued, In as findExistingReview, Ir as readQueue, It as ownersOf, J as stopChild, Jn as isLiveJob, Jr as resolveTarget, Jt as eventName, K as serverEntryExists, Kn as saveJob, Kr as WITHHELD_REASON, Kt as createPrState, L as compactQueue, Ln as checkCodeFreshness, Lr as doneVerb, Lt as QUOTE_LIMITS, M as extractPullRequestLinks, Mn as removeWorktree, Mr as queueAdd, Mt as repositoryKey, N as parsePullRequestLink, Nn as requestJobStop, Nr as queueAddMany, Nt as storeArchitecture, O as reportRun, On as createWorktree, Or as lifecyclePill, Ot as checkArchitecture, P as unsupportedReason, Pr as queueRemove, Pt as storedScopes, Q as DEFAULT_WORKSPACE_PORT, Qr as git, Qt as markRead, Rn as checkReview, Rr as isIssueOutstanding, Rt as REPOSITORY_SCOPE, S as isDriving, Sn as remoteHost, Sr as LIFECYCLES, St as waitingHistory, T as beginRun, Tr as isLifecycle, Tt as RUNTIME_WORDS, Un as fail, Ur as processDiscussion, Ut as branchOf, Vn as allJobs, Vr as questionsOutstanding, Vt as writeJsonAtomically, W as readServerRecord, Wn as isTerminal, Wr as recommendationsOf, Wt as subjectOf, Xn as progressSteps, Xr as isCommitReview, Xt as listNotifications, Yn as phasesFor, Yr as DetachedHeadError, Yt as headline, Z as writeServerRecord, Zn as purposeOf, Zr as resolveBase, Zt as markAllRead, _ as liveUpdateFor, _n as extractJsonText, _t as telemetryUploadConsent, a as wasBlocked, ai as STORE_ROOT, an as withUsageRecorded, ar as kissDecisionRefusal, at as failureOf$1, bn as forgeResolver, br as workspaceRevision, c as RefreshUnavailableError, ci as agentFor$1, cn as authorPreview, cr as lineagePosition, ct as reanalyseReview, d as mergeUpdateInto, di as AgentUnavailableError, dn as renderMarkdown, dr as mutateWorkspace, ei as resolveModel, en as pruneNotifications, er as decideKissRecommendation, et as MIN_WORKSPACE_PORT, f as updateReview, fn as CODE_BLOCK_CLASS, ft as readEvents, g as UpdateAlreadyRunningError, gn as asString, gr as reviewedCommit, gt as recordTelemetryUploadConsent, h as askCheckout, hn as renderInlineText, hr as recentWorkspaces, i as liveJobsFor, in as unreadCount$1, ir as indexReviewGroups, it as writePortPreference, j as batchIdentity, jn as readMarker, jr as sectionOf, jt as readArchitecture, k as stopRun, kn as holdWorktree, kr as offersLifecycle, kt as loadArchitecture, l as cancelUpdate, li as detectAgents, ln as hasAuthorContent, lr as loadWorkspace, lt as runJob, m as isUnchanged, mn as escapeHtml$1, mr as previousKissResult, n as deleteReview, ni as readAgentPreference, nn as removeNotification, nr as groupByLineage, nt as resolveWorkspacePort, o as discardJob, oi as AGENT_IDS, on as EXPLAIN_SIMPLY_PROMPT, or as kissDecisionsOf, ot as prepareReview, p as refreshPrContext, pn as CODE_COPY_CLASS, pr as presentedKissRunOf, pt as record, q as spawnServer, qr as kissOutcome, qt as createPrUpdate, r as lineageIds, ri as writeAgentPreference, rn as toneOf, rr as indexKeptIds, rt as statedWorkspacePort, s as FETCH_TIMEOUT_MS, si as agentById, sn as UNESTABLISHED_MOTIVATION, sr as latestKissRun, st as settleReview, t as deleteLineage, ti as clearAgentPreference, tn as readNotification, tr as followedRefName, tt as parseWorkspacePort, u as cancellableHere, ui as AgentCancelledError, un as GITHUB_MARKDOWN, vn as objectWithAnyOf, w as RUN_KINDS, wn as fillFileUrlTemplate, wr as activityLabel, wt as recordRuntimeBestEffort, x as startKissJob, xn as forgeFor, xr as workspaceStamps, xt as feedbackUrl, y as agentEnvOf, yn as claimRequestUrl, yr as touchWorkspace, yt as BUILD_VERSION, z as settleQueue, zn as followedBranch, zr as isQuestionOutstanding, zt as ancestorsOf } from "./delete_review-CRPRIRKg.js";
1
+ import { $ as MAX_WORKSPACE_PORT, $n as isClaimed, $r as resolveBase, $t as markReadForReview, A as relativeTime$1, An as isWorktreeHeld, Ar as parseLifecycle, At as partFiles, B as readIndexToken, Bn as reviewedShaOf, Br as issuesOutstanding, Bt as isArchitectureId, C as track, Cn as originRemote, Cr as activityAttributes, Ct as mapRuntimes, D as endRun, Dn as MANAGED_ROOT, Dr as lifecycleItemLabel, Dt as ARCHITECTURE_DIR, E as completeRun, En as ReviewBeingDeletedError, Er as lifecycleChip, Et as describeRuntimes, F as runBatch, Fr as queueStamp, Ft as trackedFiles, Gn as loadJob, Gr as simpleExplanationOf, Gt as createArchitectureNotification, H as clearServerRecordOwnedBy, Hn as endedAt, Hr as reviewerDispositions, Ht as announceJob, I as activateQueued, In as findExistingReview, Ir as readQueue, It as ownersOf, J as stopChild, Jn as isLiveJob, Jr as WITHHELD_REASON, Jt as eventName, K as serverEntryExists, Kn as saveJob, Kr as withSimpleExplanation, Kt as createPrState, L as compactQueue, Ln as checkCodeFreshness, Lr as doneVerb, Lt as QUOTE_LIMITS, M as extractPullRequestLinks, Mn as removeWorktree, Mr as queueAdd, Mt as repositoryKey, N as parsePullRequestLink, Nn as requestJobStop, Nr as queueAddMany, Nt as storeArchitecture, O as reportRun, On as createWorktree, Or as lifecyclePill, Ot as checkArchitecture, P as unsupportedReason, Pr as queueRemove, Pt as storedScopes, Q as DEFAULT_WORKSPACE_PORT, Qr as isCommitReview, Qt as markRead, Rn as checkReview, Rr as isIssueOutstanding, Rt as REPOSITORY_SCOPE, S as isDriving, Sn as remoteHost, Sr as LIFECYCLES, St as waitingHistory, T as beginRun, Tr as isLifecycle, Tt as RUNTIME_WORDS, Un as fail, Ur as processDiscussion, Ut as branchOf, Vn as allJobs, Vr as questionsOutstanding, Vt as writeJsonAtomically, W as readServerRecord, Wn as isTerminal, Wr as recommendationsOf, Wt as subjectOf, Xn as progressSteps, Xr as resolveTarget, Xt as listNotifications, Yn as phasesFor, Yr as kissOutcome, Yt as headline, Z as writeServerRecord, Zn as purposeOf, Zr as DetachedHeadError, Zt as markAllRead, _ as liveUpdateFor, _n as extractJsonText, _t as telemetryUploadConsent, a as wasBlocked, ai as writeAgentPreference, an as withUsageRecorded, ar as kissDecisionRefusal, at as failureOf$1, bn as forgeResolver, br as workspaceRevision, c as RefreshUnavailableError, ci as AGENT_IDS, cn as authorPreview, cr as lineagePosition, ct as reanalyseReview, d as mergeUpdateInto, di as detectAgents, dn as renderMarkdown, dr as mutateWorkspace, ei as git, en as pruneNotifications, er as decideKissRecommendation, et as MIN_WORKSPACE_PORT, f as updateReview, fi as AgentCancelledError, fn as CODE_BLOCK_CLASS, ft as readEvents, g as UpdateAlreadyRunningError, gn as asString, gr as reviewedCommit, gt as recordTelemetryUploadConsent, h as askCheckout, hn as renderInlineText, hr as recentWorkspaces, i as liveJobsFor, ii as readAgentPreference, in as unreadCount$1, ir as indexReviewGroups, it as writePortPreference, j as batchIdentity, jn as readMarker, jr as sectionOf, jt as readArchitecture, k as stopRun, kn as holdWorktree, kr as offersLifecycle, kt as loadArchitecture, l as cancelUpdate, li as agentById, ln as hasAuthorContent, lr as loadWorkspace, lt as runJob, m as isUnchanged, mn as escapeHtml$1, mr as previousKissResult, n as deleteReview, ni as resolveModel, nn as removeNotification, nr as groupByLineage, nt as resolveWorkspacePort, o as discardJob, on as EXPLAIN_SIMPLY_PROMPT, or as kissDecisionsOf, ot as prepareReview, p as refreshPrContext, pi as AgentUnavailableError, pn as CODE_COPY_CLASS, pr as presentedKissRunOf, pt as record, q as spawnServer, qr as describeAuthorship, qt as createPrUpdate, r as lineageIds, ri as clearAgentPreference, rn as toneOf, rr as indexKeptIds, rt as statedWorkspacePort, s as FETCH_TIMEOUT_MS, si as STORE_ROOT, sn as UNESTABLISHED_MOTIVATION, sr as latestKissRun, st as settleReview, t as deleteLineage, ti as resolveAgent, tn as readNotification, tr as followedRefName, tt as parseWorkspacePort, u as cancellableHere, ui as agentFor$1, un as GITHUB_MARKDOWN, vn as objectWithAnyOf, w as RUN_KINDS, wn as fillFileUrlTemplate, wr as activityLabel, wt as recordRuntimeBestEffort, x as startKissJob, xn as forgeFor, xr as workspaceStamps, xt as feedbackUrl, y as agentEnvOf, yn as claimRequestUrl, yr as touchWorkspace, yt as BUILD_VERSION, z as settleQueue, zn as followedBranch, zr as isQuestionOutstanding, zt as ancestorsOf } from "./delete_review-BgIZpcsP.js";
2
2
  import { basename, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { execFile } from "node:child_process";
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
@@ -453,6 +453,8 @@ var MESSAGE_COPY_CLASS = "pr-helper-msg-copy";
453
453
  var MESSAGE_ACTIONS_CLASS = "pr-helper-chat-actions";
454
454
  /** The Markdown itself, for surfaces that hold no message state to resolve an id against. */
455
455
  var MESSAGE_SOURCE_ATTR = "data-copy-source";
456
+ /** A finding's text for a feedback draft, on its feedback checkbox. Copy's text less the reviewer's own working. */
457
+ var FEEDBACK_SOURCE_ATTR = "data-feedback-source";
456
458
  var STATES$1 = {
457
459
  idle: {
458
460
  icon: "<path d=\"M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z\"></path><path d=\"M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z\"></path>",
@@ -1095,23 +1097,253 @@ function renderAskBuddyMenu(kind) {
1095
1097
  </div>`;
1096
1098
  }
1097
1099
  //#endregion
1100
+ //#region ../../packages/review-harness/src/domain/ui/components/simple_explanation.ts
1101
+ /**
1102
+ * A finding's simple explanation: Buddy AI's plain-English account of it, kept on the card because
1103
+ * the reviewer asked for one.
1104
+ *
1105
+ * For reading the finding, not part of it. It carries no copy source, so Copy and Prepare feedback,
1106
+ * which take the card's first one, can only ever find the finding's own.
1107
+ *
1108
+ * A native disclosure, closed when the page loads and open when it has just been written. Whether a
1109
+ * reader left it open is not kept: the explanation is the durable part.
1110
+ */
1111
+ var SIMPLE_EXPLANATION_LABEL = "Simple explanation";
1112
+ var ASK_MORE_LABEL = "Ask more →";
1113
+ function renderSimpleExplanation(text, options = {}) {
1114
+ const body = renderMarkdown(text);
1115
+ const askMore = options.askMore ? `<button type="button" class="pr-helper-simple-explanation-more" data-simple-explanation-more>${ASK_MORE_LABEL}</button>` : "";
1116
+ const content = askMore && body.endsWith("</p>") ? `${body.slice(0, -4)} ${askMore}</p>` : body + askMore;
1117
+ return `<details class="pr-helper-simple-explanation" data-simple-explanation${options.open ? " open" : ""}>
1118
+ <summary class="pr-helper-simple-explanation-summary">${SIMPLE_EXPLANATION_LABEL}</summary>
1119
+ <div class="pr-helper-simple-explanation-body pr-helper-markdown">${content}</div>
1120
+ </details>`;
1121
+ }
1122
+ //#endregion
1123
+ //#region ../../packages/review-harness/src/domain/ui/components/pill_tooltips.ts
1124
+ /**
1125
+ * What every pill in the sidebar actually means, in one place.
1126
+ *
1127
+ * A pill is a single uppercase word standing in for a whole axis of judgement, and the axes are
1128
+ * not self-evident: MAJOR and "medium confidence" sit side by side on the same card and answer
1129
+ * completely different questions. Severity is how much the finding would matter *if it is real*;
1130
+ * confidence is how sure the analysis is that it *is* real. A reviewer who has collapsed those
1131
+ * into one mental "how bad is this" scale is reading the card wrong, and no amount of colour
1132
+ * fixes that — only words do.
1133
+ *
1134
+ * The wording is derived from the field guidance the model is actually given (see
1135
+ * STRUCTURED_OUTPUT_INSTRUCTIONS in ai_review/pipeline.ts). This file must not invent a richer
1136
+ * taxonomy than the prompt asks for: if the prompt does not define what separates "major" from
1137
+ * "minor", neither does the tooltip. It names the axis and where the value sits on it, and
1138
+ * leaves the grading to the model that did the grading.
1139
+ *
1140
+ * House style, because a tooltip is read once at a glance and never studied:
1141
+ *
1142
+ * 1. Open with the concept, not a sentence the reader has to parse into one. "PR risk — the
1143
+ * overall risk level assigned to this pull request" lands before the clause finishes;
1144
+ * "Overall risk this analysis assigned the pull request" makes them unpick the grammar first.
1145
+ * 2. Describe what the thing *is*, never how the extension builds it. A follow-up "builds on the
1146
+ * original analysis" — that it is composed into the stored session is our problem, not theirs.
1147
+ * 3. Plain reviewer's English over product vocabulary. "During your review", not "before you
1148
+ * sign off"; "the section of the Guide", not the section's own title read back at them.
1149
+ */
1150
+ /** Ordered worst-first, matching how the prompt lists the enum. */
1151
+ var SEVERITY_SCALE = [
1152
+ "critical",
1153
+ "major",
1154
+ "minor",
1155
+ "nit"
1156
+ ];
1157
+ var ATTENTION_SCALE = [
1158
+ "high",
1159
+ "medium",
1160
+ "low"
1161
+ ];
1162
+ /**
1163
+ * "2 of 4: critical → major → minor → nit" — the value's rank and the whole scale it sits on.
1164
+ *
1165
+ * Both halves earn their place: the rank alone leaves the reader guessing what the other steps
1166
+ * are, and the scale alone makes them count. An arrow rather than a bullet or comma, so the
1167
+ * ordering is visible rather than something the reader has to already know.
1168
+ */
1169
+ function position(value, scale) {
1170
+ const index = scale.indexOf(value);
1171
+ if (index === -1) return scale.join(" → ");
1172
+ return `${index + 1} of ${scale.length}: ${scale.join(" → ")}`;
1173
+ }
1174
+ function severityTip(severity) {
1175
+ return `Severity — how much this would matter if it turns out to be real. ${position(severity, SEVERITY_SCALE)}.`;
1176
+ }
1177
+ /**
1178
+ * Only medium and low ever reach the screen (renderIssueConfidence suppresses "high"), but all
1179
+ * three are worded here so the copy does not depend on that rendering decision staying put.
1180
+ *
1181
+ * The second sentence is the one that matters. Confidence and severity sit side by side on the
1182
+ * same card, and a reader who has not been told otherwise will assume they are the same scale
1183
+ * read twice — so the tooltip says they are not, in as many words.
1184
+ */
1185
+ function confidenceTip(confidence) {
1186
+ return `Confidence — how sure the analysis is that this issue is real. This is separate from how serious it would be. ${{
1187
+ high: "High means the analysis is confident it’s real.",
1188
+ medium: "Medium means it’s worth confirming in the code before acting on it.",
1189
+ low: "Low means it was likely reasoned from the diff alone, so check the surrounding code first."
1190
+ }[confidence]}`;
1191
+ }
1192
+ function importanceTip(importance) {
1193
+ return `Importance — how much attention this question deserves during your review. ${position(importance, ATTENTION_SCALE)}.`;
1194
+ }
1195
+ /**
1196
+ * The four statuses an "Analyse new changes" pass assigns to a finding.
1197
+ *
1198
+ * Each says what the *product* concluded and, where it matters, how much to trust it. "Still open"
1199
+ * carries the load: it is both a real judgement ("the changes do not address this") and the safe
1200
+ * default applied when the analysis said nothing about a finding at all, and the reviewer cannot
1201
+ * tell those apart from the badge — so the copy commits to the weaker of the two readings rather
1202
+ * than claiming a judgement that may not have been made.
1203
+ */
1204
+ function findingStatusTip(status) {
1205
+ return {
1206
+ resolved: "Resolved — the new commits appear to address this finding. Worth a glance to confirm before you close it out.",
1207
+ "still-open": "Still open — the new commits do not address this finding. Findings the analysis did not comment on are shown this way too, so this never disappears on its own.",
1208
+ affected: "Affected — the code this finding concerns has changed, but not in a way that settles it. Re-read this one.",
1209
+ new: "New — introduced by the changes pushed since your previous analysis."
1210
+ }[status];
1211
+ }
1212
+ /** The reviewer-facing label for a status. Kept beside the tooltips so the two cannot drift. */
1213
+ function findingStatusLabel(status) {
1214
+ return {
1215
+ resolved: "Resolved",
1216
+ "still-open": "Still open",
1217
+ affected: "Affected",
1218
+ new: "New"
1219
+ }[status];
1220
+ }
1221
+ /**
1222
+ * What the commits pushed since did to an open question.
1223
+ *
1224
+ * "Still open" carries the same double load it does on a finding: a real judgement, and the default
1225
+ * for a question the reassessment never mentioned. The copy commits to the weaker reading, because
1226
+ * the reviewer must not read silence as an answer.
1227
+ */
1228
+ function questionStatusTip(status) {
1229
+ return {
1230
+ answered: "Answered — the new commits settle this, and the answer is below. Worth a glance to confirm before you drop it.",
1231
+ "still-open": "Still open — the new commits do not settle this. Questions the reassessment did not comment on are shown this way too, so this never disappears on its own.",
1232
+ affected: "Affected — the code this question is about has changed without answering it. Read the question again before you ask it."
1233
+ }[status];
1234
+ }
1235
+ /** The reviewer-facing label for a question status. Kept beside the tooltip so the two cannot drift. */
1236
+ function questionStatusLabel(status) {
1237
+ return {
1238
+ answered: "Answered",
1239
+ "still-open": "Still open",
1240
+ affected: "Affected"
1241
+ }[status];
1242
+ }
1243
+ /**
1244
+ * The four impacts a repository investigation assigns to a finding.
1245
+ *
1246
+ * Every one of these says the evidence came from *outside* the diff, because that is the whole
1247
+ * reason to trust them more than the analysis they revise — and equally the reason to say where
1248
+ * they came from. "A coding agent read the repository" is the qualification, and a reviewer who
1249
+ * does not know that cannot weigh it.
1250
+ *
1251
+ * "Investigated" carries the same load "Still open" does on the other axis: it is both a real
1252
+ * judgement ("this evidence does not bear on that finding") and the default applied when the
1253
+ * reassessment said nothing about a finding at all. The copy commits to the weaker reading rather
1254
+ * than claiming a judgement that may not have been made.
1255
+ */
1256
+ function investigationImpactTip(impact) {
1257
+ return {
1258
+ confirmed: "Confirmed — a repository investigation found evidence supporting this finding. It no longer rests on the diff alone.",
1259
+ refuted: "Refuted — a repository investigation found evidence that this concern does not hold. Kept on the list, with the reasoning, rather than removed.",
1260
+ revised: "Revised — a repository investigation changed what this finding says. Where its severity or confidence moved, the previous value is shown alongside.",
1261
+ unchanged: "Investigated — this finding was reassessed against a repository investigation and stands as it was. Findings the reassessment did not comment on are shown this way too."
1262
+ }[impact];
1263
+ }
1264
+ /**
1265
+ * The reviewer-facing label. Note `unchanged` → "Investigated": the reviewer's world contains a
1266
+ * finding that was looked into and survived, which is information. "Unchanged" describes our
1267
+ * bookkeeping — what the field did — and says nothing about the code.
1268
+ */
1269
+ function investigationImpactLabel(impact) {
1270
+ return {
1271
+ confirmed: "Confirmed",
1272
+ refuted: "Refuted",
1273
+ revised: "Revised",
1274
+ unchanged: "Investigated"
1275
+ }[impact];
1276
+ }
1277
+ var DISMISSED_TIP = "Dismissed by you — you decided this finding needs no action. It stops counting towards what is left, and stays here so the decision is visible rather than lost.";
1278
+ var DISMISSAL_STALE_TIP = "Changed since you dismissed this — the code under this finding has moved, so the reading you dismissed it on no longer holds. Your decision is kept; the finding is back on the list until you confirm it.";
1279
+ var SECTION_TAG_TIP = "Guide section — the section of the Guide this finding belongs to. Click to open it in the Guide tab.";
1280
+ /**
1281
+ * Builds a pill's `class` and `data-tip` attributes together.
1282
+ *
1283
+ * They are emitted as one string rather than left to the call site because they are useless
1284
+ * apart: `data-tip` without `pr-helper-tip` never renders, and `pr-helper-tip` without a tip
1285
+ * renders an empty black rectangle on hover. Taking the pill's own classes as an argument means
1286
+ * there is no way to add the tooltip and clobber the styling, which is what a bare
1287
+ * `class="pr-helper-tip"` helper would have invited.
1288
+ *
1289
+ * Emits a leading space so it drops straight in after a tag name.
1290
+ */
1291
+ function pillAttrs(classNames, tip) {
1292
+ return ` class="${escapeHtml$1(classNames)} pr-helper-tip" data-tip="${escapeHtml$1(tip)}"`;
1293
+ }
1294
+ //#endregion
1098
1295
  //#region ../../packages/review-harness/src/domain/ui/components/finding_copy.ts
1099
- /** An issue: its title in bold, the description, then the location when it has one. */
1296
+ /** An issue's Copy: its title in bold, the description, the update, the investigation, then the location. */
1100
1297
  function issueMarkdown(issue) {
1298
+ return issueText(issue, true);
1299
+ }
1300
+ /** An issue as a feedback draft gives it: its Copy without the investigation. */
1301
+ function issueFeedbackMarkdown(issue) {
1302
+ return issueText(issue, false);
1303
+ }
1304
+ /** A question's Copy: the question in bold, why it matters, the update, the investigation, then the files. */
1305
+ function questionMarkdown(question) {
1306
+ return questionText(question, true);
1307
+ }
1308
+ /** A question as a feedback draft gives it: its Copy without the investigation. */
1309
+ function questionFeedbackMarkdown(question) {
1310
+ return questionText(question, false);
1311
+ }
1312
+ function issueText(issue, investigation) {
1101
1313
  return join$1([
1102
1314
  bold(issue.title),
1103
1315
  issue.description,
1316
+ update(issue.reconciliationStatus && findingStatusLabel(issue.reconciliationStatus), issue.reconciliationNote),
1317
+ ...investigation ? [investigated(issue.investigationImpact, issue.investigationNote)] : [],
1104
1318
  where(issue.file, issue.line, issue.relatedFiles)
1105
1319
  ]);
1106
1320
  }
1107
- /** A question: the question in bold, why it matters when the review said, then the files. */
1108
- function questionMarkdown(question) {
1321
+ function questionText(question, investigation) {
1109
1322
  return join$1([
1110
1323
  bold(question.question),
1111
1324
  question.whyThisMatters,
1325
+ labelled("Answered by the new changes", question.reconciliationAnswer),
1326
+ update(question.reconciliationStatus && questionStatusLabel(question.reconciliationStatus), question.reconciliationNote),
1327
+ ...investigation ? [labelled("Answer from investigation", question.investigationAnswer), investigated(question.investigationImpact, question.investigationNote)] : [],
1112
1328
  where(question.file, void 0, question.relatedFiles)
1113
1329
  ]);
1114
1330
  }
1331
+ /** What the last update said about the finding, under the status the card shows beside it. */
1332
+ function update(status, note) {
1333
+ if (!note?.trim()) return void 0;
1334
+ return `**After the new commits${status ? ` (${status.toLowerCase()})` : ""}:** ${note.trim()}`;
1335
+ }
1336
+ /**
1337
+ * What an investigation concluded, under the verdict the card's badge shows. "Investigated" is the
1338
+ * badge for a finding the evidence left as it was, which says nothing a heading does not.
1339
+ */
1340
+ function investigated(impact, note) {
1341
+ if (!note?.trim()) return void 0;
1342
+ return `**Investigation${impact && impact !== "unchanged" ? ` (${investigationImpactLabel(impact).toLowerCase()})` : ""}:** ${note.trim()}`;
1343
+ }
1344
+ function labelled(label, text) {
1345
+ return text?.trim() ? `**${label}:** ${text.trim()}` : void 0;
1346
+ }
1115
1347
  function bold(text) {
1116
1348
  return `**${text.trim()}**`;
1117
1349
  }
@@ -1352,178 +1584,6 @@ function provenanceDetailLines(session) {
1352
1584
  ];
1353
1585
  }
1354
1586
  //#endregion
1355
- //#region ../../packages/review-harness/src/domain/ui/components/pill_tooltips.ts
1356
- /**
1357
- * What every pill in the sidebar actually means, in one place.
1358
- *
1359
- * A pill is a single uppercase word standing in for a whole axis of judgement, and the axes are
1360
- * not self-evident: MAJOR and "medium confidence" sit side by side on the same card and answer
1361
- * completely different questions. Severity is how much the finding would matter *if it is real*;
1362
- * confidence is how sure the analysis is that it *is* real. A reviewer who has collapsed those
1363
- * into one mental "how bad is this" scale is reading the card wrong, and no amount of colour
1364
- * fixes that — only words do.
1365
- *
1366
- * The wording is derived from the field guidance the model is actually given (see
1367
- * STRUCTURED_OUTPUT_INSTRUCTIONS in ai_review/pipeline.ts). This file must not invent a richer
1368
- * taxonomy than the prompt asks for: if the prompt does not define what separates "major" from
1369
- * "minor", neither does the tooltip. It names the axis and where the value sits on it, and
1370
- * leaves the grading to the model that did the grading.
1371
- *
1372
- * House style, because a tooltip is read once at a glance and never studied:
1373
- *
1374
- * 1. Open with the concept, not a sentence the reader has to parse into one. "PR risk — the
1375
- * overall risk level assigned to this pull request" lands before the clause finishes;
1376
- * "Overall risk this analysis assigned the pull request" makes them unpick the grammar first.
1377
- * 2. Describe what the thing *is*, never how the extension builds it. A follow-up "builds on the
1378
- * original analysis" — that it is composed into the stored session is our problem, not theirs.
1379
- * 3. Plain reviewer's English over product vocabulary. "During your review", not "before you
1380
- * sign off"; "the section of the Guide", not the section's own title read back at them.
1381
- */
1382
- /** Ordered worst-first, matching how the prompt lists the enum. */
1383
- var SEVERITY_SCALE = [
1384
- "critical",
1385
- "major",
1386
- "minor",
1387
- "nit"
1388
- ];
1389
- var ATTENTION_SCALE = [
1390
- "high",
1391
- "medium",
1392
- "low"
1393
- ];
1394
- /**
1395
- * "2 of 4: critical → major → minor → nit" — the value's rank and the whole scale it sits on.
1396
- *
1397
- * Both halves earn their place: the rank alone leaves the reader guessing what the other steps
1398
- * are, and the scale alone makes them count. An arrow rather than a bullet or comma, so the
1399
- * ordering is visible rather than something the reader has to already know.
1400
- */
1401
- function position(value, scale) {
1402
- const index = scale.indexOf(value);
1403
- if (index === -1) return scale.join(" → ");
1404
- return `${index + 1} of ${scale.length}: ${scale.join(" → ")}`;
1405
- }
1406
- function severityTip(severity) {
1407
- return `Severity — how much this would matter if it turns out to be real. ${position(severity, SEVERITY_SCALE)}.`;
1408
- }
1409
- /**
1410
- * Only medium and low ever reach the screen (renderIssueConfidence suppresses "high"), but all
1411
- * three are worded here so the copy does not depend on that rendering decision staying put.
1412
- *
1413
- * The second sentence is the one that matters. Confidence and severity sit side by side on the
1414
- * same card, and a reader who has not been told otherwise will assume they are the same scale
1415
- * read twice — so the tooltip says they are not, in as many words.
1416
- */
1417
- function confidenceTip(confidence) {
1418
- return `Confidence — how sure the analysis is that this issue is real. This is separate from how serious it would be. ${{
1419
- high: "High means the analysis is confident it’s real.",
1420
- medium: "Medium means it’s worth confirming in the code before acting on it.",
1421
- low: "Low means it was likely reasoned from the diff alone, so check the surrounding code first."
1422
- }[confidence]}`;
1423
- }
1424
- function importanceTip(importance) {
1425
- return `Importance — how much attention this question deserves during your review. ${position(importance, ATTENTION_SCALE)}.`;
1426
- }
1427
- /**
1428
- * The four statuses an "Analyse new changes" pass assigns to a finding.
1429
- *
1430
- * Each says what the *product* concluded and, where it matters, how much to trust it. "Still open"
1431
- * carries the load: it is both a real judgement ("the changes do not address this") and the safe
1432
- * default applied when the analysis said nothing about a finding at all, and the reviewer cannot
1433
- * tell those apart from the badge — so the copy commits to the weaker of the two readings rather
1434
- * than claiming a judgement that may not have been made.
1435
- */
1436
- function findingStatusTip(status) {
1437
- return {
1438
- resolved: "Resolved — the new commits appear to address this finding. Worth a glance to confirm before you close it out.",
1439
- "still-open": "Still open — the new commits do not address this finding. Findings the analysis did not comment on are shown this way too, so this never disappears on its own.",
1440
- affected: "Affected — the code this finding concerns has changed, but not in a way that settles it. Re-read this one.",
1441
- new: "New — introduced by the changes pushed since your previous analysis."
1442
- }[status];
1443
- }
1444
- /** The reviewer-facing label for a status. Kept beside the tooltips so the two cannot drift. */
1445
- function findingStatusLabel(status) {
1446
- return {
1447
- resolved: "Resolved",
1448
- "still-open": "Still open",
1449
- affected: "Affected",
1450
- new: "New"
1451
- }[status];
1452
- }
1453
- /**
1454
- * What the commits pushed since did to an open question.
1455
- *
1456
- * "Still open" carries the same double load it does on a finding: a real judgement, and the default
1457
- * for a question the reassessment never mentioned. The copy commits to the weaker reading, because
1458
- * the reviewer must not read silence as an answer.
1459
- */
1460
- function questionStatusTip(status) {
1461
- return {
1462
- answered: "Answered — the new commits settle this, and the answer is below. Worth a glance to confirm before you drop it.",
1463
- "still-open": "Still open — the new commits do not settle this. Questions the reassessment did not comment on are shown this way too, so this never disappears on its own.",
1464
- affected: "Affected — the code this question is about has changed without answering it. Read the question again before you ask it."
1465
- }[status];
1466
- }
1467
- /** The reviewer-facing label for a question status. Kept beside the tooltip so the two cannot drift. */
1468
- function questionStatusLabel(status) {
1469
- return {
1470
- answered: "Answered",
1471
- "still-open": "Still open",
1472
- affected: "Affected"
1473
- }[status];
1474
- }
1475
- /**
1476
- * The four impacts a repository investigation assigns to a finding.
1477
- *
1478
- * Every one of these says the evidence came from *outside* the diff, because that is the whole
1479
- * reason to trust them more than the analysis they revise — and equally the reason to say where
1480
- * they came from. "A coding agent read the repository" is the qualification, and a reviewer who
1481
- * does not know that cannot weigh it.
1482
- *
1483
- * "Investigated" carries the same load "Still open" does on the other axis: it is both a real
1484
- * judgement ("this evidence does not bear on that finding") and the default applied when the
1485
- * reassessment said nothing about a finding at all. The copy commits to the weaker reading rather
1486
- * than claiming a judgement that may not have been made.
1487
- */
1488
- function investigationImpactTip(impact) {
1489
- return {
1490
- confirmed: "Confirmed — a repository investigation found evidence supporting this finding. It no longer rests on the diff alone.",
1491
- refuted: "Refuted — a repository investigation found evidence that this concern does not hold. Kept on the list, with the reasoning, rather than removed.",
1492
- revised: "Revised — a repository investigation changed what this finding says. Where its severity or confidence moved, the previous value is shown alongside.",
1493
- unchanged: "Investigated — this finding was reassessed against a repository investigation and stands as it was. Findings the reassessment did not comment on are shown this way too."
1494
- }[impact];
1495
- }
1496
- /**
1497
- * The reviewer-facing label. Note `unchanged` → "Investigated": the reviewer's world contains a
1498
- * finding that was looked into and survived, which is information. "Unchanged" describes our
1499
- * bookkeeping — what the field did — and says nothing about the code.
1500
- */
1501
- function investigationImpactLabel(impact) {
1502
- return {
1503
- confirmed: "Confirmed",
1504
- refuted: "Refuted",
1505
- revised: "Revised",
1506
- unchanged: "Investigated"
1507
- }[impact];
1508
- }
1509
- var DISMISSED_TIP = "Dismissed by you — you decided this finding needs no action. It stops counting towards what is left, and stays here so the decision is visible rather than lost.";
1510
- var DISMISSAL_STALE_TIP = "Changed since you dismissed this — the code under this finding has moved, so the reading you dismissed it on no longer holds. Your decision is kept; the finding is back on the list until you confirm it.";
1511
- var SECTION_TAG_TIP = "Guide section — the section of the Guide this finding belongs to. Click to open it in the Guide tab.";
1512
- /**
1513
- * Builds a pill's `class` and `data-tip` attributes together.
1514
- *
1515
- * They are emitted as one string rather than left to the call site because they are useless
1516
- * apart: `data-tip` without `pr-helper-tip` never renders, and `pr-helper-tip` without a tip
1517
- * renders an empty black rectangle on hover. Taking the pill's own classes as an argument means
1518
- * there is no way to add the tooltip and clobber the styling, which is what a bare
1519
- * `class="pr-helper-tip"` helper would have invited.
1520
- *
1521
- * Emits a leading space so it drops straight in after a tag name.
1522
- */
1523
- function pillAttrs(classNames, tip) {
1524
- return ` class="${escapeHtml$1(classNames)} pr-helper-tip" data-tip="${escapeHtml$1(tip)}"`;
1525
- }
1526
- //#endregion
1527
1587
  //#region ../../packages/review-harness/src/domain/ai_review/investigation_record.ts
1528
1588
  /**
1529
1589
  * The records for one finding, oldest first. Records accumulate: every "Investigate Again" appends.
@@ -2532,7 +2592,7 @@ function renderIssuesTab(session, investigatingId, options) {
2532
2592
  <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)}">
2533
2593
  <div class="pr-helper-issue-header">
2534
2594
  <div class="pr-helper-issue-title-row">
2535
- ${options?.feedbackSelect ? renderFeedbackPick("issue", issue.title) : ""}
2595
+ ${options?.feedbackSelect ? renderFeedbackPick("issue", issue.title, issueFeedbackMarkdown(issue)) : ""}
2536
2596
  <p class="pr-helper-issue-title">${renderInlineText(issue.title)}</p>
2537
2597
  ${renderFindingCopySource(issueMarkdown(issue), "issue")}
2538
2598
  ${options?.askBuddy ? renderAskBuddyMenu("issue") : ""}
@@ -2548,7 +2608,7 @@ function renderIssuesTab(session, investigatingId, options) {
2548
2608
  </div>
2549
2609
  ${renderStaleDismissal(stale)}
2550
2610
  ${renderSeverityRevision(issue.priorSeverity, issue.severity)}
2551
- <p class="pr-helper-issue-desc">${renderInlineText(issue.description)}</p>
2611
+ <p class="pr-helper-issue-desc">${renderInlineText(issue.description)}</p>${renderCardExplanation(simpleExplanationOf(session, "issue", issue.id)?.text, options)}
2552
2612
  ${renderReconciliationNote(issue.reconciliationNote)}
2553
2613
  ${renderInvestigationNote(issue.investigationNote)}
2554
2614
  ${renderExistingDiscussion(issue.existingDiscussion)}
@@ -2581,6 +2641,14 @@ function closedFindingsSummary(counts) {
2581
2641
  if (byCommits === 0) return `Dismissed by you (${counts.done})`;
2582
2642
  return `Dealt with (${counts.done})`;
2583
2643
  }
2644
+ /**
2645
+ * The finding's simple explanation, under what the finding says and above where it stands. Nothing
2646
+ * at all when the reviewer has not asked for one, so such a card is the card it always was.
2647
+ */
2648
+ function renderCardExplanation(text, options) {
2649
+ if (!text?.trim()) return "";
2650
+ return renderSimpleExplanation(text, { askMore: options?.askBuddy });
2651
+ }
2584
2652
  /** The reviewer's own decision, said on the card so the closed pile is never one undifferentiated heap. */
2585
2653
  function renderDismissedBadge(isDismissed) {
2586
2654
  if (!isDismissed) return "";
@@ -2624,7 +2692,7 @@ var INTENT_QUESTION_GROUP = "prb-intent";
2624
2692
  function questionHome(q) {
2625
2693
  return q.category === "intent" ? INTENT_QUESTION_GROUP : q.sectionId;
2626
2694
  }
2627
- function renderQuestionCard(q, sections, answered, records, now = Date.now(), investigatingId, options) {
2695
+ function renderQuestionCard(q, sections, answered, records, now = Date.now(), investigatingId, options, explanation) {
2628
2696
  const importanceBadge = q.importance ? `<span${pillAttrs(`pr-helper-question-importance-badge pr-helper-question-importance-${q.importance}`, importanceTip(q.importance))}>${escapeHtml$1(q.importance)}</span>` : "";
2629
2697
  const jumpLink = renderQuestionJump(q.file, options);
2630
2698
  const settled = q.reconciliationStatus === "answered";
@@ -2635,7 +2703,7 @@ function renderQuestionCard(q, sections, answered, records, now = Date.now(), in
2635
2703
  <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)}"${questionHome(q) ? ` data-section-id="${escapeHtml$1(questionHome(q))}"` : ""}>
2636
2704
  <div class="pr-helper-issue-header">
2637
2705
  <div class="pr-helper-issue-title-row">
2638
- ${options?.feedbackSelect ? renderFeedbackPick("question", q.question) : ""}
2706
+ ${options?.feedbackSelect ? renderFeedbackPick("question", q.question, questionFeedbackMarkdown(q)) : ""}
2639
2707
  <p class="pr-helper-issue-title">${renderInlineText(q.question)}</p>
2640
2708
  ${renderFindingCopySource(questionMarkdown(q), "question")}
2641
2709
  ${options?.askBuddy ? renderAskBuddyMenu("question") : ""}
@@ -2652,7 +2720,7 @@ function renderQuestionCard(q, sections, answered, records, now = Date.now(), in
2652
2720
  ${importanceBadge}
2653
2721
  </div>
2654
2722
  </div>
2655
- ${q.whyThisMatters ? `<h5>Why this matters</h5><p class="pr-helper-issue-desc">${renderInlineText(q.whyThisMatters)}</p>` : ""}
2723
+ ${q.whyThisMatters ? `<h5>Why this matters</h5><p class="pr-helper-issue-desc">${renderInlineText(q.whyThisMatters)}</p>` : ""}${renderCardExplanation(explanation, options)}
2656
2724
  ${reconciliationAnswer}
2657
2725
  ${renderReconciliationNote(q.reconciliationNote)}
2658
2726
  ${investigationAnswer}
@@ -2694,9 +2762,12 @@ function renderQuestionsProgress(questions, answered) {
2694
2762
  * `autocomplete="off"` because the browser otherwise restores a box's tick when the page is loaded
2695
2763
  * again from history, and the selection it belonged to went with the page that made it: Back to a
2696
2764
  * review showed a ticked box that nothing had selected.
2765
+ *
2766
+ * It carries the text the card gives a feedback draft, which is narrower than its Copy: see
2767
+ * finding_copy.ts.
2697
2768
  */
2698
- function renderFeedbackPick(kind, title) {
2699
- return `<input type="checkbox" class="pr-helper-feedback-pick" data-feedback-pick="${kind}" autocomplete="off" aria-label="${escapeHtml$1(`Include in feedback: ${title.trim()}`)}">`;
2769
+ function renderFeedbackPick(kind, title, feedback) {
2770
+ return `<input type="checkbox" class="pr-helper-feedback-pick" data-feedback-pick="${kind}" ${FEEDBACK_SOURCE_ATTR}="${escapeHtml$1(feedback)}" autocomplete="off" aria-label="${escapeHtml$1(`Include in feedback: ${title.trim()}`)}">`;
2700
2771
  }
2701
2772
  function renderQuestionJump(file, options) {
2702
2773
  if (!file || options?.suppressFileJump) return "";
@@ -2716,7 +2787,7 @@ function renderQuestionsTab(session, investigatingId, options) {
2716
2787
  const progress = renderQuestionsProgress(result.questions, answered);
2717
2788
  const records = session.investigations;
2718
2789
  const now = Date.now();
2719
- const card = (q) => renderQuestionCard(q, sections, answered.has(q.id), records, now, investigatingId, options);
2790
+ const card = (q) => renderQuestionCard(q, sections, answered.has(q.id), records, now, investigatingId, options, simpleExplanationOf(session, "question", q.id)?.text);
2720
2791
  const open = result.questions.filter((q) => isQuestionOutstanding(q, answered));
2721
2792
  const settled = result.questions.filter((q) => !isQuestionOutstanding(q, answered));
2722
2793
  const closed = settled.length === 0 ? "" : renderClosedFindings(`Answered (${settled.length})`, settled.map(card).join(""), "questions", open.length === 0);
@@ -10067,11 +10138,45 @@ function defaultFetchDeps() {
10067
10138
  }
10068
10139
  //#endregion
10069
10140
  //#region ../../packages/review-harness/src/workspace/help_page.ts
10070
- /** The diagrams are text on purpose: they are the same ones `prreviewbuddy --help` prints. */
10071
- var TOPICS = [{
10072
- id: "what-gets-reviewed",
10073
- title: "What gets reviewed",
10074
- body: `<p>A review is the combined change between a base and the branch you review, shown as one
10141
+ /**
10142
+ * The agents by their own names, from the registry, so a new adapter is named here without this
10143
+ * file being touched. One that will not load is left out rather than taking the page with it.
10144
+ */
10145
+ var AGENT_PRODUCTS = ((names) => names.length <= 1 ? names[0] ?? "" : `${names.slice(0, -1).join(", ")} or ${names[names.length - 1]}`)([...AGENT_IDS].sort().flatMap((id) => {
10146
+ try {
10147
+ const agent = agentById(id);
10148
+ return agent ? [agent.displayName] : [];
10149
+ } catch {
10150
+ return [];
10151
+ }
10152
+ }));
10153
+ /**
10154
+ * The concepts behind the CLI's commands live here rather than in `prreviewbuddy --help`, which
10155
+ * says what to type and points here for what it means. The diagrams are text on purpose.
10156
+ */
10157
+ var TOPICS = [
10158
+ {
10159
+ id: "getting-started",
10160
+ title: "Getting started",
10161
+ body: `<p>PR Review Buddy has a coding agent you already use (${escapeHtml$1(AGENT_PRODUCTS)})
10162
+ analyse a change in a checkout of its own, and lays out what it found here, in your workspace,
10163
+ for you to review. The agent's analysis is a guide to the change, not a verdict on it.</p>
10164
+ <p>Start a review from a terminal, in the repository:</p>
10165
+ <ul>
10166
+ <li><code>prreviewbuddy review</code> for the branch you have checked out</li>
10167
+ <li><code>prreviewbuddy review --pr &lt;url&gt;</code> for a pull request</li>
10168
+ <li><code>prreviewbuddy open</code> to list the reviews you have and open this workspace</li>
10169
+ </ul>
10170
+ <p>Or press <kbd class="prb-kbd">⌘P</kbd> (<kbd class="prb-kbd">Ctrl+P</kbd> elsewhere) to add a
10171
+ review from here. <code>prreviewbuddy help</code> lists every command, and
10172
+ <code>prreviewbuddy review --help</code> every option a review takes.</p>
10173
+ <p>There is no account and no API key. PR Review Buddy uses the coding agent installed on your
10174
+ machine and that agent's existing authentication, and never asks you for a credential.</p>`
10175
+ },
10176
+ {
10177
+ id: "what-gets-reviewed",
10178
+ title: "What gets reviewed",
10179
+ body: `<p>A review is the combined change between a base and the branch you review, shown as one
10075
10180
  diff. It is not just the latest commit. The base determines the split point: the commit where
10076
10181
  your branch left it (git calls this the merge-base). PR Review Buddy reviews the changes on your
10077
10182
  branch since that point.</p>
@@ -10111,10 +10216,115 @@ feature: E</pre>
10111
10216
  </ul>
10112
10217
  <p>Every review shows its own range. The commit count beside the branch name at the top of a
10113
10218
  review opens its split point and the commits it covers.</p>`
10114
- }, {
10115
- id: "keyboard",
10116
- title: "Keyboard",
10117
- body: `<p>Press <kbd class="prb-kbd">⌘K</kbd> on a Mac, or <kbd class="prb-kbd">Ctrl+K</kbd>
10219
+ },
10220
+ {
10221
+ id: "isolated-checkouts",
10222
+ title: "Isolated checkouts",
10223
+ body: `<p>Every review is made in an isolated checkout: a separate git worktree of your
10224
+ repository, created at the exact commit being reviewed. Your own checkout is never read or
10225
+ written while a review runs, so you can keep working, switch branches or leave uncommitted
10226
+ changes where they are.</p>
10227
+ <p>A review is pinned to that commit. Commits you add afterwards do not change it; Update review
10228
+ brings it up to date when you are ready.</p>
10229
+ <p>The checkout is kept for six hours after the review finishes, because it is what Buddy AI
10230
+ reads to answer your questions about the change, and what Update review starts from. After
10231
+ that it is removed. The review itself stays, and a later question makes a new checkout for
10232
+ itself.</p>`
10233
+ },
10234
+ {
10235
+ id: "updating-and-reanalysing",
10236
+ title: "Updating and reanalysing",
10237
+ body: `<p>Four things can happen to a review once it exists. Each is a flag on
10238
+ <code>prreviewbuddy review</code>, and the ones you need most are also in the workspace.</p>
10239
+ <ul>
10240
+ <li><b>Update</b> brings the review up to date with commits added since it was made. Its
10241
+ findings are checked against the new commits rather than thrown away, and each says whether
10242
+ the new commits affected it. In the workspace, Update review appears on a review whose
10243
+ branch has moved on.</li>
10244
+ <li><b>Reanalyse</b> analyses the same commit again, for a fresh judgement of the same change,
10245
+ for example with a different model.</li>
10246
+ <li><b>Fresh review</b> reviews the target again as a new review, and keeps the existing one
10247
+ beside it.</li>
10248
+ <li><b>Delete</b> removes a review and the checkouts it was made in. See
10249
+ <a href="#reviews-and-ids">Reviews and review IDs</a>.</li>
10250
+ </ul>`
10251
+ },
10252
+ {
10253
+ id: "reviews-and-ids",
10254
+ title: "Reviews and review IDs",
10255
+ body: `<p>The same branch can have more than one review: a fresh review, for one, keeps the review
10256
+ before it. A review ID tells them apart. It identifies one review, not a branch, and stays the same when the review is updated
10257
+ or reanalysed.</p>
10258
+ <p>Copy review ID is in each review's menu, and <code>prreviewbuddy open</code> prints the ID
10259
+ beside every review. Use it with <code>--review</code> to act on that one review from a
10260
+ terminal.</p>
10261
+ <p>Deleting a review removes its record and the isolated checkouts it was made in. It never
10262
+ touches your branch or its commits. Delete one from its menu, or tick reviews in the Reviews
10263
+ list, one at a time or a whole branch at once, and delete them together.</p>`
10264
+ },
10265
+ {
10266
+ id: "several-pull-requests",
10267
+ title: "Several pull requests at once",
10268
+ body: `<p><code>prreviewbuddy review --pr &lt;url&gt; &lt;url&gt; …</code> makes one ordinary
10269
+ review per pull request, each with its own link, and prints them all when it ends. Two run at
10270
+ a time unless <code>--concurrency</code> says otherwise.</p>
10271
+ <p>One that fails does not stop the others. Ctrl-C stops them all, and running the same command
10272
+ again carries each one on from where it got to. Add <code>--queue</code> to put each review on
10273
+ your review queue as soon as it exists, rather than when it is ready.</p>`
10274
+ },
10275
+ {
10276
+ id: "review-lenses",
10277
+ title: "Review lenses",
10278
+ body: `<p>A review lens asks a specialised question of the same change. It runs beside the
10279
+ standard review, in the same workspace, rather than replacing it, and it does not need the
10280
+ standard review to have been made first.</p>
10281
+ <p>The KISS lens asks not whether the change is correct, but whether the complexity it
10282
+ introduces is justified, or whether the same requirements could be met with less
10283
+ machinery.</p>
10284
+ <p>A lens writes for one of two audiences, and you always say which:</p>
10285
+ <ul>
10286
+ <li><b>Author</b>: for whoever wrote the change, as changes to make before submitting it.</li>
10287
+ <li><b>Reviewer</b>: for somebody reviewing somebody else's work, with whether each point is
10288
+ worth raising.</li>
10289
+ </ul>
10290
+ <p>It is never guessed from what you are reviewing, because a reviewer checks out somebody
10291
+ else's branch and an author reviews their own pull request. Run one with
10292
+ <code>prreviewbuddy review --lens kiss --audience author</code> or
10293
+ <code>--audience reviewer</code>.</p>`
10294
+ },
10295
+ {
10296
+ id: "workspace-server",
10297
+ title: "The workspace server",
10298
+ body: `<p>One workspace server on your machine serves every review. It keeps running after the
10299
+ terminal that started it closes, so a review stays open, and its link keeps working, until you
10300
+ stop the server.</p>
10301
+ <p>Your workspace is at <code>http://localhost:${DEFAULT_WORKSPACE_PORT}</code> unless you have
10302
+ moved it. Change the port in Settings, or with <code>prreviewbuddy config set port</code>; the new
10303
+ port is used the next time the server starts. If another program is already on that port, PR
10304
+ Review Buddy stops and says so rather than quietly moving somewhere you have not
10305
+ bookmarked.</p>
10306
+ <p>Restart the server with the button in the bar at the top. Stop it from Settings, which
10307
+ stops every open review, not only the one in front of you. Nothing is deleted, and
10308
+ <code>prreviewbuddy open</code> starts it again from any directory.</p>`
10309
+ },
10310
+ {
10311
+ id: "retention",
10312
+ title: "How long things are kept",
10313
+ body: `<p>Everything lives in <code>~/.prreviewbuddy</code> on your machine, and is cleared up
10314
+ automatically:</p>
10315
+ <ul>
10316
+ <li><b>Reviews</b> are kept for thirty days after you last opened or changed them.</li>
10317
+ <li><b>Reviews you kept</b> (saved, marked Done, or on your review queue) are kept for ninety
10318
+ days instead.</li>
10319
+ <li><b>Isolated checkouts</b> are removed six hours after their work finishes. See
10320
+ <a href="#isolated-checkouts">Isolated checkouts</a>.</li>
10321
+ </ul>
10322
+ <p><code>prreviewbuddy uninstall</code> removes all of it at once.</p>`
10323
+ },
10324
+ {
10325
+ id: "keyboard",
10326
+ title: "Keyboard",
10327
+ body: `<p>Press <kbd class="prb-kbd">⌘K</kbd> on a Mac, or <kbd class="prb-kbd">Ctrl+K</kbd>
10118
10328
  elsewhere, to open the command palette. It lists what you can do on the page you are on, and
10119
10329
  after a key like <kbd class="prb-kbd">M</kbd> it shows the keys that can follow. It stays open
10120
10330
  until you choose something or press Escape. <kbd class="prb-kbd">⌘K</kbd> then
@@ -10122,7 +10332,14 @@ feature: E</pre>
10122
10332
  <p>Single keys, such as <kbd class="prb-kbd">J</kbd> and <kbd class="prb-kbd">K</kbd> on the
10123
10333
  Reviews list, never act while you are typing in a field.</p>
10124
10334
  <div data-shortcut-table></div>`
10125
- }];
10335
+ }
10336
+ ];
10337
+ /** Every topic by name at the top, so one in the middle is found by reading a list, not a page. */
10338
+ function renderContents() {
10339
+ return `<nav class="prb-help-contents" aria-label="Help topics">
10340
+ <ul>${TOPICS.map((topic) => `<li><a href="#${topic.id}">${escapeHtml$1(topic.title)}</a></li>`).join("")}</ul>
10341
+ </nav>`;
10342
+ }
10126
10343
  function renderHelpPage(view) {
10127
10344
  return `<!doctype html>
10128
10345
  <html lang="en"${themeAttribute(view.theme)}>
@@ -10147,6 +10364,7 @@ ${renderWorkspaceRail({
10147
10364
  unread: view.unreadNotifications ?? 0
10148
10365
  })}
10149
10366
  <main class="prb-index prb-help">
10367
+ ${renderContents()}
10150
10368
  ${TOPICS.map((topic) => `<section class="prb-help-topic" id="${topic.id}">
10151
10369
  <h2>${escapeHtml$1(topic.title)}</h2>
10152
10370
  ${topic.body}
@@ -11059,14 +11277,14 @@ var AskFailedError = class extends Error {};
11059
11277
  * the assistant keeps working after the session that produced the review has closed, which is the
11060
11278
  * whole reason the workspace is served rather than written to a file.
11061
11279
  */
11062
- async function askAboutChange(workspace, question, sectionId, signal) {
11280
+ async function askAboutChange(workspace, question, sectionId, signal, context = {}) {
11063
11281
  const cwd = await askCheckout(workspace);
11064
11282
  const env = agentEnvOf(workspace);
11065
11283
  const requestedModel = resolveModel({ review: { session: workspace.session } }) ?? void 0;
11066
11284
  try {
11067
11285
  return extractAnswer(await withUsageRecorded(workspace.id, "ask", env, (onUsage) => agentFor$1(env).run({
11068
11286
  capability: "read-code",
11069
- prompt: buildPrompt$2(workspace, question, sectionId, cwd),
11287
+ prompt: buildPrompt$2(workspace, question, sectionId, cwd, context),
11070
11288
  cwd,
11071
11289
  originRepoPath: workspace.repoPath,
11072
11290
  env,
@@ -11125,7 +11343,7 @@ function extractAnswer(raw) {
11125
11343
  * where X is in another theme, and a prompt that had quietly dropped X would answer confidently
11126
11344
  * and wrongly.
11127
11345
  */
11128
- function buildPrompt$2(workspace, question, sectionId, cwd) {
11346
+ function buildPrompt$2(workspace, question, sectionId, cwd, context = {}) {
11129
11347
  const sections = workspace.session.result?.sections ?? [];
11130
11348
  const reading = readingThemeLine(sections, sectionId);
11131
11349
  const recent = workspace.transcript.slice(-4);
@@ -11148,10 +11366,47 @@ function buildPrompt$2(workspace, question, sectionId, cwd) {
11148
11366
  return `Q${where ? ` ${where}` : ""}: ${entry.question}\nA: ${entry.answer}`;
11149
11367
  }).join("\n\n")
11150
11368
  ] : [],
11369
+ ...continuingFrom(workspace, context.continuing),
11370
+ ...explainingFinding(workspace, context.explaining),
11151
11371
  "---",
11152
11372
  `QUESTION: ${question}`
11153
11373
  ].join("\n");
11154
11374
  }
11375
+ /**
11376
+ * The simple explanation a follow-up was asked from, however long ago it was written.
11377
+ *
11378
+ * Given in full rather than left to the recent turns above, which hold only the last four: a
11379
+ * reviewer who comes back a week later and presses Ask more is continuing from the explanation on
11380
+ * the card, not from whatever the conversation happens to still remember.
11381
+ */
11382
+ function continuingFrom(workspace, continuing) {
11383
+ if (!continuing) return [];
11384
+ return [
11385
+ "---",
11386
+ `THE REVIEWER IS CONTINUING FROM THIS PLAIN-ENGLISH EXPLANATION YOU GAVE EARLIER OF ${findingName(workspace, continuing)}:`,
11387
+ continuing.text
11388
+ ];
11389
+ }
11390
+ /**
11391
+ * Explain simply on a finding, whose answer is kept on the card and read again after the code has
11392
+ * moved on. So it explains the claim, what the finding says and why it was raised, rather than
11393
+ * asserting what the code does now: a later update or investigation says whether the claim still
11394
+ * holds, and an explanation written as a present-tense fact about the code would contradict it.
11395
+ */
11396
+ function explainingFinding(workspace, explaining) {
11397
+ if (!explaining) return [];
11398
+ return [
11399
+ "---",
11400
+ `THE REVIEWER ASKED FOR A SIMPLE EXPLANATION OF ${findingName(workspace, explaining)}. It is kept beside that finding and read again later, possibly after the code has changed.`,
11401
+ "Explain in plain English what the finding is saying and why it was raised: the concern as the review stated it, in terms of what was reviewed (for example \"this was raised because...\"), not as a statement about how the code behaves now. Do not judge whether it still holds; the review tracks that separately."
11402
+ ];
11403
+ }
11404
+ /** `THE ISSUE "<title>"` or `THE QUESTION "<question>"`, or just the kind when the finding is gone. */
11405
+ function findingName(workspace, finding) {
11406
+ const result = workspace.session.result;
11407
+ const title = finding.kind === "issue" ? result?.issues.find((issue) => issue.id === finding.id)?.title : result?.questions.find((question) => question.id === finding.id)?.question;
11408
+ return `THE ${finding.kind.toUpperCase()}${title ? ` "${title.trim()}"` : ""}`;
11409
+ }
11155
11410
  //#endregion
11156
11411
  //#region ../../packages/review-harness/src/workspace/diagram.ts
11157
11412
  /** The same budget a question gets. Drawing is reading, and reading is what takes the time. */
@@ -13439,6 +13694,9 @@ async function handleAsk(req, res, workspace) {
13439
13694
  const question = typeof body?.question === "string" ? body.question.trim() : "";
13440
13695
  if (!question) return send(res, 400, { error: "No question given." });
13441
13696
  const sectionId = typeof body?.sectionId === "string" ? body.sectionId : null;
13697
+ const explains = findingNamed(workspace.session, body?.explains);
13698
+ const continuesFrom = findingNamed(workspace.session, body?.continuesExplanation);
13699
+ const continuing = continuesFrom ? simpleExplanationOf(workspace.session, continuesFrom.kind, continuesFrom.id) : void 0;
13442
13700
  if (liveUpdateFor(workspace.id)) return send(res, 409, {
13443
13701
  error: "This review is being updated. Ask again when the new review is ready.",
13444
13702
  updating: true
@@ -13470,7 +13728,10 @@ async function handleAsk(req, res, workspace) {
13470
13728
  try {
13471
13729
  let answer;
13472
13730
  try {
13473
- answer = await askAboutChange(workspace, question, sectionId, run.signal);
13731
+ answer = await askAboutChange(workspace, question, sectionId, run.signal, {
13732
+ ...explains ? { explaining: explains } : {},
13733
+ ...continuing ? { continuing } : {}
13734
+ });
13474
13735
  } catch (error) {
13475
13736
  failed();
13476
13737
  if (run.state === "stopped") return sendIfOpen(res, 200, { stopped: true });
@@ -13486,6 +13747,7 @@ async function handleAsk(req, res, workspace) {
13486
13747
  durationMs: Date.now() - startedAt,
13487
13748
  ok: true
13488
13749
  });
13750
+ let explanation;
13489
13751
  mutateWorkspace(workspace.id, (fresh) => {
13490
13752
  fresh.transcript.push({
13491
13753
  question,
@@ -13493,12 +13755,38 @@ async function handleAsk(req, res, workspace) {
13493
13755
  sectionId,
13494
13756
  askedAt: Date.now()
13495
13757
  });
13758
+ if (explains && findingNamed(fresh.session, explains) && answer.trim()) {
13759
+ explanation = {
13760
+ ...explains,
13761
+ text: answer.trim()
13762
+ };
13763
+ fresh.session.simpleExplanations = withSimpleExplanation(fresh.session.simpleExplanations, explanation);
13764
+ }
13496
13765
  });
13497
- sendIfOpen(res, 200, { answer });
13766
+ sendIfOpen(res, 200, explanation ? {
13767
+ answer,
13768
+ explanation
13769
+ } : { answer });
13498
13770
  } finally {
13499
13771
  endRun(run);
13500
13772
  }
13501
13773
  }
13774
+ /** An issue or question of this review, named by kind and id; null for anything else. */
13775
+ function findingNamed(session, value) {
13776
+ const named = value;
13777
+ if (typeof named?.id !== "string") return null;
13778
+ const id = named.id;
13779
+ const result = session.result;
13780
+ if (named.kind === "issue") return result?.issues.some((issue) => issue.id === id) ? {
13781
+ kind: "issue",
13782
+ id
13783
+ } : null;
13784
+ if (named.kind === "question") return result?.questions.some((question) => question.id === id) ? {
13785
+ kind: "question",
13786
+ id
13787
+ } : null;
13788
+ return null;
13789
+ }
13502
13790
  /** Only while somebody is listening. A page that left took its socket with it. */
13503
13791
  function sendIfOpen(res, status, body) {
13504
13792
  if (res.writableEnded || res.destroyed) return;