prreviewbuddy 0.76.9 → 0.77.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 +27 -0
- package/dist/{delete_review-BX3lu5Q7.js → delete_review-BHOzMQ0_.js} +49 -1
- package/dist/main.js +1 -1
- package/dist/server.js +315 -90
- package/package.json +1 -1
- package/static/{chunk-egI_P3oB.js → chunk-BoQsVuYd.js} +1 -1
- package/static/chunk-CEQv4zVy.js +1 -0
- 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 +73 -4
- package/static/workspace.js +1 -1
- 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";
|
|
@@ -10688,7 +10688,8 @@ var TOPICS = [
|
|
|
10688
10688
|
<li><code>prreviewbuddy open</code> to list the reviews you have and open this workspace</li>
|
|
10689
10689
|
</ul>
|
|
10690
10690
|
<p>Or press <kbd class="prb-kbd">⌘P</kbd> (<kbd class="prb-kbd">Ctrl+P</kbd> elsewhere) to add a
|
|
10691
|
-
review from here.
|
|
10691
|
+
review from here. The panel follows the reviews it started while it stays open, and closing it
|
|
10692
|
+
stops nothing. <code>prreviewbuddy help</code> lists every command, and
|
|
10692
10693
|
<code>prreviewbuddy review --help</code> every option a review takes.</p>
|
|
10693
10694
|
<p>There is no account and no API key. PR Review Buddy uses the coding agent installed on your
|
|
10694
10695
|
machine and that agent's existing authentication, and never asks you for a credential.</p>`
|
|
@@ -11598,16 +11599,24 @@ function createSlots(capacity) {
|
|
|
11598
11599
|
}
|
|
11599
11600
|
/** How many pull requests one submission may name. See `MAX_PULL_REQUESTS_PER_BATCH`. */
|
|
11600
11601
|
var MAX_BATCH = 20;
|
|
11601
|
-
/**
|
|
11602
|
+
/**
|
|
11603
|
+
* How long a finished batch stays answerable once nobody is asking about it. An open panel keeps
|
|
11604
|
+
* asking (`KEEPALIVE` in the client is well inside this), so a batch is only forgotten after its
|
|
11605
|
+
* panel has gone, and its Retry with it.
|
|
11606
|
+
*/
|
|
11602
11607
|
var FINISHED_BATCH_TTL_MS = 6e5;
|
|
11603
11608
|
function realAddReviewDeps() {
|
|
11604
11609
|
return {
|
|
11605
11610
|
resolveAgent: (flag) => resolveAgent(flag ? { flag } : {}),
|
|
11606
|
-
agents: async () => (await detectAgents()).map((agent) =>
|
|
11607
|
-
|
|
11608
|
-
|
|
11609
|
-
|
|
11610
|
-
|
|
11611
|
+
agents: async () => (await detectAgents()).map((agent) => {
|
|
11612
|
+
const modelHelp = modelHelpOf(agent.id);
|
|
11613
|
+
return {
|
|
11614
|
+
id: agent.id,
|
|
11615
|
+
displayName: agent.displayName,
|
|
11616
|
+
installed: agent.state === "installed",
|
|
11617
|
+
...modelHelp ? { modelHelp } : {}
|
|
11618
|
+
};
|
|
11619
|
+
}),
|
|
11611
11620
|
folders: () => discoverWorkingFolders(),
|
|
11612
11621
|
summaries: () => recentWorkspaces(Number.MAX_SAFE_INTEGER),
|
|
11613
11622
|
remoteOf: originRemote,
|
|
@@ -11631,6 +11640,8 @@ function realAddReviewDeps() {
|
|
|
11631
11640
|
findExisting: findExistingReview,
|
|
11632
11641
|
prepare: prepareReview,
|
|
11633
11642
|
settle: (workspaceId) => settleReview(workspaceId, "standard"),
|
|
11643
|
+
observe: (workspaceId) => observeReview(workspaceId),
|
|
11644
|
+
commitPresent: (root, sha) => refExists(root, sha),
|
|
11634
11645
|
track,
|
|
11635
11646
|
chooseFolder: folderChooser(),
|
|
11636
11647
|
concurrency: 2,
|
|
@@ -11646,6 +11657,13 @@ function realAddReviewDeps() {
|
|
|
11646
11657
|
home: homedir()
|
|
11647
11658
|
};
|
|
11648
11659
|
}
|
|
11660
|
+
function modelHelpOf(agentId) {
|
|
11661
|
+
try {
|
|
11662
|
+
return agentById(agentId).modelSelectionHelp;
|
|
11663
|
+
} catch {
|
|
11664
|
+
return;
|
|
11665
|
+
}
|
|
11666
|
+
}
|
|
11649
11667
|
function agentNameOf(agentId) {
|
|
11650
11668
|
try {
|
|
11651
11669
|
return agentById(agentId).displayName;
|
|
@@ -11653,10 +11671,60 @@ function agentNameOf(agentId) {
|
|
|
11653
11671
|
return agentId;
|
|
11654
11672
|
}
|
|
11655
11673
|
}
|
|
11656
|
-
var refuse = (status, error) => ({
|
|
11674
|
+
var refuse = (status, error, extra = {}) => ({
|
|
11657
11675
|
status,
|
|
11658
|
-
body: {
|
|
11676
|
+
body: {
|
|
11677
|
+
error,
|
|
11678
|
+
...extra
|
|
11679
|
+
}
|
|
11659
11680
|
});
|
|
11681
|
+
function baseChoiceOf(error) {
|
|
11682
|
+
const option = (where) => {
|
|
11683
|
+
const matched = where === "remote" ? error.remote : error.local;
|
|
11684
|
+
const commits = where === "remote" ? error.ranges?.remote : error.ranges?.local;
|
|
11685
|
+
return {
|
|
11686
|
+
base: shorthandOf(matched.ref),
|
|
11687
|
+
display: where === "remote" ? shorthandOf(matched.ref) : shortNameOf(matched.ref),
|
|
11688
|
+
where,
|
|
11689
|
+
...commits ? { commits: commits.length } : {}
|
|
11690
|
+
};
|
|
11691
|
+
};
|
|
11692
|
+
return {
|
|
11693
|
+
name: shortNameOf(error.local.ref),
|
|
11694
|
+
discovered: error.discovered,
|
|
11695
|
+
...error.divergence ? { divergence: error.divergence } : {},
|
|
11696
|
+
options: [option("remote"), option("local")]
|
|
11697
|
+
};
|
|
11698
|
+
}
|
|
11699
|
+
/**
|
|
11700
|
+
* What the panel says for an error a review could not get past.
|
|
11701
|
+
*
|
|
11702
|
+
* Two errors are known by their class and said in the panel's own words. Anything else keeps its
|
|
11703
|
+
* message, which is already written for a person, but only its first paragraph is the sentence:
|
|
11704
|
+
* the rest (ref listings, commands to run) goes behind Technical details, kept whole for whoever
|
|
11705
|
+
* needs to diagnose it.
|
|
11706
|
+
*/
|
|
11707
|
+
function refusalOf(error) {
|
|
11708
|
+
const text = message(error);
|
|
11709
|
+
if (error instanceof AmbiguousRefError) {
|
|
11710
|
+
const choice = baseChoiceOf(error);
|
|
11711
|
+
return {
|
|
11712
|
+
error: `\`${choice.name}\` is both a local and a remote branch, and they are different commits. Choose which one to compare against.`,
|
|
11713
|
+
detail: text,
|
|
11714
|
+
baseChoice: choice
|
|
11715
|
+
};
|
|
11716
|
+
}
|
|
11717
|
+
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." };
|
|
11718
|
+
return briefly(text);
|
|
11719
|
+
}
|
|
11720
|
+
/** A message's first paragraph as the sentence, and the whole of it as the detail when there is more. */
|
|
11721
|
+
function briefly(text) {
|
|
11722
|
+
const first = text.split(/\n\s*\n/)[0].trim();
|
|
11723
|
+
return first && first !== text.trim() ? {
|
|
11724
|
+
error: first,
|
|
11725
|
+
detail: text
|
|
11726
|
+
} : { error: text };
|
|
11727
|
+
}
|
|
11660
11728
|
/** A path as a person reads it, with their home folder as `~`. */
|
|
11661
11729
|
function displayPath(path, home) {
|
|
11662
11730
|
return home && (path === home || path.startsWith(`${home}/`)) ? `~${path.slice(home.length)}` : path;
|
|
@@ -11685,15 +11753,15 @@ function unresolvedAgent(resolution) {
|
|
|
11685
11753
|
* harness's own fallback: an id is always passed down, so `startJob` never has to guess.
|
|
11686
11754
|
*/
|
|
11687
11755
|
async function agentFor(flag, deps) {
|
|
11688
|
-
if (flag !== void 0 && typeof flag !== "string") return refuse(400, "Say which agent by its id.");
|
|
11756
|
+
if (flag !== void 0 && typeof flag !== "string") return refuse(400, "Say which agent by its id.", { field: "agent" });
|
|
11689
11757
|
const resolution = await deps.resolveAgent(flag || void 0);
|
|
11690
11758
|
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));
|
|
11759
|
+
if (resolution.kind === "ambiguous") return refuse(400, "More than one review agent is installed. Choose one under Review options.", { field: "agent" });
|
|
11760
|
+
return refuse(resolution.kind === "unknown-id" ? 400 : 409, unresolvedAgent(resolution), { field: "agent" });
|
|
11693
11761
|
}
|
|
11694
11762
|
function modelOf(value) {
|
|
11695
11763
|
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.");
|
|
11764
|
+
if (typeof value !== "string" || value.length > 200) return refuse(400, "The model must be a short name.", { field: "model" });
|
|
11697
11765
|
return value.trim() || void 0;
|
|
11698
11766
|
}
|
|
11699
11767
|
/**
|
|
@@ -11802,38 +11870,115 @@ async function previewPullRequests(text, deps) {
|
|
|
11802
11870
|
}
|
|
11803
11871
|
var batches = /* @__PURE__ */ new Map();
|
|
11804
11872
|
function forgetFinished(now) {
|
|
11805
|
-
for (const [id, batch] of batches) if (batch.finishedAt !== null && now - batch.finishedAt > FINISHED_BATCH_TTL_MS) batches.delete(id);
|
|
11873
|
+
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
11874
|
}
|
|
11807
11875
|
function newBatch(rows) {
|
|
11808
11876
|
forgetFinished(Date.now());
|
|
11877
|
+
const now = Date.now();
|
|
11809
11878
|
const batch = {
|
|
11810
11879
|
id: randomUUID(),
|
|
11811
11880
|
rows,
|
|
11812
|
-
finishedAt:
|
|
11881
|
+
finishedAt: now,
|
|
11882
|
+
seenAt: now,
|
|
11883
|
+
running: 0,
|
|
11884
|
+
rerun: () => {}
|
|
11813
11885
|
};
|
|
11814
11886
|
batches.set(batch.id, batch);
|
|
11815
11887
|
return batch;
|
|
11816
11888
|
}
|
|
11889
|
+
/**
|
|
11890
|
+
* Count work against a batch, inside the restart guard. The batch is finished only when nothing
|
|
11891
|
+
* counted against it is still going, so a retry reopens a batch an open panel can then follow again.
|
|
11892
|
+
*/
|
|
11893
|
+
function follow(batch, work, deps, driving = true) {
|
|
11894
|
+
batch.running += 1;
|
|
11895
|
+
batch.finishedAt = null;
|
|
11896
|
+
(driving ? deps.track(work) : work).catch(() => {}).finally(() => {
|
|
11897
|
+
batch.running -= 1;
|
|
11898
|
+
if (batch.running === 0) batch.finishedAt = Date.now();
|
|
11899
|
+
});
|
|
11900
|
+
}
|
|
11901
|
+
function fail(row, error) {
|
|
11902
|
+
const refusal = refusalOf(error);
|
|
11903
|
+
row.status = "failed";
|
|
11904
|
+
row.reason = refusal.error;
|
|
11905
|
+
if (refusal.detail) row.detail = refusal.detail;
|
|
11906
|
+
}
|
|
11817
11907
|
var rowsOf = (batch) => batch.rows.map((row) => ({ ...row }));
|
|
11818
11908
|
/**
|
|
11819
11909
|
* Create the review for a row's target, or find the one it has, under the per-target lock. The
|
|
11820
11910
|
* caller holds an analysis slot: nothing is created without one, so nothing created waits.
|
|
11821
11911
|
*/
|
|
11822
|
-
|
|
11823
|
-
|
|
11912
|
+
/**
|
|
11913
|
+
* `fresh` is a fresh start's: a new review of the target as it is now, keeping whatever review the
|
|
11914
|
+
* branch or request already has, as `--fresh` does. Without it the lookup finds the latest review of
|
|
11915
|
+
* the same branch or request, which for a fresh start is the very failure being started over.
|
|
11916
|
+
*/
|
|
11917
|
+
async function prepareRow(row, target, agentId, model, deps, fresh = false) {
|
|
11918
|
+
const prepared = await oneCreationAtATime(target, () => deps.prepare(target, agentId, fresh, model));
|
|
11824
11919
|
row.reviewId = prepared.kind === "new" ? prepared.workspaceId : prepared.existing.id;
|
|
11825
|
-
if (prepared.kind === "existing") row.status = "already-reviewed";
|
|
11920
|
+
if (prepared.kind === "existing") row.status = prepared.existing.inProgress ? "reviewing" : "already-reviewed";
|
|
11826
11921
|
else row.status = "reviewing";
|
|
11827
11922
|
if (prepared.kind === "resume") row.kept = keptOf(prepared.failure.agentId, prepared.failure.requestedModel, deps);
|
|
11923
|
+
else delete row.kept;
|
|
11828
11924
|
return prepared;
|
|
11829
11925
|
}
|
|
11830
|
-
|
|
11831
|
-
|
|
11926
|
+
/**
|
|
11927
|
+
* Take a prepared row to its end. A new or resumed review is run here, holding the slot. One that
|
|
11928
|
+
* something else is already analysing is watched instead, never driven: the watch is handed to the
|
|
11929
|
+
* batch rather than awaited, so the caller returns, and its slot goes back, at once, while the row
|
|
11930
|
+
* stays Analysing until that analysis ends. Counted against the batch, not against restarts.
|
|
11931
|
+
*/
|
|
11932
|
+
async function completeRow(row, prepared, batch, deps) {
|
|
11933
|
+
if (prepared.kind !== "existing") return settleRow(row, deps.settle(row.reviewId));
|
|
11934
|
+
if (prepared.existing.inProgress) follow(batch, settleRow(row, deps.observe(row.reviewId)), deps, false);
|
|
11935
|
+
}
|
|
11936
|
+
async function settleRow(row, settling) {
|
|
11937
|
+
const settled = await settling;
|
|
11832
11938
|
if (settled.ok) row.status = "ready";
|
|
11833
|
-
else {
|
|
11939
|
+
else if (settled.stopped) {
|
|
11834
11940
|
row.status = "failed";
|
|
11835
|
-
row.reason =
|
|
11941
|
+
row.reason = "Stopped";
|
|
11942
|
+
} else fail(row, settled.reason);
|
|
11943
|
+
}
|
|
11944
|
+
/**
|
|
11945
|
+
* A retry runs against the commit the row was first resolved to, never a newer one: a retry
|
|
11946
|
+
* resumes what was asked for, and a moved branch or pull request is a fresh review's business. When
|
|
11947
|
+
* that commit has gone (a force push, then a prune), there is nothing left to retry, and the row
|
|
11948
|
+
* says so and offers the fresh start instead of quietly reviewing something else.
|
|
11949
|
+
*/
|
|
11950
|
+
async function stillThere(row, target, what, deps) {
|
|
11951
|
+
if (await deps.commitPresent(target.originRepoPath, target.sha)) return true;
|
|
11952
|
+
row.status = "failed";
|
|
11953
|
+
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.`;
|
|
11954
|
+
row.stale = true;
|
|
11955
|
+
return false;
|
|
11956
|
+
}
|
|
11957
|
+
/**
|
|
11958
|
+
* Retry a row whose review exists, by that review's id. Not through the lookup a start uses, which
|
|
11959
|
+
* finds the latest review of the branch or request: one made since, of newer commits, would be
|
|
11960
|
+
* handed back in place of the one that failed, and a retry would quietly become a different review.
|
|
11961
|
+
*
|
|
11962
|
+
* Resumed when it is still a failure nothing is running, as its own page's Retry does. Otherwise
|
|
11963
|
+
* something has taken it up since (that Retry, a terminal), and it is watched rather than run again.
|
|
11964
|
+
*/
|
|
11965
|
+
async function retryReview(row, batch, deps) {
|
|
11966
|
+
const reviewId = row.reviewId;
|
|
11967
|
+
const kept = deps.resumeOf(reviewId);
|
|
11968
|
+
row.status = "reviewing";
|
|
11969
|
+
if (kept) {
|
|
11970
|
+
row.kept = kept;
|
|
11971
|
+
return settleRow(row, deps.settle(reviewId));
|
|
11836
11972
|
}
|
|
11973
|
+
follow(batch, settleRow(row, deps.observe(reviewId)), deps, false);
|
|
11974
|
+
}
|
|
11975
|
+
/**
|
|
11976
|
+
* A fresh start that ended before it made its review leaves the row exactly as it was: the old
|
|
11977
|
+
* target, the old review, and still `stale`. So the row keeps offering the fresh start, and a plain
|
|
11978
|
+
* Retry finds the old commit gone again, rather than resuming the old review against it.
|
|
11979
|
+
*/
|
|
11980
|
+
function keepFreshPending(row, replaced) {
|
|
11981
|
+
if (row.status === "failed" && !replaced) row.stale = true;
|
|
11837
11982
|
}
|
|
11838
11983
|
function keptOf(agentId, model, deps) {
|
|
11839
11984
|
return {
|
|
@@ -11879,40 +12024,55 @@ async function startPullRequestBatch(body, deps) {
|
|
|
11879
12024
|
})));
|
|
11880
12025
|
/** Which row claimed each pull request, by what it resolved to rather than how it was spelled. */
|
|
11881
12026
|
const claimed = /* @__PURE__ */ new Map();
|
|
11882
|
-
|
|
12027
|
+
/** Each row's pull request as it was first resolved: head, base and all. What a retry runs against. */
|
|
12028
|
+
const resolved = [];
|
|
12029
|
+
const lane = async (item, index, fresh = false) => {
|
|
11883
12030
|
const row = batch.rows[index];
|
|
11884
12031
|
const release = await deps.slots.acquire();
|
|
11885
12032
|
row.status = "reviewing";
|
|
12033
|
+
/** A fresh start has made its review, so the row now belongs to the new target. */
|
|
12034
|
+
let replaced = false;
|
|
11886
12035
|
try {
|
|
11887
|
-
|
|
11888
|
-
if (
|
|
11889
|
-
row
|
|
11890
|
-
row.
|
|
11891
|
-
|
|
12036
|
+
let target = fresh ? void 0 : resolved[index];
|
|
12037
|
+
if (target) {
|
|
12038
|
+
if (!await stillThere(row, target, "pull request", deps)) return;
|
|
12039
|
+
if (row.reviewId) return await retryReview(row, batch, deps);
|
|
12040
|
+
} else {
|
|
12041
|
+
const remote = await deps.remoteOf(item.folder);
|
|
12042
|
+
if (!isGitHubRepository(remote, item.owner, item.repo)) {
|
|
12043
|
+
row.status = "failed";
|
|
12044
|
+
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}`;
|
|
12045
|
+
return;
|
|
12046
|
+
}
|
|
12047
|
+
target = await deps.resolvePullRequest(item.folder, item.url);
|
|
12048
|
+
if (!fresh) resolved[index] = target;
|
|
11892
12049
|
}
|
|
11893
|
-
const target = await deps.resolvePullRequest(item.folder, item.url);
|
|
11894
12050
|
const identity = batchIdentity(target);
|
|
11895
12051
|
const first = claimed.get(identity);
|
|
11896
|
-
if (first !== void 0) {
|
|
12052
|
+
if (first !== void 0 && first !== index) {
|
|
11897
12053
|
row.status = "already-reviewed";
|
|
11898
12054
|
row.reason = `Same pull request as ${batch.rows[first].label}`;
|
|
11899
12055
|
return;
|
|
11900
12056
|
}
|
|
11901
12057
|
claimed.set(identity, index);
|
|
11902
|
-
|
|
12058
|
+
const prepared = await prepareRow(row, target, agent.agentId, model, deps, fresh);
|
|
12059
|
+
if (fresh) {
|
|
12060
|
+
resolved[index] = target;
|
|
12061
|
+
replaced = true;
|
|
12062
|
+
}
|
|
12063
|
+
await completeRow(row, prepared, batch, deps);
|
|
11903
12064
|
} catch (error) {
|
|
11904
|
-
row
|
|
11905
|
-
row.reason = message(error);
|
|
12065
|
+
fail(row, error);
|
|
11906
12066
|
} finally {
|
|
12067
|
+
if (fresh) keepFreshPending(row, replaced);
|
|
11907
12068
|
release();
|
|
11908
12069
|
}
|
|
11909
12070
|
};
|
|
11910
|
-
|
|
12071
|
+
batch.rerun = (index, fresh) => follow(batch, lane(items[index], index, fresh), deps);
|
|
12072
|
+
follow(batch, runBatch(items, {
|
|
11911
12073
|
concurrency: deps.concurrency,
|
|
11912
12074
|
run: lane
|
|
11913
|
-
})
|
|
11914
|
-
batch.finishedAt = Date.now();
|
|
11915
|
-
});
|
|
12075
|
+
}), deps);
|
|
11916
12076
|
return {
|
|
11917
12077
|
status: 200,
|
|
11918
12078
|
body: {
|
|
@@ -11922,13 +12082,13 @@ async function startPullRequestBatch(body, deps) {
|
|
|
11922
12082
|
};
|
|
11923
12083
|
}
|
|
11924
12084
|
/**
|
|
11925
|
-
* A batch's rows as they stand.
|
|
11926
|
-
*
|
|
11927
|
-
* restart, which is the case worth saying plainly: whatever had not started never will.
|
|
12085
|
+
* A batch's rows as they stand. Asking counts as watching: a batch an open panel is still asking
|
|
12086
|
+
* about is never forgotten, so its Retry stays good for as long as the panel is open.
|
|
11928
12087
|
*/
|
|
11929
12088
|
function batchRows(id) {
|
|
11930
12089
|
const batch = batches.get(id);
|
|
11931
|
-
if (!batch) return
|
|
12090
|
+
if (!batch) return lostBatch();
|
|
12091
|
+
batch.seenAt = Date.now();
|
|
11932
12092
|
return {
|
|
11933
12093
|
status: 200,
|
|
11934
12094
|
body: {
|
|
@@ -11937,10 +12097,39 @@ function batchRows(id) {
|
|
|
11937
12097
|
}
|
|
11938
12098
|
};
|
|
11939
12099
|
}
|
|
11940
|
-
/**
|
|
11941
|
-
|
|
11942
|
-
|
|
11943
|
-
|
|
12100
|
+
/**
|
|
12101
|
+
* Said without guessing why. A restart forgets every batch, and a batch nobody has asked about for
|
|
12102
|
+
* a while is forgotten too; the server cannot tell which happened, so it says what follows from
|
|
12103
|
+
* either.
|
|
12104
|
+
*/
|
|
12105
|
+
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.");
|
|
12106
|
+
/**
|
|
12107
|
+
* Run one failed row of a batch again, as it was first asked for: the same folder, agent, model
|
|
12108
|
+
* and base, and the same commits, never the branch or pull request as it has since moved on. A
|
|
12109
|
+
* review that was created and then failed resumes, as a Retry does on its own page. `fresh` is the
|
|
12110
|
+
* one exception, sent only when those commits have gone: it looks again at what is there now.
|
|
12111
|
+
* The other rows are left exactly as they are, and the batch is followed again until it ends.
|
|
12112
|
+
*/
|
|
12113
|
+
function retryBatchRow(id, key, fresh = false) {
|
|
12114
|
+
const batch = batches.get(id);
|
|
12115
|
+
if (!batch) return lostBatch();
|
|
12116
|
+
const index = batch.rows.findIndex((row) => row.key === key);
|
|
12117
|
+
const row = batch.rows[index];
|
|
12118
|
+
if (!row) return refuse(404, "That review is not part of this batch.");
|
|
12119
|
+
if (row.status !== "failed") return refuse(409, "Only a review that failed can be tried again.");
|
|
12120
|
+
row.status = "waiting";
|
|
12121
|
+
delete row.reason;
|
|
12122
|
+
delete row.detail;
|
|
12123
|
+
delete row.stale;
|
|
12124
|
+
batch.seenAt = Date.now();
|
|
12125
|
+
batch.rerun(index, fresh === true);
|
|
12126
|
+
return {
|
|
12127
|
+
status: 200,
|
|
12128
|
+
body: {
|
|
12129
|
+
rows: rowsOf(batch),
|
|
12130
|
+
finished: false
|
|
12131
|
+
}
|
|
12132
|
+
};
|
|
11944
12133
|
}
|
|
11945
12134
|
/** The recent and discoverable folders, for the Branch panel's list. */
|
|
11946
12135
|
async function listFolders(deps) {
|
|
@@ -11978,7 +12167,7 @@ async function inspectFolder(rawPath, deps, rawBase) {
|
|
|
11978
12167
|
status: 200,
|
|
11979
12168
|
body: {
|
|
11980
12169
|
ok: false,
|
|
11981
|
-
|
|
12170
|
+
...refusalOf(error)
|
|
11982
12171
|
}
|
|
11983
12172
|
};
|
|
11984
12173
|
}
|
|
@@ -12008,10 +12197,14 @@ async function inspectFolder(rawPath, deps, rawBase) {
|
|
|
12008
12197
|
/**
|
|
12009
12198
|
* Review the branch checked out in a folder: `prreviewbuddy` run there.
|
|
12010
12199
|
*
|
|
12011
|
-
*
|
|
12012
|
-
*
|
|
12013
|
-
*
|
|
12014
|
-
*
|
|
12200
|
+
* Always answers with a one-row batch, which is what the panel follows, so a branch shows its
|
|
12201
|
+
* progress exactly as a pull request does. With a free analysis slot it answers once the review
|
|
12202
|
+
* exists, and the analysis runs on in this process, counted by `track` like a Retry. With none free
|
|
12203
|
+
* it answers `waiting` and creates nothing until a slot frees: a review created to wait is a job
|
|
12204
|
+
* nobody is running, which the sweep would fail. A review the branch already has needs no slot.
|
|
12205
|
+
*
|
|
12206
|
+
* A refusal before anything exists comes back as a refusal, with the base question when that is
|
|
12207
|
+
* what it was, so the panel can ask it beside the folder rather than as a failed row.
|
|
12015
12208
|
*/
|
|
12016
12209
|
async function startBranchReview(body, deps) {
|
|
12017
12210
|
const path = typeof body?.path === "string" ? expandPath(body.path, deps.home) : null;
|
|
@@ -12023,7 +12216,10 @@ async function startBranchReview(body, deps) {
|
|
|
12023
12216
|
try {
|
|
12024
12217
|
target = await deps.resolveBranch(path, base);
|
|
12025
12218
|
} catch (error) {
|
|
12026
|
-
return
|
|
12219
|
+
return {
|
|
12220
|
+
status: 400,
|
|
12221
|
+
body: refusalOf(error)
|
|
12222
|
+
};
|
|
12027
12223
|
}
|
|
12028
12224
|
const agent = await agentFor(body?.agent, deps);
|
|
12029
12225
|
if (isAnswer(agent)) return agent;
|
|
@@ -12032,55 +12228,82 @@ async function startBranchReview(body, deps) {
|
|
|
12032
12228
|
label: target.branch,
|
|
12033
12229
|
status: "waiting"
|
|
12034
12230
|
};
|
|
12231
|
+
const batch = newBatch([row]);
|
|
12232
|
+
/**
|
|
12233
|
+
* The branch's run, holding a slot. A retry runs against the commit first resolved, wherever the
|
|
12234
|
+
* branch has moved since, unless `fresh`, which looks at the folder again as it is now.
|
|
12235
|
+
*/
|
|
12236
|
+
const run = async (release, again) => {
|
|
12237
|
+
const fresh = again === "fresh";
|
|
12238
|
+
let replaced = false;
|
|
12239
|
+
try {
|
|
12240
|
+
const next = fresh ? await deps.resolveBranch(path, base) : target;
|
|
12241
|
+
if (again === "retry") {
|
|
12242
|
+
if (!await stillThere(row, target, "branch", deps)) return;
|
|
12243
|
+
if (row.reviewId) return await retryReview(row, batch, deps);
|
|
12244
|
+
}
|
|
12245
|
+
const prepared = await prepareRow(row, next, agent.agentId, model, deps, fresh);
|
|
12246
|
+
target = next;
|
|
12247
|
+
replaced = fresh;
|
|
12248
|
+
await completeRow(row, prepared, batch, deps);
|
|
12249
|
+
} catch (error) {
|
|
12250
|
+
fail(row, error);
|
|
12251
|
+
} finally {
|
|
12252
|
+
if (fresh) keepFreshPending(row, replaced);
|
|
12253
|
+
release();
|
|
12254
|
+
}
|
|
12255
|
+
};
|
|
12256
|
+
batch.rerun = (_index, fresh) => follow(batch, (async () => run(await deps.slots.acquire(), fresh ? "fresh" : "retry"))(), deps);
|
|
12257
|
+
const answer = (outcome) => ({
|
|
12258
|
+
status: 200,
|
|
12259
|
+
body: {
|
|
12260
|
+
outcome,
|
|
12261
|
+
batchId: batch.id,
|
|
12262
|
+
rows: rowsOf(batch),
|
|
12263
|
+
...row.reviewId ? { reviewId: row.reviewId } : {},
|
|
12264
|
+
...row.kept ? { kept: row.kept } : {}
|
|
12265
|
+
}
|
|
12266
|
+
});
|
|
12035
12267
|
const release = deps.slots.tryAcquire();
|
|
12036
12268
|
if (!release) {
|
|
12037
12269
|
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
|
|
12270
|
+
if (existing && !deps.resumeOf(existing.id)) {
|
|
12271
|
+
row.reviewId = existing.id;
|
|
12272
|
+
if (!existing.inProgress) {
|
|
12273
|
+
row.status = "already-reviewed";
|
|
12274
|
+
return answer("existing");
|
|
12064
12275
|
}
|
|
12065
|
-
|
|
12276
|
+
row.status = "reviewing";
|
|
12277
|
+
follow(batch, settleRow(row, deps.observe(existing.id)), deps, false);
|
|
12278
|
+
return answer("existing");
|
|
12279
|
+
}
|
|
12280
|
+
follow(batch, (async () => run(await deps.slots.acquire(), "first"))(), deps);
|
|
12281
|
+
return answer("waiting");
|
|
12066
12282
|
}
|
|
12067
12283
|
let prepared;
|
|
12068
12284
|
try {
|
|
12069
12285
|
prepared = await prepareRow(row, target, agent.agentId, model, deps);
|
|
12070
12286
|
} catch (error) {
|
|
12071
12287
|
release();
|
|
12072
|
-
|
|
12288
|
+
batches.delete(batch.id);
|
|
12289
|
+
return {
|
|
12290
|
+
status: 409,
|
|
12291
|
+
body: refusalOf(error)
|
|
12292
|
+
};
|
|
12073
12293
|
}
|
|
12074
|
-
if (prepared.kind === "existing")
|
|
12075
|
-
|
|
12076
|
-
|
|
12077
|
-
|
|
12078
|
-
|
|
12079
|
-
|
|
12080
|
-
|
|
12081
|
-
|
|
12294
|
+
if (prepared.kind === "existing") {
|
|
12295
|
+
await completeRow(row, prepared, batch, deps);
|
|
12296
|
+
release();
|
|
12297
|
+
} else follow(batch, (async () => {
|
|
12298
|
+
try {
|
|
12299
|
+
await settleRow(row, deps.settle(row.reviewId));
|
|
12300
|
+
} catch (error) {
|
|
12301
|
+
fail(row, error);
|
|
12302
|
+
} finally {
|
|
12303
|
+
release();
|
|
12082
12304
|
}
|
|
12083
|
-
};
|
|
12305
|
+
})(), deps);
|
|
12306
|
+
return answer(prepared.kind);
|
|
12084
12307
|
}
|
|
12085
12308
|
async function chooseFolder(deps) {
|
|
12086
12309
|
if (!deps.chooseFolder) return refuse(404, "Choosing a folder is not available on this system. Type its path instead.");
|
|
@@ -13916,6 +14139,8 @@ async function handleAddReview(req, res, pathname, options) {
|
|
|
13916
14139
|
if (route === "pull-requests") return startPullRequestBatch(body, deps);
|
|
13917
14140
|
if (route === "folder/inspect") return inspectFolder(body?.path, deps, body?.base);
|
|
13918
14141
|
if (route === "branch") return startBranchReview(body, deps);
|
|
14142
|
+
const retry = /^batches\/([0-9a-f-]{36})\/retry$/.exec(route);
|
|
14143
|
+
if (retry) return retryBatchRow(retry[1], body?.key, body?.fresh);
|
|
13919
14144
|
return null;
|
|
13920
14145
|
};
|
|
13921
14146
|
const result = await answer();
|
|
@@ -15309,7 +15534,7 @@ function markAbandoned(job, now) {
|
|
|
15309
15534
|
if (isClaimed(job.id, now)) return false;
|
|
15310
15535
|
const current = loadJob(job.id);
|
|
15311
15536
|
if (!current || current.updatedAt !== job.updatedAt) return false;
|
|
15312
|
-
const abandoned = fail(current, ABANDONED_MESSAGE);
|
|
15537
|
+
const abandoned = fail$1(current, ABANDONED_MESSAGE);
|
|
15313
15538
|
saveJob(abandoned);
|
|
15314
15539
|
announceJob(abandoned);
|
|
15315
15540
|
return true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prreviewbuddy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.77.1",
|
|
4
4
|
"description": "Understand code changes before you approve them. Local branch and pull request reviews with Claude Code, Codex or Gemini CLI.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://prreviewbuddy.com",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{X as e}from"./chunk-
|
|
1
|
+
import{X as e}from"./chunk-CEQv4zVy.js";async function t(e,t){try{await navigator.clipboard.writeText(t);let n=e.textContent;e.textContent=`Copied`,window.setTimeout(()=>{e.textContent=n},1200)}catch{let n=document.createElement(`input`);n.type=`text`,n.readOnly=!0,n.value=t,n.className=`prb-menu-id`,n.setAttribute(`data-id-field`,``),e.after(n),n.select()}}var n=`button:not([disabled])`;function r(e){return e.querySelector(`[data-menu-panel]`)}function i(e){return e.querySelector(`[data-menu-trigger]`)}function a(e){let t=r(e);return t?[...t.querySelectorAll(`input:not([disabled]), textarea:not([disabled]), button:not([disabled])`)].filter(e=>!e.closest(`[hidden]`)):[]}function o(e){let t=r(e);return t?[...t.querySelectorAll(n)].filter(e=>!e.closest(`[hidden]`)):[]}var s=`data-menu-placement`;function c(e){let t=e.ownerDocument.defaultView,n=0,r=t?.innerHeight??1/0;for(let i=e.parentElement;i&&t;i=i.parentElement){let e=t.getComputedStyle(i).overflowY;if(e===`visible`||e===``)continue;let a=i.getBoundingClientRect();n=Math.max(n,a.top),r=Math.min(r,a.bottom)}return{top:n,bottom:r}}var l=8,u=120;function d(e){let t=r(e),n=i(e);if(!t||!n)return;t.removeAttribute(s),t.style.removeProperty(`max-height`),t.style.removeProperty(`overflow-y`),t.style.removeProperty(`box-sizing`),t.scrollTop=0;let a=t.getBoundingClientRect(),o=n.getBoundingClientRect(),d=c(e),f=Math.max(0,a.top-o.bottom),p=d.bottom-o.bottom-f-l,m=o.top-d.top-f-l,h=a.height,g=h<=p?`below`:h<=m||m>p?`above`:`below`;g===`above`&&t.setAttribute(s,`above`);let _=g===`above`?m:p;h<=_||(t.style.boxSizing=`border-box`,t.style.maxHeight=`${Math.max(Math.floor(_),u)}px`,t.style.overflowY=`auto`)}function f(e){return!e.metaKey&&!e.ctrlKey&&!e.altKey&&!e.shiftKey?e.key===`j`?`ArrowDown`:e.key===`k`?`ArrowUp`:e.key:e.key}function p(t,n={}){let s=null,c=null,l=n.within?`${n.within} [data-menu]`:`[data-menu]`,u=e=>e&&(!n.within||e.closest(n.within))?e:null,p=(e,t={})=>{let a=r(e);if(!a||a.hidden)return;a.hidden=!0;let o=i(e);o?.setAttribute(`aria-expanded`,`false`);let l=s===e?c:null;s===e&&(s=null,c=null),n.onClose?.(e),t.returnFocus&&(l??(()=>o?.focus()))()},m=()=>{for(let e of t.querySelectorAll(l))p(e)},h=(e,t,a=null)=>{let l=r(e);l&&(m(),c=a,n.onOpen?.(e),l.hidden=!1,d(e),i(e)?.setAttribute(`aria-expanded`,`true`),s=e,t&&o(e)[0]?.focus())};return t.addEventListener(`click`,e=>{let t=e.target,n=u(t?.closest?.(`[data-menu]`)??null);if(!n){m();return}if(t?.closest(`[data-menu-trigger]`)){let t=e.detail===0;r(n)?.hidden?h(n,t):p(n);return}s&&s!==n&&p(s)}),t.addEventListener(`keydown`,t=>{if(s&&!s.isConnected&&(s=null),!s)return;let n=s;if(t.key===`Escape`){t.preventDefault(),p(n,{returnFocus:!0});return}if(!n.contains(t.target))return;if(t.key===`Tab`){if(n.hasAttribute(`data-menu-form`)){let e=a(n),r=e.indexOf(t.target);if(e.length>0){t.preventDefault(),e[(r+(t.shiftKey?-1:1)+e.length)%e.length].focus();return}}p(n);return}if(e(t.target))return;let r=o(n);if(r.length===0)return;let i=r.indexOf(t.target),c=f(t),l=c===`ArrowDown`?r[(i+1)%r.length]:c===`ArrowUp`?r[(i-1+r.length)%r.length]:c===`Home`?r[0]:c===`End`?r[r.length-1]:null;l&&(t.preventDefault(),l.focus(),l.scrollIntoView({block:`nearest`}))}),{open:(e,t={})=>h(e,t.focusFirst===!0,t.returnFocus??null),close:p,closeAll:m}}function m(e){return e<3e4?1e3:e<12e4?3e3:e<6e5?8e3:15e3}var h=`Fetch selected`;function g(e){return`${e} ${e===1?`repository`:`repositories`}`}function _(e){return`Fetching ${g(e)}…`}function v(e){return e.fetched===0?`Could not fetch ${g(e.repositories)}`:e.fetched===e.repositories?`Fetched ${g(e.repositories)}`:`Fetched ${e.fetched} of ${g(e.repositories)}`}function y(e,t){let n=new Set,r=new Set;for(let i of e)for(let e of i.lineage){let i=t[e];i!==void 0&&(n.add(e),r.add(i))}return{ids:[...n],repositories:r.size}}function b(e,t){return y(t,e).repositories>0}export{_ as a,t as c,v as i,b as n,m as o,y as r,p as s,h as t};
|