prreviewbuddy 0.64.2 → 0.70.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +128 -0
- package/dist/{delete_review-l4p_p6oh.js → delete_review-CRPRIRKg.js} +7013 -6596
- package/dist/main.js +2 -328
- package/dist/server.js +697 -7
- package/package.json +1 -1
- package/static/chunk-BsU84lX4.js +1 -0
- package/static/chunk-C8kkSv38.js +1 -0
- package/static/chunk-DqEA-C0S.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 +84 -0
- package/static/sidebar.css +2 -0
- package/static/workspace.js +63 -63
- package/static/chunk-BadSI8AL.js +0 -1
- package/static/chunk-CXf6hwXL.js +0 -1
- package/static/chunk-CndkgJPh.js +0 -1
package/dist/server.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as MAX_WORKSPACE_PORT, $n as isClaimed, $r as resolveAgent, $t as markReadForReview, A as relativeTime$1, An as isWorktreeHeld, Ar as parseLifecycle, At as partFiles, B as readIndexToken, Bn as reviewedShaOf, Br as issuesOutstanding, Bt as isArchitectureId, C as track, Cn as originRemote, Cr as activityAttributes, Ct as mapRuntimes, D as endRun, Dn as MANAGED_ROOT, Dr as lifecycleItemLabel, Dt as ARCHITECTURE_DIR, E as completeRun, En as ReviewBeingDeletedError, Er as lifecycleChip, Et as describeRuntimes, F as runBatch, Fr as queueStamp, Ft as trackedFiles, Gn as loadJob, Gr as describeAuthorship, Gt as createArchitectureNotification, H as clearServerRecordOwnedBy, Hn as endedAt, Hr as reviewerDispositions, Ht as announceJob, I as activateQueued, In as findExistingReview, Ir as readQueue, It as ownersOf, J as stopChild, Jn as isLiveJob, Jr as resolveTarget, Jt as eventName, K as serverEntryExists, Kn as saveJob, Kr as WITHHELD_REASON, Kt as createPrState, L as compactQueue, Ln as checkCodeFreshness, Lr as doneVerb, Lt as QUOTE_LIMITS, M as extractPullRequestLinks, Mn as removeWorktree, Mr as queueAdd, Mt as repositoryKey, N as parsePullRequestLink, Nn as requestJobStop, Nr as queueAddMany, Nt as storeArchitecture, O as reportRun, On as createWorktree, Or as lifecyclePill, Ot as checkArchitecture, P as unsupportedReason, Pr as queueRemove, Pt as storedScopes, Q as DEFAULT_WORKSPACE_PORT, Qr as git, Qt as markRead, Rn as checkReview, Rr as isIssueOutstanding, Rt as REPOSITORY_SCOPE, S as isDriving, Sn as remoteHost, Sr as LIFECYCLES, St as waitingHistory, T as beginRun, Tr as isLifecycle, Tt as RUNTIME_WORDS, Un as fail, Ur as processDiscussion, Ut as branchOf, Vn as allJobs, Vr as questionsOutstanding, Vt as writeJsonAtomically, W as readServerRecord, Wn as isTerminal, Wr as recommendationsOf, Wt as subjectOf, Xn as progressSteps, Xr as isCommitReview, Xt as listNotifications, Yn as phasesFor, Yr as DetachedHeadError, Yt as headline, Z as writeServerRecord, Zn as purposeOf, Zr as resolveBase, Zt as markAllRead, _ as liveUpdateFor, _n as extractJsonText, _t as telemetryUploadConsent, a as wasBlocked, ai as STORE_ROOT, an as withUsageRecorded, ar as kissDecisionRefusal, at as failureOf$1, bn as forgeResolver, br as workspaceRevision, c as RefreshUnavailableError, ci as agentFor$1, cn as authorPreview, cr as lineagePosition, ct as reanalyseReview, d as mergeUpdateInto, di as AgentUnavailableError, dn as renderMarkdown, dr as mutateWorkspace, ei as resolveModel, en as pruneNotifications, er as decideKissRecommendation, et as MIN_WORKSPACE_PORT, f as updateReview, fn as CODE_BLOCK_CLASS, ft as readEvents, g as UpdateAlreadyRunningError, gn as asString, gr as reviewedCommit, gt as recordTelemetryUploadConsent, h as askCheckout, hn as renderInlineText, hr as recentWorkspaces, i as liveJobsFor, in as unreadCount$1, ir as indexReviewGroups, it as writePortPreference, j as batchIdentity, jn as readMarker, jr as sectionOf, jt as readArchitecture, k as stopRun, kn as holdWorktree, kr as offersLifecycle, kt as loadArchitecture, l as cancelUpdate, li as detectAgents, ln as hasAuthorContent, lr as loadWorkspace, lt as runJob, m as isUnchanged, mn as escapeHtml$1, mr as previousKissResult, n as deleteReview, ni as readAgentPreference, nn as removeNotification, nr as groupByLineage, nt as resolveWorkspacePort, o as discardJob, oi as AGENT_IDS, on as EXPLAIN_SIMPLY_PROMPT, or as kissDecisionsOf, ot as prepareReview, p as refreshPrContext, pn as CODE_COPY_CLASS, pr as presentedKissRunOf, pt as record, q as spawnServer, qr as kissOutcome, qt as createPrUpdate, r as lineageIds, ri as writeAgentPreference, rn as toneOf, rr as indexKeptIds, rt as statedWorkspacePort, s as FETCH_TIMEOUT_MS, si as agentById, sn as UNESTABLISHED_MOTIVATION, sr as latestKissRun, st as settleReview, t as deleteLineage, ti as clearAgentPreference, tn as readNotification, tr as followedRefName, tt as parseWorkspacePort, u as cancellableHere, ui as AgentCancelledError, un as GITHUB_MARKDOWN, vn as objectWithAnyOf, w as RUN_KINDS, wn as fillFileUrlTemplate, wr as activityLabel, wt as recordRuntimeBestEffort, x as startKissJob, xn as forgeFor, xr as workspaceStamps, xt as feedbackUrl, y as agentEnvOf, yn as claimRequestUrl, yr as touchWorkspace, yt as BUILD_VERSION, z as settleQueue, zn as followedBranch, zr as isQuestionOutstanding, zt as ancestorsOf } from "./delete_review-CRPRIRKg.js";
|
|
2
2
|
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
5
|
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
6
|
+
import { homedir } from "node:os";
|
|
5
7
|
import { fileURLToPath } from "node:url";
|
|
6
8
|
import { createServer } from "node:http";
|
|
7
9
|
//#region ../../packages/review-harness/src/domain/ai_review/review_composition.ts
|
|
@@ -7241,7 +7243,7 @@ function startArchitectureRun(workspace, scope, deps = {}) {
|
|
|
7241
7243
|
const now = deps.now ?? Date.now;
|
|
7242
7244
|
const startedAt = now();
|
|
7243
7245
|
const env = agentEnvOf(workspace);
|
|
7244
|
-
const agent = agentFor(env);
|
|
7246
|
+
const agent = agentFor$1(env);
|
|
7245
7247
|
const requestedModel = resolveModel({ review: { session: workspace.session } }) ?? void 0;
|
|
7246
7248
|
const record = {
|
|
7247
7249
|
version: 1,
|
|
@@ -10372,6 +10374,656 @@ function reviewsRevision(live = analysisStates()) {
|
|
|
10372
10374
|
return hash.digest("base64url");
|
|
10373
10375
|
}
|
|
10374
10376
|
//#endregion
|
|
10377
|
+
//#region ../../packages/review-harness/src/workspace/working_folders.ts
|
|
10378
|
+
/**
|
|
10379
|
+
* The local Git working folders PR Review Buddy can find right now.
|
|
10380
|
+
*
|
|
10381
|
+
* One question and nothing more: which folders on this machine could a review run in. The answer is
|
|
10382
|
+
* derived each time from what is already known, the folders past reviews ran in and the other
|
|
10383
|
+
* worktrees of those repositories, and nothing is stored. A worktree an agent made yesterday shows
|
|
10384
|
+
* up because `git worktree list` names it, not because anything remembered it.
|
|
10385
|
+
*
|
|
10386
|
+
* A folder is a folder. A linked worktree is its own entry with its own path and is never folded
|
|
10387
|
+
* into the clone it belongs to: a review is filed under the folder it ran in, and the worktree is
|
|
10388
|
+
* where the branch is checked out.
|
|
10389
|
+
*
|
|
10390
|
+
* Not a registry. If one is wanted later it replaces `discoverWorkingFolders` and keeps the shape.
|
|
10391
|
+
*/
|
|
10392
|
+
function canonical(path) {
|
|
10393
|
+
try {
|
|
10394
|
+
return realpathSync(path);
|
|
10395
|
+
} catch {
|
|
10396
|
+
return path;
|
|
10397
|
+
}
|
|
10398
|
+
}
|
|
10399
|
+
/** `git worktree list --porcelain`, main worktree first, bare and prunable entries left out. */
|
|
10400
|
+
async function worktreesOf(repoPath) {
|
|
10401
|
+
let output;
|
|
10402
|
+
try {
|
|
10403
|
+
output = await git(repoPath, [
|
|
10404
|
+
"worktree",
|
|
10405
|
+
"list",
|
|
10406
|
+
"--porcelain"
|
|
10407
|
+
]);
|
|
10408
|
+
} catch {
|
|
10409
|
+
return [];
|
|
10410
|
+
}
|
|
10411
|
+
const entries = [];
|
|
10412
|
+
for (const block of output.split(/\n\s*\n/)) {
|
|
10413
|
+
const lines = block.split("\n");
|
|
10414
|
+
const path = lines.find((line) => line.startsWith("worktree "))?.slice(9);
|
|
10415
|
+
if (!path || lines.includes("bare") || lines.some((line) => line.startsWith("prunable"))) continue;
|
|
10416
|
+
const ref = lines.find((line) => line.startsWith("branch "))?.slice(7) ?? null;
|
|
10417
|
+
entries.push({
|
|
10418
|
+
path,
|
|
10419
|
+
branch: ref ? ref.replace(/^refs\/heads\//, "") : null
|
|
10420
|
+
});
|
|
10421
|
+
}
|
|
10422
|
+
return entries;
|
|
10423
|
+
}
|
|
10424
|
+
async function discoverWorkingFolders(summaries = recentWorkspaces(Number.MAX_SAFE_INTEGER)) {
|
|
10425
|
+
const lastReviewed = /* @__PURE__ */ new Map();
|
|
10426
|
+
for (const summary of summaries) {
|
|
10427
|
+
if (!summary.repoPath) continue;
|
|
10428
|
+
const key = canonical(summary.repoPath);
|
|
10429
|
+
lastReviewed.set(key, Math.max(lastReviewed.get(key) ?? 0, summary.createdAt));
|
|
10430
|
+
}
|
|
10431
|
+
const found = /* @__PURE__ */ new Map();
|
|
10432
|
+
for (const seed of lastReviewed.keys()) {
|
|
10433
|
+
if (found.has(seed) || !existsSync(seed)) continue;
|
|
10434
|
+
(await worktreesOf(seed)).forEach((entry, index) => {
|
|
10435
|
+
const key = canonical(entry.path);
|
|
10436
|
+
if (!found.has(key) && existsSync(key)) found.set(key, {
|
|
10437
|
+
entry: {
|
|
10438
|
+
...entry,
|
|
10439
|
+
path: key
|
|
10440
|
+
},
|
|
10441
|
+
main: index === 0
|
|
10442
|
+
});
|
|
10443
|
+
});
|
|
10444
|
+
}
|
|
10445
|
+
return (await Promise.all([...found.values()].map(async ({ entry, main }) => ({
|
|
10446
|
+
path: entry.path,
|
|
10447
|
+
remote: await originRemote(entry.path),
|
|
10448
|
+
branch: entry.branch,
|
|
10449
|
+
main,
|
|
10450
|
+
lastReviewedAt: lastReviewed.get(entry.path) ?? null
|
|
10451
|
+
})))).sort((a, b) => (b.lastReviewedAt ?? 0) - (a.lastReviewedAt ?? 0) || Number(b.main) - Number(a.main) || a.path.localeCompare(b.path));
|
|
10452
|
+
}
|
|
10453
|
+
/** Whether a remote is this github.com repository. GitHub names are case-insensitive. */
|
|
10454
|
+
function isGitHubRepository(remote, owner, repo) {
|
|
10455
|
+
return Boolean(remote && remote.host === "github.com" && remote.owner.toLowerCase() === owner.toLowerCase() && remote.repo.toLowerCase() === repo.toLowerCase());
|
|
10456
|
+
}
|
|
10457
|
+
/**
|
|
10458
|
+
* The folders a pull request could be reviewed in, the suggested one first.
|
|
10459
|
+
*
|
|
10460
|
+
* Every match is returned, because which one is right is the reviewer's call: an agent's worktree
|
|
10461
|
+
* and the main clone hold the same objects and differ in what is checked out. The order only
|
|
10462
|
+
* decides the default. A folder that already has a review of this pull request comes first, so
|
|
10463
|
+
* reviewing it again finds that review rather than starting a second one filed under another
|
|
10464
|
+
* folder; then the main worktree; then whichever was reviewed in most recently.
|
|
10465
|
+
*/
|
|
10466
|
+
function candidatesFor(folders, link, summaries) {
|
|
10467
|
+
const reviewedHere = new Set(summaries.filter((summary) => summary.prNumber === link.number).map((summary) => canonical(summary.repoPath)));
|
|
10468
|
+
const matching = folders.filter((folder) => isGitHubRepository(folder.remote, link.owner, link.repo));
|
|
10469
|
+
const rank = (folder) => reviewedHere.has(folder.path) ? 0 : folder.main ? 1 : 2;
|
|
10470
|
+
return [...matching].sort((a, b) => rank(a) - rank(b) || (b.lastReviewedAt ?? 0) - (a.lastReviewedAt ?? 0));
|
|
10471
|
+
}
|
|
10472
|
+
/** The review of this pull request already filed under one of these folders, if there is one. */
|
|
10473
|
+
function reviewedIn(folders, link, summaries) {
|
|
10474
|
+
const paths = new Set(folders.map((folder) => folder.path));
|
|
10475
|
+
return summaries.find((summary) => summary.prNumber === link.number && paths.has(canonical(summary.repoPath))) ?? null;
|
|
10476
|
+
}
|
|
10477
|
+
//#endregion
|
|
10478
|
+
//#region ../../packages/review-harness/src/workspace/choose_folder.ts
|
|
10479
|
+
/**
|
|
10480
|
+
* Ask the person at this machine to pick a folder, with the operating system's own picker.
|
|
10481
|
+
*
|
|
10482
|
+
* The workspace is a page served by a local daemon, and a page cannot learn a folder's path from
|
|
10483
|
+
* the browser's directory picker, so the daemon asks instead. The operation is "choose a working
|
|
10484
|
+
* folder"; how a platform does it stays in this file, so neither the routes nor the page know.
|
|
10485
|
+
*
|
|
10486
|
+
* Only macOS has an implementation, through `osascript`. Elsewhere there is no chooser, and the
|
|
10487
|
+
* Add review panel falls back to the path field and the Recent list, which is all v1 needs.
|
|
10488
|
+
*/
|
|
10489
|
+
/** Long enough for somebody to browse; short enough that a forgotten dialog does not hold a request for ever. */
|
|
10490
|
+
var CHOOSER_TIMEOUT_MS = 3e5;
|
|
10491
|
+
function folderChooser(platform = process.platform) {
|
|
10492
|
+
if (platform !== "darwin") return null;
|
|
10493
|
+
return () => new Promise((resolve, reject) => {
|
|
10494
|
+
execFile("osascript", ["-e", "POSIX path of (choose folder with prompt \"Choose the working folder to review\")"], { timeout: CHOOSER_TIMEOUT_MS }, (error, stdout, stderr) => {
|
|
10495
|
+
if (error) return /-128/.test(String(stderr)) ? resolve(null) : reject(/* @__PURE__ */ new Error("The folder picker could not be opened."));
|
|
10496
|
+
resolve(stdout.trim().replace(/\/$/, "") || null);
|
|
10497
|
+
});
|
|
10498
|
+
});
|
|
10499
|
+
}
|
|
10500
|
+
function createSlots(capacity) {
|
|
10501
|
+
let free = capacity;
|
|
10502
|
+
const waiting = [];
|
|
10503
|
+
const releaser = () => {
|
|
10504
|
+
let given = false;
|
|
10505
|
+
return () => {
|
|
10506
|
+
if (given) return;
|
|
10507
|
+
given = true;
|
|
10508
|
+
const next = waiting.shift();
|
|
10509
|
+
if (next) next(releaser());
|
|
10510
|
+
else free += 1;
|
|
10511
|
+
};
|
|
10512
|
+
};
|
|
10513
|
+
return {
|
|
10514
|
+
acquire: () => {
|
|
10515
|
+
if (free > 0) {
|
|
10516
|
+
free -= 1;
|
|
10517
|
+
return Promise.resolve(releaser());
|
|
10518
|
+
}
|
|
10519
|
+
return new Promise((resolve) => waiting.push(resolve));
|
|
10520
|
+
},
|
|
10521
|
+
tryAcquire: () => {
|
|
10522
|
+
if (free === 0 || waiting.length > 0) return null;
|
|
10523
|
+
free -= 1;
|
|
10524
|
+
return releaser();
|
|
10525
|
+
}
|
|
10526
|
+
};
|
|
10527
|
+
}
|
|
10528
|
+
/** How many pull requests one submission may name. See `MAX_PULL_REQUESTS_PER_BATCH`. */
|
|
10529
|
+
var MAX_BATCH = 20;
|
|
10530
|
+
/** How long a finished batch stays answerable, for a panel that is still open on it. */
|
|
10531
|
+
var FINISHED_BATCH_TTL_MS = 6e5;
|
|
10532
|
+
function realAddReviewDeps() {
|
|
10533
|
+
return {
|
|
10534
|
+
resolveAgent: (flag) => resolveAgent(flag ? { flag } : {}),
|
|
10535
|
+
agents: async () => (await detectAgents()).map((agent) => ({
|
|
10536
|
+
id: agent.id,
|
|
10537
|
+
displayName: agent.displayName,
|
|
10538
|
+
installed: agent.state === "installed"
|
|
10539
|
+
})),
|
|
10540
|
+
folders: () => discoverWorkingFolders(),
|
|
10541
|
+
summaries: () => recentWorkspaces(Number.MAX_SAFE_INTEGER),
|
|
10542
|
+
remoteOf: originRemote,
|
|
10543
|
+
resolvePullRequest: (folder, url) => resolveTarget({
|
|
10544
|
+
repoPath: folder,
|
|
10545
|
+
prUrl: url,
|
|
10546
|
+
resolveRequest: forgeResolver
|
|
10547
|
+
}),
|
|
10548
|
+
resolveBranch: (folder, base) => resolveTarget({
|
|
10549
|
+
repoPath: folder,
|
|
10550
|
+
...base ? { base } : {}
|
|
10551
|
+
}),
|
|
10552
|
+
defaultBase: async (root) => {
|
|
10553
|
+
try {
|
|
10554
|
+
const base = await resolveBase(root, void 0);
|
|
10555
|
+
return (base.followRef ?? base.display).replace(/^refs\/(?:heads|remotes)\//, "");
|
|
10556
|
+
} catch {
|
|
10557
|
+
return null;
|
|
10558
|
+
}
|
|
10559
|
+
},
|
|
10560
|
+
findExisting: findExistingReview,
|
|
10561
|
+
prepare: prepareReview,
|
|
10562
|
+
settle: (workspaceId) => settleReview(workspaceId, "standard"),
|
|
10563
|
+
track,
|
|
10564
|
+
chooseFolder: folderChooser(),
|
|
10565
|
+
concurrency: 2,
|
|
10566
|
+
slots: createSlots(2),
|
|
10567
|
+
resumeOf: (reviewId) => {
|
|
10568
|
+
const failure = failureOf$1(reviewId);
|
|
10569
|
+
return failure ? {
|
|
10570
|
+
agent: agentNameOf(failure.agentId),
|
|
10571
|
+
...failure.requestedModel ? { model: failure.requestedModel } : {}
|
|
10572
|
+
} : null;
|
|
10573
|
+
},
|
|
10574
|
+
agentName: agentNameOf,
|
|
10575
|
+
home: homedir()
|
|
10576
|
+
};
|
|
10577
|
+
}
|
|
10578
|
+
function agentNameOf(agentId) {
|
|
10579
|
+
try {
|
|
10580
|
+
return agentById(agentId).displayName;
|
|
10581
|
+
} catch {
|
|
10582
|
+
return agentId;
|
|
10583
|
+
}
|
|
10584
|
+
}
|
|
10585
|
+
var refuse = (status, error) => ({
|
|
10586
|
+
status,
|
|
10587
|
+
body: { error }
|
|
10588
|
+
});
|
|
10589
|
+
/** A path as a person reads it, with their home folder as `~`. */
|
|
10590
|
+
function displayPath(path, home) {
|
|
10591
|
+
return home && (path === home || path.startsWith(`${home}/`)) ? `~${path.slice(home.length)}` : path;
|
|
10592
|
+
}
|
|
10593
|
+
/** A path a person typed, made absolute: `~` is their home folder, and anything relative is refused. */
|
|
10594
|
+
function expandPath(raw, home) {
|
|
10595
|
+
const trimmed = raw.trim().replace(/(.)\/+$/, "$1");
|
|
10596
|
+
if (!trimmed) return null;
|
|
10597
|
+
const expanded = trimmed === "~" ? home : trimmed.startsWith("~/") ? `${home}${trimmed.slice(1)}` : trimmed;
|
|
10598
|
+
return isAbsolute(expanded) ? expanded : null;
|
|
10599
|
+
}
|
|
10600
|
+
function message(error) {
|
|
10601
|
+
return error instanceof Error ? error.message : String(error);
|
|
10602
|
+
}
|
|
10603
|
+
/** Why no agent could be used, in a sentence for the panel. The CLI's own wording names flags. */
|
|
10604
|
+
function unresolvedAgent(resolution) {
|
|
10605
|
+
switch (resolution.kind) {
|
|
10606
|
+
case "none": return "No review agent is installed on this machine. Install one of the agents PR Review Buddy supports, then try again.";
|
|
10607
|
+
case "unknown-id": return `"${resolution.id}" is not an agent PR Review Buddy knows. Choose another under Review options, or change the default in Settings.`;
|
|
10608
|
+
case "unavailable": return `${resolution.agent.displayName} can't be used from the workspace: ${resolution.reason}`;
|
|
10609
|
+
}
|
|
10610
|
+
}
|
|
10611
|
+
/**
|
|
10612
|
+
* The agent a submission runs under: the one named under Review options, or else the same answer
|
|
10613
|
+
* the CLI reaches with no flag (the configured default, or the only one installed). Never the
|
|
10614
|
+
* harness's own fallback: an id is always passed down, so `startJob` never has to guess.
|
|
10615
|
+
*/
|
|
10616
|
+
async function agentFor(flag, deps) {
|
|
10617
|
+
if (flag !== void 0 && typeof flag !== "string") return refuse(400, "Say which agent by its id.");
|
|
10618
|
+
const resolution = await deps.resolveAgent(flag || void 0);
|
|
10619
|
+
if (resolution.kind === "resolved") return { agentId: resolution.agent.id };
|
|
10620
|
+
if (resolution.kind === "ambiguous") return refuse(400, "More than one review agent is installed. Choose one under Review options.");
|
|
10621
|
+
return refuse(resolution.kind === "unknown-id" ? 400 : 409, unresolvedAgent(resolution));
|
|
10622
|
+
}
|
|
10623
|
+
function modelOf(value) {
|
|
10624
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
10625
|
+
if (typeof value !== "string" || value.length > 200) return refuse(400, "The model must be a short name.");
|
|
10626
|
+
return value.trim() || void 0;
|
|
10627
|
+
}
|
|
10628
|
+
/**
|
|
10629
|
+
* The tail of each target's queue of creations, by batch identity. In-process, like the fetch
|
|
10630
|
+
* queue in `target.ts`.
|
|
10631
|
+
*
|
|
10632
|
+
* `prepareReview` looks for an existing review and then creates one, and nothing between the two
|
|
10633
|
+
* stops a second start of the same target, from another tab or another paste, from looking in the
|
|
10634
|
+
* same gap and creating a second review of it. One at a time per target closes that gap for every
|
|
10635
|
+
* start this server makes; the second one finds the first one's review.
|
|
10636
|
+
*/
|
|
10637
|
+
var creations = /* @__PURE__ */ new Map();
|
|
10638
|
+
function oneCreationAtATime(target, work) {
|
|
10639
|
+
const key = batchIdentity(target);
|
|
10640
|
+
const mine = (creations.get(key) ?? Promise.resolve()).then(work, work);
|
|
10641
|
+
const tail = mine.catch(() => {});
|
|
10642
|
+
creations.set(key, tail);
|
|
10643
|
+
tail.then(() => {
|
|
10644
|
+
if (creations.get(key) === tail) creations.delete(key);
|
|
10645
|
+
});
|
|
10646
|
+
return mine;
|
|
10647
|
+
}
|
|
10648
|
+
var isAnswer = (value) => typeof value === "object" && value !== null && "status" in value && "body" in value;
|
|
10649
|
+
async function addReviewOptions(deps) {
|
|
10650
|
+
const [agents, resolution] = await Promise.all([deps.agents(), deps.resolveAgent()]);
|
|
10651
|
+
return {
|
|
10652
|
+
status: 200,
|
|
10653
|
+
body: {
|
|
10654
|
+
agents,
|
|
10655
|
+
agent: resolution.kind === "resolved" ? {
|
|
10656
|
+
kind: "resolved",
|
|
10657
|
+
id: resolution.agent.id,
|
|
10658
|
+
displayName: resolution.agent.displayName
|
|
10659
|
+
} : resolution.kind === "ambiguous" ? { kind: "ambiguous" } : {
|
|
10660
|
+
kind: "refused",
|
|
10661
|
+
error: unresolvedAgent(resolution)
|
|
10662
|
+
},
|
|
10663
|
+
canChooseFolder: deps.chooseFolder !== null
|
|
10664
|
+
}
|
|
10665
|
+
};
|
|
10666
|
+
}
|
|
10667
|
+
function reviewedRow(reviewId, deps) {
|
|
10668
|
+
const kept = deps.resumeOf(reviewId);
|
|
10669
|
+
return {
|
|
10670
|
+
reviewId,
|
|
10671
|
+
...kept ? { kept } : {}
|
|
10672
|
+
};
|
|
10673
|
+
}
|
|
10674
|
+
function folderOption(folder, home) {
|
|
10675
|
+
return {
|
|
10676
|
+
path: folder.path,
|
|
10677
|
+
display: displayPath(folder.path, home),
|
|
10678
|
+
branch: folder.branch
|
|
10679
|
+
};
|
|
10680
|
+
}
|
|
10681
|
+
/**
|
|
10682
|
+
* What a paste would start, without starting anything: rows, the folders each could run in, and
|
|
10683
|
+
* which already have a review. No forge is asked and nothing is fetched, so it is cheap enough to
|
|
10684
|
+
* run on every pause in typing.
|
|
10685
|
+
*
|
|
10686
|
+
* Every link gets a row, however many there are. Past the batch limit the panel refuses to start
|
|
10687
|
+
* rather than this quietly dropping the tail: a reviewer who pasted twenty-one pull requests and
|
|
10688
|
+
* got twenty reviews would not find out which one was missing.
|
|
10689
|
+
*/
|
|
10690
|
+
async function previewPullRequests(text, deps) {
|
|
10691
|
+
if (typeof text !== "string") return refuse(400, "Paste the pull request links to review.");
|
|
10692
|
+
const links = extractPullRequestLinks(text);
|
|
10693
|
+
const [folders, summaries] = links.some((link) => link.kind === "github") ? [await deps.folders(), deps.summaries()] : [[], []];
|
|
10694
|
+
return {
|
|
10695
|
+
status: 200,
|
|
10696
|
+
body: {
|
|
10697
|
+
rows: links.map((link) => {
|
|
10698
|
+
if (link.kind === "unsupported") return {
|
|
10699
|
+
key: link.key,
|
|
10700
|
+
label: `${link.provider} ${link.provider === "GitLab" ? "merge request" : "pull request"}`,
|
|
10701
|
+
url: link.url,
|
|
10702
|
+
ok: false,
|
|
10703
|
+
reason: unsupportedReason(link.provider),
|
|
10704
|
+
folders: []
|
|
10705
|
+
};
|
|
10706
|
+
const label = `${link.owner}/${link.repo} #${link.number}`;
|
|
10707
|
+
const candidates = candidatesFor(folders, link, summaries);
|
|
10708
|
+
if (candidates.length === 0) return {
|
|
10709
|
+
key: link.key,
|
|
10710
|
+
label,
|
|
10711
|
+
url: link.url,
|
|
10712
|
+
ok: false,
|
|
10713
|
+
reason: "Local repository not found",
|
|
10714
|
+
needsFolder: true,
|
|
10715
|
+
folders: []
|
|
10716
|
+
};
|
|
10717
|
+
const reviewed = reviewedIn(candidates, link, summaries);
|
|
10718
|
+
return {
|
|
10719
|
+
key: link.key,
|
|
10720
|
+
label,
|
|
10721
|
+
url: link.url,
|
|
10722
|
+
ok: true,
|
|
10723
|
+
folders: candidates.map((folder) => folderOption(folder, deps.home)),
|
|
10724
|
+
folder: candidates[0].path,
|
|
10725
|
+
...reviewed ? { reviewed: reviewedRow(reviewed.id, deps) } : {}
|
|
10726
|
+
};
|
|
10727
|
+
}),
|
|
10728
|
+
limit: MAX_BATCH
|
|
10729
|
+
}
|
|
10730
|
+
};
|
|
10731
|
+
}
|
|
10732
|
+
var batches = /* @__PURE__ */ new Map();
|
|
10733
|
+
function forgetFinished(now) {
|
|
10734
|
+
for (const [id, batch] of batches) if (batch.finishedAt !== null && now - batch.finishedAt > FINISHED_BATCH_TTL_MS) batches.delete(id);
|
|
10735
|
+
}
|
|
10736
|
+
function newBatch(rows) {
|
|
10737
|
+
forgetFinished(Date.now());
|
|
10738
|
+
const batch = {
|
|
10739
|
+
id: randomUUID(),
|
|
10740
|
+
rows,
|
|
10741
|
+
finishedAt: null
|
|
10742
|
+
};
|
|
10743
|
+
batches.set(batch.id, batch);
|
|
10744
|
+
return batch;
|
|
10745
|
+
}
|
|
10746
|
+
var rowsOf = (batch) => batch.rows.map((row) => ({ ...row }));
|
|
10747
|
+
/**
|
|
10748
|
+
* Create the review for a row's target, or find the one it has, under the per-target lock. The
|
|
10749
|
+
* caller holds an analysis slot: nothing is created without one, so nothing created waits.
|
|
10750
|
+
*/
|
|
10751
|
+
async function prepareRow(row, target, agentId, model, deps) {
|
|
10752
|
+
const prepared = await oneCreationAtATime(target, () => deps.prepare(target, agentId, false, model));
|
|
10753
|
+
row.reviewId = prepared.kind === "new" ? prepared.workspaceId : prepared.existing.id;
|
|
10754
|
+
if (prepared.kind === "existing") row.status = "already-reviewed";
|
|
10755
|
+
else row.status = "reviewing";
|
|
10756
|
+
if (prepared.kind === "resume") row.kept = keptOf(prepared.failure.agentId, prepared.failure.requestedModel, deps);
|
|
10757
|
+
return prepared;
|
|
10758
|
+
}
|
|
10759
|
+
async function settleRow(row, deps) {
|
|
10760
|
+
const settled = await deps.settle(row.reviewId);
|
|
10761
|
+
if (settled.ok) row.status = "ready";
|
|
10762
|
+
else {
|
|
10763
|
+
row.status = "failed";
|
|
10764
|
+
row.reason = settled.stopped ? "Stopped" : settled.reason;
|
|
10765
|
+
}
|
|
10766
|
+
}
|
|
10767
|
+
function keptOf(agentId, model, deps) {
|
|
10768
|
+
return {
|
|
10769
|
+
agent: deps.agentName(agentId),
|
|
10770
|
+
...model ? { model } : {}
|
|
10771
|
+
};
|
|
10772
|
+
}
|
|
10773
|
+
/**
|
|
10774
|
+
* Start a batch and answer at once. The analyses take minutes, and the panel is not a progress
|
|
10775
|
+
* dialog: it may watch the batch while it stays open, and the reviews appear in the list either way.
|
|
10776
|
+
*
|
|
10777
|
+
* Each pull request waits for one of the server's analysis slots (`deps.slots`), shared with every
|
|
10778
|
+
* other batch and every branch review, and is resolved and created only once it has one.
|
|
10779
|
+
*/
|
|
10780
|
+
async function startPullRequestBatch(body, deps) {
|
|
10781
|
+
const raw = Array.isArray(body?.items) ? body.items : null;
|
|
10782
|
+
if (!raw || raw.length === 0) return refuse(400, "Say which pull requests to review.");
|
|
10783
|
+
if (raw.length > MAX_BATCH) return refuse(400, `At most ${MAX_BATCH} pull requests can be started at once.`);
|
|
10784
|
+
const items = [];
|
|
10785
|
+
for (const entry of raw) {
|
|
10786
|
+
const url = typeof entry?.url === "string" ? entry.url : "";
|
|
10787
|
+
const link = parsePullRequestLink(url);
|
|
10788
|
+
if (!link || link.kind !== "github") return refuse(400, `${url || "That"} is not a github.com pull request link.`);
|
|
10789
|
+
const folder = typeof entry?.folder === "string" ? expandPath(entry.folder, deps.home) : null;
|
|
10790
|
+
if (!folder) return refuse(400, `Choose a local folder for ${link.owner}/${link.repo} #${link.number}.`);
|
|
10791
|
+
items.push({
|
|
10792
|
+
url: link.url,
|
|
10793
|
+
owner: link.owner,
|
|
10794
|
+
repo: link.repo,
|
|
10795
|
+
number: link.number,
|
|
10796
|
+
key: link.key,
|
|
10797
|
+
folder
|
|
10798
|
+
});
|
|
10799
|
+
}
|
|
10800
|
+
const model = modelOf(body?.model);
|
|
10801
|
+
if (isAnswer(model)) return model;
|
|
10802
|
+
const agent = await agentFor(body?.agent, deps);
|
|
10803
|
+
if (isAnswer(agent)) return agent;
|
|
10804
|
+
const batch = newBatch(items.map((item) => ({
|
|
10805
|
+
key: item.key,
|
|
10806
|
+
label: `${item.owner}/${item.repo} #${item.number}`,
|
|
10807
|
+
status: "waiting"
|
|
10808
|
+
})));
|
|
10809
|
+
/** Which row claimed each pull request, by what it resolved to rather than how it was spelled. */
|
|
10810
|
+
const claimed = /* @__PURE__ */ new Map();
|
|
10811
|
+
const lane = async (item, index) => {
|
|
10812
|
+
const row = batch.rows[index];
|
|
10813
|
+
const release = await deps.slots.acquire();
|
|
10814
|
+
row.status = "reviewing";
|
|
10815
|
+
try {
|
|
10816
|
+
const remote = await deps.remoteOf(item.folder);
|
|
10817
|
+
if (!isGitHubRepository(remote, item.owner, item.repo)) {
|
|
10818
|
+
row.status = "failed";
|
|
10819
|
+
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}`;
|
|
10820
|
+
return;
|
|
10821
|
+
}
|
|
10822
|
+
const target = await deps.resolvePullRequest(item.folder, item.url);
|
|
10823
|
+
const identity = batchIdentity(target);
|
|
10824
|
+
const first = claimed.get(identity);
|
|
10825
|
+
if (first !== void 0) {
|
|
10826
|
+
row.status = "already-reviewed";
|
|
10827
|
+
row.reason = `Same pull request as ${batch.rows[first].label}`;
|
|
10828
|
+
return;
|
|
10829
|
+
}
|
|
10830
|
+
claimed.set(identity, index);
|
|
10831
|
+
if ((await prepareRow(row, target, agent.agentId, model, deps)).kind !== "existing") await settleRow(row, deps);
|
|
10832
|
+
} catch (error) {
|
|
10833
|
+
row.status = "failed";
|
|
10834
|
+
row.reason = message(error);
|
|
10835
|
+
} finally {
|
|
10836
|
+
release();
|
|
10837
|
+
}
|
|
10838
|
+
};
|
|
10839
|
+
deps.track(runBatch(items, {
|
|
10840
|
+
concurrency: deps.concurrency,
|
|
10841
|
+
run: lane
|
|
10842
|
+
})).catch(() => {}).finally(() => {
|
|
10843
|
+
batch.finishedAt = Date.now();
|
|
10844
|
+
});
|
|
10845
|
+
return {
|
|
10846
|
+
status: 200,
|
|
10847
|
+
body: {
|
|
10848
|
+
batchId: batch.id,
|
|
10849
|
+
rows: rowsOf(batch)
|
|
10850
|
+
}
|
|
10851
|
+
};
|
|
10852
|
+
}
|
|
10853
|
+
/**
|
|
10854
|
+
* A batch's rows as they stand. A batch this server does not know was either finished long ago,
|
|
10855
|
+
* which an open panel never asks about because it stops asking once a batch finishes, or lost to a
|
|
10856
|
+
* restart, which is the case worth saying plainly: whatever had not started never will.
|
|
10857
|
+
*/
|
|
10858
|
+
function batchRows(id) {
|
|
10859
|
+
const batch = batches.get(id);
|
|
10860
|
+
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.");
|
|
10861
|
+
return {
|
|
10862
|
+
status: 200,
|
|
10863
|
+
body: {
|
|
10864
|
+
rows: rowsOf(batch),
|
|
10865
|
+
finished: batch.finishedAt !== null
|
|
10866
|
+
}
|
|
10867
|
+
};
|
|
10868
|
+
}
|
|
10869
|
+
/** A target refusal in the panel's words where the CLI's would name a command. */
|
|
10870
|
+
function targetRefusal(error) {
|
|
10871
|
+
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.";
|
|
10872
|
+
return message(error);
|
|
10873
|
+
}
|
|
10874
|
+
/** The recent and discoverable folders, for the Branch panel's list. */
|
|
10875
|
+
async function listFolders(deps) {
|
|
10876
|
+
return {
|
|
10877
|
+
status: 200,
|
|
10878
|
+
body: { folders: (await deps.folders()).map((folder) => ({
|
|
10879
|
+
...folderOption(folder, deps.home),
|
|
10880
|
+
repo: folder.remote ? folder.remote.repo : basename(folder.path)
|
|
10881
|
+
})) }
|
|
10882
|
+
};
|
|
10883
|
+
}
|
|
10884
|
+
/**
|
|
10885
|
+
* What reviewing this folder would review, read without changing anything: the same target a bare
|
|
10886
|
+
* `prreviewbuddy` here would resolve, and the review it would find.
|
|
10887
|
+
*
|
|
10888
|
+
* With the base the reviewer named, when they named one, so what this checks is the comparison
|
|
10889
|
+
* that would be submitted. Without it, a repository whose default base is ambiguous could never be
|
|
10890
|
+
* reviewed from here, though naming the base is exactly what settles it.
|
|
10891
|
+
*/
|
|
10892
|
+
async function inspectFolder(rawPath, deps, rawBase) {
|
|
10893
|
+
const path = typeof rawPath === "string" ? expandPath(rawPath, deps.home) : null;
|
|
10894
|
+
if (!path) return {
|
|
10895
|
+
status: 200,
|
|
10896
|
+
body: {
|
|
10897
|
+
ok: false,
|
|
10898
|
+
error: "Enter the full path of a folder, or choose one."
|
|
10899
|
+
}
|
|
10900
|
+
};
|
|
10901
|
+
const named = typeof rawBase === "string" && rawBase.trim() ? rawBase.trim() : void 0;
|
|
10902
|
+
let target;
|
|
10903
|
+
try {
|
|
10904
|
+
target = await deps.resolveBranch(path, named);
|
|
10905
|
+
} catch (error) {
|
|
10906
|
+
return {
|
|
10907
|
+
status: 200,
|
|
10908
|
+
body: {
|
|
10909
|
+
ok: false,
|
|
10910
|
+
error: targetRefusal(error)
|
|
10911
|
+
}
|
|
10912
|
+
};
|
|
10913
|
+
}
|
|
10914
|
+
const [remote, base, existing] = await Promise.all([
|
|
10915
|
+
deps.remoteOf(target.originRepoPath),
|
|
10916
|
+
named ? Promise.resolve(named) : deps.defaultBase(target.originRepoPath),
|
|
10917
|
+
deps.findExisting(target)
|
|
10918
|
+
]);
|
|
10919
|
+
const kept = existing ? deps.resumeOf(existing.id) : null;
|
|
10920
|
+
return {
|
|
10921
|
+
status: 200,
|
|
10922
|
+
body: {
|
|
10923
|
+
ok: true,
|
|
10924
|
+
path: target.originRepoPath,
|
|
10925
|
+
display: displayPath(target.originRepoPath, deps.home),
|
|
10926
|
+
repo: remote ? remote.repo : basename(target.originRepoPath),
|
|
10927
|
+
branch: target.branch,
|
|
10928
|
+
base,
|
|
10929
|
+
...existing ? { reviewed: {
|
|
10930
|
+
reviewId: existing.id,
|
|
10931
|
+
inProgress: existing.inProgress,
|
|
10932
|
+
...kept ? { kept } : {}
|
|
10933
|
+
} } : {}
|
|
10934
|
+
}
|
|
10935
|
+
};
|
|
10936
|
+
}
|
|
10937
|
+
/**
|
|
10938
|
+
* Review the branch checked out in a folder: `prreviewbuddy` run there.
|
|
10939
|
+
*
|
|
10940
|
+
* With a free analysis slot it answers once the review exists, and the analysis runs on in this
|
|
10941
|
+
* process, counted by `track` like a Retry. With none free it answers `waiting` and a one-row batch
|
|
10942
|
+
* to watch, and creates nothing until a slot frees: a review created to wait is a job nobody is
|
|
10943
|
+
* running, which the sweep would fail. A review the branch already has needs no slot to hand back.
|
|
10944
|
+
*/
|
|
10945
|
+
async function startBranchReview(body, deps) {
|
|
10946
|
+
const path = typeof body?.path === "string" ? expandPath(body.path, deps.home) : null;
|
|
10947
|
+
if (!path) return refuse(400, "Choose the working folder to review.");
|
|
10948
|
+
const base = typeof body?.base === "string" && body.base.trim() ? body.base.trim() : void 0;
|
|
10949
|
+
const model = modelOf(body?.model);
|
|
10950
|
+
if (isAnswer(model)) return model;
|
|
10951
|
+
let target;
|
|
10952
|
+
try {
|
|
10953
|
+
target = await deps.resolveBranch(path, base);
|
|
10954
|
+
} catch (error) {
|
|
10955
|
+
return refuse(400, targetRefusal(error));
|
|
10956
|
+
}
|
|
10957
|
+
const agent = await agentFor(body?.agent, deps);
|
|
10958
|
+
if (isAnswer(agent)) return agent;
|
|
10959
|
+
const row = {
|
|
10960
|
+
key: batchIdentity(target),
|
|
10961
|
+
label: target.branch,
|
|
10962
|
+
status: "waiting"
|
|
10963
|
+
};
|
|
10964
|
+
const release = deps.slots.tryAcquire();
|
|
10965
|
+
if (!release) {
|
|
10966
|
+
const existing = await deps.findExisting(target);
|
|
10967
|
+
if (existing && !deps.resumeOf(existing.id)) return {
|
|
10968
|
+
status: 200,
|
|
10969
|
+
body: {
|
|
10970
|
+
outcome: "existing",
|
|
10971
|
+
reviewId: existing.id
|
|
10972
|
+
}
|
|
10973
|
+
};
|
|
10974
|
+
const batch = newBatch([row]);
|
|
10975
|
+
deps.track((async () => {
|
|
10976
|
+
const later = await deps.slots.acquire();
|
|
10977
|
+
try {
|
|
10978
|
+
if ((await prepareRow(row, target, agent.agentId, model, deps)).kind !== "existing") await settleRow(row, deps);
|
|
10979
|
+
} catch (error) {
|
|
10980
|
+
row.status = "failed";
|
|
10981
|
+
row.reason = message(error);
|
|
10982
|
+
} finally {
|
|
10983
|
+
later();
|
|
10984
|
+
}
|
|
10985
|
+
})()).finally(() => {
|
|
10986
|
+
batch.finishedAt = Date.now();
|
|
10987
|
+
});
|
|
10988
|
+
return {
|
|
10989
|
+
status: 200,
|
|
10990
|
+
body: {
|
|
10991
|
+
outcome: "waiting",
|
|
10992
|
+
batchId: batch.id
|
|
10993
|
+
}
|
|
10994
|
+
};
|
|
10995
|
+
}
|
|
10996
|
+
let prepared;
|
|
10997
|
+
try {
|
|
10998
|
+
prepared = await prepareRow(row, target, agent.agentId, model, deps);
|
|
10999
|
+
} catch (error) {
|
|
11000
|
+
release();
|
|
11001
|
+
return refuse(409, message(error));
|
|
11002
|
+
}
|
|
11003
|
+
if (prepared.kind === "existing") release();
|
|
11004
|
+
else deps.track(deps.settle(row.reviewId)).catch(() => {}).finally(release);
|
|
11005
|
+
return {
|
|
11006
|
+
status: 200,
|
|
11007
|
+
body: {
|
|
11008
|
+
outcome: prepared.kind,
|
|
11009
|
+
reviewId: row.reviewId,
|
|
11010
|
+
...row.kept ? { kept: row.kept } : {}
|
|
11011
|
+
}
|
|
11012
|
+
};
|
|
11013
|
+
}
|
|
11014
|
+
async function chooseFolder(deps) {
|
|
11015
|
+
if (!deps.chooseFolder) return refuse(404, "Choosing a folder is not available on this system. Type its path instead.");
|
|
11016
|
+
try {
|
|
11017
|
+
const path = await deps.chooseFolder();
|
|
11018
|
+
return {
|
|
11019
|
+
status: 200,
|
|
11020
|
+
body: path ? { path } : { cancelled: true }
|
|
11021
|
+
};
|
|
11022
|
+
} catch (error) {
|
|
11023
|
+
return refuse(500, message(error));
|
|
11024
|
+
}
|
|
11025
|
+
}
|
|
11026
|
+
//#endregion
|
|
10375
11027
|
//#region ../../packages/review-harness/src/workspace/ask.ts
|
|
10376
11028
|
/** Long enough for a real look at the repository; short enough that a hang is not forever. */
|
|
10377
11029
|
var ASK_TIMEOUT_MS = 18e4;
|
|
@@ -10412,7 +11064,7 @@ async function askAboutChange(workspace, question, sectionId, signal) {
|
|
|
10412
11064
|
const env = agentEnvOf(workspace);
|
|
10413
11065
|
const requestedModel = resolveModel({ review: { session: workspace.session } }) ?? void 0;
|
|
10414
11066
|
try {
|
|
10415
|
-
return extractAnswer(await withUsageRecorded(workspace.id, "ask", env, (onUsage) => agentFor(env).run({
|
|
11067
|
+
return extractAnswer(await withUsageRecorded(workspace.id, "ask", env, (onUsage) => agentFor$1(env).run({
|
|
10416
11068
|
capability: "read-code",
|
|
10417
11069
|
prompt: buildPrompt$2(workspace, question, sectionId, cwd),
|
|
10418
11070
|
cwd,
|
|
@@ -10448,7 +11100,7 @@ async function askAboutChange(workspace, question, sectionId, signal) {
|
|
|
10448
11100
|
function explained(error, env) {
|
|
10449
11101
|
if (!(error instanceof Error) || error instanceof AgentCancelledError) return error;
|
|
10450
11102
|
try {
|
|
10451
|
-
const plain = agentFor(env).explainFailure?.(error.message, env);
|
|
11103
|
+
const plain = agentFor$1(env).explainFailure?.(error.message, env);
|
|
10452
11104
|
return plain ? new AskFailedError(plain) : error;
|
|
10453
11105
|
} catch {
|
|
10454
11106
|
return error;
|
|
@@ -10560,7 +11212,7 @@ async function drawDiagram(workspace, sectionId, signal) {
|
|
|
10560
11212
|
const requestedModel = resolveModel({ review: { session: workspace.session } }) ?? void 0;
|
|
10561
11213
|
let raw;
|
|
10562
11214
|
try {
|
|
10563
|
-
raw = await withUsageRecorded(workspace.id, "diagram", env, (onUsage) => agentFor(env).run({
|
|
11215
|
+
raw = await withUsageRecorded(workspace.id, "diagram", env, (onUsage) => agentFor$1(env).run({
|
|
10564
11216
|
capability: "read-code",
|
|
10565
11217
|
prompt: buildPrompt$1(workspace, sectionId, cwd),
|
|
10566
11218
|
cwd,
|
|
@@ -10747,7 +11399,7 @@ async function drawChangeMap(workspace, signal) {
|
|
|
10747
11399
|
const requestedModel = resolveModel({ review: { session: workspace.session } }) ?? void 0;
|
|
10748
11400
|
let raw;
|
|
10749
11401
|
try {
|
|
10750
|
-
raw = await withUsageRecorded(workspace.id, "change-map", env, (onUsage) => agentFor(env).run({
|
|
11402
|
+
raw = await withUsageRecorded(workspace.id, "change-map", env, (onUsage) => agentFor$1(env).run({
|
|
10751
11403
|
capability: "read-code",
|
|
10752
11404
|
prompt: buildPrompt(workspace, cwd),
|
|
10753
11405
|
cwd,
|
|
@@ -11445,6 +12097,7 @@ async function handle(req, res, options) {
|
|
|
11445
12097
|
if (url.pathname === "/fetch-updates") return handleFetchUpdates(req, res, options.indexToken);
|
|
11446
12098
|
if (url.pathname === "/shutdown") return handleIndexShutdown(req, res, options);
|
|
11447
12099
|
if (url.pathname === "/restart") return handleIndexRestart(req, res, options);
|
|
12100
|
+
if (url.pathname.startsWith("/add-review/")) return handleAddReview(req, res, url.pathname, options);
|
|
11448
12101
|
const match = /^\/(?:r|api)\/([0-9a-f-]{36})(\/[a-z]+)?$/.exec(url.pathname);
|
|
11449
12102
|
if (!match) return send(res, 404, { error: "Not found" });
|
|
11450
12103
|
const [, id, action] = match;
|
|
@@ -12002,6 +12655,43 @@ async function handleFetchUpdates(req, res, indexToken) {
|
|
|
12002
12655
|
send(res, 200, await runFetchUpdates(new Set(ids)));
|
|
12003
12656
|
}
|
|
12004
12657
|
/**
|
|
12658
|
+
* Add review: start pull request and branch reviews from any workspace page.
|
|
12659
|
+
*
|
|
12660
|
+
* The index credential, as the cookie, because starting a review is the machine's business rather
|
|
12661
|
+
* than any one review's, the same authority as `/queue` and `/fetch-updates`. The cookie is set on
|
|
12662
|
+
* `Path=/`, so a review page carries it too. See `add_review.ts` for what each route does.
|
|
12663
|
+
*/
|
|
12664
|
+
async function handleAddReview(req, res, pathname, options) {
|
|
12665
|
+
if (!options.indexToken || !secretMatches(options.indexToken, indexCookie(req))) return send(res, 403, { error: "Not authorised." });
|
|
12666
|
+
const deps = options.addReview ?? (addReviewDeps ??= realAddReviewDeps());
|
|
12667
|
+
const route = pathname.slice(12);
|
|
12668
|
+
const answer = async () => {
|
|
12669
|
+
if (req.method === "GET") {
|
|
12670
|
+
if (route === "options") return addReviewOptions(deps);
|
|
12671
|
+
if (route === "folders") return listFolders(deps);
|
|
12672
|
+
const batch = /^batches\/([0-9a-f-]{36})$/.exec(route);
|
|
12673
|
+
if (batch) return batchRows(batch[1]);
|
|
12674
|
+
return null;
|
|
12675
|
+
}
|
|
12676
|
+
if (req.method !== "POST") return {
|
|
12677
|
+
status: 405,
|
|
12678
|
+
body: { error: "Method not allowed." }
|
|
12679
|
+
};
|
|
12680
|
+
if (route === "folder/choose") return chooseFolder(deps);
|
|
12681
|
+
const body = await readJson(req);
|
|
12682
|
+
if (route === "pull-requests/preview") return previewPullRequests(body?.text, deps);
|
|
12683
|
+
if (route === "pull-requests") return startPullRequestBatch(body, deps);
|
|
12684
|
+
if (route === "folder/inspect") return inspectFolder(body?.path, deps, body?.base);
|
|
12685
|
+
if (route === "branch") return startBranchReview(body, deps);
|
|
12686
|
+
return null;
|
|
12687
|
+
};
|
|
12688
|
+
const result = await answer();
|
|
12689
|
+
if (!result) return send(res, 404, { error: "Not found." });
|
|
12690
|
+
send(res, result.status, result.body);
|
|
12691
|
+
}
|
|
12692
|
+
/** Built on first use, so a server nobody adds a review on never runs the detectors. */
|
|
12693
|
+
var addReviewDeps;
|
|
12694
|
+
/**
|
|
12005
12695
|
* Fetch remote updates, from one review's own page, on its own token.
|
|
12006
12696
|
*
|
|
12007
12697
|
* The list's fetch scoped to this review: `GET` says whether it applies (the page draws the item
|