@dev.fast/whiteboard 0.3.3 → 0.3.4-preview.20261019.125
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/{account-alias-xOdmMnDH.js → account-alias-ClRtWF_-.js} +8 -6
- package/dist/{agent-cli-DT7MdaqL.js → agent-cli-C6AM9dml.js} +15 -10
- package/dist/agent-cli-DKpWo-4F.js +2 -0
- package/dist/{agent-client-BRBzv3AV.js → agent-client-G4KxB8zB.js} +13 -3
- package/dist/{authoring-tools-DGSEag8y.js → authoring-tools-Dfo0uOSj.js} +34 -27
- package/dist/{background-server-WKzI_a98.js → background-server-BF_lLFxY.js} +5 -5
- package/dist/{background-server-C_REWFkG.js → background-server-DJfYWRoN.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/{cli-BBSDi_dX.js → cli-CqOiiTW_.js} +1 -1
- package/dist/{cli-runner-DshTBske.js → cli-runner-D8CsXff4.js} +26 -26
- package/dist/cli.js +2 -2
- package/dist/{client-BN_FDxFr.js → client-lKVF_Elo.js} +253 -47
- package/dist/{fuzzy-match-bFX6wm8j.js → fuzzy-match-DH9lZivo.js} +42 -14
- package/dist/{headless-host-CS4Gv0c5.js → headless-host-C4bhVPZx.js} +513 -140
- package/dist/{headless-host-CzvYpKOX.js → headless-host-CeNnXxiL.js} +1 -1
- package/dist/lens-selection-CyJapcsl.js +214 -0
- package/dist/{local-data-DtQTIzHX.js → local-data-BdqqCYUD.js} +2434 -328
- package/dist/{mcp-DpKnnmHH.js → mcp-B2mvDEhC.js} +5 -9
- package/dist/{package-paths-jMg6JxDP.js → package-paths-Cm1tR8c4.js} +1 -1
- package/dist/{profile-DaqmODhc.js → profile-DcigfSms.js} +20 -8
- package/dist/profile-DthRnZ7x.js +2 -0
- package/dist/{remote-attach-DCngg8cl.js → remote-attach-DGwVRmY5.js} +4 -4
- package/dist/{remote-extensions-BCAbQZTv.js → remote-extensions-CferWwM9.js} +3 -3
- package/dist/{remote-extensions-Cf3L-ZGG.js → remote-extensions-D8W2IgKH.js} +1 -1
- package/dist/{remote-language-server-UWQgpoh-.js → remote-language-server-By-rEosC.js} +5 -5
- package/dist/{remote-uninstall-CcDOcGyi.js → remote-uninstall-DgHnDAFH.js} +6 -6
- package/dist/{request-origin-Zcn_CjX4.js → request-origin-DDmfnytX.js} +38 -10
- package/dist/{review-app-launcher-tqSQPlbu.js → review-app-launcher-CG3TFZEx.js} +2 -2
- package/dist/{review-home-paths-Lc02WFuX.js → review-home-paths-D1JVitqN.js} +19 -17
- package/dist/{review-telemetry-BHWJsnoD.js → review-telemetry-Myzm7o6-.js} +5 -5
- package/dist/runtime-Bj2tD33R.js +27 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/{s3-77-qkEhs.js → s3-DtJFWC0G.js} +1 -1
- package/dist/{s3-config-DDWWMh5x.js → s3-config-BTO12tCC.js} +1 -1
- package/dist/{s3-config-0xuCh-F_.js → s3-config-CSy3fx8H.js} +1 -1
- package/dist/server/desktop-host.js +29 -76
- package/dist/{server-discovery-BVqrE1_-.js → server-discovery-Bwa_6Tsj.js} +3 -3
- package/dist/sharing/index.d.ts +486 -70
- package/dist/sharing/index.js +1 -1
- package/dist/{src-DsM3jO8G.js → src-BiWNbcF_.js} +89 -12
- package/dist/{src-BMPuxWv4.js → src-WPaQn75m.js} +208 -73
- package/dist/{stored-document-migration-CChxtFAy.js → stored-document-migration-dFbewRTG.js} +270 -269
- package/dist/{thread-state-5pYr6_zM.js → thread-state-Do5LX10I.js} +7 -2
- package/dist/{tool-failure-BFt2xj9i.js → tool-failure-C9J3r7KD.js} +10 -3
- package/instructions/authoring.md +3 -2
- package/package.json +4 -4
- package/src/agent-connect.ts +8 -2
- package/src/ask/agents.ts +51 -9
- package/src/ask/checkout-files.ts +2 -5
- package/src/ask/thread-state.ts +4 -0
- package/src/ask/thread.ts +16 -0
- package/src/ask/threads.ts +24 -3
- package/src/cli-runner.ts +4 -1
- package/src/diff-selection-migration.ts +48 -34
- package/src/heading-anchors.ts +42 -0
- package/src/lens-selection.ts +38 -2
- package/src/review-api/README.md +52 -13
- package/src/review-api/activity.ts +1 -4
- package/src/review-api/agent-cli.ts +9 -1
- package/src/review-api/anchor-quotes.ts +14 -7
- package/src/review-api/ask-history.ts +137 -51
- package/src/review-api/authoring-tools.ts +45 -30
- package/src/review-api/blocks/definition.ts +1 -1
- package/src/review-api/blocks/markdown.ts +2 -2
- package/src/review-api/board-order.ts +144 -0
- package/src/review-api/checkout-groups.ts +560 -0
- package/src/review-api/document-headings.ts +58 -27
- package/src/review-api/document-text.ts +31 -2
- package/src/review-api/document.ts +281 -34
- package/src/review-api/foreign-references.ts +328 -0
- package/src/review-api/github-provider.ts +51 -13
- package/src/review-api/http.ts +702 -123
- package/src/review-api/instructions.ts +0 -19
- package/src/review-api/link-preview.ts +113 -0
- package/src/review-api/local-data.ts +310 -46
- package/src/review-api/mcp.ts +3 -11
- package/src/review-api/onboarding.ts +1 -1
- package/src/review-api/profile.ts +37 -6
- package/src/review-api/public-tools.ts +24 -2
- package/src/review-api/pull-request.ts +82 -49
- package/src/review-api/request-origin.ts +10 -0
- package/src/review-api/review-progress.ts +61 -27
- package/src/review-api/scratchpad-plan-upgrade.ts +250 -0
- package/src/review-api/store-migrations.ts +357 -0
- package/src/review-api/store-schema.ts +38 -29
- package/src/review-api/store.ts +2144 -480
- package/src/review-api/workspaces.ts +52 -13
- package/src/review-api/worktree-events.ts +3 -5
- package/src/review-api/worktree-source.ts +3 -7
- package/src/review-api/worktree-structural.integration.ts +72 -0
- package/src/review-stack.ts +15 -23
- package/src/runtime.ts +4 -30
- package/src/server/desktop-host.ts +6 -1
- package/src/server/desktop-server.ts +1 -31
- package/src/server/headless-host.ts +0 -2
- package/src/server/hono-http.ts +2 -1
- package/src/server/legacy-tutorial-cleanup.ts +2 -3
- package/src/server/review-gateway-list.ts +3 -5
- package/src/server/review-gateway-memory.ts +1 -1
- package/src/server/review-lifecycle-telemetry.ts +2 -2
- package/src/server/review-server-core.ts +1 -5
- package/src/server/structural-comparisons.ts +47 -0
- package/src/sharing/export.ts +36 -7
- package/src/sharing/host.ts +34 -14
- package/src/sharing/import.ts +156 -24
- package/src/sharing/repository.ts +128 -20
- package/src/startup-trace.ts +3 -3
- package/src/stored-document-migration.ts +110 -19
- package/src/ui-telemetry-events.ts +25 -9
- package/dist/agent-cli-DeCrUC2s.js +0 -2
- package/dist/profile-Cz7L781D.js +0 -2
- package/dist/runtime-CKiWhGrf.js +0 -52
- package/instructions/scratchpad.md +0 -23
- package/src/review-preferences.ts +0 -88
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
import { d as git, r as defaultBranch } from "./src-
|
|
1
|
+
import { E as runVcs, S as resolveRepoContextSync, d as git, r as defaultBranch } from "./src-WPaQn75m.js";
|
|
2
2
|
import { c as jsonString, m as isStringValue, n as isJsonObject, p as isObjectValue, u as parseJsonText } from "./src-X9phtB2j.js";
|
|
3
|
-
import { a as sharedReviewId, c as verifyShareRepository, d as
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { B as TraceStorageDeniedError, D as listReviewTraceSessions, I as isTraceStorageConfigured, O as loadReviewAgentTrace, R as resolveTraceStorage, T as traceMachineEnabled, at as
|
|
7
|
-
import {
|
|
3
|
+
import { a as sharedReviewId, c as verifyShareRepository, d as validateShareSources, f as StreamLimitError, h as shareIdSchema, l as exportShare, m as parseShareLink, n as ShareClient, p as readBoundedStream, r as SharePreflightError, s as readShareRepository, t as ShareAuthError, u as referenceCommits } from "./client-lKVF_Elo.js";
|
|
4
|
+
import { f as textualRows, h as diffrMissingError, i as inspectSnapshot, m as diffrExecutable, p as invalidateStructuralComparisons, r as commandSchema } from "./local-data-BdqqCYUD.js";
|
|
5
|
+
import { N as reviewDiffrSummarizerInputSchema, O as ReviewFeedbackContactSchema, P as decodeStructuralDiffEvent, a as parseReviewVerbRequest, c as boardIconKind, i as parseReviewDesktopVerbResult, j as ReviewOnboardingRunRequestSchema, k as ReviewFeedbackSchema, l as boardTitle, m as REVIEW_CLIENT_REMOTE, p as REVIEW_CLIENT_HEADER, v as REVIEW_HOST_HEADER } from "./src-BiWNbcF_.js";
|
|
6
|
+
import { B as TraceStorageDeniedError, D as listReviewTraceSessions, I as isTraceStorageConfigured, O as loadReviewAgentTrace, R as resolveTraceStorage, T as traceMachineEnabled, at as readStoreAuth, et as shellQuote, it as clearStoreAuth, ot as runStoreLogin, rt as DEFAULT_STORE_ORIGIN, z as selectTraceStorage } from "./review-home-paths-D1JVitqN.js";
|
|
7
|
+
import { _ as scopedCoverage, h as emptyCoverage, i as anchorSelection, l as resolveDiffSelection, m as coverageSources, n as SelectionUnavailableError, o as comparisonKey, s as formatAnchor, u as selectionKey, v as subtractIntervals, y as unionIntervals } from "./lens-selection-CyJapcsl.js";
|
|
8
|
+
import { B as stepSources, F as selectionReferences, G as lensSelections, J as fileLineRangeSchema, V as UNCATEGORIZED_LENS_ID, _ as boardRepositoryId, f as anchorPins, g as boardLinks, x as elements } from "./stored-document-migration-dFbewRTG.js";
|
|
8
9
|
import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
|
|
9
|
-
import {
|
|
10
|
+
import { L as writePrivateJsonAtomic, N as devReviewHome, h as TraceConfigurationError, i as isS3MockMode, j as withFileLock } from "./s3-config-BTO12tCC.js";
|
|
10
11
|
import { t as errorMessage } from "./error-message-OtiDonty.js";
|
|
11
|
-
import { i as readReviewPackageVersion, n as readBuildCommit, t as findReviewPackageRoot } from "./package-paths-
|
|
12
|
-
import { _ as reviewInstanceIdentity, i as readReviewServerDiscovery, n as desktopOwnsHome, o as reviewServerDiscoveryPath, p as desktopUsesHome, r as headlessServerLockPath, t as DESKTOP_TAKEOVER_SIGNAL } from "./server-discovery-
|
|
13
|
-
import { c as askPicksSchema, d as selectionMarkdown, l as askQuestionSchema, n as askAgentIds, r as askChoiceKinds, s as askPermissionsSchema, t as applyAskChange, u as AgentSelectionSchema } from "./thread-state-
|
|
14
|
-
import { t as openReviewProfile } from "./profile-
|
|
15
|
-
import {
|
|
16
|
-
import { a as defaultAskPermissions,
|
|
17
|
-
import { a as
|
|
18
|
-
import { a as headlessServerOwner, c as stopBackgroundServer } from "./background-server-
|
|
12
|
+
import { i as readReviewPackageVersion, n as readBuildCommit, t as findReviewPackageRoot } from "./package-paths-Cm1tR8c4.js";
|
|
13
|
+
import { _ as reviewInstanceIdentity, i as readReviewServerDiscovery, n as desktopOwnsHome, o as reviewServerDiscoveryPath, p as desktopUsesHome, r as headlessServerLockPath, t as DESKTOP_TAKEOVER_SIGNAL } from "./server-discovery-Bwa_6Tsj.js";
|
|
14
|
+
import { c as askPicksSchema, d as selectionMarkdown, l as askQuestionSchema, n as askAgentIds, r as askChoiceKinds, s as askPermissionsSchema, t as applyAskChange, u as AgentSelectionSchema } from "./thread-state-Do5LX10I.js";
|
|
15
|
+
import { t as openReviewProfile } from "./profile-DcigfSms.js";
|
|
16
|
+
import { c as REVIEW_APP_SESSION_ID_HEADER, d as sanitizeUiTelemetryEvent, f as cleanTelemetryText, o as reviewRequestOrigin, r as REVIEW_PERSON_HEADER, s as BUNDLE_FRAME_PATTERN, t as ASK_THREAD_ENV, u as isReportableCleanedMessage } from "./request-origin-DDmfnytX.js";
|
|
17
|
+
import { a as defaultAskPermissions, i as askAgents, l as launchAskAgent, n as askAgentSettings, o as detectAskAgents, r as askAgentTakesMcp, t as fuzzyRank } from "./fuzzy-match-DH9lZivo.js";
|
|
18
|
+
import { a as inspectQuerySchema, i as renderInstructions, o as queryAnchor, r as instructionsQuerySchema, s as readQuerySchemas, t as authoringTools } from "./authoring-tools-Dfo0uOSj.js";
|
|
19
|
+
import { a as headlessServerOwner, c as stopBackgroundServer } from "./background-server-BF_lLFxY.js";
|
|
19
20
|
import crypto$1, { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
20
21
|
import { z } from "zod";
|
|
21
22
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
@@ -213,6 +214,7 @@ var AskThread = class {
|
|
|
213
214
|
/** The number of changes so far; a snapshot carries the one it includes. */
|
|
214
215
|
seq = 0;
|
|
215
216
|
listeners = /* @__PURE__ */ new Set();
|
|
217
|
+
statusListeners = /* @__PURE__ */ new Set();
|
|
216
218
|
closers = /* @__PURE__ */ new Set();
|
|
217
219
|
decisions = /* @__PURE__ */ new Map();
|
|
218
220
|
process;
|
|
@@ -318,11 +320,19 @@ var AskThread = class {
|
|
|
318
320
|
snapshot: this.state
|
|
319
321
|
};
|
|
320
322
|
}
|
|
323
|
+
get status() {
|
|
324
|
+
return this.state.status;
|
|
325
|
+
}
|
|
321
326
|
/** Every change after the current `seq`, in order. */
|
|
322
327
|
subscribe(listener) {
|
|
323
328
|
this.listeners.add(listener);
|
|
324
329
|
return () => this.listeners.delete(listener);
|
|
325
330
|
}
|
|
331
|
+
/** Calls back when the status changes, without following the thread. */
|
|
332
|
+
onStatusChange(listener) {
|
|
333
|
+
this.statusListeners.add(listener);
|
|
334
|
+
return () => this.statusListeners.delete(listener);
|
|
335
|
+
}
|
|
326
336
|
/** Whether anything follows the thread, as an open panel does. */
|
|
327
337
|
watched() {
|
|
328
338
|
return this.listeners.size > 0;
|
|
@@ -579,7 +589,8 @@ var AskThread = class {
|
|
|
579
589
|
const generation = ++this.generation;
|
|
580
590
|
const launched = await this.launch(this.start.agent, this.start.cwd, {
|
|
581
591
|
permissions: this.state.permissions,
|
|
582
|
-
mcpServers: this.mcpServers
|
|
592
|
+
mcpServers: this.mcpServers,
|
|
593
|
+
threadId: this.id
|
|
583
594
|
});
|
|
584
595
|
if (this.closed || this.halted || generation !== this.generation) {
|
|
585
596
|
launched.stop();
|
|
@@ -1177,6 +1188,7 @@ var AskThread = class {
|
|
|
1177
1188
|
change
|
|
1178
1189
|
};
|
|
1179
1190
|
for (const listener of this.listeners) listener(update);
|
|
1191
|
+
if (change.type === "set" && change.status) for (const listener of this.statusListeners) listener();
|
|
1180
1192
|
}
|
|
1181
1193
|
};
|
|
1182
1194
|
//#endregion
|
|
@@ -1261,13 +1273,16 @@ function cliAskTools(cliPath, selector) {
|
|
|
1261
1273
|
}] : []
|
|
1262
1274
|
];
|
|
1263
1275
|
return {
|
|
1264
|
-
mcpServers: () => {
|
|
1276
|
+
mcpServers: (threadId) => {
|
|
1265
1277
|
const cli = cliPath();
|
|
1266
1278
|
return cli ? [{
|
|
1267
1279
|
name: "whiteboard",
|
|
1268
1280
|
command: process.execPath,
|
|
1269
1281
|
args: [cli, "mcp"],
|
|
1270
|
-
env: env()
|
|
1282
|
+
env: [...env(), {
|
|
1283
|
+
name: ASK_THREAD_ENV,
|
|
1284
|
+
value: threadId
|
|
1285
|
+
}]
|
|
1271
1286
|
}] : [];
|
|
1272
1287
|
},
|
|
1273
1288
|
cli: () => {
|
|
@@ -1295,6 +1310,7 @@ var AskThreads = class {
|
|
|
1295
1310
|
* model. */
|
|
1296
1311
|
offers = /* @__PURE__ */ new Map();
|
|
1297
1312
|
mcpServers;
|
|
1313
|
+
statusListeners = /* @__PURE__ */ new Set();
|
|
1298
1314
|
constructor(launch, tools = {}, limits = askThreadLimits, unwatchedIdleMs = UNWATCHED_IDLE_MS) {
|
|
1299
1315
|
this.launch = launch;
|
|
1300
1316
|
this.tools = tools;
|
|
@@ -1315,6 +1331,8 @@ var AskThreads = class {
|
|
|
1315
1331
|
open(start) {
|
|
1316
1332
|
const thread = new AskThread(this.launch, start, this.mcpServers, this.limits);
|
|
1317
1333
|
this.threads.set(thread.id, thread);
|
|
1334
|
+
thread.onStatusChange(() => this.statusChanged());
|
|
1335
|
+
this.statusChanged();
|
|
1318
1336
|
thread.open();
|
|
1319
1337
|
this.sweeper ??= setInterval(() => this.sweep(), SWEEP_MS);
|
|
1320
1338
|
this.sweeper.unref?.();
|
|
@@ -1345,6 +1363,16 @@ var AskThreads = class {
|
|
|
1345
1363
|
if (now - since >= this.unwatchedIdleMs) this.close(id);
|
|
1346
1364
|
}
|
|
1347
1365
|
}
|
|
1366
|
+
/** Calls back when a thread opens, closes or changes its status. */
|
|
1367
|
+
subscribeStatus(listener) {
|
|
1368
|
+
this.statusListeners.add(listener);
|
|
1369
|
+
return () => {
|
|
1370
|
+
this.statusListeners.delete(listener);
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
statusChanged() {
|
|
1374
|
+
for (const listener of this.statusListeners) listener();
|
|
1375
|
+
}
|
|
1348
1376
|
get(id) {
|
|
1349
1377
|
return this.threads.get(id);
|
|
1350
1378
|
}
|
|
@@ -1361,9 +1389,9 @@ var AskThreads = class {
|
|
|
1361
1389
|
}
|
|
1362
1390
|
close(id, save = true) {
|
|
1363
1391
|
const thread = this.threads.get(id);
|
|
1364
|
-
this.threads.delete(id);
|
|
1365
1392
|
this.unwatchedSince.delete(id);
|
|
1366
1393
|
thread?.close(save);
|
|
1394
|
+
if (this.threads.delete(id)) this.statusChanged();
|
|
1367
1395
|
}
|
|
1368
1396
|
closeDeletedReviews(isDeleted) {
|
|
1369
1397
|
for (const [id, thread] of this.threads) if (isDeleted(thread.reviewId)) this.close(id, false);
|
|
@@ -1376,7 +1404,7 @@ var AskThreads = class {
|
|
|
1376
1404
|
};
|
|
1377
1405
|
//#endregion
|
|
1378
1406
|
//#region src/server/diffr-config.ts
|
|
1379
|
-
const execFileAsync
|
|
1407
|
+
const execFileAsync = promisify(execFile);
|
|
1380
1408
|
let writes = Promise.resolve();
|
|
1381
1409
|
let testing = false;
|
|
1382
1410
|
function serialized(operation) {
|
|
@@ -1417,7 +1445,7 @@ function diffrArgv(args) {
|
|
|
1417
1445
|
async function diffr(args, rootPath, options = {}) {
|
|
1418
1446
|
const executable = diffrExecutable();
|
|
1419
1447
|
try {
|
|
1420
|
-
const operation = execFileAsync
|
|
1448
|
+
const operation = execFileAsync(executable, diffrArgv(args), {
|
|
1421
1449
|
cwd: rootPath,
|
|
1422
1450
|
maxBuffer: 16777216,
|
|
1423
1451
|
signal: options.signal ?? AbortSignal.timeout(3e4),
|
|
@@ -1847,7 +1875,7 @@ function applyCorsHeaders(request, response) {
|
|
|
1847
1875
|
const varyFields = vary?.split(",").map((field) => field.trim().toLowerCase());
|
|
1848
1876
|
if (!varyFields?.includes("*") && !varyFields?.includes("origin")) response.headers.set("vary", vary ? `${vary}, Origin` : "Origin");
|
|
1849
1877
|
}
|
|
1850
|
-
response.headers.set("access-control-allow-headers", `content-type, x-review-token, ${REVIEW_APP_SESSION_ID_HEADER}`);
|
|
1878
|
+
response.headers.set("access-control-allow-headers", `content-type, x-review-token, ${REVIEW_PERSON_HEADER}, ${REVIEW_APP_SESSION_ID_HEADER}`);
|
|
1851
1879
|
response.headers.set("access-control-allow-methods", "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS");
|
|
1852
1880
|
response.headers.set("access-control-allow-private-network", "true");
|
|
1853
1881
|
response.headers.set("access-control-expose-headers", REVIEW_HOST_HEADER);
|
|
@@ -2276,11 +2304,15 @@ async function reviewProgress(store, data, snapshot, signal = new AbortControlle
|
|
|
2276
2304
|
return [];
|
|
2277
2305
|
}
|
|
2278
2306
|
if (!file) {
|
|
2279
|
-
const [base, head] = await Promise.all([
|
|
2280
|
-
|
|
2281
|
-
|
|
2307
|
+
const [base, head] = await Promise.all(["base", "head"].map((side) => data.file(sourcePins, side, source.file).then((value) => value.text, (error) => {
|
|
2308
|
+
if (error instanceof ReviewInputError && error.status === 404) return null;
|
|
2309
|
+
throw error;
|
|
2310
|
+
})));
|
|
2311
|
+
if (base === null && head === null) throw new SelectionUnavailableError("missing_file", "File is unavailable at these pins.");
|
|
2312
|
+
if (base !== null && head !== null && base !== head) throw new SelectionUnavailableError("no_alignment", "Changed context file has no alignment.");
|
|
2313
|
+
rows = textualRows(source.file, "", base?.split("\n").length ?? 0, head?.split("\n").length ?? 0);
|
|
2282
2314
|
}
|
|
2283
|
-
if (!rows) throw new
|
|
2315
|
+
if (!rows) throw new SelectionUnavailableError("no_alignment", "File has no text alignment.");
|
|
2284
2316
|
const result = resolveDiffSelection(source, rows, file ?? { path: source.file });
|
|
2285
2317
|
resolvedSelections[key] = result;
|
|
2286
2318
|
return result;
|
|
@@ -2288,8 +2320,8 @@ async function reviewProgress(store, data, snapshot, signal = new AbortControlle
|
|
|
2288
2320
|
const resolveAvailable = async (source) => {
|
|
2289
2321
|
try {
|
|
2290
2322
|
return await resolve(source);
|
|
2291
|
-
} catch {
|
|
2292
|
-
unavailableSelections[selectionKey(source)] =
|
|
2323
|
+
} catch (error) {
|
|
2324
|
+
unavailableSelections[selectionKey(source)] = error instanceof SelectionUnavailableError ? error.reason : "other";
|
|
2293
2325
|
return [];
|
|
2294
2326
|
}
|
|
2295
2327
|
};
|
|
@@ -2315,7 +2347,10 @@ async function reviewProgress(store, data, snapshot, signal = new AbortControlle
|
|
|
2315
2347
|
};
|
|
2316
2348
|
}
|
|
2317
2349
|
}));
|
|
2318
|
-
await Promise.all(selectionReferences(snapshot.document, {
|
|
2350
|
+
await Promise.all(selectionReferences(snapshot.document, {
|
|
2351
|
+
tolerant: true,
|
|
2352
|
+
repositoryId: boardRepositoryId(snapshot)
|
|
2353
|
+
}).map(async ({ source }) => {
|
|
2319
2354
|
try {
|
|
2320
2355
|
await resolveAvailable(source);
|
|
2321
2356
|
} catch {}
|
|
@@ -2331,7 +2366,7 @@ async function reviewProgress(store, data, snapshot, signal = new AbortControlle
|
|
|
2331
2366
|
unavailable: counting || uncategorized.length ? void 0 : "All changed lines are covered by lenses"
|
|
2332
2367
|
});
|
|
2333
2368
|
const progress = {
|
|
2334
|
-
complete: !counting,
|
|
2369
|
+
complete: !counting && !pendingSources.size,
|
|
2335
2370
|
files,
|
|
2336
2371
|
referenceFiles,
|
|
2337
2372
|
lenses,
|
|
@@ -2388,12 +2423,11 @@ function lensReport(lenses, progress) {
|
|
|
2388
2423
|
}
|
|
2389
2424
|
//#endregion
|
|
2390
2425
|
//#region src/ask/checkout-files.ts
|
|
2391
|
-
const execFileAsync$1 = promisify(execFile);
|
|
2392
2426
|
/** The checkout's files relative to its root: tracked ones, and new ones git
|
|
2393
2427
|
* does not ignore. None where git cannot list them. */
|
|
2394
2428
|
async function checkoutFiles(root) {
|
|
2395
2429
|
try {
|
|
2396
|
-
const { stdout } = await
|
|
2430
|
+
const { stdout } = await runVcs("git", [
|
|
2397
2431
|
"-C",
|
|
2398
2432
|
root,
|
|
2399
2433
|
"ls-files",
|
|
@@ -2564,7 +2598,6 @@ function watchAskThreads(threads, backlog = 256) {
|
|
|
2564
2598
|
}
|
|
2565
2599
|
//#endregion
|
|
2566
2600
|
//#region src/review-stack.ts
|
|
2567
|
-
const execFileAsync = promisify(execFile);
|
|
2568
2601
|
const GitHubStacksSchema = z.array(z.object({ pull_requests: z.array(z.object({
|
|
2569
2602
|
number: z.number().int().positive(),
|
|
2570
2603
|
head: z.object({ ref: z.string().min(1) })
|
|
@@ -2612,12 +2645,12 @@ async function gitHubStack(subject, reviews, runGitHubApi) {
|
|
|
2612
2645
|
}
|
|
2613
2646
|
/** The chain of reviews through this one, linked by the base each names. */
|
|
2614
2647
|
async function localStack(subject, reviews, repositoryPath) {
|
|
2615
|
-
const group = reviews.find((review) => review.reviewId === subject.reviewId)?.
|
|
2648
|
+
const group = reviews.find((review) => review.reviewId === subject.reviewId)?.project?.id;
|
|
2616
2649
|
const current = subject.origin?.branch;
|
|
2617
2650
|
if (!group || !current) return [];
|
|
2618
2651
|
const main = subject.origin?.baseBranch ? await defaultBranch(repositoryPath).catch(() => null) : null;
|
|
2619
2652
|
const name = (ref) => ref.replace(/^origin\//, "");
|
|
2620
|
-
const inGroup = reviews.filter((review) => review.
|
|
2653
|
+
const inGroup = reviews.filter((review) => review.project?.id === group);
|
|
2621
2654
|
const latest = (field, branch) => {
|
|
2622
2655
|
const matches = inGroup.filter((review) => review.origin?.[field] === branch);
|
|
2623
2656
|
return matches.findLast((review) => !review.trashedAt) ?? matches.at(-1);
|
|
@@ -2644,8 +2677,8 @@ async function forkPoint(repositoryPath, main, branch) {
|
|
|
2644
2677
|
const ref = `refs/heads/${branch}`;
|
|
2645
2678
|
const fork = await git(repositoryPath, [
|
|
2646
2679
|
"merge-base",
|
|
2647
|
-
main,
|
|
2648
|
-
ref
|
|
2680
|
+
{ rev: main },
|
|
2681
|
+
{ rev: ref }
|
|
2649
2682
|
], { allowFailure: true });
|
|
2650
2683
|
if (!fork.ok) return null;
|
|
2651
2684
|
const base = fork.stdout.trim();
|
|
@@ -2655,22 +2688,17 @@ async function forkPoint(repositoryPath, main, branch) {
|
|
|
2655
2688
|
"log",
|
|
2656
2689
|
"--reverse",
|
|
2657
2690
|
"--format=%s",
|
|
2658
|
-
`${base}..${ref}`
|
|
2691
|
+
{ rev: `${base}..${ref}` }
|
|
2659
2692
|
], { allowFailure: true })).stdout.split("\n")[0] || null
|
|
2660
2693
|
};
|
|
2661
2694
|
}
|
|
2662
2695
|
async function defaultRunGitHubApi(host, endpoint) {
|
|
2663
|
-
const { stdout } = await
|
|
2696
|
+
const { stdout } = await runVcs("gh", [
|
|
2664
2697
|
"api",
|
|
2665
2698
|
"--hostname",
|
|
2666
|
-
host,
|
|
2667
|
-
endpoint
|
|
2699
|
+
{ value: host },
|
|
2700
|
+
{ value: endpoint }
|
|
2668
2701
|
], {
|
|
2669
|
-
env: {
|
|
2670
|
-
...process.env,
|
|
2671
|
-
GH_PROMPT_DISABLED: "1"
|
|
2672
|
-
},
|
|
2673
|
-
encoding: "utf8",
|
|
2674
2702
|
timeout: 1e4,
|
|
2675
2703
|
maxBuffer: 2097152
|
|
2676
2704
|
});
|
|
@@ -2708,7 +2736,6 @@ const publishSchema = z.strictObject({
|
|
|
2708
2736
|
/** Mounted behind local host authentication. Account credentials never enter the renderer. */
|
|
2709
2737
|
function mountSharingHost(app, store, data, shared, options = {}) {
|
|
2710
2738
|
const startLogin = options.login ?? runStoreLogin;
|
|
2711
|
-
const openUrl = options.openUrl ?? openUrlInBrowser;
|
|
2712
2739
|
let login = { pending: false };
|
|
2713
2740
|
app.get("/sharing/account", async (context) => {
|
|
2714
2741
|
const account = await readStoreAuth();
|
|
@@ -2731,9 +2758,9 @@ function mountSharingHost(app, store, data, shared, options = {}) {
|
|
|
2731
2758
|
traces: false,
|
|
2732
2759
|
stdout: discard,
|
|
2733
2760
|
stderr: discard,
|
|
2734
|
-
openUrl:
|
|
2735
|
-
|
|
2736
|
-
|
|
2761
|
+
openUrl: options.openUrl,
|
|
2762
|
+
onDeviceCode: (device) => {
|
|
2763
|
+
Object.assign(login, device);
|
|
2737
2764
|
}
|
|
2738
2765
|
}).then((code) => {
|
|
2739
2766
|
login = { pending: false };
|
|
@@ -2802,8 +2829,16 @@ function mountSharingPublisher(app, store, data, options = {}) {
|
|
|
2802
2829
|
validateShareSources(snapshot);
|
|
2803
2830
|
const account = await readSharingAuth();
|
|
2804
2831
|
if (!account) throw new ReviewInputError("Set DEV_REVIEW_SHARE_TOKEN for CI, or run whiteboard login before sharing.", 409);
|
|
2805
|
-
const
|
|
2806
|
-
const
|
|
2832
|
+
const repositoryId = snapshot.pins?.repositoryId ?? snapshot.target?.repositoryId;
|
|
2833
|
+
const root = repositoryId ? store.repositoryPath(repositoryId) : void 0;
|
|
2834
|
+
const readRepository = options.readRepository ?? readShareRepository;
|
|
2835
|
+
const repository = !root ? void 0 : snapshot.pins ? await readRepository(root) : await (async () => {
|
|
2836
|
+
try {
|
|
2837
|
+
return await readRepository(root);
|
|
2838
|
+
} catch {
|
|
2839
|
+
return;
|
|
2840
|
+
}
|
|
2841
|
+
})();
|
|
2807
2842
|
const bundle = await exportShare({
|
|
2808
2843
|
store,
|
|
2809
2844
|
data,
|
|
@@ -2811,7 +2846,8 @@ function mountSharingPublisher(app, store, data, options = {}) {
|
|
|
2811
2846
|
version: snapshot.version,
|
|
2812
2847
|
repository
|
|
2813
2848
|
});
|
|
2814
|
-
const
|
|
2849
|
+
const commits = referenceCommits(snapshot);
|
|
2850
|
+
const verification = root && (snapshot.pins || commits.length) && repository ? verifyRepository(root, snapshot.pins, repository, commits).then(() => ({ ok: true }), (error) => ({
|
|
2815
2851
|
ok: false,
|
|
2816
2852
|
error
|
|
2817
2853
|
})) : Promise.resolve({ ok: true });
|
|
@@ -2887,8 +2923,14 @@ const clip = (line) => line.trim() === "" ? "(blank line)" : line.length > 120 ?
|
|
|
2887
2923
|
*/
|
|
2888
2924
|
async function anchorQuotes(before, after, read) {
|
|
2889
2925
|
const key = (id, source) => `${id}\0${selectionKey(source)}`;
|
|
2890
|
-
const known = new Set(before ? selectionReferences(before.document, {
|
|
2891
|
-
|
|
2926
|
+
const known = new Set(before ? selectionReferences(before.document, {
|
|
2927
|
+
tolerant: true,
|
|
2928
|
+
repositoryId: boardRepositoryId(before)
|
|
2929
|
+
}).map(({ id, source }) => key(id, source)) : []);
|
|
2930
|
+
const added = selectionReferences(after.document, {
|
|
2931
|
+
tolerant: true,
|
|
2932
|
+
repositoryId: boardRepositoryId(after)
|
|
2933
|
+
}).filter(({ id, source }) => !known.has(key(id, source)));
|
|
2892
2934
|
return {
|
|
2893
2935
|
quotes: (await Promise.all(added.slice(0, QUOTE_LIMIT).map(async ({ source }) => {
|
|
2894
2936
|
try {
|
|
@@ -2913,15 +2955,18 @@ async function anchorQuotes(before, after, read) {
|
|
|
2913
2955
|
//#endregion
|
|
2914
2956
|
//#region src/review-api/document-text.ts
|
|
2915
2957
|
/** A reading view of saved content, not another document format to maintain. */
|
|
2916
|
-
function documentText(snapshot, targetId, full = false) {
|
|
2958
|
+
function documentText(snapshot, targetId, full = false, relations) {
|
|
2917
2959
|
const target = targetId === void 0 ? void 0 : elements(snapshot.document).find((item) => item.id === targetId);
|
|
2918
2960
|
if (targetId !== void 0 && !target) throw new ReviewInputError("Target not found in this version.", 404);
|
|
2919
2961
|
const detailed = full || target !== void 0;
|
|
2920
|
-
const lines = [
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2962
|
+
const lines = [`# ${boardTitle(snapshot.title)}`, `Session ${snapshot.reviewId} · version ${snapshot.version}`];
|
|
2963
|
+
if (relations && targetId === void 0) {
|
|
2964
|
+
const list = (boards) => boards.map((board) => `${boardTitle(board.title)} (${board.id})`).join(", ");
|
|
2965
|
+
if (relations.parent) lines.push(`Parent: ${list([relations.parent])}`);
|
|
2966
|
+
if (relations.children.length) lines.push(`Children, in order: ${list(relations.children)}`);
|
|
2967
|
+
if (relations.dependsOn.length) lines.push(`Depends on: ${list(relations.dependsOn)}`);
|
|
2968
|
+
}
|
|
2969
|
+
lines.push("");
|
|
2925
2970
|
const write = (depth, text) => {
|
|
2926
2971
|
for (const line of text.split("\n")) lines.push(" ".repeat(depth) + line);
|
|
2927
2972
|
};
|
|
@@ -3349,7 +3394,7 @@ function registerOnboardingRoutes(app, store, ask, host, run = runCommand) {
|
|
|
3349
3394
|
agent: input.agent,
|
|
3350
3395
|
...host.historyHooks({
|
|
3351
3396
|
id,
|
|
3352
|
-
reviewId,
|
|
3397
|
+
anchor: { board: reviewId },
|
|
3353
3398
|
agent: input.agent,
|
|
3354
3399
|
version: created.version,
|
|
3355
3400
|
head,
|
|
@@ -3497,12 +3542,16 @@ const askDecisionSchema = z.strictObject({
|
|
|
3497
3542
|
permissionId: z.string().min(1),
|
|
3498
3543
|
optionId: z.string().min(1)
|
|
3499
3544
|
});
|
|
3500
|
-
|
|
3545
|
+
/** A person's save carries the whole board, which edits one at a time can
|
|
3546
|
+
* grow past the size of a single command. */
|
|
3547
|
+
const PERSON_EDIT_MAX_BYTES = 16777216;
|
|
3548
|
+
/** A request the gateway forwarded from another machine. */
|
|
3549
|
+
const remoteCaller = (context) => context.req.header(REVIEW_CLIENT_HEADER) === REVIEW_CLIENT_REMOTE;
|
|
3501
3550
|
/** Both hosts mount this behind their token authentication. */
|
|
3502
3551
|
function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
3503
3552
|
desktopAvailable: Boolean(open),
|
|
3504
3553
|
softwareMapEnabled: false
|
|
3505
|
-
}),
|
|
3554
|
+
}), traceEnabled = async () => false, status = () => ({}), hooks = {}, ask) {
|
|
3506
3555
|
const app = new Hono();
|
|
3507
3556
|
app.onError((error, context) => {
|
|
3508
3557
|
if (error instanceof HttpJsonError) return context.json({ error: error.message }, error.statusCode);
|
|
@@ -3531,15 +3580,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3531
3580
|
await next();
|
|
3532
3581
|
};
|
|
3533
3582
|
const sharedCommandSchema = z.object({ operation: z.object({ reviewId: z.string().optional() }) });
|
|
3534
|
-
const ensureScratchpad = async (id) => {
|
|
3535
|
-
if (open && scratchpadEnabled() && (!id || id === SCRATCHPAD_ID)) await store.ensureScratchpad();
|
|
3536
|
-
};
|
|
3537
|
-
const refuseDisabledScratchpad = (id) => {
|
|
3538
|
-
if (id === SCRATCHPAD_ID && !scratchpadEnabled()) throw new ReviewInputError(SCRATCHPAD_DISABLED, 409);
|
|
3539
|
-
};
|
|
3540
3583
|
const sharedGuard = async (context, next) => {
|
|
3541
|
-
refuseDisabledScratchpad(context.req.param("id"));
|
|
3542
|
-
await ensureScratchpad(context.req.param("id"));
|
|
3543
3584
|
const id = context.req.param("id");
|
|
3544
3585
|
if (!id || !isShared(id)) return next();
|
|
3545
3586
|
const query = readQuerySchemas.get.parse({ version: context.req.query("version") });
|
|
@@ -3558,23 +3599,53 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3558
3599
|
if (!snapshot || version !== void 0 && version !== snapshot.version) throw new ReviewInputError("Shared review version is unavailable.", 404);
|
|
3559
3600
|
return snapshot;
|
|
3560
3601
|
};
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3602
|
+
/** Adds a PR board's state, as this host last read it from GitHub. */
|
|
3603
|
+
const withPullRequestState = (board) => {
|
|
3604
|
+
const state = data?.pullRequestState(board.origin?.pullRequestUrl ?? board.pullRequestUrl);
|
|
3605
|
+
return state ? {
|
|
3606
|
+
...board,
|
|
3607
|
+
pullRequestState: state
|
|
3608
|
+
} : board;
|
|
3564
3609
|
};
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3610
|
+
const catalog = () => [...store.list().map(withPullRequestState), ...shared?.list() ?? []];
|
|
3611
|
+
/** Where a board sits among its project's, and its PR's facts, for the
|
|
3612
|
+
* board's header. */
|
|
3613
|
+
const placeOf = (id) => {
|
|
3614
|
+
if (isShared(id) || !store.has(id)) return { siblings: [] };
|
|
3615
|
+
const { board, parent, siblings, children } = store.place(id);
|
|
3616
|
+
const url = board.origin?.pullRequestUrl;
|
|
3617
|
+
const state = data?.pullRequestState(url);
|
|
3618
|
+
const baseBranch = url && data?.pullRequestBaseBranch(url);
|
|
3619
|
+
const headBranch = url && data?.pullRequestHeadBranch(url);
|
|
3620
|
+
const place = ({ reviewId, title, target }) => ({
|
|
3621
|
+
reviewId,
|
|
3622
|
+
title,
|
|
3623
|
+
icon: boardIconKind(target)
|
|
3624
|
+
});
|
|
3625
|
+
const listed = (child) => {
|
|
3626
|
+
const childState = data?.pullRequestState(child.origin?.pullRequestUrl);
|
|
3627
|
+
return {
|
|
3628
|
+
...place(child),
|
|
3629
|
+
...child.origin?.pullRequestNumber && { pullRequestNumber: child.origin.pullRequestNumber },
|
|
3630
|
+
...childState && { pullRequestState: childState }
|
|
3631
|
+
};
|
|
3632
|
+
};
|
|
3633
|
+
return {
|
|
3634
|
+
...state && { pullRequestState: state },
|
|
3635
|
+
...baseBranch && { baseBranch },
|
|
3636
|
+
...headBranch && { headBranch },
|
|
3637
|
+
board: place(board),
|
|
3638
|
+
...parent && { parent: place(parent) },
|
|
3639
|
+
siblings: siblings.map(listed),
|
|
3640
|
+
...children?.length && { children: children.map(listed) }
|
|
3641
|
+
};
|
|
3642
|
+
};
|
|
3643
|
+
app.get("/", (context) => context.json(catalog()));
|
|
3569
3644
|
app.get("/feedback-eligibility", (context) => context.json({ eligible: store.feedbackEligible() }));
|
|
3570
|
-
const instructionContext = async () => ({
|
|
3571
|
-
desktopAvailable: Boolean(open),
|
|
3572
|
-
scratchpadEnabled: scratchpadEnabled(),
|
|
3573
|
-
traceEnabled: await traceEnabled()
|
|
3574
|
-
});
|
|
3645
|
+
const instructionContext = async () => ({ traceEnabled: await traceEnabled() });
|
|
3575
3646
|
app.get("/authoring", async (context) => {
|
|
3576
3647
|
const instructions = await instructionContext();
|
|
3577
|
-
return context.json(authoringTools(
|
|
3648
|
+
return context.json(authoringTools(instructions.traceEnabled));
|
|
3578
3649
|
});
|
|
3579
3650
|
app.get("/instructions", async (context) => {
|
|
3580
3651
|
const { topic } = instructionsQuerySchema.parse(context.req.query());
|
|
@@ -3628,10 +3699,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3628
3699
|
...status(),
|
|
3629
3700
|
desktopAvailable: (await capabilities()).desktopAvailable
|
|
3630
3701
|
}));
|
|
3631
|
-
app.get("/capabilities", async (context) => context.json(
|
|
3632
|
-
...await capabilities(),
|
|
3633
|
-
scratchpadEnabled: scratchpadEnabled()
|
|
3634
|
-
}));
|
|
3702
|
+
app.get("/capabilities", async (context) => context.json(await capabilities()));
|
|
3635
3703
|
app.get("/:id/activity", (context) => {
|
|
3636
3704
|
const id = context.req.param("id");
|
|
3637
3705
|
readReview(id);
|
|
@@ -3653,6 +3721,9 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3653
3721
|
action
|
|
3654
3722
|
} : input));
|
|
3655
3723
|
});
|
|
3724
|
+
app.post("/:id/person-edit", async (context) => context.json(await store.personEdit(context.req.param("id"), await readBoundedRequestJson(context.req.raw, PERSON_EDIT_MAX_BYTES))));
|
|
3725
|
+
app.post("/:id/focus", async (context) => context.json(store.focus(context.req.param("id"), await readBoundedRequestJson(context.req.raw))));
|
|
3726
|
+
app.post("/:id/discard", async (context) => context.json(await store.discard(context.req.param("id"))));
|
|
3656
3727
|
app.get("/watch", (context) => {
|
|
3657
3728
|
let input;
|
|
3658
3729
|
try {
|
|
@@ -3669,6 +3740,16 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3669
3740
|
let dirtyList = lists;
|
|
3670
3741
|
const dirtyReviews = new Set(reviewIds);
|
|
3671
3742
|
let sentList;
|
|
3743
|
+
const linksRevision = /* @__PURE__ */ new Map();
|
|
3744
|
+
const linksKey = (id) => {
|
|
3745
|
+
if (!data || isShared(id) || !store.has(id)) return "";
|
|
3746
|
+
const { outgoing, backlinks } = store.links(id);
|
|
3747
|
+
return JSON.stringify([...outgoing, ...backlinks].map((link) => link.pullRequestUrl && data.pullRequestState(link.pullRequestUrl, { seen: false })));
|
|
3748
|
+
};
|
|
3749
|
+
const linksKeys = new Map([...reviewIds].map((reviewId) => [reviewId, linksKey(reviewId)]));
|
|
3750
|
+
const placeRevision = /* @__PURE__ */ new Map();
|
|
3751
|
+
const placeKey = (id) => JSON.stringify(placeOf(id));
|
|
3752
|
+
const placeKeys = new Map([...reviewIds].map((reviewId) => [reviewId, placeKey(reviewId)]));
|
|
3672
3753
|
const readReviewLine = (reviewId) => {
|
|
3673
3754
|
try {
|
|
3674
3755
|
return {
|
|
@@ -3677,7 +3758,9 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3677
3758
|
value: {
|
|
3678
3759
|
...readReview(reviewId),
|
|
3679
3760
|
activity: store.activity.read(reviewId),
|
|
3680
|
-
coverageRevision: data?.coverageRevision ?? 0
|
|
3761
|
+
coverageRevision: data?.coverageRevision ?? 0,
|
|
3762
|
+
linksRevision: linksRevision.get(reviewId) ?? 0,
|
|
3763
|
+
placeRevision: placeRevision.get(reviewId) ?? 0
|
|
3681
3764
|
}
|
|
3682
3765
|
};
|
|
3683
3766
|
} catch (error) {
|
|
@@ -3709,6 +3792,18 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3709
3792
|
dirtyReviews.add(reviewId);
|
|
3710
3793
|
return true;
|
|
3711
3794
|
};
|
|
3795
|
+
/** Bumps the place of each board whose place changed. */
|
|
3796
|
+
const refreshPlaces = () => {
|
|
3797
|
+
let marked = false;
|
|
3798
|
+
for (const [id, key] of placeKeys) {
|
|
3799
|
+
const next = placeKey(id);
|
|
3800
|
+
if (next === key) continue;
|
|
3801
|
+
placeKeys.set(id, next);
|
|
3802
|
+
placeRevision.set(id, (placeRevision.get(id) ?? 0) + 1);
|
|
3803
|
+
marked = markReview(id) || marked;
|
|
3804
|
+
}
|
|
3805
|
+
return marked;
|
|
3806
|
+
};
|
|
3712
3807
|
const markLists = () => {
|
|
3713
3808
|
dirtyList ||= lists;
|
|
3714
3809
|
return lists;
|
|
@@ -3721,7 +3816,12 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3721
3816
|
notify();
|
|
3722
3817
|
}) ?? (() => {}),
|
|
3723
3818
|
store.subscribe((result) => {
|
|
3724
|
-
|
|
3819
|
+
let marked = markReview(result.reviewId);
|
|
3820
|
+
for (const from of store.linkingBoards(result.reviewId)) {
|
|
3821
|
+
linksRevision.set(from, (linksRevision.get(from) ?? 0) + 1);
|
|
3822
|
+
marked = markReview(from) || marked;
|
|
3823
|
+
}
|
|
3824
|
+
if (marked) notify();
|
|
3725
3825
|
}),
|
|
3726
3826
|
store.activity.subscribe((id) => {
|
|
3727
3827
|
if (markReview(id)) notify();
|
|
@@ -3729,11 +3829,24 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3729
3829
|
store.activity.subscribeWorking(() => {
|
|
3730
3830
|
if (markLists()) notify();
|
|
3731
3831
|
}),
|
|
3832
|
+
data?.subscribePullRequestStates(() => {
|
|
3833
|
+
let marked = markLists();
|
|
3834
|
+
marked = refreshPlaces() || marked;
|
|
3835
|
+
for (const reviewId of reviewIds) {
|
|
3836
|
+
const next = linksKey(reviewId);
|
|
3837
|
+
if (next === linksKeys.get(reviewId)) continue;
|
|
3838
|
+
linksKeys.set(reviewId, next);
|
|
3839
|
+
linksRevision.set(reviewId, (linksRevision.get(reviewId) ?? 0) + 1);
|
|
3840
|
+
marked = markReview(reviewId) || marked;
|
|
3841
|
+
}
|
|
3842
|
+
if (marked) notify();
|
|
3843
|
+
}) ?? (() => {}),
|
|
3732
3844
|
shared?.subscribe(() => {
|
|
3733
3845
|
if (markLists()) notify();
|
|
3734
3846
|
}) ?? (() => {}),
|
|
3735
3847
|
store.subscribeCatalog(() => {
|
|
3736
|
-
|
|
3848
|
+
const marked = refreshPlaces();
|
|
3849
|
+
if (markLists() || marked) notify();
|
|
3737
3850
|
})
|
|
3738
3851
|
];
|
|
3739
3852
|
return () => stops.forEach((stop) => stop());
|
|
@@ -3789,19 +3902,18 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3789
3902
|
storage: z.enum(["s3", "hosted"]).optional(),
|
|
3790
3903
|
trace: z.string().min(1).optional()
|
|
3791
3904
|
});
|
|
3792
|
-
const tracePins = (id, version) => {
|
|
3793
|
-
const
|
|
3794
|
-
|
|
3795
|
-
return pins;
|
|
3905
|
+
const tracePins = async (id, version) => {
|
|
3906
|
+
const snapshot = readReview(id, version);
|
|
3907
|
+
return snapshot.pins ?? await data.documentPins(snapshot);
|
|
3796
3908
|
};
|
|
3797
3909
|
app.get("/:id/agent-traces", async (context) => {
|
|
3798
3910
|
const query = traceQuery.parse(context.req.query());
|
|
3799
|
-
const pins = tracePins(context.req.param("id"), query.version);
|
|
3911
|
+
const pins = await tracePins(context.req.param("id"), query.version);
|
|
3800
3912
|
return context.json(await listPinnedTraces(store.repositoryPath(pins.repositoryId), pins, query.storage));
|
|
3801
3913
|
});
|
|
3802
3914
|
app.get("/:id/agent-traces/:sessionId", async (context) => {
|
|
3803
3915
|
const query = traceQuery.parse(context.req.query());
|
|
3804
|
-
const pins = tracePins(context.req.param("id"), query.version);
|
|
3916
|
+
const pins = await tracePins(context.req.param("id"), query.version);
|
|
3805
3917
|
const result = await readStoredTrace(store.repositoryPath(pins.repositoryId), context.req.param("sessionId"), query.trace, query.storage);
|
|
3806
3918
|
if (!result.ok) return context.json({
|
|
3807
3919
|
ok: false,
|
|
@@ -3844,7 +3956,16 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3844
3956
|
const input = z.strictObject({ path: z.string().min(1) }).parse(await readBoundedRequestJson(context.req.raw));
|
|
3845
3957
|
return context.json(await data.register(input.path));
|
|
3846
3958
|
});
|
|
3847
|
-
app.post("/resources", async (context) =>
|
|
3959
|
+
app.post("/resources", async (context) => {
|
|
3960
|
+
const body = await readBoundedRequestJson(context.req.raw, 8388608);
|
|
3961
|
+
const named = z.looseObject({ repositoryPath: z.string().min(1) }).safeParse(body);
|
|
3962
|
+
if (!named.success) return context.json(await data.upload(body));
|
|
3963
|
+
const { repositoryPath, ...upload } = named.data;
|
|
3964
|
+
return context.json(await data.upload({
|
|
3965
|
+
...upload,
|
|
3966
|
+
repositoryId: (await data.register(repositoryPath)).id
|
|
3967
|
+
}));
|
|
3968
|
+
});
|
|
3848
3969
|
app.get("/:id/resources/:resourceId", async (context) => {
|
|
3849
3970
|
const id = context.req.param("id");
|
|
3850
3971
|
const snapshot = readReview(id);
|
|
@@ -3951,6 +4072,11 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3951
4072
|
const { pins } = await data.resolveSource(readReview(context.req.param("id"), input.version), input.commit, queryAnchor(input));
|
|
3952
4073
|
return context.json(await data.changes(pins));
|
|
3953
4074
|
});
|
|
4075
|
+
app.get("/:id/commit/:commit", async (context) => {
|
|
4076
|
+
const repositoryId = boardRepositoryId(readReview(context.req.param("id")));
|
|
4077
|
+
if (!repositoryId) throw new ReviewInputError("This board has no repository.", 404);
|
|
4078
|
+
return context.json(await data.commitInfo(repositoryId, context.req.param("commit")));
|
|
4079
|
+
});
|
|
3954
4080
|
app.get("/:id/commits", async (context) => {
|
|
3955
4081
|
const input = readQuerySchemas.commits.parse(context.req.query());
|
|
3956
4082
|
return context.json(await data.commits((await data.resolveSource(readReview(context.req.param("id"), input.version))).pins));
|
|
@@ -3983,20 +4109,12 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3983
4109
|
head: `b/${diff.newPath}`
|
|
3984
4110
|
} : void 0);
|
|
3985
4111
|
return [
|
|
3986
|
-
`Whiteboard: ${snapshot.title}`,
|
|
4112
|
+
`Whiteboard: ${boardTitle(snapshot.title)}`,
|
|
3987
4113
|
`Session ID: ${snapshot.reviewId}`,
|
|
3988
4114
|
`Version: ${snapshot.version}`,
|
|
3989
4115
|
...selection.apiSource?.commit ? [`Selected commit: ${selection.apiSource.commit}`] : [],
|
|
3990
|
-
...selection.apiSource?.pins ? [
|
|
3991
|
-
|
|
3992
|
-
...selection.apiSource.pins.base ? [`Selected base: ${selection.apiSource.pins.base}`] : [],
|
|
3993
|
-
`Selected head: ${selection.apiSource.pins.head}`
|
|
3994
|
-
] : [],
|
|
3995
|
-
...snapshot.pins ? [
|
|
3996
|
-
`Repository ID: ${snapshot.pins.repositoryId}`,
|
|
3997
|
-
`Session base: ${snapshot.pins.base}`,
|
|
3998
|
-
`Session head: ${snapshot.pins.head}`
|
|
3999
|
-
] : [],
|
|
4116
|
+
...selection.apiSource?.pins ? [...selection.apiSource.pins.base ? [`Selected base: ${selection.apiSource.pins.base}`] : [], `Selected head: ${selection.apiSource.pins.head}`] : [],
|
|
4117
|
+
...snapshot.pins ? [`Session base: ${snapshot.pins.base}`, `Session head: ${snapshot.pins.head}`] : [],
|
|
4000
4118
|
`Read this version with session_get({"sessionId":"${snapshot.reviewId}","version":${snapshot.version},"full":true}).`,
|
|
4001
4119
|
"",
|
|
4002
4120
|
text,
|
|
@@ -4005,9 +4123,10 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
4005
4123
|
].join("\n");
|
|
4006
4124
|
}
|
|
4007
4125
|
if (ask && data) {
|
|
4126
|
+
const projectOf = (reviewId) => isShared(reviewId) || !store.has(reviewId) ? void 0 : store.projectOf(reviewId);
|
|
4008
4127
|
const readThread = (reviewId, threadId) => {
|
|
4009
4128
|
const thread = ask.threads.get(threadId);
|
|
4010
|
-
if (!thread
|
|
4129
|
+
if (!thread) throw new ReviewInputError("This conversation has ended.", 404);
|
|
4011
4130
|
return thread;
|
|
4012
4131
|
};
|
|
4013
4132
|
/** Where a thread in a review saves itself: its Ask history, recorded
|
|
@@ -4057,7 +4176,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
4057
4176
|
const reach = await ask.threads.reach(agent, threadId);
|
|
4058
4177
|
const { reviewId } = snapshot;
|
|
4059
4178
|
return [
|
|
4060
|
-
`A reviewer is reading "${snapshot.title}" in Whiteboard and has a question about ${selection.target.kind === "review" ? "it" : "a selection"}.`,
|
|
4179
|
+
`A reviewer is reading "${boardTitle(snapshot.title)}" in Whiteboard and has a question about ${selection.target.kind === "review" ? "it" : "a selection"}.`,
|
|
4061
4180
|
checkout.live ? "Your working directory is the repository the review describes." : `Your working directory is a checkout of the review's head commit, ${checkout.head}. Answer from this code, not from other branches.`,
|
|
4062
4181
|
"Answer the question for a staff engineer: lead with the answer and keep it short. Explain at the level of components and data flow before functions, and check each claim about the code against code you have read.",
|
|
4063
4182
|
"Name files by their path from the checkout root, with a line where it helps, as in `src/app.ts:42`; the reviewer can open them from your answer.",
|
|
@@ -4108,13 +4227,17 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
4108
4227
|
const permissions = input.permissions ?? defaultAskPermissions(input.agent);
|
|
4109
4228
|
const initialContext = await askContext(id, input.agent, snapshot, checkout, input.selection, version);
|
|
4110
4229
|
assertWritable(reviewId);
|
|
4230
|
+
store.askHistory.markViewed(id, createdAt);
|
|
4111
4231
|
const thread = ask.threads.open({
|
|
4112
4232
|
id,
|
|
4113
4233
|
reviewId,
|
|
4114
4234
|
agent: input.agent,
|
|
4115
4235
|
...historyHooks({
|
|
4116
4236
|
id,
|
|
4117
|
-
|
|
4237
|
+
anchor: target.kind === "text" && target.anchor ? {
|
|
4238
|
+
board: reviewId,
|
|
4239
|
+
block: target.anchor.blockId
|
|
4240
|
+
} : { board: reviewId },
|
|
4118
4241
|
agent: input.agent,
|
|
4119
4242
|
version: snapshot.version,
|
|
4120
4243
|
head: checkout.head,
|
|
@@ -4138,23 +4261,95 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
4138
4261
|
});
|
|
4139
4262
|
return context.json({ threadId: thread.id });
|
|
4140
4263
|
});
|
|
4264
|
+
/** Every project's threads for the sidebar: who runs them, where they
|
|
4265
|
+
* started and live, and which boards they changed. */
|
|
4266
|
+
const projectThreads = (reviewId) => {
|
|
4267
|
+
const entries = store.askHistory.listAll();
|
|
4268
|
+
const ids = entries.map((entry) => entry.id);
|
|
4269
|
+
const edited = store.boardsChangedByThreads(ids);
|
|
4270
|
+
const viewed = store.askHistory.viewedAt(ids);
|
|
4271
|
+
const boards = /* @__PURE__ */ new Map();
|
|
4272
|
+
const board = (id) => {
|
|
4273
|
+
let found = boards.get(id);
|
|
4274
|
+
if (!found) {
|
|
4275
|
+
found = store.has(id) ? store.boardFace(id) : {
|
|
4276
|
+
reviewId: id,
|
|
4277
|
+
icon: boardIconKind(void 0)
|
|
4278
|
+
};
|
|
4279
|
+
boards.set(id, found);
|
|
4280
|
+
}
|
|
4281
|
+
return found;
|
|
4282
|
+
};
|
|
4283
|
+
const project = projectOf(reviewId);
|
|
4284
|
+
return {
|
|
4285
|
+
board: reviewId,
|
|
4286
|
+
...project && { project },
|
|
4287
|
+
threads: entries.map((entry) => {
|
|
4288
|
+
const anchor = entry.anchor ?? { board: reviewId };
|
|
4289
|
+
const status = ask.threads.get(entry.id)?.status;
|
|
4290
|
+
const viewedAt = viewed.get(entry.id);
|
|
4291
|
+
const unread = status !== "starting" && status !== "running" && status !== "waiting" && viewedAt !== void 0 && entry.updatedAt > viewedAt;
|
|
4292
|
+
return {
|
|
4293
|
+
id: entry.id,
|
|
4294
|
+
...entry.projectId && { projectId: entry.projectId },
|
|
4295
|
+
...store.has(anchor.board) && { place: store.boardPlace(anchor.board) },
|
|
4296
|
+
agent: entry.agent,
|
|
4297
|
+
agentName: askAgents[entry.agent].name,
|
|
4298
|
+
opening: entry.question ?? entry.title,
|
|
4299
|
+
pinned: entry.selection.target.kind === "text" && entry.selection.target.anchor !== void 0,
|
|
4300
|
+
...status && { status },
|
|
4301
|
+
...unread && { unread },
|
|
4302
|
+
updatedAt: entry.updatedAt,
|
|
4303
|
+
anchor: {
|
|
4304
|
+
board: board(anchor.board),
|
|
4305
|
+
...anchor.block && { block: anchor.block }
|
|
4306
|
+
},
|
|
4307
|
+
edited: edited.get(entry.id) ?? []
|
|
4308
|
+
};
|
|
4309
|
+
})
|
|
4310
|
+
};
|
|
4311
|
+
};
|
|
4141
4312
|
app.get("/:id/ask/threads", (context) => {
|
|
4142
4313
|
const reviewId = context.req.param("id");
|
|
4143
4314
|
readReview(reviewId);
|
|
4144
|
-
|
|
4315
|
+
if (context.req.query("watch") !== void 0) {
|
|
4316
|
+
let sent = "";
|
|
4317
|
+
return watch(() => {
|
|
4318
|
+
const line = JSON.stringify(projectThreads(reviewId));
|
|
4319
|
+
if (line === sent) return [];
|
|
4320
|
+
sent = line;
|
|
4321
|
+
return [line];
|
|
4322
|
+
}, (notify) => {
|
|
4323
|
+
const stops = [
|
|
4324
|
+
store.askHistory.subscribe(notify),
|
|
4325
|
+
ask.threads.subscribeStatus(notify),
|
|
4326
|
+
store.subscribe(() => notify())
|
|
4327
|
+
];
|
|
4328
|
+
return () => stops.forEach((stop) => stop());
|
|
4329
|
+
});
|
|
4330
|
+
}
|
|
4331
|
+
return context.json({
|
|
4332
|
+
threads: store.askHistory.list(reviewId, projectOf(reviewId)),
|
|
4333
|
+
board: reviewId
|
|
4334
|
+
});
|
|
4145
4335
|
});
|
|
4146
4336
|
app.post("/:id/ask/watch", reviewAccess("read"), async (context) => {
|
|
4147
4337
|
const reviewId = context.req.param("id");
|
|
4148
4338
|
readReview(reviewId);
|
|
4149
4339
|
const { threads } = askWatchSchema.parse(await readBoundedRequestJson(context.req.raw));
|
|
4150
4340
|
return watchAskThreads(new Map(threads.map((threadId) => {
|
|
4151
|
-
|
|
4152
|
-
return [threadId, thread?.reviewId === reviewId ? thread : void 0];
|
|
4341
|
+
return [threadId, ask.threads.get(threadId)];
|
|
4153
4342
|
})));
|
|
4154
4343
|
});
|
|
4344
|
+
app.get("/:id/ask/:threadId/boards", (context) => {
|
|
4345
|
+
const threadId = context.req.param("threadId");
|
|
4346
|
+
const anchor = store.askHistory.get(threadId)?.anchor.board ?? ask.threads.get(threadId)?.reviewId;
|
|
4347
|
+
if (remoteCaller(context) || !anchor) throw new ReviewInputError("This conversation was not found.", 404);
|
|
4348
|
+
return context.json({ boards: store.boardsChangedBy(threadId) });
|
|
4349
|
+
});
|
|
4155
4350
|
app.get("/:id/ask/:threadId", (context) => {
|
|
4156
4351
|
const record = store.askHistory.get(context.req.param("threadId"));
|
|
4157
|
-
if (record
|
|
4352
|
+
if (!record) throw new ReviewInputError("This conversation was not found.", 404);
|
|
4158
4353
|
const target = record.selection.target;
|
|
4159
4354
|
return context.json({
|
|
4160
4355
|
id: record.id,
|
|
@@ -4176,18 +4371,19 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
4176
4371
|
app.post("/:id/ask/:threadId/open", reviewAccess("edit"), async (context) => {
|
|
4177
4372
|
const reviewId = context.req.param("id");
|
|
4178
4373
|
const threadId = context.req.param("threadId");
|
|
4179
|
-
if (ask.threads.get(threadId)
|
|
4374
|
+
if (ask.threads.get(threadId)) return context.json({ threadId });
|
|
4180
4375
|
const record = store.askHistory.get(threadId);
|
|
4181
4376
|
const { picks } = askOpenSchema.parse(await readBoundedRequestJson(context.req.raw, void 0, {}));
|
|
4182
|
-
if (record
|
|
4183
|
-
const
|
|
4377
|
+
if (!record) throw new ReviewInputError("This conversation was not found.", 404);
|
|
4378
|
+
const anchor = record.anchor.board;
|
|
4379
|
+
const snapshot = readReview(anchor, record.version);
|
|
4184
4380
|
const checkout = await data.agentCheckout(snapshot);
|
|
4185
4381
|
const target = record.selection.target;
|
|
4186
4382
|
const initialContext = await askContext(record.id, record.agent, snapshot, checkout, record.selection, record.version);
|
|
4187
4383
|
assertWritable(reviewId);
|
|
4188
4384
|
ask.threads.open({
|
|
4189
4385
|
id: record.id,
|
|
4190
|
-
reviewId,
|
|
4386
|
+
reviewId: anchor,
|
|
4191
4387
|
agent: record.agent,
|
|
4192
4388
|
cwd: checkout.rootPath,
|
|
4193
4389
|
head: checkout.head,
|
|
@@ -4263,27 +4459,91 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
4263
4459
|
await readThread(context.req.param("id"), context.req.param("threadId")).cancel();
|
|
4264
4460
|
return context.json({ ok: true });
|
|
4265
4461
|
});
|
|
4462
|
+
app.post("/:id/ask/:threadId/viewed", (context) => {
|
|
4463
|
+
const threadId = context.req.param("threadId");
|
|
4464
|
+
if (!ask.threads.get(threadId) && !store.askHistory.get(threadId)) throw new ReviewInputError("This conversation was not found.", 404);
|
|
4465
|
+
store.askHistory.markViewed(threadId);
|
|
4466
|
+
return context.json({ ok: true });
|
|
4467
|
+
});
|
|
4266
4468
|
app.post("/:id/ask/:threadId/close", reviewAccess("read"), (context) => {
|
|
4267
4469
|
readThread(context.req.param("id"), context.req.param("threadId"));
|
|
4268
4470
|
ask.threads.close(context.req.param("threadId"), !readReview(context.req.param("id")).trashedAt);
|
|
4269
4471
|
return context.json({ ok: true });
|
|
4270
4472
|
});
|
|
4271
4473
|
app.delete("/:id/ask/:threadId", reviewAccess("edit"), (context) => {
|
|
4272
|
-
|
|
4474
|
+
context.req.param("id");
|
|
4273
4475
|
const threadId = context.req.param("threadId");
|
|
4274
|
-
if (store.askHistory.get(threadId)
|
|
4275
|
-
if (ask.threads.get(threadId)
|
|
4476
|
+
if (!store.askHistory.get(threadId)) throw new ReviewInputError("This conversation was not found.", 404);
|
|
4477
|
+
if (ask.threads.get(threadId)) ask.threads.close(threadId);
|
|
4276
4478
|
store.askHistory.delete(threadId);
|
|
4277
4479
|
return context.json({ ok: true });
|
|
4278
4480
|
});
|
|
4279
4481
|
}
|
|
4482
|
+
app.get("/:id/place", (context) => {
|
|
4483
|
+
const id = context.req.param("id");
|
|
4484
|
+
if (!isShared(id)) store.assertExists(id);
|
|
4485
|
+
return context.json(placeOf(id));
|
|
4486
|
+
});
|
|
4280
4487
|
app.get("/:id/stack", async (context) => {
|
|
4281
4488
|
const query = readQuerySchemas.get.parse(context.req.query());
|
|
4282
4489
|
const id = context.req.param("id");
|
|
4283
4490
|
const snapshot = readReview(id, query.version);
|
|
4284
4491
|
if (isShared(id) || !snapshot.pins) return context.json({ layers: [] });
|
|
4285
|
-
const
|
|
4286
|
-
|
|
4492
|
+
const repositoryPath = store.repositories().find((repository) => repository.id === snapshot.pins.repositoryId)?.path;
|
|
4493
|
+
const layers = await resolveReviewStackLayers(snapshot, store.list(), { repositoryPath });
|
|
4494
|
+
const trunk = layers.length > 1 && repositoryPath ? await defaultBranch(repositoryPath).catch(() => null) : null;
|
|
4495
|
+
return context.json({
|
|
4496
|
+
layers: layers.map((layer) => {
|
|
4497
|
+
const state = data?.pullRequestState(layer.pullRequestUrl ?? void 0);
|
|
4498
|
+
return state ? {
|
|
4499
|
+
...layer,
|
|
4500
|
+
pullRequestState: state
|
|
4501
|
+
} : layer;
|
|
4502
|
+
}),
|
|
4503
|
+
...trunk && { base: trunk.ref.replace(/^origin\//, "") }
|
|
4504
|
+
});
|
|
4505
|
+
});
|
|
4506
|
+
app.get("/:id/project", (context) => {
|
|
4507
|
+
const id = context.req.param("id");
|
|
4508
|
+
if (isShared(id)) return context.json({
|
|
4509
|
+
project: null,
|
|
4510
|
+
boards: []
|
|
4511
|
+
});
|
|
4512
|
+
const project = store.summary(id)?.project ?? null;
|
|
4513
|
+
return context.json({
|
|
4514
|
+
project,
|
|
4515
|
+
boards: project ? store.list().filter((board) => board.project?.id === project.id && !board.trashedAt).map(withPullRequestState) : []
|
|
4516
|
+
});
|
|
4517
|
+
});
|
|
4518
|
+
app.get("/:id/links", (context) => {
|
|
4519
|
+
const id = context.req.param("id");
|
|
4520
|
+
if (isShared(id)) return context.json({
|
|
4521
|
+
outgoing: boardLinks(id, readReview(id).document).map(({ from, to }) => ({
|
|
4522
|
+
from,
|
|
4523
|
+
to,
|
|
4524
|
+
missing: true,
|
|
4525
|
+
elsewhere: true
|
|
4526
|
+
})),
|
|
4527
|
+
backlinks: []
|
|
4528
|
+
});
|
|
4529
|
+
const { version } = readQuerySchemas.get.pick({ version: true }).parse({ version: context.req.query("version") });
|
|
4530
|
+
const { outgoing, backlinks } = store.links(id, version);
|
|
4531
|
+
return context.json({
|
|
4532
|
+
outgoing: outgoing.map(withPullRequestState),
|
|
4533
|
+
backlinks: backlinks.map(withPullRequestState)
|
|
4534
|
+
});
|
|
4535
|
+
});
|
|
4536
|
+
app.get("/:id/links/peek", (context) => {
|
|
4537
|
+
const id = context.req.param("id");
|
|
4538
|
+
if (isShared(id)) return context.json({});
|
|
4539
|
+
const { version } = readQuerySchemas.get.pick({ version: true }).parse({ version: context.req.query("version") });
|
|
4540
|
+
const board = context.req.query("board");
|
|
4541
|
+
const block = context.req.query("block");
|
|
4542
|
+
if (!board) throw new ReviewInputError("Name the linked board.", 400);
|
|
4543
|
+
return context.json(store.linkPeek(id, {
|
|
4544
|
+
board,
|
|
4545
|
+
...block && { block }
|
|
4546
|
+
}, version));
|
|
4287
4547
|
});
|
|
4288
4548
|
app.get("/:id/history", (context) => {
|
|
4289
4549
|
const id = context.req.param("id");
|
|
@@ -4292,14 +4552,39 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
4292
4552
|
return context.json([{
|
|
4293
4553
|
version: snapshot.version,
|
|
4294
4554
|
title: snapshot.title,
|
|
4295
|
-
createdAt: snapshot.createdAt
|
|
4555
|
+
createdAt: snapshot.createdAt,
|
|
4556
|
+
author: "agent"
|
|
4296
4557
|
}]);
|
|
4297
4558
|
});
|
|
4559
|
+
/** A board's relations as session_list reports them: only those it has. */
|
|
4560
|
+
const listedRelations = ({ parentId, children, dependsOn }) => {
|
|
4561
|
+
const listed = {};
|
|
4562
|
+
if (parentId) listed.parentId = parentId;
|
|
4563
|
+
if (children.length) listed.children = children;
|
|
4564
|
+
if (dependsOn.length) listed.dependsOn = dependsOn;
|
|
4565
|
+
return listed;
|
|
4566
|
+
};
|
|
4567
|
+
/** A board's relations, each board named by its title. */
|
|
4568
|
+
const relatedBoards = (id) => {
|
|
4569
|
+
const { parentId, children, dependsOn } = store.relations(id);
|
|
4570
|
+
const named = (board) => ({
|
|
4571
|
+
id: board,
|
|
4572
|
+
title: store.read(board).title
|
|
4573
|
+
});
|
|
4574
|
+
return {
|
|
4575
|
+
...parentId && { parent: named(parentId) },
|
|
4576
|
+
children: children.map(named),
|
|
4577
|
+
dependsOn: dependsOn.map(named)
|
|
4578
|
+
};
|
|
4579
|
+
};
|
|
4298
4580
|
app.get("/:id/inspect", (context) => {
|
|
4299
4581
|
const query = inspectQuerySchema.parse(context.req.query());
|
|
4300
4582
|
const id = context.req.param("id");
|
|
4301
4583
|
const snapshot = readReview(id, query.version);
|
|
4302
|
-
return context.json(query.format === "text" ? documentText(snapshot, query.targetId, Boolean(query.full)) : query.targetId !== void 0 ? inspectSnapshot(snapshot, query.targetId) :
|
|
4584
|
+
return context.json(query.format === "text" ? documentText(snapshot, query.targetId, Boolean(query.full), isShared(id) ? void 0 : relatedBoards(id)) : query.targetId !== void 0 ? inspectSnapshot(snapshot, query.targetId) : {
|
|
4585
|
+
...query.full ? snapshot : inspectSnapshot(snapshot),
|
|
4586
|
+
...!isShared(id) && listedRelations(store.relations(id))
|
|
4587
|
+
});
|
|
4303
4588
|
});
|
|
4304
4589
|
app.get("/:id", async (context) => {
|
|
4305
4590
|
const query = readQuerySchemas.get.parse(context.req.query());
|
|
@@ -4326,17 +4611,13 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
4326
4611
|
};
|
|
4327
4612
|
app.post("/commands", async (context) => {
|
|
4328
4613
|
const { command: body, open: requestedOpen } = takeCreateOpen(await readBoundedRequestJson(context.req.raw));
|
|
4329
|
-
const
|
|
4614
|
+
const register = async (path) => data ? data.register(path) : store.registerRepository(path);
|
|
4615
|
+
const request = withBoardRepository(await locateRepositories(await pullRequestTargets(body, register, (url, repositoryId) => {
|
|
4330
4616
|
if (!data) throw new ReviewInputError("Repositories are unavailable.");
|
|
4331
|
-
return data.
|
|
4332
|
-
});
|
|
4617
|
+
return data.resolvePullRequest(url, { id: repositoryId });
|
|
4618
|
+
}, (id) => store.read(id)), register), (id) => store.has(id) ? store.read(id) : void 0);
|
|
4333
4619
|
const input = commandSchema.parse(request);
|
|
4334
4620
|
const command = sharedCommandSchema.safeParse(input);
|
|
4335
|
-
if (command.success) {
|
|
4336
|
-
refuseDisabledScratchpad(command.data.operation.reviewId);
|
|
4337
|
-
await ensureScratchpad(command.data.operation.reviewId);
|
|
4338
|
-
}
|
|
4339
|
-
if (input.operation.type === "create" && input.operation.kind === "scratchpad") refuseDisabledScratchpad(SCRATCHPAD_ID);
|
|
4340
4621
|
if (command.success && command.data.operation.reviewId?.startsWith("shared-")) {
|
|
4341
4622
|
const parsed = commandSchema.parse(input);
|
|
4342
4623
|
if (shared && parsed.operation.type === "delete") {
|
|
@@ -4361,7 +4642,12 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
4361
4642
|
}
|
|
4362
4643
|
throw new ReviewInputError("Shared reviews are read-only.", 409);
|
|
4363
4644
|
}
|
|
4364
|
-
const
|
|
4645
|
+
const threadId = remoteCaller(context) ? void 0 : context.req.header("x-review-thread")?.trim() || void 0;
|
|
4646
|
+
const author = !remoteCaller(context) && context.req.header("x-review-person") !== void 0 ? { kind: "person" } : {
|
|
4647
|
+
kind: "agent",
|
|
4648
|
+
...threadId && { threadId }
|
|
4649
|
+
};
|
|
4650
|
+
const result = await store.execute(input, void 0, author);
|
|
4365
4651
|
if (result.deleted || result.lifecycle && store.read(result.reviewId).trashedAt) ask?.threads.closeDeletedReviews((id) => id === result.reviewId);
|
|
4366
4652
|
if (input.operation.type === "edit" && data && !result.deleted) {
|
|
4367
4653
|
const { quotes, unquoted } = await anchorQuotes(result.version > 0 ? store.read(result.reviewId, result.version - 1) : void 0, store.read(result.reviewId, result.version), async (pins, side, file) => (await data.file(pins, side, file)).text);
|
|
@@ -4382,7 +4668,6 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
4382
4668
|
if (input.operation.type !== "create") return context.json(result);
|
|
4383
4669
|
if (result.created !== false) hooks.onReviewCreated?.({
|
|
4384
4670
|
reviewId: result.reviewId,
|
|
4385
|
-
kind: input.operation.kind === "scratchpad" ? "scratchpad" : "review",
|
|
4386
4671
|
blocks: store.read(result.reviewId).document.length,
|
|
4387
4672
|
...reviewRequestOrigin(context.req.raw.headers)
|
|
4388
4673
|
});
|
|
@@ -4446,6 +4731,95 @@ async function locateRepositories(body, register) {
|
|
|
4446
4731
|
}
|
|
4447
4732
|
};
|
|
4448
4733
|
}
|
|
4734
|
+
/** Turns an agent's pullRequest target into what the store takes: a create
|
|
4735
|
+
* from pullRequestUrl, or for set_target the PR's resolved commits. */
|
|
4736
|
+
async function pullRequestTargets(body, register, resolve, board) {
|
|
4737
|
+
const target = z.strictObject({
|
|
4738
|
+
kind: z.literal("pullRequest"),
|
|
4739
|
+
repositoryPath: z.string().min(1),
|
|
4740
|
+
pullRequest: z.union([z.string().min(1), z.number().int().positive()])
|
|
4741
|
+
});
|
|
4742
|
+
const located = z.looseObject({ operation: z.discriminatedUnion("type", [z.looseObject({
|
|
4743
|
+
type: z.literal("create"),
|
|
4744
|
+
target
|
|
4745
|
+
}), z.strictObject({
|
|
4746
|
+
type: z.literal("set_target"),
|
|
4747
|
+
reviewId: z.string().min(1),
|
|
4748
|
+
target
|
|
4749
|
+
})]) }).safeParse(body);
|
|
4750
|
+
if (!located.success) return body;
|
|
4751
|
+
const { target: pullRequest, ...operation } = located.data.operation;
|
|
4752
|
+
if (operation.type === "set_target") board(operation.reviewId);
|
|
4753
|
+
const { id: repositoryId } = await register(pullRequest.repositoryPath);
|
|
4754
|
+
const url = pullRequestAt(pullRequest.repositoryPath, pullRequest.pullRequest);
|
|
4755
|
+
if (operation.type === "create") return {
|
|
4756
|
+
...located.data,
|
|
4757
|
+
operation: {
|
|
4758
|
+
...operation,
|
|
4759
|
+
pullRequestUrl: url,
|
|
4760
|
+
repositoryId
|
|
4761
|
+
}
|
|
4762
|
+
};
|
|
4763
|
+
return {
|
|
4764
|
+
...located.data,
|
|
4765
|
+
operation: {
|
|
4766
|
+
...operation,
|
|
4767
|
+
target: (await resolve(url, repositoryId)).target,
|
|
4768
|
+
pullRequestUrl: url
|
|
4769
|
+
}
|
|
4770
|
+
};
|
|
4771
|
+
}
|
|
4772
|
+
/** A PR's URL: as given, or its number on the checkout's GitHub repository. */
|
|
4773
|
+
function pullRequestAt(repositoryPath, pullRequest) {
|
|
4774
|
+
if (isStringValue(pullRequest) && !/^\d+$/.test(pullRequest)) return pullRequest;
|
|
4775
|
+
const slug = resolveRepoContextSync(repositoryPath)?.githubSlug;
|
|
4776
|
+
if (!slug) throw new ReviewInputError("This checkout has no GitHub remote to find the PR number in; pass the PR's URL.");
|
|
4777
|
+
return `https://github.com/${slug}/pull/${pullRequest}`;
|
|
4778
|
+
}
|
|
4779
|
+
/** A pins object as an edit spells it: commits, with no other fields. A
|
|
4780
|
+
* diagram may key its own data `pins`, and that is left alone. */
|
|
4781
|
+
/** Pins as an agent writes them: the board's repository is the host's to
|
|
4782
|
+
* add. */
|
|
4783
|
+
const agentPinsSchema = z.strictObject({
|
|
4784
|
+
head: z.string().min(1),
|
|
4785
|
+
base: z.string().min(1).optional()
|
|
4786
|
+
});
|
|
4787
|
+
/** An object an agent meant as pins: one naming a head, base or repository.
|
|
4788
|
+
* Content may have a field called `pins` that is nothing of the kind. */
|
|
4789
|
+
const meantAsPins = (value) => isJsonObject(value) && [
|
|
4790
|
+
"head",
|
|
4791
|
+
"base",
|
|
4792
|
+
"repositoryId"
|
|
4793
|
+
].some((key) => key in value);
|
|
4794
|
+
/** Sets every pins object in an edit to the board's own repository. */
|
|
4795
|
+
function withBoardRepository(body, board) {
|
|
4796
|
+
const located = z.looseObject({ operation: z.looseObject({
|
|
4797
|
+
type: z.enum(["edit", "lens_edit"]),
|
|
4798
|
+
reviewId: z.string(),
|
|
4799
|
+
edit: z.json()
|
|
4800
|
+
}) }).safeParse(body);
|
|
4801
|
+
if (!located.success) return body;
|
|
4802
|
+
const snapshot = board(located.data.operation.reviewId);
|
|
4803
|
+
const repositoryId = snapshot ? boardRepositoryId(snapshot) : void 0;
|
|
4804
|
+
const edit = JSON.parse(JSON.stringify(located.data.operation.edit), (key, value) => {
|
|
4805
|
+
if (key !== "pins" || !meantAsPins(value)) return value;
|
|
4806
|
+
const { repositoryId: _ignored, ...written } = value;
|
|
4807
|
+
const pins = agentPinsSchema.safeParse(written);
|
|
4808
|
+
if (!pins.success) throw new ReviewInputError(`Write pins as {head, base?}, each a commit: ${pins.error.issues.map((issue) => `pins.${issue.path.join(".")}: ${issue.message}`).join("; ")}.`);
|
|
4809
|
+
if (!repositoryId) throw new ReviewInputError("This whiteboard has no repository for its pins to name commits of.");
|
|
4810
|
+
return {
|
|
4811
|
+
...pins.data,
|
|
4812
|
+
repositoryId
|
|
4813
|
+
};
|
|
4814
|
+
});
|
|
4815
|
+
return {
|
|
4816
|
+
...located.data,
|
|
4817
|
+
operation: {
|
|
4818
|
+
...located.data.operation,
|
|
4819
|
+
edit
|
|
4820
|
+
}
|
|
4821
|
+
};
|
|
4822
|
+
}
|
|
4449
4823
|
function watch(lines, subscribe) {
|
|
4450
4824
|
let stop = () => {};
|
|
4451
4825
|
const encoder = new TextEncoder();
|
|
@@ -4614,7 +4988,7 @@ function createWhiteboardCore(input) {
|
|
|
4614
4988
|
if (result.deleted || result.lifecycle) closeDeletedThreads();
|
|
4615
4989
|
});
|
|
4616
4990
|
const stopShared = askThreads && shared?.subscribe(closeDeletedThreads);
|
|
4617
|
-
const api = createReviewApi(store, data, callbacks.open, shared, callbacks.capabilities,
|
|
4991
|
+
const api = createReviewApi(store, data, callbacks.open, shared, callbacks.capabilities, () => traceMachineEnabled(), input.status, input.hooks, askThreads && {
|
|
4618
4992
|
threads: askThreads,
|
|
4619
4993
|
agents: async () => detectAskAgents(await ask?.env)
|
|
4620
4994
|
});
|
|
@@ -4899,7 +5273,6 @@ async function serve(input) {
|
|
|
4899
5273
|
token: discovery.token,
|
|
4900
5274
|
instanceId: discovery.instanceId,
|
|
4901
5275
|
softwareMapEnabled: input.softwareMapEnabled,
|
|
4902
|
-
scratchpad: () => false,
|
|
4903
5276
|
status: () => ({
|
|
4904
5277
|
key: "headless",
|
|
4905
5278
|
home: input.stateDir
|