prreviewbuddy 0.92.0 → 0.95.2

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,9 +1,9 @@
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
- import { basename, delimiter, isAbsolute, join, relative, resolve } from "node:path";
3
- import { execFile, spawn } from "node:child_process";
1
+ import { $ as statusPillLabel, $i as lifecycleItemLabel, $n as writeJsonAtomically, $r as findExistingReview, $t as statedWorkspacePort, A as anythingRunning, Aa as currentGradeIn, Ai as indexReviewGroups, An as BUILD_VERSION, Ao as deleteCollection, Ar as chatConversationSection, At as isRequestId, B as completedLabel, Ba as statusLookup, Bi as previousKissResult, Bn as checkArchitecture, Bt as readServerRecord, C as parsePullRequestLink, Ca as isExplanationAction, Cn as readEvents, Cr as ASK_REREAD_CEILING_MS, Ct as findingRefs, D as INDEX_SECTIONS, Da as analysisRefOf, Di as followedRefName, Dn as recordTelemetryUploadConsent, Do as collectionsStamp, Dr as isUnchanged, Dt as endAskRequest, E as relativeTime$1, Ea as NO_STATUS, Ei as decideKissRecommendation, Er as storedConversation, Et as reviewTagsIn, F as collectionFold, Fa as isGradeFor, Fi as loadWorkspace, Fn as recordRuntimeBestEffort, Fo as STORE_ROOT, Fr as renderMarkdown, Ft as readIndexToken, G as markItemLabel, Ga as extractJsonText, Gi as touchWorkspace, Gn as storeArchitecture, Gr as createWorktree, H as homeSection, Ha as followUpFindingId, Hi as reviewedCommit, Hn as partFiles, Hr as isBusy, Ht as serverEntryExists, Ia as isOffered, In as RUNTIME_WORDS, Ir as CODE_BLOCK_CLASS, J as queueItemLabel, Ja as WITHHELD_REASON, Ji as LIFECYCLES, Jn as ownersOf, Jr as readMarker, Jt as DEFAULT_WORKSPACE_PORT, K as omittedText, Ka as objectWithAnyOf, Ki as workspaceRevision, Kn as storedScopes, Kr as holdWorktree, L as collectionPlacement, La as promptGrade, Li as mutateWorkspace, Ln as describeRuntimes, Lr as CODE_COPY_CLASS, Lt as clearServerRecordOwnedBy, M as authorOptions, Ma as findingIn, Mi as kissDecisionsOf, Mn as feedbackUrl, Mo as readCollections, Mr as authorPreview, Mt as markAskRunning, N as collectionCountLabel, Na as findingStatus, Ni as latestKissRun, Nn as waitingHistory, No as renameCollection, Nr as hasAuthorContent, O as INDEX_SECTION_NAMES, Oa as assessmentLookup, Oi as groupByLineage, On as telemetryUploadConsent, Oo as createCollection, Or as agentEnvOf, P as collectionEmptyText, Pa as gradeScale, Pi as lineagePosition, Pn as mapRuntimes, Po as setMembership, Pr as GITHUB_MARKDOWN, Q as statusLabel, Qa as DetachedHeadError, Qi as lifecycleChip, Qn as isArchitectureId, Qt as resolveWorkspacePort, R as collectionsLine, Ra as reassessIn, Rn as formatDuration, Rr as escapeHtml$1, S as extractPullRequestLinks, Sa as explanationOf, So as versionFrom, Sr as existingPaths, St as findingRef, T as runBatch, Ta as ANALYSIS_ONLY, To as AgentUnavailableError, Tr as saveRefresh, Tt as reviewTag, U as isCollectionId, Ua as SEVERITIES, Un as readArchitecture, Ur as withReviewLock, Ut as spawnServer, V as countLabel, Va as wasReassessed, Vi as recentWorkspaces, Vn as loadArchitecture, Vr as ReviewBeingDeletedError, W as isFoldKey, Wa as asString, Wn as repositoryKey, Wr as MANAGED_ROOT, Wt as stopChild, X as sectionOpen, Xa as resolveTarget, Xi as activityLabel, Xn as REPOSITORY_SCOPE, Xr as requestJobStop, Xt as MIN_WORKSPACE_PORT, Y as queueOptionLabel, Ya as kissOutcome, Yi as activityAttributes, Yn as QUOTE_LIMITS, Yr as removeWorktree, Yt as MAX_WORKSPACE_PORT, Z as selectAllLabel, Za as originRemote, Zi as isLifecycle, Zn as ancestorsOf, Zt as parseWorkspacePort, _ as endRun, _a as reviewerDispositions, _i as isLiveJob, _n as prepareReview, _o as agentIdentityOf, _r as retireNotification, _t as modelProvenanceOf, a as lineageIds, aa as sectionOf$1, ai as forgeResolver, an as cancellableHere, ao as git, ar as createPrState, at as lineageKeysFor, b as stopRun, ba as recommendationsOf, bi as progressSteps, bn as runJob, bo as parkedReason, br as unreadCount$1, bt as humanizeModelId, c as discardJob, ca as queueRemove, ci as allJobs, cn as recheckConversation, co as resolveModel, cr as listNotifications, ct as compactQueue, d as startKissJob, da as doneVerb, di as fail$1, dn as findingsChanged, do as readAgentPreference, dr as markReadForReview, ea as lifecycleName, ei as checkCodeFreshness, en as writePortPreference, eo as isCommitReview, er as announceJob, et as deliver, f as isDriving, fa as isIssueOutstanding, fi as isTerminal, fn as UpdateAlreadyRunningError, fo as writeAgentPreference, fr as moveIds, g as completeRun, ga as resolvedByConversation, gn as observeReview, go as agentFor$1, gr as removeNotification, h as beginRun, ha as questionsOutstanding, hi as saveJob, hn as failureOf$1, ho as agentById, hr as readStateOf, i as deleteReview, ia as parseLifecycle, ii as fetchHead, in as cancelUpdate, io as shorthandOf, ir as createArchitectureNotification, it as retryableMiss, j as authorOptionLabel, ja as findingAssessment, ji as kissDecisionRefusal, jo as nameProblemText, jr as UNESTABLISHED_MOTIVATION, k as PR_FILTER_OPTIONS, ka as closesFinding, ki as indexKeptIds, ko as createWithMembers, kr as EXPLAIN_SIMPLY_PROMPT, l as kissAgentId, la as queueStamp, li as attemptIdOf, ln as updateReview, lo as agentIdOf, lr as markAllRead, m as RUN_KINDS, ma as issuesOutstanding, mi as pruneJobResidue, mn as runningJobs, mo as AGENT_IDS, mr as readNotification, n as MCP_TOOL_NAMES, na as lifecyclePillLabel, ni as followedBranch, nn as FETCH_TIMEOUT_MS, no as AmbiguousRefError, nr as subjectOf, o as liveJobsFor, oa as queueAdd, oi as fillFileUrlTemplate, on as mergeUpdateInto, oo as refExists, or as createPrUpdate, ot as settleCollections, p as track, pa as isQuestionOutstanding, pi as loadJob, pn as liveUpdateFor, pr as pruneNotifications, pt as cliEntry, q as pinItemLabel, qa as describeAuthorship, qi as workspaceStamps, qn as trackedFiles, qr as isWorktreeHeld, qt as writeServerRecord, r as deleteLineage, ra as offersLifecycle, ri as reviewedShaOf, rn as RefreshUnavailableError, ro as shortNameOf, rr as categoryOf, rt as openDestination, s as wasBlocked, sa as queueAddMany, si as RUN_RECORD_RETENTION_MS, so as resolveAgent, sr as eventName, st as activateQueued, ta as lifecyclePillAction, ti as checkReview, to as resolveBase, tr as branchOf, ua as readQueue, ui as endedAt, un as askCheckout, uo as clearAgentPreference, ur as markRead, ut as settleQueue, v as heldRuns, va as verdictClosure, vi as isTerminalJobPhase, vn as settleReview, vo as detectAgents, vr as toneOf, vt as provenanceDetailLines, w as unsupportedReason, wa as withExplanation, wi as isClaimed, wn as record, wo as AgentCancelledError, wr as prepareConversationWithin, wt as refOf, x as batchIdentity, xa as explanationActionOf, xi as purposeOf, xo as runnableAgents, xr as withUsageRecorded, xt as findingAtRef, y as reportRun, ya as processDiscussion, yi as phasesFor, yn as reanalyseReview, yo as isAgentId, yr as unreadAround, yt as agentDisplayName, z as completedFold, za as sourcePhrase, zi as presentedKissRunOf, zn as ARCHITECTURE_DIR, zr as renderInlineText } from "./mcp_tools-DDbcXys2.js";
2
+ import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
4
3
  import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
