@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
package/src/review-api/http.ts
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type JsonObject,
|
|
3
|
+
type JsonValue,
|
|
4
|
+
isJsonObject,
|
|
5
|
+
isStringValue,
|
|
6
|
+
} from "@dev.fast/json";
|
|
7
|
+
import { defaultBranch, resolveRepoContext } from "@dev.fast/local-vcs";
|
|
2
8
|
import {
|
|
3
9
|
REVIEW_CLIENT_HEADER,
|
|
4
10
|
REVIEW_CLIENT_REMOTE,
|
|
11
|
+
type ReviewApiSummary,
|
|
12
|
+
type ReviewBoardPlace,
|
|
13
|
+
type ReviewProjectThreads,
|
|
5
14
|
type ReviewRemoteLanguageEnvironment,
|
|
6
15
|
type ReviewRemoteNavigatorAnswer,
|
|
7
16
|
type ReviewStreamLine,
|
|
8
17
|
type ReviewStructuralDiffEvent,
|
|
18
|
+
type ReviewThreadBoard,
|
|
19
|
+
boardIconKind,
|
|
20
|
+
boardTitle,
|
|
9
21
|
} from "@dev.fast/review-protocol";
|
|
10
22
|
import { errorMessage } from "@dev.fast/trace-core";
|
|
11
23
|
import {
|
|
@@ -51,12 +63,13 @@ import { anchorQuotes } from "./anchor-quotes.js";
|
|
|
51
63
|
import type { AskRecord } from "./ask-history.js";
|
|
52
64
|
import { authoringTools } from "./authoring-tools.js";
|
|
53
65
|
import { documentText } from "./document-text.js";
|
|
54
|
-
import { ReviewInputError } from "./document.js";
|
|
55
66
|
import {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
ReviewInputError,
|
|
68
|
+
type ReviewTarget,
|
|
69
|
+
boardLinks,
|
|
70
|
+
boardRepositoryId,
|
|
71
|
+
} from "./document.js";
|
|
72
|
+
import { instructionsQuerySchema, renderInstructions } from "./instructions.js";
|
|
60
73
|
import type { LocalReviewData } from "./local-data.js";
|
|
61
74
|
import { registerOnboardingRoutes } from "./onboarding.js";
|
|
62
75
|
import {
|
|
@@ -65,6 +78,8 @@ import {
|
|
|
65
78
|
readQuerySchemas,
|
|
66
79
|
} from "./read-schemas.js";
|
|
67
80
|
import {
|
|
81
|
+
REVIEW_PERSON_HEADER,
|
|
82
|
+
REVIEW_THREAD_HEADER,
|
|
68
83
|
type ReviewRequestVia,
|
|
69
84
|
reviewRequestOrigin,
|
|
70
85
|
} from "./request-origin.js";
|
|
@@ -77,8 +92,9 @@ import {
|
|
|
77
92
|
uncategorizedReport,
|
|
78
93
|
} from "./review-progress.js";
|
|
79
94
|
import {
|
|
95
|
+
type Author,
|
|
96
|
+
type PlacedBoard,
|
|
80
97
|
type ReviewStore,
|
|
81
|
-
SCRATCHPAD_ID,
|
|
82
98
|
type Snapshot,
|
|
83
99
|
commandSchema,
|
|
84
100
|
inspectSnapshot,
|
|
@@ -142,13 +158,8 @@ const askDecisionSchema = z.strictObject({
|
|
|
142
158
|
export interface AuthoringCapabilities {
|
|
143
159
|
desktopAvailable: boolean;
|
|
144
160
|
softwareMapEnabled: boolean;
|
|
145
|
-
/** Off, the host neither makes nor lists the scratchpad, and refuses its id. */
|
|
146
|
-
scratchpadEnabled: boolean;
|
|
147
161
|
}
|
|
148
162
|
|
|
149
|
-
const SCRATCHPAD_DISABLED =
|
|
150
|
-
"The scratchpad is off. Turn it on in Whiteboard Desktop Settings.";
|
|
151
|
-
|
|
152
163
|
/**
|
|
153
164
|
* What the host reports about reviews, for telemetry. `onReviewCreated` fires
|
|
154
165
|
* again for a replayed create command; consumers dedupe by review id.
|
|
@@ -156,7 +167,6 @@ const SCRATCHPAD_DISABLED =
|
|
|
156
167
|
export interface ReviewApiHooks {
|
|
157
168
|
onReviewCreated?: (event: {
|
|
158
169
|
reviewId: string;
|
|
159
|
-
kind: "review" | "scratchpad";
|
|
160
170
|
blocks: number;
|
|
161
171
|
via: ReviewRequestVia;
|
|
162
172
|
agentKind?: ReviewSessionAgent;
|
|
@@ -164,6 +174,14 @@ export interface ReviewApiHooks {
|
|
|
164
174
|
sharing?: SharingHostEvents;
|
|
165
175
|
}
|
|
166
176
|
|
|
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
|
+
|
|
167
185
|
/** Both hosts mount this behind their token authentication. */
|
|
168
186
|
export function createReviewApi(
|
|
169
187
|
store: ReviewStore,
|
|
@@ -174,14 +192,11 @@ export function createReviewApi(
|
|
|
174
192
|
}) => Promise<{ softwareMapEnabled: boolean }>,
|
|
175
193
|
shared?: SharedReviewStore,
|
|
176
194
|
capabilities: () =>
|
|
177
|
-
|
|
|
178
|
-
| Promise<
|
|
195
|
+
| AuthoringCapabilities
|
|
196
|
+
| Promise<AuthoringCapabilities> = () => ({
|
|
179
197
|
desktopAvailable: Boolean(open),
|
|
180
198
|
softwareMapEnabled: false,
|
|
181
199
|
}),
|
|
182
|
-
// Synchronous because the catalog is read inside watch callbacks. The host
|
|
183
|
-
// keeps it current from its preferences file.
|
|
184
|
-
scratchpadEnabled: () => boolean = () => false,
|
|
185
200
|
// Read per request: capture can change from outside this server.
|
|
186
201
|
traceEnabled: () => Promise<boolean> = async () => false,
|
|
187
202
|
/** Which server this is, for whiteboard_status. */
|
|
@@ -252,23 +267,7 @@ export function createReviewApi(
|
|
|
252
267
|
operation: z.object({ reviewId: z.string().optional() }),
|
|
253
268
|
});
|
|
254
269
|
|
|
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
|
-
|
|
268
270
|
const sharedGuard: MiddlewareHandler = async (context, next) => {
|
|
269
|
-
refuseDisabledScratchpad(context.req.param("id"));
|
|
270
|
-
await ensureScratchpad(context.req.param("id"));
|
|
271
|
-
|
|
272
271
|
const id = context.req.param("id");
|
|
273
272
|
|
|
274
273
|
if (!id || !isShared(id)) return next();
|
|
@@ -300,22 +299,68 @@ export function createReviewApi(
|
|
|
300
299
|
return snapshot;
|
|
301
300
|
};
|
|
302
301
|
|
|
303
|
-
|
|
304
|
-
|
|
302
|
+
/** Adds a PR board's state, as this host last read it from GitHub. */
|
|
303
|
+
const withPullRequestState = <
|
|
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
|
+
);
|
|
305
314
|
|
|
306
|
-
return
|
|
307
|
-
...(scratchpadEnabled()
|
|
308
|
-
? local
|
|
309
|
-
: local.filter((summary) => summary.kind !== "scratchpad")),
|
|
310
|
-
...(shared?.list() ?? []),
|
|
311
|
-
];
|
|
315
|
+
return state ? { ...board, pullRequestState: state } : board;
|
|
312
316
|
};
|
|
313
317
|
|
|
314
|
-
|
|
315
|
-
|
|
318
|
+
const catalog = () => [
|
|
319
|
+
...store.list().map(withPullRequestState),
|
|
320
|
+
...(shared?.list() ?? []),
|
|
321
|
+
];
|
|
316
322
|
|
|
317
|
-
|
|
318
|
-
|
|
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);
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
...place(child),
|
|
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()));
|
|
319
364
|
|
|
320
365
|
app.get("/feedback-eligibility", (context) =>
|
|
321
366
|
context.json({ eligible: store.feedbackEligible() }),
|
|
@@ -324,20 +369,13 @@ export function createReviewApi(
|
|
|
324
369
|
// Server-owned state only: asking the Desktop canvas would let a stalled
|
|
325
370
|
// renderer block tool listing and the first instructions call.
|
|
326
371
|
const instructionContext = async () => ({
|
|
327
|
-
desktopAvailable: Boolean(open),
|
|
328
|
-
scratchpadEnabled: scratchpadEnabled(),
|
|
329
372
|
traceEnabled: await traceEnabled(),
|
|
330
373
|
});
|
|
331
374
|
|
|
332
375
|
app.get("/authoring", async (context) => {
|
|
333
376
|
const instructions = await instructionContext();
|
|
334
377
|
|
|
335
|
-
return context.json(
|
|
336
|
-
authoringTools(
|
|
337
|
-
scratchpadAvailable(instructions),
|
|
338
|
-
instructions.traceEnabled,
|
|
339
|
-
),
|
|
340
|
-
);
|
|
378
|
+
return context.json(authoringTools(instructions.traceEnabled));
|
|
341
379
|
});
|
|
342
380
|
app.get("/instructions", async (context) => {
|
|
343
381
|
const { topic } = instructionsQuerySchema.parse(context.req.query());
|
|
@@ -468,10 +506,7 @@ export function createReviewApi(
|
|
|
468
506
|
);
|
|
469
507
|
|
|
470
508
|
app.get("/capabilities", async (context) =>
|
|
471
|
-
context.json(
|
|
472
|
-
...(await capabilities()),
|
|
473
|
-
scratchpadEnabled: scratchpadEnabled(),
|
|
474
|
-
}),
|
|
509
|
+
context.json(await capabilities()),
|
|
475
510
|
);
|
|
476
511
|
|
|
477
512
|
app.get("/:id/activity", (context) => {
|
|
@@ -503,6 +538,27 @@ export function createReviewApi(
|
|
|
503
538
|
);
|
|
504
539
|
},
|
|
505
540
|
);
|
|
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
|
+
);
|
|
506
562
|
app.get("/watch", (context) => {
|
|
507
563
|
let input: unknown;
|
|
508
564
|
|
|
@@ -530,6 +586,36 @@ export function createReviewApi(
|
|
|
530
586
|
let dirtyList = lists;
|
|
531
587
|
const dirtyReviews = new Set(reviewIds);
|
|
532
588
|
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
|
+
);
|
|
533
619
|
|
|
534
620
|
const readReviewLine = (reviewId: string): ReviewStreamLine<Snapshot> => {
|
|
535
621
|
try {
|
|
@@ -540,6 +626,8 @@ export function createReviewApi(
|
|
|
540
626
|
...readReview(reviewId),
|
|
541
627
|
activity: store.activity.read(reviewId),
|
|
542
628
|
coverageRevision: data?.coverageRevision ?? 0,
|
|
629
|
+
linksRevision: linksRevision.get(reviewId) ?? 0,
|
|
630
|
+
placeRevision: placeRevision.get(reviewId) ?? 0,
|
|
543
631
|
},
|
|
544
632
|
};
|
|
545
633
|
} catch (error) {
|
|
@@ -584,6 +672,22 @@ export function createReviewApi(
|
|
|
584
672
|
return true;
|
|
585
673
|
};
|
|
586
674
|
|
|
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
|
+
|
|
587
691
|
const markLists = () => {
|
|
588
692
|
dirtyList ||= lists;
|
|
589
693
|
|
|
@@ -598,7 +702,14 @@ export function createReviewApi(
|
|
|
598
702
|
notify();
|
|
599
703
|
}) ?? (() => {}),
|
|
600
704
|
store.subscribe((result) => {
|
|
601
|
-
|
|
705
|
+
let marked = markReview(result.reviewId);
|
|
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();
|
|
602
713
|
}),
|
|
603
714
|
store.activity.subscribe((id) => {
|
|
604
715
|
if (markReview(id)) notify();
|
|
@@ -606,11 +717,31 @@ export function createReviewApi(
|
|
|
606
717
|
store.activity.subscribeWorking(() => {
|
|
607
718
|
if (markLists()) notify();
|
|
608
719
|
}),
|
|
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
|
+
}) ?? (() => {}),
|
|
609
738
|
shared?.subscribe(() => {
|
|
610
739
|
if (markLists()) notify();
|
|
611
740
|
}) ?? (() => {}),
|
|
612
741
|
store.subscribeCatalog(() => {
|
|
613
|
-
|
|
742
|
+
const marked = refreshPlaces();
|
|
743
|
+
|
|
744
|
+
if (markLists() || marked) notify();
|
|
614
745
|
}),
|
|
615
746
|
];
|
|
616
747
|
|
|
@@ -684,21 +815,15 @@ export function createReviewApi(
|
|
|
684
815
|
});
|
|
685
816
|
|
|
686
817
|
// Traces are stored beside the review's own repository.
|
|
687
|
-
const tracePins = (id: string, version?: number) => {
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
if (!pins)
|
|
691
|
-
throw new ReviewInputError(
|
|
692
|
-
"This document has no source pins of its own.",
|
|
693
|
-
409,
|
|
694
|
-
);
|
|
818
|
+
const tracePins = async (id: string, version?: number) => {
|
|
819
|
+
const snapshot = readReview(id, version);
|
|
695
820
|
|
|
696
|
-
return pins;
|
|
821
|
+
return snapshot.pins ?? (await data.documentPins(snapshot));
|
|
697
822
|
};
|
|
698
823
|
|
|
699
824
|
app.get("/:id/agent-traces", async (context) => {
|
|
700
825
|
const query = traceQuery.parse(context.req.query());
|
|
701
|
-
const pins = tracePins(context.req.param("id"), query.version);
|
|
826
|
+
const pins = await tracePins(context.req.param("id"), query.version);
|
|
702
827
|
|
|
703
828
|
return context.json(
|
|
704
829
|
await listPinnedTraces(
|
|
@@ -710,7 +835,7 @@ export function createReviewApi(
|
|
|
710
835
|
});
|
|
711
836
|
app.get("/:id/agent-traces/:sessionId", async (context) => {
|
|
712
837
|
const query = traceQuery.parse(context.req.query());
|
|
713
|
-
const pins = tracePins(context.req.param("id"), query.version);
|
|
838
|
+
const pins = await tracePins(context.req.param("id"), query.version);
|
|
714
839
|
|
|
715
840
|
const result = await readStoredTrace(
|
|
716
841
|
store.repositoryPath(pins.repositoryId),
|
|
@@ -789,13 +914,27 @@ export function createReviewApi(
|
|
|
789
914
|
|
|
790
915
|
return context.json(await data!.register(input.path));
|
|
791
916
|
});
|
|
792
|
-
app.post("/resources", async (context) =>
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
917
|
+
app.post("/resources", async (context) => {
|
|
918
|
+
const body = await readBoundedRequestJson(
|
|
919
|
+
context.req.raw,
|
|
920
|
+
8 * 1024 * 1024,
|
|
921
|
+
);
|
|
922
|
+
|
|
923
|
+
// Agents send a checkout path; swap it for a repository id.
|
|
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
|
+
});
|
|
799
938
|
app.get("/:id/resources/:resourceId", async (context) => {
|
|
800
939
|
const id = context.req.param("id");
|
|
801
940
|
const snapshot = readReview(id);
|
|
@@ -996,6 +1135,20 @@ export function createReviewApi(
|
|
|
996
1135
|
|
|
997
1136
|
return context.json(await data.changes(pins));
|
|
998
1137
|
});
|
|
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
|
+
|
|
999
1152
|
app.get("/:id/commits", async (context) => {
|
|
1000
1153
|
const input = readQuerySchemas.commits.parse(context.req.query());
|
|
1001
1154
|
|
|
@@ -1074,6 +1227,9 @@ export function createReviewApi(
|
|
|
1074
1227
|
.join("\n");
|
|
1075
1228
|
}
|
|
1076
1229
|
|
|
1230
|
+
if (target.kind === "block")
|
|
1231
|
+
excerpt = documentText(snapshot, target.blockId, true);
|
|
1232
|
+
|
|
1077
1233
|
const diff = selection.selectedDiff;
|
|
1078
1234
|
|
|
1079
1235
|
const text = selectionMarkdown(
|
|
@@ -1085,7 +1241,7 @@ export function createReviewApi(
|
|
|
1085
1241
|
);
|
|
1086
1242
|
|
|
1087
1243
|
return [
|
|
1088
|
-
`Whiteboard: ${snapshot.title}`,
|
|
1244
|
+
`Whiteboard: ${boardTitle(snapshot.title)}`,
|
|
1089
1245
|
`Session ID: ${snapshot.reviewId}`,
|
|
1090
1246
|
`Version: ${snapshot.version}`,
|
|
1091
1247
|
...(selection.apiSource?.commit
|
|
@@ -1093,7 +1249,6 @@ export function createReviewApi(
|
|
|
1093
1249
|
: []),
|
|
1094
1250
|
...(selection.apiSource?.pins
|
|
1095
1251
|
? [
|
|
1096
|
-
`Selected repository ID: ${selection.apiSource.pins.repositoryId}`,
|
|
1097
1252
|
...(selection.apiSource.pins.base
|
|
1098
1253
|
? [`Selected base: ${selection.apiSource.pins.base}`]
|
|
1099
1254
|
: []),
|
|
@@ -1102,7 +1257,6 @@ export function createReviewApi(
|
|
|
1102
1257
|
: []),
|
|
1103
1258
|
...(snapshot.pins
|
|
1104
1259
|
? [
|
|
1105
|
-
`Repository ID: ${snapshot.pins.repositoryId}`,
|
|
1106
1260
|
`Session base: ${snapshot.pins.base}`,
|
|
1107
1261
|
`Session head: ${snapshot.pins.head}`,
|
|
1108
1262
|
]
|
|
@@ -1116,10 +1270,15 @@ export function createReviewApi(
|
|
|
1116
1270
|
}
|
|
1117
1271
|
|
|
1118
1272
|
if (ask && data) {
|
|
1273
|
+
const projectOf = (reviewId: string) =>
|
|
1274
|
+
isShared(reviewId) || !store.has(reviewId)
|
|
1275
|
+
? undefined
|
|
1276
|
+
: store.projectOf(reviewId);
|
|
1277
|
+
|
|
1119
1278
|
const readThread = (reviewId: string, threadId: string) => {
|
|
1120
1279
|
const thread = ask.threads.get(threadId);
|
|
1121
1280
|
|
|
1122
|
-
if (!thread
|
|
1281
|
+
if (!thread)
|
|
1123
1282
|
throw new ReviewInputError("This conversation has ended.", 404);
|
|
1124
1283
|
|
|
1125
1284
|
return thread;
|
|
@@ -1198,7 +1357,7 @@ export function createReviewApi(
|
|
|
1198
1357
|
const { reviewId } = snapshot;
|
|
1199
1358
|
|
|
1200
1359
|
return [
|
|
1201
|
-
`A reviewer is reading "${snapshot.title}" in Whiteboard and has a question about ${selection.target.kind === "review" ? "it" : "a selection"}.`,
|
|
1360
|
+
`A reviewer is reading "${boardTitle(snapshot.title)}" in Whiteboard and has a question about ${selection.target.kind === "review" ? "it" : "a selection"}.`,
|
|
1202
1361
|
checkout.live
|
|
1203
1362
|
? "Your working directory is the repository the review describes."
|
|
1204
1363
|
: `Your working directory is a checkout of the review's head commit, ${checkout.head}. Answer from this code, not from other branches.`,
|
|
@@ -1326,6 +1485,9 @@ export function createReviewApi(
|
|
|
1326
1485
|
|
|
1327
1486
|
assertWritable(reviewId);
|
|
1328
1487
|
|
|
1488
|
+
// Asked on screen: seen from the start.
|
|
1489
|
+
store.askHistory.markViewed(id, createdAt);
|
|
1490
|
+
|
|
1329
1491
|
const thread = ask.threads.open({
|
|
1330
1492
|
id,
|
|
1331
1493
|
reviewId,
|
|
@@ -1334,7 +1496,10 @@ export function createReviewApi(
|
|
|
1334
1496
|
// replaces a session the agent could not reopen.
|
|
1335
1497
|
...historyHooks({
|
|
1336
1498
|
id,
|
|
1337
|
-
|
|
1499
|
+
anchor:
|
|
1500
|
+
target.kind === "text" && target.anchor
|
|
1501
|
+
? { board: reviewId, block: target.anchor.blockId }
|
|
1502
|
+
: { board: reviewId },
|
|
1338
1503
|
agent: input.agent,
|
|
1339
1504
|
version: snapshot.version,
|
|
1340
1505
|
head: checkout.head,
|
|
@@ -1360,12 +1525,129 @@ export function createReviewApi(
|
|
|
1360
1525
|
return context.json({ threadId: thread.id });
|
|
1361
1526
|
});
|
|
1362
1527
|
|
|
1528
|
+
/** Every project's threads for the sidebar: who runs them, where they
|
|
1529
|
+
* started and live, and which boards they changed. */
|
|
1530
|
+
const projectThreads = (reviewId: string): ReviewProjectThreads => {
|
|
1531
|
+
const entries = store.askHistory.listAll();
|
|
1532
|
+
|
|
1533
|
+
const ids = entries.map((entry) => entry.id);
|
|
1534
|
+
const edited = store.boardsChangedByThreads(ids);
|
|
1535
|
+
|
|
1536
|
+
const boards = new Map<string, ReviewThreadBoard>();
|
|
1537
|
+
|
|
1538
|
+
const board = (id: string) => {
|
|
1539
|
+
let found = boards.get(id);
|
|
1540
|
+
|
|
1541
|
+
if (!found) {
|
|
1542
|
+
found = store.has(id)
|
|
1543
|
+
? store.boardFace(id)
|
|
1544
|
+
: { reviewId: id, icon: boardIconKind(undefined) };
|
|
1545
|
+
boards.set(id, found);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
return found;
|
|
1549
|
+
};
|
|
1550
|
+
|
|
1551
|
+
const project = projectOf(reviewId);
|
|
1552
|
+
|
|
1553
|
+
return {
|
|
1554
|
+
board: reviewId,
|
|
1555
|
+
...(project && { project }),
|
|
1556
|
+
threads: entries.map((entry) => {
|
|
1557
|
+
const anchor = entry.anchor ?? { board: reviewId };
|
|
1558
|
+
const status = ask.threads.get(entry.id)?.status;
|
|
1559
|
+
const { viewedAt } = entry;
|
|
1560
|
+
|
|
1561
|
+
// A turn finished since the reviewer last saw the thread.
|
|
1562
|
+
const unread =
|
|
1563
|
+
status !== "starting" &&
|
|
1564
|
+
status !== "running" &&
|
|
1565
|
+
status !== "waiting" &&
|
|
1566
|
+
viewedAt !== undefined &&
|
|
1567
|
+
entry.updatedAt > viewedAt;
|
|
1568
|
+
|
|
1569
|
+
return {
|
|
1570
|
+
id: entry.id,
|
|
1571
|
+
...(entry.projectId && { projectId: entry.projectId }),
|
|
1572
|
+
...(store.has(anchor.board) && {
|
|
1573
|
+
place: store.boardPlace(anchor.board),
|
|
1574
|
+
}),
|
|
1575
|
+
agent: entry.agent,
|
|
1576
|
+
agentName: askAgents[entry.agent].name,
|
|
1577
|
+
opening: entry.question ?? entry.title,
|
|
1578
|
+
pinned:
|
|
1579
|
+
entry.selection.target.kind === "text" &&
|
|
1580
|
+
entry.selection.target.anchor !== undefined,
|
|
1581
|
+
...(status && { status }),
|
|
1582
|
+
...(unread && { unread }),
|
|
1583
|
+
updatedAt: entry.updatedAt,
|
|
1584
|
+
anchor: {
|
|
1585
|
+
board: board(anchor.board),
|
|
1586
|
+
...(anchor.block && { block: anchor.block }),
|
|
1587
|
+
},
|
|
1588
|
+
edited: edited.get(entry.id) ?? [],
|
|
1589
|
+
};
|
|
1590
|
+
}),
|
|
1591
|
+
};
|
|
1592
|
+
};
|
|
1593
|
+
|
|
1363
1594
|
app.get("/:id/ask/threads", (context) => {
|
|
1364
1595
|
const reviewId = context.req.param("id");
|
|
1365
1596
|
|
|
1366
1597
|
readReview(reviewId);
|
|
1367
1598
|
|
|
1368
|
-
|
|
1599
|
+
if (context.req.query("watch") !== undefined) {
|
|
1600
|
+
// Sent again only when it reads differently.
|
|
1601
|
+
let sent = "";
|
|
1602
|
+
|
|
1603
|
+
return watch(
|
|
1604
|
+
() => {
|
|
1605
|
+
const line = JSON.stringify(projectThreads(reviewId));
|
|
1606
|
+
|
|
1607
|
+
if (line === sent) return [];
|
|
1608
|
+
sent = line;
|
|
1609
|
+
|
|
1610
|
+
return [line];
|
|
1611
|
+
},
|
|
1612
|
+
(notify) => {
|
|
1613
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1614
|
+
|
|
1615
|
+
const later = () => {
|
|
1616
|
+
timer ??= setTimeout(() => {
|
|
1617
|
+
timer = undefined;
|
|
1618
|
+
notify();
|
|
1619
|
+
}, 100);
|
|
1620
|
+
};
|
|
1621
|
+
|
|
1622
|
+
const stops = [
|
|
1623
|
+
store.askHistory.subscribe(later),
|
|
1624
|
+
ask.threads.subscribeStatus(later),
|
|
1625
|
+
// Another process's version may have changed any of these.
|
|
1626
|
+
store.subscribe((change) => {
|
|
1627
|
+
if (
|
|
1628
|
+
change.deleted ||
|
|
1629
|
+
change.lifecycle ||
|
|
1630
|
+
change.titleChanged ||
|
|
1631
|
+
change.placeChanged ||
|
|
1632
|
+
change.threadId ||
|
|
1633
|
+
change.external
|
|
1634
|
+
)
|
|
1635
|
+
later();
|
|
1636
|
+
}),
|
|
1637
|
+
];
|
|
1638
|
+
|
|
1639
|
+
return () => {
|
|
1640
|
+
clearTimeout(timer);
|
|
1641
|
+
stops.forEach((stop) => stop());
|
|
1642
|
+
};
|
|
1643
|
+
},
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
return context.json({
|
|
1648
|
+
threads: store.askHistory.list(reviewId, projectOf(reviewId)),
|
|
1649
|
+
board: reviewId,
|
|
1650
|
+
});
|
|
1369
1651
|
});
|
|
1370
1652
|
|
|
1371
1653
|
// The threads of every open Ask in a review, over one connection. Before
|
|
@@ -1384,20 +1666,32 @@ export function createReviewApi(
|
|
|
1384
1666
|
threads.map((threadId) => {
|
|
1385
1667
|
const thread = ask.threads.get(threadId);
|
|
1386
1668
|
|
|
1387
|
-
return [
|
|
1388
|
-
threadId,
|
|
1389
|
-
thread?.reviewId === reviewId ? thread : undefined,
|
|
1390
|
-
];
|
|
1669
|
+
return [threadId, thread];
|
|
1391
1670
|
}),
|
|
1392
1671
|
),
|
|
1393
1672
|
);
|
|
1394
1673
|
});
|
|
1395
1674
|
|
|
1675
|
+
// The boards a thread changed: titles from any project, so only for a
|
|
1676
|
+
// local caller.
|
|
1677
|
+
app.get("/:id/ask/:threadId/boards", (context) => {
|
|
1678
|
+
const threadId = context.req.param("threadId");
|
|
1679
|
+
const record = store.askHistory.get(threadId);
|
|
1680
|
+
|
|
1681
|
+
const anchor =
|
|
1682
|
+
record?.anchor.board ?? ask.threads.get(threadId)?.reviewId;
|
|
1683
|
+
|
|
1684
|
+
if (remoteCaller(context) || !anchor)
|
|
1685
|
+
throw new ReviewInputError("This conversation was not found.", 404);
|
|
1686
|
+
|
|
1687
|
+
return context.json({ boards: store.boardsChangedBy(threadId) });
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1396
1690
|
// A saved conversation, without starting its agent.
|
|
1397
1691
|
app.get("/:id/ask/:threadId", (context) => {
|
|
1398
1692
|
const record = store.askHistory.get(context.req.param("threadId"));
|
|
1399
1693
|
|
|
1400
|
-
if (record
|
|
1694
|
+
if (!record)
|
|
1401
1695
|
throw new ReviewInputError("This conversation was not found.", 404);
|
|
1402
1696
|
const target = record.selection.target;
|
|
1403
1697
|
|
|
@@ -1429,17 +1723,19 @@ export function createReviewApi(
|
|
|
1429
1723
|
const threadId = context.req.param("threadId");
|
|
1430
1724
|
const live = ask.threads.get(threadId);
|
|
1431
1725
|
|
|
1432
|
-
if (live
|
|
1726
|
+
if (live) return context.json({ threadId });
|
|
1433
1727
|
const record = store.askHistory.get(threadId);
|
|
1434
1728
|
|
|
1435
1729
|
const { picks } = askOpenSchema.parse(
|
|
1436
1730
|
await readBoundedRequestJson(context.req.raw, undefined, {}),
|
|
1437
1731
|
);
|
|
1438
1732
|
|
|
1439
|
-
if (record
|
|
1733
|
+
if (!record)
|
|
1440
1734
|
throw new ReviewInputError("This conversation was not found.", 404);
|
|
1441
1735
|
|
|
1442
|
-
|
|
1736
|
+
// The thread runs in the checkout of the board it started on.
|
|
1737
|
+
const anchor = record.anchor.board;
|
|
1738
|
+
const snapshot = readReview(anchor, record.version);
|
|
1443
1739
|
const checkout = await data.agentCheckout(snapshot);
|
|
1444
1740
|
const target = record.selection.target;
|
|
1445
1741
|
|
|
@@ -1455,7 +1751,7 @@ export function createReviewApi(
|
|
|
1455
1751
|
assertWritable(reviewId);
|
|
1456
1752
|
ask.threads.open({
|
|
1457
1753
|
id: record.id,
|
|
1458
|
-
reviewId,
|
|
1754
|
+
reviewId: anchor,
|
|
1459
1755
|
agent: record.agent,
|
|
1460
1756
|
cwd: checkout.rootPath,
|
|
1461
1757
|
head: checkout.head,
|
|
@@ -1647,6 +1943,16 @@ export function createReviewApi(
|
|
|
1647
1943
|
},
|
|
1648
1944
|
);
|
|
1649
1945
|
|
|
1946
|
+
app.post("/:id/ask/:threadId/viewed", (context) => {
|
|
1947
|
+
const threadId = context.req.param("threadId");
|
|
1948
|
+
|
|
1949
|
+
if (!ask.threads.get(threadId) && !store.askHistory.get(threadId))
|
|
1950
|
+
throw new ReviewInputError("This conversation was not found.", 404);
|
|
1951
|
+
store.askHistory.markViewed(threadId);
|
|
1952
|
+
|
|
1953
|
+
return context.json({ ok: true });
|
|
1954
|
+
});
|
|
1955
|
+
|
|
1650
1956
|
// Closing an Ask ends its agent; the conversation stays saved. A tab
|
|
1651
1957
|
// switched away from or a reload leaves it running, to come back to.
|
|
1652
1958
|
app.post("/:id/ask/:threadId/close", reviewAccess("read"), (context) => {
|
|
@@ -1664,17 +1970,29 @@ export function createReviewApi(
|
|
|
1664
1970
|
const reviewId = context.req.param("id");
|
|
1665
1971
|
const threadId = context.req.param("threadId");
|
|
1666
1972
|
|
|
1667
|
-
|
|
1973
|
+
const record = store.askHistory.get(threadId);
|
|
1974
|
+
|
|
1975
|
+
if (!record)
|
|
1668
1976
|
throw new ReviewInputError("This conversation was not found.", 404);
|
|
1669
1977
|
|
|
1670
|
-
if (ask.threads.get(threadId)
|
|
1671
|
-
ask.threads.close(threadId);
|
|
1978
|
+
if (ask.threads.get(threadId)) ask.threads.close(threadId);
|
|
1672
1979
|
store.askHistory.delete(threadId);
|
|
1673
1980
|
|
|
1674
1981
|
return context.json({ ok: true });
|
|
1675
1982
|
});
|
|
1676
1983
|
}
|
|
1677
1984
|
|
|
1985
|
+
app.get("/:id/place", (context) => {
|
|
1986
|
+
const id = context.req.param("id");
|
|
1987
|
+
|
|
1988
|
+
if (!isShared(id)) store.assertExists(id);
|
|
1989
|
+
|
|
1990
|
+
return context.json(placeOf(id));
|
|
1991
|
+
});
|
|
1992
|
+
|
|
1993
|
+
// The board's branch stack: its GitHub stack, else the reviews chained
|
|
1994
|
+
// through the bases they name, bottom first. Apart from its place among
|
|
1995
|
+
// boards.
|
|
1678
1996
|
app.get("/:id/stack", async (context) => {
|
|
1679
1997
|
const query = readQuerySchemas.get.parse(context.req.query());
|
|
1680
1998
|
const id = context.req.param("id");
|
|
@@ -1682,14 +2000,93 @@ export function createReviewApi(
|
|
|
1682
2000
|
|
|
1683
2001
|
if (isShared(id) || !snapshot.pins) return context.json({ layers: [] });
|
|
1684
2002
|
|
|
2003
|
+
const repositoryPath = store
|
|
2004
|
+
.repositories()
|
|
2005
|
+
.find(
|
|
2006
|
+
(repository) => repository.id === snapshot.pins!.repositoryId,
|
|
2007
|
+
)?.path;
|
|
2008
|
+
|
|
2009
|
+
// A GitHub stack's boards may sit in any project.
|
|
1685
2010
|
const layers = await resolveReviewStackLayers(snapshot, store.list(), {
|
|
1686
|
-
repositoryPath
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
2011
|
+
repositoryPath,
|
|
2012
|
+
});
|
|
2013
|
+
|
|
2014
|
+
// The branch the stack sits on: the repository's default branch.
|
|
2015
|
+
const trunk =
|
|
2016
|
+
layers.length > 1 && repositoryPath
|
|
2017
|
+
? await defaultBranch(repositoryPath).catch(() => null)
|
|
2018
|
+
: null;
|
|
2019
|
+
|
|
2020
|
+
return context.json({
|
|
2021
|
+
layers: layers.map((layer) => {
|
|
2022
|
+
const state = data?.pullRequestState(layer.pullRequestUrl ?? undefined);
|
|
2023
|
+
|
|
2024
|
+
return state ? { ...layer, pullRequestState: state } : layer;
|
|
2025
|
+
}),
|
|
2026
|
+
...(trunk && { base: trunk.ref.replace(/^origin\//, "") }),
|
|
2027
|
+
});
|
|
2028
|
+
});
|
|
2029
|
+
|
|
2030
|
+
// The board's project and its whiteboards, for the board's sidebar.
|
|
2031
|
+
app.get("/:id/project", (context) => {
|
|
2032
|
+
const id = context.req.param("id");
|
|
2033
|
+
|
|
2034
|
+
if (isShared(id)) return context.json({ project: null, boards: [] });
|
|
2035
|
+
|
|
2036
|
+
const project = store.summary(id)?.project ?? null;
|
|
2037
|
+
|
|
2038
|
+
return context.json({
|
|
2039
|
+
project,
|
|
2040
|
+
boards: project
|
|
2041
|
+
? store
|
|
2042
|
+
.listProject(project.id)
|
|
2043
|
+
.filter((board) => !board.trashedAt)
|
|
2044
|
+
.map(withPullRequestState)
|
|
2045
|
+
: [],
|
|
1690
2046
|
});
|
|
2047
|
+
});
|
|
2048
|
+
app.get("/:id/links", (context) => {
|
|
2049
|
+
const id = context.req.param("id");
|
|
2050
|
+
|
|
2051
|
+
// A shared board's links name boards on the sender's machine: shown,
|
|
2052
|
+
// but not followed.
|
|
2053
|
+
if (isShared(id))
|
|
2054
|
+
return context.json({
|
|
2055
|
+
outgoing: boardLinks(id, readReview(id).document).map(
|
|
2056
|
+
({ from, to }) => ({ from, to, missing: true, elsewhere: true }),
|
|
2057
|
+
),
|
|
2058
|
+
backlinks: [],
|
|
2059
|
+
});
|
|
2060
|
+
|
|
2061
|
+
const { version } = readQuerySchemas.get
|
|
2062
|
+
.pick({ version: true })
|
|
2063
|
+
.parse({ version: context.req.query("version") });
|
|
2064
|
+
|
|
2065
|
+
const { outgoing, backlinks } = store.links(id, version);
|
|
1691
2066
|
|
|
1692
|
-
return context.json({
|
|
2067
|
+
return context.json({
|
|
2068
|
+
outgoing: outgoing.map(withPullRequestState),
|
|
2069
|
+
backlinks: backlinks.map(withPullRequestState),
|
|
2070
|
+
});
|
|
2071
|
+
});
|
|
2072
|
+
// A link's hover card, read when it opens.
|
|
2073
|
+
app.get("/:id/links/peek", (context) => {
|
|
2074
|
+
const id = context.req.param("id");
|
|
2075
|
+
|
|
2076
|
+
if (isShared(id)) return context.json({});
|
|
2077
|
+
|
|
2078
|
+
const { version } = readQuerySchemas.get
|
|
2079
|
+
.pick({ version: true })
|
|
2080
|
+
.parse({ version: context.req.query("version") });
|
|
2081
|
+
|
|
2082
|
+
const board = context.req.query("board");
|
|
2083
|
+
const block = context.req.query("block");
|
|
2084
|
+
|
|
2085
|
+
if (!board) throw new ReviewInputError("Name the linked board.", 400);
|
|
2086
|
+
|
|
2087
|
+
return context.json(
|
|
2088
|
+
store.linkPeek(id, { board, ...(block && { block }) }, version),
|
|
2089
|
+
);
|
|
1693
2090
|
});
|
|
1694
2091
|
|
|
1695
2092
|
app.get("/:id/history", (context) => {
|
|
@@ -1703,9 +2100,44 @@ export function createReviewApi(
|
|
|
1703
2100
|
version: snapshot.version,
|
|
1704
2101
|
title: snapshot.title,
|
|
1705
2102
|
createdAt: snapshot.createdAt,
|
|
2103
|
+
author: "agent",
|
|
1706
2104
|
},
|
|
1707
2105
|
]);
|
|
1708
2106
|
});
|
|
2107
|
+
|
|
2108
|
+
/** A board's relations as session_list reports them: only those it has. */
|
|
2109
|
+
const listedRelations = ({
|
|
2110
|
+
parentId,
|
|
2111
|
+
children,
|
|
2112
|
+
dependsOn,
|
|
2113
|
+
}: ReturnType<typeof store.relations>) => {
|
|
2114
|
+
const listed: Partial<ReturnType<typeof store.relations>> = {};
|
|
2115
|
+
|
|
2116
|
+
if (parentId) listed.parentId = parentId;
|
|
2117
|
+
|
|
2118
|
+
if (children.length) listed.children = children;
|
|
2119
|
+
|
|
2120
|
+
if (dependsOn.length) listed.dependsOn = dependsOn;
|
|
2121
|
+
|
|
2122
|
+
return listed;
|
|
2123
|
+
};
|
|
2124
|
+
|
|
2125
|
+
/** A board's relations, each board named by its title. */
|
|
2126
|
+
const relatedBoards = (id: string) => {
|
|
2127
|
+
const { parentId, children, dependsOn } = store.relations(id);
|
|
2128
|
+
|
|
2129
|
+
const named = (board: string) => ({
|
|
2130
|
+
id: board,
|
|
2131
|
+
title: store.read(board).title,
|
|
2132
|
+
});
|
|
2133
|
+
|
|
2134
|
+
return {
|
|
2135
|
+
...(parentId && { parent: named(parentId) }),
|
|
2136
|
+
children: children.map(named),
|
|
2137
|
+
dependsOn: dependsOn.map(named),
|
|
2138
|
+
};
|
|
2139
|
+
};
|
|
2140
|
+
|
|
1709
2141
|
app.get("/:id/inspect", (context) => {
|
|
1710
2142
|
const query = inspectQuerySchema.parse(context.req.query());
|
|
1711
2143
|
const id = context.req.param("id");
|
|
@@ -1713,12 +2145,19 @@ export function createReviewApi(
|
|
|
1713
2145
|
|
|
1714
2146
|
return context.json(
|
|
1715
2147
|
query.format === "text"
|
|
1716
|
-
? documentText(
|
|
2148
|
+
? documentText(
|
|
2149
|
+
snapshot,
|
|
2150
|
+
query.targetId,
|
|
2151
|
+
Boolean(query.full),
|
|
2152
|
+
isShared(id) ? undefined : relatedBoards(id),
|
|
2153
|
+
)
|
|
1717
2154
|
: query.targetId !== undefined
|
|
1718
2155
|
? inspectSnapshot(snapshot, query.targetId)
|
|
1719
|
-
:
|
|
1720
|
-
|
|
1721
|
-
|
|
2156
|
+
: {
|
|
2157
|
+
...(query.full ? snapshot : inspectSnapshot(snapshot)),
|
|
2158
|
+
// Its place among the project's boards, as session_list has it.
|
|
2159
|
+
...(!isShared(id) && listedRelations(store.relations(id))),
|
|
2160
|
+
},
|
|
1722
2161
|
);
|
|
1723
2162
|
});
|
|
1724
2163
|
app.get("/:id", async (context) => {
|
|
@@ -1778,27 +2217,32 @@ export function createReviewApi(
|
|
|
1778
2217
|
await readBoundedRequestJson(context.req.raw),
|
|
1779
2218
|
);
|
|
1780
2219
|
|
|
1781
|
-
|
|
1782
|
-
|
|
2220
|
+
// Without local data (a bare store), a path is recorded as it is.
|
|
2221
|
+
const register = async (path: string) =>
|
|
2222
|
+
data ? data.register(path) : store.registerRepository(path);
|
|
1783
2223
|
|
|
1784
|
-
|
|
1785
|
-
|
|
2224
|
+
const request = withBoardRepository(
|
|
2225
|
+
await locateRepositories(
|
|
2226
|
+
await pullRequestTargets(
|
|
2227
|
+
body,
|
|
2228
|
+
register,
|
|
2229
|
+
(url, repositoryId) => {
|
|
2230
|
+
if (!data)
|
|
2231
|
+
throw new ReviewInputError("Repositories are unavailable.");
|
|
2232
|
+
|
|
2233
|
+
return data.resolvePullRequest(url, { id: repositoryId });
|
|
2234
|
+
},
|
|
2235
|
+
(id) => store.read(id),
|
|
2236
|
+
),
|
|
2237
|
+
register,
|
|
2238
|
+
),
|
|
2239
|
+
(id) => (store.has(id) ? store.read(id) : undefined),
|
|
2240
|
+
);
|
|
1786
2241
|
|
|
1787
2242
|
const input = commandSchema.parse(request);
|
|
1788
2243
|
|
|
1789
2244
|
const command = sharedCommandSchema.safeParse(input);
|
|
1790
2245
|
|
|
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
|
-
|
|
1802
2246
|
if (
|
|
1803
2247
|
command.success &&
|
|
1804
2248
|
command.data.operation.reviewId?.startsWith("shared-")
|
|
@@ -1832,11 +2276,6 @@ export function createReviewApi(
|
|
|
1832
2276
|
);
|
|
1833
2277
|
else await shared.setAttention(parsed.operation.reviewId);
|
|
1834
2278
|
|
|
1835
|
-
const reviewId = parsed.operation.reviewId;
|
|
1836
|
-
|
|
1837
|
-
if (shared.get(reviewId).snapshot.trashedAt)
|
|
1838
|
-
ask?.threads.closeDeletedReviews((id) => id === reviewId);
|
|
1839
|
-
|
|
1840
2279
|
return context.json({
|
|
1841
2280
|
reviewId: parsed.operation.reviewId,
|
|
1842
2281
|
version: shared.get(parsed.operation.reviewId).snapshot.version,
|
|
@@ -1847,13 +2286,23 @@ export function createReviewApi(
|
|
|
1847
2286
|
throw new ReviewInputError("Shared reviews are read-only.", 409);
|
|
1848
2287
|
}
|
|
1849
2288
|
|
|
1850
|
-
|
|
2289
|
+
// Who is writing, trusted only from local callers: a person from
|
|
2290
|
+
// Desktop or the canvas, or an agent, perhaps a thread's. The store
|
|
2291
|
+
// keeps each author's rules.
|
|
2292
|
+
const threadId = remoteCaller(context)
|
|
2293
|
+
? undefined
|
|
2294
|
+
: context.req.header(REVIEW_THREAD_HEADER)?.trim() || undefined;
|
|
2295
|
+
|
|
2296
|
+
const author: Author =
|
|
2297
|
+
!remoteCaller(context) &&
|
|
2298
|
+
context.req.header(REVIEW_PERSON_HEADER) !== undefined
|
|
2299
|
+
? { kind: "person" }
|
|
2300
|
+
: {
|
|
2301
|
+
kind: "agent",
|
|
2302
|
+
...(threadId && { threadId }),
|
|
2303
|
+
};
|
|
1851
2304
|
|
|
1852
|
-
|
|
1853
|
-
result.deleted ||
|
|
1854
|
-
(result.lifecycle && store.read(result.reviewId).trashedAt)
|
|
1855
|
-
)
|
|
1856
|
-
ask?.threads.closeDeletedReviews((id) => id === result.reviewId);
|
|
2305
|
+
const result = await store.execute(input, undefined, author);
|
|
1857
2306
|
|
|
1858
2307
|
if (input.operation.type === "edit" && data && !result.deleted) {
|
|
1859
2308
|
const { quotes, unquoted } = await anchorQuotes(
|
|
@@ -1893,7 +2342,6 @@ export function createReviewApi(
|
|
|
1893
2342
|
if (result.created !== false)
|
|
1894
2343
|
hooks.onReviewCreated?.({
|
|
1895
2344
|
reviewId: result.reviewId,
|
|
1896
|
-
kind: input.operation.kind === "scratchpad" ? "scratchpad" : "review",
|
|
1897
2345
|
blocks: store.read(result.reviewId).document.length,
|
|
1898
2346
|
...reviewRequestOrigin(context.req.raw.headers),
|
|
1899
2347
|
});
|
|
@@ -1979,16 +2427,165 @@ async function locateRepositories(
|
|
|
1979
2427
|
};
|
|
1980
2428
|
}
|
|
1981
2429
|
|
|
2430
|
+
/** Turns an agent's pullRequest target into what the store takes: a create
|
|
2431
|
+
* from pullRequestUrl, or for set_target the PR's resolved commits. */
|
|
2432
|
+
async function pullRequestTargets(
|
|
2433
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Request body boundary: commandSchema parses the result.
|
|
2434
|
+
body: unknown,
|
|
2435
|
+
register: (path: string) => Promise<{ id: string }>,
|
|
2436
|
+
resolve: (
|
|
2437
|
+
url: string,
|
|
2438
|
+
repositoryId: string,
|
|
2439
|
+
) => Promise<{ target: ReviewTarget }>,
|
|
2440
|
+
board: (id: string) => void,
|
|
2441
|
+
) {
|
|
2442
|
+
const target = z.strictObject({
|
|
2443
|
+
kind: z.literal("pullRequest"),
|
|
2444
|
+
repositoryPath: z.string().min(1),
|
|
2445
|
+
pullRequest: z.union([z.string().min(1), z.number().int().positive()]),
|
|
2446
|
+
});
|
|
2447
|
+
|
|
2448
|
+
// A set_target is checked whole before anything is registered or fetched.
|
|
2449
|
+
const located = z
|
|
2450
|
+
.looseObject({
|
|
2451
|
+
operation: z.discriminatedUnion("type", [
|
|
2452
|
+
z.looseObject({ type: z.literal("create"), target }),
|
|
2453
|
+
z.strictObject({
|
|
2454
|
+
type: z.literal("set_target"),
|
|
2455
|
+
reviewId: z.string().min(1),
|
|
2456
|
+
target,
|
|
2457
|
+
}),
|
|
2458
|
+
]),
|
|
2459
|
+
})
|
|
2460
|
+
.safeParse(body);
|
|
2461
|
+
|
|
2462
|
+
if (!located.success) return body;
|
|
2463
|
+
const { target: pullRequest, ...operation } = located.data.operation;
|
|
2464
|
+
|
|
2465
|
+
if (operation.type === "set_target") board(operation.reviewId);
|
|
2466
|
+
const { id: repositoryId } = await register(pullRequest.repositoryPath);
|
|
2467
|
+
|
|
2468
|
+
const url = await pullRequestAt(
|
|
2469
|
+
pullRequest.repositoryPath,
|
|
2470
|
+
pullRequest.pullRequest,
|
|
2471
|
+
);
|
|
2472
|
+
|
|
2473
|
+
if (operation.type === "create")
|
|
2474
|
+
return {
|
|
2475
|
+
...located.data,
|
|
2476
|
+
operation: { ...operation, pullRequestUrl: url, repositoryId },
|
|
2477
|
+
};
|
|
2478
|
+
|
|
2479
|
+
return {
|
|
2480
|
+
...located.data,
|
|
2481
|
+
operation: {
|
|
2482
|
+
...operation,
|
|
2483
|
+
target: (await resolve(url, repositoryId)).target,
|
|
2484
|
+
pullRequestUrl: url,
|
|
2485
|
+
},
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
/** A PR's URL: as given, or its number on the checkout's GitHub repository. */
|
|
2490
|
+
async function pullRequestAt(
|
|
2491
|
+
repositoryPath: string,
|
|
2492
|
+
pullRequest: string | number,
|
|
2493
|
+
) {
|
|
2494
|
+
if (isStringValue(pullRequest) && !/^\d+$/.test(pullRequest))
|
|
2495
|
+
return pullRequest;
|
|
2496
|
+
|
|
2497
|
+
const slug = (await resolveRepoContext(repositoryPath))?.githubSlug;
|
|
2498
|
+
|
|
2499
|
+
if (!slug)
|
|
2500
|
+
throw new ReviewInputError(
|
|
2501
|
+
"This checkout has no GitHub remote to find the PR number in; pass the PR's URL.",
|
|
2502
|
+
);
|
|
2503
|
+
|
|
2504
|
+
return `https://github.com/${slug}/pull/${pullRequest}`;
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
/** A pins object as an edit spells it: commits, with no other fields. A
|
|
2508
|
+
* diagram may key its own data `pins`, and that is left alone. */
|
|
2509
|
+
/** Pins as an agent writes them: the board's repository is the host's to
|
|
2510
|
+
* add. */
|
|
2511
|
+
const agentPinsSchema = z.strictObject({
|
|
2512
|
+
head: z.string().min(1),
|
|
2513
|
+
base: z.string().min(1).optional(),
|
|
2514
|
+
});
|
|
2515
|
+
|
|
2516
|
+
/** An object an agent meant as pins: one naming a head, base or repository.
|
|
2517
|
+
* Content may have a field called `pins` that is nothing of the kind. */
|
|
2518
|
+
const meantAsPins = (value: JsonValue): value is JsonObject =>
|
|
2519
|
+
isJsonObject(value) &&
|
|
2520
|
+
["head", "base", "repositoryId"].some((key) => key in value);
|
|
2521
|
+
|
|
2522
|
+
/** Sets every pins object in an edit to the board's own repository. */
|
|
2523
|
+
function withBoardRepository(
|
|
2524
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Request body boundary: commandSchema parses the result.
|
|
2525
|
+
body: unknown,
|
|
2526
|
+
board: (id: string) => Snapshot | undefined,
|
|
2527
|
+
) {
|
|
2528
|
+
const located = z
|
|
2529
|
+
.looseObject({
|
|
2530
|
+
operation: z.looseObject({
|
|
2531
|
+
type: z.enum(["edit", "lens_edit"]),
|
|
2532
|
+
reviewId: z.string(),
|
|
2533
|
+
edit: z.json(),
|
|
2534
|
+
}),
|
|
2535
|
+
})
|
|
2536
|
+
.safeParse(body);
|
|
2537
|
+
|
|
2538
|
+
if (!located.success) return body;
|
|
2539
|
+
const snapshot = board(located.data.operation.reviewId);
|
|
2540
|
+
|
|
2541
|
+
const repositoryId = snapshot ? boardRepositoryId(snapshot) : undefined;
|
|
2542
|
+
|
|
2543
|
+
const edit = JSON.parse(
|
|
2544
|
+
JSON.stringify(located.data.operation.edit),
|
|
2545
|
+
(key, value: JsonValue) => {
|
|
2546
|
+
if (key !== "pins" || !meantAsPins(value)) return value;
|
|
2547
|
+
|
|
2548
|
+
// Checked as the agent wrote them, so a mistake names head or base,
|
|
2549
|
+
// never the repository the host adds.
|
|
2550
|
+
const { repositoryId: _ignored, ...written } = value;
|
|
2551
|
+
const pins = agentPinsSchema.safeParse(written);
|
|
2552
|
+
|
|
2553
|
+
if (!pins.success)
|
|
2554
|
+
throw new ReviewInputError(
|
|
2555
|
+
`Write pins as {head, base?}, each a commit: ${pins.error.issues
|
|
2556
|
+
.map((issue) => `pins.${issue.path.join(".")}: ${issue.message}`)
|
|
2557
|
+
.join("; ")}.`,
|
|
2558
|
+
);
|
|
2559
|
+
|
|
2560
|
+
if (!repositoryId)
|
|
2561
|
+
throw new ReviewInputError(
|
|
2562
|
+
"This whiteboard has no repository for its pins to name commits of.",
|
|
2563
|
+
);
|
|
2564
|
+
|
|
2565
|
+
return { ...pins.data, repositoryId };
|
|
2566
|
+
},
|
|
2567
|
+
);
|
|
2568
|
+
|
|
2569
|
+
return {
|
|
2570
|
+
...located.data,
|
|
2571
|
+
operation: { ...located.data.operation, edit },
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
|
|
1982
2575
|
function watch(
|
|
1983
2576
|
lines: () => string[],
|
|
1984
2577
|
subscribe: (notify: () => void) => () => void,
|
|
1985
2578
|
) {
|
|
1986
2579
|
let stop = () => {};
|
|
1987
2580
|
|
|
2581
|
+
let missed = false;
|
|
2582
|
+
|
|
1988
2583
|
const encoder = new TextEncoder();
|
|
1989
2584
|
|
|
1990
2585
|
const send = (controller: ReadableStreamDefaultController<Uint8Array>) => {
|
|
1991
|
-
|
|
2586
|
+
missed = controller.desiredSize === null || controller.desiredSize <= 0;
|
|
2587
|
+
|
|
2588
|
+
if (missed) return;
|
|
1992
2589
|
|
|
1993
2590
|
try {
|
|
1994
2591
|
const pending = lines();
|
|
@@ -2015,7 +2612,9 @@ function watch(
|
|
|
2015
2612
|
});
|
|
2016
2613
|
send(controller);
|
|
2017
2614
|
},
|
|
2018
|
-
pull
|
|
2615
|
+
pull(controller) {
|
|
2616
|
+
if (missed) send(controller);
|
|
2617
|
+
},
|
|
2019
2618
|
cancel() {
|
|
2020
2619
|
stop();
|
|
2021
2620
|
},
|