@dev.fast/whiteboard 0.3.4 → 0.3.5-preview.20261020.130

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.
Files changed (115) hide show
  1. package/dist/{account-alias-CprZojXd.js → account-alias-CMyXCuuq.js} +13 -8
  2. package/dist/{agent-cli-CThN7Q59.js → agent-cli-CahKfDT0.js} +15 -10
  3. package/dist/agent-cli-CvYV2R6c.js +2 -0
  4. package/dist/{agent-client-BOOi7EUR.js → agent-client-D2RmE9NL.js} +13 -3
  5. package/dist/{authoring-tools-BNwuMnz6.js → authoring-tools-BLhYKrNN.js} +34 -27
  6. package/dist/{background-server-DLSDogkI.js → background-server-CFUU4VtN.js} +1 -1
  7. package/dist/{background-server-CkvxA-6O.js → background-server-k4K_Hvh1.js} +5 -5
  8. package/dist/build-info.json +1 -1
  9. package/dist/{cli-R4yfa5DT.js → cli-BUVSMGud.js} +1 -1
  10. package/dist/{cli-runner-Cuc9FSzu.js → cli-runner-C7W6VA94.js} +26 -26
  11. package/dist/cli.js +4 -3
  12. package/dist/{client-BN_FDxFr.js → client-B0f4EJ7v.js} +253 -47
  13. package/dist/{fuzzy-match-BO9bK37N.js → fuzzy-match-BbaX-dGh.js} +6 -4
  14. package/dist/{headless-host-Cog6SpS9.js → headless-host-C7xYMe74.js} +513 -140
  15. package/dist/{headless-host-CVBOQyRS.js → headless-host-cYdlRYPB.js} +1 -1
  16. package/dist/lens-selection-CAtsGaym.js +214 -0
  17. package/dist/{local-data-D_9tvdsp.js → local-data-DrNSaiS5.js} +2416 -320
  18. package/dist/{mcp-D1hziANa.js → mcp-yXnjUkOV.js} +6 -10
  19. package/dist/{package-paths-C--__jEv.js → package-paths-BWxO-UEL.js} +1 -1
  20. package/dist/{profile-DokFw782.js → profile-BlqZlCPZ.js} +20 -8
  21. package/dist/profile-CGxNfeYQ.js +2 -0
  22. package/dist/{remote-attach-CEJ-kvDY.js → remote-attach-CZTrtWIk.js} +4 -4
  23. package/dist/{remote-extensions-Blcjr9Ze.js → remote-extensions-CNXwqK0D.js} +3 -3
  24. package/dist/{remote-extensions-GN8Gf_qG.js → remote-extensions-VP-kfq0C.js} +1 -1
  25. package/dist/{remote-language-server-BTKBQ7x4.js → remote-language-server-BiPzol99.js} +5 -5
  26. package/dist/{remote-uninstall-DW7Gdryh.js → remote-uninstall-CFTkoioA.js} +6 -6
  27. package/dist/{request-origin-Zcn_CjX4.js → request-origin--8lLCclv.js} +55 -14
  28. package/dist/{review-app-launcher-CkwLclzR.js → review-app-launcher-KA2_mOeo.js} +2 -2
  29. package/dist/{review-home-paths-j2BRAaNv.js → review-home-paths-D8AoCdd2.js} +19 -17
  30. package/dist/{review-telemetry-BLNwYnLV.js → review-telemetry-B2JniTIs.js} +5 -5
  31. package/dist/runtime-Bj2tD33R.js +27 -0
  32. package/dist/runtime.d.ts +1 -1
  33. package/dist/runtime.js +1 -1
  34. package/dist/{s3-DtJFWC0G.js → s3-CsG2EWED.js} +1 -1
  35. package/dist/{s3-config-BTO12tCC.js → s3-config-CjcLyFV5.js} +4 -2
  36. package/dist/{s3-config-CSy3fx8H.js → s3-config-zz4GWh2T.js} +1 -1
  37. package/dist/server/desktop-host.js +29 -76
  38. package/dist/{server-discovery-Bey8IqXH.js → server-discovery-CRXG48Uz.js} +3 -3
  39. package/dist/sharing/index.d.ts +484 -70
  40. package/dist/sharing/index.js +1 -1
  41. package/dist/{src-DsM3jO8G.js → src-CyYCvVHx.js} +90 -17
  42. package/dist/{src-BMPuxWv4.js → src-WPaQn75m.js} +208 -73
  43. package/dist/{stored-document-migration-CChxtFAy.js → stored-document-migration-B_BijGHF.js} +270 -269
  44. package/dist/{thread-state-5pYr6_zM.js → thread-state-D6MlnGv7.js} +7 -2
  45. package/dist/{tool-failure-C9MpbXJJ.js → tool-failure-_makh_Sr.js} +10 -3
  46. package/instructions/authoring.md +18 -5
  47. package/package.json +6 -6
  48. package/src/ask/agents.ts +13 -1
  49. package/src/ask/checkout-files.ts +2 -5
  50. package/src/ask/thread-state.ts +4 -0
  51. package/src/ask/thread.ts +16 -0
  52. package/src/ask/threads.ts +24 -3
  53. package/src/cli-install.ts +13 -2
  54. package/src/cli-runner.ts +4 -1
  55. package/src/cli.ts +4 -1
  56. package/src/diff-selection-migration.ts +48 -34
  57. package/src/heading-anchors.ts +42 -0
  58. package/src/lens-selection.ts +38 -2
  59. package/src/review-api/README.md +52 -13
  60. package/src/review-api/activity.ts +1 -4
  61. package/src/review-api/agent-cli.ts +9 -1
  62. package/src/review-api/anchor-quotes.ts +14 -7
  63. package/src/review-api/ask-history.ts +137 -51
  64. package/src/review-api/authoring-tools.ts +45 -30
  65. package/src/review-api/blocks/definition.ts +1 -1
  66. package/src/review-api/blocks/markdown.ts +2 -2
  67. package/src/review-api/board-order.ts +144 -0
  68. package/src/review-api/checkout-groups.ts +560 -0
  69. package/src/review-api/document-headings.ts +58 -27
  70. package/src/review-api/document-text.ts +31 -2
  71. package/src/review-api/document.ts +281 -34
  72. package/src/review-api/foreign-references.ts +328 -0
  73. package/src/review-api/github-provider.ts +51 -13
  74. package/src/review-api/http.ts +702 -123
  75. package/src/review-api/instructions.ts +0 -19
  76. package/src/review-api/link-preview.ts +113 -0
  77. package/src/review-api/local-data.ts +310 -46
  78. package/src/review-api/mcp.ts +4 -12
  79. package/src/review-api/onboarding.ts +1 -1
  80. package/src/review-api/profile.ts +37 -6
  81. package/src/review-api/public-tools.ts +24 -2
  82. package/src/review-api/pull-request.ts +82 -49
  83. package/src/review-api/request-origin.ts +10 -0
  84. package/src/review-api/review-progress.ts +61 -27
  85. package/src/review-api/scratchpad-plan-upgrade.ts +250 -0
  86. package/src/review-api/store-migrations.ts +357 -0
  87. package/src/review-api/store-schema.ts +38 -29
  88. package/src/review-api/store.ts +2144 -480
  89. package/src/review-api/worktree-events.ts +3 -5
  90. package/src/review-api/worktree-source.ts +3 -7
  91. package/src/review-api/worktree-structural.integration.ts +72 -0
  92. package/src/review-stack.ts +15 -23
  93. package/src/runtime.ts +4 -30
  94. package/src/server/desktop-host.ts +6 -1
  95. package/src/server/desktop-server.ts +1 -31
  96. package/src/server/headless-host.ts +0 -2
  97. package/src/server/hono-http.ts +2 -1
  98. package/src/server/legacy-tutorial-cleanup.ts +2 -3
  99. package/src/server/review-gateway-list.ts +3 -5
  100. package/src/server/review-gateway-memory.ts +1 -1
  101. package/src/server/review-lifecycle-telemetry.ts +2 -2
  102. package/src/server/review-server-core.ts +1 -5
  103. package/src/server/structural-comparisons.ts +47 -0
  104. package/src/sharing/export.ts +36 -7
  105. package/src/sharing/host.ts +34 -14
  106. package/src/sharing/import.ts +156 -24
  107. package/src/sharing/repository.ts +128 -20
  108. package/src/startup-trace.ts +3 -3
  109. package/src/stored-document-migration.ts +110 -19
  110. package/src/ui-telemetry-events.ts +44 -14
  111. package/dist/agent-cli-C6SPSII0.js +0 -2
  112. package/dist/profile-CYPmvBoA.js +0 -2
  113. package/dist/runtime-CKiWhGrf.js +0 -52
  114. package/instructions/scratchpad.md +0 -23
  115. package/src/review-preferences.ts +0 -88
