@dev.fast/whiteboard 0.3.4-preview.20261019.127 → 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.
Files changed (115) hide show
  1. package/dist/{account-alias-B5-HYi1D.js → account-alias-CprZojXd.js} +8 -13
  2. package/dist/agent-cli-C6SPSII0.js +2 -0
  3. package/dist/{agent-cli-CohHf6Ua.js → agent-cli-CThN7Q59.js} +10 -15
  4. package/dist/{agent-client-Dtc-Kn_w.js → agent-client-BOOi7EUR.js} +3 -13
  5. package/dist/{authoring-tools-B7KU04f7.js → authoring-tools-BNwuMnz6.js} +27 -34
  6. package/dist/{background-server-C03RKxKS.js → background-server-CkvxA-6O.js} +5 -5
  7. package/dist/{background-server-CB9C4uoc.js → background-server-DLSDogkI.js} +1 -1
  8. package/dist/build-info.json +1 -1
  9. package/dist/{cli-CJaEb7zC.js → cli-R4yfa5DT.js} +1 -1
  10. package/dist/{cli-runner-DGDIqh3V.js → cli-runner-Cuc9FSzu.js} +26 -26
  11. package/dist/cli.js +3 -4
  12. package/dist/{client-lKVF_Elo.js → client-BN_FDxFr.js} +47 -253
  13. package/dist/{fuzzy-match-BmVJJm9I.js → fuzzy-match-BO9bK37N.js} +4 -6
  14. package/dist/{headless-host-DRtjf9WA.js → headless-host-CVBOQyRS.js} +1 -1
  15. package/dist/{headless-host-XhL1X4vX.js → headless-host-Cog6SpS9.js} +140 -513
  16. package/dist/{local-data-Cpl4ENbJ.js → local-data-D_9tvdsp.js} +323 -2419
  17. package/dist/{mcp-ovmiUE_x.js → mcp-D1hziANa.js} +9 -5
  18. package/dist/{package-paths-BWxO-UEL.js → package-paths-C--__jEv.js} +1 -1
  19. package/dist/profile-CYPmvBoA.js +2 -0
  20. package/dist/{profile-BadSkCkI.js → profile-DokFw782.js} +8 -20
  21. package/dist/{remote-attach-Dd-YobM6.js → remote-attach-CEJ-kvDY.js} +4 -4
  22. package/dist/{remote-extensions-CNXwqK0D.js → remote-extensions-Blcjr9Ze.js} +3 -3
  23. package/dist/{remote-extensions-VP-kfq0C.js → remote-extensions-GN8Gf_qG.js} +1 -1
  24. package/dist/{remote-language-server-BXk87KCr.js → remote-language-server-BTKBQ7x4.js} +5 -5
  25. package/dist/{remote-uninstall-DuOzsvaD.js → remote-uninstall-DW7Gdryh.js} +6 -6
  26. package/dist/{request-origin-B9qCPiVB.js → request-origin-Zcn_CjX4.js} +12 -45
  27. package/dist/{review-app-launcher-C7el2Uoy.js → review-app-launcher-CkwLclzR.js} +2 -2
  28. package/dist/{review-home-paths-D8AoCdd2.js → review-home-paths-j2BRAaNv.js} +17 -19
  29. package/dist/{review-telemetry-BsoRMC2w.js → review-telemetry-BLNwYnLV.js} +5 -5
  30. package/dist/runtime-CKiWhGrf.js +52 -0
  31. package/dist/runtime.d.ts +1 -1
  32. package/dist/runtime.js +1 -1
  33. package/dist/{s3-CsG2EWED.js → s3-DtJFWC0G.js} +1 -1
  34. package/dist/{s3-config-CjcLyFV5.js → s3-config-BTO12tCC.js} +2 -4
  35. package/dist/{s3-config-zz4GWh2T.js → s3-config-CSy3fx8H.js} +1 -1
  36. package/dist/server/desktop-host.js +76 -29
  37. package/dist/{server-discovery-CKjw3-6h.js → server-discovery-Bey8IqXH.js} +3 -3
  38. package/dist/sharing/index.d.ts +70 -484
  39. package/dist/sharing/index.js +1 -1
  40. package/dist/{src-WPaQn75m.js → src-BMPuxWv4.js} +73 -208
  41. package/dist/{src-BiWNbcF_.js → src-DsM3jO8G.js} +12 -89
  42. package/dist/{stored-document-migration-dFbewRTG.js → stored-document-migration-CChxtFAy.js} +269 -270
  43. package/dist/{thread-state-Do5LX10I.js → thread-state-5pYr6_zM.js} +2 -7
  44. package/dist/{tool-failure-DvI-gys2.js → tool-failure-C9MpbXJJ.js} +3 -10
  45. package/instructions/authoring.md +2 -3
  46. package/instructions/scratchpad.md +23 -0
  47. package/package.json +4 -4
  48. package/src/ask/agents.ts +1 -13
  49. package/src/ask/checkout-files.ts +5 -2
  50. package/src/ask/thread-state.ts +0 -4
  51. package/src/ask/thread.ts +0 -16
  52. package/src/ask/threads.ts +3 -24
  53. package/src/cli-install.ts +2 -13
  54. package/src/cli-runner.ts +1 -4
  55. package/src/cli.ts +1 -4
  56. package/src/diff-selection-migration.ts +34 -48
  57. package/src/lens-selection.ts +2 -38
  58. package/src/review-api/README.md +13 -52
  59. package/src/review-api/activity.ts +4 -1
  60. package/src/review-api/agent-cli.ts +1 -9
  61. package/src/review-api/anchor-quotes.ts +7 -14
  62. package/src/review-api/ask-history.ts +51 -137
  63. package/src/review-api/authoring-tools.ts +30 -45
  64. package/src/review-api/blocks/definition.ts +1 -1
  65. package/src/review-api/blocks/markdown.ts +2 -2
  66. package/src/review-api/document-headings.ts +27 -58
  67. package/src/review-api/document-text.ts +2 -31
  68. package/src/review-api/document.ts +34 -281
  69. package/src/review-api/github-provider.ts +13 -51
  70. package/src/review-api/http.ts +123 -702
  71. package/src/review-api/instructions.ts +19 -0
  72. package/src/review-api/local-data.ts +46 -310
  73. package/src/review-api/mcp.ts +11 -3
  74. package/src/review-api/onboarding.ts +1 -1
  75. package/src/review-api/profile.ts +6 -37
  76. package/src/review-api/public-tools.ts +2 -24
  77. package/src/review-api/pull-request.ts +49 -82
  78. package/src/review-api/request-origin.ts +0 -10
  79. package/src/review-api/review-progress.ts +27 -61
  80. package/src/review-api/store-schema.ts +29 -38
  81. package/src/review-api/store.ts +478 -2142
  82. package/src/review-api/worktree-events.ts +5 -3
  83. package/src/review-api/worktree-source.ts +7 -3
  84. package/src/review-api/worktree-structural.integration.ts +0 -72
  85. package/src/review-preferences.ts +88 -0
  86. package/src/review-stack.ts +23 -15
  87. package/src/runtime.ts +30 -4
  88. package/src/server/desktop-host.ts +1 -6
  89. package/src/server/desktop-server.ts +31 -1
  90. package/src/server/headless-host.ts +2 -0
  91. package/src/server/hono-http.ts +1 -2
  92. package/src/server/legacy-tutorial-cleanup.ts +3 -2
  93. package/src/server/review-gateway-list.ts +5 -3
  94. package/src/server/review-gateway-memory.ts +1 -1
  95. package/src/server/review-lifecycle-telemetry.ts +2 -2
  96. package/src/server/review-server-core.ts +5 -1
  97. package/src/server/structural-comparisons.ts +0 -47
  98. package/src/sharing/export.ts +7 -36
  99. package/src/sharing/host.ts +14 -34
  100. package/src/sharing/import.ts +24 -156
  101. package/src/sharing/repository.ts +20 -128
  102. package/src/startup-trace.ts +3 -3
  103. package/src/stored-document-migration.ts +19 -110
  104. package/src/ui-telemetry-events.ts +11 -33
  105. package/dist/agent-cli-_YyMIMRY.js +0 -2
  106. package/dist/lens-selection-CyJapcsl.js +0 -214
  107. package/dist/profile-DJvkBJNQ.js +0 -2
  108. package/dist/runtime-Bj2tD33R.js +0 -27
  109. package/src/heading-anchors.ts +0 -42
  110. package/src/review-api/board-order.ts +0 -144
  111. package/src/review-api/checkout-groups.ts +0 -560
  112. package/src/review-api/foreign-references.ts +0 -328
  113. package/src/review-api/link-preview.ts +0 -113
  114. package/src/review-api/scratchpad-plan-upgrade.ts +0 -250
  115. package/src/review-api/store-migrations.ts +0 -357
@@ -1,22 +1,21 @@
1
- import { E as runVcs, S as resolveRepoContextSync, d as git, r as defaultBranch } from "./src-WPaQn75m.js";
1
+ import { d as git, r as defaultBranch } from "./src-BMPuxWv4.js";
2
2
  import { c as jsonString, m as isStringValue, n as isJsonObject, p as isObjectValue, u as parseJsonText } from "./src-X9phtB2j.js";
