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/dist/server.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as resolveWorkspacePort, $r as explanationOf, $t as readNotification, A as parsePullRequestLink, Ai as readAgentPreference, An as MANAGED_ROOT, Ar as activityLabel, At as storeArchitecture, Bn as checkCodeFreshness, Br as queueRemove, Bt as branchOf, C as completeRun, Ci as DetachedHeadError, Cn as renderMarkdown, Ct as describeRuntimes, D as relativeTime$1, Di as resolveAgent, Dn as renderInlineText, Dr as workspaceStamps, Dt as partFiles, E as stopRun, Ei as git, En as escapeHtml$1, Er as workspaceRevision, Et as loadArchitecture, Fi as agentById, Fn as removeWorktree, Fr as offersLifecycle, Ft as REPOSITORY_SCOPE, G as stopChild, Gn as forgeResolver, Gr as isQuestionOutstanding, Gt as createPrUpdate, Hn as followedBranch, Hr as readQueue, Ht as categoryOf, I as settleQueue, Ii as agentFor$1, In as requestJobStop, Ir as parseLifecycle, It as ancestorsOf, J as writeServerRecord, Jn as endedAt, Jr as resolvedByConversation, Jt as markAllRead, Kn as fillFileUrlTemplate, Kr as issuesOutstanding, Kt as eventName, L as readIndexToken, Li as detectAgents, Lr as sectionOf, Lt as isArchitectureId, M as runBatch, Mn as holdWorktree, Mr as lifecycleChip, Mt as trackedFiles, N as activateQueued, Ni as STORE_ROOT, Nn as isWorktreeHeld, Nr as lifecycleItemLabel, Nt as ownersOf, O as batchIdentity, Oi as resolveModel, Or as LIFECYCLES, Ot as readArchitecture, P as compactQueue, Pi as AGENT_IDS, Pn as readMarker, Pr as lifecyclePill, Pt as QUOTE_LIMITS, Q as parseWorkspacePort, Qn as saveJob, Qr as explanationActionOf, Qt as pruneNotifications, Ri as AgentCancelledError, Rr as queueAdd, Rt as writeJsonAtomically, S as beginRun, Si as originRemote, Sn as GITHUB_MARKDOWN, Sr as reviewedCommit, St as RUNTIME_WORDS, T as reportRun, Ti as resolveBase, Tn as CODE_COPY_CLASS, Tr as touchWorkspace, Tt as checkArchitecture, U as serverEntryExists, Un as reviewedShaOf, Ur as doneVerb, Ut as createArchitectureNotification, V as readServerRecord, Vn as checkReview, Vr as queueStamp, Vt as subjectOf, W as spawnServer, Wn as fetchHead, Wr as isIssueOutstanding, Wt as createPrState, X as MAX_WORKSPACE_PORT, Xn as isTerminal, Xr as processDiscussion, Xt as markReadForReview, Y as DEFAULT_WORKSPACE_PORT, Yn as fail, Yr as reviewerDispositions, Yt as markRead, Z as MIN_WORKSPACE_PORT, Zn as loadJob, Zr as recommendationsOf, Zt as moveIds, _i as objectWithAnyOf, _n as EXPLAIN_SIMPLY_PROMPT, _r as mutateWorkspace, a as wasBlocked, ai as currentGradeIn, an as unreadCount$1, ar as isClaimed, at as reanalyseReview, b as track, bi as kissOutcome, bn as authorPreview, br as previousKissResult, bt as mapRuntimes, c as RefreshUnavailableError, ci as gradeScale, cn as ASK_REREAD_CEILING_MS, cr as groupByLineage, d as mergeUpdateInto, di as reassessIn, dn as saveRefresh, dr as kissDecisionRefusal, ei as isExplanationAction, en as readStateOf, er as isLiveJob, et as statedWorkspacePort, f as updateReview, fi as sourcePhrase, fn as storedConversation, fr as kissDecisionsOf, gi as extractJsonText, gn as agentEnvOf, gt as BUILD_VERSION, h as liveUpdateFor, hi as asString, hn as isUnchanged, hr as loadWorkspace, i as liveJobsFor, ii as assessmentLookup, in as unreadAround, it as settleReview, j as unsupportedReason, ji as writeAgentPreference, jn as createWorktree, jr as isLifecycle, jt as storedScopes, k as extractPullRequestLinks, ki as clearAgentPreference, kn as ReviewBeingDeletedError, kr as activityAttributes, kt as repositoryKey, l as cancelUpdate, li as isGradeFor, ln as adoptLegacyBaseline, lr as indexKeptIds, lt as readEvents, m as UpdateAlreadyRunningError, mi as followUpFindingId, mn as refreshPrContext, mr as lineagePosition, mt as telemetryUploadConsent, n as deleteReview, ni as ANALYSIS_ONLY, nn as retireNotification, nr as progressSteps, nt as failureOf$1, o as discardJob, oi as findingAssessment, on as withUsageRecorded, or as decideKissRecommendation, ot as runJob, p as askCheckout, pi as wasReassessed, pn as unforeseenFailure, pr as latestKissRun, pt as recordTelemetryUploadConsent, qn as allJobs, qr as questionsOutstanding, qt as listNotifications, r as lineageIds, ri as analysisRefOf, rn as toneOf, rr as purposeOf, rt as prepareReview, s as FETCH_TIMEOUT_MS, si as findingIn, sn as existingPaths, sr as followedRefName, t as deleteLineage, ti as withExplanation, tn as removeNotification, tr as phasesFor, tt as writePortPreference, u as cancellableHere, ui as promptGrade, un as prepareConversationWithin, ur as indexReviewGroups, ut as record, v as startKissJob, vi as describeAuthorship, vn as chatConversationSection, vt as feedbackUrl, w as endRun, wi as isCommitReview, wn as CODE_BLOCK_CLASS, wt as ARCHITECTURE_DIR, x as RUN_KINDS, xi as resolveTarget, xn as hasAuthorContent, xr as recentWorkspaces, xt as recordRuntimeBestEffort, y as isDriving, yi as WITHHELD_REASON, yn as UNESTABLISHED_MOTIVATION, yr as presentedKissRunOf, yt as waitingHistory, z as clearServerRecordOwnedBy, zi as AgentUnavailableError, zn as findExistingReview, zr as queueAddMany, zt as announceJob } from "./delete_review-BX3lu5Q7.js";
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. <code>prreviewbuddy help</code> lists every command, and
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
- /** How long a finished batch stays answerable, for a panel that is still open on it. */
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
- id: agent.id,
11608
- displayName: agent.displayName,
11609
- installed: agent.state === "installed"
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: { error }
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: null
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
- async function prepareRow(row, target, agentId, model, deps) {
11823
- const prepared = await oneCreationAtATime(target, () => deps.prepare(target, agentId, false, model));
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
- async function settleRow(row, deps) {
11831
- const settled = await deps.settle(row.reviewId);
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 = settled.stopped ? "Stopped" : settled.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
- const lane = async (item, index) => {
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
- const remote = await deps.remoteOf(item.folder);
11888
- if (!isGitHubRepository(remote, item.owner, item.repo)) {
11889
- row.status = "failed";
11890
- 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}`;
11891
- return;
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
- if ((await prepareRow(row, target, agent.agentId, model, deps)).kind !== "existing") await settleRow(row, deps);
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.status = "failed";
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
- deps.track(runBatch(items, {
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
- })).catch(() => {}).finally(() => {
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. A batch this server does not know was either finished long ago,
11926
- * which an open panel never asks about because it stops asking once a batch finishes, or lost to a
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 refuse(404, "The workspace server no longer knows this batch, most likely because it restarted. Reviews that had started are in your list. Any that were still waiting did not start.");
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
- /** A target refusal in the panel's words where the CLI's would name a command. */
11941
- function targetRefusal(error) {
11942
- if (error instanceof DetachedHeadError) return "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.";
11943
- return message(error);
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
- error: targetRefusal(error)
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
- * With a free analysis slot it answers once the review exists, and the analysis runs on in this
12012
- * process, counted by `track` like a Retry. With none free it answers `waiting` and a one-row batch
12013
- * to watch, and creates nothing until a slot frees: a review created to wait is a job nobody is
12014
- * running, which the sweep would fail. A review the branch already has needs no slot to hand back.
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 refuse(400, targetRefusal(error));
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)) return {
12039
- status: 200,
12040
- body: {
12041
- outcome: "existing",
12042
- reviewId: existing.id
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
- return refuse(409, message(error));
12288
+ batches.delete(batch.id);
12289
+ return {
12290
+ status: 409,
12291
+ body: refusalOf(error)
12292
+ };
12073
12293
  }
12074
- if (prepared.kind === "existing") release();
12075
- else deps.track(deps.settle(row.reviewId)).catch(() => {}).finally(release);
12076
- return {
12077
- status: 200,
12078
- body: {
12079
- outcome: prepared.kind,
12080
- reviewId: row.reviewId,
12081
- ...row.kept ? { kept: row.kept } : {}
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.76.9",
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-C0lM8IBt.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};
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};