prreviewbuddy 0.93.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 versionFrom, $i as isExplanationAction, $n as hasAuthorContent, $r as latestKissRun, $t as waitingHistory, A as settleQueue, Aa as isCommitReview, Ai as deleteCollection, An as markRead, Ar as endedAt, At as askCheckout, B as humanizeModelId, Ba as clearAgentPreference, Bi as readQueue, Bn as unreadCount$1, Br as progressSteps, Bt as runJob, C as parsePullRequestLink, Ca as describeAuthorship, Ci as lifecyclePillLabel, Cn as categoryOf, Cr as reviewedShaOf, Ct as RefreshUnavailableError, D as activateQueued, Da as originRemote, Di as collectionsStamp, Dn as eventName, Dr as RUN_RECORD_RETENTION_MS, E as relativeTime$1, Ea as resolveTarget, Ei as sectionOf$1, En as createPrUpdate, Er as fillFileUrlTemplate, Et as mergeUpdateInto, Fa as git, Fi as updateCollections, Fn as readStateOf, Fr as saveJob, Ft as failureOf$1, G as reviewTag, Ga as AGENT_IDS, Gi as questionsOutstanding, Gn as saveRefresh, H as findingRef, Ha as writeAgentPreference, Hi as isIssueOutstanding, Hn as existingPaths, Ia as refExists, Ii as queueAdd, In as removeNotification, It as observeReview, Ja as agentIdentityOf, Ji as verdictClosure, Jn as agentEnvOf, Jr as groupByLineage, Jt as telemetryUploadConsent, K as reviewTagsIn, Ka as agentById, Ki as resolvedByConversation, Kn as storedConversation, Kr as decideKissRecommendation, L as modelProvenanceOf, La as resolveAgent, Li as queueAddMany, Ln as retireNotification, Lr as isLiveJob, Lt as prepareReview, Ma as AmbiguousRefError, Mi as readCollections, Mn as moveIds, Mr as isTerminal, Mt as UpdateAlreadyRunningError, N as cliEntry, Na as shortNameOf, Ni as renameCollection, Nn as pruneNotifications, Nr as loadJob, Nt as liveUpdateFor, O as compactQueue, Oa as DetachedHeadError, Oi as createCollection, On as listNotifications, Or as allJobs, Ot as recheckConversation, Pa as shorthandOf, Pi as setMembership, Pn as readNotification, Pr as pruneJobResidue, Pt as runningJobs, Q as markAskRunning, Qa as runnableAgents, Qi as explanationOf, Qn as authorPreview, Qr as kissDecisionsOf, Qt as feedbackUrl, R as provenanceDetailLines, Ra as resolveModel, Ri as queueRemove, Rn as toneOf, Rr as isTerminalJobPhase, Rt as settleReview, S as extractPullRequestLinks, Sa as objectWithAnyOf, Si as lifecyclePillAction, Sn as subjectOf, Sr as followedBranch, St as FETCH_TIMEOUT_MS, T as runBatch, Ta as kissOutcome, Ti as parseLifecycle, Tn as createPrState, Tr as forgeResolver, Tt as cancellableHere, U as findingRefs, Ui as isQuestionOutstanding, Un as ASK_REREAD_CEILING_MS, Ut as readEvents, V as findingAtRef, Va as readAgentPreference, Vi as doneVerb, Vn as withUsageRecorded, Vr as purposeOf, W as refOf, Wa as STORE_ROOT, Wi as issuesOutstanding, Wn as prepareConversationWithin, Wr as isClaimed, Wt as record, X as isRequestId, Xa as isAgentId, Xi as recommendationsOf, Xn as chatConversationSection, Xr as indexReviewGroups, Xt as BUILD_VERSION, Ya as detectAgents, Yi as processDiscussion, Yn as EXPLAIN_SIMPLY_PROMPT, Yr as indexKeptIds, Za as parkedReason, Zi as explanationActionOf, Zn as UNESTABLISHED_MOTIVATION, Zr as kissDecisionRefusal, _ as endRun, _a as wasReassessed, _i as activityLabel, _n as ancestorsOf, _t as parseWorkspacePort, a as lineageIds, aa as closesFinding, an as ARCHITECTURE_DIR, ar as renderInlineText, b as stopRun, ba as asString, bi as lifecycleItemLabel, bn as announceJob, br as checkCodeFreshness, bt as writePortPreference, c as discardJob, ca as findingIn, ci as recentWorkspaces, cn as partFiles, cr as isBusy, ct as serverEntryExists, d as startKissJob, da as isGradeFor, di as summarise, dn as storeArchitecture, dr as createWorktree, ea as withExplanation, ei as lineageKey, en as mapRuntimes, er as GITHUB_MARKDOWN, f as isDriving, fa as isOffered, fi as touchWorkspace, fn as storedScopes, fr as holdWorktree, g as completeRun, ga as statusLookup, gi as activityAttributes, gn as REPOSITORY_SCOPE, gr as requestJobStop, gt as MIN_WORKSPACE_PORT, h as beginRun, ha as sourcePhrase, hi as LIFECYCLES, hn as QUOTE_LIMITS, hr as removeWorktree, ht as MAX_WORKSPACE_PORT, i as deleteReview, ia as assessmentLookup, ii as mutateWorkspace, in as formatDuration, ir as escapeHtml$1, ja as resolveBase, ji as nameProblemText, jn as markReadForReview, jr as fail$1, jt as findingsChanged, ki as createWithMembers, kn as markAllRead, kr as attemptIdOf, kt as updateReview, l as kissAgentId, la as findingStatus, li as reviewedCommit, ln as readArchitecture, lr as withReviewLock, lt as spawnServer, m as RUN_KINDS, ma as reassessIn, mi as workspaceStamps, mn as ownersOf, mr as readMarker, mt as DEFAULT_WORKSPACE_PORT, n as MCP_TOOL_NAMES, na as NO_STATUS, ni as loadWorkspace, nn as RUNTIME_WORDS, no as AgentUnavailableError, nr as CODE_BLOCK_CLASS, o as liveJobsFor, oa as currentGradeIn, oi as presentedKissRunOf, on as checkArchitecture, ot as readServerRecord, p as track, pa as promptGrade, pi as workspaceRevision, pn as trackedFiles, pr as isWorktreeHeld, pt as writeServerRecord, q as endAskRequest, qa as agentFor$1, qi as reviewerDispositions, qn as isUnchanged, qr as followedRefName, qt as recordTelemetryUploadConsent, r as deleteLineage, ra as analysisRefOf, rn as describeRuntimes, rr as CODE_COPY_CLASS, rt as clearServerRecordOwnedBy, s as wasBlocked, sa as findingAssessment, si as previousKissResult, sn as loadArchitecture, sr as ReviewBeingDeletedError, ta as ANALYSIS_ONLY, ti as lineagePosition, tn as recordRuntimeBestEffort, to as AgentCancelledError, tr as renderMarkdown, tt as readIndexToken, ua as gradeScale, un as repositoryKey, ur as MANAGED_ROOT, ut as stopChild, v as heldRuns, va as followUpFindingId, vi as isLifecycle, vn as isArchitectureId, vt as resolveWorkspacePort, w as unsupportedReason, wa as WITHHELD_REASON, wi as offersLifecycle, wn as createArchitectureNotification, wr as fetchHead, wt as cancelUpdate, x as batchIdentity, xa as extractJsonText, xi as lifecycleName$1, xn as branchOf, xr as checkReview, y as reportRun, ya as SEVERITIES, yi as lifecycleChip, yn as writeJsonAtomically, yr as findExistingReview, yt as statedWorkspacePort, z as agentDisplayName, za as agentIdOf, zi as queueStamp, zn as unreadAround, zr as phasesFor, zt as reanalyseReview } from "./mcp_tools-DbFR9Z90.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);
@@ -10395,7 +10084,8 @@ function renderReviewsPage(view) {
10395
10084
  const collectedHome = /* @__PURE__ */ new Set();
10396
10085
  for (const group of ordered) {
10397
10086
  const collected = (memberOf.get(group.key) ?? []).length > 0;
10398
- 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");
10399
10089
  if (home === void 0) collectedHome.add(group);
10400
10090
  sections[home ?? sectionOf$1(group.latest.lifecycle, activityOf(group))].push(group);
10401
10091
  }
@@ -12903,7 +12593,14 @@ feature: E</pre>
12903
12593
  a time unless <code>--concurrency</code> says otherwise.</p>
12904
12594
  <p>One that fails does not stop the others. Ctrl-C stops them all, and running the same command
12905
12595
  again carries each one on from where it got to. Add <code>--queue</code> to put each review on
12906
- 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>`
12907
12604
  },
12908
12605
  {
12909
12606
  id: "review-lenses",
@@ -13196,50 +12893,6 @@ function renderNotificationRow(notification, gone) {
13196
12893
  </li>`;
13197
12894
  }
