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/CHANGELOG.md +80 -0
- package/dist/main.js +596 -135
- package/dist/{mcp_tools-DbFR9Z90.js → mcp_tools-DDbcXys2.js} +1022 -463
- package/dist/server.js +147 -371
- package/package.json +1 -1
- package/static/{chunk-CIJm20ER.js → chunk-CQIPqX7U.js} +1 -1
- package/static/chunk-Cs33-tV3.js +1 -0
- package/static/{chunk-Ahis0VaG.js → chunk-Cum6sHaC.js} +1 -1
- package/static/{chunk-Cg4cN8Vk.js → chunk-D1LBeOJc.js} +1 -1
- package/static/help.js +1 -1
- package/static/notifications.js +1 -1
- package/static/reviews.js +1 -1
- package/static/runs.js +1 -1
- package/static/settings.js +1 -1
- package/static/shell.css +11 -0
- package/static/workspace.js +1 -1
- package/static/chunk-DRtin0n-.js +0 -1
package/dist/server.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { $
|
|
2
|
-
import {
|
|
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 {
|
|
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
|
|
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
|
|
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) => ({
|
|
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();
|