prreviewbuddy 0.90.0 → 0.91.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $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-BR7_XprF.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,99 @@ 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
+ step: 0
1577
+ };
1578
+ for (const entry of entries) {
1579
+ tally.bySection[entry.section] = (tally.bySection[entry.section] ?? 0) + 1;
1580
+ if (entry.next) tally[entry.next] += 1;
1581
+ }
1582
+ return tally;
1583
+ }
1584
+ function tallyIssues(issues) {
1585
+ return tallyEntries(issues.map((issue) => {
1586
+ const step = issue.consequence?.next;
1587
+ return {
1588
+ section: sectionOf(issue),
1589
+ ...step ? { next: step.kind ?? "step" } : {}
1590
+ };
1591
+ }));
1592
+ }
1593
+ var count = (n, one, many) => `${n} ${n === 1 ? one : many}`;
1594
+ /**
1595
+ * One factual line over the sections: how many issues are outstanding, and how many of them name
1596
+ * a step, by the kind the analysis gave it. The per-section counts are the nav's, so they are not
1597
+ * repeated here. Always "issues": Questions keep their own tab and their own count.
1598
+ */
1599
+ function issuesAttentionLine(tally) {
1600
+ const total = Object.values(tally.bySection).reduce((sum, n) => sum + (n ?? 0), 0);
1601
+ if (total === 0) return "No issues are outstanding from this analysis.";
1602
+ const steps = [
1603
+ tally.verify ? count(tally.verify, "verification step", "verification steps") : "",
1604
+ tally.decide ? count(tally.decide, "decision", "decisions") : "",
1605
+ tally.step ? count(tally.step, "next step", "next steps") : ""
1606
+ ].filter(Boolean);
1607
+ return [count(total, "outstanding issue", "outstanding issues"), ...steps].join(" · ");
1608
+ }
1609
+ /**
1610
+ * Buttons to each section with its count. Buttons, not `#` links: the hash routes the review page.
1611
+ * Drawn only when there are two sections to choose between; a single one is already in view.
1612
+ */
1613
+ function renderImpactNavItems(tally) {
1614
+ const present = ISSUE_SECTIONS.filter((section) => (tally.bySection[section] ?? 0) > 0);
1615
+ if (present.length < 2) return "";
1616
+ 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("");
1617
+ }
1618
+ //#endregion
1525
1619
  //#region ../../packages/review-harness/src/domain/ai_review/review_outcome.ts
1526
1620
  /**
1527
1621
  * The one reading of a session, for every surface.
@@ -3292,16 +3386,6 @@ function renderInvestigationNote(note) {
3292
3386
  return `<p class="pr-helper-investigation-note">${renderInlineText(note)}</p>`;
3293
3387
  }
3294
3388
  /**
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
3389
  * Work that is done, kept and closed.
3306
3390
  *
3307
3391
  * Collapsed rather than dropped: the reviewer asked whether these were fixed, and "they are gone
@@ -3309,8 +3393,8 @@ function findingsProgressBody(total, open, verb = "resolved") {
3309
3393
  * ten resolved findings above five open ones is the layout that made the count feel wrong in the
3310
3394
  * first place.
3311
3395
  */
