prreviewbuddy 0.75.0 → 0.77.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 +164 -0
- package/dist/{delete_review-BWKGCqwI.js → delete_review-BHOzMQ0_.js} +2257 -702
- package/dist/main.js +1 -1
- package/dist/server.js +529 -153
- package/package.json +1 -1
- package/static/{chunk-egI_P3oB.js → chunk-BoQsVuYd.js} +1 -1
- package/static/chunk-CEQv4zVy.js +1 -0
- package/static/help.js +1 -1
- package/static/notifications.js +1 -1
- package/static/reviews.js +1 -1
- package/static/settings.js +1 -1
- package/static/shell.css +73 -4
- package/static/sidebar.css +13 -0
- package/static/workspace.js +3 -3
- package/static/chunk-C0lM8IBt.js +0 -1
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as resolveWorkspacePort, $n as saveJob, $r as explanationActionOf, $t as pruneNotifications, A as parsePullRequestLink, Ai as git, An as ReviewBeingDeletedError, Ar as activityAttributes, At as repositoryKey, Bi as agentById, Bn as findExistingReview, Br as queueAddMany, Bt as announceJob, C as completeRun, Ci as originRemote, Cn as GITHUB_MARKDOWN, Cr as reviewedCommit, Ct as RUNTIME_WORDS, D as relativeTime$1, Di as AmbiguousRefError, Dn as escapeHtml$1, Dr as workspaceRevision, Dt as loadArchitecture, E as stopRun, Ei as resolveBase, En as CODE_COPY_CLASS, Er as touchWorkspace, Et as checkArchitecture, Fi as readAgentPreference, Fn as readMarker, Fr as lifecyclePill, Ft as QUOTE_LIMITS, G as stopChild, Gn as fetchHead, Gr as isIssueOutstanding, Gt as createPrState, Hi as detectAgents, Hn as checkReview, Hr as queueStamp, Ht as subjectOf, I as settleQueue, Ii as writeAgentPreference, In as removeWorktree, Ir as offersLifecycle, It as REPOSITORY_SCOPE, J as writeServerRecord, Jn as allJobs, Jr as questionsOutstanding, Jt as listNotifications, Kn as forgeResolver, Kr as isQuestionOutstanding, Kt as createPrUpdate, L as readIndexToken, Ln as requestJobStop, Lr as parseLifecycle, Lt as ancestorsOf, M as runBatch, Mi as resolveAgent, Mn as createWorktree, Mr as isLifecycle, Mt as storedScopes, N as activateQueued, Ni as resolveModel, Nn as holdWorktree, Nr as lifecycleChip, Nt as trackedFiles, O as batchIdentity, Oi as shortNameOf, On as renderInlineText, Or as workspaceStamps, Ot as partFiles, P as compactQueue, Pi as clearAgentPreference, Pn as isWorktreeHeld, Pr as lifecycleItemLabel, Pt as ownersOf, Q as parseWorkspacePort, Qn as loadJob, Qr as recommendationsOf, Qt as moveIds, Ri as STORE_ROOT, Rr as sectionOf, Rt as isArchitectureId, S as beginRun, Si as resolveTarget, Sn as hasAuthorContent, Sr as recentWorkspaces, St as recordRuntimeBestEffort, T as reportRun, Ti as isCommitReview, Tn as CODE_BLOCK_CLASS, Tt as ARCHITECTURE_DIR, U as serverEntryExists, Ui as AgentCancelledError, Un as followedBranch, Ur as readQueue, Ut as categoryOf, V as readServerRecord, Vi as agentFor$1, Vn as checkCodeFreshness, Vr as queueRemove, Vt as branchOf, W as spawnServer, Wi as AgentUnavailableError, Wn as reviewedShaOf, Wr as doneVerb, Wt as createArchitectureNotification, X as MAX_WORKSPACE_PORT, Xn as fail$1, Xr as reviewerDispositions, Xt as markRead, Y as DEFAULT_WORKSPACE_PORT, Yn as endedAt, Yr as resolvedByConversation, Yt as markAllRead, Z as MIN_WORKSPACE_PORT, Zn as isTerminal, Zr as processDiscussion, Zt as markReadForReview, _i as extractJsonText, _n as agentEnvOf, _t as BUILD_VERSION, a as wasBlocked, ai as assessmentLookup, an as unreadAround, at as settleReview, b as track, bi as WITHHELD_REASON, bn as UNESTABLISHED_MOTIVATION, br as presentedKissRunOf, bt as waitingHistory, c as RefreshUnavailableError, ci as findingIn, cn as existingPaths, cr as followedRefName, d as mergeUpdateInto, di as promptGrade, dn as prepareConversationWithin, dr as indexReviewGroups, dt as record, ei as explanationOf, en as readNotification, et as statedWorkspacePort, f as updateReview, fi as reassessIn, fn as saveRefresh, fr as kissDecisionRefusal, gi as asString, gn as isUnchanged, gr as loadWorkspace, h as liveUpdateFor, hi as followUpFindingId, hn as refreshPrContext, hr as lineagePosition, ht as telemetryUploadConsent, i as liveJobsFor, ii as analysisRefOf, in as toneOf, ir as purposeOf, it as prepareReview, j as unsupportedReason, ji as refExists, jn as MANAGED_ROOT, jr as activityLabel, jt as storeArchitecture, k as extractPullRequestLinks, ki as shorthandOf, kr as LIFECYCLES, kt as readArchitecture, l as cancelUpdate, li as gradeScale, ln as ASK_REREAD_CEILING_MS, lr as groupByLineage, m as UpdateAlreadyRunningError, mi as wasReassessed, mn as unforeseenFailure, mr as latestKissRun, mt as recordTelemetryUploadConsent, n as deleteReview, ni as withExplanation, nn as removeNotification, nr as phasesFor, nt as failureOf$1, o as discardJob, oi as currentGradeIn, on as unreadCount$1, or as isClaimed, ot as reanalyseReview, p as askCheckout, pi as sourcePhrase, pn as storedConversation, pr as kissDecisionsOf, qn as fillFileUrlTemplate, qr as issuesOutstanding, qt as eventName, r as lineageIds, ri as ANALYSIS_ONLY, rn as retireNotification, rr as progressSteps, rt as observeReview, s as FETCH_TIMEOUT_MS, si as findingAssessment, sn as withUsageRecorded, sr as decideKissRecommendation, st as runJob, t as deleteLineage, ti as isExplanationAction, tn as readStateOf, tr as isLiveJob, tt as writePortPreference, u as cancellableHere, ui as isGradeFor, un as adoptLegacyBaseline, ur as indexKeptIds, ut as readEvents, v as startKissJob, vi as objectWithAnyOf, vn as EXPLAIN_SIMPLY_PROMPT, vr as mutateWorkspace, w as endRun, wi as DetachedHeadError, wn as renderMarkdown, wt as describeRuntimes, x as RUN_KINDS, xi as kissOutcome, xn as authorPreview, xr as previousKissResult, xt as mapRuntimes, y as isDriving, yi as describeAuthorship, yn as chatConversationSection, yt as feedbackUrl, z as clearServerRecordOwnedBy, zi as AGENT_IDS, zr as queueAdd, zt as writeJsonAtomically } from "./delete_review-BHOzMQ0_.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";
|
|
@@ -1210,13 +1210,43 @@ function importanceTip(importance) {
|
|
|
1210
1210
|
* tell those apart from the badge — so the copy commits to the weaker of the two readings rather
|
|
1211
1211
|
* than claiming a judgement that may not have been made.
|
|
1212
1212
|
*/
|
|
1213
|
-
function findingStatusTip(status) {
|
|
1213
|
+
function findingStatusTip(status, against = "commits") {
|
|
1214
1214
|
return {
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1215
|
+
commits: {
|
|
1216
|
+
resolved: "Resolved — the new commits appear to address this finding. Worth a glance to confirm before you close it out.",
|
|
1217
|
+
"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.",
|
|
1218
|
+
affected: "Affected — the code this finding concerns has changed, but not in a way that settles it. Re-read this one.",
|
|
1219
|
+
new: "New — introduced by the changes pushed since your previous analysis."
|
|
1220
|
+
},
|
|
1221
|
+
conversation: {
|
|
1222
|
+
resolved: "Resolved — a comment on the pull request shows this finding no longer applies. Worth a glance to confirm before you close it out.",
|
|
1223
|
+
"still-open": "Still open — nothing said on the pull request settles this finding. Findings the analysis did not comment on are shown this way too, so this never disappears on its own.",
|
|
1224
|
+
affected: "Affected — something said on the pull request bears on this finding without settling it. Re-read this one.",
|
|
1225
|
+
new: "New — raised in the pull request conversation since your previous analysis, and confirmed in the code."
|
|
1226
|
+
},
|
|
1227
|
+
"commits-and-conversation": {
|
|
1228
|
+
resolved: "Resolved — the new commits or the pull request conversation appear to settle this finding. Worth a glance to confirm before you close it out.",
|
|
1229
|
+
"still-open": "Still open — neither the new commits nor the pull request conversation settle this finding. Findings the analysis did not comment on are shown this way too, so this never disappears on its own.",
|
|
1230
|
+
affected: "Affected — the new commits or the pull request conversation bear on this finding without settling it. Re-read this one.",
|
|
1231
|
+
new: "New — found in the commits or the conversation since your previous analysis."
|
|
1232
|
+
}
|
|
1233
|
+
}[against][status];
|
|
1234
|
+
}
|
|
1235
|
+
/** What an update's note on a finding came after, as Copy heads it. */
|
|
1236
|
+
function reconciledAfter(against = "commits") {
|
|
1237
|
+
return {
|
|
1238
|
+
commits: "After the new commits",
|
|
1239
|
+
conversation: "After the pull request conversation",
|
|
1240
|
+
"commits-and-conversation": "After the new commits and comments"
|
|
1241
|
+
}[against];
|
|
1242
|
+
}
|
|
1243
|
+
/** Where an update's answer to a question came from, as its heading. */
|
|
1244
|
+
function reconciledAnswerHeading(against = "commits") {
|
|
1245
|
+
return {
|
|
1246
|
+
commits: "Answered by the new changes",
|
|
1247
|
+
conversation: "Answered by the pull request conversation",
|
|
1248
|
+
"commits-and-conversation": "Answered by the new changes or comments"
|
|
1249
|
+
}[against];
|
|
1220
1250
|
}
|
|
1221
1251
|
/** The reviewer-facing label for a status. Kept beside the tooltips so the two cannot drift. */
|
|
1222
1252
|
function findingStatusLabel(status) {
|
|
@@ -1234,12 +1264,24 @@ function findingStatusLabel(status) {
|
|
|
1234
1264
|
* for a question the reassessment never mentioned. The copy commits to the weaker reading, because
|
|
1235
1265
|
* the reviewer must not read silence as an answer.
|
|
1236
1266
|
*/
|
|
1237
|
-
function questionStatusTip(status) {
|
|
1267
|
+
function questionStatusTip(status, against = "commits") {
|
|
1238
1268
|
return {
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1269
|
+
commits: {
|
|
1270
|
+
answered: "Answered — the new commits settle this, and the answer is below. Worth a glance to confirm before you drop it.",
|
|
1271
|
+
"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.",
|
|
1272
|
+
affected: "Affected — the code this question is about has changed without answering it. Read the question again before you ask it."
|
|
1273
|
+
},
|
|
1274
|
+
conversation: {
|
|
1275
|
+
answered: "Answered — the pull request conversation settles this, and the answer is below. Worth a glance to confirm before you drop it.",
|
|
1276
|
+
"still-open": "Still open — nothing said on the pull request settles this. Questions the reassessment did not comment on are shown this way too, so this never disappears on its own.",
|
|
1277
|
+
affected: "Affected — something said on the pull request bears on this question without answering it. Read the question again before you ask it."
|
|
1278
|
+
},
|
|
1279
|
+
"commits-and-conversation": {
|
|
1280
|
+
answered: "Answered — the new commits or the pull request conversation settle this, and the answer is below. Worth a glance to confirm before you drop it.",
|
|
1281
|
+
"still-open": "Still open — neither the new commits nor the pull request conversation settle this. Questions the reassessment did not comment on are shown this way too, so this never disappears on its own.",
|
|
1282
|
+
affected: "Affected — the new commits or the pull request conversation bear on this question without answering it. Read the question again before you ask it."
|
|
1283
|
+
}
|
|
1284
|
+
}[against][status];
|
|
1243
1285
|
}
|
|
1244
1286
|
/** The reviewer-facing label for a question status. Kept beside the tooltip so the two cannot drift. */
|
|
1245
1287
|
function questionStatusLabel(status) {
|
|
@@ -1301,6 +1343,102 @@ function pillAttrs(classNames, tip) {
|
|
|
1301
1343
|
return ` class="${escapeHtml$1(classNames)} pr-helper-tip" data-tip="${escapeHtml$1(tip)}"`;
|
|
1302
1344
|
}
|
|
1303
1345
|
//#endregion
|
|
1346
|
+
//#region ../../packages/review-harness/src/domain/utils/relative_time.ts
|
|
1347
|
+
function formatRelativeTime(ts, now = Date.now()) {
|
|
1348
|
+
const diffMs = Math.max(0, now - ts);
|
|
1349
|
+
const minute = 6e4;
|
|
1350
|
+
const hour = 60 * minute;
|
|
1351
|
+
const day = 24 * hour;
|
|
1352
|
+
if (diffMs < minute) return "Just now";
|
|
1353
|
+
if (diffMs < hour) {
|
|
1354
|
+
const mins = Math.floor(diffMs / minute);
|
|
1355
|
+
return `${mins} min${mins === 1 ? "" : "s"} ago`;
|
|
1356
|
+
}
|
|
1357
|
+
if (diffMs < day) {
|
|
1358
|
+
const hours = Math.floor(diffMs / hour);
|
|
1359
|
+
return `${hours} hour${hours === 1 ? "" : "s"} ago`;
|
|
1360
|
+
}
|
|
1361
|
+
const days = Math.floor(diffMs / day);
|
|
1362
|
+
return `${days} day${days === 1 ? "" : "s"} ago`;
|
|
1363
|
+
}
|
|
1364
|
+
//#endregion
|
|
1365
|
+
//#region ../../packages/review-harness/src/domain/ui/components/status_history.ts
|
|
1366
|
+
/**
|
|
1367
|
+
* What a step did, by where it came from. A settled finding's three outcomes are kept apart
|
|
1368
|
+
* because they are different statements: reopened says the finding is valid again, marked for
|
|
1369
|
+
* reassessment says nobody could tell, and still resolved says the resolution survived.
|
|
1370
|
+
*/
|
|
1371
|
+
function statusStepHeadline(from, step) {
|
|
1372
|
+
if (step.carried) return `${findingStatusLabel(step.status)}, before history was kept`;
|
|
1373
|
+
if (from === "resolved") {
|
|
1374
|
+
if (step.status === "still-open") return "Reopened";
|
|
1375
|
+
if (step.status === "affected") return "Marked for reassessment";
|
|
1376
|
+
if (step.status === "resolved") return "Still resolved";
|
|
1377
|
+
}
|
|
1378
|
+
return findingStatusLabel(step.status);
|
|
1379
|
+
}
|
|
1380
|
+
/** Who or what decided a step. */
|
|
1381
|
+
function statusBasisPhrase(step) {
|
|
1382
|
+
switch (step.basis) {
|
|
1383
|
+
case "code": return step.commitSha ? `by the new commits, at ${step.commitSha.slice(0, 7)}` : "by the new commits";
|
|
1384
|
+
case "conversation": return "by the pull request conversation";
|
|
1385
|
+
case "manual": return "by you";
|
|
1386
|
+
default: return "";
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
/** Why a settled finding was looked at again, when that is why. */
|
|
1390
|
+
function statusTriggerSentence(step) {
|
|
1391
|
+
if (step.trigger === "evidence-edited") return "The comment it was resolved by was edited.";
|
|
1392
|
+
if (step.trigger === "evidence-deleted") return "The comment it was resolved by was deleted.";
|
|
1393
|
+
return "";
|
|
1394
|
+
}
|
|
1395
|
+
/** Whose words these are, and what they were. */
|
|
1396
|
+
function evidenceAttribution(evidence) {
|
|
1397
|
+
const what = evidence.kind === "review" ? "review" : "comment";
|
|
1398
|
+
if (evidence.deleted) return `@${evidence.author}'s ${what}, since deleted`;
|
|
1399
|
+
return `@${evidence.author}'s ${what}, as it read then`;
|
|
1400
|
+
}
|
|
1401
|
+
/** The steps paired with the status each moved from: the previous step's, or none for the first. */
|
|
1402
|
+
function statusSteps(history) {
|
|
1403
|
+
return history.map((step, index) => ({
|
|
1404
|
+
from: index > 0 ? history[index - 1].status : void 0,
|
|
1405
|
+
step
|
|
1406
|
+
}));
|
|
1407
|
+
}
|
|
1408
|
+
/** Whether there is a history worth showing: at least one change recorded, not only a carried start. */
|
|
1409
|
+
function hasStatusHistory(history) {
|
|
1410
|
+
return !!history?.some((step) => !step.carried);
|
|
1411
|
+
}
|
|
1412
|
+
function renderEvidence$2(evidence, now) {
|
|
1413
|
+
if (!evidence) return "";
|
|
1414
|
+
const edited = evidence.editedAt && Number.isFinite(Date.parse(evidence.editedAt)) ? ` · edited ${escapeHtml$1(formatRelativeTime(Date.parse(evidence.editedAt), now))}` : "";
|
|
1415
|
+
const body = evidence.body ? `<p class="pr-helper-status-evidence-body">${renderInlineText(evidence.body)}</p>` : "";
|
|
1416
|
+
const link = evidence.url && /^https?:\/\//i.test(evidence.url) ? `<a class="pr-helper-status-evidence-link" href="${escapeHtml$1(evidence.url)}" target="_blank" rel="noopener noreferrer">Open the ${evidence.kind === "review" ? "review" : "comment"}</a>` : "";
|
|
1417
|
+
return `<blockquote class="pr-helper-status-evidence" data-status-evidence>
|
|
1418
|
+
<p class="pr-helper-status-evidence-who">${escapeHtml$1(evidenceAttribution(evidence))}${edited}</p>
|
|
1419
|
+
${body}${link}
|
|
1420
|
+
</blockquote>`;
|
|
1421
|
+
}
|
|
1422
|
+
function renderStep$1(from, step, now) {
|
|
1423
|
+
const source = [statusBasisPhrase(step), step.at !== void 0 ? formatRelativeTime(step.at, now) : ""].filter(Boolean).join(" · ");
|
|
1424
|
+
const trigger = statusTriggerSentence(step);
|
|
1425
|
+
return `<li class="pr-helper-reassessment-step" data-status-step="${escapeHtml$1(step.status)}">
|
|
1426
|
+
<span class="pr-helper-reassessment-step-move pr-helper-status-step-headline">${escapeHtml$1(statusStepHeadline(from, step))}</span>
|
|
1427
|
+
${source ? `<span class="pr-helper-reassessment-step-source">${escapeHtml$1(source)}</span>` : ""}
|
|
1428
|
+
${trigger ? `<p class="pr-helper-reassessment-reason">${escapeHtml$1(trigger)}</p>` : ""}
|
|
1429
|
+
${step.reason ? `<p class="pr-helper-reassessment-reason">${renderInlineText(step.reason)}</p>` : ""}
|
|
1430
|
+
${renderEvidence$2(step.evidence, now)}
|
|
1431
|
+
</li>`;
|
|
1432
|
+
}
|
|
1433
|
+
/** The disclosure, closed. Nothing when the status has never changed. */
|
|
1434
|
+
function renderStatusHistory(history, now = Date.now()) {
|
|
1435
|
+
if (!hasStatusHistory(history)) return "";
|
|
1436
|
+
return `<details class="pr-helper-reassessment-history pr-helper-status-history" data-status-history>
|
|
1437
|
+
<summary class="pr-helper-reassessment-history-summary">Status history</summary>
|
|
1438
|
+
<ol class="pr-helper-reassessment-steps">${statusSteps(history).map(({ from, step }) => renderStep$1(from, step, now)).join("")}</ol>
|
|
1439
|
+
</details>`;
|
|
1440
|
+
}
|
|
1441
|
+
//#endregion
|
|
1304
1442
|
//#region ../../packages/review-harness/src/domain/ui/components/finding_copy.ts
|
|
1305
1443
|
/**
|
|
1306
1444
|
* An issue's Copy: its title in bold, the description, the update, the investigation, how its grade
|
|
@@ -1325,8 +1463,12 @@ function issueText(issue, investigation, assessment) {
|
|
|
1325
1463
|
return join$1([
|
|
1326
1464
|
bold(issue.title),
|
|
1327
1465
|
issue.description,
|
|
1328
|
-
update(issue.reconciliationStatus && findingStatusLabel(issue.reconciliationStatus), issue.reconciliationNote),
|
|
1329
|
-
...investigation ? [
|
|
1466
|
+
update(issue.reconciliationStatus && findingStatusLabel(issue.reconciliationStatus), issue.reconciliationNote, issue.reconciledAgainst),
|
|
1467
|
+
...investigation ? [
|
|
1468
|
+
investigated(issue.investigationImpact, issue.investigationNote),
|
|
1469
|
+
reassessed(assessment),
|
|
1470
|
+
statusHistory(issue.statusHistory)
|
|
1471
|
+
] : [],
|
|
1330
1472
|
where(issue.file, issue.line, issue.relatedFiles)
|
|
1331
1473
|
]);
|
|
1332
1474
|
}
|
|
@@ -1334,8 +1476,8 @@ function questionText(question, investigation, assessment) {
|
|
|
1334
1476
|
return join$1([
|
|
1335
1477
|
bold(question.question),
|
|
1336
1478
|
question.whyThisMatters,
|
|
1337
|
-
labelled(
|
|
1338
|
-
update(question.reconciliationStatus && questionStatusLabel(question.reconciliationStatus), question.reconciliationNote),
|
|
1479
|
+
labelled(reconciledAnswerHeading(question.reconciledAgainst), question.reconciliationAnswer),
|
|
1480
|
+
update(question.reconciliationStatus && questionStatusLabel(question.reconciliationStatus), question.reconciliationNote, question.reconciledAgainst),
|
|
1339
1481
|
...investigation ? [
|
|
1340
1482
|
labelled("Answer from investigation", question.investigationAnswer),
|
|
1341
1483
|
investigated(question.investigationImpact, question.investigationNote),
|
|
@@ -1345,9 +1487,9 @@ function questionText(question, investigation, assessment) {
|
|
|
1345
1487
|
]);
|
|
1346
1488
|
}
|
|
1347
1489
|
/** What the last update said about the finding, under the status the card shows beside it. */
|
|
1348
|
-
function update(status, note) {
|
|
1490
|
+
function update(status, note, against) {
|
|
1349
1491
|
if (!note?.trim()) return void 0;
|
|
1350
|
-
return
|
|
1492
|
+
return `**${reconciledAfter(against)}${status ? ` (${status.toLowerCase()})` : ""}:** ${note.trim()}`;
|
|
1351
1493
|
}
|
|
1352
1494
|
/**
|
|
1353
1495
|
* What an investigation concluded, under the verdict the card's badge shows. "Investigated" is the
|
|
@@ -1367,6 +1509,24 @@ function reassessed(assessment) {
|
|
|
1367
1509
|
const move = `${assessment.original ?? "ungraded"} → ${assessment.current}, ${sourcePhrase(last.source)}`;
|
|
1368
1510
|
return last.reason?.trim() ? `**Reassessed (${move}):** ${last.reason.trim()}` : `**Reassessed:** ${move}.`;
|
|
1369
1511
|
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Every change of status, each with what it rested on and the evidence as it read then: the record
|
|
1514
|
+
* of why the review reached its conclusion, which a link to a since-edited comment could not keep.
|
|
1515
|
+
* Copy only, like the reassessed grade; a feedback draft is not the place for the review's working.
|
|
1516
|
+
*/
|
|
1517
|
+
function statusHistory(history) {
|
|
1518
|
+
if (!hasStatusHistory(history)) return void 0;
|
|
1519
|
+
return `**Status history:**\n${statusSteps(history).map(({ from, step }) => {
|
|
1520
|
+
const basis = statusBasisPhrase(step);
|
|
1521
|
+
const said = [statusTriggerSentence(step), step.reason?.trim()].filter(Boolean).join(" ");
|
|
1522
|
+
const head = `- ${statusStepHeadline(from, step)}${basis ? ` ${basis}` : ""}${said ? `: ${said}` : ""}`;
|
|
1523
|
+
const evidence = step.evidence;
|
|
1524
|
+
if (!evidence) return head;
|
|
1525
|
+
const quoted = evidence.body ? `: ${evidence.body.replace(/\s+/g, " ")}` : "";
|
|
1526
|
+
const link = evidence.url ? ` (${evidence.url})` : "";
|
|
1527
|
+
return `${head}\n > ${evidenceAttribution(evidence)}${quoted}${link}`;
|
|
1528
|
+
}).join("\n")}`;
|
|
1529
|
+
}
|
|
1370
1530
|
function labelled(label, text) {
|
|
1371
1531
|
return text?.trim() ? `**${label}:** ${text.trim()}` : void 0;
|
|
1372
1532
|
}
|
|
@@ -1409,25 +1569,6 @@ function shortSha$1(sha) {
|
|
|
1409
1569
|
return sha.slice(0, MIN_ABBREVIATED_LENGTH);
|
|
1410
1570
|
}
|
|
1411
1571
|
//#endregion
|
|
1412
|
-
//#region ../../packages/review-harness/src/domain/utils/relative_time.ts
|
|
1413
|
-
function formatRelativeTime(ts, now = Date.now()) {
|
|
1414
|
-
const diffMs = Math.max(0, now - ts);
|
|
1415
|
-
const minute = 6e4;
|
|
1416
|
-
const hour = 60 * minute;
|
|
1417
|
-
const day = 24 * hour;
|
|
1418
|
-
if (diffMs < minute) return "Just now";
|
|
1419
|
-
if (diffMs < hour) {
|
|
1420
|
-
const mins = Math.floor(diffMs / minute);
|
|
1421
|
-
return `${mins} min${mins === 1 ? "" : "s"} ago`;
|
|
1422
|
-
}
|
|
1423
|
-
if (diffMs < day) {
|
|
1424
|
-
const hours = Math.floor(diffMs / hour);
|
|
1425
|
-
return `${hours} hour${hours === 1 ? "" : "s"} ago`;
|
|
1426
|
-
}
|
|
1427
|
-
const days = Math.floor(diffMs / day);
|
|
1428
|
-
return `${days} day${days === 1 ? "" : "s"} ago`;
|
|
1429
|
-
}
|
|
1430
|
-
//#endregion
|
|
1431
1572
|
//#region ../../packages/review-harness/src/domain/ui/components/intent_note.ts
|
|
1432
1573
|
/**
|
|
1433
1574
|
* The Overview's unclear motive, kept in step with its Intent question.
|
|
@@ -2613,9 +2754,9 @@ function renderIssueConfidence(confidence) {
|
|
|
2613
2754
|
* is the axis the reviewer is scanning for. They already know what the findings were; what they
|
|
2614
2755
|
* came back to learn is which ones the author dealt with.
|
|
2615
2756
|
*/
|
|
2616
|
-
function renderFindingStatus(status) {
|
|
2757
|
+
function renderFindingStatus(status, against) {
|
|
2617
2758
|
if (!status) return "";
|
|
2618
|
-
return `<span${pillAttrs(`pr-helper-finding-status pr-helper-finding-status-${status}`, findingStatusTip(status))}>${escapeHtml$1(findingStatusLabel(status))}</span>`;
|
|
2759
|
+
return `<span${pillAttrs(`pr-helper-finding-status pr-helper-finding-status-${status}`, findingStatusTip(status, against))}>${escapeHtml$1(findingStatusLabel(status))}</span>`;
|
|
2619
2760
|
}
|
|
2620
2761
|
/**
|
|
2621
2762
|
* The same pill, for a question.
|
|
@@ -2624,9 +2765,9 @@ function renderFindingStatus(status) {
|
|
|
2624
2765
|
* a finding is `resolved` and a question is `answered`, which are different events. Nothing fixed
|
|
2625
2766
|
* the question; somebody now knows the answer.
|
|
2626
2767
|
*/
|
|
2627
|
-
function renderQuestionStatus(status) {
|
|
2768
|
+
function renderQuestionStatus(status, against) {
|
|
2628
2769
|
if (!status) return "";
|
|
2629
|
-
return `<span${pillAttrs(`pr-helper-finding-status pr-helper-finding-status-${status === "answered" ? "resolved" : status}`, questionStatusTip(status))}>${escapeHtml$1(questionStatusLabel(status))}</span>`;
|
|
2770
|
+
return `<span${pillAttrs(`pr-helper-finding-status pr-helper-finding-status-${status === "answered" ? "resolved" : status}`, questionStatusTip(status, against))}>${escapeHtml$1(questionStatusLabel(status))}</span>`;
|
|
2630
2771
|
}
|
|
2631
2772
|
/** The model's one-line reason for a status, when it gave one. */
|
|
2632
2773
|
function renderReconciliationNote(note) {
|
|
@@ -2771,7 +2912,7 @@ function renderIssuesTab(session, investigatingId, options) {
|
|
|
2771
2912
|
${options?.askBuddy ? renderAskBuddyMenu("issue") : ""}
|
|
2772
2913
|
</div>
|
|
2773
2914
|
<div class="pr-helper-issue-badges">
|
|
2774
|
-
${renderFindingStatus(issue.reconciliationStatus)}
|
|
2915
|
+
${renderFindingStatus(issue.reconciliationStatus, issue.reconciledAgainst)}
|
|
2775
2916
|
${renderDismissedBadge(isDismissed)}
|
|
2776
2917
|
${renderInvestigationImpact(issue.investigationImpact)}
|
|
2777
2918
|
${renderSectionTag(issue.sectionId, sections)}
|
|
@@ -2783,6 +2924,7 @@ function renderIssuesTab(session, investigatingId, options) {
|
|
|
2783
2924
|
${renderAssessmentRegion("issue", assessment, grading)}
|
|
2784
2925
|
<p class="pr-helper-issue-desc">${renderInlineText(issue.description)}</p>${renderCardExplanation(explanationOf(session, "issue", issue.id), options)}
|
|
2785
2926
|
${renderReconciliationNote(issue.reconciliationNote)}
|
|
2927
|
+
${renderStatusHistory(issue.statusHistory, now)}
|
|
2786
2928
|
${renderInvestigationNote(issue.investigationNote)}
|
|
2787
2929
|
${renderExistingDiscussion(issue.existingDiscussion)}
|
|
2788
2930
|
${renderFileLocation(issue.file, "pr-helper-issue-loc", issue.line)}
|
|
@@ -2811,7 +2953,12 @@ function renderIssuesTab(session, investigatingId, options) {
|
|
|
2811
2953
|
*/
|
|
2812
2954
|
function closedFindingsSummary(counts) {
|
|
2813
2955
|
const byCommits = counts.done - counts.reviewerClosed;
|
|
2814
|
-
if (counts.reviewerClosed === 0)
|
|
2956
|
+
if (counts.reviewerClosed === 0) {
|
|
2957
|
+
const byConversation = counts.byConversation ?? 0;
|
|
2958
|
+
if (byConversation === 0) return `Resolved by the new commits (${counts.done})`;
|
|
2959
|
+
if (byConversation === counts.done) return `Resolved by the conversation (${counts.done})`;
|
|
2960
|
+
return `Resolved (${counts.done})`;
|
|
2961
|
+
}
|
|
2815
2962
|
if (byCommits === 0) return `Dismissed by you (${counts.done})`;
|
|
2816
2963
|
return `Dealt with (${counts.done})`;
|
|
2817
2964
|
}
|
|
@@ -2854,7 +3001,8 @@ function renderStaleDismissal(stale) {
|
|
|
2854
3001
|
function renderDismissToggle(issue, isDismissed, stale, dismissal) {
|
|
2855
3002
|
const label = stale ? "Dismiss again" : isDismissed ? "Reopen" : "Dismiss";
|
|
2856
3003
|
const settled = issue.reconciliationStatus === "resolved";
|
|
2857
|
-
|
|
3004
|
+
const settledBy = settled ? ` data-settled-by="${resolvedByConversation(issue) ? "conversation" : "code"}"` : "";
|
|
3005
|
+
return `<button type="button" class="pr-helper-issue-dismiss-toggle" data-issue-dismissed="${escapeHtml$1(issue.id)}" data-dismissed="${isDismissed}"${stale ? " data-stale=\"true\"" : ""}${dismissal ? ` data-dismissed-in="${escapeHtml$1(dismissal.sessionId)}"` : ""}${settled ? ` data-settled="true"${settledBy}` : ""}>${label}</button>`;
|
|
2858
3006
|
}
|
|
2859
3007
|
/**
|
|
2860
3008
|
* The group an Intent question lives in, keyed like a theme's so the workspace client finds its
|
|
@@ -2873,7 +3021,7 @@ function renderQuestionCard(q, sections, answered, records, now = Date.now(), in
|
|
|
2873
3021
|
const settled = q.reconciliationStatus === "answered";
|
|
2874
3022
|
const answerToggle = `<button type="button" class="pr-helper-question-answer-toggle" data-question-answered="${escapeHtml$1(q.id)}" data-answered="${answered}"${settled ? " data-settled=\"true\"" : ""}>${answered ? "Mark as unanswered" : "Mark answered"}</button>`;
|
|
2875
3023
|
const investigationAnswer = q.investigationAnswer?.trim() ? `<h5>Answer from investigation</h5><p class="pr-helper-issue-desc pr-helper-investigation-answer">${renderInlineText(q.investigationAnswer)}</p>` : "";
|
|
2876
|
-
const reconciliationAnswer = q.reconciliationAnswer?.trim() ? `<h5
|
|
3024
|
+
const reconciliationAnswer = q.reconciliationAnswer?.trim() ? `<h5>${escapeHtml$1(reconciledAnswerHeading(q.reconciledAgainst))}</h5><p class="pr-helper-issue-desc pr-helper-investigation-answer">${renderInlineText(q.reconciliationAnswer)}</p>` : "";
|
|
2877
3025
|
return `
|
|
2878
3026
|
<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))}"` : ""}>
|
|
2879
3027
|
<div class="pr-helper-issue-header">
|
|
@@ -2884,7 +3032,7 @@ function renderQuestionCard(q, sections, answered, records, now = Date.now(), in
|
|
|
2884
3032
|
${options?.askBuddy ? renderAskBuddyMenu("question") : ""}
|
|
2885
3033
|
</div>
|
|
2886
3034
|
<div class="pr-helper-issue-badges">
|
|
2887
|
-
${renderQuestionStatus(q.reconciliationStatus)}
|
|
3035
|
+
${renderQuestionStatus(q.reconciliationStatus, q.reconciledAgainst)}
|
|
2888
3036
|
${renderInvestigationImpact(q.investigationImpact)}
|
|
2889
3037
|
<!-- Rendered on every card, hidden by CSS inside a theme group, because the heading
|
|
2890
3038
|
above it already says the same word. The client moves cards between the group and
|
|
@@ -5217,14 +5365,19 @@ function updateFacts(outcome) {
|
|
|
5217
5365
|
const counts = outcome.counts;
|
|
5218
5366
|
if (!counts || !outcome.toSha) return null;
|
|
5219
5367
|
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
5368
|
+
const byConversation = counts.resolvedByConversation ?? 0;
|
|
5369
|
+
const fixed = counts.resolved - byConversation;
|
|
5220
5370
|
const changes = [
|
|
5221
|
-
|
|
5371
|
+
fixed ? `${plural(fixed, "issue", "issues")} fixed` : "",
|
|
5372
|
+
byConversation ? `${plural(byConversation, "issue", "issues")} resolved by the conversation` : "",
|
|
5373
|
+
counts.reopened ? `${plural(counts.reopened, "issue", "issues")} reopened` : "",
|
|
5222
5374
|
counts.newIssues ? plural(counts.newIssues, "new issue", "new issues") : "",
|
|
5223
5375
|
counts.affected ? `${counts.affected} to look at again` : "",
|
|
5224
5376
|
counts.questionsAnswered ? `${plural(counts.questionsAnswered, "question", "questions")} answered` : ""
|
|
5225
5377
|
].filter(Boolean);
|
|
5226
5378
|
const grades = counts.gradeChanges ? [`${plural(counts.gradeChanges, "grade change", "grade changes")} suggested`] : [];
|
|
5227
|
-
|
|
5379
|
+
const basis = outcome.reassessed;
|
|
5380
|
+
return [...basis && !basis.code ? [escapeHtml$1("Reassessed against the conversation")] : [`<code>${escapeHtml$1(outcome.toSha.slice(0, 7))}</code>`, ...basis?.discussion ? [escapeHtml$1("with the conversation")] : []], ...[...changes.length ? changes : ["No change to the findings"], ...grades].map(escapeHtml$1)];
|
|
5228
5381
|
}
|
|
5229
5382
|
/**
|
|
5230
5383
|
* The two things that can go quietly half-wrong, said out loud.
|
|
@@ -10535,7 +10688,8 @@ var TOPICS = [
|
|
|
10535
10688
|
<li><code>prreviewbuddy open</code> to list the reviews you have and open this workspace</li>
|
|
10536
10689
|
</ul>
|
|
10537
10690
|
<p>Or press <kbd class="prb-kbd">⌘P</kbd> (<kbd class="prb-kbd">Ctrl+P</kbd> elsewhere) to add a
|
|
10538
|
-
review from here.
|
|
10691
|
+
review from here. The panel follows the reviews it started while it stays open, and closing it
|
|
10692
|
+
stops nothing. <code>prreviewbuddy help</code> lists every command, and
|
|
10539
10693
|
<code>prreviewbuddy review --help</code> every option a review takes.</p>
|
|
10540
10694
|
<p>There is no account and no API key. PR Review Buddy uses the coding agent installed on your
|
|
10541
10695
|
machine and that agent's existing authentication, and never asks you for a credential.</p>`
|
|
@@ -11445,16 +11599,24 @@ function createSlots(capacity) {
|
|
|
11445
11599
|
}
|
|
11446
11600
|
/** How many pull requests one submission may name. See `MAX_PULL_REQUESTS_PER_BATCH`. */
|
|
11447
11601
|
var MAX_BATCH = 20;
|
|
11448
|
-
/**
|
|
11602
|
+
/**
|
|
11603
|
+
* How long a finished batch stays answerable once nobody is asking about it. An open panel keeps
|
|
11604
|
+
* asking (`KEEPALIVE` in the client is well inside this), so a batch is only forgotten after its
|
|
11605
|
+
* panel has gone, and its Retry with it.
|
|
11606
|
+
*/
|
|
11449
11607
|
var FINISHED_BATCH_TTL_MS = 6e5;
|
|
11450
11608
|
function realAddReviewDeps() {
|
|
11451
11609
|
return {
|
|
11452
11610
|
resolveAgent: (flag) => resolveAgent(flag ? { flag } : {}),
|
|
11453
|
-
agents: async () => (await detectAgents()).map((agent) =>
|
|
11454
|
-
|
|
11455
|
-
|
|
11456
|
-
|
|
11457
|
-
|
|
11611
|
+
agents: async () => (await detectAgents()).map((agent) => {
|
|
11612
|
+
const modelHelp = modelHelpOf(agent.id);
|
|
11613
|
+
return {
|
|
11614
|
+
id: agent.id,
|
|
11615
|
+
displayName: agent.displayName,
|
|
11616
|
+
installed: agent.state === "installed",
|
|
11617
|
+
...modelHelp ? { modelHelp } : {}
|
|
11618
|
+
};
|
|
11619
|
+
}),
|
|
11458
11620
|
folders: () => discoverWorkingFolders(),
|
|
11459
11621
|
summaries: () => recentWorkspaces(Number.MAX_SAFE_INTEGER),
|
|
11460
11622
|
remoteOf: originRemote,
|
|
@@ -11478,6 +11640,8 @@ function realAddReviewDeps() {
|
|
|
11478
11640
|
findExisting: findExistingReview,
|
|
11479
11641
|
prepare: prepareReview,
|
|
11480
11642
|
settle: (workspaceId) => settleReview(workspaceId, "standard"),
|
|
11643
|
+
observe: (workspaceId) => observeReview(workspaceId),
|
|
11644
|
+
commitPresent: (root, sha) => refExists(root, sha),
|
|
11481
11645
|
track,
|
|
11482
11646
|
chooseFolder: folderChooser(),
|
|
11483
11647
|
concurrency: 2,
|
|
@@ -11493,6 +11657,13 @@ function realAddReviewDeps() {
|
|
|
11493
11657
|
home: homedir()
|
|
11494
11658
|
};
|
|
11495
11659
|
}
|
|
11660
|
+
function modelHelpOf(agentId) {
|
|
11661
|
+
try {
|
|
11662
|
+
return agentById(agentId).modelSelectionHelp;
|
|
11663
|
+
} catch {
|
|
11664
|
+
return;
|
|
11665
|
+
}
|
|
11666
|
+
}
|
|
11496
11667
|
function agentNameOf(agentId) {
|
|
11497
11668
|
try {
|
|
11498
11669
|
return agentById(agentId).displayName;
|
|
@@ -11500,10 +11671,60 @@ function agentNameOf(agentId) {
|
|
|
11500
11671
|
return agentId;
|
|
11501
11672
|
}
|
|
11502
11673
|
}
|
|
11503
|
-
var refuse = (status, error) => ({
|
|
11674
|
+
var refuse = (status, error, extra = {}) => ({
|
|
11504
11675
|
status,
|
|
11505
|
-
body: {
|
|
11676
|
+
body: {
|
|
11677
|
+
error,
|
|
11678
|
+
...extra
|
|
11679
|
+
}
|
|
11506
11680
|
});
|
|
11681
|
+
function baseChoiceOf(error) {
|
|
11682
|
+
const option = (where) => {
|
|
11683
|
+
const matched = where === "remote" ? error.remote : error.local;
|
|
11684
|
+
const commits = where === "remote" ? error.ranges?.remote : error.ranges?.local;
|
|
11685
|
+
return {
|
|
11686
|
+
base: shorthandOf(matched.ref),
|
|
11687
|
+
display: where === "remote" ? shorthandOf(matched.ref) : shortNameOf(matched.ref),
|
|
11688
|
+
where,
|
|
11689
|
+
...commits ? { commits: commits.length } : {}
|
|
11690
|
+
};
|
|
11691
|
+
};
|
|
11692
|
+
return {
|
|
11693
|
+
name: shortNameOf(error.local.ref),
|
|
11694
|
+
discovered: error.discovered,
|
|
11695
|
+
...error.divergence ? { divergence: error.divergence } : {},
|
|
11696
|
+
options: [option("remote"), option("local")]
|
|
11697
|
+
};
|
|
11698
|
+
}
|
|
11699
|
+
/**
|
|
11700
|
+
* What the panel says for an error a review could not get past.
|
|
11701
|
+
*
|
|
11702
|
+
* Two errors are known by their class and said in the panel's own words. Anything else keeps its
|
|
11703
|
+
* message, which is already written for a person, but only its first paragraph is the sentence:
|
|
11704
|
+
* the rest (ref listings, commands to run) goes behind Technical details, kept whole for whoever
|
|
11705
|
+
* needs to diagnose it.
|
|
11706
|
+
*/
|
|
11707
|
+
function refusalOf(error) {
|
|
11708
|
+
const text = message(error);
|
|
11709
|
+
if (error instanceof AmbiguousRefError) {
|
|
11710
|
+
const choice = baseChoiceOf(error);
|
|
11711
|
+
return {
|
|
11712
|
+
error: `\`${choice.name}\` is both a local and a remote branch, and they are different commits. Choose which one to compare against.`,
|
|
11713
|
+
detail: text,
|
|
11714
|
+
baseChoice: choice
|
|
11715
|
+
};
|
|
11716
|
+
}
|
|
11717
|
+
if (error instanceof DetachedHeadError) return { error: "This folder is on a detached HEAD, so there is no branch to review. Check out a branch in it, or review its pull request instead." };
|
|
11718
|
+
return briefly(text);
|
|
11719
|
+
}
|
|
11720
|
+
/** A message's first paragraph as the sentence, and the whole of it as the detail when there is more. */
|
|
11721
|
+
function briefly(text) {
|
|
11722
|
+
const first = text.split(/\n\s*\n/)[0].trim();
|
|
11723
|
+
return first && first !== text.trim() ? {
|
|
11724
|
+
error: first,
|
|
11725
|
+
detail: text
|
|
11726
|
+
} : { error: text };
|
|
11727
|
+
}
|
|
11507
11728
|
/** A path as a person reads it, with their home folder as `~`. */
|
|
11508
11729
|
function displayPath(path, home) {
|
|
11509
11730
|
return home && (path === home || path.startsWith(`${home}/`)) ? `~${path.slice(home.length)}` : path;
|
|
@@ -11532,15 +11753,15 @@ function unresolvedAgent(resolution) {
|
|
|
11532
11753
|
* harness's own fallback: an id is always passed down, so `startJob` never has to guess.
|
|
11533
11754
|
*/
|
|
11534
11755
|
async function agentFor(flag, deps) {
|
|
11535
|
-
if (flag !== void 0 && typeof flag !== "string") return refuse(400, "Say which agent by its id.");
|
|
11756
|
+
if (flag !== void 0 && typeof flag !== "string") return refuse(400, "Say which agent by its id.", { field: "agent" });
|
|
11536
11757
|
const resolution = await deps.resolveAgent(flag || void 0);
|
|
11537
11758
|
if (resolution.kind === "resolved") return { agentId: resolution.agent.id };
|
|
11538
|
-
if (resolution.kind === "ambiguous") return refuse(400, "More than one review agent is installed. Choose one under Review options.");
|
|
11539
|
-
return refuse(resolution.kind === "unknown-id" ? 400 : 409, unresolvedAgent(resolution));
|
|
11759
|
+
if (resolution.kind === "ambiguous") return refuse(400, "More than one review agent is installed. Choose one under Review options.", { field: "agent" });
|
|
11760
|
+
return refuse(resolution.kind === "unknown-id" ? 400 : 409, unresolvedAgent(resolution), { field: "agent" });
|
|
11540
11761
|
}
|
|
11541
11762
|
function modelOf(value) {
|
|
11542
11763
|
if (value === void 0 || value === null || value === "") return void 0;
|
|
11543
|
-
if (typeof value !== "string" || value.length > 200) return refuse(400, "The model must be a short name.");
|
|
11764
|
+
if (typeof value !== "string" || value.length > 200) return refuse(400, "The model must be a short name.", { field: "model" });
|
|
11544
11765
|
return value.trim() || void 0;
|
|
11545
11766
|
}
|
|
11546
11767
|
/**
|
|
@@ -11649,38 +11870,115 @@ async function previewPullRequests(text, deps) {
|
|
|
11649
11870
|
}
|
|
11650
11871
|
var batches = /* @__PURE__ */ new Map();
|
|
11651
11872
|
function forgetFinished(now) {
|
|
11652
|
-
for (const [id, batch] of batches) if (batch.finishedAt !== null && now - batch.finishedAt > FINISHED_BATCH_TTL_MS) batches.delete(id);
|
|
11873
|
+
for (const [id, batch] of batches) if (batch.finishedAt !== null && now - Math.max(batch.finishedAt, batch.seenAt) > FINISHED_BATCH_TTL_MS) batches.delete(id);
|
|
11653
11874
|
}
|
|
11654
11875
|
function newBatch(rows) {
|
|
11655
11876
|
forgetFinished(Date.now());
|
|
11877
|
+
const now = Date.now();
|
|
11656
11878
|
const batch = {
|
|
11657
11879
|
id: randomUUID(),
|
|
11658
11880
|
rows,
|
|
11659
|
-
finishedAt:
|
|
11881
|
+
finishedAt: now,
|
|
11882
|
+
seenAt: now,
|
|
11883
|
+
running: 0,
|
|
11884
|
+
rerun: () => {}
|
|
11660
11885
|
};
|
|
11661
11886
|
batches.set(batch.id, batch);
|
|
11662
11887
|
return batch;
|
|
11663
11888
|
}
|
|
11889
|
+
/**
|
|
11890
|
+
* Count work against a batch, inside the restart guard. The batch is finished only when nothing
|
|
11891
|
+
* counted against it is still going, so a retry reopens a batch an open panel can then follow again.
|
|
11892
|
+
*/
|
|
11893
|
+
function follow(batch, work, deps, driving = true) {
|
|
11894
|
+
batch.running += 1;
|
|
11895
|
+
batch.finishedAt = null;
|
|
11896
|
+
(driving ? deps.track(work) : work).catch(() => {}).finally(() => {
|
|
11897
|
+
batch.running -= 1;
|
|
11898
|
+
if (batch.running === 0) batch.finishedAt = Date.now();
|
|
11899
|
+
});
|
|
11900
|
+
}
|
|
11901
|
+
function fail(row, error) {
|
|
11902
|
+
const refusal = refusalOf(error);
|
|
11903
|
+
row.status = "failed";
|
|
11904
|
+
row.reason = refusal.error;
|
|
11905
|
+
if (refusal.detail) row.detail = refusal.detail;
|
|
11906
|
+
}
|
|
11664
11907
|
var rowsOf = (batch) => batch.rows.map((row) => ({ ...row }));
|
|
11665
11908
|
/**
|
|
11666
11909
|
* Create the review for a row's target, or find the one it has, under the per-target lock. The
|
|
11667
11910
|
* caller holds an analysis slot: nothing is created without one, so nothing created waits.
|
|
11668
11911
|
*/
|
|
11669
|
-
|
|
11670
|
-
|
|
11912
|
+
/**
|
|
11913
|
+
* `fresh` is a fresh start's: a new review of the target as it is now, keeping whatever review the
|
|
11914
|
+
* branch or request already has, as `--fresh` does. Without it the lookup finds the latest review of
|
|
11915
|
+
* the same branch or request, which for a fresh start is the very failure being started over.
|
|
11916
|
+
*/
|
|
11917
|
+
async function prepareRow(row, target, agentId, model, deps, fresh = false) {
|
|
11918
|
+
const prepared = await oneCreationAtATime(target, () => deps.prepare(target, agentId, fresh, model));
|
|
11671
11919
|
row.reviewId = prepared.kind === "new" ? prepared.workspaceId : prepared.existing.id;
|
|
11672
|
-
if (prepared.kind === "existing") row.status = "already-reviewed";
|
|
11920
|
+
if (prepared.kind === "existing") row.status = prepared.existing.inProgress ? "reviewing" : "already-reviewed";
|
|
11673
11921
|
else row.status = "reviewing";
|
|
11674
11922
|
if (prepared.kind === "resume") row.kept = keptOf(prepared.failure.agentId, prepared.failure.requestedModel, deps);
|
|
11923
|
+
else delete row.kept;
|
|
11675
11924
|
return prepared;
|
|
11676
11925
|
}
|
|
11677
|
-
|
|
11678
|
-
|
|
11926
|
+
/**
|
|
11927
|
+
* Take a prepared row to its end. A new or resumed review is run here, holding the slot. One that
|
|
11928
|
+
* something else is already analysing is watched instead, never driven: the watch is handed to the
|
|
11929
|
+
* batch rather than awaited, so the caller returns, and its slot goes back, at once, while the row
|
|
11930
|
+
* stays Analysing until that analysis ends. Counted against the batch, not against restarts.
|
|
11931
|
+
*/
|
|
11932
|
+
async function completeRow(row, prepared, batch, deps) {
|
|
11933
|
+
if (prepared.kind !== "existing") return settleRow(row, deps.settle(row.reviewId));
|
|
11934
|
+
if (prepared.existing.inProgress) follow(batch, settleRow(row, deps.observe(row.reviewId)), deps, false);
|
|
11935
|
+
}
|
|
11936
|
+
async function settleRow(row, settling) {
|
|
11937
|
+
const settled = await settling;
|
|
11679
11938
|
if (settled.ok) row.status = "ready";
|
|
11680
|
-
else {
|
|
11939
|
+
else if (settled.stopped) {
|
|
11681
11940
|
row.status = "failed";
|
|
11682
|
-
row.reason =
|
|
11941
|
+
row.reason = "Stopped";
|
|
11942
|
+
} else fail(row, settled.reason);
|
|
11943
|
+
}
|
|
11944
|
+
/**
|
|
11945
|
+
* A retry runs against the commit the row was first resolved to, never a newer one: a retry
|
|
11946
|
+
* resumes what was asked for, and a moved branch or pull request is a fresh review's business. When
|
|
11947
|
+
* that commit has gone (a force push, then a prune), there is nothing left to retry, and the row
|
|
11948
|
+
* says so and offers the fresh start instead of quietly reviewing something else.
|
|
11949
|
+
*/
|
|
11950
|
+
async function stillThere(row, target, what, deps) {
|
|
11951
|
+
if (await deps.commitPresent(target.originRepoPath, target.sha)) return true;
|
|
11952
|
+
row.status = "failed";
|
|
11953
|
+
row.reason = `The commit this review was of (${target.sha.slice(0, 7)}) is no longer in this repository, so it can't be retried as it was. Start a fresh review to analyse the ${what} as it is now.`;
|
|
11954
|
+
row.stale = true;
|
|
11955
|
+
return false;
|
|
11956
|
+
}
|
|
11957
|
+
/**
|
|
11958
|
+
* Retry a row whose review exists, by that review's id. Not through the lookup a start uses, which
|
|
11959
|
+
* finds the latest review of the branch or request: one made since, of newer commits, would be
|
|
11960
|
+
* handed back in place of the one that failed, and a retry would quietly become a different review.
|
|
11961
|
+
*
|
|
11962
|
+
* Resumed when it is still a failure nothing is running, as its own page's Retry does. Otherwise
|
|
11963
|
+
* something has taken it up since (that Retry, a terminal), and it is watched rather than run again.
|
|
11964
|
+
*/
|
|
11965
|
+
async function retryReview(row, batch, deps) {
|
|
11966
|
+
const reviewId = row.reviewId;
|
|
11967
|
+
const kept = deps.resumeOf(reviewId);
|
|
11968
|
+
row.status = "reviewing";
|
|
11969
|
+
if (kept) {
|
|
11970
|
+
row.kept = kept;
|
|
11971
|
+
return settleRow(row, deps.settle(reviewId));
|
|
11683
11972
|
}
|
|
11973
|
+
follow(batch, settleRow(row, deps.observe(reviewId)), deps, false);
|
|
11974
|
+
}
|
|
11975
|
+
/**
|
|
11976
|
+
* A fresh start that ended before it made its review leaves the row exactly as it was: the old
|
|
11977
|
+
* target, the old review, and still `stale`. So the row keeps offering the fresh start, and a plain
|
|
11978
|
+
* Retry finds the old commit gone again, rather than resuming the old review against it.
|
|
11979
|
+
*/
|
|
11980
|
+
function keepFreshPending(row, replaced) {
|
|
11981
|
+
if (row.status === "failed" && !replaced) row.stale = true;
|
|
11684
11982
|
}
|
|
11685
11983
|
function keptOf(agentId, model, deps) {
|
|
11686
11984
|
return {
|
|
@@ -11726,40 +12024,55 @@ async function startPullRequestBatch(body, deps) {
|
|
|
11726
12024
|
})));
|
|
11727
12025
|
/** Which row claimed each pull request, by what it resolved to rather than how it was spelled. */
|
|
11728
12026
|
const claimed = /* @__PURE__ */ new Map();
|
|
11729
|
-
|
|
12027
|
+
/** Each row's pull request as it was first resolved: head, base and all. What a retry runs against. */
|
|
12028
|
+
const resolved = [];
|
|
12029
|
+
const lane = async (item, index, fresh = false) => {
|
|
11730
12030
|
const row = batch.rows[index];
|
|
11731
12031
|
const release = await deps.slots.acquire();
|
|
11732
12032
|
row.status = "reviewing";
|
|
12033
|
+
/** A fresh start has made its review, so the row now belongs to the new target. */
|
|
12034
|
+
let replaced = false;
|
|
11733
12035
|
try {
|
|
11734
|
-
|
|
11735
|
-
if (
|
|
11736
|
-
row
|
|
11737
|
-
row.
|
|
11738
|
-
|
|
12036
|
+
let target = fresh ? void 0 : resolved[index];
|
|
12037
|
+
if (target) {
|
|
12038
|
+
if (!await stillThere(row, target, "pull request", deps)) return;
|
|
12039
|
+
if (row.reviewId) return await retryReview(row, batch, deps);
|
|
12040
|
+
} else {
|
|
12041
|
+
const remote = await deps.remoteOf(item.folder);
|
|
12042
|
+
if (!isGitHubRepository(remote, item.owner, item.repo)) {
|
|
12043
|
+
row.status = "failed";
|
|
12044
|
+
row.reason = remote ? `This folder is a clone of ${remote.owner}/${remote.repo}, not ${item.owner}/${item.repo}` : `This folder is not a clone of ${item.owner}/${item.repo}`;
|
|
12045
|
+
return;
|
|
12046
|
+
}
|
|
12047
|
+
target = await deps.resolvePullRequest(item.folder, item.url);
|
|
12048
|
+
if (!fresh) resolved[index] = target;
|
|
11739
12049
|
}
|
|
11740
|
-
const target = await deps.resolvePullRequest(item.folder, item.url);
|
|
11741
12050
|
const identity = batchIdentity(target);
|
|
11742
12051
|
const first = claimed.get(identity);
|
|
11743
|
-
if (first !== void 0) {
|
|
12052
|
+
if (first !== void 0 && first !== index) {
|
|
11744
12053
|
row.status = "already-reviewed";
|
|
11745
12054
|
row.reason = `Same pull request as ${batch.rows[first].label}`;
|
|
11746
12055
|
return;
|
|
11747
12056
|
}
|
|
11748
12057
|
claimed.set(identity, index);
|
|
11749
|
-
|
|
12058
|
+
const prepared = await prepareRow(row, target, agent.agentId, model, deps, fresh);
|
|
12059
|
+
if (fresh) {
|
|
12060
|
+
resolved[index] = target;
|
|
12061
|
+
replaced = true;
|
|
12062
|
+
}
|
|
12063
|
+
await completeRow(row, prepared, batch, deps);
|
|
11750
12064
|
} catch (error) {
|
|
11751
|
-
row
|
|
11752
|
-
row.reason = message(error);
|
|
12065
|
+
fail(row, error);
|
|
11753
12066
|
} finally {
|
|
12067
|
+
if (fresh) keepFreshPending(row, replaced);
|
|
11754
12068
|
release();
|
|
11755
12069
|
}
|
|
11756
12070
|
};
|
|
11757
|
-
|
|
12071
|
+
batch.rerun = (index, fresh) => follow(batch, lane(items[index], index, fresh), deps);
|
|
12072
|
+
follow(batch, runBatch(items, {
|
|
11758
12073
|
concurrency: deps.concurrency,
|
|
11759
12074
|
run: lane
|
|
11760
|
-
})
|
|
11761
|
-
batch.finishedAt = Date.now();
|
|
11762
|
-
});
|
|
12075
|
+
}), deps);
|
|
11763
12076
|
return {
|
|
11764
12077
|
status: 200,
|
|
11765
12078
|
body: {
|
|
@@ -11769,13 +12082,13 @@ async function startPullRequestBatch(body, deps) {
|
|
|
11769
12082
|
};
|
|
11770
12083
|
}
|
|
11771
12084
|
/**
|
|
11772
|
-
* A batch's rows as they stand.
|
|
11773
|
-
*
|
|
11774
|
-
* restart, which is the case worth saying plainly: whatever had not started never will.
|
|
12085
|
+
* A batch's rows as they stand. Asking counts as watching: a batch an open panel is still asking
|
|
12086
|
+
* about is never forgotten, so its Retry stays good for as long as the panel is open.
|
|
11775
12087
|
*/
|
|
11776
12088
|
function batchRows(id) {
|
|
11777
12089
|
const batch = batches.get(id);
|
|
11778
|
-
if (!batch) return
|
|
12090
|
+
if (!batch) return lostBatch();
|
|
12091
|
+
batch.seenAt = Date.now();
|
|
11779
12092
|
return {
|
|
11780
12093
|
status: 200,
|
|
11781
12094
|
body: {
|
|
@@ -11784,10 +12097,39 @@ function batchRows(id) {
|
|
|
11784
12097
|
}
|
|
11785
12098
|
};
|
|
11786
12099
|
}
|
|
11787
|
-
/**
|
|
11788
|
-
|
|
11789
|
-
|
|
11790
|
-
|
|
12100
|
+
/**
|
|
12101
|
+
* Said without guessing why. A restart forgets every batch, and a batch nobody has asked about for
|
|
12102
|
+
* a while is forgotten too; the server cannot tell which happened, so it says what follows from
|
|
12103
|
+
* either.
|
|
12104
|
+
*/
|
|
12105
|
+
var lostBatch = () => refuse(404, "This batch is no longer on the workspace server, so it can’t be followed or retried here. Reviews that had started are in your reviews list. Any that were still queued did not start.");
|
|
12106
|
+
/**
|
|
12107
|
+
* Run one failed row of a batch again, as it was first asked for: the same folder, agent, model
|
|
12108
|
+
* and base, and the same commits, never the branch or pull request as it has since moved on. A
|
|
12109
|
+
* review that was created and then failed resumes, as a Retry does on its own page. `fresh` is the
|
|
12110
|
+
* one exception, sent only when those commits have gone: it looks again at what is there now.
|
|
12111
|
+
* The other rows are left exactly as they are, and the batch is followed again until it ends.
|
|
12112
|
+
*/
|
|
12113
|
+
function retryBatchRow(id, key, fresh = false) {
|
|
12114
|
+
const batch = batches.get(id);
|
|
12115
|
+
if (!batch) return lostBatch();
|
|
12116
|
+
const index = batch.rows.findIndex((row) => row.key === key);
|
|
12117
|
+
const row = batch.rows[index];
|
|
12118
|
+
if (!row) return refuse(404, "That review is not part of this batch.");
|
|
12119
|
+
if (row.status !== "failed") return refuse(409, "Only a review that failed can be tried again.");
|
|
12120
|
+
row.status = "waiting";
|
|
12121
|
+
delete row.reason;
|
|
12122
|
+
delete row.detail;
|
|
12123
|
+
delete row.stale;
|
|
12124
|
+
batch.seenAt = Date.now();
|
|
12125
|
+
batch.rerun(index, fresh === true);
|
|
12126
|
+
return {
|
|
12127
|
+
status: 200,
|
|
12128
|
+
body: {
|
|
12129
|
+
rows: rowsOf(batch),
|
|
12130
|
+
finished: false
|
|
12131
|
+
}
|
|
12132
|
+
};
|
|
11791
12133
|
}
|
|
11792
12134
|
/** The recent and discoverable folders, for the Branch panel's list. */
|
|
11793
12135
|
async function listFolders(deps) {
|
|
@@ -11825,7 +12167,7 @@ async function inspectFolder(rawPath, deps, rawBase) {
|
|
|
11825
12167
|
status: 200,
|
|
11826
12168
|
body: {
|
|
11827
12169
|
ok: false,
|
|
11828
|
-
|
|
12170
|
+
...refusalOf(error)
|
|
11829
12171
|
}
|
|
11830
12172
|
};
|
|
11831
12173
|
}
|
|
@@ -11855,10 +12197,14 @@ async function inspectFolder(rawPath, deps, rawBase) {
|
|
|
11855
12197
|
/**
|
|
11856
12198
|
* Review the branch checked out in a folder: `prreviewbuddy` run there.
|
|
11857
12199
|
*
|
|
11858
|
-
*
|
|
11859
|
-
*
|
|
11860
|
-
*
|
|
11861
|
-
*
|
|
12200
|
+
* Always answers with a one-row batch, which is what the panel follows, so a branch shows its
|
|
12201
|
+
* progress exactly as a pull request does. With a free analysis slot it answers once the review
|
|
12202
|
+
* exists, and the analysis runs on in this process, counted by `track` like a Retry. With none free
|
|
12203
|
+
* it answers `waiting` and creates nothing until a slot frees: a review created to wait is a job
|
|
12204
|
+
* nobody is running, which the sweep would fail. A review the branch already has needs no slot.
|
|
12205
|
+
*
|
|
12206
|
+
* A refusal before anything exists comes back as a refusal, with the base question when that is
|
|
12207
|
+
* what it was, so the panel can ask it beside the folder rather than as a failed row.
|
|
11862
12208
|
*/
|
|
11863
12209
|
async function startBranchReview(body, deps) {
|
|
11864
12210
|
const path = typeof body?.path === "string" ? expandPath(body.path, deps.home) : null;
|
|
@@ -11870,7 +12216,10 @@ async function startBranchReview(body, deps) {
|
|
|
11870
12216
|
try {
|
|
11871
12217
|
target = await deps.resolveBranch(path, base);
|
|
11872
12218
|
} catch (error) {
|
|
11873
|
-
return
|
|
12219
|
+
return {
|
|
12220
|
+
status: 400,
|
|
12221
|
+
body: refusalOf(error)
|
|
12222
|
+
};
|
|
11874
12223
|
}
|
|
11875
12224
|
const agent = await agentFor(body?.agent, deps);
|
|
11876
12225
|
if (isAnswer(agent)) return agent;
|
|
@@ -11879,55 +12228,82 @@ async function startBranchReview(body, deps) {
|
|
|
11879
12228
|
label: target.branch,
|
|
11880
12229
|
status: "waiting"
|
|
11881
12230
|
};
|
|
12231
|
+
const batch = newBatch([row]);
|
|
12232
|
+
/**
|
|
12233
|
+
* The branch's run, holding a slot. A retry runs against the commit first resolved, wherever the
|
|
12234
|
+
* branch has moved since, unless `fresh`, which looks at the folder again as it is now.
|
|
12235
|
+
*/
|
|
12236
|
+
const run = async (release, again) => {
|
|
12237
|
+
const fresh = again === "fresh";
|
|
12238
|
+
let replaced = false;
|
|
12239
|
+
try {
|
|
12240
|
+
const next = fresh ? await deps.resolveBranch(path, base) : target;
|
|
12241
|
+
if (again === "retry") {
|
|
12242
|
+
if (!await stillThere(row, target, "branch", deps)) return;
|
|
12243
|
+
if (row.reviewId) return await retryReview(row, batch, deps);
|
|
12244
|
+
}
|
|
12245
|
+
const prepared = await prepareRow(row, next, agent.agentId, model, deps, fresh);
|
|
12246
|
+
target = next;
|
|
12247
|
+
replaced = fresh;
|
|
12248
|
+
await completeRow(row, prepared, batch, deps);
|
|
12249
|
+
} catch (error) {
|
|
12250
|
+
fail(row, error);
|
|
12251
|
+
} finally {
|
|
12252
|
+
if (fresh) keepFreshPending(row, replaced);
|
|
12253
|
+
release();
|
|
12254
|
+
}
|
|
12255
|
+
};
|
|
12256
|
+
batch.rerun = (_index, fresh) => follow(batch, (async () => run(await deps.slots.acquire(), fresh ? "fresh" : "retry"))(), deps);
|
|
12257
|
+
const answer = (outcome) => ({
|
|
12258
|
+
status: 200,
|
|
12259
|
+
body: {
|
|
12260
|
+
outcome,
|
|
12261
|
+
batchId: batch.id,
|
|
12262
|
+
rows: rowsOf(batch),
|
|
12263
|
+
...row.reviewId ? { reviewId: row.reviewId } : {},
|
|
12264
|
+
...row.kept ? { kept: row.kept } : {}
|
|
12265
|
+
}
|
|
12266
|
+
});
|
|
11882
12267
|
const release = deps.slots.tryAcquire();
|
|
11883
12268
|
if (!release) {
|
|
11884
12269
|
const existing = await deps.findExisting(target);
|
|
11885
|
-
if (existing && !deps.resumeOf(existing.id))
|
|
11886
|
-
|
|
11887
|
-
|
|
11888
|
-
|
|
11889
|
-
|
|
11890
|
-
}
|
|
11891
|
-
};
|
|
11892
|
-
const batch = newBatch([row]);
|
|
11893
|
-
deps.track((async () => {
|
|
11894
|
-
const later = await deps.slots.acquire();
|
|
11895
|
-
try {
|
|
11896
|
-
if ((await prepareRow(row, target, agent.agentId, model, deps)).kind !== "existing") await settleRow(row, deps);
|
|
11897
|
-
} catch (error) {
|
|
11898
|
-
row.status = "failed";
|
|
11899
|
-
row.reason = message(error);
|
|
11900
|
-
} finally {
|
|
11901
|
-
later();
|
|
11902
|
-
}
|
|
11903
|
-
})()).finally(() => {
|
|
11904
|
-
batch.finishedAt = Date.now();
|
|
11905
|
-
});
|
|
11906
|
-
return {
|
|
11907
|
-
status: 200,
|
|
11908
|
-
body: {
|
|
11909
|
-
outcome: "waiting",
|
|
11910
|
-
batchId: batch.id
|
|
12270
|
+
if (existing && !deps.resumeOf(existing.id)) {
|
|
12271
|
+
row.reviewId = existing.id;
|
|
12272
|
+
if (!existing.inProgress) {
|
|
12273
|
+
row.status = "already-reviewed";
|
|
12274
|
+
return answer("existing");
|
|
11911
12275
|
}
|
|
11912
|
-
|
|
12276
|
+
row.status = "reviewing";
|
|
12277
|
+
follow(batch, settleRow(row, deps.observe(existing.id)), deps, false);
|
|
12278
|
+
return answer("existing");
|
|
12279
|
+
}
|
|
12280
|
+
follow(batch, (async () => run(await deps.slots.acquire(), "first"))(), deps);
|
|
12281
|
+
return answer("waiting");
|
|
11913
12282
|
}
|
|
11914
12283
|
let prepared;
|
|
11915
12284
|
try {
|
|
11916
12285
|
prepared = await prepareRow(row, target, agent.agentId, model, deps);
|
|
11917
12286
|
} catch (error) {
|
|
11918
12287
|
release();
|
|
11919
|
-
|
|
12288
|
+
batches.delete(batch.id);
|
|
12289
|
+
return {
|
|
12290
|
+
status: 409,
|
|
12291
|
+
body: refusalOf(error)
|
|
12292
|
+
};
|
|
11920
12293
|
}
|
|
11921
|
-
if (prepared.kind === "existing")
|
|
11922
|
-
|
|
11923
|
-
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
|
|
11927
|
-
|
|
11928
|
-
|
|
12294
|
+
if (prepared.kind === "existing") {
|
|
12295
|
+
await completeRow(row, prepared, batch, deps);
|
|
12296
|
+
release();
|
|
12297
|
+
} else follow(batch, (async () => {
|
|
12298
|
+
try {
|
|
12299
|
+
await settleRow(row, deps.settle(row.reviewId));
|
|
12300
|
+
} catch (error) {
|
|
12301
|
+
fail(row, error);
|
|
12302
|
+
} finally {
|
|
12303
|
+
release();
|
|
11929
12304
|
}
|
|
11930
|
-
};
|
|
12305
|
+
})(), deps);
|
|
12306
|
+
return answer(prepared.kind);
|
|
11931
12307
|
}
|
|
11932
12308
|
async function chooseFolder(deps) {
|
|
11933
12309
|
if (!deps.chooseFolder) return refuse(404, "Choosing a folder is not available on this system. Type its path instead.");
|
|
@@ -11951,7 +12327,7 @@ A developer is reviewing a specific code change and is looking at a review guide
|
|
|
11951
12327
|
|
|
11952
12328
|
Answer only in relation to this change and the repository it lives in. You have the working tree, so open the files, trace callers and read history wherever that settles the question. Do not speculate about code you could have read.
|
|
11953
12329
|
|
|
11954
|
-
If a question cannot be answered from this change and
|
|
12330
|
+
If a question cannot be answered from this change, this repository and the pull request context below, say so plainly and say what information is missing. Do not suggest shell commands, API calls or other ways to retrieve forge data yourself. Do not pad the answer out with general advice about the topic; a developer mid-review wants the specific answer or an honest "that isn't determinable from here".
|
|
11955
12331
|
|
|
11956
12332
|
Write in short paragraphs and keep it to what was asked. Markdown renders, so use it where it carries meaning: backticks for identifiers and paths, a fenced block for a snippet worth reading as code rather than described in a sentence, a bullet list where the answer genuinely is a list. Prose is still the default. No headings, and do not break a paragraph into bullets to look organised.`;
|
|
11957
12333
|
var AskUnavailableError = class extends Error {};
|
|
@@ -12058,6 +12434,8 @@ function buildPrompt$2(workspace, question, sectionId, cwd, context = {}) {
|
|
|
12058
12434
|
"---",
|
|
12059
12435
|
"DIFF:",
|
|
12060
12436
|
workspace.diff,
|
|
12437
|
+
"---",
|
|
12438
|
+
chatConversationSection(context.conversation ?? storedConversation(workspace)),
|
|
12061
12439
|
...reading ? ["---", reading] : [],
|
|
12062
12440
|
...recent.length > 0 ? [
|
|
12063
12441
|
"---",
|
|
@@ -13761,6 +14139,8 @@ async function handleAddReview(req, res, pathname, options) {
|
|
|
13761
14139
|
if (route === "pull-requests") return startPullRequestBatch(body, deps);
|
|
13762
14140
|
if (route === "folder/inspect") return inspectFolder(body?.path, deps, body?.base);
|
|
13763
14141
|
if (route === "branch") return startBranchReview(body, deps);
|
|
14142
|
+
const retry = /^batches\/([0-9a-f-]{36})\/retry$/.exec(route);
|
|
14143
|
+
if (retry) return retryBatchRow(retry[1], body?.key, body?.fresh);
|
|
13764
14144
|
return null;
|
|
13765
14145
|
};
|
|
13766
14146
|
const result = await answer();
|
|
@@ -14320,25 +14700,15 @@ async function kissFreshnessOrNull(workspace) {
|
|
|
14320
14700
|
*/
|
|
14321
14701
|
async function handleRefreshPr(res, workspace) {
|
|
14322
14702
|
const reattributing = JSON.stringify(workspace.session.result);
|
|
14703
|
+
adoptLegacyBaseline(workspace);
|
|
14704
|
+
const startedAt = Date.now();
|
|
14323
14705
|
let outcome;
|
|
14324
14706
|
try {
|
|
14325
14707
|
outcome = await refreshPrContext(workspace);
|
|
14326
14708
|
} catch {
|
|
14327
|
-
outcome =
|
|
14328
|
-
at: Date.now(),
|
|
14329
|
-
ok: false,
|
|
14330
|
-
message: "The pull request could not be re-read. Nothing here has changed."
|
|
14331
|
-
};
|
|
14709
|
+
outcome = unforeseenFailure(startedAt);
|
|
14332
14710
|
}
|
|
14333
|
-
workspace
|
|
14334
|
-
mutateWorkspace(workspace.id, (current) => {
|
|
14335
|
-
current.prContext = workspace.prContext;
|
|
14336
|
-
current.lastPrRefresh = workspace.lastPrRefresh;
|
|
14337
|
-
if (JSON.stringify(current.session.result) === reattributing) current.session = {
|
|
14338
|
-
...current.session,
|
|
14339
|
-
result: workspace.session.result
|
|
14340
|
-
};
|
|
14341
|
-
});
|
|
14711
|
+
saveRefresh(workspace, outcome, reattributing);
|
|
14342
14712
|
record({
|
|
14343
14713
|
event: "pr_conversation_rechecked",
|
|
14344
14714
|
reviewId: workspace.id,
|
|
@@ -14557,10 +14927,16 @@ async function handleAsk(req, res, workspace) {
|
|
|
14557
14927
|
try {
|
|
14558
14928
|
let answer;
|
|
14559
14929
|
try {
|
|
14930
|
+
const reattributing = JSON.stringify(workspace.session.result);
|
|
14931
|
+
const { prepared, refresh } = await prepareConversationWithin(workspace, ASK_REREAD_CEILING_MS);
|
|
14932
|
+
if (refresh) try {
|
|
14933
|
+
saveRefresh(workspace, refresh, reattributing);
|
|
14934
|
+
} catch {}
|
|
14560
14935
|
answer = await askAboutChange(workspace, question, sectionId, run.signal, {
|
|
14561
14936
|
...explains ? { explaining: explains } : {},
|
|
14562
14937
|
...continuing ? { continuing } : {},
|
|
14563
|
-
...reassessed ? { reassessing: reassessed } : {}
|
|
14938
|
+
...reassessed ? { reassessing: reassessed } : {},
|
|
14939
|
+
conversation: prepared
|
|
14564
14940
|
});
|
|
14565
14941
|
} catch (error) {
|
|
14566
14942
|
failed();
|
|
@@ -15158,7 +15534,7 @@ function markAbandoned(job, now) {
|
|
|
15158
15534
|
if (isClaimed(job.id, now)) return false;
|
|
15159
15535
|
const current = loadJob(job.id);
|
|
15160
15536
|
if (!current || current.updatedAt !== job.updatedAt) return false;
|
|
15161
|
-
const abandoned = fail(current, ABANDONED_MESSAGE);
|
|
15537
|
+
const abandoned = fail$1(current, ABANDONED_MESSAGE);
|
|
15162
15538
|
saveJob(abandoned);
|
|
15163
15539
|
announceJob(abandoned);
|
|
15164
15540
|
return true;
|