13198
12895
  //#endregion
13199
- //#region ../../packages/review-harness/src/workspace/collections_live.ts
13200
- /**
13201
- * Collections as they relate to reviews that actually exist.
13202
- *
13203
- * `collections.ts` stores lineage keys and must not learn to read the review store, for the cycle
13204
- * `queue_live.ts` describes: the prune in `store.ts` imports it. Anything that asks both questions
13205
- * lives here, which imports both and is imported by neither.
13206
- */
13207
- /**
13208
- * Drop members whose lineage has no review any more, deleted elsewhere or pruned by age, and store
13209
- * the result if that changed anything. Read against every group, not the windowed list, so a branch
13210
- * the recency window happens to hide keeps its place.
13211
- *
13212
- * Compacted again under the lock rather than written from what was read, so a member somebody added
13213
- * since is kept. On a failed write the compacted copy still stands in, so the page draws what exists.
13214
- */
13215
- function settleCollections(groups) {
13216
- const live = new Set(groups.map((group) => group.key));
13217
- const compact = (collections) => collections.some((collection) => collection.members.some((key) => !live.has(key))) ? collections.map((collection) => ({
13218
- ...collection,
13219
- members: collection.members.filter((key) => live.has(key))
13220
- })) : null;
13221
- const read = readCollections();
13222
- const compacted = compact(read);
13223
- if (compacted === null) return read;
13224
- return updateCollections((current) => compact(current)) ?? compacted;
13225
- }
13226
- /**
13227
- * The lineage each review id belongs to, for the routes: the page sends the ids on its cards, and
13228
- * the lineage is the server's to derive. An id with no review, or one that cannot be summarised, is
13229
- * left out, and the caller says so.
13230
- */
13231
- function lineageKeysFor(ids) {
13232
- const keys = /* @__PURE__ */ new Map();
13233
- for (const id of new Set(ids)) {
13234
- const workspace = loadWorkspace(id);
13235
- if (!workspace) continue;
13236
- try {
13237
- keys.set(id, lineageKey(summarise(workspace)));
13238
- } catch {}
13239
- }
13240
- return keys;
13241
- }
13242
- //#endregion
13243
12896
  //#region ../../packages/review-harness/src/workspace/working_folders.ts