3
- import { a as sharedReviewId, c as verifyShareRepository, d as validateShareSources, f as StreamLimitError, h as shareIdSchema, l as exportShare, m as parseShareLink, n as ShareClient, p as readBoundedStream, r as SharePreflightError, s as readShareRepository, t as ShareAuthError, u as referenceCommits } from "./client-lKVF_Elo.js";
4
- import { f as textualRows, h as diffrMissingError, i as inspectSnapshot, m as diffrExecutable, p as invalidateStructuralComparisons, r as commandSchema } from "./local-data-Cpl4ENbJ.js";
5
- import { N as reviewDiffrSummarizerInputSchema, O as ReviewFeedbackContactSchema, P as decodeStructuralDiffEvent, a as parseReviewVerbRequest, c as boardIconKind, i as parseReviewDesktopVerbResult, j as ReviewOnboardingRunRequestSchema, k as ReviewFeedbackSchema, l as boardTitle, m as REVIEW_CLIENT_REMOTE, p as REVIEW_CLIENT_HEADER, v as REVIEW_HOST_HEADER } from "./src-BiWNbcF_.js";
6
- import { B as TraceStorageDeniedError, D as listReviewTraceSessions, I as isTraceStorageConfigured, O as loadReviewAgentTrace, R as resolveTraceStorage, T as traceMachineEnabled, at as readStoreAuth, et as shellQuote, it as clearStoreAuth, ot as runStoreLogin, rt as DEFAULT_STORE_ORIGIN, z as selectTraceStorage } from "./review-home-paths-D8AoCdd2.js";
7
- import { _ as scopedCoverage, h as emptyCoverage, i as anchorSelection, l as resolveDiffSelection, m as coverageSources, n as SelectionUnavailableError, o as comparisonKey, s as formatAnchor, u as selectionKey, v as subtractIntervals, y as unionIntervals } from "./lens-selection-CyJapcsl.js";
8
- import { B as stepSources, F as selectionReferences, G as lensSelections, J as fileLineRangeSchema, V as UNCATEGORIZED_LENS_ID, _ as boardRepositoryId, f as anchorPins, g as boardLinks, x as elements } from "./stored-document-migration-dFbewRTG.js";
3
+ import { a as sharedReviewId, c as verifyShareRepository, d as StreamLimitError, f as readBoundedStream, l as exportShare, m as shareIdSchema, n as ShareClient, p as parseShareLink, r as SharePreflightError, s as readShareRepository, t as ShareAuthError, u as validateShareSources } from "./client-BN_FDxFr.js";
4
+ import { a as inspectSnapshot, d as invalidateStructuralComparisons, f as diffrExecutable, i as commandSchema, p as diffrMissingError, r as SCRATCHPAD_ID } from "./local-data-D_9tvdsp.js";
5
+ import { D as ReviewFeedbackSchema, E as ReviewFeedbackContactSchema, M as reviewDiffrSummarizerInputSchema, N as decodeStructuralDiffEvent, a as parseReviewVerbRequest, g as REVIEW_HOST_HEADER, i as parseReviewDesktopVerbResult, k as ReviewOnboardingRunRequestSchema } from "./src-DsM3jO8G.js";
6
+ import { B as TraceStorageDeniedError, D as listReviewTraceSessions, I as isTraceStorageConfigured, O as loadReviewAgentTrace, R as resolveTraceStorage, T as traceMachineEnabled, at as openUrlInBrowser, et as shellQuote, it as clearStoreAuth, ot as readStoreAuth, rt as DEFAULT_STORE_ORIGIN, st as runStoreLogin, z as selectTraceStorage } from "./review-home-paths-j2BRAaNv.js";
7
+ import { E as selectionReferences, G as comparisonKey, I as lensSelections, J as selectionKey, K as formatAnchor, M as UNCATEGORIZED_LENS_ID, Q as emptyCoverage, W as anchorSelection, Z as coverageSources, et as scopedCoverage, h as elements, j as stepSources, nt as unionIntervals, q as resolveDiffSelection, tt as subtractIntervals, u as anchorPins, z as fileLineRangeSchema } from "./stored-document-migration-CChxtFAy.js";
9
8
  import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
10
- import { A as processStartIdentity, L as writePrivateJsonAtomic, N as devReviewHome, h as TraceConfigurationError, i as isS3MockMode, j as withFileLock } from "./s3-config-CjcLyFV5.js";
9
+ import { A as processStartIdentity, L as writePrivateJsonAtomic, N as devReviewHome, h as TraceConfigurationError, i as isS3MockMode, j as withFileLock } from "./s3-config-BTO12tCC.js";
11
10
  import { t as errorMessage } from "./error-message-OtiDonty.js";
12
- import { i as readReviewPackageVersion, n as readBuildCommit, t as findReviewPackageRoot } from "./package-paths-BWxO-UEL.js";
13
- import { _ as reviewInstanceIdentity, n as desktopOwnsHome, o as reviewServerDiscoveryPath, p as desktopUsesHome, r as headlessServerLockPath, t as DESKTOP_TAKEOVER_SIGNAL } from "./server-discovery-CKjw3-6h.js";
14
- import { c as askPicksSchema, d as selectionMarkdown, l as askQuestionSchema, n as askAgentIds, r as askChoiceKinds, s as askPermissionsSchema, t as applyAskChange, u as AgentSelectionSchema } from "./thread-state-Do5LX10I.js";
15
- import { t as openReviewProfile } from "./profile-BadSkCkI.js";
16
- import { c as REVIEW_APP_SESSION_ID_HEADER, d as sanitizeUiTelemetryEvent, f as cleanTelemetryText, o as reviewRequestOrigin, r as REVIEW_PERSON_HEADER, s as BUNDLE_FRAME_PATTERN, t as ASK_THREAD_ENV, u as isReportableCleanedMessage } from "./request-origin-B9qCPiVB.js";
17
- import { a as defaultAskPermissions, i as askAgents, l as launchAskAgent, n as askAgentSettings, o as detectAskAgents, r as askAgentTakesMcp, t as fuzzyRank } from "./fuzzy-match-BmVJJm9I.js";
18
- import { a as inspectQuerySchema, i as renderInstructions, o as queryAnchor, r as instructionsQuerySchema, s as readQuerySchemas, t as authoringTools } from "./authoring-tools-B7KU04f7.js";
19
- import { a as headlessServerOwner, c as stopBackgroundServer, o as recordedBackgroundServer } from "./background-server-C03RKxKS.js";
11
+ import { i as readReviewPackageVersion, n as readBuildCommit, t as findReviewPackageRoot } from "./package-paths-C--__jEv.js";
12
+ import { _ as reviewInstanceIdentity, n as desktopOwnsHome, o as reviewServerDiscoveryPath, p as desktopUsesHome, r as headlessServerLockPath, t as DESKTOP_TAKEOVER_SIGNAL } from "./server-discovery-Bey8IqXH.js";
13
+ import { c as askPicksSchema, d as selectionMarkdown, l as askQuestionSchema, n as askAgentIds, r as askChoiceKinds, s as askPermissionsSchema, t as applyAskChange, u as AgentSelectionSchema } from "./thread-state-5pYr6_zM.js";
14
+ import { t as openReviewProfile } from "./profile-DokFw782.js";
15
+ import { a as REVIEW_APP_SESSION_ID_HEADER, c as sanitizeUiTelemetryEvent, i as BUNDLE_FRAME_PATTERN, l as cleanTelemetryText, r as reviewRequestOrigin, s as isReportableCleanedMessage } from "./request-origin-Zcn_CjX4.js";
16
+ import { a as defaultAskPermissions, i as askAgents, l as launchAskAgent, n as askAgentSettings, o as detectAskAgents, r as askAgentTakesMcp, t as fuzzyRank } from "./fuzzy-match-BO9bK37N.js";
17
+ import { a as scratchpadAvailable, c as readQuerySchemas, i as renderInstructions, o as inspectQuerySchema, r as instructionsQuerySchema, s as queryAnchor, t as authoringTools } from "./authoring-tools-BNwuMnz6.js";
18
+ import { a as headlessServerOwner, c as stopBackgroundServer, o as recordedBackgroundServer } from "./background-server-CkvxA-6O.js";
20
19
  import crypto$1, { createHash, randomBytes, randomUUID } from "node:crypto";
21
20
  import { z } from "zod";
22
21
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
@@ -214,7 +213,6 @@ var AskThread = class {
214
213
  /** The number of changes so far; a snapshot carries the one it includes. */
215
214
  seq = 0;
216
215
  listeners = /* @__PURE__ */ new Set();
217
- statusListeners = /* @__PURE__ */ new Set();
218
216
  closers = /* @__PURE__ */ new Set();
219
217
  decisions = /* @__PURE__ */ new Map();
220
218
  process;
@@ -320,19 +318,11 @@ var AskThread = class {
320
318
  snapshot: this.state
321
319
  };
322
320
  }
323
- get status() {
324
- return this.state.status;
325
- }
326
321
  /** Every change after the current `seq`, in order. */
327
322
  subscribe(listener) {
328
323
  this.listeners.add(listener);
329
324
  return () => this.listeners.delete(listener);
330
325
  }
331
- /** Calls back when the status changes, without following the thread. */
332
- onStatusChange(listener) {
333
- this.statusListeners.add(listener);
334
- return () => this.statusListeners.delete(listener);
335
- }
336
326
  /** Whether anything follows the thread, as an open panel does. */
337
327
  watched() {
338
328
  return this.listeners.size > 0;
@@ -589,8 +579,7 @@ var AskThread = class {
589
579
  const generation = ++this.generation;
590
580
  const launched = await this.launch(this.start.agent, this.start.cwd, {
591
581
  permissions: this.state.permissions,
592
- mcpServers: this.mcpServers,
593
- threadId: this.id
582
+ mcpServers: this.mcpServers
594
583
  });
595
584
  if (this.closed || this.halted || generation !== this.generation) {
596
585
  launched.stop();
@@ -1188,7 +1177,6 @@ var AskThread = class {
1188
1177
  change
1189
1178
  };
1190
1179
  for (const listener of this.listeners) listener(update);
1191
- if (change.type === "set" && change.status) for (const listener of this.statusListeners) listener();
1192
1180
  }
1193
1181
  };
1194
1182
  //#endregion
@@ -1273,16 +1261,13 @@ function cliAskTools(cliPath, selector) {
1273
1261
  }] : []
1274
1262
  ];
