@dev.fast/whiteboard 0.3.3 → 0.3.4-preview.20261019.126
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{account-alias-xOdmMnDH.js → account-alias-B5-HYi1D.js} +16 -9
- package/dist/{agent-cli-DT7MdaqL.js → agent-cli-CohHf6Ua.js} +15 -10
- package/dist/agent-cli-_YyMIMRY.js +2 -0
- package/dist/{agent-client-BRBzv3AV.js → agent-client-Dtc-Kn_w.js} +13 -3
- package/dist/{authoring-tools-DGSEag8y.js → authoring-tools-B7KU04f7.js} +34 -27
- package/dist/{background-server-WKzI_a98.js → background-server-C03RKxKS.js} +5 -5
- package/dist/{background-server-C_REWFkG.js → background-server-CB9C4uoc.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/{cli-BBSDi_dX.js → cli-CJaEb7zC.js} +1 -1
- package/dist/{cli-runner-DshTBske.js → cli-runner-DGDIqh3V.js} +26 -26
- package/dist/cli.js +4 -3
- package/dist/{client-BN_FDxFr.js → client-lKVF_Elo.js} +253 -47
- package/dist/{fuzzy-match-bFX6wm8j.js → fuzzy-match-BmVJJm9I.js} +42 -14
- package/dist/{headless-host-CzvYpKOX.js → headless-host-DRtjf9WA.js} +1 -1
- package/dist/{headless-host-CS4Gv0c5.js → headless-host-XhL1X4vX.js} +523 -146
- package/dist/lens-selection-CyJapcsl.js +214 -0
- package/dist/{local-data-DtQTIzHX.js → local-data-Cpl4ENbJ.js} +2434 -328
- package/dist/{mcp-DpKnnmHH.js → mcp-ovmiUE_x.js} +5 -9
- package/dist/{package-paths-jMg6JxDP.js → package-paths-BWxO-UEL.js} +1 -1
- package/dist/{profile-DaqmODhc.js → profile-BadSkCkI.js} +20 -8
- package/dist/profile-DJvkBJNQ.js +2 -0
- package/dist/{remote-attach-DCngg8cl.js → remote-attach-Dd-YobM6.js} +4 -4
- package/dist/{remote-extensions-BCAbQZTv.js → remote-extensions-CNXwqK0D.js} +3 -3
- package/dist/{remote-extensions-Cf3L-ZGG.js → remote-extensions-VP-kfq0C.js} +1 -1
- package/dist/{remote-language-server-UWQgpoh-.js → remote-language-server-BXk87KCr.js} +5 -5
- package/dist/{remote-uninstall-CcDOcGyi.js → remote-uninstall-DuOzsvaD.js} +6 -6
- package/dist/{request-origin-Zcn_CjX4.js → request-origin-B9qCPiVB.js} +45 -12
- package/dist/{review-app-launcher-tqSQPlbu.js → review-app-launcher-C7el2Uoy.js} +2 -2
- package/dist/{review-home-paths-Lc02WFuX.js → review-home-paths-D8AoCdd2.js} +19 -17
- package/dist/{review-telemetry-BHWJsnoD.js → review-telemetry-BsoRMC2w.js} +5 -5
- package/dist/runtime-Bj2tD33R.js +27 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/{s3-77-qkEhs.js → s3-CsG2EWED.js} +1 -1
- package/dist/{s3-config-DDWWMh5x.js → s3-config-CjcLyFV5.js} +5 -3
- package/dist/{s3-config-0xuCh-F_.js → s3-config-zz4GWh2T.js} +1 -1
- package/dist/server/desktop-host.js +29 -76
- package/dist/{server-discovery-BVqrE1_-.js → server-discovery-CKjw3-6h.js} +6 -5
- package/dist/sharing/index.d.ts +486 -70
- package/dist/sharing/index.js +1 -1
- package/dist/{src-DsM3jO8G.js → src-BiWNbcF_.js} +89 -12
- package/dist/{src-BMPuxWv4.js → src-WPaQn75m.js} +208 -73
- package/dist/{stored-document-migration-CChxtFAy.js → stored-document-migration-dFbewRTG.js} +270 -269
- package/dist/{thread-state-5pYr6_zM.js → thread-state-Do5LX10I.js} +7 -2
- package/dist/{tool-failure-BFt2xj9i.js → tool-failure-DvI-gys2.js} +10 -3
- package/instructions/authoring.md +3 -2
- package/package.json +4 -4
- package/src/agent-connect.ts +8 -2
- package/src/ask/agents.ts +51 -9
- package/src/ask/checkout-files.ts +2 -5
- package/src/ask/thread-state.ts +4 -0
- package/src/ask/thread.ts +16 -0
- package/src/ask/threads.ts +24 -3
- package/src/cli-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/review-api/README.md +52 -13
- package/src/review-api/activity.ts +1 -4
- package/src/review-api/agent-cli.ts +9 -1
- package/src/review-api/anchor-quotes.ts +14 -7
- package/src/review-api/ask-history.ts +137 -51
- package/src/review-api/authoring-tools.ts +45 -30
- package/src/review-api/blocks/definition.ts +1 -1
- package/src/review-api/blocks/markdown.ts +2 -2
- package/src/review-api/board-order.ts +144 -0
- package/src/review-api/checkout-groups.ts +560 -0
- package/src/review-api/document-headings.ts +58 -27
- package/src/review-api/document-text.ts +31 -2
- package/src/review-api/document.ts +281 -34
- package/src/review-api/foreign-references.ts +328 -0
- package/src/review-api/github-provider.ts +51 -13
- package/src/review-api/http.ts +702 -123
- package/src/review-api/instructions.ts +0 -19
- package/src/review-api/link-preview.ts +113 -0
- package/src/review-api/local-data.ts +310 -46
- package/src/review-api/mcp.ts +3 -11
- package/src/review-api/onboarding.ts +1 -1
- package/src/review-api/profile.ts +37 -6
- package/src/review-api/public-tools.ts +24 -2
- package/src/review-api/pull-request.ts +82 -49
- package/src/review-api/request-origin.ts +10 -0
- package/src/review-api/review-progress.ts +61 -27
- package/src/review-api/scratchpad-plan-upgrade.ts +250 -0
- package/src/review-api/store-migrations.ts +357 -0
- package/src/review-api/store-schema.ts +38 -29
- package/src/review-api/store.ts +2144 -480
- package/src/review-api/workspaces.ts +52 -13
- package/src/review-api/worktree-events.ts +3 -5
- package/src/review-api/worktree-source.ts +3 -7
- package/src/review-api/worktree-structural.integration.ts +72 -0
- package/src/review-stack.ts +15 -23
- package/src/runtime.ts +4 -30
- package/src/server/desktop-host.ts +6 -1
- package/src/server/desktop-server.ts +1 -31
- package/src/server/headless-host.ts +21 -14
- package/src/server/hono-http.ts +2 -1
- package/src/server/legacy-tutorial-cleanup.ts +2 -3
- package/src/server/review-gateway-list.ts +3 -5
- package/src/server/review-gateway-memory.ts +1 -1
- package/src/server/review-lifecycle-telemetry.ts +2 -2
- package/src/server/review-server-core.ts +1 -5
- package/src/server/structural-comparisons.ts +47 -0
- package/src/server-discovery.ts +4 -2
- package/src/sharing/export.ts +36 -7
- package/src/sharing/host.ts +34 -14
- package/src/sharing/import.ts +156 -24
- package/src/sharing/repository.ts +128 -20
- package/src/startup-trace.ts +3 -3
- package/src/stored-document-migration.ts +110 -19
- package/src/ui-telemetry-events.ts +33 -11
- package/dist/agent-cli-DeCrUC2s.js +0 -2
- package/dist/profile-Cz7L781D.js +0 -2
- package/dist/runtime-CKiWhGrf.js +0 -52
- package/instructions/scratchpad.md +0 -23
- package/src/review-preferences.ts +0 -88
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { boardTitle } from "@dev.fast/review-protocol";
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
type Element,
|
|
3
5
|
ReviewInputError,
|
|
@@ -6,11 +8,22 @@ import {
|
|
|
6
8
|
} from "./document.js";
|
|
7
9
|
import type { Snapshot } from "./store.js";
|
|
8
10
|
|
|
11
|
+
interface RelatedBoard {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
9
16
|
/** A reading view of saved content, not another document format to maintain. */
|
|
10
17
|
export function documentText(
|
|
11
18
|
snapshot: Snapshot,
|
|
12
19
|
targetId?: string,
|
|
13
20
|
full = false,
|
|
21
|
+
/** Where the whiteboard sits among its project's, by title. */
|
|
22
|
+
relations?: {
|
|
23
|
+
parent?: RelatedBoard;
|
|
24
|
+
children: RelatedBoard[];
|
|
25
|
+
dependsOn: RelatedBoard[];
|
|
26
|
+
},
|
|
14
27
|
): string {
|
|
15
28
|
const target =
|
|
16
29
|
targetId === undefined
|
|
@@ -22,11 +35,27 @@ export function documentText(
|
|
|
22
35
|
const detailed = full || target !== undefined;
|
|
23
36
|
|
|
24
37
|
const lines = [
|
|
25
|
-
`# ${snapshot.title}`,
|
|
38
|
+
`# ${boardTitle(snapshot.title)}`,
|
|
26
39
|
`Session ${snapshot.reviewId} · version ${snapshot.version}`,
|
|
27
|
-
"",
|
|
28
40
|
];
|
|
29
41
|
|
|
42
|
+
if (relations && targetId === undefined) {
|
|
43
|
+
const list = (boards: RelatedBoard[]) =>
|
|
44
|
+
boards
|
|
45
|
+
.map((board) => `${boardTitle(board.title)} (${board.id})`)
|
|
46
|
+
.join(", ");
|
|
47
|
+
|
|
48
|
+
if (relations.parent) lines.push(`Parent: ${list([relations.parent])}`);
|
|
49
|
+
|
|
50
|
+
if (relations.children.length)
|
|
51
|
+
lines.push(`Children, in order: ${list(relations.children)}`);
|
|
52
|
+
|
|
53
|
+
if (relations.dependsOn.length)
|
|
54
|
+
lines.push(`Depends on: ${list(relations.dependsOn)}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
lines.push("");
|
|
58
|
+
|
|
30
59
|
const write = (depth: number, text: string) => {
|
|
31
60
|
for (const line of text.split("\n")) lines.push(" ".repeat(depth) + line);
|
|
32
61
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { type JsonValue, isStringValue } from "@dev.fast/json";
|
|
1
2
|
import {
|
|
2
3
|
type Anchor,
|
|
3
4
|
type LensSource,
|
|
5
|
+
commitLinkTarget,
|
|
4
6
|
parseAnchor,
|
|
5
7
|
selectionProblem,
|
|
6
8
|
sourceAnchors,
|
|
@@ -144,6 +146,11 @@ const targets = <Repository extends Record<string, z.ZodType>>(
|
|
|
144
146
|
repository: Repository,
|
|
145
147
|
) =>
|
|
146
148
|
z.discriminatedUnion("kind", [
|
|
149
|
+
/** The project of the repository, no change: a plan. */
|
|
150
|
+
z.strictObject({
|
|
151
|
+
kind: z.literal("project"),
|
|
152
|
+
...repository,
|
|
153
|
+
}),
|
|
147
154
|
z.strictObject({
|
|
148
155
|
kind: z.literal("worktree"),
|
|
149
156
|
...repository,
|
|
@@ -159,8 +166,16 @@ const targets = <Repository extends Record<string, z.ZodType>>(
|
|
|
159
166
|
|
|
160
167
|
export const reviewTargetSchema = targets({ repositoryId: label });
|
|
161
168
|
|
|
162
|
-
/**
|
|
163
|
-
|
|
169
|
+
/** Targets as agents write them: a checkout path instead of a repository id,
|
|
170
|
+
* plus a PR by URL or number. */
|
|
171
|
+
export const pathTargetSchema = z.discriminatedUnion("kind", [
|
|
172
|
+
...targets({ repositoryPath: label }).options,
|
|
173
|
+
z.strictObject({
|
|
174
|
+
kind: z.literal("pullRequest"),
|
|
175
|
+
repositoryPath: label,
|
|
176
|
+
pullRequest: z.union([label, z.number().int().positive()]),
|
|
177
|
+
}),
|
|
178
|
+
]);
|
|
164
179
|
|
|
165
180
|
export type ReviewTarget = z.infer<typeof reviewTargetSchema>;
|
|
166
181
|
|
|
@@ -175,11 +190,99 @@ const unitTypes = new Set(["step", "flow_node", "flow_edge"]);
|
|
|
175
190
|
export const isUnit = (element: Element): element is Unit =>
|
|
176
191
|
unitTypes.has(element.type);
|
|
177
192
|
|
|
193
|
+
/** Rewrites old review-source: and review-trace: links to their whiteboard: form. */
|
|
194
|
+
export function canonicalLink(href: string): string {
|
|
195
|
+
return href.replace(
|
|
196
|
+
/^review-(source|trace):/i,
|
|
197
|
+
(_, kind: string) => `whiteboard:${kind.toLowerCase()}/`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Markdown link destinations and definitions that begin with `target`, a
|
|
202
|
+
* regex source; the match's first group is what comes before it. */
|
|
203
|
+
const linkDestination = (target: string) =>
|
|
204
|
+
new RegExp(String.raw`(\]\(\s*<?|^\s*\[[^\]\n]+\]:\s*<?)` + target, "gim");
|
|
205
|
+
|
|
206
|
+
/** Every Markdown text in `value`, rewritten. */
|
|
207
|
+
function rewriteMarkdown<T>(
|
|
208
|
+
value: T,
|
|
209
|
+
rewrite: (markdown: string) => string,
|
|
210
|
+
): T {
|
|
211
|
+
// SAFETY: a JSON round trip of stored or parsed JSON, rewriting only
|
|
212
|
+
// Markdown text.
|
|
213
|
+
return JSON.parse(JSON.stringify(value), (key, field: JsonValue) =>
|
|
214
|
+
key === "markdown" && isStringValue(field) ? rewrite(field) : field,
|
|
215
|
+
) as T;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const retiredLinks = linkDestination("review-(source|trace):");
|
|
219
|
+
|
|
220
|
+
/** Rewrites old link prefixes in Markdown link destinations and definitions. */
|
|
221
|
+
export function respellLinks(markdown: string): string {
|
|
222
|
+
return markdown.replace(
|
|
223
|
+
retiredLinks,
|
|
224
|
+
(_, before: string, kind: string) =>
|
|
225
|
+
`${before}whiteboard:${kind.toLowerCase()}/`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Every Markdown text in `document` with its links spelled the current
|
|
230
|
+
* way. */
|
|
231
|
+
export function respellDocumentLinks<T>(document: T): T {
|
|
232
|
+
return rewriteMarkdown(document, respellLinks);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function respellEditLinks(edit: Edit): Edit {
|
|
236
|
+
return rewriteMarkdown(edit, respellLinks);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Names a block from outside its board. Block ids are unique only within a board. */
|
|
240
|
+
export type BlockRef = { board: string; block: string };
|
|
241
|
+
|
|
242
|
+
export type BoardRef = BlockRef | { board: string; block?: undefined };
|
|
243
|
+
|
|
244
|
+
/** Parses whiteboard:board/<id> or whiteboard:board/<id>#<blockId>. */
|
|
245
|
+
export function boardLink(href: string): BoardRef | undefined {
|
|
246
|
+
const match = /^whiteboard:board\/([^#/]+)(?:#(.+))?$/.exec(href);
|
|
247
|
+
|
|
248
|
+
if (!match) return undefined;
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const board = decodeURIComponent(match[1]!);
|
|
252
|
+
|
|
253
|
+
return match[2] === undefined
|
|
254
|
+
? { board }
|
|
255
|
+
: { board, block: decodeURIComponent(match[2]) };
|
|
256
|
+
} catch {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** A link from a block to a board or one of its blocks. */
|
|
262
|
+
export interface BoardLinkRef {
|
|
263
|
+
from: BlockRef;
|
|
264
|
+
to: BoardRef;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Every board link in a document. Malformed links are skipped; writes reject them. */
|
|
268
|
+
export function boardLinks(board: string, document: Block[]): BoardLinkRef[] {
|
|
269
|
+
return elements(document).flatMap((element) =>
|
|
270
|
+
element.type === "markdown"
|
|
271
|
+
? [...markdownNodes(parseMarkdown(element.markdown))].flatMap((node) => {
|
|
272
|
+
const to =
|
|
273
|
+
node.type === "link" ? boardLink(node.url ?? "") : undefined;
|
|
274
|
+
|
|
275
|
+
return to ? [{ from: { board, block: element.id! }, to }] : [];
|
|
276
|
+
})
|
|
277
|
+
: [],
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
178
281
|
/** Inline trace quotes keep paragraph/list layout while using retained resources. */
|
|
179
282
|
export function traceQuoteLink(
|
|
180
283
|
href: string,
|
|
181
284
|
): { traceId: string; eventId: string } | undefined {
|
|
182
|
-
const match = /^
|
|
285
|
+
const match = /^whiteboard:trace\/([^#]+)#(.+)$/.exec(canonicalLink(href));
|
|
183
286
|
|
|
184
287
|
return match
|
|
185
288
|
? {
|
|
@@ -215,9 +318,12 @@ export function resourceReferences(document: Block[]): Block[] {
|
|
|
215
318
|
|
|
216
319
|
return [...markdownNodes(parseMarkdown(block.markdown))].flatMap(
|
|
217
320
|
(node): Block[] => {
|
|
218
|
-
if (
|
|
321
|
+
if (
|
|
322
|
+
node.type !== "link" ||
|
|
323
|
+
!canonicalLink(node.url ?? "").startsWith("whiteboard:trace/")
|
|
324
|
+
)
|
|
219
325
|
return [];
|
|
220
|
-
const quote = traceQuoteLink(node.url);
|
|
326
|
+
const quote = traceQuoteLink(node.url ?? "");
|
|
221
327
|
|
|
222
328
|
if (!quote) throw new ReviewInputError("Invalid trace quote link.");
|
|
223
329
|
|
|
@@ -237,7 +343,13 @@ function documentReferences(
|
|
|
237
343
|
{
|
|
238
344
|
tolerant = false,
|
|
239
345
|
markdown = true,
|
|
240
|
-
|
|
346
|
+
repositoryId,
|
|
347
|
+
}: {
|
|
348
|
+
tolerant?: boolean;
|
|
349
|
+
markdown?: boolean;
|
|
350
|
+
/** The board's repository, which commit links read from. */
|
|
351
|
+
repositoryId?: string;
|
|
352
|
+
} = {},
|
|
241
353
|
): {
|
|
242
354
|
id: string;
|
|
243
355
|
source: LensSource;
|
|
@@ -274,27 +386,53 @@ function documentReferences(
|
|
|
274
386
|
return [...markdownNodes(parseMarkdown(element.markdown))].flatMap(
|
|
275
387
|
(node) => {
|
|
276
388
|
if (node.type !== "link") return [];
|
|
277
|
-
const href = node.url ?? "";
|
|
389
|
+
const href = canonicalLink(node.url ?? "");
|
|
278
390
|
|
|
279
|
-
if (
|
|
391
|
+
if (!href.startsWith("whiteboard:source/")) {
|
|
280
392
|
// Trace links are checked by resourceReferences.
|
|
281
|
-
if (href.startsWith("
|
|
393
|
+
if (href.startsWith("whiteboard:trace/")) return [];
|
|
394
|
+
|
|
395
|
+
// Board links are checked against the store on write.
|
|
396
|
+
if (href.startsWith("whiteboard:board/"))
|
|
397
|
+
return boardLink(href)
|
|
398
|
+
? []
|
|
399
|
+
: reject(
|
|
400
|
+
`Use whiteboard:board/<sessionId> or whiteboard:board/<sessionId>#<blockId> to link a board, in block ${element.id}.`,
|
|
401
|
+
);
|
|
282
402
|
|
|
283
403
|
if (/^(?:https?:\/\/|mailto:|#)/i.test(href)) return [];
|
|
284
404
|
|
|
285
405
|
return reject(
|
|
286
|
-
`Unsupported Markdown link ${JSON.stringify(href)} in block ${element.id}. Use [label](
|
|
406
|
+
`Unsupported Markdown link ${JSON.stringify(href)} in block ${element.id}. Use [label](whiteboard:source/head/path#L10-L24) or whiteboard:source/base/path#L10-L24 for repository files, whiteboard:source/diff/path#L84-R90 across sides, or whiteboard:source/<commit>/path#L10-L24 at a commit of the board's repository, with a repository-relative path and verified line numbers. Link another board as whiteboard:board/<sessionId>, or one of its blocks as whiteboard:board/<sessionId>#<blockId>. External links must use https://, http://, or mailto:; document anchors use #heading.`,
|
|
287
407
|
);
|
|
288
408
|
}
|
|
289
409
|
|
|
290
410
|
let target: string;
|
|
291
411
|
|
|
292
412
|
try {
|
|
293
|
-
target = decodeURIComponent(
|
|
413
|
+
target = decodeURIComponent(
|
|
414
|
+
href.slice("whiteboard:source/".length),
|
|
415
|
+
);
|
|
294
416
|
} catch {
|
|
295
417
|
return reject("Invalid URL encoding in source link.");
|
|
296
418
|
}
|
|
297
419
|
|
|
420
|
+
// A commit of the board's repository: the commit is the link's pins.
|
|
421
|
+
const commit = commitLinkTarget(target);
|
|
422
|
+
|
|
423
|
+
if (commit)
|
|
424
|
+
return repositoryId
|
|
425
|
+
? select(commit.anchor, {
|
|
426
|
+
repositoryId,
|
|
427
|
+
head: commit.commit,
|
|
428
|
+
}).map((source) => ({
|
|
429
|
+
id: `${element.id}:${node.url}`,
|
|
430
|
+
source,
|
|
431
|
+
}))
|
|
432
|
+
: reject(
|
|
433
|
+
`A commit link names a commit of the board's repository, and this board has none, in block ${element.id}.`,
|
|
434
|
+
);
|
|
435
|
+
|
|
298
436
|
// Links share the anchor grammar blocks use.
|
|
299
437
|
const anchor = target.replace(/^(head|base|diff)\//i, (side) =>
|
|
300
438
|
side.toLowerCase(),
|
|
@@ -302,7 +440,7 @@ function documentReferences(
|
|
|
302
440
|
|
|
303
441
|
if (!parseAnchor(anchor))
|
|
304
442
|
return reject(
|
|
305
|
-
"Use
|
|
443
|
+
"Use whiteboard:source/head/path#L10-L24 (or base, diff/path#L84-R90 across sides, or <commit>/path#L10-L24 at a commit of the board's repository) for a source link.",
|
|
306
444
|
);
|
|
307
445
|
|
|
308
446
|
return select(anchor, element.pins).map((source) => ({
|
|
@@ -414,10 +552,108 @@ function documentReferences(
|
|
|
414
552
|
/** All authored attachments, including code peeks, select the aligned diff. */
|
|
415
553
|
export const selectionReferences = documentReferences;
|
|
416
554
|
|
|
555
|
+
const commitLinkDestination = linkDestination(
|
|
556
|
+
String.raw`whiteboard:source\/([0-9a-f]{7,40})\/`,
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
/** The commits a document's Markdown commit links name, as written. */
|
|
560
|
+
export function commitLinkCommits(document: Block[]): string[] {
|
|
561
|
+
const commits = new Set<string>();
|
|
562
|
+
|
|
563
|
+
JSON.stringify(document, (key, value: JsonValue) => {
|
|
564
|
+
if (key === "markdown" && isStringValue(value))
|
|
565
|
+
for (const match of value.matchAll(commitLinkDestination))
|
|
566
|
+
commits.add(match[2]!);
|
|
567
|
+
|
|
568
|
+
return value;
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
return [...commits];
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** Rewrites the commits Markdown commit links name, as a short commit
|
|
575
|
+
* becomes the full one on save. */
|
|
576
|
+
export function respellCommitLinks(
|
|
577
|
+
document: Block[],
|
|
578
|
+
commits: ReadonlyMap<string, string>,
|
|
579
|
+
): Block[] {
|
|
580
|
+
return rewriteMarkdown(document, (markdown) =>
|
|
581
|
+
markdown.replace(
|
|
582
|
+
commitLinkDestination,
|
|
583
|
+
(link, before: string, commit: string) =>
|
|
584
|
+
`${before}whiteboard:source/${commits.get(commit) ?? commit}/`,
|
|
585
|
+
),
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** Blocks that carry pins of their own for the references in them. */
|
|
590
|
+
const pinnedBlockTypes = new Set([
|
|
591
|
+
"markdown",
|
|
592
|
+
"code_peek",
|
|
593
|
+
"sequence",
|
|
594
|
+
"flow_diagram",
|
|
595
|
+
"call_stack_diff",
|
|
596
|
+
"database_lens",
|
|
597
|
+
]);
|
|
598
|
+
|
|
599
|
+
const sideLinkDestination = linkDestination(
|
|
600
|
+
String.raw`whiteboard:source\/(head|base)\/`,
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Makes every reference that reads at the document's pins carry them
|
|
605
|
+
* itself, for a board about to lose its pins: Markdown head/ and base/
|
|
606
|
+
* links become commit links, and a block whose references still lean on
|
|
607
|
+
* the document's pins (any anchor, or a diff/ link) takes them as its own.
|
|
608
|
+
* Nothing changes what it points at.
|
|
609
|
+
*/
|
|
610
|
+
export function pinDocumentReferences(document: Block[], pins: Pins): Block[] {
|
|
611
|
+
const own: SourcePins = {
|
|
612
|
+
repositoryId: pins.repositoryId,
|
|
613
|
+
base: pins.base,
|
|
614
|
+
head: pins.head,
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
const pinned = structuredClone(document);
|
|
618
|
+
|
|
619
|
+
for (const element of elements(pinned)) {
|
|
620
|
+
if (!pinnedBlockTypes.has(element.type)) continue;
|
|
621
|
+
|
|
622
|
+
// SAFETY: every pinned block type has optional source pins of its own.
|
|
623
|
+
const block = element as Block & { pins?: SourcePins };
|
|
624
|
+
|
|
625
|
+
if (block.pins) continue;
|
|
626
|
+
|
|
627
|
+
if (block.type === "markdown")
|
|
628
|
+
block.markdown = respellLinks(block.markdown).replace(
|
|
629
|
+
sideLinkDestination,
|
|
630
|
+
(_, before: string, side: string) =>
|
|
631
|
+
`${before}whiteboard:source/${side.toLowerCase() === "base" ? pins.base : pins.head}/`,
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
if (
|
|
635
|
+
documentReferences([block], { tolerant: true }).some(
|
|
636
|
+
(reference) => !reference.source.pins,
|
|
637
|
+
)
|
|
638
|
+
)
|
|
639
|
+
block.pins = own;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return pinned;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** The repository a board's commit links read from: its target's. */
|
|
646
|
+
export function boardRepositoryId(board: {
|
|
647
|
+
target?: { repositoryId: string };
|
|
648
|
+
pins?: { repositoryId: string };
|
|
649
|
+
}): string | undefined {
|
|
650
|
+
return board.target?.repositoryId ?? board.pins?.repositoryId;
|
|
651
|
+
}
|
|
652
|
+
|
|
417
653
|
/** Per-side read coordinates for endpoint validation and retained source quotes. */
|
|
418
654
|
export function sourceReferences(
|
|
419
655
|
document: Block[],
|
|
420
|
-
options: { tolerant?: boolean } = {},
|
|
656
|
+
options: { tolerant?: boolean; repositoryId?: string } = {},
|
|
421
657
|
) {
|
|
422
658
|
return selectionReferences(document, options).flatMap((ref) =>
|
|
423
659
|
sourceAnchors(ref.source).map((source) => ({ ...ref, source })),
|
|
@@ -425,12 +661,16 @@ export function sourceReferences(
|
|
|
425
661
|
}
|
|
426
662
|
|
|
427
663
|
/** Includes lenses and maps without inline ranges. */
|
|
428
|
-
|
|
664
|
+
/** Whether a board reads code at its own pins: a commit link or a block
|
|
665
|
+
* with pins of its own names its commits itself. */
|
|
666
|
+
export function readsAtBoardPins(review: {
|
|
429
667
|
document: Block[];
|
|
430
668
|
lenses?: readonly Lens[];
|
|
431
669
|
}): boolean {
|
|
432
670
|
return Boolean(
|
|
433
|
-
sourceReferences(review.document).
|
|
671
|
+
sourceReferences(review.document, { tolerant: true }).some(
|
|
672
|
+
(reference) => !reference.source.pins,
|
|
673
|
+
) ||
|
|
434
674
|
review.lenses?.length ||
|
|
435
675
|
resourceReferences(review.document).some(
|
|
436
676
|
(block) => block.type === "software_map",
|
|
@@ -514,6 +754,8 @@ export interface EditSummary {
|
|
|
514
754
|
units?: string[];
|
|
515
755
|
/** The agent whose courier draws this edit, when the host could tell. */
|
|
516
756
|
activityId?: string;
|
|
757
|
+
/** The Ask thread that made this edit, if any. */
|
|
758
|
+
threadId?: string;
|
|
517
759
|
}
|
|
518
760
|
|
|
519
761
|
/** A component an edit wrote, named so the author can address it. */
|
|
@@ -711,18 +953,33 @@ export function assignFreshIds(
|
|
|
711
953
|
element: Element,
|
|
712
954
|
allocate: (prefix: string) => string,
|
|
713
955
|
): void {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
element.id = allocate(idPrefix(element));
|
|
956
|
+
assignIds(element, allocate, true);
|
|
957
|
+
}
|
|
717
958
|
|
|
718
|
-
|
|
959
|
+
/** Give ids to the parts of an element tree that have none, keeping the
|
|
960
|
+
* rest: a person's save names the blocks it kept and leaves new ones bare. */
|
|
961
|
+
export function assignMissingIds(
|
|
962
|
+
element: Element,
|
|
963
|
+
allocate: (prefix: string) => string,
|
|
964
|
+
): void {
|
|
965
|
+
assignIds(element, allocate, false);
|
|
966
|
+
}
|
|
719
967
|
|
|
968
|
+
function assignIds(
|
|
969
|
+
element: Element,
|
|
970
|
+
allocate: (prefix: string) => string,
|
|
971
|
+
fresh: boolean,
|
|
972
|
+
): void {
|
|
720
973
|
const assign = (item: { id?: string }, prefix: string) => {
|
|
721
|
-
if (item.id
|
|
974
|
+
if (item.id === undefined) item.id = allocate(prefix);
|
|
975
|
+
else if (fresh)
|
|
722
976
|
throw new ReviewInputError("IDs are assigned by the server.");
|
|
723
|
-
item.id = allocate(prefix);
|
|
724
977
|
};
|
|
725
978
|
|
|
979
|
+
assign(element, idPrefix(element));
|
|
980
|
+
|
|
981
|
+
for (const child of children(element)) assignIds(child, allocate, fresh);
|
|
982
|
+
|
|
726
983
|
if (element.type === "call_stack_diff")
|
|
727
984
|
for (const frame of [...element.base, ...element.head])
|
|
728
985
|
assign(frame, "frame");
|
|
@@ -735,16 +992,10 @@ export function assignFreshIds(
|
|
|
735
992
|
}
|
|
736
993
|
}
|
|
737
994
|
|
|
738
|
-
export interface ApplyEditOptions {
|
|
739
|
-
/** Where a root-level insert with no placement lands: the end by default. */
|
|
740
|
-
placement?: "first" | "last";
|
|
741
|
-
}
|
|
742
|
-
|
|
743
995
|
export function applyEdit(
|
|
744
996
|
document: Block[],
|
|
745
997
|
edit: Edit,
|
|
746
998
|
allocate: (prefix: string) => string,
|
|
747
|
-
options: ApplyEditOptions = {},
|
|
748
999
|
): Applied {
|
|
749
1000
|
adoptFlowUnits(document, allocate);
|
|
750
1001
|
|
|
@@ -808,14 +1059,10 @@ export function applyEdit(
|
|
|
808
1059
|
// Resolve the destination before detaching, then compute its final position.
|
|
809
1060
|
if (from) from.splice(from.indexOf(element), 1);
|
|
810
1061
|
|
|
811
|
-
// A running log reads newest first, so a root insert with no placement
|
|
812
|
-
// may lead the document; a move or a unit inside a diagram never does.
|
|
813
1062
|
const index =
|
|
814
|
-
afterId
|
|
815
|
-
? siblings.
|
|
816
|
-
:
|
|
817
|
-
? 0
|
|
818
|
-
: siblings.length;
|
|
1063
|
+
afterId === undefined
|
|
1064
|
+
? siblings.length
|
|
1065
|
+
: siblings.findIndex((s) => s.id === afterId) + 1;
|
|
819
1066
|
|
|
820
1067
|
siblings.splice(index, 0, element);
|
|
821
1068
|
};
|