4
+ import { basename, delimiter, isAbsolute, join, relative, resolve } from "node:path";
5
5
  import { homedir } from "node:os";
6
- import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
6
+ import { execFile, spawn } from "node:child_process";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { createServer } from "node:http";
9
9
  //#region ../../packages/review-harness/src/domain/ai_review/review_composition.ts
@@ -477,317 +477,6 @@ function renderIntegrationsNotice(names) {
477
477
  </div>`;
478
478
  }
479
479
  //#endregion
480
- //#region ../../packages/review-harness/src/workspace/reviews_index.ts
481
- /**
482
- * The sections of the list, top to bottom.
483
- *
484
- * Pinned is not a lifecycle state. It is where a card is drawn, on top of a lifecycle it keeps: a
485
- * pinned review can be Active, Waiting or Done, and unpinning it puts it back in the section that
486
- * says. That is why it lives here, beside the other placement rules, and not in `LIFECYCLES`.
487
- */
488
- var INDEX_SECTIONS = [
489
- "pinned",
490
- "active",
491
- "waiting",
492
- "done"
493
- ];
494
- /**
495
- * Every section the list draws, top to bottom: In progress, then the card sections.
496
- *
497
- * In progress is not in `INDEX_SECTIONS` because no card is ever placed there. It lists work, one
498
- * entry per running job or waiting row (`deriveOperations`), and a review being updated stays a
499
- * card in its own section while its update is listed here. Windows, Load more, the selection and
500
- * the counts are all about cards, so they keep reading `INDEX_SECTIONS`; folding, its memory and its
501
- * keyboard commands are about headings, so they read this.
502
- */
503
- var LIST_SECTIONS = ["in-progress", ...INDEX_SECTIONS];
504
- /**
505
- * Each section's name, as its heading says it, and the key that folds it from the keyboard
506
- * (⌘K L and this key). The commands are made from this table, so a section added here is a section
507
- * the keyboard can fold, with no second list to remember.
508
- */
509
- var lifecycleName = (id) => LIFECYCLES.find((lifecycle) => lifecycle.id === id).name;
510
- var INDEX_SECTION_NAMES = {
511
- "in-progress": {
512
- title: "In progress",
513
- key: "i"
514
- },
515
- pinned: {
516
- title: "Pinned",
517
- key: "p"
518
- },
519
- active: {
520
- title: lifecycleName("active"),
521
- key: "a"
522
- },
523
- waiting: {
524
- title: lifecycleName("waiting"),
525
- key: "w"
526
- },
527
- done: {
528
- title: lifecycleName("done"),
529
- key: "d"
530
- }
531
- };
532
- /**
533
- * Whether a section starts open. The reviewer's choice wins, and otherwise everything is open but
534
- * the archives: Done, and each collection's completed reviews. A new collection starts open. The
535
- * one place that default is written.
536
- */
537
- function sectionOpen(section, collapsed) {
538
- return !(collapsed?.[section] ?? (section === "done" || section.startsWith("collection-done:")));
539
- }
540
- /** A section the list draws a heading for, and so one whose fold can be stored. */
541
- function isListSection(value) {
542
- return typeof value === "string" && LIST_SECTIONS.includes(value);
543
- }
544
- /** A fold the store accepts: a list section, or a collection's, by an id shaped like one. */
545
- function isFoldKey(value) {
546
- if (isListSection(value)) return true;
547
- if (typeof value !== "string") return false;
548
- const match = /^collection(?:-done)?:(.*)$/.exec(value);
549
- return match !== null && isCollectionId(match[1]);
550
- }
551
- function collectionFold(id) {
552
- return `collection:${id}`;
553
- }
554
- function completedFold(id) {
555
- return `collection-done:${id}`;
556
- }
557
- var COLLECTION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
558
- /**
559
- * Whether a value is shaped like a collection's id. Here, in the module both halves share, because
560
- * the hash and the fold store check it in the browser and on the server alike.
561
- */
562
- function isCollectionId(value) {
563
- return typeof value === "string" && COLLECTION_ID.test(value);
564
- }
565
- /**
566
- * Which section a card is drawn in: Pinned when pinned, and otherwise where its lifecycle says.
567
- *
568
- * Pinning controls prominence in the general list and never overrides an ordering the reviewer
569
- * asked for. Under such a filter a pinned card stays in its lifecycle section, in the order the
570
- * filter gives, and only its star says it is pinned.
571
- */
572
- function indexSectionOf(pinned, lifecycle, activity, filter) {
573
- return pinned && !ordersExplicitly(filter) ? "pinned" : sectionOf$1(lifecycle, activity);
574
- }
575
- /**
576
- * Where a review's own card is drawn, or absent when it is drawn only inside its collections.
577
- *
578
- * Pinned first and exclusive, as it always was: a pinned review is drawn in Pinned and nowhere else,
579
- * collected or not, and unpinning it puts it back where the rest of these rules say. A review in a
580
- * collection is drawn there instead of in Active, Waiting or Done, so those three list what nobody
581
- * has grouped. Under the Queue filter neither applies: the list is the queue in its order, each card
582
- * in its lifecycle section, the same reason `indexSectionOf` gives for Pinned.
583
- */
584
- function homeSection(pinned, lifecycle, activity, collected, filter) {
585
- if (collected && !pinned && !ordersExplicitly(filter)) return void 0;
586
- return indexSectionOf(pinned, lifecycle, activity, filter);
587
- }
588
- /**
589
- * Where a review is drawn inside each collection it belongs to: with the collection's reviews still
590
- * needing attention, under its completed reviews, or not at all.
591
- *
592
- * Completed means Done, by the same derivation that places the card everywhere else, so Waiting is
593
- * still in play and the Reviewed mark has nothing to do with it. One state for every collection the
594
- * review is in: marking it Done moves it in all of them at once. Not drawn while pinned (Pinned is
595
- * exclusive) or under the Queue filter (see `homeSection`).
596
- */
597
- function collectionPlacement(pinned, lifecycle, activity, filter) {
598
- if (pinned || ordersExplicitly(filter)) return void 0;
599
- return sectionOf$1(lifecycle, activity) === "done" ? "completed" : "active";
600
- }
601
- /** A collection heading's count: what still needs attention, and the members drawn in Pinned. */
602
- function collectionCountLabel(active, pinned) {
603
- const label = `${active} active`;
604
- return pinned === 0 ? label : `${label} · ${pinned} pinned`;
605
- }
606
- /**
607
- * The quiet line on a card naming its collections, or absent when it would say nothing new.
608
- *
609
- * Inside a collection the heading already names that one, so the line says only where else the
610
- * review is ("Also in Release 0.82"), and nothing for a review in one collection. Outside every
611
- * collection heading, which is Pinned and the Queue filter, it names them all.
612
- */
613
- function collectionsLine(memberOf, collections, current) {
614
- const names = collections.filter((collection) => collection.id !== current && memberOf.includes(collection.id)).map((collection) => collection.name);
615
- if (names.length === 0) return void 0;
616
- return `${current === void 0 ? "In" : "Also in"} ${names.join(", ")}`;
617
- }
618
- /**
619
- * What a collection says when its main list is empty, or absent when it is not. A collection is
620
- * never hidden for having nothing active: it is still somewhere its finished reviews live, and
621
- * somewhere to put the next one.
622
- */
623
- function collectionEmptyText(counts, filtered) {
624
- if (counts.active > 0) return void 0;
625
- if (filtered) return "Nothing in this collection matches the filters.";
626
- if (counts.members === 0) return "Nothing here yet. Add reviews from a card’s menu, or select several and choose Manage collections.";
627
- if (counts.pinned > 0 && counts.completed === 0) return "Its reviews are pinned, so they are listed under Pinned.";
628
- return "No active reviews";
629
- }
630
- /** Said under a collection whose oldest members are not drawn, so it never looks smaller than it is. */
631
- function omittedText(omitted) {
632
- return omitted === 1 ? "1 older review in this collection is not listed." : `${omitted} older reviews in this collection are not listed.`;
633
- }
634
- /** The disclosure holding a collection's Done reviews. */
635
- function completedLabel(completed) {
636
- return `Show completed (${completed})`;
637
- }
638
- /**
639
- * Whether a filter replaces the activity order with one the reviewer set. Queue is the only one:
640
- * the list is the queue, in its order.
641
- * `orderIndex` and `indexSectionOf` both ask this, so the ordering and the placement cannot disagree.
642
- */
643
- function ordersExplicitly(filter) {
644
- return filter === "queue";
645
- }
646
- /**
647
- * "45 reviews · 13 branches", and "13 reviews" when those are the same thirteen things.
648
- *
649
- * The two numbers are different questions and only both worth asking once they differ. A page of
650
- * thirteen branches reviewed once each would read "13 reviews · 13 branches", which invites the
651
- * reader to work out what the distinction is on the one page where there isn't one.
652
- *
653
- * `of` is every review on the page, given while the search or a filter is narrowing the list:
654
- * "12 of 71 reviews" says the list is filtered and by how much, where "12 reviews" reads as all
655
- * there are.
656
- */
657
- function countLabel(reviews, branches, of) {
658
- const label = of === void 0 ? `${reviews} review${reviews === 1 ? "" : "s"}` : `${reviews} of ${of} review${of === 1 ? "" : "s"}`;
659
- if (reviews === branches) return label;
660
- return `${label} · ${branches} branch${branches === 1 ? "" : "es"}`;
661
- }
662
- /**
663
- * Every login the author dropdown offers, once each, in alphabetical order of what the reader sees:
664
- * the name where there is one, the login otherwise, regardless of case. Read from the cards the page
665
- * is showing, so it never lists somebody with nothing to show.
666
- *
667
- * A login's name is the one on its newest review that has a name, chosen here by `createdAt` rather
668
- * than by the order the caller happens to pass, so an account renamed upstream reads as it does now.
669
- */
670
- function authorOptions(cards) {
671
- const newest = /* @__PURE__ */ new Map();
672
- for (const card of cards) {
673
- if (!card.handle) continue;
674
- const seen = newest.get(card.handle);
675
- if (!seen) newest.set(card.handle, {
676
- name: card.name || void 0,
677
- createdAt: card.createdAt
678
- });
679
- else if (card.name && (!seen.name || card.createdAt > seen.createdAt)) newest.set(card.handle, {
680
- name: card.name,
681
- createdAt: card.createdAt
682
- });
683
- }
684
- return [...newest].map(([handle, { name }]) => name && name !== handle ? {
685
- handle,
686
- name
687
- } : { handle }).sort((a, b) => (a.name ?? a.handle).localeCompare(b.name ?? b.handle, void 0, { sensitivity: "base" }));
688
- }
689
- /** An author option as the dropdown spells it: the name and the login, or the login alone. */
690
- function authorOptionLabel(option) {
691
- return option.name ? `${option.name} · @${option.handle}` : `@${option.handle}`;
692
- }
693
- /**
694
- * A card menu's status item, which sets the state it names. The bulk bar's words for the same two
695
- * actions, so a card and a selection never disagree. Here rather than in the page renderer because
696
- * the browser relabels the item after a change, and that renderer reaches the store.
697
- */
698
- function markItemLabel(mark) {
699
- return mark === "complete" ? "Mark reviewed" : "Mark not reviewed";
700
- }
701
- /**
702
- * The review page's Pin item, which says what pressing it will do. The words of the index's star,
703
- * so pinning from either place reads as the same act. Here because the browser relabels the item
704
- * after the change, for the reason `markItemLabel` is here.
705
- */
706
- function pinItemLabel(pinned) {
707
- return pinned ? "Unpin review" : "Pin review";
708
- }
709
- /**
710
- * The queue item, on a card and on the review's own page. "review queue" rather than "queue",
711
- * because a menu is where somebody meets the idea for the first time. Here because the browser
712
- * relabels it after a change, on both pages.
713
- */
714
- function queueItemLabel(queued) {
715
- return queued ? "Remove from review queue" : "Add to review queue";
716
- }
717
- /**
718
- * The Queue pill, which carries a number only once there is one.
719
- *
720
- * "Queue 0" is a count of something that is not there, and reads as a fault. Bare "Queue" reads as
721
- * the place it is -- empty, and still the thing you would click to put something in it.
722
- */
723
- function queueOptionLabel(queued) {
724
- return queued === 0 ? "Queue" : `Queue (${queued})`;
725
- }
726
- /**
727
- * The words on the status chip.
728
- *
729
- * Here rather than in the renderer because the browser now writes this chip too: marking a
730
- * selection reviewed repaints it without asking the server for the page again, and two copies of
731
- * these three words is how a page ends up saying "Reviewed" in one place and "Complete" in the
732
- * other about the same review.
733
- */
734
- function statusLabel(status) {
735
- if (status === "complete") return "Reviewed";
736
- return status === "in-progress" ? "Not reviewed" : "New";
737
- }
738
- /**
739
- * The status pill's accessible name: where the reviewer is, then what pressing it does. The visible
740
- * words are the state, so a screen reader hears both rather than only a state it cannot act on.
741
- */
742
- function statusPillLabel(status) {
743
- return `${statusLabel(status)}. Mark as ${status === "complete" ? "not reviewed" : "reviewed"}`;
744
- }
745
- /**
746
- * The bulk bar's way to tick everything the filters show, across pages.
747
- *
748
- * Counted ("Select all 7") only while the headline is not already saying "of 7", so the number is
749
- * on the bar once. With PR set to Merged this is how a reviewer clears the merged reviews in one go,
750
- * by way of the existing Mark reviewed.
751
- */
752
- function selectAllLabel(inView, scopeShown = false) {
753
- return inView === 0 || scopeShown ? "Select all" : `Select all ${inView}`;
754
- }
755
- /** The PR filter's options, in order, as the select shows them. */
756
- var PR_FILTER_OPTIONS = [
757
- {
758
- value: "any",
759
- label: "Any"
760
- },
761
- {
762
- value: "open",
763
- label: "Open"
764
- },
765
- {
766
- value: "merged",
767
- label: "Merged"
768
- },
769
- {
770
- value: "closed",
771
- label: "Closed"
772
- }
773
- ];
774
- /**
775
- * Whether any of that is work still going on, which is what sets the polling cadence.
776
- *
777
- * Here rather than beside `analysisStates` because this module is the DOM-free, filesystem-free
778
- * arithmetic both halves of the index share, and the renderer needs this one answer without
779
- * needing an import that reads the jobs directory to get it.
780
- *
781
- * A stopped analysis is a state, not an activity. Counting it as live would leave the index asking
782
- * once a second about a review that finished failing an hour ago, for as long as the tab stayed
783
- * open.
784
- */
785
- function anythingRunning(states, operations = []) {
786
- if (operations.length > 0) return true;
787
- for (const state of states.values()) if (state.phase !== "failed") return true;
788
- return false;
789
- }
790
- //#endregion
791
480
  //#region ../../packages/review-harness/src/workspace/fetch_updates_view.ts
792
481
  /** A card's and a review page's item. Named for what it reaches: a remote branch, not the list. */
793
482
  var FETCH_REVIEW_LABEL = "Fetch remote updates";
@@ -1033,7 +722,7 @@ function statusPillHtml(status, attributes = "") {
1033
722
  */
1034
723
  function lifecycleControlHtml(lifecycle, activity, options) {
1035
724
  const state = lifecycle?.state ?? "active";
1036
- const name = escapeHtml$1(lifecycleName$1(lifecycle));
725
+ const name = escapeHtml$1(lifecycleName(lifecycle));
1037
726
  const attributes = options.attributes ?? "";
1038
727
  if (!options.interactive) return `<span class="prb-state-pill prb-lifecycle-pill" data-lifecycle-pill="${state}"${attributes}>${name}</span>`;
1039
728
  const action = lifecyclePillAction(lifecycle);
@@ -5015,13 +4704,12 @@ ${renderDecisions(processed)}
5015
4704
  ${processed.all.length === 0 ? "<p class=\"prb-pr-empty\">This pull request has no comments yet.</p>" : buckets}`;
5016
4705
  }
