prreviewbuddy 0.76.9 → 0.78.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/dist/{delete_review-BX3lu5Q7.js → delete_review-BHOzMQ0_.js} +49 -1
- package/dist/main.js +1 -1
- package/dist/server.js +334 -92
- package/package.json +1 -1
- package/static/chunk-BMAMO0Tf.js +1 -0
- package/static/{chunk-egI_P3oB.js → chunk-D8xJNBjR.js} +1 -1
- package/static/help.js +1 -1
- package/static/notifications.js +1 -1
- package/static/reviews.js +1 -1
- package/static/settings.js +1 -1
- package/static/shell.css +145 -8
- package/static/workspace.js +17 -17
- package/static/chunk-C0lM8IBt.js +0 -1
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as resolveWorkspacePort, $r as
|
|
1
|
+
import { $ as resolveWorkspacePort, $n as saveJob, $r as explanationActionOf, $t as pruneNotifications, A as parsePullRequestLink, Ai as git, An as ReviewBeingDeletedError, Ar as activityAttributes, At as repositoryKey, Bi as agentById, Bn as findExistingReview, Br as queueAddMany, Bt as announceJob, C as completeRun, Ci as originRemote, Cn as GITHUB_MARKDOWN, Cr as reviewedCommit, Ct as RUNTIME_WORDS, D as relativeTime$1, Di as AmbiguousRefError, Dn as escapeHtml$1, Dr as workspaceRevision, Dt as loadArchitecture, E as stopRun, Ei as resolveBase, En as CODE_COPY_CLASS, Er as touchWorkspace, Et as checkArchitecture, Fi as readAgentPreference, Fn as readMarker, Fr as lifecyclePill, Ft as QUOTE_LIMITS, G as stopChild, Gn as fetchHead, Gr as isIssueOutstanding, Gt as createPrState, Hi as detectAgents, Hn as checkReview, Hr as queueStamp, Ht as subjectOf, I as settleQueue, Ii as writeAgentPreference, In as removeWorktree, Ir as offersLifecycle, It as REPOSITORY_SCOPE, J as writeServerRecord, Jn as allJobs, Jr as questionsOutstanding, Jt as listNotifications, Kn as forgeResolver, Kr as isQuestionOutstanding, Kt as createPrUpdate, L as readIndexToken, Ln as requestJobStop, Lr as parseLifecycle, Lt as ancestorsOf, M as runBatch, Mi as resolveAgent, Mn as createWorktree, Mr as isLifecycle, Mt as storedScopes, N as activateQueued, Ni as resolveModel, Nn as holdWorktree, Nr as lifecycleChip, Nt as trackedFiles, O as batchIdentity, Oi as shortNameOf, On as renderInlineText, Or as workspaceStamps, Ot as partFiles, P as compactQueue, Pi as clearAgentPreference, Pn as isWorktreeHeld, Pr as lifecycleItemLabel, Pt as ownersOf, Q as parseWorkspacePort, Qn as loadJob, Qr as recommendationsOf, Qt as moveIds, Ri as STORE_ROOT, Rr as sectionOf, Rt as isArchitectureId, S as beginRun, Si as resolveTarget, Sn as hasAuthorContent, Sr as recentWorkspaces, St as recordRuntimeBestEffort, T as reportRun, Ti as isCommitReview, Tn as CODE_BLOCK_CLASS, Tt as ARCHITECTURE_DIR, U as serverEntryExists, Ui as AgentCancelledError, Un as followedBranch, Ur as readQueue, Ut as categoryOf, V as readServerRecord, Vi as agentFor$1, Vn as checkCodeFreshness, Vr as queueRemove, Vt as branchOf, W as spawnServer, Wi as AgentUnavailableError, Wn as reviewedShaOf, Wr as doneVerb, Wt as createArchitectureNotification, X as MAX_WORKSPACE_PORT, Xn as fail$1, Xr as reviewerDispositions, Xt as markRead, Y as DEFAULT_WORKSPACE_PORT, Yn as endedAt, Yr as resolvedByConversation, Yt as markAllRead, Z as MIN_WORKSPACE_PORT, Zn as isTerminal, Zr as processDiscussion, Zt as markReadForReview, _i as extractJsonText, _n as agentEnvOf, _t as BUILD_VERSION, a as wasBlocked, ai as assessmentLookup, an as unreadAround, at as settleReview, b as track, bi as WITHHELD_REASON, bn as UNESTABLISHED_MOTIVATION, br as presentedKissRunOf, bt as waitingHistory, c as RefreshUnavailableError, ci as findingIn, cn as existingPaths, cr as followedRefName, d as mergeUpdateInto, di as promptGrade, dn as prepareConversationWithin, dr as indexReviewGroups, dt as record, ei as explanationOf, en as readNotification, et as statedWorkspacePort, f as updateReview, fi as reassessIn, fn as saveRefresh, fr as kissDecisionRefusal, gi as asString, gn as isUnchanged, gr as loadWorkspace, h as liveUpdateFor, hi as followUpFindingId, hn as refreshPrContext, hr as lineagePosition, ht as telemetryUploadConsent, i as liveJobsFor, ii as analysisRefOf, in as toneOf, ir as purposeOf, it as prepareReview, j as unsupportedReason, ji as refExists, jn as MANAGED_ROOT, jr as activityLabel, jt as storeArchitecture, k as extractPullRequestLinks, ki as shorthandOf, kr as LIFECYCLES, kt as readArchitecture, l as cancelUpdate, li as gradeScale, ln as ASK_REREAD_CEILING_MS, lr as groupByLineage, m as UpdateAlreadyRunningError, mi as wasReassessed, mn as unforeseenFailure, mr as latestKissRun, mt as recordTelemetryUploadConsent, n as deleteReview, ni as withExplanation, nn as removeNotification, nr as phasesFor, nt as failureOf$1, o as discardJob, oi as currentGradeIn, on as unreadCount$1, or as isClaimed, ot as reanalyseReview, p as askCheckout, pi as sourcePhrase, pn as storedConversation, pr as kissDecisionsOf, qn as fillFileUrlTemplate, qr as issuesOutstanding, qt as eventName, r as lineageIds, ri as ANALYSIS_ONLY, rn as retireNotification, rr as progressSteps, rt as observeReview, s as FETCH_TIMEOUT_MS, si as findingAssessment, sn as withUsageRecorded, sr as decideKissRecommendation, st as runJob, t as deleteLineage, ti as isExplanationAction, tn as readStateOf, tr as isLiveJob, tt as writePortPreference, u as cancellableHere, ui as isGradeFor, un as adoptLegacyBaseline, ur as indexKeptIds, ut as readEvents, v as startKissJob, vi as objectWithAnyOf, vn as EXPLAIN_SIMPLY_PROMPT, vr as mutateWorkspace, w as endRun, wi as DetachedHeadError, wn as renderMarkdown, wt as describeRuntimes, x as RUN_KINDS, xi as kissOutcome, xn as authorPreview, xr as previousKissResult, xt as mapRuntimes, y as isDriving, yi as describeAuthorship, yn as chatConversationSection, yt as feedbackUrl, z as clearServerRecordOwnedBy, zi as AGENT_IDS, zr as queueAdd, zt as writeJsonAtomically } from "./delete_review-BHOzMQ0_.js";
|
|
2
2
|
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -310,9 +310,13 @@ function cardLifecycleChip(pinned, lifecycle, activity) {
|
|
|
310
310
|
* The two numbers are different questions and only both worth asking once they differ. A page of
|
|
311
311
|
* thirteen branches reviewed once each would read "13 reviews · 13 branches", which invites the
|
|
312
312
|
* reader to work out what the distinction is on the one page where there isn't one.
|
|
313
|
+
*
|
|
314
|
+
* `of` is every review on the page, given while the search or a filter is narrowing the list:
|
|
315
|
+
* "12 of 71 reviews" says the list is filtered and by how much, where "12 reviews" reads as all
|
|
316
|
+
* there are.
|
|
313
317
|
*/
|
|
314
|
-
function countLabel(reviews, branches) {
|
|
315
|
-
const label = `${reviews} review${reviews === 1 ? "" : "s"}`;
|
|
318
|
+
function countLabel(reviews, branches, of) {
|
|
319
|
+
const label = of === void 0 ? `${reviews} review${reviews === 1 ? "" : "s"}` : `${reviews} of ${of} review${of === 1 ? "" : "s"}`;
|
|
316
320
|
if (reviews === branches) return label;
|
|
317
321
|
return `${label} · ${branches} branch${branches === 1 ? "" : "es"}`;
|
|
318
322
|
}
|
|
@@ -9475,9 +9479,17 @@ function renderControls(queued, authors, anyRequest) {
|
|
|
9475
9479
|
</select>
|
|
9476
9480
|
</label>
|
|
9477
9481
|
${anyRequest ? renderPrFilter() : ""}
|
|
9482
|
+
${renderClearFilters()}
|
|
9478
9483
|
</div>`;
|
|
9479
9484
|
}
|
|
9480
9485
|
/**
|
|
9486
|
+
* Puts the search and every filter back at once. Hidden until the browser finds one narrowing the
|
|
9487
|
+
* list: the hash is the browser's to read, and without script there is nothing to clear.
|
|
9488
|
+
*/
|
|
9489
|
+
function renderClearFilters() {
|
|
9490
|
+
return `<button type="button" class="prb-index-clear" data-clear-filters hidden><span aria-hidden="true">×</span> Clear filters</button>`;
|
|
9491
|
+
}
|
|
9492
|
+
/**
|
|
9481
9493
|
* The pull request's state upstream, beside Review state and not inside it, so the two combine.
|
|
9482
9494
|
*
|
|
9483
9495
|
* Absent when no card is of a pull request, for the author filter's reason: a select whose every
|
|
@@ -9505,11 +9517,16 @@ var GITHUB_MARK = "<svg class=\"prb-index-forge-mark\" viewBox=\"0 0 16 16\" wid
|
|
|
9505
9517
|
*
|
|
9506
9518
|
* Absent when no card carries a login, which is every page of local-branch reviews: a dropdown
|
|
9507
9519
|
* whose only choice is Everyone is not a choice.
|
|
9520
|
+
*
|
|
9521
|
+
* The button holds the chosen name where the browser draws a select's menu itself, so a long one
|
|
9522
|
+
* can be cut short inside the box's fixed width (shell.css). A browser that does not parses the
|
|
9523
|
+
* button and its contents away and draws the select as it always has.
|
|
9508
9524
|
*/
|
|
9509
9525
|
function renderAuthorFilter(authors) {
|
|
9510
9526
|
if (authors.length === 0) return "";
|
|
9511
9527
|
return `<label class="prb-index-status prb-index-forge">${GITHUB_MARK}Author:
|
|
9512
9528
|
<select name="author" data-author-filter aria-label="Filter by pull request author">
|
|
9529
|
+
<button><selectedcontent></selectedcontent></button>
|
|
9513
9530
|
<option value="">Everyone</option>${authors.map((author) => `<option value="${escapeHtml$1(author.handle)}">${escapeHtml$1(authorOptionLabel(author))}</option>`).join("")}
|
|
9514
9531
|
</select>
|
|
9515
9532
|
</label>`;
|
|
@@ -10688,7 +10705,8 @@ var TOPICS = [
|
|
|
10688
10705
|
<li><code>prreviewbuddy open</code> to list the reviews you have and open this workspace</li>
|
|
10689
10706
|
</ul>
|
|
10690
10707
|
<p>Or press <kbd class="prb-kbd">⌘P</kbd> (<kbd class="prb-kbd">Ctrl+P</kbd> elsewhere) to add a
|
|
10691
|
-
review from here.
|
|
10708
|
+
review from here. The panel follows the reviews it started while it stays open, and closing it
|
|
10709
|
+
stops nothing. <code>prreviewbuddy help</code> lists every command, and
|
|
10692
10710
|
<code>prreviewbuddy review --help</code> every option a review takes.</p>
|
|
10693
10711
|
<p>There is no account and no API key. PR Review Buddy uses the coding agent installed on your
|
|
10694
10712
|
machine and that agent's existing authentication, and never asks you for a credential.</p>`
|
|
@@ -11598,16 +11616,24 @@ function createSlots(capacity) {
|
|
|
11598
11616
|
}
|
|
11599
11617
|
/** How many pull requests one submission may name. See `MAX_PULL_REQUESTS_PER_BATCH`. */
|
|
11600
11618
|
var MAX_BATCH = 20;
|
|
11601
|
-
/**
|
|
11619
|
+
/**
|
|
11620
|
+
* How long a finished batch stays answerable once nobody is asking about it. An open panel keeps
|
|
11621
|
+
* asking (`KEEPALIVE` in the client is well inside this), so a batch is only forgotten after its
|
|
11622
|
+
* panel has gone, and its Retry with it.
|
|
11623
|
+
*/
|
|
11602
11624
|
var FINISHED_BATCH_TTL_MS = 6e5;
|
|
11603
11625
|
function realAddReviewDeps() {
|
|
11604
11626
|
return {
|
|
11605
11627
|
resolveAgent: (flag) => resolveAgent(flag ? { flag } : {}),
|
|
11606
|
-
agents: async () => (await detectAgents()).map((agent) =>
|
|
11607
|
-
|
|
11608
|
-
|
|
11609
|
-
|
|
11610
|
-
|
|
11628
|
+
agents: async () => (await detectAgents()).map((agent) => {
|
|
11629
|
+
const modelHelp = modelHelpOf(agent.id);
|
|
11630
|
+
return {
|
|
11631
|
+
id: agent.id,
|
|
11632
|
+
displayName: agent.displayName,
|
|
11633
|
+
installed: agent.state === "installed",
|
|
11634
|
+
...modelHelp ? { modelHelp } : {}
|
|
11635
|
+
};
|
|
11636
|
+
}),
|
|
11611
11637
|
folders: () => discoverWorkingFolders(),
|
|
11612
11638
|
summaries: () => recentWorkspaces(Number.MAX_SAFE_INTEGER),
|
|
11613
11639
|
remoteOf: originRemote,
|
|
@@ -11631,6 +11657,8 @@ function realAddReviewDeps() {
|
|
|
11631
11657
|
findExisting: findExistingReview,
|
|
11632
11658
|
prepare: prepareReview,
|
|
11633
11659
|
settle: (workspaceId) => settleReview(workspaceId, "standard"),
|
|
11660
|
+
observe: (workspaceId) => observeReview(workspaceId),
|
|
11661
|
+
commitPresent: (root, sha) => refExists(root, sha),
|
|
11634
11662
|
track,
|
|
11635
11663
|
chooseFolder: folderChooser(),
|
|
11636
11664
|
concurrency: 2,
|
|
@@ -11646,6 +11674,13 @@ function realAddReviewDeps() {
|
|
|
11646
11674
|
home: homedir()
|
|
11647
11675
|
};
|
|
11648
11676
|
}
|
|
11677
|
+
function modelHelpOf(agentId) {
|
|
11678
|
+
try {
|
|
11679
|
+
return agentById(agentId).modelSelectionHelp;
|
|
11680
|
+
} catch {
|
|
11681
|
+
return;
|
|
11682
|
+
}
|
|
11683
|
+
}
|
|
11649
11684
|
function agentNameOf(agentId) {
|
|
11650
11685
|
try {
|
|
11651
11686
|
return agentById(agentId).displayName;
|
|
@@ -11653,10 +11688,60 @@ function agentNameOf(agentId) {
|
|
|
11653
11688
|
return agentId;
|
|
11654
11689
|
}
|
|
11655
11690
|
}
|
|
11656
|
-
var refuse = (status, error) => ({
|
|
11691
|
+
var refuse = (status, error, extra = {}) => ({
|
|
11657
11692
|
status,
|
|
11658
|
-
body: {
|
|
11693
|
+
body: {
|
|
11694
|
+
error,
|
|
11695
|
+
...extra
|
|
11696
|
+
}
|
|
11659
11697
|
});
|
|
11698
|
+
function baseChoiceOf(error) {
|
|
11699
|
+
const option = (where) => {
|
|
11700
|
+
const matched = where === "remote" ? error.remote : error.local;
|
|
11701
|
+
const commits = where === "remote" ? error.ranges?.remote : error.ranges?.local;
|
|
11702
|
+
return {
|
|
11703
|
+
base: shorthandOf(matched.ref),
|
|
11704
|
+
display: where === "remote" ? shorthandOf(matched.ref) : shortNameOf(matched.ref),
|
|
11705
|
+
where,
|
|
11706
|
+
...commits ? { commits: commits.length } : {}
|
|
11707
|
+
};
|
|
11708
|
+
};
|
|
11709
|
+
return {
|
|
11710
|
+
name: shortNameOf(error.local.ref),
|
|
11711
|
+
discovered: error.discovered,
|
|
11712
|
+
...error.divergence ? { divergence: error.divergence } : {},
|
|
11713
|
+
options: [option("remote"), option("local")]
|
|
11714
|
+
};
|
|
11715
|
+
}
|
|
11716
|
+
/**
|
|
11717
|
+
* What the panel says for an error a review could not get past.
|
|
11718
|
+
*
|
|
11719
|
+
* Two errors are known by their class and said in the panel's own words. Anything else keeps its
|
|
11720
|
+
* message, which is already written for a person, but only its first paragraph is the sentence:
|
|
11721
|
+
* the rest (ref listings, commands to run) goes behind Technical details, kept whole for whoever
|
|
11722
|
+
* needs to diagnose it.
|
|
11723
|
+
*/
|
|
11724
|
+
function refusalOf(error) {
|
|
11725
|
+
const text = message(error);
|
|
11726
|
+
if (error instanceof AmbiguousRefError) {
|
|
11727
|
+
const choice = baseChoiceOf(error);
|
|
11728
|
+
return {
|
|
11729
|
+
error: `\`${choice.name}\` is both a local and a remote branch, and they are different commits. Choose which one to compare against.`,
|
|
11730
|
+
detail: text,
|
|
11731
|
+
baseChoice: choice
|
|
11732
|
+
};
|
|
11733
|
+
}
|
|
11734
|
+
if (error instanceof DetachedHeadError) return { error: "This folder is on a detached HEAD, so there is no branch to review. Check out a branch in it, or review its pull request instead." };
|
|
11735
|
+
return briefly(text);
|
|
11736
|
+
}
|
|
11737
|
+
/** A message's first paragraph as the sentence, and the whole of it as the detail when there is more. */
|
|
11738
|
+
function briefly(text) {
|
|
11739
|
+
const first = text.split(/\n\s*\n/)[0].trim();
|
|
11740
|
+
return first && first !== text.trim() ? {
|
|
11741
|
+
error: first,
|
|
11742
|
+
detail: text
|
|
11743
|
+
} : { error: text };
|
|
11744
|
+
}
|
|
11660
11745
|
/** A path as a person reads it, with their home folder as `~`. */
|
|
11661
11746
|
function displayPath(path, home) {
|
|
11662
11747
|
return home && (path === home || path.startsWith(`${home}/`)) ? `~${path.slice(home.length)}` : path;
|
|
@@ -11685,15 +11770,15 @@ function unresolvedAgent(resolution) {
|
|
|
11685
11770
|
* harness's own fallback: an id is always passed down, so `startJob` never has to guess.
|
|
11686
11771
|
*/
|
|
11687
11772
|
async function agentFor(flag, deps) {
|
|
11688
|
-
if (flag !== void 0 && typeof flag !== "string") return refuse(400, "Say which agent by its id.");
|
|
11773
|
+
if (flag !== void 0 && typeof flag !== "string") return refuse(400, "Say which agent by its id.", { field: "agent" });
|
|
11689
11774
|
const resolution = await deps.resolveAgent(flag || void 0);
|
|
11690
11775
|
if (resolution.kind === "resolved") return { agentId: resolution.agent.id };
|
|
11691
|
-
if (resolution.kind === "ambiguous") return refuse(400, "More than one review agent is installed. Choose one under Review options.");
|
|
11692
|
-
return refuse(resolution.kind === "unknown-id" ? 400 : 409, unresolvedAgent(resolution));
|
|
11776
|
+
if (resolution.kind === "ambiguous") return refuse(400, "More than one review agent is installed. Choose one under Review options.", { field: "agent" });
|
|
11777
|
+
return refuse(resolution.kind === "unknown-id" ? 400 : 409, unresolvedAgent(resolution), { field: "agent" });
|
|
11693
11778
|
}
|
|
11694
11779
|
function modelOf(value) {
|
|
11695
11780
|
if (value === void 0 || value === null || value === "") return void 0;
|
|
11696
|
-
if (typeof value !== "string" || value.length > 200) return refuse(400, "The model must be a short name.");
|
|
11781
|
+
if (typeof value !== "string" || value.length > 200) return refuse(400, "The model must be a short name.", { field: "model" });
|
|
11697
11782
|
return value.trim() || void 0;
|
|
11698
11783
|
}
|
|
11699
11784
|
/**
|
|
@@ -11802,38 +11887,115 @@ async function previewPullRequests(text, deps) {
|
|
|
11802
11887
|
}
|
|
11803
11888
|
var batches = /* @__PURE__ */ new Map();
|
|
11804
11889
|
function forgetFinished(now) {
|
|
11805
|
-
for (const [id, batch] of batches) if (batch.finishedAt !== null && now - batch.finishedAt > FINISHED_BATCH_TTL_MS) batches.delete(id);
|
|
11890
|
+
for (const [id, batch] of batches) if (batch.finishedAt !== null && now - Math.max(batch.finishedAt, batch.seenAt) > FINISHED_BATCH_TTL_MS) batches.delete(id);
|
|
11806
11891
|
}
|
|
11807
11892
|
function newBatch(rows) {
|
|
11808
11893
|
forgetFinished(Date.now());
|
|
11894
|
+
const now = Date.now();
|
|
11809
11895
|
const batch = {
|
|
11810
11896
|
id: randomUUID(),
|
|
11811
11897
|
rows,
|
|
11812
|
-
finishedAt:
|
|
11898
|
+
finishedAt: now,
|
|
11899
|
+
seenAt: now,
|
|
11900
|
+
running: 0,
|
|
11901
|
+
rerun: () => {}
|
|
11813
11902
|
};
|
|
11814
11903
|
batches.set(batch.id, batch);
|
|
11815
11904
|
return batch;
|
|
11816
11905
|
}
|
|
11906
|
+
/**
|
|
11907
|
+
* Count work against a batch, inside the restart guard. The batch is finished only when nothing
|
|
11908
|
+
* counted against it is still going, so a retry reopens a batch an open panel can then follow again.
|
|
11909
|
+
*/
|
|
11910
|
+
function follow(batch, work, deps, driving = true) {
|
|
11911
|
+
batch.running += 1;
|
|
11912
|
+
batch.finishedAt = null;
|
|
11913
|
+
(driving ? deps.track(work) : work).catch(() => {}).finally(() => {
|
|
11914
|
+
batch.running -= 1;
|
|
11915
|
+
if (batch.running === 0) batch.finishedAt = Date.now();
|
|
11916
|
+
});
|
|
11917
|
+
}
|
|
11918
|
+
function fail(row, error) {
|
|
11919
|
+
const refusal = refusalOf(error);
|
|
11920
|
+
row.status = "failed";
|
|
11921
|
+
row.reason = refusal.error;
|
|
11922
|
+
if (refusal.detail) row.detail = refusal.detail;
|
|
11923
|
+
}
|
|
11817
11924
|
var rowsOf = (batch) => batch.rows.map((row) => ({ ...row }));
|
|
11818
11925
|
/**
|
|
11819
11926
|
* Create the review for a row's target, or find the one it has, under the per-target lock. The
|
|
11820
11927
|
* caller holds an analysis slot: nothing is created without one, so nothing created waits.
|
|
11821
11928
|
*/
|
|
11822
|
-
|
|
11823
|
-
|
|
11929
|
+
/**
|
|
11930
|
+
* `fresh` is a fresh start's: a new review of the target as it is now, keeping whatever review the
|
|
11931
|
+
* branch or request already has, as `--fresh` does. Without it the lookup finds the latest review of
|
|
11932
|
+
* the same branch or request, which for a fresh start is the very failure being started over.
|
|
11933
|
+
*/
|
|
11934
|
+
async function prepareRow(row, target, agentId, model, deps, fresh = false) {
|
|
11935
|
+
const prepared = await oneCreationAtATime(target, () => deps.prepare(target, agentId, fresh, model));
|
|
11824
11936
|
row.reviewId = prepared.kind === "new" ? prepared.workspaceId : prepared.existing.id;
|
|
11825
|
-
if (prepared.kind === "existing") row.status = "already-reviewed";
|
|
11937
|
+
if (prepared.kind === "existing") row.status = prepared.existing.inProgress ? "reviewing" : "already-reviewed";
|
|
11826
11938
|
else row.status = "reviewing";
|
|
11827
11939
|
if (prepared.kind === "resume") row.kept = keptOf(prepared.failure.agentId, prepared.failure.requestedModel, deps);
|
|
11940
|
+
else delete row.kept;
|
|
11828
11941
|
return prepared;
|
|
11829
11942
|
}
|
|
11830
|
-
|
|
11831
|
-
|
|
11943
|
+
/**
|
|
11944
|
+
* Take a prepared row to its end. A new or resumed review is run here, holding the slot. One that
|
|
11945
|
+
* something else is already analysing is watched instead, never driven: the watch is handed to the
|
|
11946
|
+
* batch rather than awaited, so the caller returns, and its slot goes back, at once, while the row
|
|
11947
|
+
* stays Analysing until that analysis ends. Counted against the batch, not against restarts.
|
|
11948
|
+
*/
|
|
11949
|
+
async function completeRow(row, prepared, batch, deps) {
|
|
11950
|
+
if (prepared.kind !== "existing") return settleRow(row, deps.settle(row.reviewId));
|
|
11951
|
+
if (prepared.existing.inProgress) follow(batch, settleRow(row, deps.observe(row.reviewId)), deps, false);
|
|
11952
|
+
}
|
|
11953
|
+
async function settleRow(row, settling) {
|
|
11954
|
+
const settled = await settling;
|
|
11832
11955
|
if (settled.ok) row.status = "ready";
|
|
11833
|
-
else {
|
|
11956
|
+
else if (settled.stopped) {
|
|
11834
11957
|
row.status = "failed";
|
|
11835
|
-
row.reason =
|
|
11958
|
+
row.reason = "Stopped";
|
|
11959
|
+
} else fail(row, settled.reason);
|
|
11960
|
+
}
|
|
11961
|
+
/**
|
|
11962
|
+
* A retry runs against the commit the row was first resolved to, never a newer one: a retry
|
|
11963
|
+
* resumes what was asked for, and a moved branch or pull request is a fresh review's business. When
|
|
11964
|
+
* that commit has gone (a force push, then a prune), there is nothing left to retry, and the row
|
|
11965
|
+
* says so and offers the fresh start instead of quietly reviewing something else.
|
|
11966
|
+
*/
|
|
11967
|
+
async function stillThere(row, target, what, deps) {
|
|
11968
|
+
if (await deps.commitPresent(target.originRepoPath, target.sha)) return true;
|
|
11969
|
+
row.status = "failed";
|
|
11970
|
+
row.reason = `The commit this review was of (${target.sha.slice(0, 7)}) is no longer in this repository, so it can't be retried as it was. Start a fresh review to analyse the ${what} as it is now.`;
|
|
11971
|
+
row.stale = true;
|
|
11972
|
+
return false;
|
|
11973
|
+
}
|
|
11974
|
+
/**
|
|
11975
|
+
* Retry a row whose review exists, by that review's id. Not through the lookup a start uses, which
|
|
11976
|
+
* finds the latest review of the branch or request: one made since, of newer commits, would be
|
|
11977
|
+
* handed back in place of the one that failed, and a retry would quietly become a different review.
|
|
11978
|
+
*
|
|
11979
|
+
* Resumed when it is still a failure nothing is running, as its own page's Retry does. Otherwise
|
|
11980
|
+
* something has taken it up since (that Retry, a terminal), and it is watched rather than run again.
|
|
11981
|
+
*/
|
|
11982
|
+
async function retryReview(row, batch, deps) {
|
|
11983
|
+
const reviewId = row.reviewId;
|
|
11984
|
+
const kept = deps.resumeOf(reviewId);
|
|
11985
|
+
row.status = "reviewing";
|
|
11986
|
+
if (kept) {
|
|
11987
|
+
row.kept = kept;
|
|
11988
|
+
return settleRow(row, deps.settle(reviewId));
|
|
11836
11989
|
}
|
|
11990
|
+
follow(batch, settleRow(row, deps.observe(reviewId)), deps, false);
|
|
11991
|
+
}
|
|
11992
|
+
/**
|
|
11993
|
+
* A fresh start that ended before it made its review leaves the row exactly as it was: the old
|
|
11994
|
+
* target, the old review, and still `stale`. So the row keeps offering the fresh start, and a plain
|
|
11995
|
+
* Retry finds the old commit gone again, rather than resuming the old review against it.
|
|
11996
|
+
*/
|
|
11997
|
+
function keepFreshPending(row, replaced) {
|
|
11998
|
+
if (row.status === "failed" && !replaced) row.stale = true;
|
|
11837
11999
|
}
|
|
11838
12000
|
function keptOf(agentId, model, deps) {
|
|
11839
12001
|
return {
|
|
@@ -11879,40 +12041,55 @@ async function startPullRequestBatch(body, deps) {
|
|
|
11879
12041
|
})));
|
|
11880
12042
|
/** Which row claimed each pull request, by what it resolved to rather than how it was spelled. */
|
|
11881
12043
|
const claimed = /* @__PURE__ */ new Map();
|
|
11882
|
-
|
|
12044
|
+
/** Each row's pull request as it was first resolved: head, base and all. What a retry runs against. */
|
|
12045
|
+
const resolved = [];
|
|
12046
|
+
const lane = async (item, index, fresh = false) => {
|
|
11883
12047
|
const row = batch.rows[index];
|
|
11884
12048
|
const release = await deps.slots.acquire();
|
|
11885
12049
|
row.status = "reviewing";
|
|
12050
|
+
/** A fresh start has made its review, so the row now belongs to the new target. */
|
|
12051
|
+
let replaced = false;
|
|
11886
12052
|
try {
|
|
11887
|
-
|
|
11888
|
-
if (
|
|
11889
|
-
row
|
|
11890
|
-
row.
|
|
11891
|
-
|
|
12053
|
+
let target = fresh ? void 0 : resolved[index];
|
|
12054
|
+
if (target) {
|
|
12055
|
+
if (!await stillThere(row, target, "pull request", deps)) return;
|
|
12056
|
+
if (row.reviewId) return await retryReview(row, batch, deps);
|
|
12057
|
+
} else {
|
|
12058
|
+
const remote = await deps.remoteOf(item.folder);
|
|
12059
|
+
if (!isGitHubRepository(remote, item.owner, item.repo)) {
|
|
12060
|
+
row.status = "failed";
|
|
12061
|
+
row.reason = remote ? `This folder is a clone of ${remote.owner}/${remote.repo}, not ${item.owner}/${item.repo}` : `This folder is not a clone of ${item.owner}/${item.repo}`;
|
|
12062
|
+
return;
|
|
12063
|
+
}
|
|
12064
|
+
target = await deps.resolvePullRequest(item.folder, item.url);
|
|
12065
|
+
if (!fresh) resolved[index] = target;
|
|
11892
12066
|
}
|
|
11893
|
-
const target = await deps.resolvePullRequest(item.folder, item.url);
|
|
11894
12067
|
const identity = batchIdentity(target);
|
|
11895
12068
|
const first = claimed.get(identity);
|
|
11896
|
-
if (first !== void 0) {
|
|
12069
|
+
if (first !== void 0 && first !== index) {
|
|
11897
12070
|
row.status = "already-reviewed";
|
|
11898
12071
|
row.reason = `Same pull request as ${batch.rows[first].label}`;
|
|
11899
12072
|
return;
|
|
11900
12073
|
}
|
|
11901
12074
|
claimed.set(identity, index);
|
|
11902
|
-
|
|
12075
|
+
const prepared = await prepareRow(row, target, agent.agentId, model, deps, fresh);
|
|
12076
|
+
if (fresh) {
|
|
12077
|
+
resolved[index] = target;
|
|
12078
|
+
replaced = true;
|
|
12079
|
+
}
|
|
12080
|
+
await completeRow(row, prepared, batch, deps);
|
|
11903
12081
|
} catch (error) {
|
|
11904
|
-
row
|
|
11905
|
-
row.reason = message(error);
|
|
12082
|
+
fail(row, error);
|
|
11906
12083
|
} finally {
|
|
12084
|
+
if (fresh) keepFreshPending(row, replaced);
|
|
11907
12085
|
release();
|
|
11908
12086
|
}
|
|
11909
12087
|
};
|
|
11910
|
-
|
|
12088
|
+
batch.rerun = (index, fresh) => follow(batch, lane(items[index], index, fresh), deps);
|
|
12089
|
+
follow(batch, runBatch(items, {
|
|
11911
12090
|
concurrency: deps.concurrency,
|
|
11912
12091
|
run: lane
|
|
11913
|
-
})
|
|
11914
|
-
batch.finishedAt = Date.now();
|
|
11915
|
-
});
|
|
12092
|
+
}), deps);
|
|
11916
12093
|
return {
|
|
11917
12094
|
status: 200,
|
|
11918
12095
|
body: {
|
|
@@ -11922,13 +12099,13 @@ async function startPullRequestBatch(body, deps) {
|
|
|
11922
12099
|
};
|
|
11923
12100
|
}
|
|
11924
12101
|
/**
|
|
11925
|
-
* A batch's rows as they stand.
|
|
11926
|
-
*
|
|
11927
|
-
* restart, which is the case worth saying plainly: whatever had not started never will.
|
|
12102
|
+
* A batch's rows as they stand. Asking counts as watching: a batch an open panel is still asking
|
|
12103
|
+
* about is never forgotten, so its Retry stays good for as long as the panel is open.
|
|
11928
12104
|
*/
|
|
11929
12105
|
function batchRows(id) {
|
|
11930
12106
|
const batch = batches.get(id);
|
|
11931
|
-
if (!batch) return
|
|
12107
|
+
if (!batch) return lostBatch();
|
|
12108
|
+
batch.seenAt = Date.now();
|
|
11932
12109
|
return {
|
|
11933
12110
|
status: 200,
|
|
11934
12111
|
body: {
|
|
@@ -11937,10 +12114,39 @@ function batchRows(id) {
|
|
|
11937
12114
|
}
|
|
11938
12115
|
};
|
|
11939
12116
|
}
|
|
11940
|
-
/**
|
|
11941
|
-
|
|
11942
|
-
|
|
11943
|
-
|
|
12117
|
+
/**
|
|
12118
|
+
* Said without guessing why. A restart forgets every batch, and a batch nobody has asked about for
|
|
12119
|
+
* a while is forgotten too; the server cannot tell which happened, so it says what follows from
|
|
12120
|
+
* either.
|
|
12121
|
+
*/
|
|
12122
|
+
var lostBatch = () => refuse(404, "This batch is no longer on the workspace server, so it can’t be followed or retried here. Reviews that had started are in your reviews list. Any that were still queued did not start.");
|
|
12123
|
+
/**
|
|
12124
|
+
* Run one failed row of a batch again, as it was first asked for: the same folder, agent, model
|
|
12125
|
+
* and base, and the same commits, never the branch or pull request as it has since moved on. A
|
|
12126
|
+
* review that was created and then failed resumes, as a Retry does on its own page. `fresh` is the
|
|
12127
|
+
* one exception, sent only when those commits have gone: it looks again at what is there now.
|
|
12128
|
+
* The other rows are left exactly as they are, and the batch is followed again until it ends.
|
|
12129
|
+
*/
|
|
12130
|
+
function retryBatchRow(id, key, fresh = false) {
|
|
12131
|
+
const batch = batches.get(id);
|
|
12132
|
+
if (!batch) return lostBatch();
|
|
12133
|
+
const index = batch.rows.findIndex((row) => row.key === key);
|
|
12134
|
+
const row = batch.rows[index];
|
|
12135
|
+
if (!row) return refuse(404, "That review is not part of this batch.");
|
|
12136
|
+
if (row.status !== "failed") return refuse(409, "Only a review that failed can be tried again.");
|
|
12137
|
+
row.status = "waiting";
|
|
12138
|
+
delete row.reason;
|
|
12139
|
+
delete row.detail;
|
|
12140
|
+
delete row.stale;
|
|
12141
|
+
batch.seenAt = Date.now();
|
|
12142
|
+
batch.rerun(index, fresh === true);
|
|
12143
|
+
return {
|
|
12144
|
+
status: 200,
|
|
12145
|
+
body: {
|
|
12146
|
+
rows: rowsOf(batch),
|
|
12147
|
+
finished: false
|
|
12148
|
+
}
|
|
12149
|
+
};
|
|
11944
12150
|
}
|
|
11945
12151
|
/** The recent and discoverable folders, for the Branch panel's list. */
|
|
11946
12152
|
async function listFolders(deps) {
|
|
@@ -11978,7 +12184,7 @@ async function inspectFolder(rawPath, deps, rawBase) {
|
|
|
11978
12184
|
status: 200,
|
|
11979
12185
|
body: {
|
|
11980
12186
|
ok: false,
|
|
11981
|
-
|
|
12187
|
+
...refusalOf(error)
|
|
11982
12188
|
}
|
|
11983
12189
|
};
|
|
11984
12190
|
}
|
|
@@ -12008,10 +12214,14 @@ async function inspectFolder(rawPath, deps, rawBase) {
|
|
|
12008
12214
|
/**
|
|
12009
12215
|
* Review the branch checked out in a folder: `prreviewbuddy` run there.
|
|
12010
12216
|
*
|
|
12011
|
-
*
|
|
12012
|
-
*
|
|
12013
|
-
*
|
|
12014
|
-
*
|
|
12217
|
+
* Always answers with a one-row batch, which is what the panel follows, so a branch shows its
|
|
12218
|
+
* progress exactly as a pull request does. With a free analysis slot it answers once the review
|
|
12219
|
+
* exists, and the analysis runs on in this process, counted by `track` like a Retry. With none free
|
|
12220
|
+
* it answers `waiting` and creates nothing until a slot frees: a review created to wait is a job
|
|
12221
|
+
* nobody is running, which the sweep would fail. A review the branch already has needs no slot.
|
|
12222
|
+
*
|
|
12223
|
+
* A refusal before anything exists comes back as a refusal, with the base question when that is
|
|
12224
|
+
* what it was, so the panel can ask it beside the folder rather than as a failed row.
|
|
12015
12225
|
*/
|
|
12016
12226
|
async function startBranchReview(body, deps) {
|
|
12017
12227
|
const path = typeof body?.path === "string" ? expandPath(body.path, deps.home) : null;
|
|
@@ -12023,7 +12233,10 @@ async function startBranchReview(body, deps) {
|
|
|
12023
12233
|
try {
|
|
12024
12234
|
target = await deps.resolveBranch(path, base);
|
|
12025
12235
|
} catch (error) {
|
|
12026
|
-
return
|
|
12236
|
+
return {
|
|
12237
|
+
status: 400,
|
|
12238
|
+
body: refusalOf(error)
|
|
12239
|
+
};
|
|
12027
12240
|
}
|
|
12028
12241
|
const agent = await agentFor(body?.agent, deps);
|
|
12029
12242
|
if (isAnswer(agent)) return agent;
|
|
@@ -12032,55 +12245,82 @@ async function startBranchReview(body, deps) {
|
|
|
12032
12245
|
label: target.branch,
|
|
12033
12246
|
status: "waiting"
|
|
12034
12247
|
};
|
|
12248
|
+
const batch = newBatch([row]);
|
|
12249
|
+
/**
|
|
12250
|
+
* The branch's run, holding a slot. A retry runs against the commit first resolved, wherever the
|
|
12251
|
+
* branch has moved since, unless `fresh`, which looks at the folder again as it is now.
|
|
12252
|
+
*/
|
|
12253
|
+
const run = async (release, again) => {
|
|
12254
|
+
const fresh = again === "fresh";
|
|
12255
|
+
let replaced = false;
|
|
12256
|
+
try {
|
|
12257
|
+
const next = fresh ? await deps.resolveBranch(path, base) : target;
|
|
12258
|
+
if (again === "retry") {
|
|
12259
|
+
if (!await stillThere(row, target, "branch", deps)) return;
|
|
12260
|
+
if (row.reviewId) return await retryReview(row, batch, deps);
|
|
12261
|
+
}
|
|
12262
|
+
const prepared = await prepareRow(row, next, agent.agentId, model, deps, fresh);
|
|
12263
|
+
target = next;
|
|
12264
|
+
replaced = fresh;
|
|
12265
|
+
await completeRow(row, prepared, batch, deps);
|
|
12266
|
+
} catch (error) {
|
|
12267
|
+
fail(row, error);
|
|
12268
|
+
} finally {
|
|
12269
|
+
if (fresh) keepFreshPending(row, replaced);
|
|
12270
|
+
release();
|
|
12271
|
+
}
|
|
12272
|
+
};
|
|
12273
|
+
batch.rerun = (_index, fresh) => follow(batch, (async () => run(await deps.slots.acquire(), fresh ? "fresh" : "retry"))(), deps);
|
|
12274
|
+
const answer = (outcome) => ({
|
|
12275
|
+
status: 200,
|
|
12276
|
+
body: {
|
|
12277
|
+
outcome,
|
|
12278
|
+
batchId: batch.id,
|
|
12279
|
+
rows: rowsOf(batch),
|
|
12280
|
+
...row.reviewId ? { reviewId: row.reviewId } : {},
|
|
12281
|
+
...row.kept ? { kept: row.kept } : {}
|
|
12282
|
+
}
|
|
12283
|
+
});
|
|
12035
12284
|
const release = deps.slots.tryAcquire();
|
|
12036
12285
|
if (!release) {
|
|
12037
12286
|
const existing = await deps.findExisting(target);
|
|
12038
|
-
if (existing && !deps.resumeOf(existing.id))
|
|
12039
|
-
|
|
12040
|
-
|
|
12041
|
-
|
|
12042
|
-
|
|
12043
|
-
}
|
|
12044
|
-
};
|
|
12045
|
-
const batch = newBatch([row]);
|
|
12046
|
-
deps.track((async () => {
|
|
12047
|
-
const later = await deps.slots.acquire();
|
|
12048
|
-
try {
|
|
12049
|
-
if ((await prepareRow(row, target, agent.agentId, model, deps)).kind !== "existing") await settleRow(row, deps);
|
|
12050
|
-
} catch (error) {
|
|
12051
|
-
row.status = "failed";
|
|
12052
|
-
row.reason = message(error);
|
|
12053
|
-
} finally {
|
|
12054
|
-
later();
|
|
12055
|
-
}
|
|
12056
|
-
})()).finally(() => {
|
|
12057
|
-
batch.finishedAt = Date.now();
|
|
12058
|
-
});
|
|
12059
|
-
return {
|
|
12060
|
-
status: 200,
|
|
12061
|
-
body: {
|
|
12062
|
-
outcome: "waiting",
|
|
12063
|
-
batchId: batch.id
|
|
12287
|
+
if (existing && !deps.resumeOf(existing.id)) {
|
|
12288
|
+
row.reviewId = existing.id;
|
|
12289
|
+
if (!existing.inProgress) {
|
|
12290
|
+
row.status = "already-reviewed";
|
|
12291
|
+
return answer("existing");
|
|
12064
12292
|
}
|
|
12065
|
-
|
|
12293
|
+
row.status = "reviewing";
|
|
12294
|
+
follow(batch, settleRow(row, deps.observe(existing.id)), deps, false);
|
|
12295
|
+
return answer("existing");
|
|
12296
|
+
}
|
|
12297
|
+
follow(batch, (async () => run(await deps.slots.acquire(), "first"))(), deps);
|
|
12298
|
+
return answer("waiting");
|
|
12066
12299
|
}
|
|
12067
12300
|
let prepared;
|
|
12068
12301
|
try {
|
|
12069
12302
|
prepared = await prepareRow(row, target, agent.agentId, model, deps);
|
|
12070
12303
|
} catch (error) {
|
|
12071
12304
|
release();
|
|
12072
|
-
|
|
12305
|
+
batches.delete(batch.id);
|
|
12306
|
+
return {
|
|
12307
|
+
status: 409,
|
|
12308
|
+
body: refusalOf(error)
|
|
12309
|
+
};
|
|
12073
12310
|
}
|
|
12074
|
-
if (prepared.kind === "existing")
|
|
12075
|
-
|
|
12076
|
-
|
|
12077
|
-
|
|
12078
|
-
|
|
12079
|
-
|
|
12080
|
-
|
|
12081
|
-
|
|
12311
|
+
if (prepared.kind === "existing") {
|
|
12312
|
+
await completeRow(row, prepared, batch, deps);
|
|
12313
|
+
release();
|
|
12314
|
+
} else follow(batch, (async () => {
|
|
12315
|
+
try {
|
|
12316
|
+
await settleRow(row, deps.settle(row.reviewId));
|
|
12317
|
+
} catch (error) {
|
|
12318
|
+
fail(row, error);
|
|
12319
|
+
} finally {
|
|
12320
|
+
release();
|
|
12082
12321
|
}
|
|
12083
|
-
};
|
|
12322
|
+
})(), deps);
|
|
12323
|
+
return answer(prepared.kind);
|
|
12084
12324
|
}
|
|
12085
12325
|
async function chooseFolder(deps) {
|
|
12086
12326
|
if (!deps.chooseFolder) return refuse(404, "Choosing a folder is not available on this system. Type its path instead.");
|
|
@@ -13916,6 +14156,8 @@ async function handleAddReview(req, res, pathname, options) {
|
|
|
13916
14156
|
if (route === "pull-requests") return startPullRequestBatch(body, deps);
|
|
13917
14157
|
if (route === "folder/inspect") return inspectFolder(body?.path, deps, body?.base);
|
|
13918
14158
|
if (route === "branch") return startBranchReview(body, deps);
|
|
14159
|
+
const retry = /^batches\/([0-9a-f-]{36})\/retry$/.exec(route);
|
|
14160
|
+
if (retry) return retryBatchRow(retry[1], body?.key, body?.fresh);
|
|
13919
14161
|
return null;
|
|
13920
14162
|
};
|
|
13921
14163
|
const result = await answer();
|
|
@@ -15309,7 +15551,7 @@ function markAbandoned(job, now) {
|
|
|
15309
15551
|
if (isClaimed(job.id, now)) return false;
|
|
15310
15552
|
const current = loadJob(job.id);
|
|
15311
15553
|
if (!current || current.updatedAt !== job.updatedAt) return false;
|
|
15312
|
-
const abandoned = fail(current, ABANDONED_MESSAGE);
|
|
15554
|
+
const abandoned = fail$1(current, ABANDONED_MESSAGE);
|
|
15313
15555
|
saveJob(abandoned);
|
|
15314
15556
|
announceJob(abandoned);
|
|
15315
15557
|
return true;
|