13244
12897
  /**
13245
12898
  * The local Git working folders PR Review Buddy can find right now.
@@ -13448,7 +13101,13 @@ function realAddReviewDeps() {
13448
13101
  } : null;
13449
13102
  },
13450
13103
  agentName: agentNameOf,
13451
- home: homedir()
13104
+ home: homedir(),
13105
+ collections: () => readCollections().map((collection) => ({
13106
+ id: collection.id,
13107
+ name: collection.name
13108
+ })),
13109
+ openDestination,
13110
+ deliver
13452
13111
  };
13453
13112
  }
13454
13113
  function modelHelpOf(agentId) {
@@ -13589,7 +13248,8 @@ async function addReviewOptions(deps) {
13589
13248
  kind: "refused",
13590
13249
  error: unresolvedAgent(resolution)
13591
13250
  },
13592
- canChooseFolder: deps.chooseFolder !== null
13251
+ canChooseFolder: deps.chooseFolder !== null,
13252
+ collections: deps.collections()
13593
13253
  }
13594
13254
  };
13595
13255
  }
@@ -13703,12 +13363,13 @@ function pendingBatchRows() {
13703
13363
  function forgetFinished(now) {
13704
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);
13705
13365
  }
13706
- function newBatch(rows) {
13366
+ function newBatch(rows, destination = { queue: false }) {
13707
13367
  forgetFinished(Date.now());
13708
13368
  const now = Date.now();
13709
13369
  const batch = {
13710
13370
  id: randomUUID(),
13711
13371
  rows,
13372
+ destination,
13712
13373
  finishedAt: now,
13713
13374
  seenAt: now,
13714
13375
  running: 0,
@@ -13735,7 +13396,14 @@ function fail(row, error) {
13735
13396
  row.reason = refusal.error;
13736
13397
  if (refusal.detail) row.detail = refusal.detail;
13737
13398
  }
13738
- 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
+ }
13739
13407
  /**
13740
13408
  * Create the review for a row's target, or find the one it has, under the per-target lock. The
13741
13409
  * caller holds an analysis slot: nothing is created without one, so nothing created waits.
@@ -13795,6 +13463,7 @@ async function stillThere(row, target, what, deps) {
13795
13463
  */