5017
4706
  /**
5018
- * Re-reading the threads, and what the last re-read found.
4707
+ * Re-checking the discussion, and what the last re-read found.
5019
4708
  *
5020
- * The narrowest useful action in the product, and the copy has to keep it narrow. This corrects
5021
- * what the review already claims about other people's threads — who raised one, whether it is
5022
- * still open — and it cannot produce a finding, because deciding whether a new comment changes
5023
- * anything is judgement and judgement is a fresh review. A reviewer who pressed this expecting a
5024
- * re-review would be worse off than one who never saw the button.
4709
+ * Two things, said in the order they happen: the threads are fetched and every "already raised"
4710
+ * note corrected, then the findings are reassessed against whatever in the discussion is new to
4711
+ * them. The copy keeps it narrow in the one way that still matters: the code is the commit the
4712
+ * review read, so this is not a re-review and a comment claiming a fix cannot settle it here.
5025
4713
  */
5026
4714
  function renderRefresh(refresh) {
5027
4715
  if (!refresh?.enabled) return "";
@@ -5029,10 +4717,12 @@ function renderRefresh(refresh) {
5029
4717
  return `<div class="prb-pr-refresh">
5030
4718
  <button type="button" data-pr-recheck>Re-check the conversation</button>
5031
4719
  <span class="prb-pr-refresh-note">
5032
- Fetches every thread again and updates what this review says about them. If someone has
5033
- replied or resolved a thread since the review ran, the "Already raised" notes on the Issues
5034
- view will be corrected to match, and any note pointing at a thread that has been deleted will
5035
- be removed. Your code is not looked at again, so no findings are added, changed or removed.
4720
+ Fetches the description and every thread again, corrects the "Already raised" notes, then
4721
+ reassesses the issues and questions against anything new. A reply can answer a question,
4722
+ resolve or reopen an issue, or suggest a different severity, and a concern it raises becomes a
4723
+ new issue only once it is confirmed in the reviewed code. A comment saying something is fixed
4724
+ is not taken on trust: the issue stays open until an update sees the fix in the code. Newer
4725
+ commits are left for Update review.
5036
4726
  </span>
5037
4727
  ${last ? `<p class="prb-pr-refresh-last" data-ok="${last.ok}">
5038
4728
  Last re-checked ${escapeHtml$1(relativeTime$1(last.at))}. ${escapeHtml$1(last.message)}
@@ -6050,6 +5740,7 @@ function renderFreshAlternative(command) {
6050
5740
  */
6051
5741
  function renderUpdateOutcome(outcome) {
6052
5742
  if (!outcome) return "";
5743
+ if (outcome.scope === "conversation") return renderRecheckOutcome(outcome);
6053
5744
  const state = outcome.ok ? "done" : outcome.stop === "refused" ? "refused" : outcome.stop === "cancelled" ? "cancelled" : "stopped";
6054
5745
  const title = outcome.ok ? "Review updated" : outcome.stop === "refused" ? "There was nothing to update against" : outcome.stop === "cancelled" ? "Update cancelled" : "The review was not updated";
6055
5746
  const facts = outcome.ok ? updateFacts(outcome) : null;
@@ -6062,6 +5753,72 @@ function renderUpdateOutcome(outcome) {
6062
5753
  </div>`;
6063
5754
  }
6064
5755
  /**
5756
+ * What a Re-check of the PR discussion did, in the same notice an update reports through.
5757
+ *
5758
+ * Five outcomes, each with its own headline, because the one the reviewer most needs to tell apart
5759
+ * is the partial: the discussion came in, so the "already raised" lines are current, but the
5760
+ * findings were not judged against it. That one says why and, when trying again could help, offers it.
5761
+ */
5762
+ function renderRecheckOutcome(outcome) {
5763
+ const retry = outcome.stop === "analysis-failed" && outcome.conversationOk === true;
5764
+ const { state, title, body } = recheckWords(outcome);
5765
+ const facts = outcome.ok && outcome.stop === null ? recheckFacts(outcome) : null;
5766
+ return `<div class="prb-update-result" data-state="${state}" data-update-result data-recheck-result role="status">
5767
+ <div class="prb-update-result-body">
5768
+ ${facts ? `<p class="prb-update-facts"><strong>${escapeHtml$1(title)}</strong>${facts.map((fact) => `<span class="prb-update-fact">${escapeHtml$1(fact)}</span>`).join("")}</p>` : `<p><strong>${escapeHtml$1(title)}</strong> ${escapeHtml$1(body)}</p>`}
5769
+ ${retry ? "<p class=\"prb-update-footnote\"><button type=\"button\" class=\"prb-fresh-quiet\" data-pr-recheck>Try again</button></p>" : ""}
5770
+ </div>
5771
+ <button type="button" class="prb-fresh-dismiss" data-update-close>Close</button>
5772
+ </div>`;
5773
+ }
5774
+ function recheckWords(outcome) {
5775
+ if (outcome.stop === "cancelled") return {
5776
+ state: "cancelled",
5777
+ title: "Re-check cancelled",
5778
+ body: "The findings and the discussion shown are exactly as they were."
5779
+ };
5780
+ if (outcome.conversationOk !== true) return {
5781
+ state: "refused",
5782
+ title: "The PR discussion was not re-checked",
5783
+ body: outcome.message
5784
+ };
5785
+ if (outcome.stop === "already-current") return {
5786
+ state: "done",
5787
+ title: "No findings changed",
5788
+ body: outcome.message
5789
+ };
5790
+ if (outcome.stop !== null) return {
5791
+ state: "stopped",
5792
+ title: "Discussion refreshed, findings not reassessed",
5793
+ body: `The latest comments are in, and the notes about who raised what are current. ${outcome.message}`
5794
+ };
5795
+ if (outcome.counts && !findingsChanged(outcome.counts)) return {
5796
+ state: "done",
5797
+ title: "No findings changed",
5798
+ body: "The new comments were read against every open issue and question, and each one stands as it was."
5799
+ };
5800
+ return {
5801
+ state: "done",
5802
+ title: "Findings reassessed against the PR discussion",
5803
+ body: outcome.message
5804
+ };
5805
+ }
5806
+ /** What a Re-check changed, largest news first, in the words an update uses. No commit: none moved. */
5807
+ function recheckFacts(outcome) {
5808
+ const counts = outcome.counts;
5809
+ if (!counts || !findingsChanged(counts)) return null;
5810
+ const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
5811
+ return [
5812
+ counts.questionsAnswered ? `${plural(counts.questionsAnswered, "question", "questions")} answered` : "",
5813
+ counts.resolved ? `${plural(counts.resolved, "issue", "issues")} resolved by the discussion` : "",
5814
+ counts.reopened ? `${plural(counts.reopened, "issue", "issues")} reopened` : "",
5815
+ counts.affected ? `${counts.affected} to look at again` : "",
5816
+ counts.newIssues ? plural(counts.newIssues, "new issue", "new issues") : "",
5817
+ counts.gradeChanges ? `${plural(counts.gradeChanges, "grade change", "grade changes")} suggested` : "",
5818
+ counts.decisionsChallenged ? `${plural(counts.decisionsChallenged, "accepted answer", "accepted answers")} put back to you` : ""
5819
+ ].filter(Boolean);
5820
+ }
5821
+ /**
6065
5822
  * A successful update as a row of facts: the commit it reached, then what changed, largest news first.
6066
5823
  *
6067
5824
  * Read at a glance rather than parsed. The sentence it replaces ("Updated to 7bc233f: 2 fixed,
@@ -6446,14 +6203,14 @@ function comparisonNote(code, hasPullRequest) {
6446
6203
  * button is the subject of the sentence now, so there is one name for one thing and the reviewer
6447
6204
  * presses the words they just read.
6448
6205
  *
6449
- * Safe to run from here for the reason it always was: re-reading threads can only correct the
6450
- * state of claims that already exist, never produce a finding.
6206
+ * It reads no new code: the findings are reassessed against the discussion at the reviewed commit,
6207
+ * and newer commits stay the Update button's.
6451
6208
  */
6452
6209
  function conversationNote(report, actions) {
6453
6210
  if (!report.conversation.attached) return "";
6454
6211
  const when = escapeHtml$1(relativeTime$1(report.conversation.importedAt));
6455
6212
  if (!actions) return `The PR conversation was last checked ${when}.`;
6456
- return `<button type="button" class="prb-fresh-quiet" data-pr-recheck>Re-check the conversation</button> to refresh comments and resolved threads without re-running the code review. Last checked ${when}.`;
6213
+ return `<button type="button" class="prb-fresh-quiet" data-pr-recheck>Re-check the conversation</button> to fetch new comments and reassess issues and questions against them, at the reviewed commit. Last checked ${when}.`;
6457
6214
  }
6458
6215
  //#endregion
6459
6216
  //#region ../../packages/review-harness/src/workspace/progress_view.ts
@@ -6551,7 +6308,7 @@ function renderProgressView(job, fileCount, cancellable = false, history) {
6551
6308
  * and can be carried on; an update has the previous review, which is why the two are worded apart.
6552
6309
  */
6553
6310
  function renderElapsed(job, cancel, history) {
6554
- const button = !cancel ? "" : cancel === "Cancel review" ? `<button type="button" class="prb-progress-cancel" data-review-cancel="${escapeHtml$1(job.id)}">Cancel review</button>` : `<button type="button" class="prb-progress-cancel" data-update-cancel="${escapeHtml$1(job.id)}">Cancel update</button>`;
6311
+ const button = !cancel ? "" : cancel === "Cancel review" ? `<button type="button" class="prb-progress-cancel" data-review-cancel="${escapeHtml$1(job.id)}">Cancel review</button>` : `<button type="button" class="prb-progress-cancel" data-update-cancel="${escapeHtml$1(job.id)}">${cancel}</button>`;
6555
6312
  return `<div class="prb-progress-foot">
6556
6313
  <div class="prb-progress-runtime">
6557
6314
  <p class="prb-progress-elapsed" data-started-at="${job.startedAt}">${escapeHtml$1(runningFor(job.startedAt))}</p>${renderHistory$1(history)}
@@ -6599,11 +6356,14 @@ function renderHistory$1(history) {
6599
6356
  */
6600
6357
  function renderUpdateProgress(job, cancellable = false, history) {
6601
6358
  if (!isLiveJob(job)) return "";
6602
- return `<div class="prb-update-progress" data-update-phase="${escapeHtml$1(job.phase)}" role="status" aria-live="polite">
6603
- <h3 class="prb-doc-title"><span class="prb-spinner" aria-hidden="true"></span>Updating this review</h3>
6604
- <p class="prb-progress-lead">The previous review will be replaced when reassessment finishes.</p>
6359
+ const recheck = job.scope === "conversation";
6360
+ const title = recheck ? "Re-checking the PR discussion" : "Updating this review";
6361
+ const lead = recheck ? "Issues and questions will be updated from the discussion when reassessment finishes." : "The previous review will be replaced when reassessment finishes.";
6362
+ return `<div class="prb-update-progress" data-update-phase="${escapeHtml$1(job.phase)}"${recheck ? " data-update-scope=\"conversation\"" : ""} role="status" aria-live="polite">
6363
+ <h3 class="prb-doc-title"><span class="prb-spinner" aria-hidden="true"></span>${title}</h3>
6364
+ <p class="prb-progress-lead">${lead}</p>
6605
6365
  ${renderSteps(job)}
6606
- ${renderElapsed(job, cancellable ? "Cancel update" : null, history)}
6366
+ ${renderElapsed(job, cancellable ? recheck ? "Cancel re-check" : "Cancel update" : null, history)}
6607
6367
  </div>`;
6608
6368
  }
6609
6369
  //#endregion
@@ -7009,7 +6769,7 @@ function renderAttributionAge(view) {
7009
6769
  return `<div class="prb-discussion-age">
7010
6770
  <details class="prb-discussion-age-detail">
7011
6771
  <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>
6772
+ <p class="prb-discussion-age-body">The issues and questions below, their &ldquo;already raised&rdquo; notes and whether each thread was still open describe 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, which also reassesses the issues and questions against it.</p>
7013
6773
  </details>
7014
6774
  <span class="prb-discussion-age-sep" aria-hidden="true">&middot;</span>
7015
6775
  <button type="button" class="prb-doc-note-link" data-view-nav="prcontext">Re-check in PR context</button>
@@ -10324,7 +10084,8 @@ function renderReviewsPage(view) {
10324
10084
  const collectedHome = /* @__PURE__ */ new Set();
10325
10085
  for (const group of ordered) {
10326
10086
  const collected = (memberOf.get(group.key) ?? []).length > 0;
10327
- const home = homeSection(group.latest.pinned === true, group.latest.lifecycle, activityOf(group), collected, "all");
10087
+ const pinned = group.latest.pinned === true;
10088
+ const home = homeSection(pinned, group.latest.lifecycle, activityOf(group), collected, "all");
10328
10089
  if (home === void 0) collectedHome.add(group);
10329
10090
  sections[home ?? sectionOf$1(group.latest.lifecycle, activityOf(group))].push(group);
10330
10091
  }
@@ -12746,8 +12507,15 @@ feature: E</pre>
12746
12507
  matters and what is needed, with the original question and its files under <b>Technical
12747
12508
  context</b>. Any answer so far stays on the card.</p>
12748
12509
  <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>
12510
+ was last checked. The issues, the questions and their “already raised” notes describe the
12511
+ discussion as it was then; <b>Re-check in PR context</b> takes you to bring it up to date.</p>
12512
+ <p>Re-checking fetches the latest comments and reassesses every issue and question against
12513
+ anything new in them. A reply can answer a question, resolve or reopen an issue, or suggest a
12514
+ different severity or importance, and a concern raised in a comment becomes a new issue once it
12515
+ is confirmed in the reviewed code. A thread marked resolved, or a comment saying something is
12516
+ fixed, does not resolve an issue on its own: it stays open until an Update sees the fix in the
12517
+ code. Re-checking reads no new commits; Update review does. When the comments only change how
12518
+ much one finding matters, <b>Reassess</b> on its card asks about that one alone.</p>
12751
12519
  <p>Resolved and dismissed issues are in the closed list at the bottom. One the new commits fixed
12752
12520
  leads with what the code now does, when the update could see it.</p>`
12753
12521
  },
@@ -12760,7 +12528,7 @@ feature: E</pre>
12760
12528
  <li><b>Update</b> brings the review up to date with commits added since it was made. Its
12761
12529
  findings are checked against the new commits rather than thrown away, and each says whether
12762
12530
  the new commits affected it. In the workspace, Update review appears on a review whose
12763
- branch has moved on.</li>
12531
+ branch has moved on. It also reads the pull request discussion, as Re-check does.</li>
12764
12532
  <li><b>Reanalyse</b> analyses the same commit again, for a fresh judgement of the same change,
12765
12533
  for example with a different model.</li>
12766
12534
  <li><b>Fresh review</b> reviews the target again as a new review, and keeps the existing one
@@ -12825,7 +12593,14 @@ feature: E</pre>
12825
12593
  a time unless <code>--concurrency</code> says otherwise.</p>
12826
12594
  <p>One that fails does not stop the others. Ctrl-C stops them all, and running the same command
12827
12595
  again carries each one on from where it got to. Add <code>--queue</code> to put each review on
12828
- your review queue as soon as it exists, rather than when it is ready.</p>`
12596
+ your review queue as soon as it exists, rather than when it is ready.</p>
12597
+ <p>To keep a batch together, add it to a collection: <code>--collection "Release review"</code>
12598
+ for one you have, or <code>--new-collection "Friday batch"</code> to make one, with
12599
+ <code>--also-queue</code> to put the reviews on your review queue as well. In New review, choose
12600
+ the same from <b>Add to</b>: <b>Reviews list only</b>, <b>Review queue</b>, a collection, or
12601
+ <b>Create collection…</b>, and tick <b>Also add to review queue</b> for both. When the batch
12602
+ ends, <b>Open collection</b> shows it. A retry adds only what is missing, to the same
12603
+ collection.</p>`
12829
12604
  },
12830
12605
  {
12831
12606
  id: "review-lenses",
@@ -13118,50 +12893,6 @@ function renderNotificationRow(notification, gone) {
13118
12893
  </li>`;
13119
12894
  }
13120
12895
  //#endregion
13121
- //#region ../../packages/review-harness/src/workspace/collections_live.ts
13122
- /**
13123
- * Collections as they relate to reviews that actually exist.
13124
- *
13125
- * `collections.ts` stores lineage keys and must not learn to read the review store, for the cycle
13126
- * `queue_live.ts` describes: the prune in `store.ts` imports it. Anything that asks both questions
13127
- * lives here, which imports both and is imported by neither.
13128
- */
13129
- /**
13130
- * Drop members whose lineage has no review any more, deleted elsewhere or pruned by age, and store
13131
- * the result if that changed anything. Read against every group, not the windowed list, so a branch
13132
- * the recency window happens to hide keeps its place.
13133
- *
13134
- * Compacted again under the lock rather than written from what was read, so a member somebody added
13135
- * since is kept. On a failed write the compacted copy still stands in, so the page draws what exists.
13136
- */
13137
- function settleCollections(groups) {
13138
- const live = new Set(groups.map((group) => group.key));
13139
- const compact = (collections) => collections.some((collection) => collection.members.some((key) => !live.has(key))) ? collections.map((collection) => ({
13140
- ...collection,
13141
- members: collection.members.filter((key) => live.has(key))
13142
- })) : null;
13143
- const read = readCollections();
13144
- const compacted = compact(read);
13145
- if (compacted === null) return read;
13146
- return updateCollections((current) => compact(current)) ?? compacted;
13147
- }
13148
- /**
13149
- * The lineage each review id belongs to, for the routes: the page sends the ids on its cards, and
13150
- * the lineage is the server's to derive. An id with no review, or one that cannot be summarised, is
13151
- * left out, and the caller says so.
13152
- */
13153
- function lineageKeysFor(ids) {
13154
- const keys = /* @__PURE__ */ new Map();
13155
- for (const id of new Set(ids)) {
13156
- const workspace = loadWorkspace(id);
13157
- if (!workspace) continue;
13158
- try {
13159
- keys.set(id, lineageKey(summarise(workspace)));
13160
- } catch {}
13161
- }
13162
- return keys;
13163
- }
13164
- //#endregion
13165
12896
  //#region ../../packages/review-harness/src/workspace/working_folders.ts
13166
12897
  /**
13167
12898
  * The local Git working folders PR Review Buddy can find right now.
@@ -13370,7 +13101,13 @@ function realAddReviewDeps() {
13370
13101
  } : null;
13371
13102
  },
13372
13103
  agentName: agentNameOf,
13373
- home: homedir()
13104
+ home: homedir(),
13105
+ collections: () => readCollections().map((collection) => ({
13106
+ id: collection.id,
13107
+ name: collection.name
13108
+ })),
13109
+ openDestination,
13110
+ deliver
13374
13111
  };
13375
13112
  }
13376
13113
  function modelHelpOf(agentId) {
@@ -13511,7 +13248,8 @@ async function addReviewOptions(deps) {
13511
13248
  kind: "refused",
13512
13249
  error: unresolvedAgent(resolution)
13513
13250
  },
13514
- canChooseFolder: deps.chooseFolder !== null
13251
+ canChooseFolder: deps.chooseFolder !== null,
13252
+ collections: deps.collections()
13515
13253
  }
13516
13254
  };
13517
13255
  }
@@ -13625,12 +13363,13 @@ function pendingBatchRows() {
13625
13363
  function forgetFinished(now) {
13626
13364
  for (const [id, batch] of batches) if (batch.finishedAt !== null && now - Math.max(batch.finishedAt, batch.seenAt) > FINISHED_BATCH_TTL_MS) batches.delete(id);
13627
13365
  }
13628
- function newBatch(rows) {
13366
+ function newBatch(rows, destination = { queue: false }) {
13629
13367
  forgetFinished(Date.now());
13630
13368
  const now = Date.now();
13631
13369
  const batch = {
13632
13370
  id: randomUUID(),
13633
13371
  rows,
13372
+ destination,
13634
13373
  finishedAt: now,
13635
13374
  seenAt: now,
13636
13375
  running: 0,
@@ -13657,7 +13396,14 @@ function fail(row, error) {
13657
13396
  row.reason = refusal.error;
13658
13397
  if (refusal.detail) row.detail = refusal.detail;
13659
13398
  }
13660
- var rowsOf = (batch) => batch.rows.map((row) => ({ ...row }));
13399
+ var rowsOf = (batch) => batch.rows.map((row) => ({
13400
+ ...row,
13401
+ ...missToRetry(row, batch) ? { addAgain: true } : {}
13402
+ }));
13403
+ /** What of the batch's destination this row's review is still worth adding to, or null. */
13404
+ function missToRetry(row, batch) {
13405
+ return row.reviewId && row.delivered ? retryableMiss(row.delivered, batch.destination) : null;
13406
+ }
13661
13407
  /**
13662
13408
  * Create the review for a row's target, or find the one it has, under the per-target lock. The
13663
13409
  * caller holds an analysis slot: nothing is created without one, so nothing created waits.
@@ -13717,6 +13463,7 @@ async function stillThere(row, target, what, deps) {
13717
13463
  */
13718
13464
  async function retryReview(row, batch, deps) {
13719
13465
  const reviewId = row.reviewId;
13466
+ deliverRow(row, batch, deps);
13720
13467
  const kept = deps.resumeOf(reviewId);
13721
13468
  begin(row);
13722
13469
  if (kept) {
@@ -13730,6 +13477,14 @@ async function retryReview(row, batch, deps) {
13730
13477
  * target, the old review, and still `stale`. So the row keeps offering the fresh start, and a plain
13731
13478
  * Retry finds the old commit gone again, rather than resuming the old review against it.
13732
13479
  */
13480
+ /**
13481
+ * Put a row's review where its batch goes, as soon as it has one. Idempotent, so it runs again on
13482
+ * every retry and fills in only what is missing. A batch for the reviews list alone does nothing.
13483
+ */
13484
+ function deliverRow(row, batch, deps) {
13485
+ if (!row.reviewId || !batch.destination.queue && !batch.destination.collection) return;
13486
+ row.delivered = deps.deliver(row.reviewId, batch.destination);
13487
+ }
13733
13488
  function keepFreshPending(row, replaced) {
13734
13489
  if (row.status === "failed" && !replaced) row.stale = true;
13735
13490
  }
@@ -13768,13 +13523,18 @@ async function startPullRequestBatch(body, deps) {
13768
13523
  }
13769
13524
  const model = modelOf(body?.model);
13770
13525
  if (isAnswer(model)) return model;
13526
+ const requested = destinationRequestOf(body?.destination);
13527
+ if (isAnswer(requested)) return requested;
13771
13528
  const agent = await agentFor(body?.agent, deps);
13772
13529
  if (isAnswer(agent)) return agent;
13530
+ const opened = deps.openDestination(requested);
13531
+ if (!opened.ok) return destinationRefusal(opened, deps);
13532
+ const destination = opened.destination;
13773
13533
  const batch = newBatch(items.map((item) => ({
13774
13534
  key: item.key,
13775
13535
  label: `${item.owner}/${item.repo} #${item.number}`,
13776
13536
  status: "waiting"
13777
- })));
13537
+ })), destination);
13778
13538
  batch.rows.forEach((row, index) => noteRow(row, {
13779
13539
  repoPath: items[index].folder,
13780
13540
  prNumber: items[index].number
@@ -13813,6 +13573,7 @@ async function startPullRequestBatch(body, deps) {
13813
13573
  }
13814
13574
  claimed.set(identity, index);
13815
13575
  const prepared = await prepareRow(row, target, agent.agentId, model, deps, fresh);
13576
+ deliverRow(row, batch, deps);
13816
13577
  if (fresh) {
13817
13578
  resolved[index] = target;
13818
13579
  replaced = true;
@@ -13834,15 +13595,79 @@ async function startPullRequestBatch(body, deps) {
13834
13595
  status: 200,
13835
13596
  body: {
13836
13597
  batchId: batch.id,
13837
- rows: rowsOf(batch)
13598
+ rows: rowsOf(batch),
13599
+ destination
13838
13600
  }
13839
13601
  };
13840
13602
  }
13841
13603
  /**
13604
+ * What the panel's Add to asked for. A collection it picked comes as an id and is matched as one,
13605
+ * never as a name; one it is to make comes as a name. Nothing, or the reviews list, is no
13606
+ * destination at all.
13607
+ */
13608
+ function destinationRequestOf(raw) {
13609
+ const choose = () => refuse(400, "Choose where the reviews go from Add to.", { field: "destination" });
13610
+ if (raw === void 0 || raw === null) return { queue: false };
13611
+ if (typeof raw !== "object") return choose();
13612
+ const value = raw;
13613
+ const queue = value.queue === true;
13614
+ switch (value.kind) {
13615
+ case "list": return { queue: false };
13616
+ case "queue": return { queue: true };
13617
+ case "collection": return typeof value.id === "string" && value.id !== "" ? {
13618
+ collectionId: value.id,
13619
+ queue
13620
+ } : choose();
13621
+ case "new-collection": return typeof value.name === "string" ? {
13622
+ newCollection: value.name,
13623
+ queue
13624
+ } : choose();
13625
+ default: return choose();
13626
+ }
13627
+ }
13628
+ /** A destination that could not be opened, said beside Add to, with the collections as they are now. */
13629
+ function destinationRefusal(refusal, deps) {
13630
+ const extra = {
13631
+ field: "destination",
13632
+ collections: deps.collections()
13633
+ };
13634
+ switch (refusal.reason) {
13635
+ case "missing": return refuse(409, "That collection no longer exists. Choose again from Add to. Nothing was started.", extra);
13636
+ case "empty":
13637
+ case "too-long":
13638
+ case "duplicate": return refuse(400, `${nameProblemText(refusal.reason)} Nothing was started.`, extra);
13639
+ case "full": return refuse(409, `You have 100 collections, which is as many as there can be. Delete one, or add these to one you have. Nothing was started.`, extra);
13640
+ default: return refuse(500, "The collection could not be saved. Nothing was started. Try again.", extra);
13641
+ }
13642
+ }
13643
+ /**
13644
+ * The batch's destination as the panel shows it, under the collection's current name: a rename
13645
+ * keeps the id, so the batch still delivers to it, and the result should call it what it is now.
13646
+ */
13647
+ function destinationNow(batch, deps) {
13648
+ const collection = batch.destination.collection;
13649
+ if (!collection || !deps) return batch.destination;
13650
+ const current = deps.collections().find((one) => one.id === collection.id);
13651
+ if (!current) return {
13652
+ ...batch.destination,
13653
+ collection: {
13654
+ ...collection,
13655
+ deleted: true
13656
+ }
13657
+ };
13658
+ return current.name !== collection.name ? {
13659
+ ...batch.destination,
13660
+ collection: {
13661
+ ...collection,
13662
+ name: current.name
13663
+ }
13664
+ } : batch.destination;
13665
+ }
13666
+ /**
13842
13667
  * A batch's rows as they stand. Asking counts as watching: a batch an open panel is still asking
13843
13668
  * about is never forgotten, so its Retry stays good for as long as the panel is open.
13844
13669
  */
13845
- function batchRows(id) {
13670
+ function batchRows(id, deps) {
13846
13671
  const batch = batches.get(id);
13847
13672
  if (!batch) return lostBatch();
13848
13673
  batch.seenAt = Date.now();
@@ -13850,7 +13675,34 @@ function batchRows(id) {
13850
13675
  status: 200,
13851
13676
  body: {
13852
13677
  rows: rowsOf(batch),
13853
- finished: batch.finishedAt !== null
13678
+ finished: batch.finishedAt !== null,
13679
+ destination: destinationNow(batch, deps)
13680
+ }
13681
+ };
13682
+ }
13683
+ /**
13684
+ * Put one row's review where the batch goes again, and nothing else: for a review that exists but
13685
+ * missed its collection or the queue (a full queue, a write that failed). The review is not run
13686
+ * again. A row with no review yet is Retry's business, which delivers once the review exists.
13687
+ */
13688
+ function deliverBatchRow(id, key, deps) {
13689
+ const batch = batches.get(id);
13690
+ if (!batch) return lostBatch();
13691
+ const row = batch.rows.find((one) => one.key === key);
13692
+ if (!row) return refuse(404, "That review is not part of this batch.");
13693
+ const again = missToRetry(row, batch);
13694
+ if (!again || !row.reviewId) return refuse(409, "Only a review that missed somewhere it can still be added can be added again.");
13695
+ row.delivered = {
13696
+ ...row.delivered,
13697
+ ...deps.deliver(row.reviewId, again)
13698
+ };
13699
+ batch.seenAt = Date.now();
13700
+ return {
13701
+ status: 200,
13702
+ body: {
13703
+ rows: rowsOf(batch),
13704
+ finished: batch.finishedAt !== null,
13705
+ destination: destinationNow(batch, deps)
13854
13706
  }
13855
13707
  };
13856
13708
  }
@@ -17867,7 +17719,7 @@ async function handleAddReview(req, res, pathname, options) {
17867
17719
  if (route === "options") return addReviewOptions(deps);
17868
17720
  if (route === "folders") return listFolders(deps);
17869
17721
  const batch = /^batches\/([0-9a-f-]{36})$/.exec(route);
17870
- if (batch) return batchRows(batch[1]);
17722
+ if (batch) return batchRows(batch[1], deps);
17871
17723
  return null;
17872
17724
  }
17873
17725
  if (req.method !== "POST") return {
@@ -17882,6 +17734,8 @@ async function handleAddReview(req, res, pathname, options) {
17882
17734
  if (route === "branch") return startBranchReview(body, deps);
17883
17735
  const retry = /^batches\/([0-9a-f-]{36})\/retry$/.exec(route);
17884
17736
  if (retry) return retryBatchRow(retry[1], body?.key, body?.fresh);
17737
+ const again = /^batches\/([0-9a-f-]{36})\/deliver$/.exec(route);
17738
+ if (again) return deliverBatchRow(again[1], body?.key, deps);
17885
17739
  return null;
17886
17740
  };
17887
17741
  const result = await answer();
@@ -18336,10 +18190,15 @@ async function sendWorkspacePage(res, workspace) {
18336
18190
  const updateJob = liveUpdateFor(workspace.id);
18337
18191
  const reviewJob = workspace.jobId ? loadJob(workspace.jobId) : void 0;
18338
18192
  const kissJob = kissRun ? loadJob(kissRun.jobId) : void 0;
18339
- const notice = workspace.lastUpdate?.acknowledged ? void 0 : workspace.lastUpdate;
18340
- if (notice) mutateWorkspace(workspace.id, (current) => {
18341
- if (current.lastUpdate?.at === notice.at) current.lastUpdate.acknowledged = true;
18342
- });
18193
+ const unseen = [workspace.lastUpdate, workspace.lastRecheck].filter((outcome) => outcome !== void 0 && !outcome.acknowledged);
18194
+ const notice = unseen.sort((a, b) => b.at - a.at)[0];
18195
+ if (notice) {
18196
+ const read = new Set(unseen.map((outcome) => outcome.at));
18197
+ mutateWorkspace(workspace.id, (current) => {
18198
+ if (current.lastUpdate && read.has(current.lastUpdate.at)) current.lastUpdate.acknowledged = true;
18199
+ if (current.lastRecheck && read.has(current.lastRecheck.at)) current.lastRecheck.acknowledged = true;
18200
+ });
18201
+ }
18343
18202
  const everyReview = recentWorkspaces(Number.MAX_SAFE_INTEGER);
18344
18203
  const html = renderWorkspace({
18345
18204
  session: workspace.session,
@@ -18568,33 +18427,78 @@ async function kissFreshnessOrNull(workspace) {
18568
18427
  }
18569
18428
  }
18570
18429
  /**
18571
- * Re-read the pull request conversation, at the reviewer's request.
18430
+ * Re-check the pull request discussion, at the reviewer's request: re-read it, then reconcile the
18431
+ * findings with whatever in it is new to them, at the commit the review read.
18572
18432
  *
18573
- * Answers 200 with `ok: false` rather than an error status when it does not work. The page's job
18574
- * here is to tell the reviewer what happened in a sentence, and a failed re-read is an outcome —
18575
- * the conversation on disk is untouched and the review is exactly as usable as it was a moment
18576
- * ago. An HTTP error would make the page choose between a stack trace and silence.
18433
+ * Answers 200 with `ok: false` rather than an error status when it does not work, for the reason the
18434
+ * update route does: a failure is an outcome the page has to explain in a sentence. 409 when an update
18435
+ * or another Re-check holds the review, which the reloaded page then shows running.
18436
+ *
18437
+ * Saved in two ways, because two different things can come of it. A reassessment that was applied
18438
+ * goes on the way an update does, onto the review as it is on disk. Anything short of that changed
18439
+ * only the conversation and the "already raised" lines, and goes on the way a re-read always has,
18440
+ * through `saveRefresh`, which will not put an older conversation over a newer one.
18577
18441
  */
18578
18442
  async function handleRefreshPr(res, workspace) {
18579
18443
  const reattributing = JSON.stringify(workspace.session.result);
18580
- adoptLegacyBaseline(workspace);
18581
- const startedAt = Date.now();
18444
+ const begun = Date.now();
18582
18445
  let outcome;
18583
18446
  try {
18584
- outcome = await refreshPrContext(workspace);
18585
- } catch {
18586
- outcome = unforeseenFailure(startedAt);
18447
+ outcome = await track(recheckConversation(workspace, { source: "workspace" }));
18448
+ } catch (error) {
18449
+ if (error instanceof UpdateAlreadyRunningError) return send(res, 409, { error: error.message });
18450
+ outcome = {
18451
+ at: Date.now(),
18452
+ ok: false,
18453
+ message: error instanceof RefreshUnavailableError || error instanceof ReviewBeingDeletedError ? error.message : "The findings could not be reassessed. Nothing about them has changed.",
18454
+ stop: "analysis-failed",
18455
+ fetchFailed: false,
18456
+ moved: false,
18457
+ ...rereadSince(workspace, begun) ? { conversationOk: workspace.lastPrRefresh.ok } : {},
18458
+ fromSha: null,
18459
+ toSha: null,
18460
+ scope: "conversation"
18461
+ };
18587
18462
  }
18588
- saveRefresh(workspace, outcome, reattributing);
18463
+ const applied = outcome.ok && outcome.stop === null;
18464
+ const cancelled = outcome.stop === "cancelled";
18465
+ workspace.lastRecheck = outcome;
18466
+ const reread = !applied && !cancelled ? rereadSince(workspace, begun) : void 0;
18467
+ if (reread) saveRefresh(workspace, reread, reattributing);
18468
+ const saved = mutateWorkspace(workspace.id, (fresh) => applied ? {
18469
+ ...mergeUpdateInto(fresh, workspace),
18470
+ lastRecheck: outcome
18471
+ } : {
18472
+ ...fresh,
18473
+ lastRecheck: outcome
18474
+ });
18475
+ const refresh = reread ?? (applied ? workspace.lastPrRefresh : void 0);
18589
18476
  record({
18590
18477
  event: "pr_conversation_rechecked",
18591
18478
  reviewId: workspace.id,
18592
- ok: outcome.ok,
18593
- changed: outcome.changes ? !isUnchanged(outcome.changes) : false,
18594
- attributionsRestated: outcome.attributionsRestated ?? 0,
18595
- attributionsWithdrawn: outcome.attributionsWithdrawn ?? 0
18479
+ ok: outcome.conversationOk === true,
18480
+ changed: refresh?.changes ? !isUnchanged(refresh.changes) : false,
18481
+ attributionsRestated: refresh?.attributionsRestated ?? 0,
18482
+ attributionsWithdrawn: refresh?.attributionsWithdrawn ?? 0,
18483
+ reassessment: reassessmentOf(outcome),
18484
+ resolved: outcome.counts?.resolved ?? 0,
18485
+ questionsAnswered: outcome.counts?.questionsAnswered ?? 0,
18486
+ newIssues: outcome.counts?.newIssues ?? 0
18596
18487
  });
18597
- send(res, 200, outcome);
18488
+ send(res, 200, saved?.lastRecheck ?? outcome);
18489
+ }
18490
+ /** The re-read this request made, if it made one: the copy's record, begun no earlier than the request. */
18491
+ function rereadSince(workspace, begun) {
18492
+ const refresh = workspace.lastPrRefresh;
18493
+ return refresh && (refresh.startedAt ?? refresh.at) >= begun ? refresh : void 0;
18494
+ }
18495
+ /** What a Re-check did to the findings, as the closed set the log records. */
18496
+ function reassessmentOf(outcome) {
18497
+ if (outcome.stop === "cancelled") return "cancelled";
18498
+ if (outcome.stop === "already-current") return "not-needed";
18499
+ if (outcome.stop === "analysis-failed") return "failed";
18500
+ if (outcome.stop !== null) return "skipped";
18501
+ return outcome.counts && findingsChanged(outcome.counts) ? "changed" : "unchanged";
18598
18502
  }
18599
18503
  /**
18600
18504
  * Bring the whole review up to date with the checkout, at the reviewer's request.