3312
- function renderClosedFindings(summary, cards, kind, nothingOpen, waiting) {
3313
- return `<details class="pr-helper-closed-findings" data-closed-findings="${kind}"${nothingOpen ? " data-nothing-open" : ""}>
3396
+ function renderClosedFindings(summary, cards, kind, nothingOpen, waiting, open = false) {
3397
+ return `<details class="pr-helper-closed-findings" data-closed-findings="${kind}"${nothingOpen ? " data-nothing-open" : ""}${open ? " open" : ""}>
3314
3398
  <summary class="pr-helper-closed-findings-summary" data-closed-findings-summary>${escapeHtml$1(summary)}${renderWaitingSuggestions(waiting)}</summary>
3315
3399
  <div class="pr-helper-closed-findings-list">${cards}</div>
3316
3400
  </details>`;
@@ -3346,13 +3430,37 @@ function renderIssuesTab(session, investigatingId, options) {
3346
3430
  const isDismissed = dispositions.isDismissed(issue);
3347
3431
  const stale = dispositions.isStale(issue);
3348
3432
  const assessment = assess("issue", issue);
3433
+ const impact = issue.consequence;
3434
+ const fixed = issue.reconciliationStatus === "resolved" ? issue.resolvedBehaviour?.trim() : void 0;
3435
+ const framed = Boolean(impact || fixed);
3436
+ const headline = fixed ?? impact?.title ?? issue.title;
3437
+ const explanation = renderCardExplanation(explanationOf(session, "issue", issue.id), options);
3438
+ const technical = `<p class="pr-helper-issue-desc">${renderInlineText(issue.description)}</p>${framed ? "" : explanation}
3439
+ ${framed ? "" : renderReconciliationNote(issue.reconciliationNote)}
3440
+ ${renderStatusHistory(issue.statusHistory, now)}
3441
+ ${renderInvestigationNote(issue.investigationNote)}
3442
+ ${renderExistingDiscussion(issue.existingDiscussion)}
3443
+ ${renderFileLocation(issue.file, "pr-helper-issue-loc", issue.line)}
3444
+ ${renderRelatedFiles(issue.relatedFiles)}
3445
+ ${renderInvestigationProvenance("issue", issue.id, records, now)}`;
3446
+ const body = framed ? `${fixed ? "" : renderConsequence(impact)}${explanation}
3447
+ ${renderReconciliationNote(issue.reconciliationNote)}
3448
+ <details class="pr-helper-issue-technical">
3449
+ <summary class="pr-helper-issue-technical-summary">Technical details</summary>
3450
+ <div class="pr-helper-issue-technical-body">
3451
+ ${fixed && impact ? renderOriginalConsequence(impact) : ""}
3452
+ <p class="pr-helper-issue-technical-title">${renderInlineText(issue.title)}</p>
3453
+ ${technical}
3454
+ </div>
3455
+ </details>` : technical;
3456
+ const next = impact?.next ? ` data-issue-next="${impact.next.kind ?? "step"}"` : "";
3349
3457
  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)}">
3458
+ <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
3459
  <div class="pr-helper-issue-header">
3352
3460
  <div class="pr-helper-issue-title-row">
3353
3461
  ${options?.feedbackSelect ? renderFeedbackPick("issue", issue.title, issueFeedbackMarkdown(issue), ref) : ""}
3354
3462
  ${renderFindingRef(ref)}
3355
- <p class="pr-helper-issue-title">${renderInlineText(issue.title)}</p>
3463
+ <p class="pr-helper-issue-title">${renderInlineText(headline)}</p>
3356
3464
  ${renderFindingCopySource(issueMarkdown(issue, assessment), "issue")}
3357
3465
  ${options?.askBuddy ? renderAskBuddyMenu("issue") : ""}
3358
3466
  </div>
@@ -3367,14 +3475,7 @@ function renderIssuesTab(session, investigatingId, options) {
3367
3475
  </div>
3368
3476
  ${renderStaleDismissal(stale)}
3369
3477
  ${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)}
3478
+ ${body}
3378
3479
  <div class="pr-helper-issue-actions">
3379
3480
  ${renderInvestigationActions("issue", issue.id, records, investigatingId)}
3380
3481
  ${renderDismissToggle(issue, isDismissed, stale, dismissals.get(issue.id), dispositions.isClosedByVerdict(issue))}
@@ -3383,11 +3484,56 @@ function renderIssuesTab(session, investigatingId, options) {
3383
3484
  `;
3384
3485
  };
3385
3486
  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("");
