prreviewbuddy 0.70.2 → 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/CHANGELOG.md +45 -0
- package/README.md +66 -174
- package/dist/{delete_review-CRPRIRKg.js → delete_review-BgIZpcsP.js} +17 -3
- package/dist/main.js +194 -130
- package/dist/server.js +487 -199
- package/package.json +5 -2
- package/static/shell.css +7 -0
- package/static/sidebar.css +48 -0
- package/static/workspace.js +65 -62
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as MAX_WORKSPACE_PORT, $n as isClaimed, $r as
|
|
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,
|
|
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
|
-
|
|
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
|
-
/**
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
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 <url></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
|
-
|
|
10116
|
-
|
|
10117
|
-
|
|
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 <url> <url> …</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,
|
|
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;
|