prreviewbuddy 0.90.0 → 0.92.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $a as versionFrom, $i as explanationOf, $n as authorPreview, $r as kissDecisionsOf, $t as recordRuntimeBestEffort, A as settleQueue, Aa as isCommitReview, Ai as createWithMembers, An as moveIds, Ar as attemptIdOf, At as UpdateAlreadyRunningError, B as humanizeModelId, Ba as clearAgentPreference, Bi as queueStamp, Bn as existingPaths, Br as phasesFor, C as parsePullRequestLink, Ca as describeAuthorship, Ci as lifecyclePillAction, Cn as createPrState, Cr as followedBranch, Ct as RefreshUnavailableError, D as activateQueued, Da as originRemote, Di as sectionOf, Dn as markAllRead, Dr as fillFileUrlTemplate, E as relativeTime$1, Ea as resolveTarget, Ei as parseLifecycle, En as listNotifications, Er as forgeResolver, Et as mergeUpdateInto, Fa as git, Fi as setMembership, Fn as retireNotification, Fr as pruneJobResidue, Ft as prepareReview, G as reviewTag, Ga as AGENT_IDS, Gi as issuesOutstanding, Gn as storedConversation, Gr as isClaimed, Gt as recordTelemetryUploadConsent, H as findingRef, Ha as writeAgentPreference, Hi as doneVerb, Hn as adoptLegacyBaseline, Hr as purposeOf, Ht as record, Ia as refExists, Ii as updateCollections, In as toneOf, Ir as saveJob, It as settleReview, Ja as agentIdentityOf, Ji as reviewerDispositions, Jn as isUnchanged, Jr as followedRefName, Jt as BUILD_VERSION, K as reviewTagsIn, Ka as agentById, Ki as questionsOutstanding, Kn as unforeseenFailure, Kt as telemetryUploadConsent, L as modelProvenanceOf, La as resolveAgent, Li as queueAdd, Ln as unreadAround, Lt as reanalyseReview, Ma as AmbiguousRefError, Mi as nameProblemText, Mn as readNotification, Mr as fail$1, Mt as runningJobs, N as cliEntry, Na as shortNameOf, Ni as readCollections, Nn as readStateOf, Nr as isTerminal, Nt as failureOf$1, O as compactQueue, Oa as DetachedHeadError, Oi as collectionsStamp, On as markRead, Or as RUN_RECORD_RETENTION_MS, Ot as updateReview, Pa as shorthandOf, Pi as renameCollection, Pn as removeNotification, Pr as loadJob, Pt as observeReview, Q as markAskRunning, Qa as runnableAgents, Qi as explanationActionOf, Qn as UNESTABLISHED_MOTIVATION, Qr as kissDecisionRefusal, Qt as mapRuntimes, R as provenanceDetailLines, Ra as resolveModel, Ri as queueAddMany, Rn as unreadCount$1, Rr as isLiveJob, Rt as runJob, S as extractPullRequestLinks, Sa as objectWithAnyOf, Si as lifecycleName$1, Sn as createArchitectureNotification, Sr as checkReview, St as FETCH_TIMEOUT_MS, T as runBatch, Ta as kissOutcome, Ti as offersLifecycle, Tn as eventName, Tr as fetchHead, Tt as cancellableHere, U as findingRefs, Ui as isIssueOutstanding, Un as prepareConversationWithin, V as findingAtRef, Va as readAgentPreference, Vi as readQueue, Vn as ASK_REREAD_CEILING_MS, Vr as progressSteps, Vt as readEvents, W as refOf, Wa as STORE_ROOT, Wi as isQuestionOutstanding, Wn as saveRefresh, X as isRequestId, Xa as isAgentId, Xi as processDiscussion, Xn as EXPLAIN_SIMPLY_PROMPT, Xr as indexKeptIds, Xt as feedbackUrl, Ya as detectAgents, Yi as verdictClosure, Yn as agentEnvOf, Yr as groupByLineage, Za as parkedReason, Zi as recommendationsOf, Zn as chatConversationSection, Zr as indexReviewGroups, Zt as waitingHistory, _ as endRun, _a as statusLookup, _i as activityAttributes, _n as writeJsonAtomically, _r as requestJobStop, _t as parseWorkspacePort, a as lineageIds, aa as assessmentLookup, ai as mutateWorkspace, an as loadArchitecture, ar as escapeHtml$1, b as stopRun, ba as asString, bi as lifecycleChip, bn as subjectOf, br as findExistingReview, bt as writePortPreference, c as discardJob, ca as findingAssessment, ci as previousKissResult, cn as repositoryKey, cr as ReviewBeingDeletedError, ct as serverEntryExists, d as startKissJob, da as gradeScale, dn as trackedFiles, dr as MANAGED_ROOT, ea as isExplanationAction, ei as latestKissRun, en as RUNTIME_WORDS, er as hasAuthorContent, f as isDriving, fa as isGradeFor, fi as summarise, fn as ownersOf, fr as createWorktree, g as completeRun, ga as sourcePhrase, gi as LIFECYCLES, gn as isArchitectureId, gr as removeWorktree, gt as MIN_WORKSPACE_PORT, h as beginRun, ha as reassessIn, hi as workspaceStamps, hn as ancestorsOf, hr as readMarker, ht as MAX_WORKSPACE_PORT, i as deleteReview, ia as analysisRefOf, in as checkArchitecture, ir as CODE_COPY_CLASS, ja as resolveBase, ji as deleteCollection, jn as pruneNotifications, jr as endedAt, jt as liveUpdateFor, ki as createCollection, kn as markReadForReview, kr as allJobs, kt as askCheckout, l as kissAgentId, la as findingIn, li as recentWorkspaces, ln as storeArchitecture, lr as isBusy, lt as spawnServer, m as RUN_KINDS, ma as promptGrade, mi as workspaceRevision, mn as REPOSITORY_SCOPE, mr as isWorktreeHeld, mt as DEFAULT_WORKSPACE_PORT, n as MCP_TOOL_NAMES, na as ANALYSIS_ONLY, ni as lineagePosition, nn as formatDuration, no as AgentUnavailableError, nr as renderMarkdown, o as liveJobsFor, oa as closesFinding, on as partFiles, or as renderInlineText, ot as readServerRecord, p as track, pa as isOffered, pi as touchWorkspace, pn as QUOTE_LIMITS, pr as holdWorktree, pt as writeServerRecord, q as endAskRequest, qa as agentFor$1, qi as resolvedByConversation, qn as refreshPrContext, qr as decideKissRecommendation, r as deleteLineage, ra as NO_STATUS, ri as loadWorkspace, rn as ARCHITECTURE_DIR, rr as CODE_BLOCK_CLASS, rt as clearServerRecordOwnedBy, s as wasBlocked, sa as currentGradeIn, si as presentedKissRunOf, sn as readArchitecture, ta as withExplanation, ti as lineageKey, tn as describeRuntimes, to as AgentCancelledError, tr as GITHUB_MARKDOWN, tt as readIndexToken, ua as findingStatus, ui as reviewedCommit, un as storedScopes, ur as withReviewLock, ut as stopChild, v as heldRuns, va as wasReassessed, vi as activityLabel, vn as announceJob, vt as resolveWorkspacePort, w as unsupportedReason, wa as WITHHELD_REASON, wi as lifecyclePillLabel, wn as createPrUpdate, wr as reviewedShaOf, wt as cancelUpdate, x as batchIdentity, xa as extractJsonText, xi as lifecycleItemLabel, xn as categoryOf, xr as checkCodeFreshness, y as reportRun, ya as followUpFindingId, yi as isLifecycle, yn as branchOf, yt as statedWorkspacePort, z as agentDisplayName, za as agentIdOf, zi as queueRemove, zn as withUsageRecorded, zr as isTerminalJobPhase } from "./mcp_tools-CSj5vcSE.js";
1
+ import { $a as runnableAgents, $i as explanationOf, $n as authorPreview, $r as kissDecisionsOf, $t as recordRuntimeBestEffort, A as settleQueue, Ai as createWithMembers, An as moveIds, Ar as attemptIdOf, At as UpdateAlreadyRunningError, B as humanizeModelId, Ba as agentIdOf, Bi as queueStamp, Bn as existingPaths, Br as phasesFor, C as parsePullRequestLink, Ca as objectWithAnyOf, Ci as lifecyclePillAction, Cn as createPrState, Cr as followedBranch, Ct as RefreshUnavailableError, D as activateQueued, Da as resolveTarget, Di as sectionOf$1, Dn as markAllRead, Dr as fillFileUrlTemplate, E as relativeTime$1, Ea as kissOutcome, Ei as parseLifecycle, En as listNotifications, Er as forgeResolver, Et as mergeUpdateInto, Fa as shorthandOf, Fi as setMembership, Fn as retireNotification, Fr as pruneJobResidue, Ft as prepareReview, G as reviewTag, Ga as STORE_ROOT, Gi as issuesOutstanding, Gn as storedConversation, Gr as isClaimed, Gt as recordTelemetryUploadConsent, H as findingRef, Ha as readAgentPreference, Hi as doneVerb, Hn as adoptLegacyBaseline, Hr as purposeOf, Ht as record, Ia as git, Ii as updateCollections, In as toneOf, Ir as saveJob, It as settleReview, Ja as agentFor$1, Ji as reviewerDispositions, Jn as isUnchanged, Jr as followedRefName, Jt as BUILD_VERSION, K as reviewTagsIn, Ka as AGENT_IDS, Ki as questionsOutstanding, Kn as unforeseenFailure, Kt as telemetryUploadConsent, L as modelProvenanceOf, La as refExists, Li as queueAdd, Ln as unreadAround, Lt as reanalyseReview, Ma as resolveBase, Mi as nameProblemText, Mn as readNotification, Mr as fail$1, Mt as runningJobs, N as cliEntry, Na as AmbiguousRefError, Ni as readCollections, Nn as readStateOf, Nr as isTerminal, Nt as failureOf$1, O as compactQueue, Oa as originRemote, Oi as collectionsStamp, On as markRead, Or as RUN_RECORD_RETENTION_MS, Ot as updateReview, Pa as shortNameOf, Pi as renameCollection, Pn as removeNotification, Pr as loadJob, Pt as observeReview, Q as markAskRunning, Qa as parkedReason, Qi as explanationActionOf, Qn as UNESTABLISHED_MOTIVATION, Qr as kissDecisionRefusal, Qt as mapRuntimes, R as provenanceDetailLines, Ra as resolveAgent, Ri as queueAddMany, Rn as unreadCount$1, Rr as isLiveJob, Rt as runJob, S as extractPullRequestLinks, Sa as extractJsonText, Si as lifecycleName$1, Sn as createArchitectureNotification, Sr as checkReview, St as FETCH_TIMEOUT_MS, T as runBatch, Ta as WITHHELD_REASON, Ti as offersLifecycle, Tn as eventName, Tr as fetchHead, Tt as cancellableHere, U as findingRefs, Ua as writeAgentPreference, Ui as isIssueOutstanding, Un as prepareConversationWithin, V as findingAtRef, Va as clearAgentPreference, Vi as readQueue, Vn as ASK_REREAD_CEILING_MS, Vr as progressSteps, Vt as readEvents, W as refOf, Wi as isQuestionOutstanding, Wn as saveRefresh, X as isRequestId, Xa as detectAgents, Xi as processDiscussion, Xn as EXPLAIN_SIMPLY_PROMPT, Xr as indexKeptIds, Xt as feedbackUrl, Ya as agentIdentityOf, Yi as verdictClosure, Yn as agentEnvOf, Yr as groupByLineage, Za as isAgentId, Zi as recommendationsOf, Zn as chatConversationSection, Zr as indexReviewGroups, Zt as waitingHistory, _ as endRun, _a as statusLookup, _i as activityAttributes, _n as writeJsonAtomically, _r as requestJobStop, _t as parseWorkspacePort, a as lineageIds, aa as assessmentLookup, ai as mutateWorkspace, an as loadArchitecture, ar as escapeHtml$1, b as stopRun, ba as SEVERITIES, bi as lifecycleChip, bn as subjectOf, br as findExistingReview, bt as writePortPreference, c as discardJob, ca as findingAssessment, ci as previousKissResult, cn as repositoryKey, cr as ReviewBeingDeletedError, ct as serverEntryExists, d as startKissJob, da as gradeScale, dn as trackedFiles, dr as MANAGED_ROOT, ea as isExplanationAction, ei as latestKissRun, en as RUNTIME_WORDS, eo as versionFrom, er as hasAuthorContent, f as isDriving, fa as isGradeFor, fi as summarise, fn as ownersOf, fr as createWorktree, g as completeRun, ga as sourcePhrase, gi as LIFECYCLES, gn as isArchitectureId, gr as removeWorktree, gt as MIN_WORKSPACE_PORT, h as beginRun, ha as reassessIn, hi as workspaceStamps, hn as ancestorsOf, hr as readMarker, ht as MAX_WORKSPACE_PORT, i as deleteReview, ia as analysisRefOf, in as checkArchitecture, ir as CODE_COPY_CLASS, ja as isCommitReview, ji as deleteCollection, jn as pruneNotifications, jr as endedAt, jt as liveUpdateFor, ka as DetachedHeadError, ki as createCollection, kn as markReadForReview, kr as allJobs, kt as askCheckout, l as kissAgentId, la as findingIn, li as recentWorkspaces, ln as storeArchitecture, lr as isBusy, lt as spawnServer, m as RUN_KINDS, ma as promptGrade, mi as workspaceRevision, mn as REPOSITORY_SCOPE, mr as isWorktreeHeld, mt as DEFAULT_WORKSPACE_PORT, n as MCP_TOOL_NAMES, na as ANALYSIS_ONLY, ni as lineagePosition, nn as formatDuration, no as AgentCancelledError, nr as renderMarkdown, o as liveJobsFor, oa as closesFinding, on as partFiles, or as renderInlineText, ot as readServerRecord, p as track, pa as isOffered, pi as touchWorkspace, pn as QUOTE_LIMITS, pr as holdWorktree, pt as writeServerRecord, q as endAskRequest, qa as agentById, qi as resolvedByConversation, qn as refreshPrContext, qr as decideKissRecommendation, r as deleteLineage, ra as NO_STATUS, ri as loadWorkspace, rn as ARCHITECTURE_DIR, ro as AgentUnavailableError, rr as CODE_BLOCK_CLASS, rt as clearServerRecordOwnedBy, s as wasBlocked, sa as currentGradeIn, si as presentedKissRunOf, sn as readArchitecture, ta as withExplanation, ti as lineageKey, tn as describeRuntimes, tr as GITHUB_MARKDOWN, tt as readIndexToken, ua as findingStatus, ui as reviewedCommit, un as storedScopes, ur as withReviewLock, ut as stopChild, v as heldRuns, va as wasReassessed, vi as activityLabel, vn as announceJob, vt as resolveWorkspacePort, w as unsupportedReason, wa as describeAuthorship, wi as lifecyclePillLabel, wn as createPrUpdate, wr as reviewedShaOf, wt as cancelUpdate, x as batchIdentity, xa as asString, xi as lifecycleItemLabel, xn as categoryOf, xr as checkCodeFreshness, y as reportRun, ya as followUpFindingId, yi as isLifecycle, yn as branchOf, yt as statedWorkspacePort, z as agentDisplayName, za as resolveModel, zi as queueRemove, zn as withUsageRecorded, zr as isTerminalJobPhase } from "./mcp_tools-BKPpd9dN.js";
2
2
  import { basename, delimiter, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { execFile, spawn } from "node:child_process";
4
4
  import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
@@ -182,6 +182,7 @@ function composeReview(session) {
182
182
  schemaVersion: 1,
183
183
  summary: primary.summary,
184
184
  ...primary.motivation ? { motivation: primary.motivation } : {},
185
+ ...primary.orientation ? { orientation: primary.orientation } : {},
185
186
  recommendation: primary.recommendation,
186
187
  nextSteps: primary.nextSteps,
187
188
  overallRisk,
@@ -569,7 +570,7 @@ function isCollectionId(value) {
569
570
  * filter gives, and only its star says it is pinned.
570
571
  */
571
572
  function indexSectionOf(pinned, lifecycle, activity, filter) {
572
- return pinned && !ordersExplicitly(filter) ? "pinned" : sectionOf(lifecycle, activity);
573
+ return pinned && !ordersExplicitly(filter) ? "pinned" : sectionOf$1(lifecycle, activity);
573
574
  }
574
575
  /**
575
576
  * Where a review's own card is drawn, or absent when it is drawn only inside its collections.
@@ -595,7 +596,7 @@ function homeSection(pinned, lifecycle, activity, collected, filter) {
595
596
  */
596
597
  function collectionPlacement(pinned, lifecycle, activity, filter) {
597
598
  if (pinned || ordersExplicitly(filter)) return void 0;
598
- return sectionOf(lifecycle, activity) === "done" ? "completed" : "active";
599
+ return sectionOf$1(lifecycle, activity) === "done" ? "completed" : "active";
599
600
  }
600
601
  /** A collection heading's count: what still needs attention, and the members drawn in Pinned. */
601
602
  function collectionCountLabel(active, pinned) {
@@ -902,7 +903,7 @@ function svg(paths) {
902
903
  * Buddy is asked about can never disagree. That is also why no severity, theme or status appears:
903
904
  * those sort the findings for the reviewer and are not part of what the author is told.
904
905
  */
905
- function count(n, noun) {
906
+ function count$1(n, noun) {
906
907
  return `${n} ${noun}${n === 1 ? "" : "s"}`;
907
908
  }
908
909
  /**
@@ -919,8 +920,8 @@ function feedbackSelectionParts(issues, questions) {
919
920
  const total = issues + questions;
920
921
  return {
921
922
  total: total === 0 ? "Nothing chosen" : `${total} chosen`,
922
- issues: issues ? count(issues, "issue") : "",
923
- questions: questions ? count(questions, "question") : ""
923
+ issues: issues ? count$1(issues, "issue") : "",
924
+ questions: questions ? count$1(questions, "question") : ""
924
925
  };
925
926
  }
926
927
  /** "5 chosen · 2 issues · 3 questions", "1 chosen · 1 question", "Nothing chosen". */
@@ -1522,6 +1523,101 @@ function splitFilePath(path) {
1522
1523
  };
1523
1524
  }
1524
1525
  //#endregion
1526
+ //#region ../../packages/review-harness/src/domain/ui/components/impact_groups.ts
1527
+ /** The pane's folds: each section, and the closed drawer under them. */
1528
+ var ISSUE_FOLDS = [
1529
+ "failure",
1530
+ "risk",
1531
+ "intent",
1532
+ "maintenance",
1533
+ "other",
1534
+ "closed"
1535
+ ];
1536
+ /** Display order: what is wrong, what may be, what only the reviewer can judge, what costs later. */
1537
+ var ISSUE_SECTIONS = [
1538
+ "failure",
1539
+ "risk",
1540
+ "intent",
1541
+ "maintenance",
1542
+ "other"
1543
+ ];
1544
+ var ISSUE_SECTION_TITLES = {
1545
+ failure: "Failures",
1546
+ risk: "Possible risks",
1547
+ intent: "Intent to confirm",
1548
+ maintenance: "Maintenance",
1549
+ other: "Other issues"
1550
+ };
1551
+ function sectionOf(issue) {
1552
+ return issue.consequence?.group ?? "other";
1553
+ }
1554
+ /**
1555
+ * Issues by section, each section by severity as the reviewer now holds it, ties in the order the
1556
+ * analysis gave them. Severity orders within a section and never moves an issue between sections.
1557
+ */
1558
+ function groupIssues(issues, gradeOf) {
1559
+ const rank = (issue) => {
1560
+ const index = SEVERITIES.indexOf(gradeOf(issue) ?? "");
1561
+ return index < 0 ? SEVERITIES.length : index;
1562
+ };
1563
+ return ISSUE_SECTIONS.map((section) => ({
1564
+ section,
1565
+ issues: issues.map((issue, index) => ({
1566
+ issue,
1567
+ index
1568
+ })).filter(({ issue }) => sectionOf(issue) === section).sort((a, b) => rank(a.issue) - rank(b.issue) || a.index - b.index).map(({ issue }) => issue)
1569
+ })).filter((group) => group.issues.length > 0);
1570
+ }
1571
+ function tallyEntries(entries) {
1572
+ const tally = {
1573
+ bySection: {},
1574
+ verify: 0,
1575
+ decide: 0,
1576
+ fix: 0,
1577
+ step: 0
1578
+ };
1579
+ for (const entry of entries) {
1580
+ tally.bySection[entry.section] = (tally.bySection[entry.section] ?? 0) + 1;
1581
+ if (entry.next) tally[entry.next] += 1;
1582
+ }
1583
+ return tally;
1584
+ }
1585
+ function tallyIssues(issues) {
1586
+ return tallyEntries(issues.map((issue) => {
1587
+ const step = issue.consequence?.next;
1588
+ return {
1589
+ section: sectionOf(issue),
1590
+ ...step ? { next: step.kind ?? "step" } : {}
1591
+ };
1592
+ }));
1593
+ }
1594
+ var count = (n, one, many) => `${n} ${n === 1 ? one : many}`;
1595
+ /**
1596
+ * One factual line over the sections: how many issues are outstanding, and how many of them name
1597
+ * a step, by the kind the analysis gave it. The per-section counts are the nav's, so they are not
1598
+ * repeated here. Always "issues": Questions keep their own tab and their own count.
1599
+ */
1600
+ function issuesAttentionLine(tally) {
1601
+ const total = Object.values(tally.bySection).reduce((sum, n) => sum + (n ?? 0), 0);
1602
+ if (total === 0) return "No issues are outstanding from this analysis.";
1603
+ const steps = [
1604
+ tally.verify ? count(tally.verify, "verification step", "verification steps") : "",
1605
+ tally.decide ? count(tally.decide, "decision", "decisions") : "",
1606
+ tally.fix ? count(tally.fix, "fix", "fixes") : "",
1607
+ tally.step ? count(tally.step, "next step", "next steps") : ""
1608
+ ].filter(Boolean);
1609
+ return [count(total, "outstanding issue", "outstanding issues"), ...steps].join(" · ");
1610
+ }
1611
+ /**
1612
+ * Buttons to each section with its count. Buttons, not `#` links: the hash routes the review page.
1613
+ * Drawn only when there are two sections to choose between; a single one is already in view.
1614
+ */
1615
+ function renderImpactNavItems(tally) {
1616
+ const present = ISSUE_SECTIONS.filter((section) => (tally.bySection[section] ?? 0) > 0);
1617
+ if (present.length < 2) return "";
1618
+ return present.map((section) => `<button type="button" class="pr-helper-issue-nav-item" data-issue-nav="${section}">${escapeHtml$1(ISSUE_SECTION_TITLES[section])}<span class="pr-helper-issue-nav-count">${tally.bySection[section]}</span></button>`).join("");
1619
+ }
1620
+ //#endregion
1525
1621
  //#region ../../packages/review-harness/src/domain/ai_review/review_outcome.ts
1526
1622
  /**
1527
1623
  * The one reading of a session, for every surface.
@@ -3292,16 +3388,6 @@ function renderInvestigationNote(note) {
3292
3388
  return `<p class="pr-helper-investigation-note">${renderInlineText(note)}</p>`;
3293
3389
  }
3294
3390
  /**
3295
- * The bar above the two groups, worded the way the Questions tab words its own.
3296
- *
3297
- * Only drawn once a reconciliation has closed something. On a first analysis every finding is
3298
- * open, "15 of 15 still open" is a sentence with no information in it, and a review that has
3299
- * never been updated should not grow a progress bar to tell the reviewer they have made none.
3300
- */
3301
- function findingsProgressBody(total, open, verb = "resolved") {
3302
- return open === 0 ? `<span class="pr-helper-questions-progress-check" aria-hidden="true">\u2713</span><span class="pr-helper-questions-progress-count">All ${total} issues ${verb}</span>` : `<span class="pr-helper-questions-progress-count">${open} of ${total} still open</span>`;
3303
- }
3304
- /**
3305
3391
  * Work that is done, kept and closed.
3306
3392
  *
3307
3393
  * Collapsed rather than dropped: the reviewer asked whether these were fixed, and "they are gone
@@ -3309,8 +3395,8 @@ function findingsProgressBody(total, open, verb = "resolved") {
3309
3395
  * ten resolved findings above five open ones is the layout that made the count feel wrong in the
3310
3396
  * first place.
3311
3397
  */
3312
- function renderClosedFindings(summary, cards, kind, nothingOpen, waiting) {
3313
- return `<details class="pr-helper-closed-findings" data-closed-findings="${kind}"${nothingOpen ? " data-nothing-open" : ""}>
3398
+ function renderClosedFindings(summary, cards, kind, nothingOpen, waiting, open = false) {
3399
+ return `<details class="pr-helper-closed-findings" data-closed-findings="${kind}"${nothingOpen ? " data-nothing-open" : ""}${open ? " open" : ""}>
3314
3400
  <summary class="pr-helper-closed-findings-summary" data-closed-findings-summary>${escapeHtml$1(summary)}${renderWaitingSuggestions(waiting)}</summary>
3315
3401
  <div class="pr-helper-closed-findings-list">${cards}</div>
3316
3402
  </details>`;
@@ -3346,13 +3432,45 @@ function renderIssuesTab(session, investigatingId, options) {
3346
3432
  const isDismissed = dispositions.isDismissed(issue);
3347
3433
  const stale = dispositions.isStale(issue);
3348
3434
  const assessment = assess("issue", issue);
3435
+ const impact = issue.consequence;
3436
+ const fixed = issue.reconciliationStatus === "resolved" ? issue.resolvedBehaviour?.trim() : void 0;
3437
+ const framed = Boolean(impact || fixed);
3438
+ const headline = fixed ?? impact?.title ?? issue.title;
3439
+ const explanation = renderCardExplanation(explanationOf(session, "issue", issue.id), options);
3440
+ const technical = `<p class="pr-helper-issue-desc">${renderInlineText(issue.description)}</p>${framed ? "" : explanation}
3441
+ ${framed ? "" : renderReconciliationNote(issue.reconciliationNote)}
3442
+ ${renderStatusHistory(issue.statusHistory, now)}
3443
+ ${renderInvestigationNote(issue.investigationNote)}
3444
+ ${renderExistingDiscussion(issue.existingDiscussion)}
3445
+ ${renderFileLocation(issue.file, "pr-helper-issue-loc", issue.line)}
3446
+ ${renderRelatedFiles(issue.relatedFiles)}
3447
+ ${renderInvestigationProvenance("issue", issue.id, records, now)}`;
3448
+ const actions = `<div class="pr-helper-issue-actions">
3449
+ ${renderInvestigationActions("issue", issue.id, records, investigatingId)}
3450
+ ${renderDismissToggle(issue, isDismissed, stale, dismissals.get(issue.id), dispositions.isClosedByVerdict(issue))}
3451
+ </div>`;
3452
+ const body = framed ? `${fixed ? "" : renderConsequence(impact)}${explanation}
3453
+ ${renderReconciliationNote(issue.reconciliationNote)}
3454
+ <div class="pr-helper-card-footer">
3455
+ <details class="pr-helper-issue-technical">
3456
+ <summary class="pr-helper-issue-technical-summary">Technical details</summary>
3457
+ <div class="pr-helper-issue-technical-body">
3458
+ ${fixed && impact ? renderOriginalConsequence(impact) : ""}
3459
+ <p class="pr-helper-issue-technical-title">${renderInlineText(issue.title)}</p>
3460
+ ${technical}
3461
+ </div>
3462
+ </details>
3463
+ ${actions}
3464
+ </div>` : `${technical}
3465
+ ${actions}`;
3466
+ const next = impact?.next ? ` data-issue-next="${impact.next.kind ?? "step"}"` : "";
3349
3467
  return `
3350
- <div class="pr-helper-issue-card ${cardGradeClass("issue", assessment.current)}${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)}">
3468
+ <div class="pr-helper-issue-card ${cardGradeClass("issue", assessment.current)}${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)}" data-issue-section="${sectionOf(issue)}"${next}>
3351
3469
  <div class="pr-helper-issue-header">
3352
3470
  <div class="pr-helper-issue-title-row">
3353
3471
  ${options?.feedbackSelect ? renderFeedbackPick("issue", issue.title, issueFeedbackMarkdown(issue), ref) : ""}
3354
3472
  ${renderFindingRef(ref)}
3355
- <p class="pr-helper-issue-title">${renderInlineText(issue.title)}</p>
3473
+ <p class="pr-helper-issue-title">${renderInlineText(headline)}</p>
3356
3474
  ${renderFindingCopySource(issueMarkdown(issue, assessment), "issue")}
3357
3475
  ${options?.askBuddy ? renderAskBuddyMenu("issue") : ""}
3358
3476
  </div>
@@ -3367,27 +3485,62 @@ function renderIssuesTab(session, investigatingId, options) {
3367
3485
  </div>
3368
3486
  ${renderStaleDismissal(stale)}
3369
3487
  ${renderAssessmentRegion("issue", assessment, grading, statusOf("issue", issue.id))}
3370
- <p class="pr-helper-issue-desc">${renderInlineText(issue.description)}</p>${renderCardExplanation(explanationOf(session, "issue", issue.id), options)}
3371
- ${renderReconciliationNote(issue.reconciliationNote)}
3372
- ${renderStatusHistory(issue.statusHistory, now)}
3373
- ${renderInvestigationNote(issue.investigationNote)}
3374
- ${renderExistingDiscussion(issue.existingDiscussion)}
3375
- ${renderFileLocation(issue.file, "pr-helper-issue-loc", issue.line)}
3376
- ${renderRelatedFiles(issue.relatedFiles)}
3377
- ${renderInvestigationProvenance("issue", issue.id, records, now)}
3378
- <div class="pr-helper-issue-actions">
3379
- ${renderInvestigationActions("issue", issue.id, records, investigatingId)}
3380
- ${renderDismissToggle(issue, isDismissed, stale, dismissals.get(issue.id), dispositions.isClosedByVerdict(issue))}
3381
- </div>
3488
+ ${body}
3382
3489
  </div>
3383
3490
  `;
3384
3491
  };
3385
3492
  const counts = issuesOutstanding(result.issues, dispositions);
3386
- if (counts.done === 0) return result.issues.map(card).join("");
3387
- const open = result.issues.filter((issue) => isIssueOutstanding(issue, dispositions)).map(card).join("");
3493
+ const outstanding = result.issues.filter((issue) => isIssueOutstanding(issue, dispositions));
3388
3494
  const closedIssues = result.issues.filter((issue) => !isIssueOutstanding(issue, dispositions));
3389
- const closed = closedIssues.map(card).join("");
3390
- return `<div class="pr-helper-findings-progress" role="status">${findingsProgressBody(counts.total, counts.open, doneVerb(counts))}</div>${open}${renderClosedFindings(closedFindingsSummary(counts), closed, "issues", counts.open === 0, closedIssues.filter((issue) => assess("issue", issue).pending || statusOf("issue", issue.id).pending).length)}`;
3495
+ const folds = options?.issueFolds ?? {};
3496
+ const tally = tallyIssues(outstanding);
3497
+ const grouped = new Map(groupIssues(outstanding, (issue) => assess("issue", issue).current).map((group) => [group.section, group.issues]));
3498
+ const present = new Set(result.issues.map(sectionOf));
3499
+ const headless = present.size === 1 && present.has("other");
3500
+ const groups = ISSUE_SECTIONS.filter((section) => present.has(section)).map((section) => {
3501
+ const issues = grouped.get(section) ?? [];
3502
+ if (headless) return `<div class="pr-helper-issue-group" data-impact-group="${section}"${issues.length ? "" : " hidden"}>
3503
+ <div class="pr-helper-issue-group-list">${issues.map(card).join("")}</div>
3504
+ </div>`;
3505
+ return `<details class="pr-helper-issue-group" data-impact-group="${section}"${folds[section] === false ? "" : " open"}${issues.length ? "" : " hidden"}>
3506
+ <summary class="pr-helper-issue-group-summary"><h4 class="pr-helper-issue-group-title">${escapeHtml$1(ISSUE_SECTION_TITLES[section])}</h4><span class="pr-helper-issue-group-count" data-issue-group-count>${issues.length}</span></summary>
3507
+ <div class="pr-helper-issue-group-list">${issues.map(card).join("")}</div>
3508
+ </details>`;
3509
+ }).join("");
3510
+ const nav = renderImpactNavItems(tally);
3511
+ const orientation = result.orientation?.trim();
3512
+ const head = `<div class="pr-helper-issues-orientation">
3513
+ ${orientation ? `<p class="pr-helper-issues-orientation-text">${renderInlineText(orientation)}</p>` : ""}
3514
+ <p class="pr-helper-issues-attention" role="status" data-issues-attention>${escapeHtml$1(issuesAttentionLine(tally))}</p>
3515
+ <nav class="pr-helper-issue-nav" aria-label="Issues by kind" data-issue-nav-list${nav ? "" : " hidden"}>${nav}</nav>
3516
+ </div>`;
3517
+ if (closedIssues.length === 0) return `${head}${groups}`;
3518
+ return `${head}${groups}${renderClosedFindings(closedFindingsSummary(counts), closedIssues.map(card).join(""), "issues", counts.open === 0, closedIssues.filter((issue) => assess("issue", issue).pending || statusOf("issue", issue.id).pending).length, folds.closed === true)}`;
3519
+ }
3520
+ /** What the issue means, read first: the scenario and the step that would settle it. */
3521
+ function renderConsequence(impact) {
3522
+ if (!impact) return "";
3523
+ return `<div class="pr-helper-issue-impact">
3524
+ <p class="pr-helper-issue-scenario">${renderInlineText(impact.scenario)}</p>
3525
+ ${renderConsequenceStep(impact.next)}
3526
+ </div>`;
3527
+ }
3528
+ var STEP_LABELS = {
3529
+ verify: "To verify:",
3530
+ decide: "To decide:",
3531
+ fix: "To fix:"
3532
+ };
3533
+ function renderConsequenceStep(step) {
3534
+ if (!step) return "";
3535
+ return `<p class="pr-helper-issue-next"><span class="pr-helper-issue-next-label">${step.kind ? STEP_LABELS[step.kind] : "Next step:"}</span> ${renderInlineText(step.text)}</p>`;
3536
+ }
3537
+ /** A fixed issue's original consequence, kept with the technical account once the fix heads the card. */
3538
+ function renderOriginalConsequence(impact) {
3539
+ return `<div class="pr-helper-issue-original">
3540
+ <h5>Before the fix</h5>
3541
+ <p class="pr-helper-issue-original-title">${renderInlineText(impact.title)}</p>
3542
+ <p class="pr-helper-issue-scenario">${renderInlineText(impact.scenario)}</p>
3543
+ </div>`;
3391
3544
  }
3392
3545
  /**
3393
3546
  * What to call the closed pile, by what is actually in it.
@@ -3470,13 +3623,47 @@ function renderQuestionCard(q, sections, answered, records, now = Date.now(), in
3470
3623
  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\"" : ""}${verdictClosed ? " data-verdict-closed=\"true\"" : ""}>${answered ? "Mark as unanswered" : "Mark answered"}</button>`;
3471
3624
  const investigationAnswer = q.investigationAnswer?.trim() ? `<h5>Answer from investigation</h5><p class="pr-helper-issue-desc pr-helper-investigation-answer">${renderInlineText(q.investigationAnswer)}</p>` : "";
3472
3625
  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>` : "";
3626
+ const why = q.whyThisMatters ? `<h5>Why this matters</h5><p class="pr-helper-issue-desc">${renderInlineText(q.whyThisMatters)}</p>` : "";
3627
+ const files = q.file || q.relatedFiles?.length ? `<h5>Files</h5>${renderFileLocation(q.file, "pr-helper-issue-loc", void 0)}${renderRelatedFiles(q.relatedFiles)}` : "";
3628
+ const provenance = renderInvestigationProvenance("question", q.id, records, now);
3629
+ const framing = q.framing;
3630
+ const actions = `<div class="pr-helper-question-actions">
3631
+ ${jumpLink}
3632
+ <div class="pr-helper-question-actions-group">
3633
+ ${renderInvestigationActions("question", q.id, records, investigatingId)}
3634
+ ${answerToggle}
3635
+ </div>
3636
+ </div>`;
3637
+ const answers = `${reconciliationAnswer}
3638
+ ${renderReconciliationNote(q.reconciliationNote)}
3639
+ ${investigationAnswer}`;
3640
+ const body = framing ? `${renderQuestionFraming(framing)}${renderCardExplanation(explanation, options)}
3641
+ ${answers}
3642
+ <div class="pr-helper-card-footer">
3643
+ <details class="pr-helper-issue-technical">
3644
+ <summary class="pr-helper-issue-technical-summary">Technical context</summary>
3645
+ <div class="pr-helper-issue-technical-body">
3646
+ <p class="pr-helper-issue-technical-title">${renderInlineText(q.question)}</p>
3647
+ ${why}
3648
+ ${renderInvestigationNote(q.investigationNote)}
3649
+ ${files}
3650
+ ${provenance}
3651
+ </div>
3652
+ </details>
3653
+ ${actions}
3654
+ </div>` : `${why}${renderCardExplanation(explanation, options)}
3655
+ ${answers}
3656
+ ${renderInvestigationNote(q.investigationNote)}
3657
+ ${files}
3658
+ ${provenance}
3659
+ ${actions}`;
3473
3660
  return `
3474
3661
  <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))}"` : ""}>
3475
3662
  <div class="pr-helper-issue-header">
3476
3663
  <div class="pr-helper-issue-title-row">
3477
3664
  ${options?.feedbackSelect ? renderFeedbackPick("question", q.question, questionFeedbackMarkdown(q), ref) : ""}
3478
3665
  ${renderFindingRef(ref)}
3479
- <p class="pr-helper-issue-title">${renderInlineText(q.question)}</p>
3666
+ <p class="pr-helper-issue-title">${renderInlineText(framing?.question ?? q.question)}</p>
3480
3667
  ${renderFindingCopySource(questionMarkdown(q, assessment), "question")}
3481
3668
  ${options?.askBuddy ? renderAskBuddyMenu("question") : ""}
3482
3669
  </div>
@@ -3493,23 +3680,17 @@ function renderQuestionCard(q, sections, answered, records, now = Date.now(), in
3493
3680
  </div>
3494
3681
  </div>
3495
3682
  ${renderAssessmentRegion("question", assessment, grading, status)}
3496
- ${q.whyThisMatters ? `<h5>Why this matters</h5><p class="pr-helper-issue-desc">${renderInlineText(q.whyThisMatters)}</p>` : ""}${renderCardExplanation(explanation, options)}
3497
- ${reconciliationAnswer}
3498
- ${renderReconciliationNote(q.reconciliationNote)}
3499
- ${investigationAnswer}
3500
- ${renderInvestigationNote(q.investigationNote)}
3501
- ${q.file || q.relatedFiles?.length ? `<h5>Files</h5>${renderFileLocation(q.file, "pr-helper-issue-loc", void 0)}${renderRelatedFiles(q.relatedFiles)}` : ""}
3502
- ${renderInvestigationProvenance("question", q.id, records, now)}
3503
- <div class="pr-helper-question-actions">
3504
- ${jumpLink}
3505
- <div class="pr-helper-question-actions-group">
3506
- ${renderInvestigationActions("question", q.id, records, investigatingId)}
3507
- ${answerToggle}
3508
- </div>
3509
- </div>
3683
+ ${body}
3510
3684
  </div>
3511
3685
  `;
3512
3686
  }
3687
+ /** What the question means, read first: why the answer matters and what is needed. */
3688
+ function renderQuestionFraming(framing) {
3689
+ return `<div class="pr-helper-issue-impact">
3690
+ <p class="pr-helper-issue-scenario">${renderInlineText(framing.why)}</p>
3691
+ ${framing.needed ? `<p class="pr-helper-issue-next pr-helper-question-needed"><span class="pr-helper-issue-next-label">Needed:</span> ${renderInlineText(framing.needed)}</p>` : ""}
3692
+ </div>`;
3693
+ }
3513
3694
  /**
3514
3695
  * The counter's contents, split out so a caller that toggles one question can refresh it without
3515
3696
  * re-rendering the tab.
@@ -6783,7 +6964,8 @@ function renderStandardViews(view, sections) {
6783
6964
  askBuddy: true,
6784
6965
  feedbackSelect: true,
6785
6966
  reassess: true,
6786
- findingRefs: true
6967
+ findingRefs: true,
6968
+ ...view.reviewLayout?.issueGroups ? { issueFolds: view.reviewLayout.issueGroups } : {}
6787
6969
  })}
6788
6970
  </section>
6789
6971
  <section class="prb-view" data-view="questions" hidden>
@@ -6810,20 +6992,28 @@ function renderStandardViews(view, sections) {
6810
6992
  * When the "already raised" claims below were last true.
6811
6993
  *
6812
6994
  * Every attribution on this view carries a thread's state — "still unresolved", "since marked
6813
- * resolved" — and that state was frozen the moment the conversation was imported. Nothing
6814
- * re-checks it, and nothing here can: the daemon reaches no forge. So a card can go on telling
6815
- * the reviewer that Sergei's thread is open an hour after Sergei closed it.
6995
+ * resolved" — and that state was frozen the moment the conversation was imported. Nothing on this
6996
+ * view re-checks it; the PR context view does, on request. Until then a card can go on telling the
6997
+ * reviewer that Sergei's thread is open an hour after Sergei closed it.
6816
6998
  *
6817
6999
  * Every other kind of staleness in this product degrades into vagueness. This one degrades into a
6818
7000
  * confident, specific, wrong claim about what a named colleague currently thinks, which is worth
6819
- * one line above the list rather than an explanation the reviewer has to go and find.
7001
+ * one line above the list: its age always in view, the explanation one disclosure away, and the way
7002
+ * to the re-check beside it.
6820
7003
  *
6821
7004
  * Only where a conversation was attached: there are no such claims to qualify otherwise.
6822
7005
  */
6823
7006
  function renderAttributionAge(view) {
6824
7007
  if (!view.prContext) return "";
6825
7008
  const imported = relativeTime$1(view.prContext.prContext.discussion.fetchedAt);
6826
- return `<p class="prb-doc-note">Any &ldquo;already raised&rdquo; note below describes the pull request as it stood ${escapeHtml$1(imported)}, when its discussion was fetched. That includes whether each thread was still open. If someone has replied or resolved a thread since then, this page has no way of knowing. Use <button type="button" class="prb-doc-note-link" data-view-nav="prcontext">Re-check the conversation</button> on the PR context view to bring it up to date.</p>`;
7009
+ return `<div class="prb-discussion-age">
7010
+ <details class="prb-discussion-age-detail">
7011
+ <summary class="prb-discussion-age-summary">PR discussion checked ${escapeHtml$1(imported)}</summary>
7012
+ <p class="prb-discussion-age-body">Any &ldquo;already raised&rdquo; note below, and whether each thread was still open, describes the pull request as it stood when its discussion was fetched. A reply or a resolved thread since then is not reflected here until the conversation is re-checked.</p>
7013
+ </details>
7014
+ <span class="prb-discussion-age-sep" aria-hidden="true">&middot;</span>
7015
+ <button type="button" class="prb-doc-note-link" data-view-nav="prcontext">Re-check in PR context</button>
7016
+ </div>`;
6827
7017
  }
6828
7018
  /**
6829
7019
  * The link out to this file on the pull request.
@@ -10136,7 +10326,7 @@ function renderReviewsPage(view) {
10136
10326
  const collected = (memberOf.get(group.key) ?? []).length > 0;
10137
10327
  const home = homeSection(group.latest.pinned === true, group.latest.lifecycle, activityOf(group), collected, "all");
10138
10328
  if (home === void 0) collectedHome.add(group);
10139
- sections[home ?? sectionOf(group.latest.lifecycle, activityOf(group))].push(group);
10329
+ sections[home ?? sectionOf$1(group.latest.lifecycle, activityOf(group))].push(group);
10140
10330
  }
10141
10331
  const storedReviews = view.groups.reduce((total, group) => total + group.history.length, 0);
10142
10332
  const renderSectionAt = (section) => renderSection(section, sections[section], (groups) => renderCards(groups, "home", (group) => collectedHome.has(group)), sectionOpen(section, view.sectionsCollapsed), sections[section].filter((group) => !inProgressOnly.has(group.latest.id) && !collectedHome.has(group)).length);
@@ -11000,10 +11190,11 @@ function isRunsSection(value) {
11000
11190
  /**
11001
11191
  * How the reviewer last laid out a review page, kept so the next review opens the same way.
11002
11192
  *
11003
- * Three things, each the layout as it last stood rather than a log of what was pressed: whether
11193
+ * Four things, each the layout as it last stood rather than a log of what was pressed: whether
11004
11194
  * Buddy AI was open or closed, whether a surface was expanded (Buddy AI itself, or the document,
11005
- * whichever pane that is on the next review), and which parts of the rail were folded. A field
11006
- * nobody has changed has no key, so the page's own defaults still decide for a new reviewer.
11195
+ * whichever pane that is on the next review), which parts of the rail were folded, and which
11196
+ * sections of the Issues pane were. A field nobody has changed has no key, so the page's own
11197
+ * defaults still decide for a new reviewer.
11007
11198
  *
11008
11199
  * Pure, so the page and the server read it with the same rules. Stored in `ui.json`; see
11009
11200
  * `preferences.ts`.
@@ -11014,35 +11205,41 @@ var RAIL_FOLDS = [
11014
11205
  "prb-rail-path",
11015
11206
  "prb-rail-kiss"
11016
11207
  ];
11017
- function readRail(value) {
11208
+ function readFolds(value, known) {
11018
11209
  if (!value || typeof value !== "object") return void 0;
11019
- const rail = {};
11020
- for (const fold of RAIL_FOLDS) {
11210
+ const folds = {};
11211
+ for (const fold of known) {
11021
11212
  const open = value[fold];
11022
- if (typeof open === "boolean") rail[fold] = open;
11213
+ if (typeof open === "boolean") folds[fold] = open;
11023
11214
  }
11024
- return Object.keys(rail).length > 0 ? rail : void 0;
11215
+ return Object.keys(folds).length > 0 ? folds : void 0;
11025
11216
  }
11217
+ var readRail = (value) => readFolds(value, RAIL_FOLDS);
11218
+ var readIssueGroups = (value) => readFolds(value, ISSUE_FOLDS);
11026
11219
  /** A stored layout, keeping only what this page knows how to draw. Anything else reads as unset. */
11027
11220
  function readReviewLayout(value) {
11028
11221
  if (!value || typeof value !== "object") return {};
11029
- const { ask, expanded, rail } = value;
11222
+ const { ask, expanded, rail, issueGroups } = value;
11030
11223
  const folds = readRail(rail);
11224
+ const groups = readIssueGroups(issueGroups);
11031
11225
  return {
11032
11226
  ...ask === "open" || ask === "closed" ? { ask } : {},
11033
11227
  ...expanded === "buddy" || expanded === "document" ? { expanded } : {},
11034
- ...folds ? { rail: folds } : {}
11228
+ ...folds ? { rail: folds } : {},
11229
+ ...groups ? { issueGroups: groups } : {}
11035
11230
  };
11036
11231
  }
11037
11232
  /** A patch from the page, or null when it holds nothing this page could have sent. */
11038
11233
  function readReviewLayoutPatch(value) {
11039
11234
  if (!value || typeof value !== "object") return null;
11040
- const { ask, expanded, rail } = value;
11235
+ const { ask, expanded, rail, issueGroups } = value;
11041
11236
  const folds = readRail(rail);
11237
+ const groups = readIssueGroups(issueGroups);
11042
11238
  const patch = {
11043
11239
  ...ask === "open" || ask === "closed" ? { ask } : {},
11044
11240
  ...expanded === "buddy" || expanded === "document" || expanded === null ? { expanded } : {},
11045
- ...folds ? { rail: folds } : {}
11241
+ ...folds ? { rail: folds } : {},
11242
+ ...groups ? { issueGroups: groups } : {}
11046
11243
  };
11047
11244
  return Object.keys(patch).length > 0 ? patch : null;
11048
11245
  }
@@ -11056,6 +11253,10 @@ function applyReviewLayoutPatch(layout, patch) {
11056
11253
  ...patch.rail ? { rail: {
11057
11254
  ...layout.rail,
11058
11255
  ...patch.rail
11256
+ } } : {},
11257
+ ...patch.issueGroups ? { issueGroups: {
11258
+ ...layout.issueGroups,
11259
+ ...patch.issueGroups
11059
11260
  } } : {}
11060
11261
  };
11061
11262
  }
@@ -12522,6 +12723,33 @@ feature: E</pre>
12522
12723
  reads to answer your questions about the change, and what Update review starts from. After
12523
12724
  that it is removed. The review itself stays, and a later question makes a new checkout for
12524
12725
  itself.</p>`
12726
+ },
12727
+ {
12728
+ id: "reading-issues",
12729
+ title: "Reading issues",
12730
+ body: `<p>Issues opens with a sentence on what the change touches and how many issues are
12731
+ outstanding. Each issue is grouped by the decision it asks of you: <b>Failures</b> the code shows
12732
+ going wrong under a stated condition, <b>Possible risks</b> still to be verified, <b>Intent to
12733
+ confirm</b> where the behaviour is right only if it was meant, and <b>Maintenance</b> for what it
12734
+ costs later. Within each group the most severe come first. Severity and confidence are shown on
12735
+ each card, separately from its group.</p>
12736
+ <p>A card leads with what the issue means and when it happens, then the step it calls for, when
12737
+ there is one: <b>To verify</b> settles whether it happens, <b>To decide</b> asks whether it was
12738
+ meant, and <b>To fix</b> names the change that would correct it. The technical account, where it
12739
+ is in the code and its history are under <b>Technical details</b> on the card. Folding a group is
12740
+ remembered for the next review.</p>
12741
+ <p>Issues from older reviews have no summary yet and are under <b>Other issues</b>. The next
12742
+ Update that finds new commits or new discussion writes one for each issue still outstanding,
12743
+ from the same evidence; it does not re-check the issue. An Update that finds nothing new changes
12744
+ nothing.</p>
12745
+ <p>Questions read the same way: each leads with the question in plain terms, why the answer
12746
+ matters and what is needed, with the original question and its files under <b>Technical
12747
+ context</b>. Any answer so far stays on the card.</p>
12748
+ <p>When the review came with a pull request discussion, the line above the issues says when it
12749
+ was last checked. Its “already raised” notes describe the discussion as it was then; <b>Re-check
12750
+ in PR context</b> takes you to bring it up to date.</p>
12751
+ <p>Resolved and dismissed issues are in the closed list at the bottom. One the new commits fixed
12752
+ leads with what the code now does, when the update could see it.</p>`
12525
12753
  },
12526
12754
  {
12527
12755
  id: "updating-and-reanalysing",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prreviewbuddy",
3
- "version": "0.90.0",
3
+ "version": "0.92.0",
4
4
  "description": "Understand code changes before you approve them. Local branch and pull request reviews with Claude Code or Codex.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://prreviewbuddy.com",
package/static/shell.css CHANGED
@@ -2386,10 +2386,6 @@ a.prb-rail-item { text-decoration: none; }
2386
2386
  line-height: var(--lh-body);
2387
2387
  }
2388
2388
 
2389
- /* A caveat above a list, in the light pane. Pane literals, not shell tokens — see
2390
- pane_palette.test.ts for why this is a rule and not a preference. */
2391
- /* Full width, because it introduces the cards below it and a narrower block reads as a floating
2392
- aside rather than as the heading note it is. */
2393
2389
  /* The control named in the note, as the control rather than as bold text. It goes to the view that
2394
2390
  holds it, because a sentence that tells the reviewer where something is and cannot take them
2395
2391
  there is asking them to do the navigating twice. */
@@ -2405,16 +2401,41 @@ a.prb-rail-item { text-decoration: none; }
2405
2401
  }
2406
2402
  .prb-doc-note-link:hover { color: var(--pane-ink); }
2407
2403
 
2408
- .prb-doc-note {
2409
- margin: 0 0 14px;
2410
- padding: 8px 11px;
2411
- border-left: 3px solid var(--pane-line);
2412
- border-radius: 0 6px 6px 0;
2413
- background: rgba(15, 23, 42, 0.035);
2404
+ /* The age of the "already raised" claims, as one quiet line above the Issues orientation. */
2405
+ .prb-discussion-age {
2406
+ display: flex;
2407
+ flex-wrap: wrap;
2408
+ align-items: baseline;
2409
+ column-gap: 6px;
2410
+ margin: 0 0 12px;
2414
2411
  color: var(--pane-ink-muted);
2415
2412
  font-size: var(--t-meta);
2416
2413
  line-height: var(--lh-body);
2417
2414
  }
2415
+ .prb-discussion-age-summary {
2416
+ cursor: pointer;
2417
+ list-style: none;
2418
+ }
2419
+ .prb-discussion-age-summary::-webkit-details-marker { display: none; }
2420
+ .prb-discussion-age-summary::before {
2421
+ content: '';
2422
+ display: inline-block;
2423
+ margin-right: 6px;
2424
+ vertical-align: 1px;
2425
+ border-left: 5px solid currentColor;
2426
+ border-top: 4px solid transparent;
2427
+ border-bottom: 4px solid transparent;
2428
+ }
2429
+ .prb-discussion-age-detail[open] > .prb-discussion-age-summary::before { transform: rotate(90deg); }
2430
+ .prb-discussion-age-summary:hover { color: var(--pane-ink); }
2431
+ /* Open, the explanation takes the row, and the re-check follows it on its own line. */
2432
+ .prb-discussion-age-detail[open] { flex-basis: 100%; }
2433
+ .prb-discussion-age-detail[open] ~ .prb-discussion-age-sep { display: none; }
2434
+ .prb-discussion-age-body {
2435
+ margin: 6px 0 4px;
2436
+ padding-left: 11px;
2437
+ border-left: 2px solid var(--pane-line);
2438
+ }
2418
2439
 
2419
2440
  .prb-pr-asof {
2420
2441
  flex-basis: 100%;