3487
+ const outstanding = result.issues.filter((issue) => isIssueOutstanding(issue, dispositions));
3388
3488
  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)}`;
3489
+ const folds = options?.issueFolds ?? {};
3490
+ const tally = tallyIssues(outstanding);
3491
+ const grouped = new Map(groupIssues(outstanding, (issue) => assess("issue", issue).current).map((group) => [group.section, group.issues]));
3492
+ const present = new Set(result.issues.map(sectionOf));
3493
+ const headless = present.size === 1 && present.has("other");
3494
+ const groups = ISSUE_SECTIONS.filter((section) => present.has(section)).map((section) => {
3495
+ const issues = grouped.get(section) ?? [];
3496
+ if (headless) return `<div class="pr-helper-issue-group" data-impact-group="${section}"${issues.length ? "" : " hidden"}>
3497
+ <div class="pr-helper-issue-group-list">${issues.map(card).join("")}</div>
3498
+ </div>`;
3499
+ return `<details class="pr-helper-issue-group" data-impact-group="${section}"${folds[section] === false ? "" : " open"}${issues.length ? "" : " hidden"}>
3500
+ <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>
3501
+ <div class="pr-helper-issue-group-list">${issues.map(card).join("")}</div>
3502
+ </details>`;
3503
+ }).join("");
3504
+ const nav = renderImpactNavItems(tally);
3505
+ const orientation = result.orientation?.trim();
3506
+ const head = `<div class="pr-helper-issues-orientation">
3507
+ ${orientation ? `<p class="pr-helper-issues-orientation-text">${renderInlineText(orientation)}</p>` : ""}
3508
+ <p class="pr-helper-issues-attention" role="status" data-issues-attention>${escapeHtml$1(issuesAttentionLine(tally))}</p>
3509
+ <nav class="pr-helper-issue-nav" aria-label="Issues by kind" data-issue-nav-list${nav ? "" : " hidden"}>${nav}</nav>
3510
+ </div>`;
3511
+ if (closedIssues.length === 0) return `${head}${groups}`;
3512
+ 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)}`;
3513
+ }
3514
+ /** What the issue means, read first: the scenario and the step that would settle it. */
3515
+ function renderConsequence(impact) {
3516
+ if (!impact) return "";
3517
+ return `<div class="pr-helper-issue-impact">
3518
+ <p class="pr-helper-issue-scenario">${renderInlineText(impact.scenario)}</p>
3519
+ ${renderConsequenceStep(impact.next)}
3520
+ </div>`;
3521
+ }
3522
+ var STEP_LABELS = {
3523
+ verify: "To verify:",
3524
+ decide: "To decide:"
3525
+ };
3526
+ function renderConsequenceStep(step) {
3527
+ if (!step) return "";
3528
+ 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>`;
3529
+ }
3530
+ /** A fixed issue's original consequence, kept with the technical account once the fix heads the card. */
3531
+ function renderOriginalConsequence(impact) {
3532
+ return `<div class="pr-helper-issue-original">
3533
+ <h5>Before the fix</h5>
3534
+ <p class="pr-helper-issue-original-title">${renderInlineText(impact.title)}</p>
3535
+ <p class="pr-helper-issue-scenario">${renderInlineText(impact.scenario)}</p>
3536
+ </div>`;
3391
3537
  }
3392
3538
  /**
3393
3539
  * What to call the closed pile, by what is actually in it.
@@ -6783,7 +6929,8 @@ function renderStandardViews(view, sections) {
6783
6929
  askBuddy: true,
6784
6930
  feedbackSelect: true,
6785
6931
  reassess: true,
6786
- findingRefs: true
6932
+ findingRefs: true,
6933
+ ...view.reviewLayout?.issueGroups ? { issueFolds: view.reviewLayout.issueGroups } : {}
6787
6934
  })}
6788
6935
  </section>
6789
6936
  <section class="prb-view" data-view="questions" hidden>
@@ -10136,7 +10283,7 @@ function renderReviewsPage(view) {
10136
10283
  const collected = (memberOf.get(group.key) ?? []).length > 0;
10137
10284
  const home = homeSection(group.latest.pinned === true, group.latest.lifecycle, activityOf(group), collected, "all");
10138
10285
  if (home === void 0) collectedHome.add(group);
10139
- sections[home ?? sectionOf(group.latest.lifecycle, activityOf(group))].push(group);
10286
+ sections[home ?? sectionOf$1(group.latest.lifecycle, activityOf(group))].push(group);
10140
10287
  }
10141
10288
  const storedReviews = view.groups.reduce((total, group) => total + group.history.length, 0);
10142
10289
  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 +11147,11 @@ function isRunsSection(value) {
11000
11147
  /**
11001
11148
  * How the reviewer last laid out a review page, kept so the next review opens the same way.
11002
11149
  *
11003
- * Three things, each the layout as it last stood rather than a log of what was pressed: whether
11150
+ * Four things, each the layout as it last stood rather than a log of what was pressed: whether
11004
11151
  * 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.
11152
+ * whichever pane that is on the next review), which parts of the rail were folded, and which
11153
+ * sections of the Issues pane were. A field nobody has changed has no key, so the page's own
11154
+ * defaults still decide for a new reviewer.
11007
11155
  *
11008
11156
  * Pure, so the page and the server read it with the same rules. Stored in `ui.json`; see
11009
11157
  * `preferences.ts`.
@@ -11014,35 +11162,41 @@ var RAIL_FOLDS = [
11014
11162
  "prb-rail-path",
11015
11163
  "prb-rail-kiss"
11016
11164
  ];
11017
- function readRail(value) {
11165
+ function readFolds(value, known) {
11018
11166
  if (!value || typeof value !== "object") return void 0;
11019
- const rail = {};
11020
- for (const fold of RAIL_FOLDS) {
11167
+ const folds = {};
11168
+ for (const fold of known) {
11021
11169
  const open = value[fold];
11022
- if (typeof open === "boolean") rail[fold] = open;
11170
+ if (typeof open === "boolean") folds[fold] = open;
11023
11171
  }
11024
- return Object.keys(rail).length > 0 ? rail : void 0;
11172
+ return Object.keys(folds).length > 0 ? folds : void 0;
11025
11173
  }
11174
+ var readRail = (value) => readFolds(value, RAIL_FOLDS);
11175
+ var readIssueGroups = (value) => readFolds(value, ISSUE_FOLDS);
11026
11176
  /** A stored layout, keeping only what this page knows how to draw. Anything else reads as unset. */
11027
11177
  function readReviewLayout(value) {
11028
11178
  if (!value || typeof value !== "object") return {};
11029
- const { ask, expanded, rail } = value;
11179
+ const { ask, expanded, rail, issueGroups } = value;
11030
11180
  const folds = readRail(rail);
11181
+ const groups = readIssueGroups(issueGroups);
11031
11182
  return {
11032
11183
  ...ask === "open" || ask === "closed" ? { ask } : {},
11033
11184
  ...expanded === "buddy" || expanded === "document" ? { expanded } : {},
11034
- ...folds ? { rail: folds } : {}
11185
+ ...folds ? { rail: folds } : {},
11186
+ ...groups ? { issueGroups: groups } : {}
11035
11187
  };
11036
11188
  }
11037
11189
  /** A patch from the page, or null when it holds nothing this page could have sent. */
11038
11190
  function readReviewLayoutPatch(value) {
11039
11191
  if (!value || typeof value !== "object") return null;
11040
- const { ask, expanded, rail } = value;
11192
+ const { ask, expanded, rail, issueGroups } = value;
11041
11193
  const folds = readRail(rail);
11194
+ const groups = readIssueGroups(issueGroups);
11042
11195
  const patch = {
11043
11196
  ...ask === "open" || ask === "closed" ? { ask } : {},
11044
11197
  ...expanded === "buddy" || expanded === "document" || expanded === null ? { expanded } : {},
11045
- ...folds ? { rail: folds } : {}
11198
+ ...folds ? { rail: folds } : {},
11199
+ ...groups ? { issueGroups: groups } : {}
11046
11200
  };
11047
11201
  return Object.keys(patch).length > 0 ? patch : null;
11048
11202
  }
@@ -11056,6 +11210,10 @@ function applyReviewLayoutPatch(layout, patch) {
11056
11210
  ...patch.rail ? { rail: {
11057
11211
  ...layout.rail,
11058
11212
  ...patch.rail
11213
+ } } : {},
11214
+ ...patch.issueGroups ? { issueGroups: {
11215
+ ...layout.issueGroups,
11216
+ ...patch.issueGroups
11059
11217
  } } : {}
11060
11218
  };
11061
11219
  }
@@ -12522,6 +12680,23 @@ feature: E</pre>
12522
12680
  reads to answer your questions about the change, and what Update review starts from. After
12523
12681
  that it is removed. The review itself stays, and a later question makes a new checkout for
12524
12682
  itself.</p>`
12683
+ },
12684
+ {
12685
+ id: "reading-issues",
12686
+ title: "Reading issues",
12687
+ body: `<p>Issues opens with a sentence on what the change touches and how many issues are
12688
+ outstanding. Each issue is grouped by the decision it asks of you: <b>Failures</b> the code shows
12689
+ going wrong under a stated condition, <b>Possible risks</b> still to be verified, <b>Intent to
12690
+ confirm</b> where the behaviour is right only if it was meant, and <b>Maintenance</b> for what it
12691
+ costs later. Within each group the most severe come first. Severity and confidence are shown on
12692
+ each card, separately from its group.</p>
12693
+ <p>A card leads with what the issue means and when it happens, then the step that would settle
12694
+ it, marked <b>To verify</b> or <b>To decide</b>. The technical account, where it is in the code and
12695
+ its history are under <b>Technical details</b> on the card. Folding a group is remembered for the
12696
+ next review. Issues from reviews made before this have no grouping and are under <b>Other
12697
+ issues</b>, as they were.</p>
12698
+ <p>Resolved and dismissed issues are in the closed list at the bottom. One the new commits fixed
12699
+ leads with what the code now does, when the update could see it.</p>`
12525
12700
  },