@@ -1,7 +1,8 @@
1
- import { f as gitAt } from "./src-BMPuxWv4.js";
1
+ import { f as gitAt } from "./src-WPaQn75m.js";
2
2
  import { n as isJsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
3
- import "./src-DsM3jO8G.js";
4
- import { C as resourceReference, D as sourceReferences, F as lensSchema, H as markdownNodes, I as lensSelections, U as parseMarkdown, Y as sourceAnchors, _ as hasCodeReferences, c as traceSchema, h as elements, k as checkReferences, l as textIncludesQuote, n as migrateStoredDocument, o as normalizedSoftwareElementSchema, p as documentSchema, r as migrateStoredLenses, s as normalizedSoftwareRelationshipSchema, t as liftFileLenses, u as anchorPins, w as resourceReferences, x as pinsSchema } from "./stored-document-migration-CChxtFAy.js";
3
+ import "./src-CyYCvVHx.js";
4
+ import { f as sourceAnchors } from "./lens-selection-CAtsGaym.js";
5
+ import { $ as parseMarkdown, A as resourceReferences, E as pinsSchema, G as lensSelections, I as sourceReferences, O as readsAtBoardPins, R as checkReferences, W as lensSchema, Z as markdownNodes, _ as boardRepositoryId, c as normalizedSoftwareRelationshipSchema, f as anchorPins, k as resourceReference, l as traceSchema, n as migrateStoredDocument, r as migrateStoredLenses, s as normalizedSoftwareElementSchema, t as liftFileLenses, u as textIncludesQuote, x as elements, y as documentSchema } from "./stored-document-migration-B_BijGHF.js";
5
6
  import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
6
7
  import { createHash, randomUUID } from "node:crypto";
7
8
  import { z } from "zod";
@@ -123,11 +124,25 @@ function digestBytes(bytes) {
123
124
  return createHash("sha256").update(bytes).digest("hex");
124
125
  }
125
126
  function validateShareSources(snapshot) {
126
- if (!snapshot.pins && hasCodeReferences(snapshot)) throw new ReviewInputError("Reviews with code references need source pins before sharing.", 409);
127
+ if (!snapshot.pins && readsAtBoardPins(snapshot)) throw new ReviewInputError("Reviews with code references need source pins before sharing.", 409);
128
+ }
129
+ /** The commits a board's references read at besides its base and head. A
130
+ * recipient fetches them too. A shared board names no repository of its
131
+ * own, so one stands in for its commit links. */
132
+ function referenceCommits(snapshot, repositoryId = boardRepositoryId(snapshot) ?? "shared") {
133
+ const own = /* @__PURE__ */ new Set([snapshot.pins?.base, snapshot.pins?.head]);
134
+ const commits = /* @__PURE__ */ new Set();
135
+ for (const { source } of sourceReferences(snapshot.document, {
136
+ tolerant: true,
137
+ repositoryId
138
+ })) for (const commit of [source.pins?.base, source.pins?.head]) if (commit && !own.has(commit)) commits.add(commit);
139
+ return [...commits].sort();
127
140
  }
128
141
  /** Freeze one stored version before any asynchronous source reads. */
129
142
  async function exportShare(input) {
130
- const { target, kind: _kind, staleSources: _staleSources, sourceUnavailable: _sourceUnavailable, lastEdit: _lastEdit, trashedAt: _trashedAt, ...snapshot } = structuredClone(input.store.read(input.reviewId, input.version));
143
+ const stored = structuredClone(input.store.read(input.reviewId, input.version));
144
+ const repositoryId = boardRepositoryId(stored);
145
+ const { target, staleSources: _staleSources, sourceUnavailable: _sourceUnavailable, lastEdit: _lastEdit, trashedAt: _trashedAt, personEdit: _personEdit, ...snapshot } = stored;
131
146
  if (target?.kind === "worktree") throw new ReviewInputError("Pin this review to commits before sharing it.");
132
147
  documentSchema.parse(snapshot.document);
133
148
  checkReferences(snapshot.document);
@@ -146,14 +161,14 @@ async function exportShare(input) {
146
161
  const json = (value) => add(Buffer.from(JSON.stringify(value)));
147
162
  const resources = [];
148
163
  const maps = {};
149
- const sources = [...sourceReferences(snapshot.document), ...lensSelections(snapshot.lenses ?? []).flatMap((selection) => sourceAnchors(selection.source).map((source) => ({
164
+ const sources = [...sourceReferences(snapshot.document, { repositoryId }), ...lensSelections(snapshot.lenses ?? []).flatMap((selection) => sourceAnchors(selection.source).map((source) => ({
150
165
  ...selection,
151
166
  source,
152
167
  peek: false
153
168
  })))];
154
169
  const pins = snapshot.pins;
155
170
  validateShareSources(snapshot);
156
- if (Boolean(pins) !== Boolean(input.repository)) throw new ReviewInputError("Shared source pins require a repository.", 409);
171
+ if ((pins || referenceCommits(stored).length) && !input.repository) throw new ReviewInputError("Shared source pins require a repository.", 409);
157
172
  for (const block of elements(snapshot.document)) {
158
173
  if (block.type !== "markdown") continue;
159
174
  for (const node of markdownNodes(parseMarkdown(block.markdown))) if (node.type === "image") throw new ReviewInputError("Convert Markdown images to managed image blocks before sharing.");
@@ -215,6 +230,14 @@ async function exportShare(input) {
215
230
  }
216
231
  //#endregion
217
232
  //#region src/sharing/repository.ts
233
+ /** A full commit id, SHA-1 or SHA-256. */
234
+ const COMMIT_ID = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
235
+ /** Refuses anything but full commit ids, before any of them reaches git:
236
+ * a shared board's commits come from someone else, and one spelled as an
237
+ * option or a refspec would make git do something else. */
238
+ function assertCommitIds(commits) {
239
+ if (!commits.every((commit) => COMMIT_ID.test(commit))) throw new ReviewInputError("The shared whiteboard names a commit that isn't a full commit id.");
240
+ }
218
241
  /** Keep credential helpers but prevent the calling shell from redirecting Git's object store. */
219
242
  function sharedGit(cwd, args) {
220
243
  const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("GIT_") || [
@@ -230,10 +253,7 @@ function sharedGit(cwd, args) {
230
253
  "core.fsmonitor=false",
231
254
  ...args
232
255
  ], {
233
- env: {
234
- ...env,
235
- GIT_TERMINAL_PROMPT: "0"
236
- },
256
+ env,
237
257
  timeout: 3e5
238
258
  });
239
259
  }
@@ -241,9 +261,21 @@ function fetchError(stderr, publishing) {
241
261
  if (/not our ref|couldn't find remote ref|unadvertised object/i.test(stderr)) return new ReviewInputError(publishing ? "Push the reviewed commits to GitHub before sharing." : "The shared commits are no longer available on GitHub. Ask the sender to restore them, then retry.", 409);
242
262
  return new ReviewInputError("Could not fetch the GitHub repository. Check your connection and Git credentials for this repository, then retry.", 409);
243
263
  }
244
- /** Recipients need complete history; publication only checks that the commits exist. */
245
- async function fetchPinnedRepository(root, url, pins, publishing = false) {
246
- if (![pins.base, pins.head].every((commit) => /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(commit))) throw new ReviewInputError("Sharing requires immutable base and head commits.");
264
+ /** Recipients need complete history; publication only checks that the commits exist.
265
+ * `commits` are other commits the board's references read at; a plan has
266
+ * only those. */
267
+ async function fetchPinnedRepository(root, url, pins, publishing = false, commits = []) {
268
+ const named = [...pins ? [{
269
+ commit: pins.base,
270
+ ref: "base"
271
+ }, {
272
+ commit: pins.head,
273
+ ref: "head"
274
+ }] : [], ...commits.map((commit) => ({
275
+ commit,
276
+ ref: `commits/${commit}`
277
+ }))];
278
+ if (!named.every(({ commit }) => COMMIT_ID.test(commit))) throw new ReviewInputError("Sharing requires immutable base and head commits.");
247
279
  await mkdir(root, {
248
280
  recursive: true,
249
281
  mode: 448
@@ -264,7 +296,7 @@ async function fetchPinnedRepository(root, url, pins, publishing = false) {
264
296
  "remote",
265
297
  "add",
266
298
  "origin",
267
- url
299
+ { remote: url }
268
300
  ]);
269
301
  }
270
302
  const refs = publishing ? "refs/heads/review" : "refs/review";
@@ -274,26 +306,129 @@ async function fetchPinnedRepository(root, url, pins, publishing = false) {
274
306
  "--no-tags",
275
307
  "--force",
276
308
  ...publishing ? ["--depth=1", "--filter=tree:0"] : [],
277
- publishing ? url : "origin",
278
- `${pins.base}:${refs}/base`,
279
- `${pins.head}:${refs}/head`
309
+ { remote: publishing ? url : "origin" },
310
+ ...named.map(({ commit, ref }) => ({ refspec: {
311
+ from: commit,
312
+ to: `${refs}/${ref}`
313
+ } }))
280
314
  ]);
281
315
  } catch (error) {
282
316
  throw fetchError(error instanceof Error && "stderr" in error ? String(error.stderr) : "", publishing);
283
317
  }
284
- if (!publishing) for (const commit of [pins.base, pins.head]) await sharedGit(root, [
318
+ if (!publishing) for (const { commit } of named) await sharedGit(root, [
285
319
  "cat-file",
286
320
  "-e",
287
- `${commit}^{commit}`
321
+ { rev: `${commit}^{commit}` }
288
322
  ]);
289
323
  }
290
- async function repositoryReady(root, pins) {
324
+ /** Fetches the commits a shared board's links read at into a checkout of
325
+ * its repository, from its origin unless it has them, and keeps each under
326
+ * `refs/review/shared/<share>/<commit>`, so `git gc` can't prune one the
327
+ * board still quotes. */
328
+ async function fetchSharedCommits(root, commits, share) {
329
+ assertCommitIds(commits);
330
+ const ref = (commit) => `refs/review/shared/${share}/${commit}`;
331
+ const missing = [];
332
+ for (const commit of commits) try {
333
+ await sharedGit(root, [
334
+ "cat-file",
335
+ "-e",
336
+ { rev: `${commit}^{commit}` }
337
+ ]);
338
+ await sharedGit(root, [
339
+ "update-ref",
340
+ { ref: ref(commit) },
341
+ { commit }
342
+ ]);
343
+ } catch {
344
+ missing.push(commit);
345
+ }
346
+ if (!missing.length) return;
347
+ try {
348
+ await sharedGit(root, [
349
+ "fetch",
350
+ "--no-tags",
351
+ "origin",
352
+ ...missing.map((commit) => ({ refspec: {
353
+ from: commit,
354
+ to: ref(commit),
355
+ force: true
356
+ } }))
357
+ ]);
358
+ } catch (error) {
359
+ throw fetchError(error instanceof Error && "stderr" in error ? String(error.stderr) : "", false);
360
+ }
361
+ }
362
+ /** Drops the refs `fetchSharedCommits` kept for a share, once the share is
363
+ * gone, so `git gc` can prune what only it quoted. */
364
+ async function forgetSharedCommits(root, share) {
365
+ const { stdout } = await sharedGit(root, [
366
+ "for-each-ref",
367
+ "--format=%(refname)",
368
+ { ref: `refs/review/shared/${share}/` }
369
+ ]);
370
+ for (const ref of stdout.split("\n").filter(Boolean)) await sharedGit(root, [
371
+ "update-ref",
372
+ "-d",
373
+ { ref }
374
+ ]);
375
+ }
376
+ /** A managed clone of a repository's default branch, for a shared board
377
+ * with no pins whose repository the receiver has no checkout of. */
378
+ async function cloneSharedRepository(root, url) {
379
+ await mkdir(root, {
380
+ recursive: true,
381
+ mode: 448
382
+ });
383
+ await sharedGit(root, ["init", "--quiet"]);
384
+ await sharedGit(root, [
385
+ "config",
386
+ "core.hooksPath",
387
+ "/dev/null"
388
+ ]);
389
+ await sharedGit(root, [
390
+ "config",
391
+ "core.fsmonitor",
392
+ "false"
393
+ ]);
394
+ await sharedGit(root, [
395
+ "remote",
396
+ "add",
397
+ "origin",
398
+ { remote: url }
399
+ ]);
400
+ try {
401
+ await sharedGit(root, [
402
+ "fetch",
403
+ "--no-tags",
404
+ "origin",
405
+ "HEAD"
406
+ ]);
407
+ } catch (error) {
408
+ throw fetchError(error instanceof Error && "stderr" in error ? String(error.stderr) : "", false);
409
+ }
410
+ await sharedGit(root, [
411
+ "checkout",
412
+ "--detach",
413
+ "FETCH_HEAD"
414
+ ]);
415
+ }
416
+ async function repositoryReady(root, pins, commits = []) {
417
+ if (![
418
+ pins.base,
419
+ pins.head,
420
+ ...commits
421
+ ].every((commit) => COMMIT_ID.test(commit))) return false;
291
422
  try {
292
423
  if ((await sharedGit(root, ["rev-parse", "HEAD"])).stdout.trim() !== pins.head) return false;
293
- for (const commit of [pins.base, pins.head]) await sharedGit(root, [
424
+ for (const commit of [
425
+ pins.base,
426
+ pins.head,
427
+ ...commits
428
+ ]) await sharedGit(root, [
294
429
  "cat-file",
295
430
  "-e",
296
- `${commit}^{commit}`
431
+ { rev: `${commit}^{commit}` }
297
432
  ]);
298
433
  return true;
299
434
  } catch {
@@ -313,11 +448,11 @@ async function readShareRepository(root) {
313
448
  }
314
449
  return { cloneUrl };
315
450
  }
316
- async function verifyShareRepository(root, pins, repository) {
451
+ async function verifyShareRepository(root, pins, repository, commits = []) {
317
452
  const { cloneUrl } = repository ?? await readShareRepository(root);
318
453
  const temporary = await mkdtemp(path.join(tmpdir(), "review-share-check-"));
319
454
  try {
320
- await fetchPinnedRepository(temporary, cloneUrl, pins, true);
455
+ await fetchPinnedRepository(temporary, cloneUrl, pins, true, commits);
321
456
  } finally {
322
457
  await rm(temporary, {
323
458
  recursive: true,
@@ -331,10 +466,11 @@ async function verifyShareRepository(root, pins, repository) {
331
466
  const sharedSnapshotSchema = z.strictObject({
332
467
  reviewId: z.string().min(1),
333
468
  version: z.number().int().nonnegative(),
334
- title: z.string().min(1),
469
+ /** Empty for a board never named; it reads "Untitled". */
470
+ title: z.string(),
335
471
  pins: pinsSchema.extend({
336
- base: z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/),
337
- head: z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/)
472
+ base: z.string().regex(COMMIT_ID),
473
+ head: z.string().regex(COMMIT_ID)
338
474
  }).optional(),
339
475
  document: documentSchema,
340
476
  lenses: z.array(lensSchema).optional(),
@@ -354,7 +490,7 @@ const counts = z.strictObject({
354
490
  deletions: count
355
491
  });
356
492
  const mapSchema = z.strictObject({
357
- commit: z.string(),
493
+ commit: z.string().regex(COMMIT_ID),
358
494
  side: z.enum(["base", "head"]),
359
495
  elements: z.array(normalizedSoftwareElementSchema),
360
496
  relationships: z.array(normalizedSoftwareRelationshipSchema),
@@ -387,7 +523,8 @@ function validateShareBundle(bundle) {
387
523
  if (snapshot.reviewId !== manifest.reviewId || snapshot.version !== manifest.version || snapshot.title !== manifest.title) throw new Error("Shared snapshot does not match its manifest.");
388
524
  checkReferences(snapshot.document);
389
525
  validateShareSources(snapshot);
390
- if (Boolean(snapshot.pins) !== Boolean(manifest.repository)) throw new Error("Shared source pins require a repository.");
526
+ assertCommitIds(referenceCommits(snapshot));
527
+ if (snapshot.pins && !manifest.repository) throw new Error("Shared source pins require a repository.");
391
528
  const presentation = sharePresentationSchema.parse(json(manifest.presentation));
392
529
  if (!snapshot.pins && Object.keys(presentation.maps).length) throw new Error("Shared maps require source pins.");
393
530
  const traces = /* @__PURE__ */ new Map();
@@ -435,6 +572,7 @@ function sharedReviewId(origin, shareId) {
435
572
  var SharedReviewStore = class {
436
573
  root;
437
574
  fetchRepository;
575
+ cloneRepository;
438
576
  loaded = /* @__PURE__ */ new Map();
439
577
  validated = /* @__PURE__ */ new Map();
440
578
  attention = /* @__PURE__ */ new Map();
@@ -456,9 +594,10 @@ var SharedReviewStore = class {
456
594
  repositories = /* @__PURE__ */ new Map();
457
595
  jobs = /* @__PURE__ */ new Map();
458
596
  states = /* @__PURE__ */ new Map();
459
- constructor(root, fetchRepository = fetchPinnedRepository) {
597
+ constructor(root, fetchRepository = fetchPinnedRepository, cloneRepository = cloneSharedRepository) {
460
598
  this.root = root;
461
599
  this.fetchRepository = fetchRepository;
600
+ this.cloneRepository = cloneRepository;
462
601
  this.purgeTimer = setInterval(() => {
463
602
  this.purgeExpired().catch((error) => console.error("Could not purge expired shared whiteboards:", error));
464
603
  }, 6e4);
@@ -518,14 +657,15 @@ var SharedReviewStore = class {
518
657
  const saved = this.validated.get(id);
519
658
  if (!saved) throw new ReviewInputError("Shared review is not available.", 404);
520
659
  if (!saved.snapshot.pins) {
521
- this.setStatus(id, "ready");
660
+ await this.prepareUnpinned(id, saved.manifest.repository?.cloneUrl);
522
661
  return;
523
662
  }
524
663
  if (!local) throw new ReviewInputError("Repository service is unavailable.", 409);
525
664
  const root = this.repositoryRoot(id);
665
+ const commits = referenceCommits(saved.snapshot);
526
666
  try {
527
667
  this.setStatus(id, "fetching");
528
- if (!await repositoryReady(root, saved.snapshot.pins)) {
668
+ if (!await repositoryReady(root, saved.snapshot.pins, commits)) {
529
669
  const prior = this.repositories.get(id);
530
670
  await local.data.workspaces.remove(id);
531
671
  if (prior) await local.data.forgetRepository(prior);
@@ -533,11 +673,11 @@ var SharedReviewStore = class {
533
673
  recursive: true,
534
674
  force: true
535
675
  });
536
- await this.fetchRepository(root, saved.manifest.repository.cloneUrl, saved.snapshot.pins);
676
+ await this.fetchRepository(root, saved.manifest.repository.cloneUrl, saved.snapshot.pins, false, commits);
537
677
  await sharedGit(root, [
538
678
  "checkout",
539
679
  "--detach",
540
- saved.snapshot.pins.head
680
+ { commit: saved.snapshot.pins.head }
541
681
  ]);
542
682
  }
543
683
  const repository = await local.data.register(root);
@@ -552,12 +692,7 @@ var SharedReviewStore = class {
552
692
  };
553
693
  this.setStatus(id, "validating");
554
694
  await local.data.validatePins(pins);
555
- const references = sourceReferences(saved.snapshot.document).map((item) => item.source);
556
- for (const map of Object.values(saved.presentation.maps)) for (const element of map.elements) for (const range of element.sourceRanges ?? []) references.push({
557
- ...range,
558
- side: map.side
559
- });
560
- await local.data.validateSources(pins, references);
695
+ await this.validateReferences(saved, repository.id, pins);
561
696
  await local.data.workspaces.open(id, pins);
562
697
  if (local.data.workspaces.list(id).some((workspace) => !workspace.rootPath)) throw new ReviewInputError("Could not prepare the pinned checkout. Retry opening the share.", 409);
563
698
  await writeFile(path.join(this.root, id, "repository.json"), JSON.stringify({
@@ -571,9 +706,71 @@ var SharedReviewStore = class {
571
706
  throw new ReviewInputError(message, 409);
572
707
  }
573
708
  }
709
+ /** Reads each of a shared board's references at its own commits, in the
710
+ * checkout of its repository. */
711
+ async validateReferences(saved, repositoryId, pins) {
712
+ const references = /* @__PURE__ */ new Map();
713
+ const add = (at, range) => {
714
+ const key = JSON.stringify([at.base, at.head]);
715
+ const group = references.get(key) ?? {
716
+ pins: at,
717
+ ranges: []
718
+ };
719
+ group.ranges.push(range);
720
+ references.set(key, group);
721
+ };
722
+ for (const { source } of sourceReferences(saved.snapshot.document, { repositoryId })) add({
723
+ ...anchorPins(source, pins),
724
+ repositoryId
725
+ }, source);
726
+ if (pins) for (const map of Object.values(saved.presentation.maps)) for (const element of map.elements) for (const range of element.sourceRanges ?? []) add(pins, {
727
+ ...range,
728
+ side: map.side
729
+ });
730
+ for (const group of references.values()) await this.local.data.validateSources(group.pins, group.ranges);
731
+ }
732
+ /** Files an unpinned board under a local checkout of its repository,
733
+ * else a managed clone. */
734
+ async prepareUnpinned(id, cloneUrl) {
735
+ const local = this.local;
736
+ const saved = this.validated.get(id);
737
+ if (!cloneUrl || !local) {
738
+ this.setStatus(id, "ready");
739
+ return;
740
+ }
741
+ const root = this.repositoryRoot(id);
742
+ try {
743
+ this.setStatus(id, "fetching");
744
+ const managed = path.join(await realpath(this.root), ".repositories");
745
+ let repositoryId = local.store.checkoutsOf(cloneUrl).find((checkout) => !local.store.repositoryPath(checkout).startsWith(managed));
746
+ if (!repositoryId) {
747
+ if (!existsSync(path.join(root, ".git"))) {
748
+ await rm(root, {
749
+ recursive: true,
750
+ force: true
751
+ });
752
+ await this.cloneRepository(root, cloneUrl);
753
+ }
754
+ repositoryId = (await local.data.register(root)).id;
755
+ }
756
+ await fetchSharedCommits(local.store.repositoryPath(repositoryId), referenceCommits(saved.snapshot, repositoryId), id);
757
+ this.setStatus(id, "validating");
758
+ await this.validateReferences(saved, repositoryId);
759
+ this.repositories.set(id, repositoryId);
760
+ await writeFile(path.join(this.root, id, "repository.json"), JSON.stringify({
761
+ repositoryId,
762
+ ready: true
763
+ }), { mode: 384 });
764
+ this.setStatus(id, "ready");
765
+ } catch (error) {
766
+ const message = error instanceof ReviewInputError ? error.message : "Could not prepare the shared repository. Check Git credentials and retry.";
767
+ this.setStatus(id, "error", message);
768
+ throw new ReviewInputError(message, 409);
769
+ }
770
+ }
574
771
  async assertReady(id) {
575
772
  const saved = this.validated.get(id);
576
- if (!saved || saved.snapshot.pins && !await repositoryReady(this.repositoryRoot(id), saved.snapshot.pins)) {
773
+ if (!saved || saved.snapshot.pins && !await repositoryReady(this.repositoryRoot(id), saved.snapshot.pins, referenceCommits(saved.snapshot))) {
577
774
  this.setStatus(id, "error", "The managed checkout is missing. Reopen the share link to fetch it again.");
578
775
  throw new ReviewInputError(this.status(id).error, 409);
579
776
  }
@@ -612,13 +809,15 @@ var SharedReviewStore = class {
612
809
  repositoryId: z.string(),
613
810
  ready: z.boolean()
614
811
  }).parse(JSON.parse(await readFile(path.join(this.root, id, "repository.json"), "utf8")));
615
- if (this.local?.store.repositoryPath(metadata.repositoryId) === path.join(await realpath(this.root), ".repositories", id)) {
812
+ if (this.local?.store.repositoryPath(metadata.repositoryId) === path.join(await realpath(this.root), ".repositories", id) || !validated.snapshot.pins && this.local?.store.repositories().some((repository) => repository.id === metadata.repositoryId)) {
616
813
  this.repositories.set(id, metadata.repositoryId);
617
814
  prepared = metadata.ready;
618
815
  }
619
816
  } catch {}
620
- if (!validated.snapshot.pins) this.setStatus(id, "ready");
621
- else if (prepared && this.local && await repositoryReady(this.repositoryRoot(id), validated.snapshot.pins)) {
817
+ if (!validated.snapshot.pins) {
818
+ if (this.repositories.has(id) || !validated.manifest.repository) this.setStatus(id, "ready");
819
+ else this.prepare(id).catch(() => {});
820
+ } else if (prepared && this.local && await repositoryReady(this.repositoryRoot(id), validated.snapshot.pins, referenceCommits(validated.snapshot))) {
622
821
  const repository = await this.local.data.register(this.repositoryRoot(id));
623
822
  this.repositories.set(id, repository.id);
624
823
  this.setStatus(id, "ready");
@@ -669,6 +868,9 @@ var SharedReviewStore = class {
669
868
  kind: "commits",
670
869
  ...validated.snapshot.pins,
671
870
  repositoryId
871
+ } : repositoryId ? {
872
+ kind: "project",
873
+ repositoryId
672
874
  } : void 0,
673
875
  shared: {
674
876
  ...bundle.attribution,
@@ -680,10 +882,12 @@ var SharedReviewStore = class {
680
882
  list() {
681
883
  const reviews = this.loaded.keys().filter((id) => this.status(id).stage === "ready" && (!this.validated.get(id)?.snapshot.pins || existsSync(path.join(this.repositoryRoot(id), ".git")))).map((id) => {
682
884
  const { document: _document, ...snapshot } = this.get(id).snapshot;
885
+ const project = snapshot.target ? this.local?.store.checkoutProject(snapshot.target.repositoryId) : void 0;
683
886
  return {
684
887
  ...snapshot,
685
888
  firstCreatedAt: snapshot.version === 0 ? snapshot.createdAt : void 0,
686
889
  repositoryName: "Shared review",
890
+ ...project && { project },
687
891
  viewedAt: this.attention.get(id)?.viewedAt ?? null,
688
892
  trashedAt: this.attention.get(id)?.trashedAt ?? null
689
893
  };
@@ -705,8 +909,10 @@ var SharedReviewStore = class {
705
909
  await this.local?.data.workspaces.remove(id);
706
910
  const repositoryId = this.repositories.get(id);
707
911
  if (repositoryId) {
912
+ const checkout = this.local?.store.repositoryPath(repositoryId);
913
+ if (checkout) await forgetSharedCommits(checkout, id).catch((error) => console.error(`Could not drop the refs kept for share ${id}:`, error));
708
914
  await this.local?.data.forgetRepository(repositoryId);
709
- this.local?.store.unregisterRepository(repositoryId);
915
+ await this.local?.store.unregisterRepository(repositoryId);
710
916
  }
711
917
  await rm(this.repositoryRoot(id), {
712
918
  recursive: true,
@@ -999,4 +1205,4 @@ var ShareClient = class {
999
1205
  }
1000
1206
  };
1001
1207
  //#endregion
1002
- export { sharedReviewId as a, verifyShareRepository as c, StreamLimitError as d, readBoundedStream as f, SharedReviewStore as i, exportShare as l, shareIdSchema as m, ShareClient as n, validateShareBundle as o, parseShareLink as p, SharePreflightError as r, readShareRepository as s, ShareAuthError as t, validateShareSources as u };
1208
+ export { sharedReviewId as a, verifyShareRepository as c, validateShareSources as d, StreamLimitError as f, shareIdSchema as h, SharedReviewStore as i, exportShare as l, parseShareLink as m, ShareClient as n, validateShareBundle as o, readBoundedStream as p, SharePreflightError as r, readShareRepository as s, ShareAuthError as t, referenceCommits as u };
@@ -1,6 +1,7 @@
1
- import "./review-home-paths-j2BRAaNv.js";
2
- import { N as devReviewHome } from "./s3-config-BTO12tCC.js";
3
- import { n as askAgentIds } from "./thread-state-5pYr6_zM.js";
1
+ import "./review-home-paths-D8AoCdd2.js";
2
+ import { N as devReviewHome } from "./s3-config-CjcLyFV5.js";
3
+ import { n as askAgentIds } from "./thread-state-D6MlnGv7.js";
4
+ import { t as ASK_THREAD_ENV } from "./request-origin--8lLCclv.js";
4
5
  import { createRequire } from "node:module";
5
6
  import { constants } from "node:fs";
6
7
  import { access, chmod, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
@@ -277,7 +278,7 @@ async function detectAskAgents(env = process.env) {
277
278
  }
278
279
  /** Runs an adapter with this server's runtime, Desktop's being Electron, or
279
280
  * the user's CLI itself. */
280
- const launchAskAgent = async (agent, cwd, { permissions = "read-only", mcpServers = [], env: base = process.env } = {}) => {
281
+ const launchAskAgent = async (agent, cwd, { permissions = "read-only", mcpServers = [], env: base = process.env, threadId } = {}) => {
281
282
  const spec = askAgents[agent];
282
283
  const executable = await findAgent(spec, base);
283
284
  if (!executable) throw new Error(`${spec.name} is not installed.`);
@@ -285,6 +286,7 @@ const launchAskAgent = async (agent, cwd, { permissions = "read-only", mcpServer
285
286
  ...base,
286
287
  ...askAgentSettings(agent, permissions).env
287
288
  };
289
+ if (threadId) env[ASK_THREAD_ENV] = threadId;
288
290
  delete env.CLAUDECODE;
289
291
  delete env.CLAUDE_CODE_ENTRYPOINT;
290
292
  const { launch } = spec;