@dev.fast/whiteboard 0.3.4-preview.20261019.126 → 0.3.4
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-B5-HYi1D.js → account-alias-CprZojXd.js} +8 -13
- package/dist/agent-cli-C6SPSII0.js +2 -0
- package/dist/{agent-cli-CohHf6Ua.js → agent-cli-CThN7Q59.js} +10 -15
- package/dist/{agent-client-Dtc-Kn_w.js → agent-client-BOOi7EUR.js} +3 -13
- package/dist/{authoring-tools-B7KU04f7.js → authoring-tools-BNwuMnz6.js} +27 -34
- package/dist/{background-server-C03RKxKS.js → background-server-CkvxA-6O.js} +5 -5
- package/dist/{background-server-CB9C4uoc.js → background-server-DLSDogkI.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/{cli-CJaEb7zC.js → cli-R4yfa5DT.js} +1 -1
- package/dist/{cli-runner-DGDIqh3V.js → cli-runner-Cuc9FSzu.js} +26 -26
- package/dist/cli.js +3 -4
- package/dist/{client-lKVF_Elo.js → client-BN_FDxFr.js} +47 -253
- package/dist/{fuzzy-match-BmVJJm9I.js → fuzzy-match-BO9bK37N.js} +4 -6
- package/dist/{headless-host-DRtjf9WA.js → headless-host-CVBOQyRS.js} +1 -1
- package/dist/{headless-host-XhL1X4vX.js → headless-host-Cog6SpS9.js} +140 -513
- package/dist/{local-data-Cpl4ENbJ.js → local-data-D_9tvdsp.js} +323 -2419
- package/dist/{mcp-ovmiUE_x.js → mcp-D1hziANa.js} +9 -5
- package/dist/{package-paths-BWxO-UEL.js → package-paths-C--__jEv.js} +1 -1
- package/dist/profile-CYPmvBoA.js +2 -0
- package/dist/{profile-BadSkCkI.js → profile-DokFw782.js} +8 -20
- package/dist/{remote-attach-Dd-YobM6.js → remote-attach-CEJ-kvDY.js} +4 -4
- package/dist/{remote-extensions-CNXwqK0D.js → remote-extensions-Blcjr9Ze.js} +3 -3
- package/dist/{remote-extensions-VP-kfq0C.js → remote-extensions-GN8Gf_qG.js} +1 -1
- package/dist/{remote-language-server-BXk87KCr.js → remote-language-server-BTKBQ7x4.js} +5 -5
- package/dist/{remote-uninstall-DuOzsvaD.js → remote-uninstall-DW7Gdryh.js} +6 -6
- package/dist/{request-origin-B9qCPiVB.js → request-origin-Zcn_CjX4.js} +12 -45
- package/dist/{review-app-launcher-C7el2Uoy.js → review-app-launcher-CkwLclzR.js} +2 -2
- package/dist/{review-home-paths-D8AoCdd2.js → review-home-paths-j2BRAaNv.js} +17 -19
- package/dist/{review-telemetry-BsoRMC2w.js → review-telemetry-BLNwYnLV.js} +5 -5
- package/dist/runtime-CKiWhGrf.js +52 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/{s3-CsG2EWED.js → s3-DtJFWC0G.js} +1 -1
- package/dist/{s3-config-CjcLyFV5.js → s3-config-BTO12tCC.js} +2 -4
- package/dist/{s3-config-zz4GWh2T.js → s3-config-CSy3fx8H.js} +1 -1
- package/dist/server/desktop-host.js +76 -29
- package/dist/{server-discovery-CKjw3-6h.js → server-discovery-Bey8IqXH.js} +3 -3
- package/dist/sharing/index.d.ts +70 -484
- package/dist/sharing/index.js +1 -1
- package/dist/{src-WPaQn75m.js → src-BMPuxWv4.js} +73 -208
- package/dist/{src-BiWNbcF_.js → src-DsM3jO8G.js} +12 -89
- package/dist/{stored-document-migration-dFbewRTG.js → stored-document-migration-CChxtFAy.js} +269 -270
- package/dist/{thread-state-Do5LX10I.js → thread-state-5pYr6_zM.js} +2 -7
- package/dist/{tool-failure-DvI-gys2.js → tool-failure-C9MpbXJJ.js} +3 -10
- package/instructions/authoring.md +2 -3
- package/instructions/scratchpad.md +23 -0
- package/package.json +4 -4
- package/src/ask/agents.ts +1 -13
- package/src/ask/checkout-files.ts +5 -2
- package/src/ask/thread-state.ts +0 -4
- package/src/ask/thread.ts +0 -16
- package/src/ask/threads.ts +3 -24
- package/src/cli-install.ts +2 -13
- package/src/cli-runner.ts +1 -4
- package/src/cli.ts +1 -4
- package/src/diff-selection-migration.ts +34 -48
- package/src/lens-selection.ts +2 -38
- package/src/review-api/README.md +13 -52
- package/src/review-api/activity.ts +4 -1
- package/src/review-api/agent-cli.ts +1 -9
- package/src/review-api/anchor-quotes.ts +7 -14
- package/src/review-api/ask-history.ts +51 -137
- package/src/review-api/authoring-tools.ts +30 -45
- package/src/review-api/blocks/definition.ts +1 -1
- package/src/review-api/blocks/markdown.ts +2 -2
- package/src/review-api/document-headings.ts +27 -58
- package/src/review-api/document-text.ts +2 -31
- package/src/review-api/document.ts +34 -281
- package/src/review-api/github-provider.ts +13 -51
- package/src/review-api/http.ts +123 -702
- package/src/review-api/instructions.ts +19 -0
- package/src/review-api/local-data.ts +46 -310
- package/src/review-api/mcp.ts +11 -3
- package/src/review-api/onboarding.ts +1 -1
- package/src/review-api/profile.ts +6 -37
- package/src/review-api/public-tools.ts +2 -24
- package/src/review-api/pull-request.ts +49 -82
- package/src/review-api/request-origin.ts +0 -10
- package/src/review-api/review-progress.ts +27 -61
- package/src/review-api/store-schema.ts +29 -38
- package/src/review-api/store.ts +478 -2142
- package/src/review-api/worktree-events.ts +5 -3
- package/src/review-api/worktree-source.ts +7 -3
- package/src/review-api/worktree-structural.integration.ts +0 -72
- package/src/review-preferences.ts +88 -0
- package/src/review-stack.ts +23 -15
- package/src/runtime.ts +30 -4
- package/src/server/desktop-host.ts +1 -6
- package/src/server/desktop-server.ts +31 -1
- package/src/server/headless-host.ts +2 -0
- package/src/server/hono-http.ts +1 -2
- package/src/server/legacy-tutorial-cleanup.ts +3 -2
- package/src/server/review-gateway-list.ts +5 -3
- 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 +5 -1
- package/src/server/structural-comparisons.ts +0 -47
- package/src/sharing/export.ts +7 -36
- package/src/sharing/host.ts +14 -34
- package/src/sharing/import.ts +24 -156
- package/src/sharing/repository.ts +20 -128
- package/src/startup-trace.ts +3 -3
- package/src/stored-document-migration.ts +19 -110
- package/src/ui-telemetry-events.ts +11 -33
- package/dist/agent-cli-_YyMIMRY.js +0 -2
- package/dist/lens-selection-CyJapcsl.js +0 -214
- package/dist/profile-DJvkBJNQ.js +0 -2
- package/dist/runtime-Bj2tD33R.js +0 -27
- package/src/heading-anchors.ts +0 -42
- package/src/review-api/board-order.ts +0 -144
- package/src/review-api/checkout-groups.ts +0 -560
- package/src/review-api/foreign-references.ts +0 -328
- package/src/review-api/link-preview.ts +0 -113
- package/src/review-api/scratchpad-plan-upgrade.ts +0 -250
- package/src/review-api/store-migrations.ts +0 -357
package/src/review-api/http.ts
CHANGED
|
@@ -1,23 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type JsonObject,
|
|
3
|
-
type JsonValue,
|
|
4
|
-
isJsonObject,
|
|
5
|
-
isStringValue,
|
|
6
|
-
} from "@dev.fast/json";
|
|
7
|
-
import { defaultBranch, resolveRepoContextSync } from "@dev.fast/local-vcs";
|
|
1
|
+
import { type JsonObject, isJsonObject } from "@dev.fast/json";
|
|
8
2
|
import {
|
|
9
3
|
REVIEW_CLIENT_HEADER,
|
|
10
4
|
REVIEW_CLIENT_REMOTE,
|
|
11
|
-
type ReviewApiSummary,
|
|
12
|
-
type ReviewBoardPlace,
|
|
13
|
-
type ReviewProjectThreads,
|
|
14
5
|
type ReviewRemoteLanguageEnvironment,
|
|
15
6
|
type ReviewRemoteNavigatorAnswer,
|
|
16
7
|
type ReviewStreamLine,
|
|
17
8
|
type ReviewStructuralDiffEvent,
|
|
18
|
-
type ReviewThreadBoard,
|
|
19
|
-
boardIconKind,
|
|
20
|
-
boardTitle,
|
|
21
9
|
} from "@dev.fast/review-protocol";
|
|
22
10
|
import { errorMessage } from "@dev.fast/trace-core";
|
|
23
11
|
import {
|
|
@@ -63,13 +51,12 @@ import { anchorQuotes } from "./anchor-quotes.js";
|
|
|
63
51
|
import type { AskRecord } from "./ask-history.js";
|
|
64
52
|
import { authoringTools } from "./authoring-tools.js";
|
|
65
53
|
import { documentText } from "./document-text.js";
|
|
54
|
+
import { ReviewInputError } from "./document.js";
|
|
66
55
|
import {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
} from "./document.js";
|
|
72
|
-
import { instructionsQuerySchema, renderInstructions } from "./instructions.js";
|
|
56
|
+
instructionsQuerySchema,
|
|
57
|
+
renderInstructions,
|
|
58
|
+
scratchpadAvailable,
|
|
59
|
+
} from "./instructions.js";
|
|
73
60
|
import type { LocalReviewData } from "./local-data.js";
|
|
74
61
|
import { registerOnboardingRoutes } from "./onboarding.js";
|
|
75
62
|
import {
|
|
@@ -78,8 +65,6 @@ import {
|
|
|
78
65
|
readQuerySchemas,
|
|
79
66
|
} from "./read-schemas.js";
|
|
80
67
|
import {
|
|
81
|
-
REVIEW_PERSON_HEADER,
|
|
82
|
-
REVIEW_THREAD_HEADER,
|
|
83
68
|
type ReviewRequestVia,
|
|
84
69
|
reviewRequestOrigin,
|
|
85
70
|
} from "./request-origin.js";
|
|
@@ -92,9 +77,8 @@ import {
|
|
|
92
77
|
uncategorizedReport,
|
|
93
78
|
} from "./review-progress.js";
|
|
94
79
|
import {
|
|
95
|
-
type Author,
|
|
96
|
-
type PlacedBoard,
|
|
97
80
|
type ReviewStore,
|
|
81
|
+
SCRATCHPAD_ID,
|
|
98
82
|
type Snapshot,
|
|
99
83
|
commandSchema,
|
|
100
84
|
inspectSnapshot,
|
|
@@ -158,8 +142,13 @@ const askDecisionSchema = z.strictObject({
|
|
|
158
142
|
export interface AuthoringCapabilities {
|
|
159
143
|
desktopAvailable: boolean;
|
|
160
144
|
softwareMapEnabled: boolean;
|
|
145
|
+
/** Off, the host neither makes nor lists the scratchpad, and refuses its id. */
|
|
146
|
+
scratchpadEnabled: boolean;
|
|
161
147
|
}
|
|
162
148
|
|
|
149
|
+
const SCRATCHPAD_DISABLED =
|
|
150
|
+
"The scratchpad is off. Turn it on in Whiteboard Desktop Settings.";
|
|
151
|
+
|
|
163
152
|
/**
|
|
164
153
|
* What the host reports about reviews, for telemetry. `onReviewCreated` fires
|
|
165
154
|
* again for a replayed create command; consumers dedupe by review id.
|
|
@@ -167,6 +156,7 @@ export interface AuthoringCapabilities {
|
|
|
167
156
|
export interface ReviewApiHooks {
|
|
168
157
|
onReviewCreated?: (event: {
|
|
169
158
|
reviewId: string;
|
|
159
|
+
kind: "review" | "scratchpad";
|
|
170
160
|
blocks: number;
|
|
171
161
|
via: ReviewRequestVia;
|
|
172
162
|
agentKind?: ReviewSessionAgent;
|
|
@@ -174,14 +164,6 @@ export interface ReviewApiHooks {
|
|
|
174
164
|
sharing?: SharingHostEvents;
|
|
175
165
|
}
|
|
176
166
|
|
|
177
|
-
/** A person's save carries the whole board, which edits one at a time can
|
|
178
|
-
* grow past the size of a single command. */
|
|
179
|
-
const PERSON_EDIT_MAX_BYTES = 16 * 1024 * 1024;
|
|
180
|
-
|
|
181
|
-
/** A request the gateway forwarded from another machine. */
|
|
182
|
-
const remoteCaller = (context: Context) =>
|
|
183
|
-
context.req.header(REVIEW_CLIENT_HEADER) === REVIEW_CLIENT_REMOTE;
|
|
184
|
-
|
|
185
167
|
/** Both hosts mount this behind their token authentication. */
|
|
186
168
|
export function createReviewApi(
|
|
187
169
|
store: ReviewStore,
|
|
@@ -192,11 +174,14 @@ export function createReviewApi(
|
|
|
192
174
|
}) => Promise<{ softwareMapEnabled: boolean }>,
|
|
193
175
|
shared?: SharedReviewStore,
|
|
194
176
|
capabilities: () =>
|
|
195
|
-
| AuthoringCapabilities
|
|
196
|
-
| Promise<AuthoringCapabilities
|
|
177
|
+
| Omit<AuthoringCapabilities, "scratchpadEnabled">
|
|
178
|
+
| Promise<Omit<AuthoringCapabilities, "scratchpadEnabled">> = () => ({
|
|
197
179
|
desktopAvailable: Boolean(open),
|
|
198
180
|
softwareMapEnabled: false,
|
|
199
181
|
}),
|
|
182
|
+
// Synchronous because the catalog is read inside watch callbacks. The host
|
|
183
|
+
// keeps it current from its preferences file.
|
|
184
|
+
scratchpadEnabled: () => boolean = () => false,
|
|
200
185
|
// Read per request: capture can change from outside this server.
|
|
201
186
|
traceEnabled: () => Promise<boolean> = async () => false,
|
|
202
187
|
/** Which server this is, for whiteboard_status. */
|
|
@@ -267,7 +252,23 @@ export function createReviewApi(
|
|
|
267
252
|
operation: z.object({ reviewId: z.string().optional() }),
|
|
268
253
|
});
|
|
269
254
|
|
|
255
|
+
// The host that can show the scratchpad keeps it: Desktop, while the
|
|
256
|
+
// preference is on. Headless servers never make one, and a pad made earlier
|
|
257
|
+
// stays in the store while it is off.
|
|
258
|
+
const ensureScratchpad = async (id?: string) => {
|
|
259
|
+
if (open && scratchpadEnabled() && (!id || id === SCRATCHPAD_ID))
|
|
260
|
+
await store.ensureScratchpad();
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const refuseDisabledScratchpad = (id?: string) => {
|
|
264
|
+
if (id === SCRATCHPAD_ID && !scratchpadEnabled())
|
|
265
|
+
throw new ReviewInputError(SCRATCHPAD_DISABLED, 409);
|
|
266
|
+
};
|
|
267
|
+
|
|
270
268
|
const sharedGuard: MiddlewareHandler = async (context, next) => {
|
|
269
|
+
refuseDisabledScratchpad(context.req.param("id"));
|
|
270
|
+
await ensureScratchpad(context.req.param("id"));
|
|
271
|
+
|
|
271
272
|
const id = context.req.param("id");
|
|
272
273
|
|
|
273
274
|
if (!id || !isShared(id)) return next();
|
|
@@ -299,68 +300,22 @@ export function createReviewApi(
|
|
|
299
300
|
return snapshot;
|
|
300
301
|
};
|
|
301
302
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
Board extends {
|
|
305
|
-
pullRequestUrl?: string;
|
|
306
|
-
origin?: { pullRequestUrl?: string };
|
|
307
|
-
},
|
|
308
|
-
>(
|
|
309
|
-
board: Board,
|
|
310
|
-
) => {
|
|
311
|
-
const state = data?.pullRequestState(
|
|
312
|
-
board.origin?.pullRequestUrl ?? board.pullRequestUrl,
|
|
313
|
-
);
|
|
303
|
+
const catalog = () => {
|
|
304
|
+
const local = store.list();
|
|
314
305
|
|
|
315
|
-
return
|
|
306
|
+
return [
|
|
307
|
+
...(scratchpadEnabled()
|
|
308
|
+
? local
|
|
309
|
+
: local.filter((summary) => summary.kind !== "scratchpad")),
|
|
310
|
+
...(shared?.list() ?? []),
|
|
311
|
+
];
|
|
316
312
|
};
|
|
317
313
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
...(shared?.list() ?? []),
|
|
321
|
-
];
|
|
322
|
-
|
|
323
|
-
/** Where a board sits among its project's, and its PR's facts, for the
|
|
324
|
-
* board's header. */
|
|
325
|
-
const placeOf = (id: string): ReviewBoardPlace => {
|
|
326
|
-
if (isShared(id) || !store.has(id)) return { siblings: [] };
|
|
327
|
-
|
|
328
|
-
const { board, parent, siblings, children } = store.place(id);
|
|
329
|
-
const url = board.origin?.pullRequestUrl;
|
|
330
|
-
const state = data?.pullRequestState(url);
|
|
331
|
-
const baseBranch = url && data?.pullRequestBaseBranch(url);
|
|
332
|
-
const headBranch = url && data?.pullRequestHeadBranch(url);
|
|
333
|
-
|
|
334
|
-
const place = ({ reviewId, title, target }: PlacedBoard) => ({
|
|
335
|
-
reviewId,
|
|
336
|
-
title,
|
|
337
|
-
icon: boardIconKind(target),
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
const listed = (child: PlacedBoard) => {
|
|
341
|
-
const childState = data?.pullRequestState(child.origin?.pullRequestUrl);
|
|
314
|
+
app.get("/", async (context) => {
|
|
315
|
+
await ensureScratchpad();
|
|
342
316
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
...(child.origin?.pullRequestNumber && {
|
|
346
|
-
pullRequestNumber: child.origin.pullRequestNumber,
|
|
347
|
-
}),
|
|
348
|
-
...(childState && { pullRequestState: childState }),
|
|
349
|
-
};
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
return {
|
|
353
|
-
...(state && { pullRequestState: state }),
|
|
354
|
-
...(baseBranch && { baseBranch }),
|
|
355
|
-
...(headBranch && { headBranch }),
|
|
356
|
-
board: place(board),
|
|
357
|
-
...(parent && { parent: place(parent) }),
|
|
358
|
-
siblings: siblings.map(listed),
|
|
359
|
-
...(children?.length && { children: children.map(listed) }),
|
|
360
|
-
};
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
app.get("/", (context) => context.json(catalog()));
|
|
317
|
+
return context.json(catalog());
|
|
318
|
+
});
|
|
364
319
|
|
|
365
320
|
app.get("/feedback-eligibility", (context) =>
|
|
366
321
|
context.json({ eligible: store.feedbackEligible() }),
|
|
@@ -369,13 +324,20 @@ export function createReviewApi(
|
|
|
369
324
|
// Server-owned state only: asking the Desktop canvas would let a stalled
|
|
370
325
|
// renderer block tool listing and the first instructions call.
|
|
371
326
|
const instructionContext = async () => ({
|
|
327
|
+
desktopAvailable: Boolean(open),
|
|
328
|
+
scratchpadEnabled: scratchpadEnabled(),
|
|
372
329
|
traceEnabled: await traceEnabled(),
|
|
373
330
|
});
|
|
374
331
|
|
|
375
332
|
app.get("/authoring", async (context) => {
|
|
376
333
|
const instructions = await instructionContext();
|
|
377
334
|
|
|
378
|
-
return context.json(
|
|
335
|
+
return context.json(
|
|
336
|
+
authoringTools(
|
|
337
|
+
scratchpadAvailable(instructions),
|
|
338
|
+
instructions.traceEnabled,
|
|
339
|
+
),
|
|
340
|
+
);
|
|
379
341
|
});
|
|
380
342
|
app.get("/instructions", async (context) => {
|
|
381
343
|
const { topic } = instructionsQuerySchema.parse(context.req.query());
|
|
@@ -506,7 +468,10 @@ export function createReviewApi(
|
|
|
506
468
|
);
|
|
507
469
|
|
|
508
470
|
app.get("/capabilities", async (context) =>
|
|
509
|
-
context.json(
|
|
471
|
+
context.json({
|
|
472
|
+
...(await capabilities()),
|
|
473
|
+
scratchpadEnabled: scratchpadEnabled(),
|
|
474
|
+
}),
|
|
510
475
|
);
|
|
511
476
|
|
|
512
477
|
app.get("/:id/activity", (context) => {
|
|
@@ -538,27 +503,6 @@ export function createReviewApi(
|
|
|
538
503
|
);
|
|
539
504
|
},
|
|
540
505
|
);
|
|
541
|
-
// The person's canvas, never an agent tool: a save of the whole board,
|
|
542
|
-
// the block in focus, and discarding a board left untouched.
|
|
543
|
-
app.post("/:id/person-edit", async (context) =>
|
|
544
|
-
context.json(
|
|
545
|
-
await store.personEdit(
|
|
546
|
-
context.req.param("id"),
|
|
547
|
-
await readBoundedRequestJson(context.req.raw, PERSON_EDIT_MAX_BYTES),
|
|
548
|
-
),
|
|
549
|
-
),
|
|
550
|
-
);
|
|
551
|
-
app.post("/:id/focus", async (context) =>
|
|
552
|
-
context.json(
|
|
553
|
-
store.focus(
|
|
554
|
-
context.req.param("id"),
|
|
555
|
-
await readBoundedRequestJson(context.req.raw),
|
|
556
|
-
),
|
|
557
|
-
),
|
|
558
|
-
);
|
|
559
|
-
app.post("/:id/discard", async (context) =>
|
|
560
|
-
context.json(await store.discard(context.req.param("id"))),
|
|
561
|
-
);
|
|
562
506
|
app.get("/watch", (context) => {
|
|
563
507
|
let input: unknown;
|
|
564
508
|
|
|
@@ -586,36 +530,6 @@ export function createReviewApi(
|
|
|
586
530
|
let dirtyList = lists;
|
|
587
531
|
const dirtyReviews = new Set(reviewIds);
|
|
588
532
|
let sentList: string | undefined;
|
|
589
|
-
// Bumped when a board a review links to changes, so its links reload.
|
|
590
|
-
const linksRevision = new Map<string, number>();
|
|
591
|
-
|
|
592
|
-
// The PR states its links show: only a change to one of them sends the
|
|
593
|
-
// review again. GitHub answers for every PR on launch, a few at a time.
|
|
594
|
-
const linksKey = (id: string) => {
|
|
595
|
-
if (!data || isShared(id) || !store.has(id)) return "";
|
|
596
|
-
const { outgoing, backlinks } = store.links(id);
|
|
597
|
-
|
|
598
|
-
return JSON.stringify(
|
|
599
|
-
[...outgoing, ...backlinks].map(
|
|
600
|
-
(link) =>
|
|
601
|
-
link.pullRequestUrl &&
|
|
602
|
-
data.pullRequestState(link.pullRequestUrl, { seen: false }),
|
|
603
|
-
),
|
|
604
|
-
);
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
const linksKeys = new Map(
|
|
608
|
-
[...reviewIds].map((reviewId) => [reviewId, linksKey(reviewId)] as const),
|
|
609
|
-
);
|
|
610
|
-
|
|
611
|
-
// Bumped when a review's parent or siblings change, so its header
|
|
612
|
-
// reloads them.
|
|
613
|
-
const placeRevision = new Map<string, number>();
|
|
614
|
-
const placeKey = (id: string) => JSON.stringify(placeOf(id));
|
|
615
|
-
|
|
616
|
-
const placeKeys = new Map(
|
|
617
|
-
[...reviewIds].map((reviewId) => [reviewId, placeKey(reviewId)] as const),
|
|
618
|
-
);
|
|
619
533
|
|
|
620
534
|
const readReviewLine = (reviewId: string): ReviewStreamLine<Snapshot> => {
|
|
621
535
|
try {
|
|
@@ -626,8 +540,6 @@ export function createReviewApi(
|
|
|
626
540
|
...readReview(reviewId),
|
|
627
541
|
activity: store.activity.read(reviewId),
|
|
628
542
|
coverageRevision: data?.coverageRevision ?? 0,
|
|
629
|
-
linksRevision: linksRevision.get(reviewId) ?? 0,
|
|
630
|
-
placeRevision: placeRevision.get(reviewId) ?? 0,
|
|
631
543
|
},
|
|
632
544
|
};
|
|
633
545
|
} catch (error) {
|
|
@@ -672,22 +584,6 @@ export function createReviewApi(
|
|
|
672
584
|
return true;
|
|
673
585
|
};
|
|
674
586
|
|
|
675
|
-
/** Bumps the place of each board whose place changed. */
|
|
676
|
-
const refreshPlaces = () => {
|
|
677
|
-
let marked = false;
|
|
678
|
-
|
|
679
|
-
for (const [id, key] of placeKeys) {
|
|
680
|
-
const next = placeKey(id);
|
|
681
|
-
|
|
682
|
-
if (next === key) continue;
|
|
683
|
-
placeKeys.set(id, next);
|
|
684
|
-
placeRevision.set(id, (placeRevision.get(id) ?? 0) + 1);
|
|
685
|
-
marked = markReview(id) || marked;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
return marked;
|
|
689
|
-
};
|
|
690
|
-
|
|
691
587
|
const markLists = () => {
|
|
692
588
|
dirtyList ||= lists;
|
|
693
589
|
|
|
@@ -702,14 +598,7 @@ export function createReviewApi(
|
|
|
702
598
|
notify();
|
|
703
599
|
}) ?? (() => {}),
|
|
704
600
|
store.subscribe((result) => {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
for (const from of store.linkingBoards(result.reviewId)) {
|
|
708
|
-
linksRevision.set(from, (linksRevision.get(from) ?? 0) + 1);
|
|
709
|
-
marked = markReview(from) || marked;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
if (marked) notify();
|
|
601
|
+
if (markReview(result.reviewId)) notify();
|
|
713
602
|
}),
|
|
714
603
|
store.activity.subscribe((id) => {
|
|
715
604
|
if (markReview(id)) notify();
|
|
@@ -717,31 +606,11 @@ export function createReviewApi(
|
|
|
717
606
|
store.activity.subscribeWorking(() => {
|
|
718
607
|
if (markLists()) notify();
|
|
719
608
|
}),
|
|
720
|
-
// A PR's state shows on its board in the list, on links to it, and
|
|
721
|
-
// with its branches in its board's header.
|
|
722
|
-
data?.subscribePullRequestStates(() => {
|
|
723
|
-
let marked = markLists();
|
|
724
|
-
|
|
725
|
-
marked = refreshPlaces() || marked;
|
|
726
|
-
|
|
727
|
-
for (const reviewId of reviewIds) {
|
|
728
|
-
const next = linksKey(reviewId);
|
|
729
|
-
|
|
730
|
-
if (next === linksKeys.get(reviewId)) continue;
|
|
731
|
-
linksKeys.set(reviewId, next);
|
|
732
|
-
linksRevision.set(reviewId, (linksRevision.get(reviewId) ?? 0) + 1);
|
|
733
|
-
marked = markReview(reviewId) || marked;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
if (marked) notify();
|
|
737
|
-
}) ?? (() => {}),
|
|
738
609
|
shared?.subscribe(() => {
|
|
739
610
|
if (markLists()) notify();
|
|
740
611
|
}) ?? (() => {}),
|
|
741
612
|
store.subscribeCatalog(() => {
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
if (markLists() || marked) notify();
|
|
613
|
+
if (markLists()) notify();
|
|
745
614
|
}),
|
|
746
615
|
];
|
|
747
616
|
|
|
@@ -815,15 +684,21 @@ export function createReviewApi(
|
|
|
815
684
|
});
|
|
816
685
|
|
|
817
686
|
// Traces are stored beside the review's own repository.
|
|
818
|
-
const tracePins =
|
|
819
|
-
const
|
|
687
|
+
const tracePins = (id: string, version?: number) => {
|
|
688
|
+
const { pins } = readReview(id, version);
|
|
820
689
|
|
|
821
|
-
|
|
690
|
+
if (!pins)
|
|
691
|
+
throw new ReviewInputError(
|
|
692
|
+
"This document has no source pins of its own.",
|
|
693
|
+
409,
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
return pins;
|
|
822
697
|
};
|
|
823
698
|
|
|
824
699
|
app.get("/:id/agent-traces", async (context) => {
|
|
825
700
|
const query = traceQuery.parse(context.req.query());
|
|
826
|
-
const pins =
|
|
701
|
+
const pins = tracePins(context.req.param("id"), query.version);
|
|
827
702
|
|
|
828
703
|
return context.json(
|
|
829
704
|
await listPinnedTraces(
|
|
@@ -835,7 +710,7 @@ export function createReviewApi(
|
|
|
835
710
|
});
|
|
836
711
|
app.get("/:id/agent-traces/:sessionId", async (context) => {
|
|
837
712
|
const query = traceQuery.parse(context.req.query());
|
|
838
|
-
const pins =
|
|
713
|
+
const pins = tracePins(context.req.param("id"), query.version);
|
|
839
714
|
|
|
840
715
|
const result = await readStoredTrace(
|
|
841
716
|
store.repositoryPath(pins.repositoryId),
|
|
@@ -914,27 +789,13 @@ export function createReviewApi(
|
|
|
914
789
|
|
|
915
790
|
return context.json(await data!.register(input.path));
|
|
916
791
|
});
|
|
917
|
-
app.post("/resources", async (context) =>
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
const named = z
|
|
925
|
-
.looseObject({ repositoryPath: z.string().min(1) })
|
|
926
|
-
.safeParse(body);
|
|
927
|
-
|
|
928
|
-
if (!named.success) return context.json(await data!.upload(body));
|
|
929
|
-
const { repositoryPath, ...upload } = named.data;
|
|
930
|
-
|
|
931
|
-
return context.json(
|
|
932
|
-
await data!.upload({
|
|
933
|
-
...upload,
|
|
934
|
-
repositoryId: (await data!.register(repositoryPath)).id,
|
|
935
|
-
}),
|
|
936
|
-
);
|
|
937
|
-
});
|
|
792
|
+
app.post("/resources", async (context) =>
|
|
793
|
+
context.json(
|
|
794
|
+
await data!.upload(
|
|
795
|
+
await readBoundedRequestJson(context.req.raw, 8 * 1024 * 1024),
|
|
796
|
+
),
|
|
797
|
+
),
|
|
798
|
+
);
|
|
938
799
|
app.get("/:id/resources/:resourceId", async (context) => {
|
|
939
800
|
const id = context.req.param("id");
|
|
940
801
|
const snapshot = readReview(id);
|
|
@@ -1135,20 +996,6 @@ export function createReviewApi(
|
|
|
1135
996
|
|
|
1136
997
|
return context.json(await data.changes(pins));
|
|
1137
998
|
});
|
|
1138
|
-
// A commit a quote is read at, in the board's repository.
|
|
1139
|
-
app.get("/:id/commit/:commit", async (context) => {
|
|
1140
|
-
const repositoryId = boardRepositoryId(
|
|
1141
|
-
readReview(context.req.param("id")),
|
|
1142
|
-
);
|
|
1143
|
-
|
|
1144
|
-
if (!repositoryId)
|
|
1145
|
-
throw new ReviewInputError("This board has no repository.", 404);
|
|
1146
|
-
|
|
1147
|
-
return context.json(
|
|
1148
|
-
await data.commitInfo(repositoryId, context.req.param("commit")),
|
|
1149
|
-
);
|
|
1150
|
-
});
|
|
1151
|
-
|
|
1152
999
|
app.get("/:id/commits", async (context) => {
|
|
1153
1000
|
const input = readQuerySchemas.commits.parse(context.req.query());
|
|
1154
1001
|
|
|
@@ -1238,7 +1085,7 @@ export function createReviewApi(
|
|
|
1238
1085
|
);
|
|
1239
1086
|
|
|
1240
1087
|
return [
|
|
1241
|
-
`Whiteboard: ${
|
|
1088
|
+
`Whiteboard: ${snapshot.title}`,
|
|
1242
1089
|
`Session ID: ${snapshot.reviewId}`,
|
|
1243
1090
|
`Version: ${snapshot.version}`,
|
|
1244
1091
|
...(selection.apiSource?.commit
|
|
@@ -1246,6 +1093,7 @@ export function createReviewApi(
|
|
|
1246
1093
|
: []),
|
|
1247
1094
|
...(selection.apiSource?.pins
|
|
1248
1095
|
? [
|
|
1096
|
+
`Selected repository ID: ${selection.apiSource.pins.repositoryId}`,
|
|
1249
1097
|
...(selection.apiSource.pins.base
|
|
1250
1098
|
? [`Selected base: ${selection.apiSource.pins.base}`]
|
|
1251
1099
|
: []),
|
|
@@ -1254,6 +1102,7 @@ export function createReviewApi(
|
|
|
1254
1102
|
: []),
|
|
1255
1103
|
...(snapshot.pins
|
|
1256
1104
|
? [
|
|
1105
|
+
`Repository ID: ${snapshot.pins.repositoryId}`,
|
|
1257
1106
|
`Session base: ${snapshot.pins.base}`,
|
|
1258
1107
|
`Session head: ${snapshot.pins.head}`,
|
|
1259
1108
|
]
|
|
@@ -1267,15 +1116,10 @@ export function createReviewApi(
|
|
|
1267
1116
|
}
|
|
1268
1117
|
|
|
1269
1118
|
if (ask && data) {
|
|
1270
|
-
const projectOf = (reviewId: string) =>
|
|
1271
|
-
isShared(reviewId) || !store.has(reviewId)
|
|
1272
|
-
? undefined
|
|
1273
|
-
: store.projectOf(reviewId);
|
|
1274
|
-
|
|
1275
1119
|
const readThread = (reviewId: string, threadId: string) => {
|
|
1276
1120
|
const thread = ask.threads.get(threadId);
|
|
1277
1121
|
|
|
1278
|
-
if (!thread)
|
|
1122
|
+
if (!thread || thread.reviewId !== reviewId)
|
|
1279
1123
|
throw new ReviewInputError("This conversation has ended.", 404);
|
|
1280
1124
|
|
|
1281
1125
|
return thread;
|
|
@@ -1354,7 +1198,7 @@ export function createReviewApi(
|
|
|
1354
1198
|
const { reviewId } = snapshot;
|
|
1355
1199
|
|
|
1356
1200
|
return [
|
|
1357
|
-
`A reviewer is reading "${
|
|
1201
|
+
`A reviewer is reading "${snapshot.title}" in Whiteboard and has a question about ${selection.target.kind === "review" ? "it" : "a selection"}.`,
|
|
1358
1202
|
checkout.live
|
|
1359
1203
|
? "Your working directory is the repository the review describes."
|
|
1360
1204
|
: `Your working directory is a checkout of the review's head commit, ${checkout.head}. Answer from this code, not from other branches.`,
|
|
@@ -1482,9 +1326,6 @@ export function createReviewApi(
|
|
|
1482
1326
|
|
|
1483
1327
|
assertWritable(reviewId);
|
|
1484
1328
|
|
|
1485
|
-
// Asked on screen: seen from the start.
|
|
1486
|
-
store.askHistory.markViewed(id, createdAt);
|
|
1487
|
-
|
|
1488
1329
|
const thread = ask.threads.open({
|
|
1489
1330
|
id,
|
|
1490
1331
|
reviewId,
|
|
@@ -1493,10 +1334,7 @@ export function createReviewApi(
|
|
|
1493
1334
|
// replaces a session the agent could not reopen.
|
|
1494
1335
|
...historyHooks({
|
|
1495
1336
|
id,
|
|
1496
|
-
|
|
1497
|
-
target.kind === "text" && target.anchor
|
|
1498
|
-
? { board: reviewId, block: target.anchor.blockId }
|
|
1499
|
-
: { board: reviewId },
|
|
1337
|
+
reviewId,
|
|
1500
1338
|
agent: input.agent,
|
|
1501
1339
|
version: snapshot.version,
|
|
1502
1340
|
head: checkout.head,
|
|
@@ -1522,109 +1360,12 @@ export function createReviewApi(
|
|
|
1522
1360
|
return context.json({ threadId: thread.id });
|
|
1523
1361
|
});
|
|
1524
1362
|
|
|
1525
|
-
/** Every project's threads for the sidebar: who runs them, where they
|
|
1526
|
-
* started and live, and which boards they changed. */
|
|
1527
|
-
const projectThreads = (reviewId: string): ReviewProjectThreads => {
|
|
1528
|
-
const entries = store.askHistory.listAll();
|
|
1529
|
-
|
|
1530
|
-
const ids = entries.map((entry) => entry.id);
|
|
1531
|
-
const edited = store.boardsChangedByThreads(ids);
|
|
1532
|
-
const viewed = store.askHistory.viewedAt(ids);
|
|
1533
|
-
|
|
1534
|
-
const boards = new Map<string, ReviewThreadBoard>();
|
|
1535
|
-
|
|
1536
|
-
const board = (id: string) => {
|
|
1537
|
-
let found = boards.get(id);
|
|
1538
|
-
|
|
1539
|
-
if (!found) {
|
|
1540
|
-
found = store.has(id)
|
|
1541
|
-
? store.boardFace(id)
|
|
1542
|
-
: { reviewId: id, icon: boardIconKind(undefined) };
|
|
1543
|
-
boards.set(id, found);
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
return found;
|
|
1547
|
-
};
|
|
1548
|
-
|
|
1549
|
-
const project = projectOf(reviewId);
|
|
1550
|
-
|
|
1551
|
-
return {
|
|
1552
|
-
board: reviewId,
|
|
1553
|
-
...(project && { project }),
|
|
1554
|
-
threads: entries.map((entry) => {
|
|
1555
|
-
const anchor = entry.anchor ?? { board: reviewId };
|
|
1556
|
-
const status = ask.threads.get(entry.id)?.status;
|
|
1557
|
-
const viewedAt = viewed.get(entry.id);
|
|
1558
|
-
|
|
1559
|
-
// A turn finished since the reviewer last saw the thread.
|
|
1560
|
-
const unread =
|
|
1561
|
-
status !== "starting" &&
|
|
1562
|
-
status !== "running" &&
|
|
1563
|
-
status !== "waiting" &&
|
|
1564
|
-
viewedAt !== undefined &&
|
|
1565
|
-
entry.updatedAt > viewedAt;
|
|
1566
|
-
|
|
1567
|
-
return {
|
|
1568
|
-
id: entry.id,
|
|
1569
|
-
...(entry.projectId && { projectId: entry.projectId }),
|
|
1570
|
-
...(store.has(anchor.board) && {
|
|
1571
|
-
place: store.boardPlace(anchor.board),
|
|
1572
|
-
}),
|
|
1573
|
-
agent: entry.agent,
|
|
1574
|
-
agentName: askAgents[entry.agent].name,
|
|
1575
|
-
opening: entry.question ?? entry.title,
|
|
1576
|
-
pinned:
|
|
1577
|
-
entry.selection.target.kind === "text" &&
|
|
1578
|
-
entry.selection.target.anchor !== undefined,
|
|
1579
|
-
...(status && { status }),
|
|
1580
|
-
...(unread && { unread }),
|
|
1581
|
-
updatedAt: entry.updatedAt,
|
|
1582
|
-
anchor: {
|
|
1583
|
-
board: board(anchor.board),
|
|
1584
|
-
...(anchor.block && { block: anchor.block }),
|
|
1585
|
-
},
|
|
1586
|
-
edited: edited.get(entry.id) ?? [],
|
|
1587
|
-
};
|
|
1588
|
-
}),
|
|
1589
|
-
};
|
|
1590
|
-
};
|
|
1591
|
-
|
|
1592
1363
|
app.get("/:id/ask/threads", (context) => {
|
|
1593
1364
|
const reviewId = context.req.param("id");
|
|
1594
1365
|
|
|
1595
1366
|
readReview(reviewId);
|
|
1596
1367
|
|
|
1597
|
-
|
|
1598
|
-
// Sent again only when it reads differently.
|
|
1599
|
-
let sent = "";
|
|
1600
|
-
|
|
1601
|
-
return watch(
|
|
1602
|
-
() => {
|
|
1603
|
-
const line = JSON.stringify(projectThreads(reviewId));
|
|
1604
|
-
|
|
1605
|
-
if (line === sent) return [];
|
|
1606
|
-
sent = line;
|
|
1607
|
-
|
|
1608
|
-
return [line];
|
|
1609
|
-
},
|
|
1610
|
-
(notify) => {
|
|
1611
|
-
const stops = [
|
|
1612
|
-
store.askHistory.subscribe(notify),
|
|
1613
|
-
ask.threads.subscribeStatus(notify),
|
|
1614
|
-
// An edit, title or target of any board a thread may show; a
|
|
1615
|
-
// deleted board may have been a thread's anchor.
|
|
1616
|
-
store.subscribe(() => notify()),
|
|
1617
|
-
];
|
|
1618
|
-
|
|
1619
|
-
return () => stops.forEach((stop) => stop());
|
|
1620
|
-
},
|
|
1621
|
-
);
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
return context.json({
|
|
1625
|
-
threads: store.askHistory.list(reviewId, projectOf(reviewId)),
|
|
1626
|
-
board: reviewId,
|
|
1627
|
-
});
|
|
1368
|
+
return context.json({ threads: store.askHistory.list(reviewId) });
|
|
1628
1369
|
});
|
|
1629
1370
|
|
|
1630
1371
|
// The threads of every open Ask in a review, over one connection. Before
|
|
@@ -1643,32 +1384,20 @@ export function createReviewApi(
|
|
|
1643
1384
|
threads.map((threadId) => {
|
|
1644
1385
|
const thread = ask.threads.get(threadId);
|
|
1645
1386
|
|
|
1646
|
-
return [
|
|
1387
|
+
return [
|
|
1388
|
+
threadId,
|
|
1389
|
+
thread?.reviewId === reviewId ? thread : undefined,
|
|
1390
|
+
];
|
|
1647
1391
|
}),
|
|
1648
1392
|
),
|
|
1649
1393
|
);
|
|
1650
1394
|
});
|
|
1651
1395
|
|
|
1652
|
-
// The boards a thread changed: titles from any project, so only for a
|
|
1653
|
-
// local caller.
|
|
1654
|
-
app.get("/:id/ask/:threadId/boards", (context) => {
|
|
1655
|
-
const threadId = context.req.param("threadId");
|
|
1656
|
-
const record = store.askHistory.get(threadId);
|
|
1657
|
-
|
|
1658
|
-
const anchor =
|
|
1659
|
-
record?.anchor.board ?? ask.threads.get(threadId)?.reviewId;
|
|
1660
|
-
|
|
1661
|
-
if (remoteCaller(context) || !anchor)
|
|
1662
|
-
throw new ReviewInputError("This conversation was not found.", 404);
|
|
1663
|
-
|
|
1664
|
-
return context.json({ boards: store.boardsChangedBy(threadId) });
|
|
1665
|
-
});
|
|
1666
|
-
|
|
1667
1396
|
// A saved conversation, without starting its agent.
|
|
1668
1397
|
app.get("/:id/ask/:threadId", (context) => {
|
|
1669
1398
|
const record = store.askHistory.get(context.req.param("threadId"));
|
|
1670
1399
|
|
|
1671
|
-
if (
|
|
1400
|
+
if (record?.reviewId !== context.req.param("id"))
|
|
1672
1401
|
throw new ReviewInputError("This conversation was not found.", 404);
|
|
1673
1402
|
const target = record.selection.target;
|
|
1674
1403
|
|
|
@@ -1700,19 +1429,17 @@ export function createReviewApi(
|
|
|
1700
1429
|
const threadId = context.req.param("threadId");
|
|
1701
1430
|
const live = ask.threads.get(threadId);
|
|
1702
1431
|
|
|
1703
|
-
if (live) return context.json({ threadId });
|
|
1432
|
+
if (live?.reviewId === reviewId) return context.json({ threadId });
|
|
1704
1433
|
const record = store.askHistory.get(threadId);
|
|
1705
1434
|
|
|
1706
1435
|
const { picks } = askOpenSchema.parse(
|
|
1707
1436
|
await readBoundedRequestJson(context.req.raw, undefined, {}),
|
|
1708
1437
|
);
|
|
1709
1438
|
|
|
1710
|
-
if (
|
|
1439
|
+
if (record?.reviewId !== reviewId)
|
|
1711
1440
|
throw new ReviewInputError("This conversation was not found.", 404);
|
|
1712
1441
|
|
|
1713
|
-
|
|
1714
|
-
const anchor = record.anchor.board;
|
|
1715
|
-
const snapshot = readReview(anchor, record.version);
|
|
1442
|
+
const snapshot = readReview(reviewId, record.version);
|
|
1716
1443
|
const checkout = await data.agentCheckout(snapshot);
|
|
1717
1444
|
const target = record.selection.target;
|
|
1718
1445
|
|
|
@@ -1728,7 +1455,7 @@ export function createReviewApi(
|
|
|
1728
1455
|
assertWritable(reviewId);
|
|
1729
1456
|
ask.threads.open({
|
|
1730
1457
|
id: record.id,
|
|
1731
|
-
reviewId
|
|
1458
|
+
reviewId,
|
|
1732
1459
|
agent: record.agent,
|
|
1733
1460
|
cwd: checkout.rootPath,
|
|
1734
1461
|
head: checkout.head,
|
|
@@ -1920,16 +1647,6 @@ export function createReviewApi(
|
|
|
1920
1647
|
},
|
|
1921
1648
|
);
|
|
1922
1649
|
|
|
1923
|
-
app.post("/:id/ask/:threadId/viewed", (context) => {
|
|
1924
|
-
const threadId = context.req.param("threadId");
|
|
1925
|
-
|
|
1926
|
-
if (!ask.threads.get(threadId) && !store.askHistory.get(threadId))
|
|
1927
|
-
throw new ReviewInputError("This conversation was not found.", 404);
|
|
1928
|
-
store.askHistory.markViewed(threadId);
|
|
1929
|
-
|
|
1930
|
-
return context.json({ ok: true });
|
|
1931
|
-
});
|
|
1932
|
-
|
|
1933
1650
|
// Closing an Ask ends its agent; the conversation stays saved. A tab
|
|
1934
1651
|
// switched away from or a reload leaves it running, to come back to.
|
|
1935
1652
|
app.post("/:id/ask/:threadId/close", reviewAccess("read"), (context) => {
|
|
@@ -1947,29 +1664,17 @@ export function createReviewApi(
|
|
|
1947
1664
|
const reviewId = context.req.param("id");
|
|
1948
1665
|
const threadId = context.req.param("threadId");
|
|
1949
1666
|
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
if (!record)
|
|
1667
|
+
if (store.askHistory.get(threadId)?.reviewId !== reviewId)
|
|
1953
1668
|
throw new ReviewInputError("This conversation was not found.", 404);
|
|
1954
1669
|
|
|
1955
|
-
if (ask.threads.get(threadId)
|
|
1670
|
+
if (ask.threads.get(threadId)?.reviewId === reviewId)
|
|
1671
|
+
ask.threads.close(threadId);
|
|
1956
1672
|
store.askHistory.delete(threadId);
|
|
1957
1673
|
|
|
1958
1674
|
return context.json({ ok: true });
|
|
1959
1675
|
});
|
|
1960
1676
|
}
|
|
1961
1677
|
|
|
1962
|
-
app.get("/:id/place", (context) => {
|
|
1963
|
-
const id = context.req.param("id");
|
|
1964
|
-
|
|
1965
|
-
if (!isShared(id)) store.assertExists(id);
|
|
1966
|
-
|
|
1967
|
-
return context.json(placeOf(id));
|
|
1968
|
-
});
|
|
1969
|
-
|
|
1970
|
-
// The board's branch stack: its GitHub stack, else the reviews chained
|
|
1971
|
-
// through the bases they name, bottom first. Apart from its place among
|
|
1972
|
-
// boards.
|
|
1973
1678
|
app.get("/:id/stack", async (context) => {
|
|
1974
1679
|
const query = readQuerySchemas.get.parse(context.req.query());
|
|
1975
1680
|
const id = context.req.param("id");
|
|
@@ -1977,94 +1682,14 @@ export function createReviewApi(
|
|
|
1977
1682
|
|
|
1978
1683
|
if (isShared(id) || !snapshot.pins) return context.json({ layers: [] });
|
|
1979
1684
|
|
|
1980
|
-
const repositoryPath = store
|
|
1981
|
-
.repositories()
|
|
1982
|
-
.find(
|
|
1983
|
-
(repository) => repository.id === snapshot.pins!.repositoryId,
|
|
1984
|
-
)?.path;
|
|
1985
|
-
|
|
1986
1685
|
const layers = await resolveReviewStackLayers(snapshot, store.list(), {
|
|
1987
|
-
repositoryPath
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
const trunk =
|
|
1992
|
-
layers.length > 1 && repositoryPath
|
|
1993
|
-
? await defaultBranch(repositoryPath).catch(() => null)
|
|
1994
|
-
: null;
|
|
1995
|
-
|
|
1996
|
-
return context.json({
|
|
1997
|
-
layers: layers.map((layer) => {
|
|
1998
|
-
const state = data?.pullRequestState(layer.pullRequestUrl ?? undefined);
|
|
1999
|
-
|
|
2000
|
-
return state ? { ...layer, pullRequestState: state } : layer;
|
|
2001
|
-
}),
|
|
2002
|
-
...(trunk && { base: trunk.ref.replace(/^origin\//, "") }),
|
|
1686
|
+
repositoryPath: store
|
|
1687
|
+
.repositories()
|
|
1688
|
+
.find((repository) => repository.id === snapshot.pins!.repositoryId)
|
|
1689
|
+
?.path,
|
|
2003
1690
|
});
|
|
2004
|
-
});
|
|
2005
|
-
|
|
2006
|
-
// The board's project and its whiteboards, for the board's sidebar.
|
|
2007
|
-
app.get("/:id/project", (context) => {
|
|
2008
|
-
const id = context.req.param("id");
|
|
2009
|
-
|
|
2010
|
-
if (isShared(id)) return context.json({ project: null, boards: [] });
|
|
2011
|
-
|
|
2012
|
-
const project = store.summary(id)?.project ?? null;
|
|
2013
|
-
|
|
2014
|
-
return context.json({
|
|
2015
|
-
project,
|
|
2016
|
-
boards: project
|
|
2017
|
-
? store
|
|
2018
|
-
.list()
|
|
2019
|
-
.filter(
|
|
2020
|
-
(board) => board.project?.id === project.id && !board.trashedAt,
|
|
2021
|
-
)
|
|
2022
|
-
.map(withPullRequestState)
|
|
2023
|
-
: [],
|
|
2024
|
-
});
|
|
2025
|
-
});
|
|
2026
|
-
app.get("/:id/links", (context) => {
|
|
2027
|
-
const id = context.req.param("id");
|
|
2028
|
-
|
|
2029
|
-
// A shared board's links name boards on the sender's machine: shown,
|
|
2030
|
-
// but not followed.
|
|
2031
|
-
if (isShared(id))
|
|
2032
|
-
return context.json({
|
|
2033
|
-
outgoing: boardLinks(id, readReview(id).document).map(
|
|
2034
|
-
({ from, to }) => ({ from, to, missing: true, elsewhere: true }),
|
|
2035
|
-
),
|
|
2036
|
-
backlinks: [],
|
|
2037
|
-
});
|
|
2038
|
-
|
|
2039
|
-
const { version } = readQuerySchemas.get
|
|
2040
|
-
.pick({ version: true })
|
|
2041
|
-
.parse({ version: context.req.query("version") });
|
|
2042
|
-
|
|
2043
|
-
const { outgoing, backlinks } = store.links(id, version);
|
|
2044
|
-
|
|
2045
|
-
return context.json({
|
|
2046
|
-
outgoing: outgoing.map(withPullRequestState),
|
|
2047
|
-
backlinks: backlinks.map(withPullRequestState),
|
|
2048
|
-
});
|
|
2049
|
-
});
|
|
2050
|
-
// A link's hover card, read when it opens.
|
|
2051
|
-
app.get("/:id/links/peek", (context) => {
|
|
2052
|
-
const id = context.req.param("id");
|
|
2053
1691
|
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
const { version } = readQuerySchemas.get
|
|
2057
|
-
.pick({ version: true })
|
|
2058
|
-
.parse({ version: context.req.query("version") });
|
|
2059
|
-
|
|
2060
|
-
const board = context.req.query("board");
|
|
2061
|
-
const block = context.req.query("block");
|
|
2062
|
-
|
|
2063
|
-
if (!board) throw new ReviewInputError("Name the linked board.", 400);
|
|
2064
|
-
|
|
2065
|
-
return context.json(
|
|
2066
|
-
store.linkPeek(id, { board, ...(block && { block }) }, version),
|
|
2067
|
-
);
|
|
1692
|
+
return context.json({ layers });
|
|
2068
1693
|
});
|
|
2069
1694
|
|
|
2070
1695
|
app.get("/:id/history", (context) => {
|
|
@@ -2078,44 +1703,9 @@ export function createReviewApi(
|
|
|
2078
1703
|
version: snapshot.version,
|
|
2079
1704
|
title: snapshot.title,
|
|
2080
1705
|
createdAt: snapshot.createdAt,
|
|
2081
|
-
author: "agent",
|
|
2082
1706
|
},
|
|
2083
1707
|
]);
|
|
2084
1708
|
});
|
|
2085
|
-
|
|
2086
|
-
/** A board's relations as session_list reports them: only those it has. */
|
|
2087
|
-
const listedRelations = ({
|
|
2088
|
-
parentId,
|
|
2089
|
-
children,
|
|
2090
|
-
dependsOn,
|
|
2091
|
-
}: ReturnType<typeof store.relations>) => {
|
|
2092
|
-
const listed: Partial<ReturnType<typeof store.relations>> = {};
|
|
2093
|
-
|
|
2094
|
-
if (parentId) listed.parentId = parentId;
|
|
2095
|
-
|
|
2096
|
-
if (children.length) listed.children = children;
|
|
2097
|
-
|
|
2098
|
-
if (dependsOn.length) listed.dependsOn = dependsOn;
|
|
2099
|
-
|
|
2100
|
-
return listed;
|
|
2101
|
-
};
|
|
2102
|
-
|
|
2103
|
-
/** A board's relations, each board named by its title. */
|
|
2104
|
-
const relatedBoards = (id: string) => {
|
|
2105
|
-
const { parentId, children, dependsOn } = store.relations(id);
|
|
2106
|
-
|
|
2107
|
-
const named = (board: string) => ({
|
|
2108
|
-
id: board,
|
|
2109
|
-
title: store.read(board).title,
|
|
2110
|
-
});
|
|
2111
|
-
|
|
2112
|
-
return {
|
|
2113
|
-
...(parentId && { parent: named(parentId) }),
|
|
2114
|
-
children: children.map(named),
|
|
2115
|
-
dependsOn: dependsOn.map(named),
|
|
2116
|
-
};
|
|
2117
|
-
};
|
|
2118
|
-
|
|
2119
1709
|
app.get("/:id/inspect", (context) => {
|
|
2120
1710
|
const query = inspectQuerySchema.parse(context.req.query());
|
|
2121
1711
|
const id = context.req.param("id");
|
|
@@ -2123,19 +1713,12 @@ export function createReviewApi(
|
|
|
2123
1713
|
|
|
2124
1714
|
return context.json(
|
|
2125
1715
|
query.format === "text"
|
|
2126
|
-
? documentText(
|
|
2127
|
-
snapshot,
|
|
2128
|
-
query.targetId,
|
|
2129
|
-
Boolean(query.full),
|
|
2130
|
-
isShared(id) ? undefined : relatedBoards(id),
|
|
2131
|
-
)
|
|
1716
|
+
? documentText(snapshot, query.targetId, Boolean(query.full))
|
|
2132
1717
|
: query.targetId !== undefined
|
|
2133
1718
|
? inspectSnapshot(snapshot, query.targetId)
|
|
2134
|
-
:
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
...(!isShared(id) && listedRelations(store.relations(id))),
|
|
2138
|
-
},
|
|
1719
|
+
: query.full
|
|
1720
|
+
? snapshot
|
|
1721
|
+
: inspectSnapshot(snapshot),
|
|
2139
1722
|
);
|
|
2140
1723
|
});
|
|
2141
1724
|
app.get("/:id", async (context) => {
|
|
@@ -2195,32 +1778,27 @@ export function createReviewApi(
|
|
|
2195
1778
|
await readBoundedRequestJson(context.req.raw),
|
|
2196
1779
|
);
|
|
2197
1780
|
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
data ? data.register(path) : store.registerRepository(path);
|
|
2201
|
-
|
|
2202
|
-
const request = withBoardRepository(
|
|
2203
|
-
await locateRepositories(
|
|
2204
|
-
await pullRequestTargets(
|
|
2205
|
-
body,
|
|
2206
|
-
register,
|
|
2207
|
-
(url, repositoryId) => {
|
|
2208
|
-
if (!data)
|
|
2209
|
-
throw new ReviewInputError("Repositories are unavailable.");
|
|
1781
|
+
const request = await locateRepositories(body, (path) => {
|
|
1782
|
+
if (!data) throw new ReviewInputError("Repositories are unavailable.");
|
|
2210
1783
|
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
(id) => store.read(id),
|
|
2214
|
-
),
|
|
2215
|
-
register,
|
|
2216
|
-
),
|
|
2217
|
-
(id) => (store.has(id) ? store.read(id) : undefined),
|
|
2218
|
-
);
|
|
1784
|
+
return data.register(path);
|
|
1785
|
+
});
|
|
2219
1786
|
|
|
2220
1787
|
const input = commandSchema.parse(request);
|
|
2221
1788
|
|
|
2222
1789
|
const command = sharedCommandSchema.safeParse(input);
|
|
2223
1790
|
|
|
1791
|
+
if (command.success) {
|
|
1792
|
+
refuseDisabledScratchpad(command.data.operation.reviewId);
|
|
1793
|
+
await ensureScratchpad(command.data.operation.reviewId);
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
if (
|
|
1797
|
+
input.operation.type === "create" &&
|
|
1798
|
+
input.operation.kind === "scratchpad"
|
|
1799
|
+
)
|
|
1800
|
+
refuseDisabledScratchpad(SCRATCHPAD_ID);
|
|
1801
|
+
|
|
2224
1802
|
if (
|
|
2225
1803
|
command.success &&
|
|
2226
1804
|
command.data.operation.reviewId?.startsWith("shared-")
|
|
@@ -2269,23 +1847,7 @@ export function createReviewApi(
|
|
|
2269
1847
|
throw new ReviewInputError("Shared reviews are read-only.", 409);
|
|
2270
1848
|
}
|
|
2271
1849
|
|
|
2272
|
-
|
|
2273
|
-
// Desktop or the canvas, or an agent, perhaps a thread's. The store
|
|
2274
|
-
// keeps each author's rules.
|
|
2275
|
-
const threadId = remoteCaller(context)
|
|
2276
|
-
? undefined
|
|
2277
|
-
: context.req.header(REVIEW_THREAD_HEADER)?.trim() || undefined;
|
|
2278
|
-
|
|
2279
|
-
const author: Author =
|
|
2280
|
-
!remoteCaller(context) &&
|
|
2281
|
-
context.req.header(REVIEW_PERSON_HEADER) !== undefined
|
|
2282
|
-
? { kind: "person" }
|
|
2283
|
-
: {
|
|
2284
|
-
kind: "agent",
|
|
2285
|
-
...(threadId && { threadId }),
|
|
2286
|
-
};
|
|
2287
|
-
|
|
2288
|
-
const result = await store.execute(input, undefined, author);
|
|
1850
|
+
const result = await store.execute(input);
|
|
2289
1851
|
|
|
2290
1852
|
if (
|
|
2291
1853
|
result.deleted ||
|
|
@@ -2331,6 +1893,7 @@ export function createReviewApi(
|
|
|
2331
1893
|
if (result.created !== false)
|
|
2332
1894
|
hooks.onReviewCreated?.({
|
|
2333
1895
|
reviewId: result.reviewId,
|
|
1896
|
+
kind: input.operation.kind === "scratchpad" ? "scratchpad" : "review",
|
|
2334
1897
|
blocks: store.read(result.reviewId).document.length,
|
|
2335
1898
|
...reviewRequestOrigin(context.req.raw.headers),
|
|
2336
1899
|
});
|
|
@@ -2416,148 +1979,6 @@ async function locateRepositories(
|
|
|
2416
1979
|
};
|
|
2417
1980
|
}
|
|
2418
1981
|
|
|
2419
|
-
/** Turns an agent's pullRequest target into what the store takes: a create
|
|
2420
|
-
* from pullRequestUrl, or for set_target the PR's resolved commits. */
|
|
2421
|
-
async function pullRequestTargets(
|
|
2422
|
-
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Request body boundary: commandSchema parses the result.
|
|
2423
|
-
body: unknown,
|
|
2424
|
-
register: (path: string) => Promise<{ id: string }>,
|
|
2425
|
-
resolve: (
|
|
2426
|
-
url: string,
|
|
2427
|
-
repositoryId: string,
|
|
2428
|
-
) => Promise<{ target: ReviewTarget }>,
|
|
2429
|
-
board: (id: string) => void,
|
|
2430
|
-
) {
|
|
2431
|
-
const target = z.strictObject({
|
|
2432
|
-
kind: z.literal("pullRequest"),
|
|
2433
|
-
repositoryPath: z.string().min(1),
|
|
2434
|
-
pullRequest: z.union([z.string().min(1), z.number().int().positive()]),
|
|
2435
|
-
});
|
|
2436
|
-
|
|
2437
|
-
// A set_target is checked whole before anything is registered or fetched.
|
|
2438
|
-
const located = z
|
|
2439
|
-
.looseObject({
|
|
2440
|
-
operation: z.discriminatedUnion("type", [
|
|
2441
|
-
z.looseObject({ type: z.literal("create"), target }),
|
|
2442
|
-
z.strictObject({
|
|
2443
|
-
type: z.literal("set_target"),
|
|
2444
|
-
reviewId: z.string().min(1),
|
|
2445
|
-
target,
|
|
2446
|
-
}),
|
|
2447
|
-
]),
|
|
2448
|
-
})
|
|
2449
|
-
.safeParse(body);
|
|
2450
|
-
|
|
2451
|
-
if (!located.success) return body;
|
|
2452
|
-
const { target: pullRequest, ...operation } = located.data.operation;
|
|
2453
|
-
|
|
2454
|
-
if (operation.type === "set_target") board(operation.reviewId);
|
|
2455
|
-
const { id: repositoryId } = await register(pullRequest.repositoryPath);
|
|
2456
|
-
|
|
2457
|
-
const url = pullRequestAt(
|
|
2458
|
-
pullRequest.repositoryPath,
|
|
2459
|
-
pullRequest.pullRequest,
|
|
2460
|
-
);
|
|
2461
|
-
|
|
2462
|
-
if (operation.type === "create")
|
|
2463
|
-
return {
|
|
2464
|
-
...located.data,
|
|
2465
|
-
operation: { ...operation, pullRequestUrl: url, repositoryId },
|
|
2466
|
-
};
|
|
2467
|
-
|
|
2468
|
-
return {
|
|
2469
|
-
...located.data,
|
|
2470
|
-
operation: {
|
|
2471
|
-
...operation,
|
|
2472
|
-
target: (await resolve(url, repositoryId)).target,
|
|
2473
|
-
pullRequestUrl: url,
|
|
2474
|
-
},
|
|
2475
|
-
};
|
|
2476
|
-
}
|
|
2477
|
-
|
|
2478
|
-
/** A PR's URL: as given, or its number on the checkout's GitHub repository. */
|
|
2479
|
-
function pullRequestAt(repositoryPath: string, pullRequest: string | number) {
|
|
2480
|
-
if (isStringValue(pullRequest) && !/^\d+$/.test(pullRequest))
|
|
2481
|
-
return pullRequest;
|
|
2482
|
-
|
|
2483
|
-
const slug = resolveRepoContextSync(repositoryPath)?.githubSlug;
|
|
2484
|
-
|
|
2485
|
-
if (!slug)
|
|
2486
|
-
throw new ReviewInputError(
|
|
2487
|
-
"This checkout has no GitHub remote to find the PR number in; pass the PR's URL.",
|
|
2488
|
-
);
|
|
2489
|
-
|
|
2490
|
-
return `https://github.com/${slug}/pull/${pullRequest}`;
|
|
2491
|
-
}
|
|
2492
|
-
|
|
2493
|
-
/** A pins object as an edit spells it: commits, with no other fields. A
|
|
2494
|
-
* diagram may key its own data `pins`, and that is left alone. */
|
|
2495
|
-
/** Pins as an agent writes them: the board's repository is the host's to
|
|
2496
|
-
* add. */
|
|
2497
|
-
const agentPinsSchema = z.strictObject({
|
|
2498
|
-
head: z.string().min(1),
|
|
2499
|
-
base: z.string().min(1).optional(),
|
|
2500
|
-
});
|
|
2501
|
-
|
|
2502
|
-
/** An object an agent meant as pins: one naming a head, base or repository.
|
|
2503
|
-
* Content may have a field called `pins` that is nothing of the kind. */
|
|
2504
|
-
const meantAsPins = (value: JsonValue): value is JsonObject =>
|
|
2505
|
-
isJsonObject(value) &&
|
|
2506
|
-
["head", "base", "repositoryId"].some((key) => key in value);
|
|
2507
|
-
|
|
2508
|
-
/** Sets every pins object in an edit to the board's own repository. */
|
|
2509
|
-
function withBoardRepository(
|
|
2510
|
-
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Request body boundary: commandSchema parses the result.
|
|
2511
|
-
body: unknown,
|
|
2512
|
-
board: (id: string) => Snapshot | undefined,
|
|
2513
|
-
) {
|
|
2514
|
-
const located = z
|
|
2515
|
-
.looseObject({
|
|
2516
|
-
operation: z.looseObject({
|
|
2517
|
-
type: z.enum(["edit", "lens_edit"]),
|
|
2518
|
-
reviewId: z.string(),
|
|
2519
|
-
edit: z.json(),
|
|
2520
|
-
}),
|
|
2521
|
-
})
|
|
2522
|
-
.safeParse(body);
|
|
2523
|
-
|
|
2524
|
-
if (!located.success) return body;
|
|
2525
|
-
const snapshot = board(located.data.operation.reviewId);
|
|
2526
|
-
|
|
2527
|
-
const repositoryId = snapshot ? boardRepositoryId(snapshot) : undefined;
|
|
2528
|
-
|
|
2529
|
-
const edit = JSON.parse(
|
|
2530
|
-
JSON.stringify(located.data.operation.edit),
|
|
2531
|
-
(key, value: JsonValue) => {
|
|
2532
|
-
if (key !== "pins" || !meantAsPins(value)) return value;
|
|
2533
|
-
|
|
2534
|
-
// Checked as the agent wrote them, so a mistake names head or base,
|
|
2535
|
-
// never the repository the host adds.
|
|
2536
|
-
const { repositoryId: _ignored, ...written } = value;
|
|
2537
|
-
const pins = agentPinsSchema.safeParse(written);
|
|
2538
|
-
|
|
2539
|
-
if (!pins.success)
|
|
2540
|
-
throw new ReviewInputError(
|
|
2541
|
-
`Write pins as {head, base?}, each a commit: ${pins.error.issues
|
|
2542
|
-
.map((issue) => `pins.${issue.path.join(".")}: ${issue.message}`)
|
|
2543
|
-
.join("; ")}.`,
|
|
2544
|
-
);
|
|
2545
|
-
|
|
2546
|
-
if (!repositoryId)
|
|
2547
|
-
throw new ReviewInputError(
|
|
2548
|
-
"This whiteboard has no repository for its pins to name commits of.",
|
|
2549
|
-
);
|
|
2550
|
-
|
|
2551
|
-
return { ...pins.data, repositoryId };
|
|
2552
|
-
},
|
|
2553
|
-
);
|
|
2554
|
-
|
|
2555
|
-
return {
|
|
2556
|
-
...located.data,
|
|
2557
|
-
operation: { ...located.data.operation, edit },
|
|
2558
|
-
};
|
|
2559
|
-
}
|
|
2560
|
-
|
|
2561
1982
|
function watch(
|
|
2562
1983
|
lines: () => string[],
|
|
2563
1984
|
subscribe: (notify: () => void) => () => void,
|