@dev.fast/whiteboard 0.3.4-preview.20261019.126 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{account-alias-B5-HYi1D.js → account-alias-CprZojXd.js} +8 -13
- package/dist/agent-cli-C6SPSII0.js +2 -0
- package/dist/{agent-cli-CohHf6Ua.js → agent-cli-CThN7Q59.js} +10 -15
- package/dist/{agent-client-Dtc-Kn_w.js → agent-client-BOOi7EUR.js} +3 -13
- package/dist/{authoring-tools-B7KU04f7.js → authoring-tools-BNwuMnz6.js} +27 -34
- package/dist/{background-server-C03RKxKS.js → background-server-CkvxA-6O.js} +5 -5
- package/dist/{background-server-CB9C4uoc.js → background-server-DLSDogkI.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/{cli-CJaEb7zC.js → cli-R4yfa5DT.js} +1 -1
- package/dist/{cli-runner-DGDIqh3V.js → cli-runner-Cuc9FSzu.js} +26 -26
- package/dist/cli.js +3 -4
- package/dist/{client-lKVF_Elo.js → client-BN_FDxFr.js} +47 -253
- package/dist/{fuzzy-match-BmVJJm9I.js → fuzzy-match-BO9bK37N.js} +4 -6
- package/dist/{headless-host-DRtjf9WA.js → headless-host-CVBOQyRS.js} +1 -1
- package/dist/{headless-host-XhL1X4vX.js → headless-host-Cog6SpS9.js} +140 -513
- package/dist/{local-data-Cpl4ENbJ.js → local-data-D_9tvdsp.js} +323 -2419
- package/dist/{mcp-ovmiUE_x.js → mcp-D1hziANa.js} +9 -5
- package/dist/{package-paths-BWxO-UEL.js → package-paths-C--__jEv.js} +1 -1
- package/dist/profile-CYPmvBoA.js +2 -0
- package/dist/{profile-BadSkCkI.js → profile-DokFw782.js} +8 -20
- package/dist/{remote-attach-Dd-YobM6.js → remote-attach-CEJ-kvDY.js} +4 -4
- package/dist/{remote-extensions-CNXwqK0D.js → remote-extensions-Blcjr9Ze.js} +3 -3
- package/dist/{remote-extensions-VP-kfq0C.js → remote-extensions-GN8Gf_qG.js} +1 -1
- package/dist/{remote-language-server-BXk87KCr.js → remote-language-server-BTKBQ7x4.js} +5 -5
- package/dist/{remote-uninstall-DuOzsvaD.js → remote-uninstall-DW7Gdryh.js} +6 -6
- package/dist/{request-origin-B9qCPiVB.js → request-origin-Zcn_CjX4.js} +12 -45
- package/dist/{review-app-launcher-C7el2Uoy.js → review-app-launcher-CkwLclzR.js} +2 -2
- package/dist/{review-home-paths-D8AoCdd2.js → review-home-paths-j2BRAaNv.js} +17 -19
- package/dist/{review-telemetry-BsoRMC2w.js → review-telemetry-BLNwYnLV.js} +5 -5
- package/dist/runtime-CKiWhGrf.js +52 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/{s3-CsG2EWED.js → s3-DtJFWC0G.js} +1 -1
- package/dist/{s3-config-CjcLyFV5.js → s3-config-BTO12tCC.js} +2 -4
- package/dist/{s3-config-zz4GWh2T.js → s3-config-CSy3fx8H.js} +1 -1
- package/dist/server/desktop-host.js +76 -29
- package/dist/{server-discovery-CKjw3-6h.js → server-discovery-Bey8IqXH.js} +3 -3
- package/dist/sharing/index.d.ts +70 -484
- package/dist/sharing/index.js +1 -1
- package/dist/{src-WPaQn75m.js → src-BMPuxWv4.js} +73 -208
- package/dist/{src-BiWNbcF_.js → src-DsM3jO8G.js} +12 -89
- package/dist/{stored-document-migration-dFbewRTG.js → stored-document-migration-CChxtFAy.js} +269 -270
- package/dist/{thread-state-Do5LX10I.js → thread-state-5pYr6_zM.js} +2 -7
- package/dist/{tool-failure-DvI-gys2.js → tool-failure-C9MpbXJJ.js} +3 -10
- package/instructions/authoring.md +2 -3
- package/instructions/scratchpad.md +23 -0
- package/package.json +4 -4
- package/src/ask/agents.ts +1 -13
- package/src/ask/checkout-files.ts +5 -2
- package/src/ask/thread-state.ts +0 -4
- package/src/ask/thread.ts +0 -16
- package/src/ask/threads.ts +3 -24
- package/src/cli-install.ts +2 -13
- package/src/cli-runner.ts +1 -4
- package/src/cli.ts +1 -4
- package/src/diff-selection-migration.ts +34 -48
- package/src/lens-selection.ts +2 -38
- package/src/review-api/README.md +13 -52
- package/src/review-api/activity.ts +4 -1
- package/src/review-api/agent-cli.ts +1 -9
- package/src/review-api/anchor-quotes.ts +7 -14
- package/src/review-api/ask-history.ts +51 -137
- package/src/review-api/authoring-tools.ts +30 -45
- package/src/review-api/blocks/definition.ts +1 -1
- package/src/review-api/blocks/markdown.ts +2 -2
- package/src/review-api/document-headings.ts +27 -58
- package/src/review-api/document-text.ts +2 -31
- package/src/review-api/document.ts +34 -281
- package/src/review-api/github-provider.ts +13 -51
- package/src/review-api/http.ts +123 -702
- package/src/review-api/instructions.ts +19 -0
- package/src/review-api/local-data.ts +46 -310
- package/src/review-api/mcp.ts +11 -3
- package/src/review-api/onboarding.ts +1 -1
- package/src/review-api/profile.ts +6 -37
- package/src/review-api/public-tools.ts +2 -24
- package/src/review-api/pull-request.ts +49 -82
- package/src/review-api/request-origin.ts +0 -10
- package/src/review-api/review-progress.ts +27 -61
- package/src/review-api/store-schema.ts +29 -38
- package/src/review-api/store.ts +478 -2142
- package/src/review-api/worktree-events.ts +5 -3
- package/src/review-api/worktree-source.ts +7 -3
- package/src/review-api/worktree-structural.integration.ts +0 -72
- package/src/review-preferences.ts +88 -0
- package/src/review-stack.ts +23 -15
- package/src/runtime.ts +30 -4
- package/src/server/desktop-host.ts +1 -6
- package/src/server/desktop-server.ts +31 -1
- package/src/server/headless-host.ts +2 -0
- package/src/server/hono-http.ts +1 -2
- package/src/server/legacy-tutorial-cleanup.ts +3 -2
- package/src/server/review-gateway-list.ts +5 -3
- package/src/server/review-gateway-memory.ts +1 -1
- package/src/server/review-lifecycle-telemetry.ts +2 -2
- package/src/server/review-server-core.ts +5 -1
- package/src/server/structural-comparisons.ts +0 -47
- package/src/sharing/export.ts +7 -36
- package/src/sharing/host.ts +14 -34
- package/src/sharing/import.ts +24 -156
- package/src/sharing/repository.ts +20 -128
- package/src/startup-trace.ts +3 -3
- package/src/stored-document-migration.ts +19 -110
- package/src/ui-telemetry-events.ts +11 -33
- package/dist/agent-cli-_YyMIMRY.js +0 -2
- package/dist/lens-selection-CyJapcsl.js +0 -214
- package/dist/profile-DJvkBJNQ.js +0 -2
- package/dist/runtime-Bj2tD33R.js +0 -27
- package/src/heading-anchors.ts +0 -42
- package/src/review-api/board-order.ts +0 -144
- package/src/review-api/checkout-groups.ts +0 -560
- package/src/review-api/foreign-references.ts +0 -328
- package/src/review-api/link-preview.ts +0 -113
- package/src/review-api/scratchpad-plan-upgrade.ts +0 -250
- package/src/review-api/store-migrations.ts +0 -357
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
|
|
3
|
-
import { type JsonValue, isJsonObject, isStringValue } from "@dev.fast/json";
|
|
4
|
-
import { parseAnchor } from "@review/lens-selection.js";
|
|
5
|
-
import {
|
|
6
|
-
markdownNodes,
|
|
7
|
-
markdownText,
|
|
8
|
-
parseMarkdown,
|
|
9
|
-
} from "@review/markdown.js";
|
|
10
|
-
|
|
11
|
-
/** A file's text at a commit; undefined when it can't be read. */
|
|
12
|
-
export type ReadAtCommit = (
|
|
13
|
-
repositoryId: string,
|
|
14
|
-
commit: string,
|
|
15
|
-
file: string,
|
|
16
|
-
) => string | undefined;
|
|
17
|
-
|
|
18
|
-
const LANGUAGES = new Map([
|
|
19
|
-
[".ts", "typescript"],
|
|
20
|
-
[".tsx", "tsx"],
|
|
21
|
-
[".js", "javascript"],
|
|
22
|
-
[".jsx", "jsx"],
|
|
23
|
-
[".mjs", "javascript"],
|
|
24
|
-
[".rs", "rust"],
|
|
25
|
-
[".go", "go"],
|
|
26
|
-
[".py", "python"],
|
|
27
|
-
[".rb", "ruby"],
|
|
28
|
-
[".java", "java"],
|
|
29
|
-
[".kt", "kotlin"],
|
|
30
|
-
[".swift", "swift"],
|
|
31
|
-
[".c", "c"],
|
|
32
|
-
[".h", "c"],
|
|
33
|
-
[".cc", "cpp"],
|
|
34
|
-
[".cpp", "cpp"],
|
|
35
|
-
[".css", "css"],
|
|
36
|
-
[".html", "html"],
|
|
37
|
-
[".json", "json"],
|
|
38
|
-
[".md", "markdown"],
|
|
39
|
-
[".sh", "bash"],
|
|
40
|
-
[".sql", "sql"],
|
|
41
|
-
[".toml", "toml"],
|
|
42
|
-
[".yaml", "yaml"],
|
|
43
|
-
[".yml", "yaml"],
|
|
44
|
-
]);
|
|
45
|
-
|
|
46
|
-
/** A source link in Markdown, in either spelling. */
|
|
47
|
-
const SOURCE_LINK = /^(?:whiteboard:source\/|review-source:)/i;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Migrates the old global scratchpad, which quoted several repositories.
|
|
51
|
-
* A board may only quote its own, so each quote of another repository is
|
|
52
|
-
* replaced:
|
|
53
|
-
* - a code peek becomes a `code` block of those lines;
|
|
54
|
-
* - a source link becomes its text, then what it pointed at in code;
|
|
55
|
-
* - a diagram attachment leaves the diagram and its lines follow it as
|
|
56
|
-
* a `code` block;
|
|
57
|
-
* - a step keeps its label and takes the lines as its code;
|
|
58
|
-
* - a call stack or database lens becomes a line saying what it was,
|
|
59
|
-
* then a `code` block for each range it quoted, in any repository; the
|
|
60
|
-
* first takes its id. A lens that quotes only its own repository is
|
|
61
|
-
* kept.
|
|
62
|
-
* New blocks take ids from `allocate`.
|
|
63
|
-
*/
|
|
64
|
-
export function snapshotForeignReferences(
|
|
65
|
-
document: JsonValue[],
|
|
66
|
-
own: string,
|
|
67
|
-
read: ReadAtCommit,
|
|
68
|
-
repositoryName: (repositoryId: string) => string,
|
|
69
|
-
allocate: (prefix: string) => string,
|
|
70
|
-
): JsonValue[] {
|
|
71
|
-
const foreign = (pins: JsonValue | undefined) =>
|
|
72
|
-
isJsonObject(pins) &&
|
|
73
|
-
isStringValue(pins.repositoryId) &&
|
|
74
|
-
pins.repositoryId !== own
|
|
75
|
-
? pins
|
|
76
|
-
: undefined;
|
|
77
|
-
|
|
78
|
-
const caption = (
|
|
79
|
-
pins: { repositoryId: string; head: string; base?: string },
|
|
80
|
-
anchor: string,
|
|
81
|
-
) => {
|
|
82
|
-
const selection = parseAnchor(anchor);
|
|
83
|
-
|
|
84
|
-
const commit =
|
|
85
|
-
selection?.start.side === "base" ? (pins.base ?? pins.head) : pins.head;
|
|
86
|
-
|
|
87
|
-
const range = selection
|
|
88
|
-
? `${selection.file}:${selection.start.line}-${selection.end.line}`
|
|
89
|
-
: anchor;
|
|
90
|
-
|
|
91
|
-
return `${repositoryName(pins.repositoryId)} · ${range} @ ${commit.slice(0, 7)}`;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const snapshot = (
|
|
95
|
-
id: JsonValue | undefined,
|
|
96
|
-
pins: { repositoryId: string; head: string; base?: string },
|
|
97
|
-
anchor: string,
|
|
98
|
-
): JsonValue => {
|
|
99
|
-
const selection = parseAnchor(anchor);
|
|
100
|
-
|
|
101
|
-
const commit =
|
|
102
|
-
selection?.start.side === "base" ? (pins.base ?? pins.head) : pins.head;
|
|
103
|
-
|
|
104
|
-
const file = selection
|
|
105
|
-
? read(pins.repositoryId, commit, selection.file)
|
|
106
|
-
: undefined;
|
|
107
|
-
|
|
108
|
-
const label = caption(pins, anchor);
|
|
109
|
-
|
|
110
|
-
const text =
|
|
111
|
-
file !== undefined && selection
|
|
112
|
-
? file
|
|
113
|
-
.split("\n")
|
|
114
|
-
.slice(selection.start.line - 1, selection.end.line)
|
|
115
|
-
.join("\n")
|
|
116
|
-
: `${label}\n(source unavailable)`;
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
type: "code",
|
|
120
|
-
...(isStringValue(id) && { id }),
|
|
121
|
-
language: selection
|
|
122
|
-
? (LANGUAGES.get(path.extname(selection.file).toLowerCase()) ?? "text")
|
|
123
|
-
: "text",
|
|
124
|
-
text,
|
|
125
|
-
caption: label,
|
|
126
|
-
};
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
const pinsIn = (pins: JsonValue | undefined) => {
|
|
130
|
-
return isJsonObject(pins) &&
|
|
131
|
-
isStringValue(pins.repositoryId) &&
|
|
132
|
-
isStringValue(pins.head)
|
|
133
|
-
? {
|
|
134
|
-
repositoryId: pins.repositoryId,
|
|
135
|
-
head: pins.head,
|
|
136
|
-
...(isStringValue(pins.base) && { base: pins.base }),
|
|
137
|
-
}
|
|
138
|
-
: undefined;
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const pinsOf = (value: JsonValue | undefined) => pinsIn(foreign(value));
|
|
142
|
-
|
|
143
|
-
const peeks = (blocks: JsonValue[]): JsonValue[] =>
|
|
144
|
-
blocks.map((block) => {
|
|
145
|
-
if (!isJsonObject(block)) return block;
|
|
146
|
-
|
|
147
|
-
if (Array.isArray(block.children))
|
|
148
|
-
return { ...block, children: peeks(block.children) };
|
|
149
|
-
|
|
150
|
-
const pins = pinsOf(block.pins);
|
|
151
|
-
|
|
152
|
-
if (block.type === "code_peek" && pins && isStringValue(block.source))
|
|
153
|
-
return snapshot(block.id, pins, block.source);
|
|
154
|
-
|
|
155
|
-
return block;
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
const rest = (blocks: JsonValue[]): JsonValue[] =>
|
|
159
|
-
blocks.flatMap((block): JsonValue | JsonValue[] => {
|
|
160
|
-
if (!isJsonObject(block)) return block;
|
|
161
|
-
|
|
162
|
-
if (Array.isArray(block.children))
|
|
163
|
-
return { ...block, children: rest(block.children) };
|
|
164
|
-
|
|
165
|
-
const blockPins = pinsOf(block.pins);
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
block.type === "markdown" &&
|
|
169
|
-
blockPins &&
|
|
170
|
-
isStringValue(block.markdown)
|
|
171
|
-
) {
|
|
172
|
-
const { pins: _pins, ...kept } = block;
|
|
173
|
-
|
|
174
|
-
return {
|
|
175
|
-
...kept,
|
|
176
|
-
markdown: unlinkSourceLinks(block.markdown, blockPins),
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (block.type === "sequence" && Array.isArray(block.steps))
|
|
181
|
-
return {
|
|
182
|
-
...block,
|
|
183
|
-
steps: block.steps.map((step) => {
|
|
184
|
-
if (!isJsonObject(step) || !isStringValue(step.source)) return step;
|
|
185
|
-
const pins = pinsOf(step.pins ?? block.pins);
|
|
186
|
-
|
|
187
|
-
if (!pins) return step;
|
|
188
|
-
const { source, pins: _pins, ...kept } = step;
|
|
189
|
-
const quoted = snapshot(undefined, pins, source);
|
|
190
|
-
|
|
191
|
-
return isJsonObject(quoted)
|
|
192
|
-
? {
|
|
193
|
-
...kept,
|
|
194
|
-
code: { language: quoted.language, text: quoted.text },
|
|
195
|
-
}
|
|
196
|
-
: kept;
|
|
197
|
-
}),
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
if (block.type === "flow_diagram" && Array.isArray(block.nodes)) {
|
|
201
|
-
const moved: JsonValue[] = [];
|
|
202
|
-
|
|
203
|
-
const diagram = {
|
|
204
|
-
...block,
|
|
205
|
-
nodes: block.nodes.map((node) =>
|
|
206
|
-
isJsonObject(node) && Array.isArray(node.attachments)
|
|
207
|
-
? {
|
|
208
|
-
...node,
|
|
209
|
-
attachments: node.attachments.filter((attachment) => {
|
|
210
|
-
if (
|
|
211
|
-
!isJsonObject(attachment) ||
|
|
212
|
-
!pinsOf(attachment.pins ?? block.pins)
|
|
213
|
-
)
|
|
214
|
-
return true;
|
|
215
|
-
|
|
216
|
-
// Its lines stay on the board, after the diagram.
|
|
217
|
-
for (const [pins, anchor] of quotes(
|
|
218
|
-
attachment,
|
|
219
|
-
pinsIn,
|
|
220
|
-
block.pins,
|
|
221
|
-
))
|
|
222
|
-
moved.push(snapshot(allocate("block"), pins, anchor));
|
|
223
|
-
|
|
224
|
-
return false;
|
|
225
|
-
}),
|
|
226
|
-
}
|
|
227
|
-
: node,
|
|
228
|
-
),
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
return [diagram, ...moved];
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (block.type === "call_stack_diff" || block.type === "database_lens") {
|
|
235
|
-
const foreignQuote = quotes(block, pinsOf)[0];
|
|
236
|
-
|
|
237
|
-
if (!foreignQuote) return block;
|
|
238
|
-
|
|
239
|
-
const what =
|
|
240
|
-
block.type === "call_stack_diff" ? "Call stack" : "Database lens";
|
|
241
|
-
|
|
242
|
-
const title = isStringValue(block.title) ? ` “${block.title}”` : "";
|
|
243
|
-
|
|
244
|
-
// Its own repository's ranges too, so none of it is lost.
|
|
245
|
-
return [
|
|
246
|
-
{
|
|
247
|
-
type: "markdown",
|
|
248
|
-
id: allocate("block"),
|
|
249
|
-
markdown: `${what}${title} (from ${repositoryName(foreignQuote[0].repositoryId)}), kept as snapshots below.`,
|
|
250
|
-
},
|
|
251
|
-
...quotes(block, pinsIn).map(([pins, anchor], index) =>
|
|
252
|
-
snapshot(index === 0 ? block.id : allocate("block"), pins, anchor),
|
|
253
|
-
),
|
|
254
|
-
];
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return block;
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
/** The ranges a block's frames or operations quote at the pins `read`
|
|
261
|
-
* accepts. */
|
|
262
|
-
function quotes(
|
|
263
|
-
block: JsonValue,
|
|
264
|
-
read: typeof pinsIn,
|
|
265
|
-
inherited?: JsonValue,
|
|
266
|
-
): [{ repositoryId: string; head: string; base?: string }, string][] {
|
|
267
|
-
if (Array.isArray(block))
|
|
268
|
-
return block.flatMap((item) => quotes(item, read, inherited));
|
|
269
|
-
|
|
270
|
-
if (!isJsonObject(block)) return [];
|
|
271
|
-
const scope = block.pins ?? inherited;
|
|
272
|
-
const pins = read(scope);
|
|
273
|
-
|
|
274
|
-
const own: [
|
|
275
|
-
{ repositoryId: string; head: string; base?: string },
|
|
276
|
-
string,
|
|
277
|
-
][] = pins
|
|
278
|
-
? [
|
|
279
|
-
block.source,
|
|
280
|
-
block.callSite,
|
|
281
|
-
...(Array.isArray(block.contextSources) ? block.contextSources : []),
|
|
282
|
-
...(Array.isArray(block.sources) ? block.sources : []),
|
|
283
|
-
].flatMap((anchor): [typeof pins, string][] =>
|
|
284
|
-
isStringValue(anchor) ? [[pins, anchor]] : [],
|
|
285
|
-
)
|
|
286
|
-
: [];
|
|
287
|
-
|
|
288
|
-
return [
|
|
289
|
-
...own,
|
|
290
|
-
...Object.entries(block)
|
|
291
|
-
.filter(([key]) => key !== "pins")
|
|
292
|
-
.flatMap(([, value]) =>
|
|
293
|
-
Array.isArray(value) || isJsonObject(value)
|
|
294
|
-
? quotes(value, read, scope)
|
|
295
|
-
: [],
|
|
296
|
-
),
|
|
297
|
-
];
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function unlinkSourceLinks(
|
|
301
|
-
markdown: string,
|
|
302
|
-
pins: { repositoryId: string; head: string; base?: string },
|
|
303
|
-
) {
|
|
304
|
-
let result = markdown;
|
|
305
|
-
|
|
306
|
-
// Rewrite from the end so earlier offsets stay valid.
|
|
307
|
-
const links = [...markdownNodes(parseMarkdown(markdown))]
|
|
308
|
-
.filter(
|
|
309
|
-
(node) =>
|
|
310
|
-
node.type === "link" &&
|
|
311
|
-
SOURCE_LINK.test(node.url ?? "") &&
|
|
312
|
-
node.position?.start.offset !== undefined &&
|
|
313
|
-
node.position.end.offset !== undefined,
|
|
314
|
-
)
|
|
315
|
-
.reverse();
|
|
316
|
-
|
|
317
|
-
// Its text, then where it pointed, so neither is lost.
|
|
318
|
-
for (const node of links)
|
|
319
|
-
result =
|
|
320
|
-
result.slice(0, node.position!.start.offset) +
|
|
321
|
-
`${markdownText(node)} (\`${caption(pins, node.url!.replace(SOURCE_LINK, ""))}\`)` +
|
|
322
|
-
result.slice(node.position!.end.offset);
|
|
323
|
-
|
|
324
|
-
return result;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return rest(peeks(document));
|
|
328
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import type { Block } from "./blocks/index.js";
|
|
2
|
-
|
|
3
|
-
/** What a board link's hover card shows of the board or block it links to. */
|
|
4
|
-
export interface BoardLinkPreview {
|
|
5
|
-
/** A block link: the title of the section the block is in. */
|
|
6
|
-
section?: string;
|
|
7
|
-
/** Markdown paragraphs, opening the board or block. */
|
|
8
|
-
prose?: string[];
|
|
9
|
-
/** A block link to code: its language and first lines. */
|
|
10
|
-
code?: { language: string; lines: string[] };
|
|
11
|
-
/** A block link to a diagram or other figure: its kind and title. */
|
|
12
|
-
figure?: { type: string; title: string };
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** About what the card shows before it fades; more is cut, not sent. */
|
|
16
|
-
const PROSE_CHARACTERS = 700;
|
|
17
|
-
|
|
18
|
-
const CODE_LINES = 6;
|
|
19
|
-
|
|
20
|
-
function childBlocks(block: Block): Block[] {
|
|
21
|
-
return "children" in block ? block.children : [];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Paragraphs of markdown, without headings. */
|
|
25
|
-
function paragraphs(markdown: string): string[] {
|
|
26
|
-
return markdown
|
|
27
|
-
.split(/\n\s*\n/)
|
|
28
|
-
.map((paragraph) => paragraph.trim())
|
|
29
|
-
.filter((paragraph) => paragraph && !paragraph.startsWith("#"));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** The prose from the first markdown block on, in reading order, until the
|
|
33
|
-
* card has enough. Diagrams and code before it are skipped. */
|
|
34
|
-
function openingProse(blocks: Block[]): string[] {
|
|
35
|
-
const prose: string[] = [];
|
|
36
|
-
let length = 0;
|
|
37
|
-
|
|
38
|
-
const walk = (list: Block[]): boolean => {
|
|
39
|
-
for (const block of list) {
|
|
40
|
-
if (block.type === "markdown")
|
|
41
|
-
for (const paragraph of paragraphs(block.markdown)) {
|
|
42
|
-
prose.push(paragraph);
|
|
43
|
-
length += paragraph.length;
|
|
44
|
-
|
|
45
|
-
if (length >= PROSE_CHARACTERS) return true;
|
|
46
|
-
}
|
|
47
|
-
else if (walk(childBlocks(block))) return true;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return false;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
walk(blocks);
|
|
54
|
-
|
|
55
|
-
return prose;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** The block and the title of the section it is in. */
|
|
59
|
-
function find(
|
|
60
|
-
blocks: Block[],
|
|
61
|
-
id: string,
|
|
62
|
-
section?: string,
|
|
63
|
-
): { block: Block; section?: string } | undefined {
|
|
64
|
-
for (const block of blocks) {
|
|
65
|
-
if (block.id === id) return { block, section };
|
|
66
|
-
|
|
67
|
-
const found = find(
|
|
68
|
-
childBlocks(block),
|
|
69
|
-
id,
|
|
70
|
-
block.type === "section" ? block.title : section,
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
if (found) return found;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return undefined;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** The opening prose of a board, or one block of it. */
|
|
80
|
-
export function boardLinkPreview(
|
|
81
|
-
document: Block[],
|
|
82
|
-
blockId?: string,
|
|
83
|
-
): BoardLinkPreview {
|
|
84
|
-
if (blockId === undefined) return { prose: openingProse(document) };
|
|
85
|
-
|
|
86
|
-
const found = find(document, blockId);
|
|
87
|
-
|
|
88
|
-
if (!found) return {};
|
|
89
|
-
|
|
90
|
-
const { block } = found;
|
|
91
|
-
// A section link is about the section itself.
|
|
92
|
-
const section = block.type === "section" ? block.title : found.section;
|
|
93
|
-
|
|
94
|
-
if (block.type === "code")
|
|
95
|
-
return {
|
|
96
|
-
...(section && { section }),
|
|
97
|
-
code: {
|
|
98
|
-
language: block.language,
|
|
99
|
-
lines: block.text.split("\n").slice(0, CODE_LINES),
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const prose = openingProse([block]);
|
|
104
|
-
|
|
105
|
-
if (prose.length) return { ...(section && { section }), prose };
|
|
106
|
-
|
|
107
|
-
const title = "title" in block ? block.title : undefined;
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
...(section && { section }),
|
|
111
|
-
figure: { type: block.type, title: title ?? "" },
|
|
112
|
-
};
|
|
113
|
-
}
|
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
|
|
4
|
-
import type { JsonValue } from "@dev.fast/json";
|
|
5
|
-
import { migrateStoredDocument } from "@review/stored-document-migration.js";
|
|
6
|
-
|
|
7
|
-
import { type Block, sourceReferences } from "./document.js";
|
|
8
|
-
import { snapshotForeignReferences } from "./foreign-references.js";
|
|
9
|
-
import type { StoreUpgradeContext } from "./store-migrations.js";
|
|
10
|
-
import { BOARD_REPOSITORY_ID_SQL, columnsOf } from "./store-schema.js";
|
|
11
|
-
|
|
12
|
-
// The old global scratchpad becomes a plan titled "Scratchpad", history
|
|
13
|
-
// kept, in the project of the repository its blocks quote most, else of the
|
|
14
|
-
// repository opened last. A board quotes only its own repository, so its
|
|
15
|
-
// quotes of others become snapshots.
|
|
16
|
-
|
|
17
|
-
const OLD_SCRATCHPAD_ID = "scratchpad";
|
|
18
|
-
|
|
19
|
-
const hasScratchpadVersions = ({ db }: StoreUpgradeContext) =>
|
|
20
|
-
Boolean(
|
|
21
|
-
db
|
|
22
|
-
.prepare("SELECT 1 FROM versions WHERE review_id=? LIMIT 1")
|
|
23
|
-
.get(OLD_SCRATCHPAD_ID),
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
type PadSnapshot = { document: JsonValue[]; [key: string]: JsonValue };
|
|
27
|
-
|
|
28
|
-
/** A pad version's snapshot with its document in today's form (old pads
|
|
29
|
-
* held anchors as objects); undefined for one that won't parse. */
|
|
30
|
-
function parsePadVersion(text: string): PadSnapshot | undefined {
|
|
31
|
-
try {
|
|
32
|
-
// SAFETY: versions holds snapshots validated when they were saved.
|
|
33
|
-
const snapshot = JSON.parse(text) as PadSnapshot;
|
|
34
|
-
const document = migrateStoredDocument(snapshot.document);
|
|
35
|
-
|
|
36
|
-
snapshot.document = Array.isArray(document) ? document : [];
|
|
37
|
-
|
|
38
|
-
return snapshot;
|
|
39
|
-
} catch {
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Each pad version as parsed, by version, for one open. The run, inside
|
|
45
|
-
* the write lock, reads the rows again: it reuses a parse only of text that
|
|
46
|
-
* hasn't changed since. */
|
|
47
|
-
const parsedPads = new WeakMap<
|
|
48
|
-
StoreUpgradeContext,
|
|
49
|
-
Map<number, { text: string; snapshot: PadSnapshot | undefined }>
|
|
50
|
-
>();
|
|
51
|
-
|
|
52
|
-
function padVersion(
|
|
53
|
-
context: StoreUpgradeContext,
|
|
54
|
-
version: number,
|
|
55
|
-
text: string,
|
|
56
|
-
) {
|
|
57
|
-
let parsed = parsedPads.get(context);
|
|
58
|
-
|
|
59
|
-
if (!parsed) parsedPads.set(context, (parsed = new Map()));
|
|
60
|
-
const known = parsed.get(version);
|
|
61
|
-
|
|
62
|
-
if (known?.text === text) return known.snapshot;
|
|
63
|
-
|
|
64
|
-
const snapshot = parsePadVersion(text);
|
|
65
|
-
|
|
66
|
-
parsed.set(version, { text, snapshot });
|
|
67
|
-
|
|
68
|
-
return snapshot;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** The pad's versions, oldest first, each parsed once per open. */
|
|
72
|
-
const scratchpadVersions = (context: StoreUpgradeContext) =>
|
|
73
|
-
context.db
|
|
74
|
-
.prepare(
|
|
75
|
-
"SELECT version,snapshot FROM versions WHERE review_id=? ORDER BY version",
|
|
76
|
-
)
|
|
77
|
-
.all(OLD_SCRATCHPAD_ID)
|
|
78
|
-
.map((row) => ({
|
|
79
|
-
version: Number(row.version),
|
|
80
|
-
snapshot: padVersion(context, Number(row.version), String(row.snapshot)),
|
|
81
|
-
}));
|
|
82
|
-
|
|
83
|
-
/** The pad's newest version that parses, reading no further back. */
|
|
84
|
-
function latestScratchpadDocument(context: StoreUpgradeContext) {
|
|
85
|
-
for (const row of context.db
|
|
86
|
-
.prepare(
|
|
87
|
-
"SELECT version,snapshot FROM versions WHERE review_id=? ORDER BY version DESC",
|
|
88
|
-
)
|
|
89
|
-
.iterate(OLD_SCRATCHPAD_ID)) {
|
|
90
|
-
const snapshot = padVersion(
|
|
91
|
-
context,
|
|
92
|
-
Number(row.version),
|
|
93
|
-
String(row.snapshot),
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
if (snapshot) return snapshot.document;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return undefined;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** The checkout the pad quotes most, else the one opened last. Only a
|
|
103
|
-
* checkout that still exists: a plan opens on it. */
|
|
104
|
-
function scratchpadRepository(context: StoreUpgradeContext) {
|
|
105
|
-
const exists = (repositoryId: string) => {
|
|
106
|
-
try {
|
|
107
|
-
return existsSync(context.repositoryPath(repositoryId));
|
|
108
|
-
} catch {
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const counts = new Map<string, number>();
|
|
114
|
-
|
|
115
|
-
// SAFETY: a stored pad's blocks, in today's form.
|
|
116
|
-
const latest = latestScratchpadDocument(context) as Block[] | undefined;
|
|
117
|
-
|
|
118
|
-
for (const { source } of sourceReferences(latest ?? [], {
|
|
119
|
-
tolerant: true,
|
|
120
|
-
})) {
|
|
121
|
-
const repositoryId = source.pins?.repositoryId;
|
|
122
|
-
|
|
123
|
-
if (repositoryId && exists(repositoryId))
|
|
124
|
-
counts.set(repositoryId, (counts.get(repositoryId) ?? 0) + 1);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const [quoted] = [...counts].sort((a, b) => b[1] - a[1])[0] ?? [];
|
|
128
|
-
|
|
129
|
-
if (quoted) return quoted;
|
|
130
|
-
|
|
131
|
-
const viewed = context.db
|
|
132
|
-
.prepare(
|
|
133
|
-
`SELECT ${BOARD_REPOSITORY_ID_SQL} AS repository
|
|
134
|
-
FROM review_attention
|
|
135
|
-
JOIN reviews ON reviews.id=review_attention.review_id
|
|
136
|
-
JOIN versions ON versions.review_id=reviews.id AND versions.version=reviews.version
|
|
137
|
-
WHERE review_attention.viewed_at IS NOT NULL AND repository IS NOT NULL
|
|
138
|
-
ORDER BY review_attention.viewed_at DESC`,
|
|
139
|
-
)
|
|
140
|
-
.all()
|
|
141
|
-
.map(({ repository }) => String(repository));
|
|
142
|
-
|
|
143
|
-
return [...viewed, ...context.repositories().map(({ id }) => id)].find(
|
|
144
|
-
exists,
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const snapshots = (
|
|
149
|
-
context: StoreUpgradeContext,
|
|
150
|
-
own: string,
|
|
151
|
-
document: JsonValue[],
|
|
152
|
-
allocate: (prefix: string) => string,
|
|
153
|
-
) =>
|
|
154
|
-
snapshotForeignReferences(
|
|
155
|
-
document,
|
|
156
|
-
own,
|
|
157
|
-
context.fileAtCommit,
|
|
158
|
-
context.repositoryName,
|
|
159
|
-
allocate,
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
/** Reads the files the pad quotes, which the context keeps, before the
|
|
163
|
-
* write lock other hosts on this home wait on. */
|
|
164
|
-
export function readScratchpadQuotes(context: StoreUpgradeContext) {
|
|
165
|
-
if (!hasScratchpadVersions(context)) return;
|
|
166
|
-
|
|
167
|
-
const quoting = scratchpadRepository(context);
|
|
168
|
-
|
|
169
|
-
if (quoting)
|
|
170
|
-
for (const { snapshot } of scratchpadVersions(context))
|
|
171
|
-
if (snapshot)
|
|
172
|
-
snapshots(context, quoting, snapshot.document, (prefix) => prefix);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export function planFromScratchpad(context: StoreUpgradeContext) {
|
|
176
|
-
const { db } = context;
|
|
177
|
-
|
|
178
|
-
// A pad row with no versions has nothing to keep; it stays as it is.
|
|
179
|
-
if (!hasScratchpadVersions(context)) return;
|
|
180
|
-
|
|
181
|
-
const own = scratchpadRepository(context);
|
|
182
|
-
const id = randomUUID();
|
|
183
|
-
|
|
184
|
-
db.prepare(
|
|
185
|
-
"INSERT INTO reviews(id,version,next_id,project_id) SELECT ?,version,next_id,? FROM reviews WHERE id=?",
|
|
186
|
-
).run(
|
|
187
|
-
id,
|
|
188
|
-
own ? context.projectOfRepository(own) : context.fallbackProject(),
|
|
189
|
-
OLD_SCRATCHPAD_ID,
|
|
190
|
-
);
|
|
191
|
-
|
|
192
|
-
let nextId = Number(
|
|
193
|
-
db.prepare("SELECT next_id FROM reviews WHERE id=?").get(id)!.next_id,
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
const old = scratchpadVersions(context);
|
|
197
|
-
|
|
198
|
-
// The run changes the snapshots it took; nothing parses them again.
|
|
199
|
-
parsedPads.delete(context);
|
|
200
|
-
|
|
201
|
-
// Every version moves over; one that no longer parses, as it is.
|
|
202
|
-
db.prepare("UPDATE versions SET review_id=? WHERE review_id=?").run(
|
|
203
|
-
id,
|
|
204
|
-
OLD_SCRATCHPAD_ID,
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
for (const { version, snapshot } of old) {
|
|
208
|
-
if (!snapshot) continue;
|
|
209
|
-
|
|
210
|
-
delete snapshot.kind;
|
|
211
|
-
|
|
212
|
-
snapshot.reviewId = id;
|
|
213
|
-
snapshot.title = "Scratchpad";
|
|
214
|
-
|
|
215
|
-
if (own) {
|
|
216
|
-
snapshot.target = { kind: "project", repositoryId: own };
|
|
217
|
-
snapshot.document = snapshots(
|
|
218
|
-
context,
|
|
219
|
-
own,
|
|
220
|
-
snapshot.document,
|
|
221
|
-
(prefix) => `${prefix}-${++nextId}`,
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
context.rewriteVersion(id, version, snapshot);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
db.prepare("UPDATE reviews SET next_id=? WHERE id=?").run(nextId, id);
|
|
229
|
-
|
|
230
|
-
// Tables a store makes after its upgrades may not exist yet. A thread's
|
|
231
|
-
// board is its review_id until ask-threads-by-project renames it, which
|
|
232
|
-
// a development build may have done already.
|
|
233
|
-
for (const [table, ...names] of [
|
|
234
|
-
["ask_conversations", "review_id", "anchor_board"],
|
|
235
|
-
["authoring_presences", "review_id"],
|
|
236
|
-
["review_coverage", "review_id"],
|
|
237
|
-
["review_attention", "review_id"],
|
|
238
|
-
]) {
|
|
239
|
-
const columns = columnsOf(db, table!);
|
|
240
|
-
const column = names.find((name) => columns.has(name));
|
|
241
|
-
|
|
242
|
-
if (column)
|
|
243
|
-
db.prepare(`UPDATE ${table} SET ${column}=? WHERE ${column}=?`).run(
|
|
244
|
-
id,
|
|
245
|
-
OLD_SCRATCHPAD_ID,
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
db.prepare("DELETE FROM reviews WHERE id=?").run(OLD_SCRATCHPAD_ID);
|
|
250
|
-
}
|