@dev.fast/whiteboard 0.3.4 → 0.3.5-preview.20261021.131
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-CprZojXd.js → account-alias-CisiYUZT.js} +13 -8
- package/dist/agent-cli-BglNRDZN.js +2 -0
- package/dist/{agent-cli-CThN7Q59.js → agent-cli-TJ7_5_B3.js} +15 -10
- package/dist/{agent-client-BOOi7EUR.js → agent-client-H53ajvrm.js} +13 -3
- package/dist/{authoring-tools-BNwuMnz6.js → authoring-tools-BCquUsB3.js} +34 -27
- package/dist/{background-server-DLSDogkI.js → background-server-BSYXD9au.js} +1 -1
- package/dist/{background-server-CkvxA-6O.js → background-server-Xom8cA_u.js} +5 -5
- package/dist/build-info.json +1 -1
- package/dist/{cli-R4yfa5DT.js → cli-BOnV4cCh.js} +1 -1
- package/dist/{cli-runner-Cuc9FSzu.js → cli-runner-cRY5NOno.js} +70 -34
- package/dist/cli.js +4 -3
- package/dist/{client-BN_FDxFr.js → client-UH1bEG7Z.js} +260 -49
- package/dist/{fuzzy-match-BO9bK37N.js → fuzzy-match-D_x3wumC.js} +6 -4
- package/dist/{headless-host-CVBOQyRS.js → headless-host-BEe-aFtB.js} +1 -1
- package/dist/{headless-host-Cog6SpS9.js → headless-host-CEb5d-A0.js} +563 -175
- package/dist/lens-selection-BxPk53c8.js +214 -0
- package/dist/{local-data-D_9tvdsp.js → local-data-mbCb1HDH.js} +2448 -347
- package/dist/{mcp-D1hziANa.js → mcp-cBtzXsmN.js} +9 -12
- package/dist/{package-paths-C--__jEv.js → package-paths-BkeQsdaw.js} +1 -1
- package/dist/profile-Dbyidrcr.js +2 -0
- package/dist/{profile-DokFw782.js → profile-duqp5uPa.js} +20 -8
- package/dist/{remote-attach-CEJ-kvDY.js → remote-attach-BeKuMm_x.js} +4 -4
- package/dist/{remote-extensions-GN8Gf_qG.js → remote-extensions-CX2fAec1.js} +1 -1
- package/dist/{remote-extensions-Blcjr9Ze.js → remote-extensions-CYWNTVSf.js} +3 -3
- package/dist/{remote-language-server-BTKBQ7x4.js → remote-language-server-DLWluAeR.js} +5 -5
- package/dist/{remote-uninstall-DW7Gdryh.js → remote-uninstall-Y3S0YmJs.js} +6 -6
- package/dist/{request-origin-Zcn_CjX4.js → request-origin-icXPv6jh.js} +55 -18
- package/dist/{review-app-launcher-CkwLclzR.js → review-app-launcher-DZvG5ZAh.js} +2 -2
- package/dist/{review-home-paths-j2BRAaNv.js → review-home-paths-BwH08ihj.js} +19 -17
- package/dist/{review-telemetry-BLNwYnLV.js → review-telemetry-AZ0imsmJ.js} +6 -6
- package/dist/runtime-CF4JXdb8.js +27 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/{s3-DtJFWC0G.js → s3-CsG2EWED.js} +1 -1
- package/dist/{s3-config-BTO12tCC.js → s3-config-CjcLyFV5.js} +4 -2
- package/dist/{s3-config-CSy3fx8H.js → s3-config-zz4GWh2T.js} +1 -1
- package/dist/server/desktop-host.js +28 -104
- package/dist/{server-discovery-Bey8IqXH.js → server-discovery-DeVTdcqK.js} +3 -3
- package/dist/sharing/index.d.ts +484 -74
- package/dist/sharing/index.js +1 -1
- package/dist/{src-DsM3jO8G.js → src-DYZLCUP_.js} +98 -18
- package/dist/{src-BMPuxWv4.js → src-DtfSndqS.js} +269 -227
- package/dist/{stored-document-migration-CChxtFAy.js → stored-document-migration-B29lve9-.js} +318 -271
- package/dist/{thread-state-5pYr6_zM.js → thread-state-BPpvKFwQ.js} +12 -2
- package/dist/{tool-failure-C9MpbXJJ.js → tool-failure-CQhyLht3.js} +10 -3
- package/instructions/authoring.md +20 -5
- package/package.json +4 -4
- package/src/agent-selection.ts +6 -0
- package/src/ask/agents.ts +13 -1
- package/src/ask/checkout-files.ts +2 -5
- package/src/ask/thread-state.ts +4 -0
- package/src/ask/thread.ts +61 -31
- package/src/ask/threads.ts +24 -3
- package/src/cli-install.ts +13 -2
- package/src/cli-runner.ts +4 -1
- package/src/cli.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/markdown.ts +42 -0
- package/src/posthog-capture-client.ts +1 -1
- 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 +118 -51
- package/src/review-api/authoring-tools.ts +45 -30
- package/src/review-api/azure-devops-provider.ts +2 -20
- package/src/review-api/azure-remote.ts +21 -0
- 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 +586 -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 +362 -67
- package/src/review-api/foreign-references.ts +328 -0
- package/src/review-api/github-provider.ts +51 -13
- package/src/review-api/http.ts +734 -135
- 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 +7 -13
- 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 +355 -0
- package/src/review-api/store-schema.ts +38 -29
- package/src/review-api/store.ts +2176 -495
- 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 -36
- package/src/server/headless-host.ts +0 -2
- package/src/server/hono-http.ts +2 -1
- 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 +143 -24
- package/src/startup-trace.ts +3 -3
- package/src/stored-document-migration.ts +110 -19
- package/src/ui-telemetry-events.ts +44 -18
- package/dist/agent-cli-C6SPSII0.js +0 -2
- package/dist/profile-CYPmvBoA.js +0 -2
- package/dist/runtime-CKiWhGrf.js +0 -52
- package/instructions/scratchpad.md +0 -23
- package/src/review-preferences.ts +0 -88
- package/src/server/legacy-tutorial-cleanup.ts +0 -38
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
import { d as git, r as defaultBranch } from "./src-
|
|
1
|
+
import { b as resolveRepoContext, d as git, r as defaultBranch, w as runVcs } from "./src-DtfSndqS.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-UH1bEG7Z.js";
|
|
4
|
+
import { f as textualRows, h as diffrMissingError, i as inspectSnapshot, m as diffrExecutable, p as invalidateStructuralComparisons, r as commandSchema } from "./local-data-mbCb1HDH.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, y as REVIEW_HOST_HEADER } from "./src-DYZLCUP_.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-BwH08ihj.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-BxPk53c8.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-B29lve9-.js";
|
|
8
9
|
import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
|
|
9
|
-
import { A as processStartIdentity, L as writePrivateJsonAtomic, N as devReviewHome, h as TraceConfigurationError, i as isS3MockMode, j as withFileLock } from "./s3-config-
|
|
10
|
+
import { A as processStartIdentity, L as writePrivateJsonAtomic, N as devReviewHome, h as TraceConfigurationError, i as isS3MockMode, j as withFileLock } from "./s3-config-CjcLyFV5.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, 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, i as askAgents, l as launchAskAgent, n as askAgentSettings, o as detectAskAgents, r as askAgentTakesMcp, t as fuzzyRank } from "./fuzzy-match-
|
|
17
|
-
import { a as
|
|
18
|
-
import { a as headlessServerOwner, c as stopBackgroundServer, o as recordedBackgroundServer } from "./background-server-
|
|
12
|
+
import { i as readReviewPackageVersion, n as readBuildCommit, t as findReviewPackageRoot } from "./package-paths-BkeQsdaw.js";
|
|
13
|
+
import { _ as reviewInstanceIdentity, n as desktopOwnsHome, o as reviewServerDiscoveryPath, p as desktopUsesHome, r as headlessServerLockPath, t as DESKTOP_TAKEOVER_SIGNAL } from "./server-discovery-DeVTdcqK.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-BPpvKFwQ.js";
|
|
15
|
+
import { t as openReviewProfile } from "./profile-duqp5uPa.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-icXPv6jh.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-D_x3wumC.js";
|
|
18
|
+
import { a as inspectQuerySchema, i as renderInstructions, o as queryAnchor, r as instructionsQuerySchema, s as readQuerySchemas, t as authoringTools } from "./authoring-tools-BCquUsB3.js";
|
|
19
|
+
import { a as headlessServerOwner, c as stopBackgroundServer, o as recordedBackgroundServer } from "./background-server-Xom8cA_u.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";
|
|
@@ -196,6 +197,12 @@ const STEER_METHOD = "_session/steering";
|
|
|
196
197
|
/** How an agent says, in its `initialize` response, that it takes steering. */
|
|
197
198
|
const steeringSchema = z.object({ steering: z.object({ supported: z.literal(true) }) });
|
|
198
199
|
const steerOutcomeSchema = z.object({ outcome: z.string() });
|
|
200
|
+
/** A review Codex couldn't run; one it ran has a risk level. */
|
|
201
|
+
const failedReviewSchema = z.object({ review: z.object({
|
|
202
|
+
status: z.literal("denied"),
|
|
203
|
+
riskLevel: z.null(),
|
|
204
|
+
rationale: z.string()
|
|
205
|
+
}) });
|
|
199
206
|
/** MCP servers the reviewer's own agent may bring whose tools only read,
|
|
200
207
|
* so their calls run without asking, like the agent reading files. */
|
|
201
208
|
const READ_ONLY_MCP_SERVERS = /* @__PURE__ */ new Set(["fff"]);
|
|
@@ -213,6 +220,7 @@ var AskThread = class {
|
|
|
213
220
|
/** The number of changes so far; a snapshot carries the one it includes. */
|
|
214
221
|
seq = 0;
|
|
215
222
|
listeners = /* @__PURE__ */ new Set();
|
|
223
|
+
statusListeners = /* @__PURE__ */ new Set();
|
|
216
224
|
closers = /* @__PURE__ */ new Set();
|
|
217
225
|
decisions = /* @__PURE__ */ new Map();
|
|
218
226
|
process;
|
|
@@ -248,6 +256,8 @@ var AskThread = class {
|
|
|
248
256
|
needsContext;
|
|
249
257
|
/** The reviewer changed what the agent may do since it was last told. */
|
|
250
258
|
permitted = false;
|
|
259
|
+
/** Why the agent's automatic approval review failed this turn. */
|
|
260
|
+
reviewFailure;
|
|
251
261
|
/** The agent could not reopen the session, for a reason signing in would
|
|
252
262
|
* not fix, so trying again starts a new one. */
|
|
253
263
|
unloadable = false;
|
|
@@ -261,9 +271,6 @@ var AskThread = class {
|
|
|
261
271
|
steerable = false;
|
|
262
272
|
/** Follow-ups waiting for the turn under way to end, oldest first. */
|
|
263
273
|
queue = [];
|
|
264
|
-
/** The review the thread answers in, and where it saves; adopting moves
|
|
265
|
-
* both. */
|
|
266
|
-
home;
|
|
267
274
|
constructor(launch, start, mcpServers = () => [], limits = askThreadLimits) {
|
|
268
275
|
this.launch = launch;
|
|
269
276
|
this.start = start;
|
|
@@ -271,15 +278,6 @@ var AskThread = class {
|
|
|
271
278
|
this.id = start.id ?? randomUUID();
|
|
272
279
|
this.mcpServers = mcpServers(this.id);
|
|
273
280
|
this.picks = { ...start.picks };
|
|
274
|
-
this.home = {
|
|
275
|
-
reviewId: start.reviewId,
|
|
276
|
-
onSession: start.onSession,
|
|
277
|
-
onTurn: start.onTurn,
|
|
278
|
-
onSave: start.onSave,
|
|
279
|
-
onPermissions: start.onPermissions,
|
|
280
|
-
onOffer: start.onOffer,
|
|
281
|
-
onTitle: start.onTitle
|
|
282
|
-
};
|
|
283
281
|
this.state = {
|
|
284
282
|
id: this.id,
|
|
285
283
|
agent: start.agent,
|
|
@@ -297,17 +295,7 @@ var AskThread = class {
|
|
|
297
295
|
this.needsContext = !("resume" in start);
|
|
298
296
|
}
|
|
299
297
|
get reviewId() {
|
|
300
|
-
return this.
|
|
301
|
-
}
|
|
302
|
-
/** Moves the thread to another review, as when its agent creates the
|
|
303
|
-
* review it was asked for, and saves it there as it stands. */
|
|
304
|
-
adopt(reviewId, hooks) {
|
|
305
|
-
this.home = {
|
|
306
|
-
reviewId,
|
|
307
|
-
...hooks
|
|
308
|
-
};
|
|
309
|
-
if (this.sessionId) hooks.onSession?.(this.sessionId);
|
|
310
|
-
hooks.onSave?.(this.state.entries);
|
|
298
|
+
return this.start.reviewId;
|
|
311
299
|
}
|
|
312
300
|
read() {
|
|
313
301
|
return this.state;
|
|
@@ -318,11 +306,19 @@ var AskThread = class {
|
|
|
318
306
|
snapshot: this.state
|
|
319
307
|
};
|
|
320
308
|
}
|
|
309
|
+
get status() {
|
|
310
|
+
return this.state.status;
|
|
311
|
+
}
|
|
321
312
|
/** Every change after the current `seq`, in order. */
|
|
322
313
|
subscribe(listener) {
|
|
323
314
|
this.listeners.add(listener);
|
|
324
315
|
return () => this.listeners.delete(listener);
|
|
325
316
|
}
|
|
317
|
+
/** Calls back when the status changes, without following the thread. */
|
|
318
|
+
onStatusChange(listener) {
|
|
319
|
+
this.statusListeners.add(listener);
|
|
320
|
+
return () => this.statusListeners.delete(listener);
|
|
321
|
+
}
|
|
326
322
|
/** Whether anything follows the thread, as an open panel does. */
|
|
327
323
|
watched() {
|
|
328
324
|
return this.listeners.size > 0;
|
|
@@ -487,7 +483,7 @@ var AskThread = class {
|
|
|
487
483
|
if (this.closed) return;
|
|
488
484
|
this.closed = true;
|
|
489
485
|
try {
|
|
490
|
-
if (save && this.sessionId && this.state.status !== "starting" && this.state.status !== "failed") this.
|
|
486
|
+
if (save && this.sessionId && this.state.status !== "starting" && this.state.status !== "failed") this.start.onSave?.(settled(this.state.entries));
|
|
491
487
|
} finally {
|
|
492
488
|
for (const resolve of this.decisions.values()) resolve({ outcome: { outcome: "cancelled" } });
|
|
493
489
|
if (this.connection && this.sessionId) this.connection.agent.notify(methods.agent.session.cancel, { sessionId: this.sessionId }).catch(() => {});
|
|
@@ -579,7 +575,8 @@ var AskThread = class {
|
|
|
579
575
|
const generation = ++this.generation;
|
|
580
576
|
const launched = await this.launch(this.start.agent, this.start.cwd, {
|
|
581
577
|
permissions: this.state.permissions,
|
|
582
|
-
mcpServers: this.mcpServers
|
|
578
|
+
mcpServers: this.mcpServers,
|
|
579
|
+
threadId: this.id
|
|
583
580
|
});
|
|
584
581
|
if (this.closed || this.halted || generation !== this.generation) {
|
|
585
582
|
launched.stop();
|
|
@@ -704,7 +701,7 @@ var AskThread = class {
|
|
|
704
701
|
const offer = { choices };
|
|
705
702
|
if (commands) offer.commands = commands;
|
|
706
703
|
if (accepts) offer.accepts = accepts;
|
|
707
|
-
this.
|
|
704
|
+
this.start.onOffer?.(offer);
|
|
708
705
|
}
|
|
709
706
|
/** Changes what the agent may do from the next answer: it starts again
|
|
710
707
|
* in that mode and reopens the conversation. */
|
|
@@ -717,7 +714,7 @@ var AskThread = class {
|
|
|
717
714
|
permissions
|
|
718
715
|
});
|
|
719
716
|
this.permitted = true;
|
|
720
|
-
this.
|
|
717
|
+
this.start.onPermissions?.(permissions);
|
|
721
718
|
this.disconnect();
|
|
722
719
|
this.settings.clear();
|
|
723
720
|
this.emit({
|
|
@@ -759,7 +756,7 @@ var AskThread = class {
|
|
|
759
756
|
});
|
|
760
757
|
if (this.closed) throw new Error("This conversation has ended.");
|
|
761
758
|
this.sessionId = response.sessionId;
|
|
762
|
-
this.
|
|
759
|
+
this.start.onSession?.(response.sessionId);
|
|
763
760
|
this.greeting = piGreetingSchema.safeParse(response._meta).data?.piAcp.startupInfo;
|
|
764
761
|
return {
|
|
765
762
|
sessionId: response.sessionId,
|
|
@@ -838,13 +835,23 @@ var AskThread = class {
|
|
|
838
835
|
severity: "info",
|
|
839
836
|
title: `${this.state.agentName} finished without replying.`
|
|
840
837
|
});
|
|
838
|
+
const reviewFailed = this.reviewFailure !== void 0 && this.state.permissions === "auto";
|
|
839
|
+
if (reviewFailed) this.push({
|
|
840
|
+
kind: "notice",
|
|
841
|
+
id: randomUUID(),
|
|
842
|
+
severity: "warning",
|
|
843
|
+
title: `${this.state.agentName}'s automatic approval review failed, so this conversation is now read-only. Ask again to continue.`,
|
|
844
|
+
description: this.reviewFailure
|
|
845
|
+
});
|
|
846
|
+
this.reviewFailure = void 0;
|
|
841
847
|
this.emit({
|
|
842
848
|
type: "set",
|
|
843
849
|
status: "idle",
|
|
844
850
|
error: stopReason === "refusal" ? `${this.state.agentName} declined to answer.` : stopReason === "max_tokens" || stopReason === "max_turn_requests" ? `${this.state.agentName} stopped before finishing.` : null
|
|
845
851
|
});
|
|
846
|
-
this.
|
|
847
|
-
this.
|
|
852
|
+
this.start.onTurn?.();
|
|
853
|
+
this.start.onSave?.(this.state.entries);
|
|
854
|
+
if (reviewFailed) await this.permit("read-only");
|
|
848
855
|
this.askQueued();
|
|
849
856
|
}
|
|
850
857
|
async requestPermission(request, signal) {
|
|
@@ -957,6 +964,7 @@ var AskThread = class {
|
|
|
957
964
|
}
|
|
958
965
|
case "tool_call":
|
|
959
966
|
this.noteMcpServer(update);
|
|
967
|
+
this.noteReview(update);
|
|
960
968
|
this.push({
|
|
961
969
|
kind: "tool",
|
|
962
970
|
id: update.toolCallId,
|
|
@@ -969,6 +977,7 @@ var AskThread = class {
|
|
|
969
977
|
return;
|
|
970
978
|
case "tool_call_update":
|
|
971
979
|
this.noteMcpServer(update);
|
|
980
|
+
this.noteReview(update);
|
|
972
981
|
if (!this.state.entries.some((entry) => entry.kind === "tool" && entry.id === update.toolCallId)) {
|
|
973
982
|
this.push({
|
|
974
983
|
kind: "tool",
|
|
@@ -1034,7 +1043,7 @@ var AskThread = class {
|
|
|
1034
1043
|
type: "set",
|
|
1035
1044
|
title
|
|
1036
1045
|
});
|
|
1037
|
-
this.
|
|
1046
|
+
this.start.onTitle?.(title);
|
|
1038
1047
|
}
|
|
1039
1048
|
return true;
|
|
1040
1049
|
}
|
|
@@ -1045,6 +1054,10 @@ var AskThread = class {
|
|
|
1045
1054
|
const server = mcpServerOf(update);
|
|
1046
1055
|
if (server) this.mcpCalls.set(update.toolCallId, server);
|
|
1047
1056
|
}
|
|
1057
|
+
noteReview(update) {
|
|
1058
|
+
const failed = failedReviewSchema.safeParse(update.rawOutput ?? update.rawInput).data;
|
|
1059
|
+
if (failed && !this.replaying) this.reviewFailure = failed.review.rationale;
|
|
1060
|
+
}
|
|
1048
1061
|
/**
|
|
1049
1062
|
* In its read-only mode Claude answers by writing a plan and asking to
|
|
1050
1063
|
* leave the mode with it. That request is refused, so the plan is shown
|
|
@@ -1177,6 +1190,7 @@ var AskThread = class {
|
|
|
1177
1190
|
change
|
|
1178
1191
|
};
|
|
1179
1192
|
for (const listener of this.listeners) listener(update);
|
|
1193
|
+
if (change.type === "set" && change.status) for (const listener of this.statusListeners) listener();
|
|
1180
1194
|
}
|
|
1181
1195
|
};
|
|
1182
1196
|
//#endregion
|
|
@@ -1261,13 +1275,16 @@ function cliAskTools(cliPath, selector) {
|
|
|
1261
1275
|
}] : []
|
|
1262
1276
|
];
|
|
1263
1277
|
return {
|
|
1264
|
-
mcpServers: () => {
|
|
1278
|
+
mcpServers: (threadId) => {
|
|
1265
1279
|
const cli = cliPath();
|
|
1266
1280
|
return cli ? [{
|
|
1267
1281
|
name: "whiteboard",
|
|
1268
1282
|
command: process.execPath,
|
|
1269
1283
|
args: [cli, "mcp"],
|
|
1270
|
-
env: env()
|
|
1284
|
+
env: [...env(), {
|
|
1285
|
+
name: ASK_THREAD_ENV,
|
|
1286
|
+
value: threadId
|
|
1287
|
+
}]
|
|
1271
1288
|
}] : [];
|
|
1272
1289
|
},
|
|
1273
1290
|
cli: () => {
|
|
@@ -1295,6 +1312,7 @@ var AskThreads = class {
|
|
|
1295
1312
|
* model. */
|
|
1296
1313
|
offers = /* @__PURE__ */ new Map();
|
|
1297
1314
|
mcpServers;
|
|
1315
|
+
statusListeners = /* @__PURE__ */ new Set();
|
|
1298
1316
|
constructor(launch, tools = {}, limits = askThreadLimits, unwatchedIdleMs = UNWATCHED_IDLE_MS) {
|
|
1299
1317
|
this.launch = launch;
|
|
1300
1318
|
this.tools = tools;
|
|
@@ -1315,6 +1333,8 @@ var AskThreads = class {
|
|
|
1315
1333
|
open(start) {
|
|
1316
1334
|
const thread = new AskThread(this.launch, start, this.mcpServers, this.limits);
|
|
1317
1335
|
this.threads.set(thread.id, thread);
|
|
1336
|
+
thread.onStatusChange(() => this.statusChanged());
|
|
1337
|
+
this.statusChanged();
|
|
1318
1338
|
thread.open();
|
|
1319
1339
|
this.sweeper ??= setInterval(() => this.sweep(), SWEEP_MS);
|
|
1320
1340
|
this.sweeper.unref?.();
|
|
@@ -1345,6 +1365,16 @@ var AskThreads = class {
|
|
|
1345
1365
|
if (now - since >= this.unwatchedIdleMs) this.close(id);
|
|
1346
1366
|
}
|
|
1347
1367
|
}
|
|
1368
|
+
/** Calls back when a thread opens, closes or changes its status. */
|
|
1369
|
+
subscribeStatus(listener) {
|
|
1370
|
+
this.statusListeners.add(listener);
|
|
1371
|
+
return () => {
|
|
1372
|
+
this.statusListeners.delete(listener);
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
statusChanged() {
|
|
1376
|
+
for (const listener of this.statusListeners) listener();
|
|
1377
|
+
}
|
|
1348
1378
|
get(id) {
|
|
1349
1379
|
return this.threads.get(id);
|
|
1350
1380
|
}
|
|
@@ -1361,9 +1391,9 @@ var AskThreads = class {
|
|
|
1361
1391
|
}
|
|
1362
1392
|
close(id, save = true) {
|
|
1363
1393
|
const thread = this.threads.get(id);
|
|
1364
|
-
this.threads.delete(id);
|
|
1365
1394
|
this.unwatchedSince.delete(id);
|
|
1366
1395
|
thread?.close(save);
|
|
1396
|
+
if (this.threads.delete(id)) this.statusChanged();
|
|
1367
1397
|
}
|
|
1368
1398
|
closeDeletedReviews(isDeleted) {
|
|
1369
1399
|
for (const [id, thread] of this.threads) if (isDeleted(thread.reviewId)) this.close(id, false);
|
|
@@ -1376,7 +1406,7 @@ var AskThreads = class {
|
|
|
1376
1406
|
};
|
|
1377
1407
|
//#endregion
|
|
1378
1408
|
//#region src/server/diffr-config.ts
|
|
1379
|
-
const execFileAsync
|
|
1409
|
+
const execFileAsync = promisify(execFile);
|
|
1380
1410
|
let writes = Promise.resolve();
|
|
1381
1411
|
let testing = false;
|
|
1382
1412
|
function serialized(operation) {
|
|
@@ -1417,7 +1447,7 @@ function diffrArgv(args) {
|
|
|
1417
1447
|
async function diffr(args, rootPath, options = {}) {
|
|
1418
1448
|
const executable = diffrExecutable();
|
|
1419
1449
|
try {
|
|
1420
|
-
const operation = execFileAsync
|
|
1450
|
+
const operation = execFileAsync(executable, diffrArgv(args), {
|
|
1421
1451
|
cwd: rootPath,
|
|
1422
1452
|
maxBuffer: 16777216,
|
|
1423
1453
|
signal: options.signal ?? AbortSignal.timeout(3e4),
|
|
@@ -1847,7 +1877,7 @@ function applyCorsHeaders(request, response) {
|
|
|
1847
1877
|
const varyFields = vary?.split(",").map((field) => field.trim().toLowerCase());
|
|
1848
1878
|
if (!varyFields?.includes("*") && !varyFields?.includes("origin")) response.headers.set("vary", vary ? `${vary}, Origin` : "Origin");
|
|
1849
1879
|
}
|
|
1850
|
-
response.headers.set("access-control-allow-headers", `content-type, x-review-token, ${REVIEW_APP_SESSION_ID_HEADER}`);
|
|
1880
|
+
response.headers.set("access-control-allow-headers", `content-type, x-review-token, ${REVIEW_PERSON_HEADER}, ${REVIEW_APP_SESSION_ID_HEADER}`);
|
|
1851
1881
|
response.headers.set("access-control-allow-methods", "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS");
|
|
1852
1882
|
response.headers.set("access-control-allow-private-network", "true");
|
|
1853
1883
|
response.headers.set("access-control-expose-headers", REVIEW_HOST_HEADER);
|
|
@@ -2276,11 +2306,15 @@ async function reviewProgress(store, data, snapshot, signal = new AbortControlle
|
|
|
2276
2306
|
return [];
|
|
2277
2307
|
}
|
|
2278
2308
|
if (!file) {
|
|
2279
|
-
const [base, head] = await Promise.all([
|
|
2280
|
-
|
|
2281
|
-
|
|
2309
|
+
const [base, head] = await Promise.all(["base", "head"].map((side) => data.file(sourcePins, side, source.file).then((value) => value.text, (error) => {
|
|
2310
|
+
if (error instanceof ReviewInputError && error.status === 404) return null;
|
|
2311
|
+
throw error;
|
|
2312
|
+
})));
|
|
2313
|
+
if (base === null && head === null) throw new SelectionUnavailableError("missing_file", "File is unavailable at these pins.");
|
|
2314
|
+
if (base !== null && head !== null && base !== head) throw new SelectionUnavailableError("no_alignment", "Changed context file has no alignment.");
|
|
2315
|
+
rows = textualRows(source.file, "", base?.split("\n").length ?? 0, head?.split("\n").length ?? 0);
|
|
2282
2316
|
}
|
|
2283
|
-
if (!rows) throw new
|
|
2317
|
+
if (!rows) throw new SelectionUnavailableError("no_alignment", "File has no text alignment.");
|
|
2284
2318
|
const result = resolveDiffSelection(source, rows, file ?? { path: source.file });
|
|
2285
2319
|
resolvedSelections[key] = result;
|
|
2286
2320
|
return result;
|
|
@@ -2288,8 +2322,8 @@ async function reviewProgress(store, data, snapshot, signal = new AbortControlle
|
|
|
2288
2322
|
const resolveAvailable = async (source) => {
|
|
2289
2323
|
try {
|
|
2290
2324
|
return await resolve(source);
|
|
2291
|
-
} catch {
|
|
2292
|
-
unavailableSelections[selectionKey(source)] =
|
|
2325
|
+
} catch (error) {
|
|
2326
|
+
unavailableSelections[selectionKey(source)] = error instanceof SelectionUnavailableError ? error.reason : "other";
|
|
2293
2327
|
return [];
|
|
2294
2328
|
}
|
|
2295
2329
|
};
|
|
@@ -2315,7 +2349,10 @@ async function reviewProgress(store, data, snapshot, signal = new AbortControlle
|
|
|
2315
2349
|
};
|
|
2316
2350
|
}
|
|
2317
2351
|
}));
|
|
2318
|
-
await Promise.all(selectionReferences(snapshot.document, {
|
|
2352
|
+
await Promise.all(selectionReferences(snapshot.document, {
|
|
2353
|
+
tolerant: true,
|
|
2354
|
+
repositoryId: boardRepositoryId(snapshot)
|
|
2355
|
+
}).map(async ({ source }) => {
|
|
2319
2356
|
try {
|
|
2320
2357
|
await resolveAvailable(source);
|
|
2321
2358
|
} catch {}
|
|
@@ -2331,7 +2368,7 @@ async function reviewProgress(store, data, snapshot, signal = new AbortControlle
|
|
|
2331
2368
|
unavailable: counting || uncategorized.length ? void 0 : "All changed lines are covered by lenses"
|
|
2332
2369
|
});
|
|
2333
2370
|
const progress = {
|
|
2334
|
-
complete: !counting,
|
|
2371
|
+
complete: !counting && !pendingSources.size,
|
|
2335
2372
|
files,
|
|
2336
2373
|
referenceFiles,
|
|
2337
2374
|
lenses,
|
|
@@ -2388,12 +2425,11 @@ function lensReport(lenses, progress) {
|
|
|
2388
2425
|
}
|
|
2389
2426
|
//#endregion
|
|
2390
2427
|
//#region src/ask/checkout-files.ts
|
|
2391
|
-
const execFileAsync$1 = promisify(execFile);
|
|
2392
2428
|
/** The checkout's files relative to its root: tracked ones, and new ones git
|
|
2393
2429
|
* does not ignore. None where git cannot list them. */
|
|
2394
2430
|
async function checkoutFiles(root) {
|
|
2395
2431
|
try {
|
|
2396
|
-
const { stdout } = await
|
|
2432
|
+
const { stdout } = await runVcs("git", [
|
|
2397
2433
|
"-C",
|
|
2398
2434
|
root,
|
|
2399
2435
|
"ls-files",
|
|
@@ -2564,7 +2600,6 @@ function watchAskThreads(threads, backlog = 256) {
|
|
|
2564
2600
|
}
|
|
2565
2601
|
//#endregion
|
|
2566
2602
|
//#region src/review-stack.ts
|
|
2567
|
-
const execFileAsync = promisify(execFile);
|
|
2568
2603
|
const GitHubStacksSchema = z.array(z.object({ pull_requests: z.array(z.object({
|
|
2569
2604
|
number: z.number().int().positive(),
|
|
2570
2605
|
head: z.object({ ref: z.string().min(1) })
|
|
@@ -2612,12 +2647,12 @@ async function gitHubStack(subject, reviews, runGitHubApi) {
|
|
|
2612
2647
|
}
|
|
2613
2648
|
/** The chain of reviews through this one, linked by the base each names. */
|
|
2614
2649
|
async function localStack(subject, reviews, repositoryPath) {
|
|
2615
|
-
const group = reviews.find((review) => review.reviewId === subject.reviewId)?.
|
|
2650
|
+
const group = reviews.find((review) => review.reviewId === subject.reviewId)?.project?.id;
|
|
2616
2651
|
const current = subject.origin?.branch;
|
|
2617
2652
|
if (!group || !current) return [];
|
|
2618
2653
|
const main = subject.origin?.baseBranch ? await defaultBranch(repositoryPath).catch(() => null) : null;
|
|
2619
2654
|
const name = (ref) => ref.replace(/^origin\//, "");
|
|
2620
|
-
const inGroup = reviews.filter((review) => review.
|
|
2655
|
+
const inGroup = reviews.filter((review) => review.project?.id === group);
|
|
2621
2656
|
const latest = (field, branch) => {
|
|
2622
2657
|
const matches = inGroup.filter((review) => review.origin?.[field] === branch);
|
|
2623
2658
|
return matches.findLast((review) => !review.trashedAt) ?? matches.at(-1);
|
|
@@ -2644,8 +2679,8 @@ async function forkPoint(repositoryPath, main, branch) {
|
|
|
2644
2679
|
const ref = `refs/heads/${branch}`;
|
|
2645
2680
|
const fork = await git(repositoryPath, [
|
|
2646
2681
|
"merge-base",
|
|
2647
|
-
main,
|
|
2648
|
-
ref
|
|
2682
|
+
{ rev: main },
|
|
2683
|
+
{ rev: ref }
|
|
2649
2684
|
], { allowFailure: true });
|
|
2650
2685
|
if (!fork.ok) return null;
|
|
2651
2686
|
const base = fork.stdout.trim();
|
|
@@ -2655,22 +2690,17 @@ async function forkPoint(repositoryPath, main, branch) {
|
|
|
2655
2690
|
"log",
|
|
2656
2691
|
"--reverse",
|
|
2657
2692
|
"--format=%s",
|
|
2658
|
-
`${base}..${ref}`
|
|
2693
|
+
{ rev: `${base}..${ref}` }
|
|
2659
2694
|
], { allowFailure: true })).stdout.split("\n")[0] || null
|
|
2660
2695
|
};
|
|
2661
2696
|
}
|
|
2662
2697
|
async function defaultRunGitHubApi(host, endpoint) {
|
|
2663
|
-
const { stdout } = await
|
|
2698
|
+
const { stdout } = await runVcs("gh", [
|
|
2664
2699
|
"api",
|
|
2665
2700
|
"--hostname",
|
|
2666
|
-
host,
|
|
2667
|
-
endpoint
|
|
2701
|
+
{ value: host },
|
|
2702
|
+
{ value: endpoint }
|
|
2668
2703
|
], {
|
|
2669
|
-
env: {
|
|
2670
|
-
...process.env,
|
|
2671
|
-
GH_PROMPT_DISABLED: "1"
|
|
2672
|
-
},
|
|
2673
|
-
encoding: "utf8",
|
|
2674
2704
|
timeout: 1e4,
|
|
2675
2705
|
maxBuffer: 2097152
|
|
2676
2706
|
});
|
|
@@ -2708,7 +2738,6 @@ const publishSchema = z.strictObject({
|
|
|
2708
2738
|
/** Mounted behind local host authentication. Account credentials never enter the renderer. */
|
|
2709
2739
|
function mountSharingHost(app, store, data, shared, options = {}) {
|
|
2710
2740
|
const startLogin = options.login ?? runStoreLogin;
|
|
2711
|
-
const openUrl = options.openUrl ?? openUrlInBrowser;
|
|
2712
2741
|
let login = { pending: false };
|
|
2713
2742
|
app.get("/sharing/account", async (context) => {
|
|
2714
2743
|
const account = await readStoreAuth();
|
|
@@ -2731,9 +2760,9 @@ function mountSharingHost(app, store, data, shared, options = {}) {
|
|
|
2731
2760
|
traces: false,
|
|
2732
2761
|
stdout: discard,
|
|
2733
2762
|
stderr: discard,
|
|
2734
|
-
openUrl:
|
|
2735
|
-
|
|
2736
|
-
|
|
2763
|
+
openUrl: options.openUrl,
|
|
2764
|
+
onDeviceCode: (device) => {
|
|
2765
|
+
Object.assign(login, device);
|
|
2737
2766
|
}
|
|
2738
2767
|
}).then((code) => {
|
|
2739
2768
|
login = { pending: false };
|
|
@@ -2802,8 +2831,16 @@ function mountSharingPublisher(app, store, data, options = {}) {
|
|
|
2802
2831
|
validateShareSources(snapshot);
|
|
2803
2832
|
const account = await readSharingAuth();
|
|
2804
2833
|
if (!account) throw new ReviewInputError("Set DEV_REVIEW_SHARE_TOKEN for CI, or run whiteboard login before sharing.", 409);
|
|
2805
|
-
const
|
|
2806
|
-
const
|
|
2834
|
+
const repositoryId = snapshot.pins?.repositoryId ?? snapshot.target?.repositoryId;
|
|
2835
|
+
const root = repositoryId ? store.repositoryPath(repositoryId) : void 0;
|
|
2836
|
+
const readRepository = options.readRepository ?? readShareRepository;
|
|
2837
|
+
const repository = !root ? void 0 : snapshot.pins ? await readRepository(root) : await (async () => {
|
|
2838
|
+
try {
|
|
2839
|
+
return await readRepository(root);
|
|
2840
|
+
} catch {
|
|
2841
|
+
return;
|
|
2842
|
+
}
|
|
2843
|
+
})();
|
|
2807
2844
|
const bundle = await exportShare({
|
|
2808
2845
|
store,
|
|
2809
2846
|
data,
|
|
@@ -2811,7 +2848,8 @@ function mountSharingPublisher(app, store, data, options = {}) {
|
|
|
2811
2848
|
version: snapshot.version,
|
|
2812
2849
|
repository
|
|
2813
2850
|
});
|
|
2814
|
-
const
|
|
2851
|
+
const commits = referenceCommits(snapshot);
|
|
2852
|
+
const verification = root && (snapshot.pins || commits.length) && repository ? verifyRepository(root, snapshot.pins, commits).then(() => ({ ok: true }), (error) => ({
|
|
2815
2853
|
ok: false,
|
|
2816
2854
|
error
|
|
2817
2855
|
})) : Promise.resolve({ ok: true });
|
|
@@ -2887,8 +2925,14 @@ const clip = (line) => line.trim() === "" ? "(blank line)" : line.length > 120 ?
|
|
|
2887
2925
|
*/
|
|
2888
2926
|
async function anchorQuotes(before, after, read) {
|
|
2889
2927
|
const key = (id, source) => `${id}\0${selectionKey(source)}`;
|
|
2890
|
-
const known = new Set(before ? selectionReferences(before.document, {
|
|
2891
|
-
|
|
2928
|
+
const known = new Set(before ? selectionReferences(before.document, {
|
|
2929
|
+
tolerant: true,
|
|
2930
|
+
repositoryId: boardRepositoryId(before)
|
|
2931
|
+
}).map(({ id, source }) => key(id, source)) : []);
|
|
2932
|
+
const added = selectionReferences(after.document, {
|
|
2933
|
+
tolerant: true,
|
|
2934
|
+
repositoryId: boardRepositoryId(after)
|
|
2935
|
+
}).filter(({ id, source }) => !known.has(key(id, source)));
|
|
2892
2936
|
return {
|
|
2893
2937
|
quotes: (await Promise.all(added.slice(0, QUOTE_LIMIT).map(async ({ source }) => {
|
|
2894
2938
|
try {
|
|
@@ -2913,15 +2957,18 @@ async function anchorQuotes(before, after, read) {
|
|
|
2913
2957
|
//#endregion
|
|
2914
2958
|
//#region src/review-api/document-text.ts
|
|
2915
2959
|
/** A reading view of saved content, not another document format to maintain. */
|
|
2916
|
-
function documentText(snapshot, targetId, full = false) {
|
|
2960
|
+
function documentText(snapshot, targetId, full = false, relations) {
|
|
2917
2961
|
const target = targetId === void 0 ? void 0 : elements(snapshot.document).find((item) => item.id === targetId);
|
|
2918
2962
|
if (targetId !== void 0 && !target) throw new ReviewInputError("Target not found in this version.", 404);
|
|
2919
2963
|
const detailed = full || target !== void 0;
|
|
2920
|
-
const lines = [
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2964
|
+
const lines = [`# ${boardTitle(snapshot.title)}`, `Session ${snapshot.reviewId} · version ${snapshot.version}`];
|
|
2965
|
+
if (relations && targetId === void 0) {
|
|
2966
|
+
const list = (boards) => boards.map((board) => `${boardTitle(board.title)} (${board.id})`).join(", ");
|
|
2967
|
+
if (relations.parent) lines.push(`Parent: ${list([relations.parent])}`);
|
|
2968
|
+
if (relations.children.length) lines.push(`Children, in order: ${list(relations.children)}`);
|
|
2969
|
+
if (relations.dependsOn.length) lines.push(`Depends on: ${list(relations.dependsOn)}`);
|
|
2970
|
+
}
|
|
2971
|
+
lines.push("");
|
|
2925
2972
|
const write = (depth, text) => {
|
|
2926
2973
|
for (const line of text.split("\n")) lines.push(" ".repeat(depth) + line);
|
|
2927
2974
|
};
|
|
@@ -3349,7 +3396,7 @@ function registerOnboardingRoutes(app, store, ask, host, run = runCommand) {
|
|
|
3349
3396
|
agent: input.agent,
|
|
3350
3397
|
...host.historyHooks({
|
|
3351
3398
|
id,
|
|
3352
|
-
reviewId,
|
|
3399
|
+
anchor: { board: reviewId },
|
|
3353
3400
|
agent: input.agent,
|
|
3354
3401
|
version: created.version,
|
|
3355
3402
|
head,
|
|
@@ -3497,12 +3544,16 @@ const askDecisionSchema = z.strictObject({
|
|
|
3497
3544
|
permissionId: z.string().min(1),
|
|
3498
3545
|
optionId: z.string().min(1)
|
|
3499
3546
|
});
|
|
3500
|
-
|
|
3547
|
+
/** A person's save carries the whole board, which edits one at a time can
|
|
3548
|
+
* grow past the size of a single command. */
|
|
3549
|
+
const PERSON_EDIT_MAX_BYTES = 16777216;
|
|
3550
|
+
/** A request the gateway forwarded from another machine. */
|
|
3551
|
+
const remoteCaller = (context) => context.req.header(REVIEW_CLIENT_HEADER) === REVIEW_CLIENT_REMOTE;
|
|
3501
3552
|
/** Both hosts mount this behind their token authentication. */
|
|
3502
3553
|
function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
3503
3554
|
desktopAvailable: Boolean(open),
|
|
3504
3555
|
softwareMapEnabled: false
|
|
3505
|
-
}),
|
|
3556
|
+
}), traceEnabled = async () => false, status = () => ({}), hooks = {}, ask) {
|
|
3506
3557
|
const app = new Hono();
|
|
3507
3558
|
app.onError((error, context) => {
|
|
3508
3559
|
if (error instanceof HttpJsonError) return context.json({ error: error.message }, error.statusCode);
|
|
@@ -3531,15 +3582,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3531
3582
|
await next();
|
|
3532
3583
|
};
|
|
3533
3584
|
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
3585
|
const sharedGuard = async (context, next) => {
|
|
3541
|
-
refuseDisabledScratchpad(context.req.param("id"));
|
|
3542
|
-
await ensureScratchpad(context.req.param("id"));
|
|
3543
3586
|
const id = context.req.param("id");
|
|
3544
3587
|
if (!id || !isShared(id)) return next();
|
|
3545
3588
|
const query = readQuerySchemas.get.parse({ version: context.req.query("version") });
|
|
@@ -3558,23 +3601,53 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3558
3601
|
if (!snapshot || version !== void 0 && version !== snapshot.version) throw new ReviewInputError("Shared review version is unavailable.", 404);
|
|
3559
3602
|
return snapshot;
|
|
3560
3603
|
};
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3604
|
+
/** Adds a PR board's state, as this host last read it from GitHub. */
|
|
3605
|
+
const withPullRequestState = (board) => {
|
|
3606
|
+
const state = data?.pullRequestState(board.origin?.pullRequestUrl ?? board.pullRequestUrl);
|
|
3607
|
+
return state ? {
|
|
3608
|
+
...board,
|
|
3609
|
+
pullRequestState: state
|
|
3610
|
+
} : board;
|
|
3564
3611
|
};
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3612
|
+
const catalog = () => [...store.list().map(withPullRequestState), ...shared?.list() ?? []];
|
|
3613
|
+
/** Where a board sits among its project's, and its PR's facts, for the
|
|
3614
|
+
* board's header. */
|
|
3615
|
+
const placeOf = (id) => {
|
|
3616
|
+
if (isShared(id) || !store.has(id)) return { siblings: [] };
|
|
3617
|
+
const { board, parent, siblings, children } = store.place(id);
|
|
3618
|
+
const url = board.origin?.pullRequestUrl;
|
|
3619
|
+
const state = data?.pullRequestState(url);
|
|
3620
|
+
const baseBranch = url && data?.pullRequestBaseBranch(url);
|
|
3621
|
+
const headBranch = url && data?.pullRequestHeadBranch(url);
|
|
3622
|
+
const place = ({ reviewId, title, target }) => ({
|
|
3623
|
+
reviewId,
|
|
3624
|
+
title,
|
|
3625
|
+
icon: boardIconKind(target)
|
|
3626
|
+
});
|
|
3627
|
+
const listed = (child) => {
|
|
3628
|
+
const childState = data?.pullRequestState(child.origin?.pullRequestUrl);
|
|
3629
|
+
return {
|
|
3630
|
+
...place(child),
|
|
3631
|
+
...child.origin?.pullRequestNumber && { pullRequestNumber: child.origin.pullRequestNumber },
|
|
3632
|
+
...childState && { pullRequestState: childState }
|
|
3633
|
+
};
|
|
3634
|
+
};
|
|
3635
|
+
return {
|
|
3636
|
+
...state && { pullRequestState: state },
|
|
3637
|
+
...baseBranch && { baseBranch },
|
|
3638
|
+
...headBranch && { headBranch },
|
|
3639
|
+
board: place(board),
|
|
3640
|
+
...parent && { parent: place(parent) },
|
|
3641
|
+
siblings: siblings.map(listed),
|
|
3642
|
+
...children?.length && { children: children.map(listed) }
|
|
3643
|
+
};
|
|
3644
|
+
};
|
|
3645
|
+
app.get("/", (context) => context.json(catalog()));
|
|
3569
3646
|
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
|
-
});
|
|
3647
|
+
const instructionContext = async () => ({ traceEnabled: await traceEnabled() });
|
|
3575
3648
|
app.get("/authoring", async (context) => {
|
|
3576
3649
|
const instructions = await instructionContext();
|
|
3577
|
-
return context.json(authoringTools(
|
|
3650
|
+
return context.json(authoringTools(instructions.traceEnabled));
|
|
3578
3651
|
});
|
|
3579
3652
|
app.get("/instructions", async (context) => {
|
|
3580
3653
|
const { topic } = instructionsQuerySchema.parse(context.req.query());
|
|
@@ -3628,10 +3701,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3628
3701
|
...status(),
|
|
3629
3702
|
desktopAvailable: (await capabilities()).desktopAvailable
|
|
3630
3703
|
}));
|
|
3631
|
-
app.get("/capabilities", async (context) => context.json(
|
|
3632
|
-
...await capabilities(),
|
|
3633
|
-
scratchpadEnabled: scratchpadEnabled()
|
|
3634
|
-
}));
|
|
3704
|
+
app.get("/capabilities", async (context) => context.json(await capabilities()));
|
|
3635
3705
|
app.get("/:id/activity", (context) => {
|
|
3636
3706
|
const id = context.req.param("id");
|
|
3637
3707
|
readReview(id);
|
|
@@ -3653,6 +3723,9 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3653
3723
|
action
|
|
3654
3724
|
} : input));
|
|
3655
3725
|
});
|
|
3726
|
+
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))));
|
|
3727
|
+
app.post("/:id/focus", async (context) => context.json(store.focus(context.req.param("id"), await readBoundedRequestJson(context.req.raw))));
|
|
3728
|
+
app.post("/:id/discard", async (context) => context.json(await store.discard(context.req.param("id"))));
|
|
3656
3729
|
app.get("/watch", (context) => {
|
|
3657
3730
|
let input;
|
|
3658
3731
|
try {
|
|
@@ -3669,6 +3742,16 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3669
3742
|
let dirtyList = lists;
|
|
3670
3743
|
const dirtyReviews = new Set(reviewIds);
|
|
3671
3744
|
let sentList;
|
|
3745
|
+
const linksRevision = /* @__PURE__ */ new Map();
|
|
3746
|
+
const linksKey = (id) => {
|
|
3747
|
+
if (!data || isShared(id) || !store.has(id)) return "";
|
|
3748
|
+
const { outgoing, backlinks } = store.links(id);
|
|
3749
|
+
return JSON.stringify([...outgoing, ...backlinks].map((link) => link.pullRequestUrl && data.pullRequestState(link.pullRequestUrl, { seen: false })));
|
|
3750
|
+
};
|
|
3751
|
+
const linksKeys = new Map([...reviewIds].map((reviewId) => [reviewId, linksKey(reviewId)]));
|
|
3752
|
+
const placeRevision = /* @__PURE__ */ new Map();
|
|
3753
|
+
const placeKey = (id) => JSON.stringify(placeOf(id));
|
|
3754
|
+
const placeKeys = new Map([...reviewIds].map((reviewId) => [reviewId, placeKey(reviewId)]));
|
|
3672
3755
|
const readReviewLine = (reviewId) => {
|
|
3673
3756
|
try {
|
|
3674
3757
|
return {
|
|
@@ -3677,7 +3760,9 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3677
3760
|
value: {
|
|
3678
3761
|
...readReview(reviewId),
|
|
3679
3762
|
activity: store.activity.read(reviewId),
|
|
3680
|
-
coverageRevision: data?.coverageRevision ?? 0
|
|
3763
|
+
coverageRevision: data?.coverageRevision ?? 0,
|
|
3764
|
+
linksRevision: linksRevision.get(reviewId) ?? 0,
|
|
3765
|
+
placeRevision: placeRevision.get(reviewId) ?? 0
|
|
3681
3766
|
}
|
|
3682
3767
|
};
|
|
3683
3768
|
} catch (error) {
|
|
@@ -3709,6 +3794,18 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3709
3794
|
dirtyReviews.add(reviewId);
|
|
3710
3795
|
return true;
|
|
3711
3796
|
};
|
|
3797
|
+
/** Bumps the place of each board whose place changed. */
|
|
3798
|
+
const refreshPlaces = () => {
|
|
3799
|
+
let marked = false;
|
|
3800
|
+
for (const [id, key] of placeKeys) {
|
|
3801
|
+
const next = placeKey(id);
|
|
3802
|
+
if (next === key) continue;
|
|
3803
|
+
placeKeys.set(id, next);
|
|
3804
|
+
placeRevision.set(id, (placeRevision.get(id) ?? 0) + 1);
|
|
3805
|
+
marked = markReview(id) || marked;
|
|
3806
|
+
}
|
|
3807
|
+
return marked;
|
|
3808
|
+
};
|
|
3712
3809
|
const markLists = () => {
|
|
3713
3810
|
dirtyList ||= lists;
|
|
3714
3811
|
return lists;
|
|
@@ -3721,7 +3818,12 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3721
3818
|
notify();
|
|
3722
3819
|
}) ?? (() => {}),
|
|
3723
3820
|
store.subscribe((result) => {
|
|
3724
|
-
|
|
3821
|
+
let marked = markReview(result.reviewId);
|
|
3822
|
+
for (const from of store.linkingBoards(result.reviewId)) {
|
|
3823
|
+
linksRevision.set(from, (linksRevision.get(from) ?? 0) + 1);
|
|
3824
|
+
marked = markReview(from) || marked;
|
|
3825
|
+
}
|
|
3826
|
+
if (marked) notify();
|
|
3725
3827
|
}),
|
|
3726
3828
|
store.activity.subscribe((id) => {
|
|
3727
3829
|
if (markReview(id)) notify();
|
|
@@ -3729,11 +3831,24 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3729
3831
|
store.activity.subscribeWorking(() => {
|
|
3730
3832
|
if (markLists()) notify();
|
|
3731
3833
|
}),
|
|
3834
|
+
data?.subscribePullRequestStates(() => {
|
|
3835
|
+
let marked = markLists();
|
|
3836
|
+
marked = refreshPlaces() || marked;
|
|
3837
|
+
for (const reviewId of reviewIds) {
|
|
3838
|
+
const next = linksKey(reviewId);
|
|
3839
|
+
if (next === linksKeys.get(reviewId)) continue;
|
|
3840
|
+
linksKeys.set(reviewId, next);
|
|
3841
|
+
linksRevision.set(reviewId, (linksRevision.get(reviewId) ?? 0) + 1);
|
|
3842
|
+
marked = markReview(reviewId) || marked;
|
|
3843
|
+
}
|
|
3844
|
+
if (marked) notify();
|
|
3845
|
+
}) ?? (() => {}),
|
|
3732
3846
|
shared?.subscribe(() => {
|
|
3733
3847
|
if (markLists()) notify();
|
|
3734
3848
|
}) ?? (() => {}),
|
|
3735
3849
|
store.subscribeCatalog(() => {
|
|
3736
|
-
|
|
3850
|
+
const marked = refreshPlaces();
|
|
3851
|
+
if (markLists() || marked) notify();
|
|
3737
3852
|
})
|
|
3738
3853
|
];
|
|
3739
3854
|
return () => stops.forEach((stop) => stop());
|
|
@@ -3789,19 +3904,18 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3789
3904
|
storage: z.enum(["s3", "hosted"]).optional(),
|
|
3790
3905
|
trace: z.string().min(1).optional()
|
|
3791
3906
|
});
|
|
3792
|
-
const tracePins = (id, version) => {
|
|
3793
|
-
const
|
|
3794
|
-
|
|
3795
|
-
return pins;
|
|
3907
|
+
const tracePins = async (id, version) => {
|
|
3908
|
+
const snapshot = readReview(id, version);
|
|
3909
|
+
return snapshot.pins ?? await data.documentPins(snapshot);
|
|
3796
3910
|
};
|
|
3797
3911
|
app.get("/:id/agent-traces", async (context) => {
|
|
3798
3912
|
const query = traceQuery.parse(context.req.query());
|
|
3799
|
-
const pins = tracePins(context.req.param("id"), query.version);
|
|
3913
|
+
const pins = await tracePins(context.req.param("id"), query.version);
|
|
3800
3914
|
return context.json(await listPinnedTraces(store.repositoryPath(pins.repositoryId), pins, query.storage));
|
|
3801
3915
|
});
|
|
3802
3916
|
app.get("/:id/agent-traces/:sessionId", async (context) => {
|
|
3803
3917
|
const query = traceQuery.parse(context.req.query());
|
|
3804
|
-
const pins = tracePins(context.req.param("id"), query.version);
|
|
3918
|
+
const pins = await tracePins(context.req.param("id"), query.version);
|
|
3805
3919
|
const result = await readStoredTrace(store.repositoryPath(pins.repositoryId), context.req.param("sessionId"), query.trace, query.storage);
|
|
3806
3920
|
if (!result.ok) return context.json({
|
|
3807
3921
|
ok: false,
|
|
@@ -3844,7 +3958,16 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3844
3958
|
const input = z.strictObject({ path: z.string().min(1) }).parse(await readBoundedRequestJson(context.req.raw));
|
|
3845
3959
|
return context.json(await data.register(input.path));
|
|
3846
3960
|
});
|
|
3847
|
-
app.post("/resources", async (context) =>
|
|
3961
|
+
app.post("/resources", async (context) => {
|
|
3962
|
+
const body = await readBoundedRequestJson(context.req.raw, 8388608);
|
|
3963
|
+
const named = z.looseObject({ repositoryPath: z.string().min(1) }).safeParse(body);
|
|
3964
|
+
if (!named.success) return context.json(await data.upload(body));
|
|
3965
|
+
const { repositoryPath, ...upload } = named.data;
|
|
3966
|
+
return context.json(await data.upload({
|
|
3967
|
+
...upload,
|
|
3968
|
+
repositoryId: (await data.register(repositoryPath)).id
|
|
3969
|
+
}));
|
|
3970
|
+
});
|
|
3848
3971
|
app.get("/:id/resources/:resourceId", async (context) => {
|
|
3849
3972
|
const id = context.req.param("id");
|
|
3850
3973
|
const snapshot = readReview(id);
|
|
@@ -3951,6 +4074,11 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3951
4074
|
const { pins } = await data.resolveSource(readReview(context.req.param("id"), input.version), input.commit, queryAnchor(input));
|
|
3952
4075
|
return context.json(await data.changes(pins));
|
|
3953
4076
|
});
|
|
4077
|
+
app.get("/:id/commit/:commit", async (context) => {
|
|
4078
|
+
const repositoryId = boardRepositoryId(readReview(context.req.param("id")));
|
|
4079
|
+
if (!repositoryId) throw new ReviewInputError("This board has no repository.", 404);
|
|
4080
|
+
return context.json(await data.commitInfo(repositoryId, context.req.param("commit")));
|
|
4081
|
+
});
|
|
3954
4082
|
app.get("/:id/commits", async (context) => {
|
|
3955
4083
|
const input = readQuerySchemas.commits.parse(context.req.query());
|
|
3956
4084
|
return context.json(await data.commits((await data.resolveSource(readReview(context.req.param("id"), input.version))).pins));
|
|
@@ -3977,26 +4105,19 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3977
4105
|
});
|
|
3978
4106
|
excerpt = `## ${target.side}: ${target.path}:${target.startLine}-${target.endLine} (${source.commit})\n` + source.text.split("\n").map((line) => ` ${line}`).join("\n");
|
|
3979
4107
|
}
|
|
4108
|
+
if (target.kind === "block") excerpt = documentText(snapshot, target.blockId, true);
|
|
3980
4109
|
const diff = selection.selectedDiff;
|
|
3981
4110
|
const text = selectionMarkdown(selection, excerpt, diff ? {
|
|
3982
4111
|
base: `a/${diff.oldPath}`,
|
|
3983
4112
|
head: `b/${diff.newPath}`
|
|
3984
4113
|
} : void 0);
|
|
3985
4114
|
return [
|
|
3986
|
-
`Whiteboard: ${snapshot.title}`,
|
|
4115
|
+
`Whiteboard: ${boardTitle(snapshot.title)}`,
|
|
3987
4116
|
`Session ID: ${snapshot.reviewId}`,
|
|
3988
4117
|
`Version: ${snapshot.version}`,
|
|
3989
4118
|
...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
|
-
] : [],
|
|
4119
|
+
...selection.apiSource?.pins ? [...selection.apiSource.pins.base ? [`Selected base: ${selection.apiSource.pins.base}`] : [], `Selected head: ${selection.apiSource.pins.head}`] : [],
|
|
4120
|
+
...snapshot.pins ? [`Session base: ${snapshot.pins.base}`, `Session head: ${snapshot.pins.head}`] : [],
|
|
4000
4121
|
`Read this version with session_get({"sessionId":"${snapshot.reviewId}","version":${snapshot.version},"full":true}).`,
|
|
4001
4122
|
"",
|
|
4002
4123
|
text,
|
|
@@ -4005,9 +4126,10 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
4005
4126
|
].join("\n");
|
|
4006
4127
|
}
|
|
4007
4128
|
if (ask && data) {
|
|
4129
|
+
const projectOf = (reviewId) => isShared(reviewId) || !store.has(reviewId) ? void 0 : store.projectOf(reviewId);
|
|
4008
4130
|
const readThread = (reviewId, threadId) => {
|
|
4009
4131
|
const thread = ask.threads.get(threadId);
|
|
4010
|
-
if (!thread
|
|
4132
|
+
if (!thread) throw new ReviewInputError("This conversation has ended.", 404);
|
|
4011
4133
|
return thread;
|
|
4012
4134
|
};
|
|
4013
4135
|
/** Where a thread in a review saves itself: its Ask history, recorded
|
|
@@ -4057,7 +4179,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
4057
4179
|
const reach = await ask.threads.reach(agent, threadId);
|
|
4058
4180
|
const { reviewId } = snapshot;
|
|
4059
4181
|
return [
|
|
4060
|
-
`A reviewer is reading "${snapshot.title}" in Whiteboard and has a question about ${selection.target.kind === "review" ? "it" : "a selection"}.`,
|
|
4182
|
+
`A reviewer is reading "${boardTitle(snapshot.title)}" in Whiteboard and has a question about ${selection.target.kind === "review" ? "it" : "a selection"}.`,
|
|
4061
4183
|
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
4184
|
"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
4185
|
"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 +4230,17 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
4108
4230
|
const permissions = input.permissions ?? defaultAskPermissions(input.agent);
|
|
4109
4231
|
const initialContext = await askContext(id, input.agent, snapshot, checkout, input.selection, version);
|
|
4110
4232
|
assertWritable(reviewId);
|
|
4233
|
+
store.askHistory.markViewed(id, createdAt);
|
|
4111
4234
|
const thread = ask.threads.open({
|
|
4112
4235
|
id,
|
|
4113
4236
|
reviewId,
|
|
4114
4237
|
agent: input.agent,
|
|
4115
4238
|
...historyHooks({
|
|
4116
4239
|
id,
|
|
4117
|
-
|
|
4240
|
+
anchor: target.kind === "text" && target.anchor ? {
|
|
4241
|
+
board: reviewId,
|
|
4242
|
+
block: target.anchor.blockId
|
|
4243
|
+
} : { board: reviewId },
|
|
4118
4244
|
agent: input.agent,
|
|
4119
4245
|
version: snapshot.version,
|
|
4120
4246
|
head: checkout.head,
|
|
@@ -4138,23 +4264,106 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
4138
4264
|
});
|
|
4139
4265
|
return context.json({ threadId: thread.id });
|
|
4140
4266
|
});
|
|
4267
|
+
/** Every project's threads for the sidebar: who runs them, where they
|
|
4268
|
+
* started and live, and which boards they changed. */
|
|
4269
|
+
const projectThreads = (reviewId) => {
|
|
4270
|
+
const entries = store.askHistory.listAll();
|
|
4271
|
+
const ids = entries.map((entry) => entry.id);
|
|
4272
|
+
const edited = store.boardsChangedByThreads(ids);
|
|
4273
|
+
const boards = /* @__PURE__ */ new Map();
|
|
4274
|
+
const board = (id) => {
|
|
4275
|
+
let found = boards.get(id);
|
|
4276
|
+
if (!found) {
|
|
4277
|
+
found = store.has(id) ? store.boardFace(id) : {
|
|
4278
|
+
reviewId: id,
|
|
4279
|
+
icon: boardIconKind(void 0)
|
|
4280
|
+
};
|
|
4281
|
+
boards.set(id, found);
|
|
4282
|
+
}
|
|
4283
|
+
return found;
|
|
4284
|
+
};
|
|
4285
|
+
const project = projectOf(reviewId);
|
|
4286
|
+
return {
|
|
4287
|
+
board: reviewId,
|
|
4288
|
+
...project && { project },
|
|
4289
|
+
threads: entries.map((entry) => {
|
|
4290
|
+
const anchor = entry.anchor ?? { board: reviewId };
|
|
4291
|
+
const status = ask.threads.get(entry.id)?.status;
|
|
4292
|
+
const { viewedAt } = entry;
|
|
4293
|
+
const unread = status !== "starting" && status !== "running" && status !== "waiting" && viewedAt !== void 0 && entry.updatedAt > viewedAt;
|
|
4294
|
+
return {
|
|
4295
|
+
id: entry.id,
|
|
4296
|
+
...entry.projectId && { projectId: entry.projectId },
|
|
4297
|
+
...store.has(anchor.board) && { place: store.boardPlace(anchor.board) },
|
|
4298
|
+
agent: entry.agent,
|
|
4299
|
+
agentName: askAgents[entry.agent].name,
|
|
4300
|
+
opening: entry.question ?? entry.title,
|
|
4301
|
+
pinned: entry.selection.target.kind === "text" && entry.selection.target.anchor !== void 0,
|
|
4302
|
+
...status && { status },
|
|
4303
|
+
...unread && { unread },
|
|
4304
|
+
updatedAt: entry.updatedAt,
|
|
4305
|
+
anchor: {
|
|
4306
|
+
board: board(anchor.board),
|
|
4307
|
+
...anchor.block && { block: anchor.block }
|
|
4308
|
+
},
|
|
4309
|
+
edited: edited.get(entry.id) ?? []
|
|
4310
|
+
};
|
|
4311
|
+
})
|
|
4312
|
+
};
|
|
4313
|
+
};
|
|
4141
4314
|
app.get("/:id/ask/threads", (context) => {
|
|
4142
4315
|
const reviewId = context.req.param("id");
|
|
4143
4316
|
readReview(reviewId);
|
|
4144
|
-
|
|
4317
|
+
if (context.req.query("watch") !== void 0) {
|
|
4318
|
+
let sent = "";
|
|
4319
|
+
return watch(() => {
|
|
4320
|
+
const line = JSON.stringify(projectThreads(reviewId));
|
|
4321
|
+
if (line === sent) return [];
|
|
4322
|
+
sent = line;
|
|
4323
|
+
return [line];
|
|
4324
|
+
}, (notify) => {
|
|
4325
|
+
let timer;
|
|
4326
|
+
const later = () => {
|
|
4327
|
+
timer ??= setTimeout(() => {
|
|
4328
|
+
timer = void 0;
|
|
4329
|
+
notify();
|
|
4330
|
+
}, 100);
|
|
4331
|
+
};
|
|
4332
|
+
const stops = [
|
|
4333
|
+
store.askHistory.subscribe(later),
|
|
4334
|
+
ask.threads.subscribeStatus(later),
|
|
4335
|
+
store.subscribe((change) => {
|
|
4336
|
+
if (change.deleted || change.lifecycle || change.titleChanged || change.placeChanged || change.threadId || change.external) later();
|
|
4337
|
+
})
|
|
4338
|
+
];
|
|
4339
|
+
return () => {
|
|
4340
|
+
clearTimeout(timer);
|
|
4341
|
+
stops.forEach((stop) => stop());
|
|
4342
|
+
};
|
|
4343
|
+
});
|
|
4344
|
+
}
|
|
4345
|
+
return context.json({
|
|
4346
|
+
threads: store.askHistory.list(reviewId, projectOf(reviewId)),
|
|
4347
|
+
board: reviewId
|
|
4348
|
+
});
|
|
4145
4349
|
});
|
|
4146
4350
|
app.post("/:id/ask/watch", reviewAccess("read"), async (context) => {
|
|
4147
4351
|
const reviewId = context.req.param("id");
|
|
4148
4352
|
readReview(reviewId);
|
|
4149
4353
|
const { threads } = askWatchSchema.parse(await readBoundedRequestJson(context.req.raw));
|
|
4150
4354
|
return watchAskThreads(new Map(threads.map((threadId) => {
|
|
4151
|
-
|
|
4152
|
-
return [threadId, thread?.reviewId === reviewId ? thread : void 0];
|
|
4355
|
+
return [threadId, ask.threads.get(threadId)];
|
|
4153
4356
|
})));
|
|
4154
4357
|
});
|
|
4358
|
+
app.get("/:id/ask/:threadId/boards", (context) => {
|
|
4359
|
+
const threadId = context.req.param("threadId");
|
|
4360
|
+
const anchor = store.askHistory.get(threadId)?.anchor.board ?? ask.threads.get(threadId)?.reviewId;
|
|
4361
|
+
if (remoteCaller(context) || !anchor) throw new ReviewInputError("This conversation was not found.", 404);
|
|
4362
|
+
return context.json({ boards: store.boardsChangedBy(threadId) });
|
|
4363
|
+
});
|
|
4155
4364
|
app.get("/:id/ask/:threadId", (context) => {
|
|
4156
4365
|
const record = store.askHistory.get(context.req.param("threadId"));
|
|
4157
|
-
if (record
|
|
4366
|
+
if (!record) throw new ReviewInputError("This conversation was not found.", 404);
|
|
4158
4367
|
const target = record.selection.target;
|
|
4159
4368
|
return context.json({
|
|
4160
4369
|
id: record.id,
|
|
@@ -4176,18 +4385,19 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
4176
4385
|
app.post("/:id/ask/:threadId/open", reviewAccess("edit"), async (context) => {
|
|
4177
4386
|
const reviewId = context.req.param("id");
|
|
4178
4387
|
const threadId = context.req.param("threadId");
|
|
4179
|
-
if (ask.threads.get(threadId)
|
|
4388
|
+
if (ask.threads.get(threadId)) return context.json({ threadId });
|
|
4180
4389
|
const record = store.askHistory.get(threadId);
|
|
4181
4390
|
const { picks } = askOpenSchema.parse(await readBoundedRequestJson(context.req.raw, void 0, {}));
|
|
4182
|
-
if (record
|
|
4183
|
-
const
|
|
4391
|
+
if (!record) throw new ReviewInputError("This conversation was not found.", 404);
|
|
4392
|
+
const anchor = record.anchor.board;
|
|
4393
|
+
const snapshot = readReview(anchor, record.version);
|
|
4184
4394
|
const checkout = await data.agentCheckout(snapshot);
|
|
4185
4395
|
const target = record.selection.target;
|
|
4186
4396
|
const initialContext = await askContext(record.id, record.agent, snapshot, checkout, record.selection, record.version);
|
|
4187
4397
|
assertWritable(reviewId);
|
|
4188
4398
|
ask.threads.open({
|
|
4189
4399
|
id: record.id,
|
|
4190
|
-
reviewId,
|
|
4400
|
+
reviewId: anchor,
|
|
4191
4401
|
agent: record.agent,
|
|
4192
4402
|
cwd: checkout.rootPath,
|
|
4193
4403
|
head: checkout.head,
|
|
@@ -4263,27 +4473,91 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
4263
4473
|
await readThread(context.req.param("id"), context.req.param("threadId")).cancel();
|
|
4264
4474
|
return context.json({ ok: true });
|
|
4265
4475
|
});
|
|
4476
|
+
app.post("/:id/ask/:threadId/viewed", (context) => {
|
|
4477
|
+
const threadId = context.req.param("threadId");
|
|
4478
|
+
if (!ask.threads.get(threadId) && !store.askHistory.get(threadId)) throw new ReviewInputError("This conversation was not found.", 404);
|
|
4479
|
+
store.askHistory.markViewed(threadId);
|
|
4480
|
+
return context.json({ ok: true });
|
|
4481
|
+
});
|
|
4266
4482
|
app.post("/:id/ask/:threadId/close", reviewAccess("read"), (context) => {
|
|
4267
4483
|
readThread(context.req.param("id"), context.req.param("threadId"));
|
|
4268
4484
|
ask.threads.close(context.req.param("threadId"), !readReview(context.req.param("id")).trashedAt);
|
|
4269
4485
|
return context.json({ ok: true });
|
|
4270
4486
|
});
|
|
4271
4487
|
app.delete("/:id/ask/:threadId", reviewAccess("edit"), (context) => {
|
|
4272
|
-
|
|
4488
|
+
context.req.param("id");
|
|
4273
4489
|
const threadId = context.req.param("threadId");
|
|
4274
|
-
if (store.askHistory.get(threadId)
|
|
4275
|
-
if (ask.threads.get(threadId)
|
|
4490
|
+
if (!store.askHistory.get(threadId)) throw new ReviewInputError("This conversation was not found.", 404);
|
|
4491
|
+
if (ask.threads.get(threadId)) ask.threads.close(threadId);
|
|
4276
4492
|
store.askHistory.delete(threadId);
|
|
4277
4493
|
return context.json({ ok: true });
|
|
4278
4494
|
});
|
|
4279
4495
|
}
|
|
4496
|
+
app.get("/:id/place", (context) => {
|
|
4497
|
+
const id = context.req.param("id");
|
|
4498
|
+
if (!isShared(id)) store.assertExists(id);
|
|
4499
|
+
return context.json(placeOf(id));
|
|
4500
|
+
});
|
|
4280
4501
|
app.get("/:id/stack", async (context) => {
|
|
4281
4502
|
const query = readQuerySchemas.get.parse(context.req.query());
|
|
4282
4503
|
const id = context.req.param("id");
|
|
4283
4504
|
const snapshot = readReview(id, query.version);
|
|
4284
4505
|
if (isShared(id) || !snapshot.pins) return context.json({ layers: [] });
|
|
4285
|
-
const
|
|
4286
|
-
|
|
4506
|
+
const repositoryPath = store.repositories().find((repository) => repository.id === snapshot.pins.repositoryId)?.path;
|
|
4507
|
+
const layers = await resolveReviewStackLayers(snapshot, store.list(), { repositoryPath });
|
|
4508
|
+
const trunk = layers.length > 1 && repositoryPath ? await defaultBranch(repositoryPath).catch(() => null) : null;
|
|
4509
|
+
return context.json({
|
|
4510
|
+
layers: layers.map((layer) => {
|
|
4511
|
+
const state = data?.pullRequestState(layer.pullRequestUrl ?? void 0);
|
|
4512
|
+
return state ? {
|
|
4513
|
+
...layer,
|
|
4514
|
+
pullRequestState: state
|
|
4515
|
+
} : layer;
|
|
4516
|
+
}),
|
|
4517
|
+
...trunk && { base: trunk.ref.replace(/^origin\//, "") }
|
|
4518
|
+
});
|
|
4519
|
+
});
|
|
4520
|
+
app.get("/:id/project", (context) => {
|
|
4521
|
+
const id = context.req.param("id");
|
|
4522
|
+
if (isShared(id)) return context.json({
|
|
4523
|
+
project: null,
|
|
4524
|
+
boards: []
|
|
4525
|
+
});
|
|
4526
|
+
const project = store.summary(id)?.project ?? null;
|
|
4527
|
+
return context.json({
|
|
4528
|
+
project,
|
|
4529
|
+
boards: project ? store.listProject(project.id).filter((board) => !board.trashedAt).map(withPullRequestState) : []
|
|
4530
|
+
});
|
|
4531
|
+
});
|
|
4532
|
+
app.get("/:id/links", (context) => {
|
|
4533
|
+
const id = context.req.param("id");
|
|
4534
|
+
if (isShared(id)) return context.json({
|
|
4535
|
+
outgoing: boardLinks(id, readReview(id).document).map(({ from, to }) => ({
|
|
4536
|
+
from,
|
|
4537
|
+
to,
|
|
4538
|
+
missing: true,
|
|
4539
|
+
elsewhere: true
|
|
4540
|
+
})),
|
|
4541
|
+
backlinks: []
|
|
4542
|
+
});
|
|
4543
|
+
const { version } = readQuerySchemas.get.pick({ version: true }).parse({ version: context.req.query("version") });
|
|
4544
|
+
const { outgoing, backlinks } = store.links(id, version);
|
|
4545
|
+
return context.json({
|
|
4546
|
+
outgoing: outgoing.map(withPullRequestState),
|
|
4547
|
+
backlinks: backlinks.map(withPullRequestState)
|
|
4548
|
+
});
|
|
4549
|
+
});
|
|
4550
|
+
app.get("/:id/links/peek", (context) => {
|
|
4551
|
+
const id = context.req.param("id");
|
|
4552
|
+
if (isShared(id)) return context.json({});
|
|
4553
|
+
const { version } = readQuerySchemas.get.pick({ version: true }).parse({ version: context.req.query("version") });
|
|
4554
|
+
const board = context.req.query("board");
|
|
4555
|
+
const block = context.req.query("block");
|
|
4556
|
+
if (!board) throw new ReviewInputError("Name the linked board.", 400);
|
|
4557
|
+
return context.json(store.linkPeek(id, {
|
|
4558
|
+
board,
|
|
4559
|
+
...block && { block }
|
|
4560
|
+
}, version));
|
|
4287
4561
|
});
|
|
4288
4562
|
app.get("/:id/history", (context) => {
|
|
4289
4563
|
const id = context.req.param("id");
|
|
@@ -4292,14 +4566,39 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
4292
4566
|
return context.json([{
|
|
4293
4567
|
version: snapshot.version,
|
|
4294
4568
|
title: snapshot.title,
|
|
4295
|
-
createdAt: snapshot.createdAt
|
|
4569
|
+
createdAt: snapshot.createdAt,
|
|
4570
|
+
author: "agent"
|
|
4296
4571
|
}]);
|
|
4297
4572
|
});
|
|
4573
|
+
/** A board's relations as session_list reports them: only those it has. */
|
|
4574
|
+
const listedRelations = ({ parentId, children, dependsOn }) => {
|
|
4575
|
+
const listed = {};
|
|
4576
|
+
if (parentId) listed.parentId = parentId;
|
|
4577
|
+
if (children.length) listed.children = children;
|
|
4578
|
+
if (dependsOn.length) listed.dependsOn = dependsOn;
|
|
4579
|
+
return listed;
|
|
4580
|
+
};
|
|
4581
|
+
/** A board's relations, each board named by its title. */
|
|
4582
|
+
const relatedBoards = (id) => {
|
|
4583
|
+
const { parentId, children, dependsOn } = store.relations(id);
|
|
4584
|
+
const named = (board) => ({
|
|
4585
|
+
id: board,
|
|
4586
|
+
title: store.read(board).title
|
|
4587
|
+
});
|
|
4588
|
+
return {
|
|
4589
|
+
...parentId && { parent: named(parentId) },
|
|
4590
|
+
children: children.map(named),
|
|
4591
|
+
dependsOn: dependsOn.map(named)
|
|
4592
|
+
};
|
|
4593
|
+
};
|
|
4298
4594
|
app.get("/:id/inspect", (context) => {
|
|
4299
4595
|
const query = inspectQuerySchema.parse(context.req.query());
|
|
4300
4596
|
const id = context.req.param("id");
|
|
4301
4597
|
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) :
|
|
4598
|
+
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) : {
|
|
4599
|
+
...query.full ? snapshot : inspectSnapshot(snapshot),
|
|
4600
|
+
...!isShared(id) && listedRelations(store.relations(id))
|
|
4601
|
+
});
|
|
4303
4602
|
});
|
|
4304
4603
|
app.get("/:id", async (context) => {
|
|
4305
4604
|
const query = readQuerySchemas.get.parse(context.req.query());
|
|
@@ -4326,17 +4625,13 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
4326
4625
|
};
|
|
4327
4626
|
app.post("/commands", async (context) => {
|
|
4328
4627
|
const { command: body, open: requestedOpen } = takeCreateOpen(await readBoundedRequestJson(context.req.raw));
|
|
4329
|
-
const
|
|
4628
|
+
const register = async (path) => data ? data.register(path) : store.registerRepository(path);
|
|
4629
|
+
const request = withBoardRepository(await locateRepositories(await pullRequestTargets(body, register, (url, repositoryId) => {
|
|
4330
4630
|
if (!data) throw new ReviewInputError("Repositories are unavailable.");
|
|
4331
|
-
return data.
|
|
4332
|
-
});
|
|
4631
|
+
return data.resolvePullRequest(url, { id: repositoryId });
|
|
4632
|
+
}, (id) => store.read(id)), register), (id) => store.has(id) ? store.read(id) : void 0);
|
|
4333
4633
|
const input = commandSchema.parse(request);
|
|
4334
4634
|
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
4635
|
if (command.success && command.data.operation.reviewId?.startsWith("shared-")) {
|
|
4341
4636
|
const parsed = commandSchema.parse(input);
|
|
4342
4637
|
if (shared && parsed.operation.type === "delete") {
|
|
@@ -4351,8 +4646,6 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
4351
4646
|
if (shared && (parsed.operation.type === "attention" || parsed.operation.type === "lifecycle")) {
|
|
4352
4647
|
if (parsed.operation.type === "lifecycle") await shared.setLifecycle(parsed.operation.reviewId, parsed.operation.state);
|
|
4353
4648
|
else await shared.setAttention(parsed.operation.reviewId);
|
|
4354
|
-
const reviewId = parsed.operation.reviewId;
|
|
4355
|
-
if (shared.get(reviewId).snapshot.trashedAt) ask?.threads.closeDeletedReviews((id) => id === reviewId);
|
|
4356
4649
|
return context.json({
|
|
4357
4650
|
reviewId: parsed.operation.reviewId,
|
|
4358
4651
|
version: shared.get(parsed.operation.reviewId).snapshot.version,
|
|
@@ -4361,8 +4654,12 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
4361
4654
|
}
|
|
4362
4655
|
throw new ReviewInputError("Shared reviews are read-only.", 409);
|
|
4363
4656
|
}
|
|
4364
|
-
const
|
|
4365
|
-
|
|
4657
|
+
const threadId = remoteCaller(context) ? void 0 : context.req.header("x-review-thread")?.trim() || void 0;
|
|
4658
|
+
const author = !remoteCaller(context) && context.req.header("x-review-person") !== void 0 ? { kind: "person" } : {
|
|
4659
|
+
kind: "agent",
|
|
4660
|
+
...threadId && { threadId }
|
|
4661
|
+
};
|
|
4662
|
+
const result = await store.execute(input, void 0, author);
|
|
4366
4663
|
if (input.operation.type === "edit" && data && !result.deleted) {
|
|
4367
4664
|
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);
|
|
4368
4665
|
return context.json({
|
|
@@ -4382,7 +4679,6 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
4382
4679
|
if (input.operation.type !== "create") return context.json(result);
|
|
4383
4680
|
if (result.created !== false) hooks.onReviewCreated?.({
|
|
4384
4681
|
reviewId: result.reviewId,
|
|
4385
|
-
kind: input.operation.kind === "scratchpad" ? "scratchpad" : "review",
|
|
4386
4682
|
blocks: store.read(result.reviewId).document.length,
|
|
4387
4683
|
...reviewRequestOrigin(context.req.raw.headers)
|
|
4388
4684
|
});
|
|
@@ -4446,11 +4742,102 @@ async function locateRepositories(body, register) {
|
|
|
4446
4742
|
}
|
|
4447
4743
|
};
|
|
4448
4744
|
}
|
|
4745
|
+
/** Turns an agent's pullRequest target into what the store takes: a create
|
|
4746
|
+
* from pullRequestUrl, or for set_target the PR's resolved commits. */
|
|
4747
|
+
async function pullRequestTargets(body, register, resolve, board) {
|
|
4748
|
+
const target = z.strictObject({
|
|
4749
|
+
kind: z.literal("pullRequest"),
|
|
4750
|
+
repositoryPath: z.string().min(1),
|
|
4751
|
+
pullRequest: z.union([z.string().min(1), z.number().int().positive()])
|
|
4752
|
+
});
|
|
4753
|
+
const located = z.looseObject({ operation: z.discriminatedUnion("type", [z.looseObject({
|
|
4754
|
+
type: z.literal("create"),
|
|
4755
|
+
target
|
|
4756
|
+
}), z.strictObject({
|
|
4757
|
+
type: z.literal("set_target"),
|
|
4758
|
+
reviewId: z.string().min(1),
|
|
4759
|
+
target
|
|
4760
|
+
})]) }).safeParse(body);
|
|
4761
|
+
if (!located.success) return body;
|
|
4762
|
+
const { target: pullRequest, ...operation } = located.data.operation;
|
|
4763
|
+
if (operation.type === "set_target") board(operation.reviewId);
|
|
4764
|
+
const { id: repositoryId } = await register(pullRequest.repositoryPath);
|
|
4765
|
+
const url = await pullRequestAt(pullRequest.repositoryPath, pullRequest.pullRequest);
|
|
4766
|
+
if (operation.type === "create") return {
|
|
4767
|
+
...located.data,
|
|
4768
|
+
operation: {
|
|
4769
|
+
...operation,
|
|
4770
|
+
pullRequestUrl: url,
|
|
4771
|
+
repositoryId
|
|
4772
|
+
}
|
|
4773
|
+
};
|
|
4774
|
+
return {
|
|
4775
|
+
...located.data,
|
|
4776
|
+
operation: {
|
|
4777
|
+
...operation,
|
|
4778
|
+
target: (await resolve(url, repositoryId)).target,
|
|
4779
|
+
pullRequestUrl: url
|
|
4780
|
+
}
|
|
4781
|
+
};
|
|
4782
|
+
}
|
|
4783
|
+
/** A PR's URL: as given, or its number on the checkout's GitHub repository. */
|
|
4784
|
+
async function pullRequestAt(repositoryPath, pullRequest) {
|
|
4785
|
+
if (isStringValue(pullRequest) && !/^\d+$/.test(pullRequest)) return pullRequest;
|
|
4786
|
+
const slug = (await resolveRepoContext(repositoryPath))?.githubSlug;
|
|
4787
|
+
if (!slug) throw new ReviewInputError("This checkout has no GitHub remote to find the PR number in; pass the PR's URL.");
|
|
4788
|
+
return `https://github.com/${slug}/pull/${pullRequest}`;
|
|
4789
|
+
}
|
|
4790
|
+
/** A pins object as an edit spells it: commits, with no other fields. A
|
|
4791
|
+
* diagram may key its own data `pins`, and that is left alone. */
|
|
4792
|
+
/** Pins as an agent writes them: the board's repository is the host's to
|
|
4793
|
+
* add. */
|
|
4794
|
+
const agentPinsSchema = z.strictObject({
|
|
4795
|
+
head: z.string().min(1),
|
|
4796
|
+
base: z.string().min(1).optional()
|
|
4797
|
+
});
|
|
4798
|
+
/** An object an agent meant as pins: one naming a head, base or repository.
|
|
4799
|
+
* Content may have a field called `pins` that is nothing of the kind. */
|
|
4800
|
+
const meantAsPins = (value) => isJsonObject(value) && [
|
|
4801
|
+
"head",
|
|
4802
|
+
"base",
|
|
4803
|
+
"repositoryId"
|
|
4804
|
+
].some((key) => key in value);
|
|
4805
|
+
/** Sets every pins object in an edit to the board's own repository. */
|
|
4806
|
+
function withBoardRepository(body, board) {
|
|
4807
|
+
const located = z.looseObject({ operation: z.looseObject({
|
|
4808
|
+
type: z.enum(["edit", "lens_edit"]),
|
|
4809
|
+
reviewId: z.string(),
|
|
4810
|
+
edit: z.json()
|
|
4811
|
+
}) }).safeParse(body);
|
|
4812
|
+
if (!located.success) return body;
|
|
4813
|
+
const snapshot = board(located.data.operation.reviewId);
|
|
4814
|
+
const repositoryId = snapshot ? boardRepositoryId(snapshot) : void 0;
|
|
4815
|
+
const edit = JSON.parse(JSON.stringify(located.data.operation.edit), (key, value) => {
|
|
4816
|
+
if (key !== "pins" || !meantAsPins(value)) return value;
|
|
4817
|
+
const { repositoryId: _ignored, ...written } = value;
|
|
4818
|
+
const pins = agentPinsSchema.safeParse(written);
|
|
4819
|
+
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("; ")}.`);
|
|
4820
|
+
if (!repositoryId) throw new ReviewInputError("This whiteboard has no repository for its pins to name commits of.");
|
|
4821
|
+
return {
|
|
4822
|
+
...pins.data,
|
|
4823
|
+
repositoryId
|
|
4824
|
+
};
|
|
4825
|
+
});
|
|
4826
|
+
return {
|
|
4827
|
+
...located.data,
|
|
4828
|
+
operation: {
|
|
4829
|
+
...located.data.operation,
|
|
4830
|
+
edit
|
|
4831
|
+
}
|
|
4832
|
+
};
|
|
4833
|
+
}
|
|
4449
4834
|
function watch(lines, subscribe) {
|
|
4450
4835
|
let stop = () => {};
|
|
4836
|
+
let missed = false;
|
|
4451
4837
|
const encoder = new TextEncoder();
|
|
4452
4838
|
const send = (controller) => {
|
|
4453
|
-
|
|
4839
|
+
missed = controller.desiredSize === null || controller.desiredSize <= 0;
|
|
4840
|
+
if (missed) return;
|
|
4454
4841
|
try {
|
|
4455
4842
|
const pending = lines();
|
|
4456
4843
|
if (pending.length) controller.enqueue(encoder.encode(pending.join("\n") + "\n"));
|
|
@@ -4472,7 +4859,9 @@ function watch(lines, subscribe) {
|
|
|
4472
4859
|
});
|
|
4473
4860
|
send(controller);
|
|
4474
4861
|
},
|
|
4475
|
-
pull
|
|
4862
|
+
pull(controller) {
|
|
4863
|
+
if (missed) send(controller);
|
|
4864
|
+
},
|
|
4476
4865
|
cancel() {
|
|
4477
4866
|
stop();
|
|
4478
4867
|
}
|
|
@@ -4614,7 +5003,7 @@ function createWhiteboardCore(input) {
|
|
|
4614
5003
|
if (result.deleted || result.lifecycle) closeDeletedThreads();
|
|
4615
5004
|
});
|
|
4616
5005
|
const stopShared = askThreads && shared?.subscribe(closeDeletedThreads);
|
|
4617
|
-
const api = createReviewApi(store, data, callbacks.open, shared, callbacks.capabilities,
|
|
5006
|
+
const api = createReviewApi(store, data, callbacks.open, shared, callbacks.capabilities, () => traceMachineEnabled(), input.status, input.hooks, askThreads && {
|
|
4618
5007
|
threads: askThreads,
|
|
4619
5008
|
agents: async () => detectAskAgents(await ask?.env)
|
|
4620
5009
|
});
|
|
@@ -4903,7 +5292,6 @@ async function serve(input) {
|
|
|
4903
5292
|
token: discovery.token,
|
|
4904
5293
|
instanceId: discovery.instanceId,
|
|
4905
5294
|
softwareMapEnabled: input.softwareMapEnabled,
|
|
4906
|
-
scratchpad: () => false,
|
|
4907
5295
|
status: () => ({
|
|
4908
5296
|
key: "headless",
|
|
4909
5297
|
home: input.stateDir
|