1275
1263
  return {
1276
- mcpServers: (threadId) => {
1264
+ mcpServers: () => {
1277
1265
  const cli = cliPath();
1278
1266
  return cli ? [{
1279
1267
  name: "whiteboard",
1280
1268
  command: process.execPath,
1281
1269
  args: [cli, "mcp"],
1282
- env: [...env(), {
1283
- name: ASK_THREAD_ENV,
1284
- value: threadId
1285
- }]
1270
+ env: env()
1286
1271
  }] : [];
1287
1272
  },
1288
1273
  cli: () => {
@@ -1310,7 +1295,6 @@ var AskThreads = class {
1310
1295
  * model. */
1311
1296
  offers = /* @__PURE__ */ new Map();
1312
1297
  mcpServers;
1313
- statusListeners = /* @__PURE__ */ new Set();
1314
1298
  constructor(launch, tools = {}, limits = askThreadLimits, unwatchedIdleMs = UNWATCHED_IDLE_MS) {
1315
1299
  this.launch = launch;
1316
1300
  this.tools = tools;
@@ -1331,8 +1315,6 @@ var AskThreads = class {
1331
1315
  open(start) {
1332
1316
  const thread = new AskThread(this.launch, start, this.mcpServers, this.limits);
1333
1317
  this.threads.set(thread.id, thread);
1334
- thread.onStatusChange(() => this.statusChanged());
1335
- this.statusChanged();
1336
1318
  thread.open();
1337
1319
  this.sweeper ??= setInterval(() => this.sweep(), SWEEP_MS);
1338
1320
  this.sweeper.unref?.();
@@ -1363,16 +1345,6 @@ var AskThreads = class {
1363
1345
  if (now - since >= this.unwatchedIdleMs) this.close(id);
1364
1346
  }
1365
1347
  }
1366
- /** Calls back when a thread opens, closes or changes its status. */
1367
- subscribeStatus(listener) {
1368
- this.statusListeners.add(listener);
1369
- return () => {
1370
- this.statusListeners.delete(listener);
1371
- };
1372
- }
1373
- statusChanged() {
1374
- for (const listener of this.statusListeners) listener();
1375
- }
1376
1348
  get(id) {
1377
1349
  return this.threads.get(id);
1378
1350
  }
@@ -1389,9 +1361,9 @@ var AskThreads = class {
1389
1361
  }
1390
1362
  close(id, save = true) {
1391
1363
  const thread = this.threads.get(id);
1364
+ this.threads.delete(id);
1392
1365
  this.unwatchedSince.delete(id);
1393
1366
  thread?.close(save);
1394
- if (this.threads.delete(id)) this.statusChanged();
1395
1367
  }
1396
1368
  closeDeletedReviews(isDeleted) {
1397
1369
  for (const [id, thread] of this.threads) if (isDeleted(thread.reviewId)) this.close(id, false);
@@ -1404,7 +1376,7 @@ var AskThreads = class {
1404
1376
  };
1405
1377
  //#endregion
1406
1378
  //#region src/server/diffr-config.ts
1407
- const execFileAsync = promisify(execFile);
1379
+ const execFileAsync$2 = promisify(execFile);
1408
1380
  let writes = Promise.resolve();
1409
1381
  let testing = false;
1410
1382
  function serialized(operation) {
@@ -1445,7 +1417,7 @@ function diffrArgv(args) {
1445
1417
  async function diffr(args, rootPath, options = {}) {
1446
1418
  const executable = diffrExecutable();
1447
1419
  try {
1448
- const operation = execFileAsync(executable, diffrArgv(args), {
1420
+ const operation = execFileAsync$2(executable, diffrArgv(args), {
1449
1421
  cwd: rootPath,
1450
1422
  maxBuffer: 16777216,
1451
1423
  signal: options.signal ?? AbortSignal.timeout(3e4),
@@ -1875,7 +1847,7 @@ function applyCorsHeaders(request, response) {
1875
1847
  const varyFields = vary?.split(",").map((field) => field.trim().toLowerCase());
1876
1848
  if (!varyFields?.includes("*") && !varyFields?.includes("origin")) response.headers.set("vary", vary ? `${vary}, Origin` : "Origin");
1877
1849
  }
1878
- response.headers.set("access-control-allow-headers", `content-type, x-review-token, ${REVIEW_PERSON_HEADER}, ${REVIEW_APP_SESSION_ID_HEADER}`);
1850
+ response.headers.set("access-control-allow-headers", `content-type, x-review-token, ${REVIEW_APP_SESSION_ID_HEADER}`);
1879
1851
  response.headers.set("access-control-allow-methods", "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS");
1880
1852
  response.headers.set("access-control-allow-private-network", "true");
1881
1853
  response.headers.set("access-control-expose-headers", REVIEW_HOST_HEADER);
@@ -2304,15 +2276,11 @@ async function reviewProgress(store, data, snapshot, signal = new AbortControlle
2304
2276
  return [];
2305
2277
  }
2306
2278
  if (!file) {
2307
- const [base, head] = await Promise.all(["base", "head"].map((side) => data.file(sourcePins, side, source.file).then((value) => value.text, (error) => {
2308
- if (error instanceof ReviewInputError && error.status === 404) return null;
2309
- throw error;
2310
- })));
2311
- if (base === null && head === null) throw new SelectionUnavailableError("missing_file", "File is unavailable at these pins.");
2312
- if (base !== null && head !== null && base !== head) throw new SelectionUnavailableError("no_alignment", "Changed context file has no alignment.");
2313
- rows = textualRows(source.file, "", base?.split("\n").length ?? 0, head?.split("\n").length ?? 0);
2279
+ const [base, head] = await Promise.all([data.file(sourcePins, "base", source.file), data.file(sourcePins, "head", source.file)]);
2280
+ if (base.text !== head.text) throw new Error("Changed context file has no alignment.");
2281
+ rows = Array.from({ length: base.text.split("\n").length }, (_, line) => [line, line]);
2314
2282
  }
2315
- if (!rows) throw new SelectionUnavailableError("no_alignment", "File has no text alignment.");
2283
+ if (!rows) throw new Error("File has no text alignment.");
2316
2284
  const result = resolveDiffSelection(source, rows, file ?? { path: source.file });
2317
2285
  resolvedSelections[key] = result;
2318
2286
  return result;
@@ -2320,8 +2288,8 @@ async function reviewProgress(store, data, snapshot, signal = new AbortControlle
2320
2288
  const resolveAvailable = async (source) => {
2321
2289
  try {
2322
2290
  return await resolve(source);
2323
- } catch (error) {
2324
- unavailableSelections[selectionKey(source)] = error instanceof SelectionUnavailableError ? error.reason : "other";
2291
+ } catch {
2292
+ unavailableSelections[selectionKey(source)] = "Source links need updating at these pins";
2325
2293
  return [];
2326
2294
  }
2327
2295
  };
@@ -2347,10 +2315,7 @@ async function reviewProgress(store, data, snapshot, signal = new AbortControlle
2347
2315
  };
2348
2316
  }
2349
2317
  }));
2350
- await Promise.all(selectionReferences(snapshot.document, {
2351
- tolerant: true,
2352
- repositoryId: boardRepositoryId(snapshot)
2353
- }).map(async ({ source }) => {
2318
+ await Promise.all(selectionReferences(snapshot.document, { tolerant: true }).map(async ({ source }) => {
2354
2319
  try {
2355
2320
  await resolveAvailable(source);
2356
2321
  } catch {}
@@ -2366,7 +2331,7 @@ async function reviewProgress(store, data, snapshot, signal = new AbortControlle
2366
2331
  unavailable: counting || uncategorized.length ? void 0 : "All changed lines are covered by lenses"
2367
2332
  });
2368
2333
  const progress = {
2369
- complete: !counting && !pendingSources.size,
2334
+ complete: !counting,
2370
2335
  files,
2371
2336
  referenceFiles,
2372
2337
  lenses,
@@ -2423,11 +2388,12 @@ function lensReport(lenses, progress) {
2423
2388
  }
2424
2389
  //#endregion
2425
2390
  //#region src/ask/checkout-files.ts
2391
+ const execFileAsync$1 = promisify(execFile);
2426
2392
  /** The checkout's files relative to its root: tracked ones, and new ones git
2427
2393
  * does not ignore. None where git cannot list them. */
2428
2394
  async function checkoutFiles(root) {
2429
2395
  try {
2430
- const { stdout } = await runVcs("git", [
2396
+ const { stdout } = await execFileAsync$1("git", [
2431
2397
  "-C",
2432
2398
  root,
2433
2399
  "ls-files",
@@ -2598,6 +2564,7 @@ function watchAskThreads(threads, backlog = 256) {
2598
2564
  }
2599
2565
  //#endregion
2600
2566
  //#region src/review-stack.ts
2567
+ const execFileAsync = promisify(execFile);
2601
2568
  const GitHubStacksSchema = z.array(z.object({ pull_requests: z.array(z.object({
2602
2569
  number: z.number().int().positive(),
2603
2570
  head: z.object({ ref: z.string().min(1) })
@@ -2645,12 +2612,12 @@ async function gitHubStack(subject, reviews, runGitHubApi) {
2645
2612
  }
2646
2613
  /** The chain of reviews through this one, linked by the base each names. */
2647
2614
  async function localStack(subject, reviews, repositoryPath) {
2648
- const group = reviews.find((review) => review.reviewId === subject.reviewId)?.project?.id;
2615
+ const group = reviews.find((review) => review.reviewId === subject.reviewId)?.repositoryGroup?.key;
2649
2616
  const current = subject.origin?.branch;
2650
2617
  if (!group || !current) return [];
2651
2618
  const main = subject.origin?.baseBranch ? await defaultBranch(repositoryPath).catch(() => null) : null;
2652
2619
  const name = (ref) => ref.replace(/^origin\//, "");
2653
- const inGroup = reviews.filter((review) => review.project?.id === group);
2620
+ const inGroup = reviews.filter((review) => review.repositoryGroup?.key === group);
2654
2621
  const latest = (field, branch) => {
2655
2622
  const matches = inGroup.filter((review) => review.origin?.[field] === branch);
2656
2623
  return matches.findLast((review) => !review.trashedAt) ?? matches.at(-1);
@@ -2677,8 +2644,8 @@ async function forkPoint(repositoryPath, main, branch) {
2677
2644
  const ref = `refs/heads/${branch}`;
2678
2645
  const fork = await git(repositoryPath, [
2679
2646
  "merge-base",
2680
- { rev: main },
2681
- { rev: ref }
2647
+ main,
2648
+ ref
2682
2649
  ], { allowFailure: true });
2683
2650
  if (!fork.ok) return null;
2684
2651
  const base = fork.stdout.trim();
@@ -2688,17 +2655,22 @@ async function forkPoint(repositoryPath, main, branch) {
2688
2655
  "log",
2689
2656
  "--reverse",
2690
2657
  "--format=%s",
2691
- { rev: `${base}..${ref}` }
2658
+ `${base}..${ref}`
2692
2659
  ], { allowFailure: true })).stdout.split("\n")[0] || null
2693
2660
  };
2694
2661
  }
2695
2662
  async function defaultRunGitHubApi(host, endpoint) {
2696
- const { stdout } = await runVcs("gh", [
2663
+ const { stdout } = await execFileAsync("gh", [
2697
2664
  "api",
2698
2665
  "--hostname",
2699
- { value: host },
2700
- { value: endpoint }
2666
+ host,
2667
+ endpoint
2701
2668
  ], {
2669
+ env: {
2670
+ ...process.env,
2671
+ GH_PROMPT_DISABLED: "1"
2672
+ },
2673
+ encoding: "utf8",
2702
2674
  timeout: 1e4,
2703
2675
  maxBuffer: 2097152
2704
2676
  });
@@ -2736,6 +2708,7 @@ const publishSchema = z.strictObject({
2736
2708
  /** Mounted behind local host authentication. Account credentials never enter the renderer. */
2737
2709
  function mountSharingHost(app, store, data, shared, options = {}) {
2738
2710
  const startLogin = options.login ?? runStoreLogin;
2711
+ const openUrl = options.openUrl ?? openUrlInBrowser;
2739
2712
  let login = { pending: false };
2740
2713
  app.get("/sharing/account", async (context) => {
2741
2714
  const account = await readStoreAuth();
@@ -2758,9 +2731,9 @@ function mountSharingHost(app, store, data, shared, options = {}) {
2758
2731
  traces: false,
2759
2732
  stdout: discard,
2760
2733
  stderr: discard,
2761
- openUrl: options.openUrl,
2762
- onDeviceCode: (device) => {
2763
- Object.assign(login, device);
2734
+ openUrl: async (url) => {
2735
+ login.url = url;
2736
+ await openUrl(url);
2764
2737
  }
2765
2738
  }).then((code) => {
2766
2739
  login = { pending: false };
@@ -2829,16 +2802,8 @@ function mountSharingPublisher(app, store, data, options = {}) {
2829
2802
  validateShareSources(snapshot);
2830
2803
  const account = await readSharingAuth();
2831
2804
  if (!account) throw new ReviewInputError("Set DEV_REVIEW_SHARE_TOKEN for CI, or run whiteboard login before sharing.", 409);
2832
- const repositoryId = snapshot.pins?.repositoryId ?? snapshot.target?.repositoryId;
2833
- const root = repositoryId ? store.repositoryPath(repositoryId) : void 0;
2834
- const readRepository = options.readRepository ?? readShareRepository;
2835
- const repository = !root ? void 0 : snapshot.pins ? await readRepository(root) : await (async () => {
2836
- try {
2837
- return await readRepository(root);
2838
- } catch {
2839
- return;
2840
- }
2841
- })();
2805
+ const root = snapshot.pins ? store.repositoryPath(snapshot.pins.repositoryId) : void 0;
2806
+ const repository = root ? await (options.readRepository ?? readShareRepository)(root) : void 0;
2842
2807
  const bundle = await exportShare({
2843
2808
  store,
2844
2809
  data,
@@ -2846,8 +2811,7 @@ function mountSharingPublisher(app, store, data, options = {}) {
2846
2811
  version: snapshot.version,
2847
2812
  repository
2848
2813
  });
2849
- const commits = referenceCommits(snapshot);
2850
- const verification = root && (snapshot.pins || commits.length) && repository ? verifyRepository(root, snapshot.pins, repository, commits).then(() => ({ ok: true }), (error) => ({
2814
+ const verification = root && snapshot.pins && repository ? verifyRepository(root, snapshot.pins, repository).then(() => ({ ok: true }), (error) => ({
2851
2815
  ok: false,
2852
2816
  error
2853
2817
  })) : Promise.resolve({ ok: true });
@@ -2923,14 +2887,8 @@ const clip = (line) => line.trim() === "" ? "(blank line)" : line.length > 120 ?
2923
2887
  */
2924
2888
  async function anchorQuotes(before, after, read) {
2925
2889
  const key = (id, source) => `${id}\0${selectionKey(source)}`;
2926
- const known = new Set(before ? selectionReferences(before.document, {
2927
- tolerant: true,
2928
- repositoryId: boardRepositoryId(before)
2929
- }).map(({ id, source }) => key(id, source)) : []);
2930
- const added = selectionReferences(after.document, {
2931
- tolerant: true,
2932
- repositoryId: boardRepositoryId(after)
2933
- }).filter(({ id, source }) => !known.has(key(id, source)));
2890
+ const known = new Set(before ? selectionReferences(before.document, { tolerant: true }).map(({ id, source }) => key(id, source)) : []);
2891
+ const added = selectionReferences(after.document, { tolerant: true }).filter(({ id, source }) => !known.has(key(id, source)));
2934
2892
  return {
2935
2893
  quotes: (await Promise.all(added.slice(0, QUOTE_LIMIT).map(async ({ source }) => {
2936
2894
  try {
@@ -2955,18 +2913,15 @@ async function anchorQuotes(before, after, read) {
2955
2913
  //#endregion
2956
2914
  //#region src/review-api/document-text.ts
2957
2915
  /** A reading view of saved content, not another document format to maintain. */
2958
- function documentText(snapshot, targetId, full = false, relations) {
2916
+ function documentText(snapshot, targetId, full = false) {
2959
2917
  const target = targetId === void 0 ? void 0 : elements(snapshot.document).find((item) => item.id === targetId);
2960
2918
  if (targetId !== void 0 && !target) throw new ReviewInputError("Target not found in this version.", 404);
2961
2919
  const detailed = full || target !== void 0;
2962
- const lines = [`# ${boardTitle(snapshot.title)}`, `Session ${snapshot.reviewId} · version ${snapshot.version}`];
2963
- if (relations && targetId === void 0) {
2964
- const list = (boards) => boards.map((board) => `${boardTitle(board.title)} (${board.id})`).join(", ");
2965
- if (relations.parent) lines.push(`Parent: ${list([relations.parent])}`);
2966
- if (relations.children.length) lines.push(`Children, in order: ${list(relations.children)}`);
2967
- if (relations.dependsOn.length) lines.push(`Depends on: ${list(relations.dependsOn)}`);
2968
- }
2969
- lines.push("");
2920
+ const lines = [
2921
+ `# ${snapshot.title}`,
2922
+ `Session ${snapshot.reviewId} · version ${snapshot.version}`,
2923
+ ""
2924
+ ];
2970
2925
  const write = (depth, text) => {
2971
2926
  for (const line of text.split("\n")) lines.push(" ".repeat(depth) + line);
2972
2927
  };
@@ -3394,7 +3349,7 @@ function registerOnboardingRoutes(app, store, ask, host, run = runCommand) {
3394
3349
  agent: input.agent,
3395
3350
  ...host.historyHooks({
3396
3351
  id,
3397
- anchor: { board: reviewId },
3352
+ reviewId,
3398
3353
  agent: input.agent,
3399
3354
  version: created.version,
3400
3355
  head,
@@ -3542,16 +3497,12 @@ const askDecisionSchema = z.strictObject({
3542
3497
  permissionId: z.string().min(1),
3543
3498
  optionId: z.string().min(1)
3544
3499
  });
3545
- /** A person's save carries the whole board, which edits one at a time can
3546
- * grow past the size of a single command. */
3547
- const PERSON_EDIT_MAX_BYTES = 16777216;
3548
- /** A request the gateway forwarded from another machine. */
3549
- const remoteCaller = (context) => context.req.header(REVIEW_CLIENT_HEADER) === REVIEW_CLIENT_REMOTE;
3500
+ const SCRATCHPAD_DISABLED = "The scratchpad is off. Turn it on in Whiteboard Desktop Settings.";
3550
3501
  /** Both hosts mount this behind their token authentication. */
3551
3502
  function createReviewApi(store, data, open, shared, capabilities = () => ({
3552
3503
  desktopAvailable: Boolean(open),
3553
3504
  softwareMapEnabled: false
3554
- }), traceEnabled = async () => false, status = () => ({}), hooks = {}, ask) {
3505
+ }), scratchpadEnabled = () => false, traceEnabled = async () => false, status = () => ({}), hooks = {}, ask) {
3555
3506
  const app = new Hono();
3556
3507
  app.onError((error, context) => {
3557
3508
  if (error instanceof HttpJsonError) return context.json({ error: error.message }, error.statusCode);
@@ -3580,7 +3531,15 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3580
3531
  await next();
3581
3532
  };
3582
3533
  const sharedCommandSchema = z.object({ operation: z.object({ reviewId: z.string().optional() }) });
3534
+ const ensureScratchpad = async (id) => {
3535
+ if (open && scratchpadEnabled() && (!id || id === SCRATCHPAD_ID)) await store.ensureScratchpad();
3536
+ };
3537
+ const refuseDisabledScratchpad = (id) => {
3538
+ if (id === SCRATCHPAD_ID && !scratchpadEnabled()) throw new ReviewInputError(SCRATCHPAD_DISABLED, 409);
3539
+ };
3583
3540
  const sharedGuard = async (context, next) => {
3541
+ refuseDisabledScratchpad(context.req.param("id"));
3542
+ await ensureScratchpad(context.req.param("id"));
3584
3543
  const id = context.req.param("id");
3585
3544
  if (!id || !isShared(id)) return next();
3586
3545
  const query = readQuerySchemas.get.parse({ version: context.req.query("version") });
@@ -3599,53 +3558,23 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3599
3558
  if (!snapshot || version !== void 0 && version !== snapshot.version) throw new ReviewInputError("Shared review version is unavailable.", 404);
3600
3559
  return snapshot;
3601
3560
  };
3602
- /** Adds a PR board's state, as this host last read it from GitHub. */
3603
- const withPullRequestState = (board) => {
3604
- const state = data?.pullRequestState(board.origin?.pullRequestUrl ?? board.pullRequestUrl);
3605
- return state ? {
3606
- ...board,
3607
- pullRequestState: state
3608
- } : board;
3609
- };
3610
- const catalog = () => [...store.list().map(withPullRequestState), ...shared?.list() ?? []];
3611
- /** Where a board sits among its project's, and its PR's facts, for the
3612
- * board's header. */
3613
- const placeOf = (id) => {
3614
- if (isShared(id) || !store.has(id)) return { siblings: [] };
3615
- const { board, parent, siblings, children } = store.place(id);
3616
- const url = board.origin?.pullRequestUrl;
3617
- const state = data?.pullRequestState(url);
3618
- const baseBranch = url && data?.pullRequestBaseBranch(url);
3619
- const headBranch = url && data?.pullRequestHeadBranch(url);
3620
- const place = ({ reviewId, title, target }) => ({
3621
- reviewId,
3622
- title,
3623
- icon: boardIconKind(target)
3624
- });
3625
- const listed = (child) => {
3626
- const childState = data?.pullRequestState(child.origin?.pullRequestUrl);
3627
- return {
3628
- ...place(child),
3629
- ...child.origin?.pullRequestNumber && { pullRequestNumber: child.origin.pullRequestNumber },
3630
- ...childState && { pullRequestState: childState }
3631
- };
3632
- };
3633
- return {
3634
- ...state && { pullRequestState: state },
3635
- ...baseBranch && { baseBranch },
3636
- ...headBranch && { headBranch },
3637
- board: place(board),
3638
- ...parent && { parent: place(parent) },
3639
- siblings: siblings.map(listed),
3640
- ...children?.length && { children: children.map(listed) }
3641
- };
3561
+ const catalog = () => {
3562
+ const local = store.list();
3563
+ return [...scratchpadEnabled() ? local : local.filter((summary) => summary.kind !== "scratchpad"), ...shared?.list() ?? []];
3642
3564
  };
3643
- app.get("/", (context) => context.json(catalog()));
3565
+ app.get("/", async (context) => {
3566
+ await ensureScratchpad();
3567
+ return context.json(catalog());
3568
+ });
3644
3569
  app.get("/feedback-eligibility", (context) => context.json({ eligible: store.feedbackEligible() }));
3645
- const instructionContext = async () => ({ traceEnabled: await traceEnabled() });
3570
+ const instructionContext = async () => ({
3571
+ desktopAvailable: Boolean(open),
3572
+ scratchpadEnabled: scratchpadEnabled(),
3573
+ traceEnabled: await traceEnabled()
3574
+ });
3646
3575
  app.get("/authoring", async (context) => {
3647
3576
  const instructions = await instructionContext();
3648
- return context.json(authoringTools(instructions.traceEnabled));
3577
+ return context.json(authoringTools(scratchpadAvailable(instructions), instructions.traceEnabled));
3649
3578
  });
3650
3579
  app.get("/instructions", async (context) => {
3651
3580
  const { topic } = instructionsQuerySchema.parse(context.req.query());
@@ -3699,7 +3628,10 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3699
3628
  ...status(),
3700
3629
  desktopAvailable: (await capabilities()).desktopAvailable
3701
3630
  }));
3702
- app.get("/capabilities", async (context) => context.json(await capabilities()));
3631
+ app.get("/capabilities", async (context) => context.json({
3632
+ ...await capabilities(),
3633
+ scratchpadEnabled: scratchpadEnabled()
3634
+ }));
3703
3635
  app.get("/:id/activity", (context) => {
3704
3636
  const id = context.req.param("id");
3705
3637
  readReview(id);
@@ -3721,9 +3653,6 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3721
3653
  action
3722
3654
  } : input));
3723
3655
  });
3724
- app.post("/:id/person-edit", async (context) => context.json(await store.personEdit(context.req.param("id"), await readBoundedRequestJson(context.req.raw, PERSON_EDIT_MAX_BYTES))));
3725
- app.post("/:id/focus", async (context) => context.json(store.focus(context.req.param("id"), await readBoundedRequestJson(context.req.raw))));
3726
- app.post("/:id/discard", async (context) => context.json(await store.discard(context.req.param("id"))));
3727
3656
  app.get("/watch", (context) => {
3728
3657
  let input;
3729
3658
  try {
@@ -3740,16 +3669,6 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3740
3669
  let dirtyList = lists;
3741
3670
  const dirtyReviews = new Set(reviewIds);
3742
3671
  let sentList;
3743
- const linksRevision = /* @__PURE__ */ new Map();
3744
- const linksKey = (id) => {
3745
- if (!data || isShared(id) || !store.has(id)) return "";
3746
- const { outgoing, backlinks } = store.links(id);
3747
- return JSON.stringify([...outgoing, ...backlinks].map((link) => link.pullRequestUrl && data.pullRequestState(link.pullRequestUrl, { seen: false })));
3748
- };
3749
- const linksKeys = new Map([...reviewIds].map((reviewId) => [reviewId, linksKey(reviewId)]));
3750
- const placeRevision = /* @__PURE__ */ new Map();
3751
- const placeKey = (id) => JSON.stringify(placeOf(id));
3752
- const placeKeys = new Map([...reviewIds].map((reviewId) => [reviewId, placeKey(reviewId)]));
3753
3672
  const readReviewLine = (reviewId) => {
3754
3673
  try {
3755
3674
  return {
@@ -3758,9 +3677,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3758
3677
  value: {
3759
3678
  ...readReview(reviewId),
3760
3679
  activity: store.activity.read(reviewId),
3761
- coverageRevision: data?.coverageRevision ?? 0,
3762
- linksRevision: linksRevision.get(reviewId) ?? 0,
3763
- placeRevision: placeRevision.get(reviewId) ?? 0
3680
+ coverageRevision: data?.coverageRevision ?? 0
3764
3681
  }
3765
3682
  };
3766
3683
  } catch (error) {
@@ -3792,18 +3709,6 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3792
3709
  dirtyReviews.add(reviewId);
3793
3710
  return true;
3794
3711
  };
3795
- /** Bumps the place of each board whose place changed. */
3796
- const refreshPlaces = () => {
3797
- let marked = false;
3798
- for (const [id, key] of placeKeys) {
3799
- const next = placeKey(id);
3800
- if (next === key) continue;
3801
- placeKeys.set(id, next);
3802
- placeRevision.set(id, (placeRevision.get(id) ?? 0) + 1);
3803
- marked = markReview(id) || marked;
3804
- }
3805
- return marked;
3806
- };
3807
3712
  const markLists = () => {
3808
3713
  dirtyList ||= lists;
3809
3714
  return lists;
@@ -3816,12 +3721,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3816
3721
  notify();
3817
3722
  }) ?? (() => {}),
3818
3723
  store.subscribe((result) => {
3819
- let marked = markReview(result.reviewId);
3820
- for (const from of store.linkingBoards(result.reviewId)) {
3821
- linksRevision.set(from, (linksRevision.get(from) ?? 0) + 1);
3822
- marked = markReview(from) || marked;
3823
- }
3824
- if (marked) notify();
3724
+ if (markReview(result.reviewId)) notify();
3825
3725
  }),
3826
3726
  store.activity.subscribe((id) => {
3827
3727
  if (markReview(id)) notify();
@@ -3829,24 +3729,11 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3829
3729
  store.activity.subscribeWorking(() => {
3830
3730
  if (markLists()) notify();
3831
3731
  }),
3832
- data?.subscribePullRequestStates(() => {
3833
- let marked = markLists();
3834
- marked = refreshPlaces() || marked;
3835
- for (const reviewId of reviewIds) {
3836
- const next = linksKey(reviewId);
3837
- if (next === linksKeys.get(reviewId)) continue;
3838
- linksKeys.set(reviewId, next);
3839
- linksRevision.set(reviewId, (linksRevision.get(reviewId) ?? 0) + 1);
3840
- marked = markReview(reviewId) || marked;
3841
- }
3842
- if (marked) notify();
3843
- }) ?? (() => {}),
3844
3732
  shared?.subscribe(() => {
3845
3733
  if (markLists()) notify();
3846
3734
  }) ?? (() => {}),
3847
3735
  store.subscribeCatalog(() => {
3848
- const marked = refreshPlaces();
3849
- if (markLists() || marked) notify();
3736
+ if (markLists()) notify();
3850
3737
  })
3851
3738
  ];
3852
3739
  return () => stops.forEach((stop) => stop());
@@ -3902,18 +3789,19 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3902
3789
  storage: z.enum(["s3", "hosted"]).optional(),
3903
3790
  trace: z.string().min(1).optional()
3904
3791
  });
3905
- const tracePins = async (id, version) => {
3906
- const snapshot = readReview(id, version);
3907
- return snapshot.pins ?? await data.documentPins(snapshot);
3792
+ const tracePins = (id, version) => {
3793
+ const { pins } = readReview(id, version);
3794
+ if (!pins) throw new ReviewInputError("This document has no source pins of its own.", 409);
3795
+ return pins;
3908
3796
  };
3909
3797
  app.get("/:id/agent-traces", async (context) => {
3910
3798
  const query = traceQuery.parse(context.req.query());
3911
- const pins = await tracePins(context.req.param("id"), query.version);
3799
+ const pins = tracePins(context.req.param("id"), query.version);
3912
3800
  return context.json(await listPinnedTraces(store.repositoryPath(pins.repositoryId), pins, query.storage));
3913
3801
  });
3914
3802
  app.get("/:id/agent-traces/:sessionId", async (context) => {
3915
3803
  const query = traceQuery.parse(context.req.query());
3916
- const pins = await tracePins(context.req.param("id"), query.version);
3804
+ const pins = tracePins(context.req.param("id"), query.version);
3917
3805
  const result = await readStoredTrace(store.repositoryPath(pins.repositoryId), context.req.param("sessionId"), query.trace, query.storage);
3918
3806
  if (!result.ok) return context.json({
3919
3807
  ok: false,
@@ -3956,16 +3844,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3956
3844
  const input = z.strictObject({ path: z.string().min(1) }).parse(await readBoundedRequestJson(context.req.raw));
3957
3845
  return context.json(await data.register(input.path));
3958
3846
  });
3959
- app.post("/resources", async (context) => {
3960
- const body = await readBoundedRequestJson(context.req.raw, 8388608);
3961
- const named = z.looseObject({ repositoryPath: z.string().min(1) }).safeParse(body);
3962
- if (!named.success) return context.json(await data.upload(body));
3963
- const { repositoryPath, ...upload } = named.data;
3964
- return context.json(await data.upload({
3965
- ...upload,
3966
- repositoryId: (await data.register(repositoryPath)).id
3967
- }));
3968
- });
3847
+ app.post("/resources", async (context) => context.json(await data.upload(await readBoundedRequestJson(context.req.raw, 8388608))));
3969
3848
  app.get("/:id/resources/:resourceId", async (context) => {
3970
3849
  const id = context.req.param("id");
3971
3850
  const snapshot = readReview(id);
@@ -4072,11 +3951,6 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
4072
3951
  const { pins } = await data.resolveSource(readReview(context.req.param("id"), input.version), input.commit, queryAnchor(input));
4073
3952
  return context.json(await data.changes(pins));
4074
3953
  });
4075
- app.get("/:id/commit/:commit", async (context) => {
4076
- const repositoryId = boardRepositoryId(readReview(context.req.param("id")));
4077
- if (!repositoryId) throw new ReviewInputError("This board has no repository.", 404);
4078
- return context.json(await data.commitInfo(repositoryId, context.req.param("commit")));
4079
- });
4080
3954
  app.get("/:id/commits", async (context) => {
4081
3955
  const input = readQuerySchemas.commits.parse(context.req.query());
4082
3956
  return context.json(await data.commits((await data.resolveSource(readReview(context.req.param("id"), input.version))).pins));
@@ -4109,12 +3983,20 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
4109
3983
  head: `b/${diff.newPath}`
4110
3984
  } : void 0);
4111
3985
  return [
4112
- `Whiteboard: ${boardTitle(snapshot.title)}`,
3986
+ `Whiteboard: ${snapshot.title}`,
4113
3987
  `Session ID: ${snapshot.reviewId}`,
4114
3988
  `Version: ${snapshot.version}`,
4115
3989
  ...selection.apiSource?.commit ? [`Selected commit: ${selection.apiSource.commit}`] : [],
4116
- ...selection.apiSource?.pins ? [...selection.apiSource.pins.base ? [`Selected base: ${selection.apiSource.pins.base}`] : [], `Selected head: ${selection.apiSource.pins.head}`] : [],
4117
- ...snapshot.pins ? [`Session base: ${snapshot.pins.base}`, `Session head: ${snapshot.pins.head}`] : [],
3990
+ ...selection.apiSource?.pins ? [
3991
+ `Selected repository ID: ${selection.apiSource.pins.repositoryId}`,
3992
+ ...selection.apiSource.pins.base ? [`Selected base: ${selection.apiSource.pins.base}`] : [],
3993
+ `Selected head: ${selection.apiSource.pins.head}`
3994
+ ] : [],
3995
+ ...snapshot.pins ? [
3996
+ `Repository ID: ${snapshot.pins.repositoryId}`,
3997
+ `Session base: ${snapshot.pins.base}`,
3998
+ `Session head: ${snapshot.pins.head}`
3999
+ ] : [],
4118
4000
  `Read this version with session_get({"sessionId":"${snapshot.reviewId}","version":${snapshot.version},"full":true}).`,
4119
4001
  "",
4120
4002
  text,
@@ -4123,10 +4005,9 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
4123
4005
  ].join("\n");
4124
4006
  }
4125
4007
  if (ask && data) {
4126
- const projectOf = (reviewId) => isShared(reviewId) || !store.has(reviewId) ? void 0 : store.projectOf(reviewId);
4127
4008
  const readThread = (reviewId, threadId) => {
4128
4009
  const thread = ask.threads.get(threadId);
4129
- if (!thread) throw new ReviewInputError("This conversation has ended.", 404);
4010
+ if (!thread || thread.reviewId !== reviewId) throw new ReviewInputError("This conversation has ended.", 404);
4130
4011
  return thread;
4131
4012
  };
4132
4013
  /** Where a thread in a review saves itself: its Ask history, recorded
@@ -4176,7 +4057,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
4176
4057
  const reach = await ask.threads.reach(agent, threadId);
4177
4058
  const { reviewId } = snapshot;
4178
4059
  return [
4179
- `A reviewer is reading "${boardTitle(snapshot.title)}" in Whiteboard and has a question about ${selection.target.kind === "review" ? "it" : "a selection"}.`,
4060
+ `A reviewer is reading "${snapshot.title}" in Whiteboard and has a question about ${selection.target.kind === "review" ? "it" : "a selection"}.`,
4180
4061
  checkout.live ? "Your working directory is the repository the review describes." : `Your working directory is a checkout of the review's head commit, ${checkout.head}. Answer from this code, not from other branches.`,
4181
4062
  "Answer the question for a staff engineer: lead with the answer and keep it short. Explain at the level of components and data flow before functions, and check each claim about the code against code you have read.",
4182
4063
  "Name files by their path from the checkout root, with a line where it helps, as in `src/app.ts:42`; the reviewer can open them from your answer.",
@@ -4227,17 +4108,13 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
4227
4108
  const permissions = input.permissions ?? defaultAskPermissions(input.agent);
4228
4109
  const initialContext = await askContext(id, input.agent, snapshot, checkout, input.selection, version);
4229
4110
  assertWritable(reviewId);
4230
- store.askHistory.markViewed(id, createdAt);
4231
4111
  const thread = ask.threads.open({
4232
4112
  id,
4233
4113
  reviewId,
4234
4114
  agent: input.agent,
4235
4115
  ...historyHooks({
4236
4116
  id,
4237
- anchor: target.kind === "text" && target.anchor ? {
4238
- board: reviewId,
4239
- block: target.anchor.blockId
4240
- } : { board: reviewId },
4117
+ reviewId,
4241
4118
  agent: input.agent,
4242
4119
  version: snapshot.version,
4243
4120
  head: checkout.head,
@@ -4261,95 +4138,23 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
4261
4138
  });
4262
4139
  return context.json({ threadId: thread.id });
4263
4140
  });
4264
- /** Every project's threads for the sidebar: who runs them, where they
4265
- * started and live, and which boards they changed. */
4266
- const projectThreads = (reviewId) => {
4267
- const entries = store.askHistory.listAll();
4268
- const ids = entries.map((entry) => entry.id);
4269
- const edited = store.boardsChangedByThreads(ids);
4270
- const viewed = store.askHistory.viewedAt(ids);
4271
- const boards = /* @__PURE__ */ new Map();
4272
- const board = (id) => {
4273
- let found = boards.get(id);
4274
- if (!found) {
4275
- found = store.has(id) ? store.boardFace(id) : {
4276
- reviewId: id,
4277
- icon: boardIconKind(void 0)
4278
- };
4279
- boards.set(id, found);
4280
- }
4281
- return found;
4282
- };
4283
- const project = projectOf(reviewId);
4284
- return {
4285
- board: reviewId,
4286
- ...project && { project },
4287
- threads: entries.map((entry) => {
4288
- const anchor = entry.anchor ?? { board: reviewId };
4289
- const status = ask.threads.get(entry.id)?.status;
4290
- const viewedAt = viewed.get(entry.id);
4291
- const unread = status !== "starting" && status !== "running" && status !== "waiting" && viewedAt !== void 0 && entry.updatedAt > viewedAt;
4292
- return {
4293
- id: entry.id,
4294
- ...entry.projectId && { projectId: entry.projectId },
4295
- ...store.has(anchor.board) && { place: store.boardPlace(anchor.board) },
4296
- agent: entry.agent,
4297
- agentName: askAgents[entry.agent].name,
4298
- opening: entry.question ?? entry.title,
4299
- pinned: entry.selection.target.kind === "text" && entry.selection.target.anchor !== void 0,
4300
- ...status && { status },
4301
- ...unread && { unread },
4302
- updatedAt: entry.updatedAt,
4303
- anchor: {
4304
- board: board(anchor.board),
4305
- ...anchor.block && { block: anchor.block }
4306
- },
4307
- edited: edited.get(entry.id) ?? []
4308
- };
4309
- })
4310
- };
4311
- };
4312
4141
  app.get("/:id/ask/threads", (context) => {
4313
4142
  const reviewId = context.req.param("id");
4314
4143
  readReview(reviewId);
4315
- if (context.req.query("watch") !== void 0) {
4316
- let sent = "";
4317
- return watch(() => {
4318
- const line = JSON.stringify(projectThreads(reviewId));
4319
- if (line === sent) return [];
4320
- sent = line;
4321
- return [line];
4322
- }, (notify) => {
4323
- const stops = [
4324
- store.askHistory.subscribe(notify),
4325
- ask.threads.subscribeStatus(notify),
4326
- store.subscribe(() => notify())
4327
- ];
4328
- return () => stops.forEach((stop) => stop());
4329
- });
4330
- }
4331
- return context.json({
4332
- threads: store.askHistory.list(reviewId, projectOf(reviewId)),
4333
- board: reviewId
4334
- });
4144
+ return context.json({ threads: store.askHistory.list(reviewId) });
4335
4145
  });
4336
4146
  app.post("/:id/ask/watch", reviewAccess("read"), async (context) => {
4337
4147
  const reviewId = context.req.param("id");
4338
4148
  readReview(reviewId);
4339
4149
  const { threads } = askWatchSchema.parse(await readBoundedRequestJson(context.req.raw));
4340
4150
  return watchAskThreads(new Map(threads.map((threadId) => {
4341
- return [threadId, ask.threads.get(threadId)];
4151
+ const thread = ask.threads.get(threadId);
4152
+ return [threadId, thread?.reviewId === reviewId ? thread : void 0];
4342
4153
  })));
4343
4154
  });
4344
- app.get("/:id/ask/:threadId/boards", (context) => {
4345
- const threadId = context.req.param("threadId");
4346
- const anchor = store.askHistory.get(threadId)?.anchor.board ?? ask.threads.get(threadId)?.reviewId;
4347
- if (remoteCaller(context) || !anchor) throw new ReviewInputError("This conversation was not found.", 404);
4348
- return context.json({ boards: store.boardsChangedBy(threadId) });
4349
- });
4350
4155
  app.get("/:id/ask/:threadId", (context) => {
4351
4156
  const record = store.askHistory.get(context.req.param("threadId"));
4352
- if (!record) throw new ReviewInputError("This conversation was not found.", 404);
4157
+ if (record?.reviewId !== context.req.param("id")) throw new ReviewInputError("This conversation was not found.", 404);
4353
4158
  const target = record.selection.target;
4354
4159
  return context.json({
4355
4160
  id: record.id,
@@ -4371,19 +4176,18 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
4371
4176
  app.post("/:id/ask/:threadId/open", reviewAccess("edit"), async (context) => {
4372
4177
  const reviewId = context.req.param("id");
4373
4178
  const threadId = context.req.param("threadId");
4374
- if (ask.threads.get(threadId)) return context.json({ threadId });
4179
+ if (ask.threads.get(threadId)?.reviewId === reviewId) return context.json({ threadId });
4375
4180
  const record = store.askHistory.get(threadId);
4376
4181
  const { picks } = askOpenSchema.parse(await readBoundedRequestJson(context.req.raw, void 0, {}));
4377
- if (!record) throw new ReviewInputError("This conversation was not found.", 404);
4378
- const anchor = record.anchor.board;
4379
- const snapshot = readReview(anchor, record.version);
4182
+ if (record?.reviewId !== reviewId) throw new ReviewInputError("This conversation was not found.", 404);
4183
+ const snapshot = readReview(reviewId, record.version);
4380
4184
  const checkout = await data.agentCheckout(snapshot);
4381
4185
  const target = record.selection.target;
4382
4186
  const initialContext = await askContext(record.id, record.agent, snapshot, checkout, record.selection, record.version);
4383
4187
  assertWritable(reviewId);
4384
4188
  ask.threads.open({
4385
4189
  id: record.id,
4386
- reviewId: anchor,
4190
+ reviewId,
4387
4191
  agent: record.agent,
4388
4192
  cwd: checkout.rootPath,
4389
4193
  head: checkout.head,
@@ -4459,91 +4263,27 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
4459
4263
  await readThread(context.req.param("id"), context.req.param("threadId")).cancel();
4460
4264
  return context.json({ ok: true });
4461
4265
  });
4462
- app.post("/:id/ask/:threadId/viewed", (context) => {
4463
- const threadId = context.req.param("threadId");
4464
- if (!ask.threads.get(threadId) && !store.askHistory.get(threadId)) throw new ReviewInputError("This conversation was not found.", 404);
4465
- store.askHistory.markViewed(threadId);
4466
- return context.json({ ok: true });
4467
- });
4468
4266
  app.post("/:id/ask/:threadId/close", reviewAccess("read"), (context) => {
4469
4267
  readThread(context.req.param("id"), context.req.param("threadId"));
4470
4268
  ask.threads.close(context.req.param("threadId"), !readReview(context.req.param("id")).trashedAt);
4471
4269
  return context.json({ ok: true });
4472
4270
  });
4473
4271
  app.delete("/:id/ask/:threadId", reviewAccess("edit"), (context) => {
4474
- context.req.param("id");
4272
+ const reviewId = context.req.param("id");
4475
4273
  const threadId = context.req.param("threadId");
4476
- if (!store.askHistory.get(threadId)) throw new ReviewInputError("This conversation was not found.", 404);
4477
- if (ask.threads.get(threadId)) ask.threads.close(threadId);
4274
+ if (store.askHistory.get(threadId)?.reviewId !== reviewId) throw new ReviewInputError("This conversation was not found.", 404);
4275
+ if (ask.threads.get(threadId)?.reviewId === reviewId) ask.threads.close(threadId);
4478
4276
  store.askHistory.delete(threadId);
4479
4277
  return context.json({ ok: true });
4480
4278
  });
4481
4279
  }
4482
- app.get("/:id/place", (context) => {
4483
- const id = context.req.param("id");
4484
- if (!isShared(id)) store.assertExists(id);
4485
- return context.json(placeOf(id));
4486
- });
4487
4280
  app.get("/:id/stack", async (context) => {
4488
4281
  const query = readQuerySchemas.get.parse(context.req.query());
4489
4282
  const id = context.req.param("id");
4490
4283
  const snapshot = readReview(id, query.version);
4491
4284
  if (isShared(id) || !snapshot.pins) return context.json({ layers: [] });
4492
- const repositoryPath = store.repositories().find((repository) => repository.id === snapshot.pins.repositoryId)?.path;
4493
- const layers = await resolveReviewStackLayers(snapshot, store.list(), { repositoryPath });
4494
- const trunk = layers.length > 1 && repositoryPath ? await defaultBranch(repositoryPath).catch(() => null) : null;
4495
- return context.json({
4496
- layers: layers.map((layer) => {
4497
- const state = data?.pullRequestState(layer.pullRequestUrl ?? void 0);
4498
- return state ? {
4499
- ...layer,
4500
- pullRequestState: state
4501
- } : layer;
4502
- }),
4503
- ...trunk && { base: trunk.ref.replace(/^origin\//, "") }
4504
- });
4505
- });
4506
- app.get("/:id/project", (context) => {
4507
- const id = context.req.param("id");
4508
- if (isShared(id)) return context.json({
4509
- project: null,
4510
- boards: []
4511
- });
4512
- const project = store.summary(id)?.project ?? null;
4513
- return context.json({
4514
- project,
4515
- boards: project ? store.list().filter((board) => board.project?.id === project.id && !board.trashedAt).map(withPullRequestState) : []
4516
- });
4517
- });
4518
- app.get("/:id/links", (context) => {
4519
- const id = context.req.param("id");
4520
- if (isShared(id)) return context.json({
4521
- outgoing: boardLinks(id, readReview(id).document).map(({ from, to }) => ({
4522
- from,
4523
- to,
4524
- missing: true,
4525
- elsewhere: true
4526
- })),
4527
- backlinks: []
4528
- });
4529
- const { version } = readQuerySchemas.get.pick({ version: true }).parse({ version: context.req.query("version") });
4530
- const { outgoing, backlinks } = store.links(id, version);
4531
- return context.json({
4532
- outgoing: outgoing.map(withPullRequestState),
4533
- backlinks: backlinks.map(withPullRequestState)
4534
- });
4535
- });
4536
- app.get("/:id/links/peek", (context) => {
4537
- const id = context.req.param("id");
4538
- if (isShared(id)) return context.json({});
4539
- const { version } = readQuerySchemas.get.pick({ version: true }).parse({ version: context.req.query("version") });
4540
- const board = context.req.query("board");
4541
- const block = context.req.query("block");
4542
- if (!board) throw new ReviewInputError("Name the linked board.", 400);
4543
- return context.json(store.linkPeek(id, {
4544
- board,
4545
- ...block && { block }
4546
- }, version));
4285
+ const layers = await resolveReviewStackLayers(snapshot, store.list(), { repositoryPath: store.repositories().find((repository) => repository.id === snapshot.pins.repositoryId)?.path });
4286
+ return context.json({ layers });
4547
4287
  });
4548
4288
  app.get("/:id/history", (context) => {
4549
4289
  const id = context.req.param("id");
@@ -4552,39 +4292,14 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
4552
4292
  return context.json([{
4553
4293
  version: snapshot.version,
4554
4294
  title: snapshot.title,
4555
- createdAt: snapshot.createdAt,
4556
- author: "agent"
4295
+ createdAt: snapshot.createdAt
4557
4296
  }]);
4558
4297
  });
4559
- /** A board's relations as session_list reports them: only those it has. */
4560
- const listedRelations = ({ parentId, children, dependsOn }) => {
4561
- const listed = {};
4562
- if (parentId) listed.parentId = parentId;
4563
- if (children.length) listed.children = children;
4564
- if (dependsOn.length) listed.dependsOn = dependsOn;
4565
- return listed;
4566
- };
4567
- /** A board's relations, each board named by its title. */
4568
- const relatedBoards = (id) => {
4569
- const { parentId, children, dependsOn } = store.relations(id);
4570
- const named = (board) => ({
4571
- id: board,
4572
- title: store.read(board).title
4573
- });
4574
- return {
4575
- ...parentId && { parent: named(parentId) },
4576
- children: children.map(named),
4577
- dependsOn: dependsOn.map(named)
4578
- };
4579
- };
4580
4298
  app.get("/:id/inspect", (context) => {
4581
4299
  const query = inspectQuerySchema.parse(context.req.query());
4582
4300
  const id = context.req.param("id");
4583
4301
  const snapshot = readReview(id, query.version);
4584
- return context.json(query.format === "text" ? documentText(snapshot, query.targetId, Boolean(query.full), isShared(id) ? void 0 : relatedBoards(id)) : query.targetId !== void 0 ? inspectSnapshot(snapshot, query.targetId) : {
4585
- ...query.full ? snapshot : inspectSnapshot(snapshot),
4586
- ...!isShared(id) && listedRelations(store.relations(id))
4587
- });
4302
+ return context.json(query.format === "text" ? documentText(snapshot, query.targetId, Boolean(query.full)) : query.targetId !== void 0 ? inspectSnapshot(snapshot, query.targetId) : query.full ? snapshot : inspectSnapshot(snapshot));
4588
4303
  });
4589
4304
  app.get("/:id", async (context) => {
4590
4305
  const query = readQuerySchemas.get.parse(context.req.query());
@@ -4611,13 +4326,17 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
4611
4326
  };
4612
4327
  app.post("/commands", async (context) => {
4613
4328
  const { command: body, open: requestedOpen } = takeCreateOpen(await readBoundedRequestJson(context.req.raw));
4614
- const register = async (path) => data ? data.register(path) : store.registerRepository(path);
4615
- const request = withBoardRepository(await locateRepositories(await pullRequestTargets(body, register, (url, repositoryId) => {
4329
+ const request = await locateRepositories(body, (path) => {
4616
4330
  if (!data) throw new ReviewInputError("Repositories are unavailable.");
4617
- return data.resolvePullRequest(url, { id: repositoryId });
4618
- }, (id) => store.read(id)), register), (id) => store.has(id) ? store.read(id) : void 0);
4331
+ return data.register(path);
4332
+ });
4619
4333
  const input = commandSchema.parse(request);
4620
4334
  const command = sharedCommandSchema.safeParse(input);
4335
+ if (command.success) {
4336
+ refuseDisabledScratchpad(command.data.operation.reviewId);
4337
+ await ensureScratchpad(command.data.operation.reviewId);
4338
+ }
4339
+ if (input.operation.type === "create" && input.operation.kind === "scratchpad") refuseDisabledScratchpad(SCRATCHPAD_ID);
4621
4340
  if (command.success && command.data.operation.reviewId?.startsWith("shared-")) {
4622
4341
  const parsed = commandSchema.parse(input);
4623
4342
  if (shared && parsed.operation.type === "delete") {
@@ -4642,12 +4361,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
4642
4361
  }
4643
4362
  throw new ReviewInputError("Shared reviews are read-only.", 409);
4644
4363
  }
4645
- const threadId = remoteCaller(context) ? void 0 : context.req.header("x-review-thread")?.trim() || void 0;
4646
- const author = !remoteCaller(context) && context.req.header("x-review-person") !== void 0 ? { kind: "person" } : {
4647
- kind: "agent",
4648
- ...threadId && { threadId }
4649
- };
4650
- const result = await store.execute(input, void 0, author);
4364
+ const result = await store.execute(input);
4651
4365
  if (result.deleted || result.lifecycle && store.read(result.reviewId).trashedAt) ask?.threads.closeDeletedReviews((id) => id === result.reviewId);
4652
4366
  if (input.operation.type === "edit" && data && !result.deleted) {
4653
4367
  const { quotes, unquoted } = await anchorQuotes(result.version > 0 ? store.read(result.reviewId, result.version - 1) : void 0, store.read(result.reviewId, result.version), async (pins, side, file) => (await data.file(pins, side, file)).text);
@@ -4668,6 +4382,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
4668
4382
  if (input.operation.type !== "create") return context.json(result);
4669
4383
  if (result.created !== false) hooks.onReviewCreated?.({
4670
4384
  reviewId: result.reviewId,
4385
+ kind: input.operation.kind === "scratchpad" ? "scratchpad" : "review",
4671
4386
  blocks: store.read(result.reviewId).document.length,
4672
4387
  ...reviewRequestOrigin(context.req.raw.headers)
4673
4388
  });
@@ -4731,95 +4446,6 @@ async function locateRepositories(body, register) {
4731
4446
  }
4732
4447
  };
4733
4448
  }
4734
- /** Turns an agent's pullRequest target into what the store takes: a create
4735
- * from pullRequestUrl, or for set_target the PR's resolved commits. */
4736
- async function pullRequestTargets(body, register, resolve, board) {
4737
- const target = z.strictObject({
4738
- kind: z.literal("pullRequest"),
4739
- repositoryPath: z.string().min(1),
4740
- pullRequest: z.union([z.string().min(1), z.number().int().positive()])
4741
- });
4742
- const located = z.looseObject({ operation: z.discriminatedUnion("type", [z.looseObject({
4743
- type: z.literal("create"),
4744
- target
4745
- }), z.strictObject({
4746
- type: z.literal("set_target"),
4747
- reviewId: z.string().min(1),
4748
- target
4749
- })]) }).safeParse(body);
4750
- if (!located.success) return body;
4751
- const { target: pullRequest, ...operation } = located.data.operation;
4752
- if (operation.type === "set_target") board(operation.reviewId);
4753
- const { id: repositoryId } = await register(pullRequest.repositoryPath);
4754
- const url = pullRequestAt(pullRequest.repositoryPath, pullRequest.pullRequest);
4755
- if (operation.type === "create") return {
4756
- ...located.data,
4757
- operation: {
4758
- ...operation,
4759
- pullRequestUrl: url,
4760
- repositoryId
4761
- }
4762
- };
4763
- return {
4764
- ...located.data,
4765
- operation: {
4766
- ...operation,
4767
- target: (await resolve(url, repositoryId)).target,
4768
- pullRequestUrl: url
4769
- }
4770
- };
4771
- }
4772
- /** A PR's URL: as given, or its number on the checkout's GitHub repository. */
4773
- function pullRequestAt(repositoryPath, pullRequest) {
4774
- if (isStringValue(pullRequest) && !/^\d+$/.test(pullRequest)) return pullRequest;
4775
- const slug = resolveRepoContextSync(repositoryPath)?.githubSlug;
4776
- if (!slug) throw new ReviewInputError("This checkout has no GitHub remote to find the PR number in; pass the PR's URL.");
4777
- return `https://github.com/${slug}/pull/${pullRequest}`;
4778
- }
4779
- /** A pins object as an edit spells it: commits, with no other fields. A
4780
- * diagram may key its own data `pins`, and that is left alone. */
4781
- /** Pins as an agent writes them: the board's repository is the host's to
4782
- * add. */
4783
- const agentPinsSchema = z.strictObject({
4784
- head: z.string().min(1),
4785
- base: z.string().min(1).optional()
4786
- });
4787
- /** An object an agent meant as pins: one naming a head, base or repository.
4788
- * Content may have a field called `pins` that is nothing of the kind. */
4789
- const meantAsPins = (value) => isJsonObject(value) && [
4790
- "head",
4791
- "base",
4792
- "repositoryId"
4793
- ].some((key) => key in value);
4794
- /** Sets every pins object in an edit to the board's own repository. */
4795
- function withBoardRepository(body, board) {
4796
- const located = z.looseObject({ operation: z.looseObject({
4797
- type: z.enum(["edit", "lens_edit"]),
4798
- reviewId: z.string(),
4799
- edit: z.json()
4800
- }) }).safeParse(body);
4801
- if (!located.success) return body;
4802
- const snapshot = board(located.data.operation.reviewId);
4803
- const repositoryId = snapshot ? boardRepositoryId(snapshot) : void 0;
4804
- const edit = JSON.parse(JSON.stringify(located.data.operation.edit), (key, value) => {
4805
- if (key !== "pins" || !meantAsPins(value)) return value;
4806
- const { repositoryId: _ignored, ...written } = value;
4807
- const pins = agentPinsSchema.safeParse(written);
4808
- if (!pins.success) throw new ReviewInputError(`Write pins as {head, base?}, each a commit: ${pins.error.issues.map((issue) => `pins.${issue.path.join(".")}: ${issue.message}`).join("; ")}.`);
4809
- if (!repositoryId) throw new ReviewInputError("This whiteboard has no repository for its pins to name commits of.");
4810
- return {
4811
- ...pins.data,
4812
- repositoryId
4813
- };
4814
- });
4815
- return {
4816
- ...located.data,
4817
- operation: {
4818
- ...located.data.operation,
4819
- edit
4820
- }
4821
- };
4822
- }
4823
4449
  function watch(lines, subscribe) {
4824
4450
  let stop = () => {};
4825
4451
  const encoder = new TextEncoder();
@@ -4988,7 +4614,7 @@ function createWhiteboardCore(input) {
4988
4614
  if (result.deleted || result.lifecycle) closeDeletedThreads();
4989
4615
  });
4990
4616
  const stopShared = askThreads && shared?.subscribe(closeDeletedThreads);
4991
- const api = createReviewApi(store, data, callbacks.open, shared, callbacks.capabilities, () => traceMachineEnabled(), input.status, input.hooks, askThreads && {
4617
+ const api = createReviewApi(store, data, callbacks.open, shared, callbacks.capabilities, input.scratchpad, () => traceMachineEnabled(), input.status, input.hooks, askThreads && {
4992
4618
  threads: askThreads,
4993
4619
  agents: async () => detectAskAgents(await ask?.env)
4994
4620
  });
@@ -5277,6 +4903,7 @@ async function serve(input) {
5277
4903
  token: discovery.token,
5278
4904
  instanceId: discovery.instanceId,
5279
4905
  softwareMapEnabled: input.softwareMapEnabled,
4906
+ scratchpad: () => false,
5280
4907
  status: () => ({
5281
4908
  key: "headless",
5282
4909
  home: input.stateDir