@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
@@ -9,6 +9,7 @@ import { COMPONENT_REFERENCE } from "./component-reference.js";
9
9
  export const INSTRUCTION_TOPICS = [
10
10
  "authoring",
11
11
  "file-lenses",
12
+ "scratchpad",
12
13
  "trace-archaeology",
13
14
  ] as const;
14
15
 
@@ -19,9 +20,15 @@ export const instructionsQuerySchema = z.strictObject({
19
20
  });
20
21
 
21
22
  export interface InstructionContext {
23
+ desktopAvailable: boolean;
24
+ scratchpadEnabled: boolean;
22
25
  traceEnabled: boolean;
23
26
  }
24
27
 
28
+ export function scratchpadAvailable(context: InstructionContext): boolean {
29
+ return context.desktopAvailable && context.scratchpadEnabled;
30
+ }
31
+
25
32
  const cache = new Map<string, Promise<string>>();
26
33
 
27
34
  function read(root: string, name: string): Promise<string> {
@@ -44,12 +51,24 @@ export async function renderInstructions(
44
51
  context: InstructionContext,
45
52
  root = findReviewPackageRoot(import.meta.url),
46
53
  ): Promise<string> {
54
+ if (topic === "scratchpad" && !scratchpadAvailable(context)) {
55
+ return "The Whiteboard scratchpad is turned off or Whiteboard Desktop is not running. Answer in chat; the scratchpad can be turned on in Whiteboard Desktop Settings.";
56
+ }
57
+
47
58
  if (topic === "trace-archaeology" && !context.traceEnabled)
48
59
  return "Trace capture is off on this machine, so no agent traces are available. It can be turned on in Whiteboard Desktop Settings under Experimental Features.";
49
60
 
61
+ if (topic === "scratchpad")
62
+ return `${await read(root, topic)}\n\n${COMPONENT_REFERENCE}`;
63
+
50
64
  if (topic !== "authoring") return read(root, topic);
51
65
 
52
66
  const more = [
67
+ ...(scratchpadAvailable(context)
68
+ ? [
69
+ '- Explaining code visually outside a review: `session_get_instructions({topic:"scratchpad"})`',
70
+ ]
71
+ : []),
53
72
  ...(context.traceEnabled
54
73
  ? [
55
74
  '- Why code exists / past agent sessions: `session_get_instructions({topic:"trace-archaeology"})`',
@@ -1,6 +1,8 @@
1
+ import { execFile } from "node:child_process";
1
2
  import { existsSync, statSync } from "node:fs";
2
3
  import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
3
4
  import path from "node:path";
5
+ import { promisify } from "node:util";
4
6
 
5
7
  import {
6
8
  type BlobBatchReader,
@@ -9,7 +11,6 @@ import {
9
11
  type LocalVcsDiffFileSummary,
10
12
  type LocalVcsKind,
11
13
  LocalVcsToolsMissingError,
12
- type VcsArg,
13
14
  createBlobBatchReader,
14
15
  detectLocalVcs,
15
16
  diffFileSummariesTrees,
@@ -21,11 +22,9 @@ import {
21
22
  listTrackedFilesAtCommit,
22
23
  readFileAtCommit,
23
24
  resolveRepoContext,
24
- runVcs,
25
25
  } from "@dev.fast/local-vcs";
26
- import { boardTitle, structuralChangeCounts } from "@dev.fast/review-protocol";
26
+ import { structuralChangeCounts } from "@dev.fast/review-protocol";
27
27
  import type {
28
- PullRequestState,
29
28
  ReviewLanguageEnvironment,
30
29
  ReviewSourceEntry,
31
30
  StructuralDiffEvent,
@@ -69,7 +68,6 @@ import {
69
68
  type ReviewTarget,
70
69
  type SourcePins,
71
70
  anchorPins,
72
- boardRepositoryId,
73
71
  elements,
74
72
  fileLineRangeSchema,
75
73
  pinnedRepositoryIds,
@@ -84,7 +82,6 @@ import {
84
82
  resolvePullRequest,
85
83
  } from "./provider.js";
86
84
  import {
87
- GitHubRefusedRead,
88
85
  type PullRequestDeps,
89
86
  defaultPullRequestDeps,
90
87
  fetchPullRequest,
@@ -168,14 +165,7 @@ interface RepositoryVcs {
168
165
  vcs?: LocalVcs;
169
166
  }
170
167
 
171
- /** How long a PR's state is shown before it is read again. */
172
- const PULL_REQUEST_STATE_TTL_MS = 5 * 60_000;
173
-
174
- /** How many PR states are read from GitHub at once. */
175
- const PULL_REQUEST_STATE_READS = 2;
176
-
177
168
  /** Local source/resource boundary, including Desktop-only local language context. */
178
-
179
169
  export class LocalReviewData {
180
170
  private readonly workspaceManager?: ReviewWorkspaces;
181
171
 
@@ -311,12 +301,7 @@ export class LocalReviewData {
311
301
  if (commit && !anchor)
312
302
  await this.comparison(await this.documentPins(snapshot), commit);
313
303
 
314
- const repositoryId =
315
- anchor?.repositoryId ??
316
- snapshot.pins?.repositoryId ??
317
- (snapshot.target?.kind === "project"
318
- ? snapshot.target.repositoryId
319
- : undefined);
304
+ const repositoryId = anchor?.repositoryId ?? snapshot.pins?.repositoryId;
320
305
 
321
306
  if (!repositoryId)
322
307
  throw new ReviewInputError(
@@ -434,7 +419,7 @@ export class LocalReviewData {
434
419
  );
435
420
 
436
421
  if (!live) {
437
- const { stdout } = await runVcs("git", [
422
+ const { stdout } = await promisify(execFile)("git", [
438
423
  "-C",
439
424
  rootPath,
440
425
  "status",
@@ -487,7 +472,7 @@ export class LocalReviewData {
487
472
  ),
488
473
  ));
489
474
 
490
- const title = `${boardTitle(snapshot.title)} — ${live ? "Live source" : side === "base" ? "Base source" : "Source"} — Whiteboard`;
475
+ const title = `${snapshot.title} — ${live ? "Live source" : side === "base" ? "Base source" : "Source"} — Whiteboard`;
491
476
 
492
477
  const workspace = previous ?? {
493
478
  folders: [],
@@ -534,14 +519,9 @@ export class LocalReviewData {
534
519
  return { workspacePath, filePath, rootPath };
535
520
  }
536
521
 
537
- /** A document read that needs default pins: its own, or for a plan its
538
- * repository's live checkout with base at head; 409 when it has neither. */
522
+ /** A document read that needs default pins; 409 when the document has none. */
539
523
  async documentPins(snapshot: Snapshot): Promise<Pins> {
540
- const pins =
541
- (await this.sourcePins(snapshot)) ??
542
- (snapshot.target?.kind === "project"
543
- ? await this.livePins(snapshot.target.repositoryId)
544
- : undefined);
524
+ const pins = await this.sourcePins(snapshot);
545
525
 
546
526
  if (!pins)
547
527
  throw new ReviewInputError(
@@ -552,21 +532,6 @@ export class LocalReviewData {
552
532
  return pins;
553
533
  }
554
534
 
555
- /** A checkout's current state, base at head: what a plan reads. */
556
- private async livePins(repositoryId: string): Promise<Pins> {
557
- const vcs = await this.vcs(repositoryId);
558
-
559
- if (!vcs) throw unavailableCheckout();
560
- const { revision, commit } = await this.worktreeState(repositoryId, vcs);
561
-
562
- return {
563
- repositoryId,
564
- base: commit,
565
- head: commit,
566
- worktreeRevision: revision,
567
- };
568
- }
569
-
570
535
  /** Pins a reference names itself, checked against the registered checkout. */
571
536
  async anchorSourcePins(anchor: SourcePins): Promise<Pins> {
572
537
  if (!existsSync(this.store.repositoryPath(anchor.repositoryId)))
@@ -580,14 +545,8 @@ export class LocalReviewData {
580
545
  async unavailableAnchors(snapshot: Snapshot): Promise<string[]> {
581
546
  const missing = new Set<string>();
582
547
 
583
- // Only Markdown needs parsing, so check the repositories first. Its
584
- // commit links read from the board's repository.
585
- const repositoryIds = pinnedRepositoryIds(snapshot.document);
586
- const boardRepository = boardRepositoryId(snapshot);
587
-
588
- if (boardRepository) repositoryIds.add(boardRepository);
589
-
590
- for (const repositoryId of repositoryIds)
548
+ // Only Markdown needs parsing, so check the repositories first.
549
+ for (const repositoryId of pinnedRepositoryIds(snapshot.document))
591
550
  try {
592
551
  if (!existsSync(this.store.repositoryPath(repositoryId)))
593
552
  missing.add(repositoryId);
@@ -608,10 +567,7 @@ export class LocalReviewData {
608
567
 
609
568
  private readonly storedReferences = new VersionCache(
610
569
  (snapshot: Snapshot) =>
611
- sourceReferences(snapshot.document, {
612
- tolerant: true,
613
- repositoryId: boardRepositoryId(snapshot),
614
- }),
570
+ sourceReferences(snapshot.document, { tolerant: true }),
615
571
  { capacity: 512 },
616
572
  );
617
573
 
@@ -636,8 +592,6 @@ export class LocalReviewData {
636
592
  subscribe?: typeof subscribe;
637
593
  /** gh, git and GitHub API access for pull request targets. */
638
594
  pullRequests?: PullRequestDeps;
639
- /** Read PR states from GitHub for the catalog. */
640
- pullRequestStates?: boolean;
641
595
  } = {},
642
596
  ) {
643
597
  if (options.manageWorkspaces !== false)
@@ -1019,7 +973,6 @@ export class LocalReviewData {
1019
973
  // that no longer exist; the author decides how to update changed source.
1020
974
  for (const reference of sourceReferences(snapshot.document, {
1021
975
  tolerant: true,
1022
- repositoryId: boardRepositoryId(snapshot),
1023
976
  })) {
1024
977
  try {
1025
978
  await this.quote(anchorPins(reference.source, pins), reference.source);
@@ -1037,13 +990,6 @@ export class LocalReviewData {
1037
990
  if (snapshot.target?.kind === "worktree")
1038
991
  return (await this.resolveTarget(snapshot.target, snapshot.pins)).pins;
1039
992
 
1040
- // A plan has no pins of its own, but its checkout must exist.
1041
- if (
1042
- snapshot.target?.kind === "project" &&
1043
- !existsSync(this.store.repositoryPath(snapshot.target.repositoryId))
1044
- )
1045
- throw unavailableCheckout();
1046
-
1047
993
  if (!snapshot.pins) return undefined;
1048
994
 
1049
995
  if (!existsSync(this.store.repositoryPath(snapshot.pins.repositoryId)))
@@ -1058,15 +1004,18 @@ export class LocalReviewData {
1058
1004
  if (!vcs || vcs.kind !== "git") return undefined;
1059
1005
 
1060
1006
  try {
1061
- const { stdout } = await runVcs("git", [
1007
+ const run = promisify(execFile);
1008
+
1009
+ const { stdout } = await run("git", [
1062
1010
  "-C",
1063
1011
  vcs.rootPath,
1064
1012
  "rev-parse",
1065
1013
  "--symbolic-full-name",
1066
1014
  "--verify",
1015
+ "--end-of-options",
1067
1016
  !headRef || /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(headRef)
1068
1017
  ? "HEAD"
1069
- : { rev: headRef },
1018
+ : headRef,
1070
1019
  ]);
1071
1020
 
1072
1021
  const ref = stdout.trim();
@@ -1091,9 +1040,6 @@ export class LocalReviewData {
1091
1040
  target: ReviewTarget,
1092
1041
  pinned?: Pins,
1093
1042
  ): Promise<{ target: ReviewTarget; pins: Pins }> {
1094
- if (target.kind === "project")
1095
- throw new ReviewInputError("A project target has no commits.");
1096
-
1097
1043
  const vcs = await this.vcs(target.repositoryId);
1098
1044
 
1099
1045
  if (!vcs) throw unavailableCheckout();
@@ -1205,147 +1151,10 @@ export class LocalReviewData {
1205
1151
 
1206
1152
  return { ref: branch, commit: fork.commit };
1207
1153
  }
1208
- /** Each PR's state, by URL, as last read. */
1209
- private readonly pullRequestStates = new Map<
1210
- string,
1211
- {
1212
- state?: PullRequestState;
1213
- /** The branch it merges into, and its own. */
1214
- baseBranch?: string;
1215
- headBranch?: string;
1216
- readAt: number;
1217
- /** When a board last showed it; a deleted board's PR is forgotten. */
1218
- seenAt: number;
1219
- reading: boolean;
1220
- }
1221
- >();
1222
-
1223
- private readonly pullRequestStateListeners = new Set<() => void>();
1224
- private pullRequestStateTimer: ReturnType<typeof setInterval> | undefined;
1225
- private readonly pullRequestStateQueue: string[] = [];
1226
- private pullRequestStateReads = 0;
1227
-
1228
- /** While GitHub rate-limits, no state is read. */
1229
- private pullRequestStatesPausedUntil = 0;
1230
-
1231
- /** Calls back when a PR's state changes. While anyone listens, every PR
1232
- * still shown is read again once its state is a few minutes old. */
1233
- subscribePullRequestStates(listener: () => void): () => void {
1234
- this.pullRequestStateListeners.add(listener);
1235
- this.pullRequestStateTimer ??= setInterval(() => {
1236
- for (const [url, entry] of this.pullRequestStates)
1237
- if (Date.now() - entry.seenAt > 2 * PULL_REQUEST_STATE_TTL_MS)
1238
- this.pullRequestStates.delete(url);
1239
- else this.pullRequestState(url, { seen: false });
1240
- this.readPullRequestStates();
1241
- }, PULL_REQUEST_STATE_TTL_MS / 5);
1242
- this.pullRequestStateTimer.unref?.();
1243
-
1244
- return () => {
1245
- this.pullRequestStateListeners.delete(listener);
1246
-
1247
- if (!this.pullRequestStateListeners.size) {
1248
- clearInterval(this.pullRequestStateTimer);
1249
- this.pullRequestStateTimer = undefined;
1250
- }
1251
- };
1252
- }
1253
-
1254
- /** A PR's state as last read from GitHub; a read older than a few minutes
1255
- * queues another in the background. Undefined until the first read lands,
1256
- * and a failed read keeps the last state. A merged or closed PR is not
1257
- * read again. */
1258
- pullRequestState(
1259
- url: string | undefined,
1260
- { seen = true }: { seen?: boolean } = {},
1261
- ): PullRequestState | undefined {
1262
- if (!url || !this.options.pullRequestStates) return undefined;
1263
-
1264
- const entry = this.pullRequestStates.get(url) ?? {
1265
- readAt: 0,
1266
- seenAt: 0,
1267
- reading: false,
1268
- };
1269
-
1270
- this.pullRequestStates.set(url, entry);
1271
-
1272
- if (seen) entry.seenAt = Date.now();
1273
-
1274
- if (
1275
- !entry.reading &&
1276
- entry.state !== "merged" &&
1277
- entry.state !== "closed" &&
1278
- Date.now() - entry.readAt > PULL_REQUEST_STATE_TTL_MS
1279
- ) {
1280
- entry.reading = true;
1281
- this.pullRequestStateQueue.push(url);
1282
- this.readPullRequestStates();
1283
- }
1284
-
1285
- return entry.state;
1286
- }
1287
-
1288
- /** Reads queued PR states a few at a time. */
1289
- private readPullRequestStates() {
1290
- while (
1291
- this.pullRequestStateReads < PULL_REQUEST_STATE_READS &&
1292
- this.pullRequestStateQueue.length &&
1293
- Date.now() >= this.pullRequestStatesPausedUntil
1294
- ) {
1295
- const url = this.pullRequestStateQueue.shift()!;
1296
- const entry = this.pullRequestStates.get(url);
1297
-
1298
- if (!entry) continue;
1299
- this.pullRequestStateReads++;
1300
- void Promise.resolve()
1301
- .then(() => {
1302
- const { provider, address } = resolvePullRequest(url);
1303
-
1304
- return provider.readPullRequest(
1305
- address,
1306
- this.options.pullRequests ?? defaultPullRequestDeps,
1307
- );
1308
- })
1309
- .then((pullRequest) => {
1310
- if (
1311
- (!pullRequest.state || pullRequest.state === entry.state) &&
1312
- pullRequest.baseRefName === entry.baseBranch &&
1313
- pullRequest.headBranch === entry.headBranch
1314
- )
1315
- return;
1316
- entry.state = pullRequest.state ?? entry.state;
1317
- entry.baseBranch = pullRequest.baseRefName;
1318
- entry.headBranch = pullRequest.headBranch;
1319
-
1320
- for (const listener of this.pullRequestStateListeners) listener();
1321
- })
1322
- .catch((error) => {
1323
- if (error instanceof GitHubRefusedRead)
1324
- this.pullRequestStatesPausedUntil = error.retryAt;
1325
- })
1326
- .finally(() => {
1327
- entry.readAt = Date.now();
1328
- entry.reading = false;
1329
- this.pullRequestStateReads--;
1330
- this.readPullRequestStates();
1331
- });
1332
- }
1333
- }
1334
-
1335
- /** The branch a PR merges into, as last read with its state. */
1336
- pullRequestBaseBranch(url: string): string | undefined {
1337
- return this.pullRequestStates.get(url)?.baseBranch;
1338
- }
1339
-
1340
- /** A PR's own branch, as last read with its state. */
1341
- pullRequestHeadBranch(url: string): string | undefined {
1342
- return this.pullRequestStates.get(url)?.headBranch;
1343
- }
1344
-
1345
1154
  /** The PR's current comparison, fetched into a registered checkout of its repository. */
1346
1155
  async resolvePullRequest(
1347
1156
  url: string,
1348
- repository: { id: string },
1157
+ repository: { id?: string; preferred?: string },
1349
1158
  ): Promise<ResolvedPullRequest> {
1350
1159
  const deps = this.options.pullRequests ?? defaultPullRequestDeps;
1351
1160
  const { provider, address } = resolvePullRequest(url);
@@ -1393,14 +1202,19 @@ export class LocalReviewData {
1393
1202
  private async pullRequestCheckout(
1394
1203
  provider: PullRequestProvider,
1395
1204
  address: PullRequestAddress,
1396
- repository: { id: string },
1205
+ repository: { id?: string; preferred?: string },
1397
1206
  deps: PullRequestDeps,
1398
1207
  ) {
1399
- const candidates = this.store
1400
- .repositories()
1401
- .filter((entry) => entry.id === repository.id);
1208
+ const registered = this.store.repositories();
1209
+
1210
+ const candidates = repository.id
1211
+ ? registered.filter((entry) => entry.id === repository.id)
1212
+ : [
1213
+ ...registered.filter((entry) => entry.id === repository.preferred),
1214
+ ...registered.filter((entry) => entry.id !== repository.preferred),
1215
+ ];
1402
1216
 
1403
- if (candidates.length === 0)
1217
+ if (repository.id && candidates.length === 0)
1404
1218
  throw new ReviewInputError("Repository is not registered.", 404);
1405
1219
 
1406
1220
  for (const { id } of candidates) {
@@ -1428,7 +1242,9 @@ export class LocalReviewData {
1428
1242
  const name = host === "github.com" ? slug : `${host}/${slug}`;
1429
1243
 
1430
1244
  throw new ReviewInputError(
1431
- `That checkout has no remote for ${name}. Name a checkout of ${name} as the target's repositoryPath.`,
1245
+ repository.id
1246
+ ? `That checkout has no remote for ${name}. Add one, or omit repositoryId.`
1247
+ : `No registered checkout has a remote for ${name}. Register a checkout of ${name} with review_register_repository first, or pass a target.`,
1432
1248
  404,
1433
1249
  );
1434
1250
  }
@@ -1485,74 +1301,6 @@ export class LocalReviewData {
1485
1301
 
1486
1302
  return { base, head };
1487
1303
  }
1488
- /** What a commit is, for a quote read at it: its subject, author and
1489
- * date, and how far behind the default branch's tip it is when both are
1490
- * known. */
1491
- async commitInfo(
1492
- repositoryId: string,
1493
- commit: string,
1494
- ): Promise<{
1495
- commit: string;
1496
- subject: string;
1497
- author: string;
1498
- authoredAt: string;
1499
- behind?: { count: number; branch: string };
1500
- }> {
1501
- const vcs = await this.vcs(repositoryId);
1502
-
1503
- if (!vcs) throw unavailableCheckout();
1504
-
1505
- const git = async (...args: VcsArg[]) =>
1506
- (await runVcs("git", ["-C", vcs.rootPath, ...args])).stdout.trim();
1507
-
1508
- let line: string;
1509
-
1510
- try {
1511
- line = await git("log", "-1", "--format=%H%x00%s%x00%an%x00%aI", {
1512
- rev: commit,
1513
- });
1514
- } catch {
1515
- throw new ReviewInputError("That commit is not in the repository.", 404);
1516
- }
1517
-
1518
- const [full = commit, subject = "", author = "", authoredAt = ""] =
1519
- line.split("\0");
1520
-
1521
- const branch = await vcs.defaultBranch().catch(() => null);
1522
-
1523
- const behind = branch
1524
- ? await git("rev-list", "--count", { rev: `${full}..${branch.commit}` })
1525
- .then((count) => ({
1526
- count: Number(count),
1527
- branch: branch.ref.replace(/^origin\//, ""),
1528
- }))
1529
- .catch(() => undefined)
1530
- : undefined;
1531
-
1532
- return {
1533
- commit: full,
1534
- subject,
1535
- author,
1536
- authoredAt,
1537
- ...(behind && { behind }),
1538
- };
1539
- }
1540
- /** The full commit a commit ID or its prefix names in the repository,
1541
- * or undefined when it names none. */
1542
- async resolveCommit(
1543
- repositoryId: string,
1544
- commit: string,
1545
- ): Promise<string | undefined> {
1546
- const vcs = await this.vcs(repositoryId);
1547
-
1548
- if (!vcs) throw unavailableCheckout();
1549
- const resolved = await vcs.resolveRevision(commit);
1550
-
1551
- // A branch or tag spelled in hex is not the commit it seems.
1552
- return resolved?.commit.startsWith(commit.toLowerCase())
1553
- ? resolved.commit
1554
- : undefined;
1555
- }
1556
1304
  async validatePins(pins: Pins) {
1557
1305
  if (pins.worktreeRevision) {
1558
1306
  if (!(await this.vcs(pins.repositoryId))) throw unavailableCheckout();
@@ -2178,41 +1926,29 @@ export function openLocalReviewStore(
2178
1926
  manageWorkspaces?: boolean;
2179
1927
  subscribe?: typeof subscribe;
2180
1928
  pullRequests?: PullRequestDeps;
2181
- pullRequestStates?: boolean;
2182
- /** From `groupCheckoutsAhead`. */
2183
- checkoutOwners?: ReadonlyMap<string, string>;
2184
1929
  } = {},
2185
1930
  ) {
2186
- const { checkoutOwners, ...dataOptions } = options;
2187
-
2188
- const store: ReviewStore = new ReviewStore(
2189
- databasePath,
2190
- {
2191
- projectSource: (snapshot, pins) => data.projectSource(snapshot, pins),
2192
- resolveTarget: (target, pinned) => data.resolveTarget(target, pinned),
2193
- resolvePullRequest: (url, repository) =>
2194
- data.resolvePullRequest(url, repository),
2195
- pullRequestTitle: (url) => data.pullRequestTitle(url),
2196
- headBranch: (pins, headRef) => data.headBranch(pins, headRef),
2197
- sourcePins: (snapshot) => data.sourcePins(snapshot),
2198
- unavailableAnchors: (snapshot) => data.unavailableAnchors(snapshot),
2199
- validatePins: (pins) => data.validatePins(pins),
2200
- resolveCommit: (repositoryId, commit) =>
2201
- data.resolveCommit(repositoryId, commit),
2202
- filesChangedBetween: (from, to) => data.filesChangedBetween(from, to),
2203
- validateSource: (pins, source, options) =>
2204
- data.validateSource(pins, source, options),
2205
- validateResource: (pins, block) => data.validateResource(pins, block),
2206
- pullRequestBaseBranch: (url) => data.pullRequestBaseBranch(url),
2207
- },
2208
- checkoutOwners,
2209
- );
1931
+ const store: ReviewStore = new ReviewStore(databasePath, {
1932
+ projectSource: (snapshot, pins) => data.projectSource(snapshot, pins),
1933
+ resolveTarget: (target, pinned) => data.resolveTarget(target, pinned),
1934
+ resolvePullRequest: (url, repository) =>
1935
+ data.resolvePullRequest(url, repository),
1936
+ pullRequestTitle: (url) => data.pullRequestTitle(url),
1937
+ headBranch: (pins, headRef) => data.headBranch(pins, headRef),
1938
+ sourcePins: (snapshot) => data.sourcePins(snapshot),
1939
+ unavailableAnchors: (snapshot) => data.unavailableAnchors(snapshot),
1940
+ validatePins: (pins) => data.validatePins(pins),
1941
+ filesChangedBetween: (from, to) => data.filesChangedBetween(from, to),
1942
+ validateSource: (pins, source, options) =>
1943
+ data.validateSource(pins, source, options),
1944
+ validateResource: (pins, block) => data.validateResource(pins, block),
1945
+ });
2210
1946
 
2211
1947
  let data: LocalReviewData;
2212
1948
 
2213
1949
  try {
2214
1950
  data = new LocalReviewData(store, {
2215
- ...dataOptions,
1951
+ ...options,
2216
1952
  workspaceDatabase: `${databasePath}.workspaces`,
2217
1953
  });
2218
1954
  } catch (error) {
@@ -29,9 +29,17 @@ import { type ToolFailure, toolFailure } from "./tool-failure.js";
29
29
  const RELOAD_TOOLS =
30
30
  "Whiteboard is running now, but this session listed Whiteboard's tools before it started, so you may see only session_get_instructions. Before authoring, reload the `whiteboard` MCP server's tools (reconnect it in your agent, or start a new agent session). If you can already see tools such as session_create, carry on.";
31
31
 
32
- function mcpAuthoringGuidance(context: { traceEnabled: boolean }): string {
32
+ function mcpAuthoringGuidance(context: {
33
+ scratchpadAvailable: boolean;
34
+ traceEnabled: boolean;
35
+ }): string {
33
36
  return [
34
37
  "Whiteboard explains code in documents the user reads in Whiteboard Desktop. Call session_get_instructions before creating or editing a Whiteboard and follow it. Read session_capabilities before authoring; session_create opens the new review in Desktop when it is available, so call session_open only for an existing review, and generate software maps only when softwareMapEnabled is true. When the user asks for a Whiteboard or to use Whiteboard (for example to review a branch, a change or a pull request, or to explain a system in Whiteboard), author a Whiteboard with the default topic.",
38
+ ...(context.scratchpadAvailable
39
+ ? [
40
+ 'When the user asks in conversation to be shown how code works or wants a diagram, without asking for a Whiteboard, call session_capabilities; if it reports scratchpadEnabled and desktopAvailable, draw on the Whiteboard scratchpad rather than answering only in chat, starting with session_get_instructions({topic:"scratchpad"}).',
41
+ ]
42
+ : []),
35
43
  ...(context.traceEnabled
36
44
  ? [
37
45
  'For why code exists, what an agent was thinking, or whether an agent solved something before, call session_get_instructions({topic:"trace-archaeology"}).',
@@ -58,7 +66,7 @@ export async function serveReviewMcp(
58
66
  environmentAgent?: ReviewSessionAgent,
59
67
  ) {
60
68
  const instructionsTool = {
61
- ...authoringTools(traceEnabled).find(
69
+ ...authoringTools(false, traceEnabled).find(
62
70
  (tool) => tool.name === "review_get_instructions",
63
71
  )!,
64
72
  name: "session_get_instructions",
@@ -127,7 +135,7 @@ export async function serveReviewMcp(
127
135
  // guidance on the discovery tool instead, including before Desktop starts.
128
136
  description:
129
137
  name === instructionsTool.name
130
- ? `${mcpAuthoringGuidance({ traceEnabled })}\n\n${description}`
138
+ ? `${mcpAuthoringGuidance({ scratchpadAvailable: true, traceEnabled })}\n\n${description}`
131
139
  : description,
132
140
  inputSchema,
133
141
  }),
@@ -462,7 +462,7 @@ export function registerOnboardingRoutes(
462
462
  agent: input.agent,
463
463
  ...host.historyHooks({
464
464
  id,
465
- anchor: { board: reviewId },
465
+ reviewId,
466
466
  agent: input.agent,
467
467
  version: created.version,
468
468
  head,