@dev.fast/whiteboard 0.3.4 → 0.3.5-preview.20261021.131
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{account-alias-CprZojXd.js → account-alias-CisiYUZT.js} +13 -8
- package/dist/agent-cli-BglNRDZN.js +2 -0
- package/dist/{agent-cli-CThN7Q59.js → agent-cli-TJ7_5_B3.js} +15 -10
- package/dist/{agent-client-BOOi7EUR.js → agent-client-H53ajvrm.js} +13 -3
- package/dist/{authoring-tools-BNwuMnz6.js → authoring-tools-BCquUsB3.js} +34 -27
- package/dist/{background-server-DLSDogkI.js → background-server-BSYXD9au.js} +1 -1
- package/dist/{background-server-CkvxA-6O.js → background-server-Xom8cA_u.js} +5 -5
- package/dist/build-info.json +1 -1
- package/dist/{cli-R4yfa5DT.js → cli-BOnV4cCh.js} +1 -1
- package/dist/{cli-runner-Cuc9FSzu.js → cli-runner-cRY5NOno.js} +70 -34
- package/dist/cli.js +4 -3
- package/dist/{client-BN_FDxFr.js → client-UH1bEG7Z.js} +260 -49
- package/dist/{fuzzy-match-BO9bK37N.js → fuzzy-match-D_x3wumC.js} +6 -4
- package/dist/{headless-host-CVBOQyRS.js → headless-host-BEe-aFtB.js} +1 -1
- package/dist/{headless-host-Cog6SpS9.js → headless-host-CEb5d-A0.js} +563 -175
- package/dist/lens-selection-BxPk53c8.js +214 -0
- package/dist/{local-data-D_9tvdsp.js → local-data-mbCb1HDH.js} +2448 -347
- package/dist/{mcp-D1hziANa.js → mcp-cBtzXsmN.js} +9 -12
- package/dist/{package-paths-C--__jEv.js → package-paths-BkeQsdaw.js} +1 -1
- package/dist/profile-Dbyidrcr.js +2 -0
- package/dist/{profile-DokFw782.js → profile-duqp5uPa.js} +20 -8
- package/dist/{remote-attach-CEJ-kvDY.js → remote-attach-BeKuMm_x.js} +4 -4
- package/dist/{remote-extensions-GN8Gf_qG.js → remote-extensions-CX2fAec1.js} +1 -1
- package/dist/{remote-extensions-Blcjr9Ze.js → remote-extensions-CYWNTVSf.js} +3 -3
- package/dist/{remote-language-server-BTKBQ7x4.js → remote-language-server-DLWluAeR.js} +5 -5
- package/dist/{remote-uninstall-DW7Gdryh.js → remote-uninstall-Y3S0YmJs.js} +6 -6
- package/dist/{request-origin-Zcn_CjX4.js → request-origin-icXPv6jh.js} +55 -18
- package/dist/{review-app-launcher-CkwLclzR.js → review-app-launcher-DZvG5ZAh.js} +2 -2
- package/dist/{review-home-paths-j2BRAaNv.js → review-home-paths-BwH08ihj.js} +19 -17
- package/dist/{review-telemetry-BLNwYnLV.js → review-telemetry-AZ0imsmJ.js} +6 -6
- package/dist/runtime-CF4JXdb8.js +27 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/{s3-DtJFWC0G.js → s3-CsG2EWED.js} +1 -1
- package/dist/{s3-config-BTO12tCC.js → s3-config-CjcLyFV5.js} +4 -2
- package/dist/{s3-config-CSy3fx8H.js → s3-config-zz4GWh2T.js} +1 -1
- package/dist/server/desktop-host.js +28 -104
- package/dist/{server-discovery-Bey8IqXH.js → server-discovery-DeVTdcqK.js} +3 -3
- package/dist/sharing/index.d.ts +484 -74
- package/dist/sharing/index.js +1 -1
- package/dist/{src-DsM3jO8G.js → src-DYZLCUP_.js} +98 -18
- package/dist/{src-BMPuxWv4.js → src-DtfSndqS.js} +269 -227
- package/dist/{stored-document-migration-CChxtFAy.js → stored-document-migration-B29lve9-.js} +318 -271
- package/dist/{thread-state-5pYr6_zM.js → thread-state-BPpvKFwQ.js} +12 -2
- package/dist/{tool-failure-C9MpbXJJ.js → tool-failure-CQhyLht3.js} +10 -3
- package/instructions/authoring.md +20 -5
- package/package.json +4 -4
- package/src/agent-selection.ts +6 -0
- package/src/ask/agents.ts +13 -1
- package/src/ask/checkout-files.ts +2 -5
- package/src/ask/thread-state.ts +4 -0
- package/src/ask/thread.ts +61 -31
- package/src/ask/threads.ts +24 -3
- package/src/cli-install.ts +13 -2
- package/src/cli-runner.ts +4 -1
- package/src/cli.ts +4 -1
- package/src/diff-selection-migration.ts +48 -34
- package/src/heading-anchors.ts +42 -0
- package/src/lens-selection.ts +38 -2
- package/src/markdown.ts +42 -0
- package/src/posthog-capture-client.ts +1 -1
- package/src/review-api/README.md +52 -13
- package/src/review-api/activity.ts +1 -4
- package/src/review-api/agent-cli.ts +9 -1
- package/src/review-api/anchor-quotes.ts +14 -7
- package/src/review-api/ask-history.ts +118 -51
- package/src/review-api/authoring-tools.ts +45 -30
- package/src/review-api/azure-devops-provider.ts +2 -20
- package/src/review-api/azure-remote.ts +21 -0
- package/src/review-api/blocks/definition.ts +1 -1
- package/src/review-api/blocks/markdown.ts +2 -2
- package/src/review-api/board-order.ts +144 -0
- package/src/review-api/checkout-groups.ts +586 -0
- package/src/review-api/document-headings.ts +58 -27
- package/src/review-api/document-text.ts +31 -2
- package/src/review-api/document.ts +362 -67
- package/src/review-api/foreign-references.ts +328 -0
- package/src/review-api/github-provider.ts +51 -13
- package/src/review-api/http.ts +734 -135
- package/src/review-api/instructions.ts +0 -19
- package/src/review-api/link-preview.ts +113 -0
- package/src/review-api/local-data.ts +310 -46
- package/src/review-api/mcp.ts +7 -13
- package/src/review-api/onboarding.ts +1 -1
- package/src/review-api/profile.ts +37 -6
- package/src/review-api/public-tools.ts +24 -2
- package/src/review-api/pull-request.ts +82 -49
- package/src/review-api/request-origin.ts +10 -0
- package/src/review-api/review-progress.ts +61 -27
- package/src/review-api/scratchpad-plan-upgrade.ts +250 -0
- package/src/review-api/store-migrations.ts +355 -0
- package/src/review-api/store-schema.ts +38 -29
- package/src/review-api/store.ts +2176 -495
- package/src/review-api/worktree-events.ts +3 -5
- package/src/review-api/worktree-source.ts +3 -7
- package/src/review-api/worktree-structural.integration.ts +72 -0
- package/src/review-stack.ts +15 -23
- package/src/runtime.ts +4 -30
- package/src/server/desktop-host.ts +6 -1
- package/src/server/desktop-server.ts +1 -36
- package/src/server/headless-host.ts +0 -2
- package/src/server/hono-http.ts +2 -1
- package/src/server/review-gateway-list.ts +3 -5
- package/src/server/review-gateway-memory.ts +1 -1
- package/src/server/review-lifecycle-telemetry.ts +2 -2
- package/src/server/review-server-core.ts +1 -5
- package/src/server/structural-comparisons.ts +47 -0
- package/src/sharing/export.ts +36 -7
- package/src/sharing/host.ts +34 -14
- package/src/sharing/import.ts +156 -24
- package/src/sharing/repository.ts +143 -24
- package/src/startup-trace.ts +3 -3
- package/src/stored-document-migration.ts +110 -19
- package/src/ui-telemetry-events.ts +44 -18
- package/dist/agent-cli-C6SPSII0.js +0 -2
- package/dist/profile-CYPmvBoA.js +0 -2
- package/dist/runtime-CKiWhGrf.js +0 -52
- package/instructions/scratchpad.md +0 -23
- package/src/review-preferences.ts +0 -88
- package/src/server/legacy-tutorial-cleanup.ts +0 -38
|
@@ -1,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,14 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type JsonValue,
|
|
3
|
+
isJsonArray,
|
|
4
|
+
isJsonObject,
|
|
5
|
+
isStringValue,
|
|
6
|
+
} from "@dev.fast/json";
|
|
1
7
|
import {
|
|
2
8
|
type Anchor,
|
|
3
9
|
type LensSource,
|
|
10
|
+
commitLinkTarget,
|
|
4
11
|
parseAnchor,
|
|
5
12
|
selectionProblem,
|
|
6
13
|
sourceAnchors,
|
|
7
14
|
} from "@review/lens-selection.js";
|
|
8
15
|
import {
|
|
16
|
+
type MarkdownNode,
|
|
17
|
+
linkDestinations,
|
|
9
18
|
markdownNodes,
|
|
10
19
|
markdownText,
|
|
11
20
|
parseMarkdown,
|
|
21
|
+
rewriteLinkDestinations,
|
|
12
22
|
} from "@review/markdown.js";
|
|
13
23
|
import {
|
|
14
24
|
type FileLineRange,
|
|
@@ -144,6 +154,11 @@ const targets = <Repository extends Record<string, z.ZodType>>(
|
|
|
144
154
|
repository: Repository,
|
|
145
155
|
) =>
|
|
146
156
|
z.discriminatedUnion("kind", [
|
|
157
|
+
/** The project of the repository, no change: a plan. */
|
|
158
|
+
z.strictObject({
|
|
159
|
+
kind: z.literal("project"),
|
|
160
|
+
...repository,
|
|
161
|
+
}),
|
|
147
162
|
z.strictObject({
|
|
148
163
|
kind: z.literal("worktree"),
|
|
149
164
|
...repository,
|
|
@@ -159,8 +174,16 @@ const targets = <Repository extends Record<string, z.ZodType>>(
|
|
|
159
174
|
|
|
160
175
|
export const reviewTargetSchema = targets({ repositoryId: label });
|
|
161
176
|
|
|
162
|
-
/**
|
|
163
|
-
|
|
177
|
+
/** Targets as agents write them: a checkout path instead of a repository id,
|
|
178
|
+
* plus a PR by URL or number. */
|
|
179
|
+
export const pathTargetSchema = z.discriminatedUnion("kind", [
|
|
180
|
+
...targets({ repositoryPath: label }).options,
|
|
181
|
+
z.strictObject({
|
|
182
|
+
kind: z.literal("pullRequest"),
|
|
183
|
+
repositoryPath: label,
|
|
184
|
+
pullRequest: z.union([label, z.number().int().positive()]),
|
|
185
|
+
}),
|
|
186
|
+
]);
|
|
164
187
|
|
|
165
188
|
export type ReviewTarget = z.infer<typeof reviewTargetSchema>;
|
|
166
189
|
|
|
@@ -175,11 +198,131 @@ const unitTypes = new Set(["step", "flow_node", "flow_edge"]);
|
|
|
175
198
|
export const isUnit = (element: Element): element is Unit =>
|
|
176
199
|
unitTypes.has(element.type);
|
|
177
200
|
|
|
201
|
+
/** Rewrites old review-source: and review-trace: links to their whiteboard: form. */
|
|
202
|
+
export function canonicalLink(href: string): string {
|
|
203
|
+
return href.replace(
|
|
204
|
+
/^review-(source|trace):/i,
|
|
205
|
+
(_, kind: string) => `whiteboard:${kind.toLowerCase()}/`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Markdown link destinations and definitions that begin with `target`, a
|
|
210
|
+
* regex source; the match's first group is what comes before it. */
|
|
211
|
+
const linkDestination = (target: string) =>
|
|
212
|
+
new RegExp(String.raw`(\]\(\s*<?|^\s*\[[^\]\n]+\]:\s*<?)` + target, "gim");
|
|
213
|
+
|
|
214
|
+
function rewriteMarkdown<T>(
|
|
215
|
+
value: T,
|
|
216
|
+
rewrite: (markdown: string) => string,
|
|
217
|
+
): T {
|
|
218
|
+
const visit = (node: JsonValue) => {
|
|
219
|
+
if (isJsonArray(node)) node.forEach(visit);
|
|
220
|
+
else if (isJsonObject(node))
|
|
221
|
+
for (const [key, field] of Object.entries(node))
|
|
222
|
+
if (key === "markdown" && isStringValue(field)) {
|
|
223
|
+
const rewritten = rewrite(field);
|
|
224
|
+
|
|
225
|
+
if (rewritten !== field) node[key] = rewritten;
|
|
226
|
+
} else visit(field);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// SAFETY: documents and edits are JSON; only Markdown text changes.
|
|
230
|
+
visit(value as JsonValue);
|
|
231
|
+
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const retiredLinks = linkDestination("review-(source|trace):");
|
|
236
|
+
|
|
237
|
+
/** Rewrites old link prefixes in link destinations only, not code. */
|
|
238
|
+
export function respellLinks(markdown: string): string {
|
|
239
|
+
if (!/review-(source|trace):/i.test(markdown)) return markdown;
|
|
240
|
+
|
|
241
|
+
return rewriteLinkDestinations(markdown, (span) =>
|
|
242
|
+
span.replace(
|
|
243
|
+
retiredLinks,
|
|
244
|
+
(_, before: string, kind: string) =>
|
|
245
|
+
`${before}whiteboard:${kind.toLowerCase()}/`,
|
|
246
|
+
),
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Every Markdown text in `document` with its links spelled the current
|
|
251
|
+
* way. */
|
|
252
|
+
export function respellDocumentLinks<T>(document: T): T {
|
|
253
|
+
return rewriteMarkdown(document, respellLinks);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function respellEditLinks(edit: Edit): Edit {
|
|
257
|
+
return rewriteMarkdown(edit, respellLinks);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Names a block from outside its board. Block ids are unique only within a board. */
|
|
261
|
+
export type BlockRef = { board: string; block: string };
|
|
262
|
+
|
|
263
|
+
export type BoardRef = BlockRef | { board: string; block?: undefined };
|
|
264
|
+
|
|
265
|
+
/** Parses whiteboard:board/<id> or whiteboard:board/<id>#<blockId>. */
|
|
266
|
+
export function boardLink(href: string): BoardRef | undefined {
|
|
267
|
+
const match = /^whiteboard:board\/([^#/]+)(?:#(.+))?$/.exec(href);
|
|
268
|
+
|
|
269
|
+
if (!match) return undefined;
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const board = decodeURIComponent(match[1]!);
|
|
273
|
+
|
|
274
|
+
return match[2] === undefined
|
|
275
|
+
? { board }
|
|
276
|
+
: { board, block: decodeURIComponent(match[2]) };
|
|
277
|
+
} catch {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** A link from a block to a board or one of its blocks. */
|
|
283
|
+
export interface BoardLinkRef {
|
|
284
|
+
from: BlockRef;
|
|
285
|
+
to: BoardRef;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const parsedBlocks = new WeakMap<
|
|
289
|
+
object,
|
|
290
|
+
{ markdown: string; nodes: MarkdownNode[] }
|
|
291
|
+
>();
|
|
292
|
+
|
|
293
|
+
/** Parsed once per text; a save reads links, sources and resources from
|
|
294
|
+
* it. */
|
|
295
|
+
function blockNodes(block: { markdown: string }): MarkdownNode[] {
|
|
296
|
+
const cached = parsedBlocks.get(block);
|
|
297
|
+
|
|
298
|
+
if (cached?.markdown === block.markdown) return cached.nodes;
|
|
299
|
+
|
|
300
|
+
const nodes = [...markdownNodes(parseMarkdown(block.markdown))];
|
|
301
|
+
|
|
302
|
+
parsedBlocks.set(block, { markdown: block.markdown, nodes });
|
|
303
|
+
|
|
304
|
+
return nodes;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Every board link in a document. Malformed links are skipped; writes reject them. */
|
|
308
|
+
export function boardLinks(board: string, document: Block[]): BoardLinkRef[] {
|
|
309
|
+
return elements(document).flatMap((element) =>
|
|
310
|
+
element.type === "markdown"
|
|
311
|
+
? blockNodes(element).flatMap((node) => {
|
|
312
|
+
const to =
|
|
313
|
+
node.type === "link" ? boardLink(node.url ?? "") : undefined;
|
|
314
|
+
|
|
315
|
+
return to ? [{ from: { board, block: element.id! }, to }] : [];
|
|
316
|
+
})
|
|
317
|
+
: [],
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
178
321
|
/** Inline trace quotes keep paragraph/list layout while using retained resources. */
|
|
179
322
|
export function traceQuoteLink(
|
|
180
323
|
href: string,
|
|
181
324
|
): { traceId: string; eventId: string } | undefined {
|
|
182
|
-
const match = /^
|
|
325
|
+
const match = /^whiteboard:trace\/([^#]+)#(.+)$/.exec(canonicalLink(href));
|
|
183
326
|
|
|
184
327
|
return match
|
|
185
328
|
? {
|
|
@@ -213,17 +356,18 @@ export function resourceReferences(document: Block[]): Block[] {
|
|
|
213
356
|
? [block]
|
|
214
357
|
: [];
|
|
215
358
|
|
|
216
|
-
return
|
|
217
|
-
(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
359
|
+
return blockNodes(block).flatMap((node): Block[] => {
|
|
360
|
+
if (
|
|
361
|
+
node.type !== "link" ||
|
|
362
|
+
!canonicalLink(node.url ?? "").startsWith("whiteboard:trace/")
|
|
363
|
+
)
|
|
364
|
+
return [];
|
|
365
|
+
const quote = traceQuoteLink(node.url ?? "");
|
|
221
366
|
|
|
222
|
-
|
|
367
|
+
if (!quote) throw new ReviewInputError("Invalid trace quote link.");
|
|
223
368
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
);
|
|
369
|
+
return [{ type: "trace_quote", ...quote, text: markdownText(node) }];
|
|
370
|
+
});
|
|
227
371
|
});
|
|
228
372
|
}
|
|
229
373
|
|
|
@@ -237,7 +381,13 @@ function documentReferences(
|
|
|
237
381
|
{
|
|
238
382
|
tolerant = false,
|
|
239
383
|
markdown = true,
|
|
240
|
-
|
|
384
|
+
repositoryId,
|
|
385
|
+
}: {
|
|
386
|
+
tolerant?: boolean;
|
|
387
|
+
markdown?: boolean;
|
|
388
|
+
/** The board's repository, which commit links read from. */
|
|
389
|
+
repositoryId?: string;
|
|
390
|
+
} = {},
|
|
241
391
|
): {
|
|
242
392
|
id: string;
|
|
243
393
|
source: LensSource;
|
|
@@ -271,46 +421,68 @@ function documentReferences(
|
|
|
271
421
|
if (element.type === "markdown" && !markdown) return [];
|
|
272
422
|
|
|
273
423
|
if (element.type === "markdown")
|
|
274
|
-
return
|
|
275
|
-
(node)
|
|
276
|
-
|
|
277
|
-
|
|
424
|
+
return blockNodes(element).flatMap((node) => {
|
|
425
|
+
if (node.type !== "link") return [];
|
|
426
|
+
const href = canonicalLink(node.url ?? "");
|
|
427
|
+
|
|
428
|
+
if (!href.startsWith("whiteboard:source/")) {
|
|
429
|
+
// Trace links are checked by resourceReferences.
|
|
430
|
+
if (href.startsWith("whiteboard:trace/")) return [];
|
|
431
|
+
|
|
432
|
+
// Board links are checked against the store on write.
|
|
433
|
+
if (href.startsWith("whiteboard:board/"))
|
|
434
|
+
return boardLink(href)
|
|
435
|
+
? []
|
|
436
|
+
: reject(
|
|
437
|
+
`Use whiteboard:board/<sessionId> or whiteboard:board/<sessionId>#<blockId> to link a board, in block ${element.id}.`,
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
if (/^(?:https?:\/\/|mailto:|#)/i.test(href)) return [];
|
|
441
|
+
|
|
442
|
+
return reject(
|
|
443
|
+
`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.`,
|
|
444
|
+
);
|
|
445
|
+
}
|
|
278
446
|
|
|
279
|
-
|
|
280
|
-
// Trace links are checked by resourceReferences.
|
|
281
|
-
if (href.startsWith("review-trace:")) return [];
|
|
447
|
+
let target: string;
|
|
282
448
|
|
|
283
|
-
|
|
449
|
+
try {
|
|
450
|
+
target = decodeURIComponent(href.slice("whiteboard:source/".length));
|
|
451
|
+
} catch {
|
|
452
|
+
return reject("Invalid URL encoding in source link.");
|
|
453
|
+
}
|
|
284
454
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
);
|
|
288
|
-
}
|
|
455
|
+
// A commit of the board's repository: the commit is the link's pins.
|
|
456
|
+
const commit = commitLinkTarget(target);
|
|
289
457
|
|
|
290
|
-
|
|
458
|
+
if (commit)
|
|
459
|
+
return repositoryId
|
|
460
|
+
? select(commit.anchor, {
|
|
461
|
+
repositoryId,
|
|
462
|
+
head: commit.commit,
|
|
463
|
+
}).map((source) => ({
|
|
464
|
+
id: `${element.id}:${node.url}`,
|
|
465
|
+
source,
|
|
466
|
+
}))
|
|
467
|
+
: reject(
|
|
468
|
+
`A commit link names a commit of the board's repository, and this board has none, in block ${element.id}.`,
|
|
469
|
+
);
|
|
291
470
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
471
|
+
// Links share the anchor grammar blocks use.
|
|
472
|
+
const anchor = target.replace(/^(head|base|diff)\//i, (side) =>
|
|
473
|
+
side.toLowerCase(),
|
|
474
|
+
);
|
|
297
475
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
476
|
+
if (!parseAnchor(anchor))
|
|
477
|
+
return reject(
|
|
478
|
+
"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.",
|
|
301
479
|
);
|
|
302
480
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
return select(anchor, element.pins).map((source) => ({
|
|
309
|
-
id: `${element.id}:${node.url}`,
|
|
310
|
-
source,
|
|
311
|
-
}));
|
|
312
|
-
},
|
|
313
|
-
);
|
|
481
|
+
return select(anchor, element.pins).map((source) => ({
|
|
482
|
+
id: `${element.id}:${node.url}`,
|
|
483
|
+
source,
|
|
484
|
+
}));
|
|
485
|
+
});
|
|
314
486
|
|
|
315
487
|
if (element.type === "code_peek")
|
|
316
488
|
return select(element.source, element.pins).map((source) => ({
|
|
@@ -414,10 +586,122 @@ function documentReferences(
|
|
|
414
586
|
/** All authored attachments, including code peeks, select the aligned diff. */
|
|
415
587
|
export const selectionReferences = documentReferences;
|
|
416
588
|
|
|
589
|
+
const commitLinkDestination = linkDestination(
|
|
590
|
+
String.raw`whiteboard:source\/([0-9a-f]{7,40})\/`,
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
const hasCommitLink = /whiteboard:source\//i;
|
|
594
|
+
|
|
595
|
+
/** Commits named by commit links, not by code samples. */
|
|
596
|
+
export function commitLinkCommits(document: Block[]): string[] {
|
|
597
|
+
const commits = new Set<string>();
|
|
598
|
+
|
|
599
|
+
for (const element of elements(document))
|
|
600
|
+
if (element.type === "markdown" && hasCommitLink.test(element.markdown))
|
|
601
|
+
for (const { from, to } of linkDestinations(blockNodes(element)))
|
|
602
|
+
for (const match of element.markdown
|
|
603
|
+
.slice(from, to)
|
|
604
|
+
.matchAll(commitLinkDestination))
|
|
605
|
+
commits.add(match[2]!);
|
|
606
|
+
|
|
607
|
+
return [...commits];
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** Rewrites the commits Markdown commit links name, as a short commit
|
|
611
|
+
* becomes the full one on save. */
|
|
612
|
+
export function respellCommitLinks(
|
|
613
|
+
document: Block[],
|
|
614
|
+
commits: ReadonlyMap<string, string>,
|
|
615
|
+
): Block[] {
|
|
616
|
+
return rewriteMarkdown(document, (markdown) =>
|
|
617
|
+
hasCommitLink.test(markdown)
|
|
618
|
+
? rewriteLinkDestinations(markdown, (span) =>
|
|
619
|
+
span.replace(
|
|
620
|
+
commitLinkDestination,
|
|
621
|
+
(_, before: string, commit: string) =>
|
|
622
|
+
`${before}whiteboard:source/${commits.get(commit) ?? commit}/`,
|
|
623
|
+
),
|
|
624
|
+
)
|
|
625
|
+
: markdown,
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** Blocks that carry pins of their own for the references in them. */
|
|
630
|
+
const pinnedBlockTypes = new Set([
|
|
631
|
+
"markdown",
|
|
632
|
+
"code_peek",
|
|
633
|
+
"sequence",
|
|
634
|
+
"flow_diagram",
|
|
635
|
+
"call_stack_diff",
|
|
636
|
+
"database_lens",
|
|
637
|
+
]);
|
|
638
|
+
|
|
639
|
+
const hasSideLink = /whiteboard:source\/(head|base)\//i;
|
|
640
|
+
|
|
641
|
+
const sideLinkDestination = linkDestination(
|
|
642
|
+
String.raw`whiteboard:source\/(head|base)\/`,
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Makes every reference that reads at the document's pins carry them
|
|
647
|
+
* itself, for a board about to lose its pins: Markdown head/ and base/
|
|
648
|
+
* links become commit links, and a block whose references still lean on
|
|
649
|
+
* the document's pins (any anchor, or a diff/ link) takes them as its own.
|
|
650
|
+
* Nothing changes what it points at.
|
|
651
|
+
*/
|
|
652
|
+
export function pinDocumentReferences(document: Block[], pins: Pins): Block[] {
|
|
653
|
+
const own: SourcePins = {
|
|
654
|
+
repositoryId: pins.repositoryId,
|
|
655
|
+
base: pins.base,
|
|
656
|
+
head: pins.head,
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const pinned = structuredClone(document);
|
|
660
|
+
|
|
661
|
+
for (const element of elements(pinned)) {
|
|
662
|
+
if (!pinnedBlockTypes.has(element.type)) continue;
|
|
663
|
+
|
|
664
|
+
// SAFETY: every pinned block type has optional source pins of its own.
|
|
665
|
+
const block = element as Block & { pins?: SourcePins };
|
|
666
|
+
|
|
667
|
+
if (block.pins) continue;
|
|
668
|
+
|
|
669
|
+
if (block.type === "markdown") {
|
|
670
|
+
block.markdown = respellLinks(block.markdown);
|
|
671
|
+
|
|
672
|
+
if (hasSideLink.test(block.markdown))
|
|
673
|
+
block.markdown = rewriteLinkDestinations(block.markdown, (span) =>
|
|
674
|
+
span.replace(
|
|
675
|
+
sideLinkDestination,
|
|
676
|
+
(_, before: string, side: string) =>
|
|
677
|
+
`${before}whiteboard:source/${side.toLowerCase() === "base" ? pins.base : pins.head}/`,
|
|
678
|
+
),
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (
|
|
683
|
+
documentReferences([block], { tolerant: true }).some(
|
|
684
|
+
(reference) => !reference.source.pins,
|
|
685
|
+
)
|
|
686
|
+
)
|
|
687
|
+
block.pins = own;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return pinned;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** The repository a board's commit links read from: its target's. */
|
|
694
|
+
export function boardRepositoryId(board: {
|
|
695
|
+
target?: { repositoryId: string };
|
|
696
|
+
pins?: { repositoryId: string };
|
|
697
|
+
}): string | undefined {
|
|
698
|
+
return board.target?.repositoryId ?? board.pins?.repositoryId;
|
|
699
|
+
}
|
|
700
|
+
|
|
417
701
|
/** Per-side read coordinates for endpoint validation and retained source quotes. */
|
|
418
702
|
export function sourceReferences(
|
|
419
703
|
document: Block[],
|
|
420
|
-
options: { tolerant?: boolean } = {},
|
|
704
|
+
options: { tolerant?: boolean; repositoryId?: string } = {},
|
|
421
705
|
) {
|
|
422
706
|
return selectionReferences(document, options).flatMap((ref) =>
|
|
423
707
|
sourceAnchors(ref.source).map((source) => ({ ...ref, source })),
|
|
@@ -425,12 +709,16 @@ export function sourceReferences(
|
|
|
425
709
|
}
|
|
426
710
|
|
|
427
711
|
/** Includes lenses and maps without inline ranges. */
|
|
428
|
-
|
|
712
|
+
/** Whether a board reads code at its own pins: a commit link or a block
|
|
713
|
+
* with pins of its own names its commits itself. */
|
|
714
|
+
export function readsAtBoardPins(review: {
|
|
429
715
|
document: Block[];
|
|
430
716
|
lenses?: readonly Lens[];
|
|
431
717
|
}): boolean {
|
|
432
718
|
return Boolean(
|
|
433
|
-
sourceReferences(review.document).
|
|
719
|
+
sourceReferences(review.document, { tolerant: true }).some(
|
|
720
|
+
(reference) => !reference.source.pins,
|
|
721
|
+
) ||
|
|
434
722
|
review.lenses?.length ||
|
|
435
723
|
resourceReferences(review.document).some(
|
|
436
724
|
(block) => block.type === "software_map",
|
|
@@ -514,6 +802,8 @@ export interface EditSummary {
|
|
|
514
802
|
units?: string[];
|
|
515
803
|
/** The agent whose courier draws this edit, when the host could tell. */
|
|
516
804
|
activityId?: string;
|
|
805
|
+
/** The Ask thread that made this edit, if any. */
|
|
806
|
+
threadId?: string;
|
|
517
807
|
}
|
|
518
808
|
|
|
519
809
|
/** A component an edit wrote, named so the author can address it. */
|
|
@@ -711,18 +1001,33 @@ export function assignFreshIds(
|
|
|
711
1001
|
element: Element,
|
|
712
1002
|
allocate: (prefix: string) => string,
|
|
713
1003
|
): void {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
element.id = allocate(idPrefix(element));
|
|
1004
|
+
assignIds(element, allocate, true);
|
|
1005
|
+
}
|
|
717
1006
|
|
|
718
|
-
|
|
1007
|
+
/** Give ids to the parts of an element tree that have none, keeping the
|
|
1008
|
+
* rest: a person's save names the blocks it kept and leaves new ones bare. */
|
|
1009
|
+
export function assignMissingIds(
|
|
1010
|
+
element: Element,
|
|
1011
|
+
allocate: (prefix: string) => string,
|
|
1012
|
+
): void {
|
|
1013
|
+
assignIds(element, allocate, false);
|
|
1014
|
+
}
|
|
719
1015
|
|
|
1016
|
+
function assignIds(
|
|
1017
|
+
element: Element,
|
|
1018
|
+
allocate: (prefix: string) => string,
|
|
1019
|
+
fresh: boolean,
|
|
1020
|
+
): void {
|
|
720
1021
|
const assign = (item: { id?: string }, prefix: string) => {
|
|
721
|
-
if (item.id
|
|
1022
|
+
if (item.id === undefined) item.id = allocate(prefix);
|
|
1023
|
+
else if (fresh)
|
|
722
1024
|
throw new ReviewInputError("IDs are assigned by the server.");
|
|
723
|
-
item.id = allocate(prefix);
|
|
724
1025
|
};
|
|
725
1026
|
|
|
1027
|
+
assign(element, idPrefix(element));
|
|
1028
|
+
|
|
1029
|
+
for (const child of children(element)) assignIds(child, allocate, fresh);
|
|
1030
|
+
|
|
726
1031
|
if (element.type === "call_stack_diff")
|
|
727
1032
|
for (const frame of [...element.base, ...element.head])
|
|
728
1033
|
assign(frame, "frame");
|
|
@@ -735,16 +1040,10 @@ export function assignFreshIds(
|
|
|
735
1040
|
}
|
|
736
1041
|
}
|
|
737
1042
|
|
|
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
1043
|
export function applyEdit(
|
|
744
1044
|
document: Block[],
|
|
745
1045
|
edit: Edit,
|
|
746
1046
|
allocate: (prefix: string) => string,
|
|
747
|
-
options: ApplyEditOptions = {},
|
|
748
1047
|
): Applied {
|
|
749
1048
|
adoptFlowUnits(document, allocate);
|
|
750
1049
|
|
|
@@ -808,14 +1107,10 @@ export function applyEdit(
|
|
|
808
1107
|
// Resolve the destination before detaching, then compute its final position.
|
|
809
1108
|
if (from) from.splice(from.indexOf(element), 1);
|
|
810
1109
|
|
|
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
1110
|
const index =
|
|
814
|
-
afterId
|
|
815
|
-
? siblings.
|
|
816
|
-
:
|
|
817
|
-
? 0
|
|
818
|
-
: siblings.length;
|
|
1111
|
+
afterId === undefined
|
|
1112
|
+
? siblings.length
|
|
1113
|
+
: siblings.findIndex((s) => s.id === afterId) + 1;
|
|
819
1114
|
|
|
820
1115
|
siblings.splice(index, 0, element);
|
|
821
1116
|
};
|