12526
12701
  {
12527
12702
  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.91.1",
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",
@@ -1521,8 +1521,7 @@
1521
1521
  Deliberately lighter than the Guide's progress box (no bar, no "next up" line): the Guide is
1522
1522
  a workflow you walk in order, so it earns a full dashboard, whereas this is a flat list where
1523
1523
  the only useful fact is how many are still open. */
1524
- .pr-helper-questions-progress,
1525
- .pr-helper-findings-progress {
1524
+ .pr-helper-questions-progress {
1526
1525
  display: flex;
1527
1526
  align-items: center;
1528
1527
  gap: 8px;
@@ -1590,6 +1589,180 @@
1590
1589
  .pr-helper-closed-findings-list .pr-helper-issue-card:hover,
1591
1590
  .pr-helper-closed-findings-list .pr-helper-question-card:hover { opacity: 1; }
1592
1591
 
1592
+ /* ── Issues by kind ──
1593
+ The pane opens on what the change touches and how much needs attention, then one section per
1594
+ kind of decision. A section is a disclosure so a reader can fold away what they have dealt
1595
+ with; its heading keeps the Questions tab's group style so the two tabs read as one product. */
1596
+ .pr-helper-issues-orientation {
1597
+ margin-bottom: 16px;
1598
+ }
1599
+
1600
+ .pr-helper-issues-orientation-text {
1601
+ font-size: 13px;
1602
+ line-height: 1.5;
1603
+ margin: 0 0 4px;
1604
+ overflow-wrap: anywhere;
1605
+ }
1606
+
1607
+ .pr-helper-issues-attention {
1608
+ font-size: 12px;
1609
+ font-weight: 600;
1610
+ color: var(--color-fg-muted, #57606a);
1611
+ margin: 0;
1612
+ }
1613
+
1614
+ .pr-helper-issue-nav {
1615
+ display: flex;
1616
+ flex-wrap: wrap;
1617
+ gap: 6px;
1618
+ margin-top: 10px;
1619
+ }
1620
+
1621
+ .pr-helper-issue-nav[hidden] { display: none; }
1622
+
1623
+ .pr-helper-issue-nav-item {
1624
+ display: inline-flex;
1625
+ align-items: center;
1626
+ gap: 6px;
1627
+ font: inherit;
1628
+ font-size: 12px;
1629
+ font-weight: 600;
1630
+ padding: 3px 10px;
1631
+ border: 1px solid var(--color-border-default, #d0d7de);
1632
+ border-radius: 100px;
1633
+ background: transparent;
1634
+ color: var(--color-fg-default, #1f2328);
1635
+ cursor: pointer;
1636
+ }
1637
+
1638
+ .pr-helper-issue-nav-item:hover { border-color: #9333ea; }
1639
+ .pr-helper-issue-nav-item:focus-visible { outline: 2px solid #9333ea; outline-offset: 2px; }
1640
+
1641
+ .pr-helper-issue-nav-count {
1642
+ color: var(--color-fg-muted, #57606a);
1643
+ font-variant-numeric: tabular-nums;
1644
+ }
1645
+
1646
+ .pr-helper-issue-group { margin-bottom: 8px; }
1647
+ .pr-helper-issue-group[hidden] { display: none; }
1648
+
1649
+ .pr-helper-issue-group-summary {
1650
+ display: flex;
1651
+ align-items: center;
1652
+ gap: 6px;
1653
+ cursor: pointer;
1654
+ list-style: none;
1655
+ padding: 4px 0;
1656
+ margin-bottom: 6px;
1657
+ color: #9333ea;
1658
+ scroll-margin-top: 12px;
1659
+ }
1660
+
1661
+ .pr-helper-issue-group-summary::-webkit-details-marker { display: none; }
1662
+
1663
+ .pr-helper-issue-group-summary::before {
1664
+ content: '';
1665
+ width: 0;
1666
+ height: 0;
1667
+ border-left: 5px solid currentColor;
1668
+ border-top: 4px solid transparent;
1669
+ border-bottom: 4px solid transparent;
1670
+ transition: transform 0.15s ease;
1671
+ }
1672
+
1673
+ .pr-helper-issue-group[open] > .pr-helper-issue-group-summary::before { transform: rotate(90deg); }
1674
+ .pr-helper-issue-group-summary:focus-visible { outline: 2px solid #9333ea; outline-offset: 2px; border-radius: 2px; }
1675
+
1676
+ .pr-helper-issue-group-title {
1677
+ font-size: 11px;
1678
+ font-weight: 700;
1679
+ text-transform: uppercase;
1680
+ letter-spacing: 0.04em;
1681
+ margin: 0;
1682
+ }
1683
+
1684
+ .pr-helper-issue-group-count {
1685
+ font-size: 11px;
1686
+ font-weight: 600;
1687
+ color: var(--color-fg-muted, #57606a);
1688
+ }
1689
+
1690
+ /* The consequence, read first. The title row is shared with every card; these sit under it. */
1691
+ .pr-helper-issue-card .pr-helper-issue-title { overflow-wrap: anywhere; }
1692
+
1693
+ .pr-helper-issue-impact { margin: 0 0 8px; }
1694
+
1695
+ .pr-helper-issue-scenario {
1696
+ font-size: 13px;
1697
+ line-height: 1.5;
1698
+ margin: 0 0 6px;
1699
+ text-align: left;
1700
+ overflow-wrap: anywhere;
1701
+ }
1702
+
1703
+ .pr-helper-issue-next {
1704
+ font-size: 13px;
1705
+ line-height: 1.5;
1706
+ margin: 0;
1707
+ overflow-wrap: anywhere;
1708
+ }
1709
+
1710
+ .pr-helper-issue-next-label { font-weight: 600; }
1711
+
1712
+ /* The technical account, one click away and never open on arrival. Same chevron as the closed
1713
+ drawer, smaller and muted, because it belongs to one card rather than to the pane. */
1714
+ .pr-helper-issue-technical { margin-top: 8px; }
1715
+
1716
+ .pr-helper-issue-technical-summary {
1717
+ display: inline-flex;
1718
+ align-items: center;
1719
+ gap: 6px;
1720
+ cursor: pointer;
1721
+ list-style: none;
1722
+ font-size: 12px;
1723
+ font-weight: 600;
1724
+ color: var(--color-fg-muted, #57606a);
1725
+ padding: 2px 0;
1726
+ }
1727
+
1728
+ .pr-helper-issue-technical-summary::-webkit-details-marker { display: none; }
1729
+
1730
+ .pr-helper-issue-technical-summary::before {
1731
+ content: '';
1732
+ width: 0;
1733
+ height: 0;
1734
+ border-left: 5px solid currentColor;
1735
+ border-top: 4px solid transparent;
1736
+ border-bottom: 4px solid transparent;
1737
+ transition: transform 0.15s ease;
1738
+ }
1739
+
1740
+ .pr-helper-issue-technical[open] > .pr-helper-issue-technical-summary::before { transform: rotate(90deg); }
1741
+ .pr-helper-issue-technical-summary:hover { color: var(--color-fg-default, #1f2328); }
1742
+ .pr-helper-issue-technical-summary:focus-visible { outline: 2px solid #9333ea; outline-offset: 2px; border-radius: 2px; }
1743
+
1744
+ .pr-helper-issue-technical-body {
1745
+ margin-top: 8px;
1746
+ padding-left: 11px;
1747
+ border-left: 2px solid var(--color-border-muted, #d8dee4);
1748
+ overflow-wrap: anywhere;
1749
+ }
1750
+
1751
+ .pr-helper-issue-technical-title {
1752
+ font-size: 13px;
1753
+ font-weight: 600;
1754
+ margin: 0 0 4px;
1755
+ }
1756
+
1757
+ .pr-helper-issue-original { margin-bottom: 10px; }
1758
+ .pr-helper-issue-original h5 { margin: 0 0 4px; }
1759
+
1760
+ .pr-helper-issue-original-title {
1761
+ font-size: 13px;
1762
+ font-weight: 600;
1763
+ margin: 0 0 4px;
1764
+ }
1765
+
1593
1766
  .pr-helper-question-actions {
1594
1767
  display: flex;
1595
1768
  align-items: center;