13796
13464
  async function retryReview(row, batch, deps) {
13797
13465
  const reviewId = row.reviewId;
13466
+ deliverRow(row, batch, deps);
13798
13467
  const kept = deps.resumeOf(reviewId);
13799
13468
  begin(row);
13800
13469
  if (kept) {
@@ -13808,6 +13477,14 @@ async function retryReview(row, batch, deps) {
13808
13477
  * target, the old review, and still `stale`. So the row keeps offering the fresh start, and a plain
13809
13478
  * Retry finds the old commit gone again, rather than resuming the old review against it.
13810
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
+ }
13811
13488
  function keepFreshPending(row, replaced) {
13812
13489
  if (row.status === "failed" && !replaced) row.stale = true;
13813
13490
  }
@@ -13846,13 +13523,18 @@ async function startPullRequestBatch(body, deps) {
13846
13523
  }
13847
13524
  const model = modelOf(body?.model);
13848
13525
  if (isAnswer(model)) return model;
13526
+ const requested = destinationRequestOf(body?.destination);
13527
+ if (isAnswer(requested)) return requested;
13849
13528
  const agent = await agentFor(body?.agent, deps);
13850
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;
13851
13533
  const batch = newBatch(items.map((item) => ({
13852
13534
  key: item.key,
13853
13535
  label: `${item.owner}/${item.repo} #${item.number}`,
13854
13536
  status: "waiting"
13855
- })));
13537
+ })), destination);
13856
13538
  batch.rows.forEach((row, index) => noteRow(row, {
13857
13539
  repoPath: items[index].folder,
13858
13540
  prNumber: items[index].number
@@ -13891,6 +13573,7 @@ async function startPullRequestBatch(body, deps) {
13891
13573
  }
13892
13574
  claimed.set(identity, index);
13893
13575
  const prepared = await prepareRow(row, target, agent.agentId, model, deps, fresh);
13576
+ deliverRow(row, batch, deps);
13894
13577
  if (fresh) {
13895
13578
  resolved[index] = target;
13896
13579
  replaced = true;
@@ -13912,23 +13595,114 @@ async function startPullRequestBatch(body, deps) {
13912
13595
  status: 200,
13913
13596
  body: {
13914
13597
  batchId: batch.id,
13915
- rows: rowsOf(batch)
13598
+ rows: rowsOf(batch),
13599
+ destination
13916
13600
  }
13917
13601
  };
13918
13602
  }
13919
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
+ /**
13920
13667
  * A batch's rows as they stand. Asking counts as watching: a batch an open panel is still asking
13921
13668
  * about is never forgotten, so its Retry stays good for as long as the panel is open.
13922
13669
  */
13923
- function batchRows(id) {
13670
+ function batchRows(id, deps) {
13671
+ const batch = batches.get(id);
13672
+ if (!batch) return lostBatch();
13673
+ batch.seenAt = Date.now();
13674
+ return {
13675
+ status: 200,
13676
+ body: {
13677
+ rows: rowsOf(batch),
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) {
13924
13689
  const batch = batches.get(id);
13925
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
+ };
13926
13699
  batch.seenAt = Date.now();
13927
13700
  return {
13928
13701
  status: 200,
13929
13702
  body: {
13930
13703
  rows: rowsOf(batch),
13931
- finished: batch.finishedAt !== null
13704
+ finished: batch.finishedAt !== null,
13705
+ destination: destinationNow(batch, deps)
13932
13706
  }
13933
13707
  };
13934
13708
  }
@@ -17945,7 +17719,7 @@ async function handleAddReview(req, res, pathname, options) {
17945
17719
  if (route === "options") return addReviewOptions(deps);
17946
17720
  if (route === "folders") return listFolders(deps);
17947
17721
  const batch = /^batches\/([0-9a-f-]{36})$/.exec(route);
17948
- if (batch) return batchRows(batch[1]);
17722
+ if (batch) return batchRows(batch[1], deps);
17949
17723
  return null;
17950
17724
  }
17951
17725
  if (req.method !== "POST") return {
@@ -17960,6 +17734,8 @@ async function handleAddReview(req, res, pathname, options) {
17960
17734
  if (route === "branch") return startBranchReview(body, deps);
17961
17735
  const retry = /^batches\/([0-9a-f-]{36})\/retry$/.exec(route);
17962
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);
17963
17739
  return null;
17964
17740
  };
17965
17741
  const result = await answer();