@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,4 +1,4 @@
1
- import { M as ReviewSelectedDiffSchema, T as ReviewApiSelectionSourceSchema } from "./src-BiWNbcF_.js";
1
+ import { A as ReviewSelectedDiffSchema, C as ReviewApiSelectionSourceSchema } from "./src-DsM3jO8G.js";
2
2
  import { z } from "zod";
3
3
  /** A semantic selection, independent of comment/thread creation. */
4
4
  const AgentSelectionSchema = z.strictObject({
@@ -374,12 +374,7 @@ const askHistoryEntrySchema = z.object({
374
374
  /** The commit the agent read. */
375
375
  head: z.string(),
376
376
  createdAt: z.string(),
377
- updatedAt: z.string(),
378
- /** The board the conversation started on, and the block if it was about one. */
379
- anchor: z.object({
380
- board: z.string(),
381
- block: z.string().optional()
382
- }).optional()
377
+ updatedAt: z.string()
383
378
  });
384
379
  //#endregion
385
380
  export { askHistoryEntrySchema as a, askPicksSchema as c, selectionMarkdown as d, askEntrySchema as i, askQuestionSchema as l, askAgentIds as n, askOfferSchema as o, askChoiceKinds as r, askPermissionsSchema as s, applyAskChange as t, AgentSelectionSchema as u };
@@ -1,6 +1,5 @@
1
1
  import { m as isStringValue, n as isJsonObject } from "./src-X9phtB2j.js";
2
- import { l as boardTitle } from "./src-BiWNbcF_.js";
3
- import { n as callAuthoringTool, s as ReviewApiError, t as ToolText } from "./agent-client-Dtc-Kn_w.js";
2
+ import { n as callAuthoringTool, s as ReviewApiError, t as ToolText } from "./agent-client-BOOi7EUR.js";
4
3
  import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
5
4
  //#region src/review-api/public-tools.ts
6
5
  function publicDescription(text) {
@@ -39,7 +38,7 @@ function publicResult(value) {
39
38
  if (Array.isArray(value)) return value.map(publicResult);
40
39
  if (!isJsonObject(value)) return value;
41
40
  return Object.fromEntries(Object.entries(value).map(([key, item]) => {
42
- return [key === "reviewId" ? "sessionId" : key === "review" ? "session" : key === "reviews" ? "sessions" : key, key === "review" || key === "reviews" ? publicResult(item) : key === "title" && "reviewId" in value && isStringValue(item) ? boardTitle(item) : item];
41
+ return [key === "reviewId" ? "sessionId" : key === "review" ? "session" : key === "reviews" ? "sessions" : key, key === "review" || key === "reviews" ? publicResult(item) : item];
43
42
  }));
44
43
  }
45
44
  async function callPublicTool(client, tool, input, signal) {
@@ -48,13 +47,7 @@ async function callPublicTool(client, tool, input, signal) {
48
47
  const fields = { ...rest };
49
48
  if (sessionId !== void 0) fields.reviewId = sessionId;
50
49
  const result = await callAuthoringTool(client, tool, fields, signal);
51
- return result instanceof ToolText ? result : withoutRepositoryIds(publicResult(result));
52
- }
53
- /** Agents never see repository ids. */
54
- function withoutRepositoryIds(value) {
55
- if (Array.isArray(value)) return value.map(withoutRepositoryIds);
56
- if (!isJsonObject(value)) return value;
57
- return Object.fromEntries(Object.entries(value).filter(([key]) => key !== "repositoryId").map(([key, item]) => [key, withoutRepositoryIds(item)]));
50
+ return result instanceof ToolText ? result : publicResult(result);
58
51
  }
59
52
  //#endregion
60
53
  //#region src/review-api/recovery.ts
@@ -3,7 +3,7 @@ you are writing an interactive rfc-style whiteboard, for consumption by a staff
3
3
  **flow**
4
4
  follow these first five steps in order, with no tool calls beyond what they need: loading the tools they use (`session_create`, `session_activity_begin`, `session_edit`, `session_get`, and later `session_activity_update` and `session_activity_end`) and, for a PR, reading its description (`gh pr view`).
5
5
 
6
- - create the whiteboard with a `target` naming the checkout by its `repositoryPath`: `{kind:"commits", head, base?}` for commits, `{kind:"pullRequest", pullRequest}` for a PR by URL or number, or `{kind:"project"}` for a plan with no change yet. if they name none, use a `worktree` target with no `base`: it reviews the checkout, uncommitted work included, against the default branch. for uncommitted work only, pass `base: "HEAD"`. untracked files are left out: run `git add -N <file>` on new files you want reviewed
6
+ - create the whiteboard, pinned to the commits/pr the user describes, naming the checkout by its `repositoryPath`. if they name none, use a `worktree` target with no `base`: it reviews the checkout, uncommitted work included, against the default branch. for uncommitted work only, pass `base: "HEAD"`. untracked files are left out: run `git add -N <file>` on new files you want reviewed
7
7
  - trigger a subagent with this exact instruction: "Call `session_get_instructions({topic:\"file-lenses\"})` and follow it for whiteboard <sessionId>, whose change is `<base>..<head>` in `<repositoryPath>`."
8
8
  - call `session_activity_begin` and pass its `activityId` on every `session_edit`
9
9
  - read the diff with git (or jj) in `repositoryPath`, at the commits `session_create` returned: `git diff <base> <head>`, or for a worktree target `git diff <base>` plus `git status` for untracked files. if it shows no changes, the target is wrong: fix it with `session_set_target` before writing
@@ -37,5 +37,4 @@ follow these first five steps in order, with no tool calls beyond what they need
37
37
  - use `session_activity_update` to provide regular status updates on your area of focus.
38
38
  - keep whiteboards short and sweet when possible (esp. for small changes.) feel free to omit sections.
39
39
  - when something (a phrase in the prose, diagram node, etc.) describes actual code in the codebase, always default to attaching/hyperlink code.
40
- - Link repository code as `[label](whiteboard:source/head/src/file.ts#L10-L24)`; use `base` for old code. To quote any other commit of the board's repository, as a plan does, put the commit in place of `head`: `[label](whiteboard:source/<commit>/src/file.ts#L10-L24)`. Use repository-relative paths and verified line numbers.
41
- - Link another whiteboard inline as `[label](whiteboard:board/<sessionId>)`, or one of its blocks as `[label](whiteboard:board/<sessionId>#<blockId>)`, wherever the prose refers to it. The board and block must exist.
40
+ - Link repository code as `[label](review-source:head/src/file.ts#L10-L24)`; use `base` for old code. Use repository-relative paths and verified line numbers.
@@ -0,0 +1,23 @@
1
+ # Scratchpad
2
+
3
+ The scratchpad is one document in Whiteboard that is not a session for any change. It has no target, base, head or lifecycle. You draw on it the way you would sketch an explanation on a whiteboard: a short paragraph, a sequence or flow diagram, a call tree, a code peek, and the smallest view that makes the point. The user reads it in the app and copies blocks back to you.
4
+
5
+ Its id is fixed: `scratchpad`. While it is turned on in Whiteboard Settings it always exists; never create, rename, trash, delete or share it.
6
+
7
+ ## When to draw here
8
+
9
+ - The user asks to be shown how something works, wants a picture of a flow, a call path, a data shape or a layout, or is thinking out loud with you before there is a concrete plan.
10
+ - The explanation spans more than one file or more than one repository, or would be an ASCII diagram in chat.
11
+
12
+ Answer in chat when one sentence or one code line does it. Do not use the scratchpad to review a change; author a session instead (`session_get_instructions({})`).
13
+
14
+ ## How
15
+
16
+ 1. `session_capabilities({})`. Draw only when `desktopAvailable` and `scratchpadEnabled` are true. Otherwise answer in chat; if the pad is off, say once that it can be turned on in Whiteboard Settings.
17
+ 2. Every source reference needs pins. For each repository you will quote, `session_register_repository({path})` for its `repositoryId`, and `git rev-parse HEAD` (or the commit the user is looking at) for a commit id. Put `pins: {repositoryId, head}` on each block that quotes code (`code_peek`, `sequence`, `flow_diagram`, `call_stack_diff`, `database_lens` and `markdown` with `review-source:` links); a step, frame, attachment or operation that quotes other commits than its block takes its own `pins`. Add `base` only when an anchor reads the base side. A reference without pins is rejected, since the scratchpad has none to lend.
18
+ 3. Read what you cite at those commits, for example `git show <head>:<path>` in the repository, so line numbers match the pins.
19
+ 4. Call `session_activity_begin` and pass the `activityId` it returns on each `session_edit` as you append blocks. Omitted placement puts the block at the top of the pad, and the response's `targetId` names it. A thought that spans several blocks reads top-down only if you insert it bottom-up (last block first) or give each following block `afterId` of the block you just inserted. Draw a diagram whole in one insert; fix one you drew earlier by patching its nodes, edges or steps by ID, and `replace` a block only when the whole picture was wrong. Each block's fields are in the Components section below; read `session_get_instructions({topic:"authoring"})` for choosing components. End the activity when finished.
20
+ 5. After your first insert in a session, `session_open({sessionId: "scratchpad"})` once so the pad is showing. Do not call it again for later edits; they appear live.
21
+ 6. In chat, say in one line what you drew. Do not repeat the diagram there.
22
+
23
+ Keep it a log, not a document: the newest thought sits at the top, above older ones. Do not build sections, outlines or status markers unless the user asks for structure. When the user asks to clear the pad, `replace` or `remove` its blocks; history keeps what was there.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dev.fast/whiteboard",
3
- "version": "0.3.4-preview.20261019.127",
3
+ "version": "0.3.4",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -74,11 +74,11 @@
74
74
  "tsx": "4.21.0",
75
75
  "typescript": "6.0.3",
76
76
  "vitest": "4.1.11",
77
+ "@dev.fast/json": "0.0.0",
77
78
  "@dev.fast/review-share-protocol": "0.1.2",
78
- "@dev.fast/review-protocol": "0.0.0",
79
79
  "@dev.fast/trace-core": "0.0.0",
80
80
  "@dev.fast/trace-protocol": "0.5.0",
81
- "@dev.fast/json": "0.0.0"
81
+ "@dev.fast/review-protocol": "0.0.0"
82
82
  },
83
83
  "engines": {
84
84
  "node": ">=24 <25"
@@ -88,7 +88,7 @@
88
88
  "repo": "devdotfast/review",
89
89
  "docs": "packages/review/instructions/authoring.md"
90
90
  },
91
- "gitHead": "5dd55db92af13eac9187d16cf07b4bd80c456724",
91
+ "gitHead": "f33bd6620742fe65ccc1d5fef416fbe8254dcf79",
92
92
  "scripts": {
93
93
  "prebuild": "pnpm run build:workspace-deps",
94
94
  "build": "node --max-old-space-size=8192 node_modules/tsdown/dist/run.mjs",
package/src/ask/agents.ts CHANGED
@@ -19,7 +19,6 @@ import {
19
19
  type AskPermissions,
20
20
  askAgentIds,
21
21
  } from "@review/ask/thread-state.js";
22
- import { ASK_THREAD_ENV } from "@review/review-api/request-origin.js";
23
22
 
24
23
  /** How Whiteboard runs an agent that speaks ACP on stdio. */
25
24
  type AskAgentLaunch =
@@ -248,8 +247,6 @@ export type AskAgentLauncher = (
248
247
  /** The MCP servers its sessions are given. */
249
248
  mcpServers?: McpServer[];
250
249
  env?: NodeJS.ProcessEnv;
251
- /** The Ask thread this agent serves. */
252
- threadId?: string;
253
250
  },
254
251
  ) => Promise<AskAgentProcess>;
255
252
 
@@ -360,12 +357,7 @@ const STDERR_LIMIT = 8_000;
360
357
  export const launchAskAgent: AskAgentLauncher = async (
361
358
  agent,
362
359
  cwd,
363
- {
364
- permissions = "read-only",
365
- mcpServers = [],
366
- env: base = process.env,
367
- threadId,
368
- } = {},
360
+ { permissions = "read-only", mcpServers = [], env: base = process.env } = {},
369
361
  ) => {
370
362
  const spec = askAgents[agent];
371
363
  const executable = await findAgent(spec, base);
@@ -377,10 +369,6 @@ export const launchAskAgent: AskAgentLauncher = async (
377
369
  ...askAgentSettings(agent, permissions).env,
378
370
  };
379
371
 
380
- // Lets `whiteboard api` calls from the agent's shell name the thread.
381
- // The MCP server gets it in its own env, since MCP clients may not pass theirs on.
382
- if (threadId) env[ASK_THREAD_ENV] = threadId;
383
-
384
372
  // A server started from inside a Claude Code session must not look nested.
385
373
  delete env.CLAUDECODE;
386
374
  delete env.CLAUDE_CODE_ENTRYPOINT;
@@ -1,10 +1,13 @@
1
- import { runVcs } from "@dev.fast/local-vcs";
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+
4
+ const execFileAsync = promisify(execFile);
2
5
 
3
6
  /** The checkout's files relative to its root: tracked ones, and new ones git
4
7
  * does not ignore. None where git cannot list them. */
5
8
  export async function checkoutFiles(root: string): Promise<string[]> {
6
9
  try {
7
- const { stdout } = await runVcs(
10
+ const { stdout } = await execFileAsync(
8
11
  "git",
9
12
  [
10
13
  "-C",
@@ -375,10 +375,6 @@ export const askHistoryEntrySchema = z.object({
375
375
  head: z.string(),
376
376
  createdAt: z.string(),
377
377
  updatedAt: z.string(),
378
- /** The board the conversation started on, and the block if it was about one. */
379
- anchor: z
380
- .object({ board: z.string(), block: z.string().optional() })
381
- .optional(),
382
378
  });
383
379
 
384
380
  export type AskHistoryEntry = z.infer<typeof askHistoryEntrySchema>;
package/src/ask/thread.ts CHANGED
@@ -176,7 +176,6 @@ export class AskThread {
176
176
  /** The number of changes so far; a snapshot carries the one it includes. */
177
177
  private seq = 0;
178
178
  private readonly listeners = new Set<(update: AskUpdate) => void>();
179
- private readonly statusListeners = new Set<() => void>();
180
179
  private readonly closers = new Set<() => void>();
181
180
  private readonly decisions = new Map<
182
181
  string,
@@ -289,10 +288,6 @@ export class AskThread {
289
288
  return { seq: this.seq, snapshot: this.state };
290
289
  }
291
290
 
292
- get status() {
293
- return this.state.status;
294
- }
295
-
296
291
  /** Every change after the current `seq`, in order. */
297
292
  subscribe(listener: (update: AskUpdate) => void) {
298
293
  this.listeners.add(listener);
@@ -300,13 +295,6 @@ export class AskThread {
300
295
  return () => this.listeners.delete(listener);
301
296
  }
302
297
 
303
- /** Calls back when the status changes, without following the thread. */
304
- onStatusChange(listener: () => void) {
305
- this.statusListeners.add(listener);
306
-
307
- return () => this.statusListeners.delete(listener);
308
- }
309
-
310
298
  /** Whether anything follows the thread, as an open panel does. */
311
299
  watched() {
312
300
  return this.listeners.size > 0;
@@ -642,7 +630,6 @@ export class AskThread {
642
630
  const launched = await this.launch(this.start.agent, this.start.cwd, {
643
631
  permissions: this.state.permissions,
644
632
  mcpServers: this.mcpServers,
645
- threadId: this.id,
646
633
  });
647
634
 
648
635
  // Closed, stopped or given up on while the process started.
@@ -1546,8 +1533,5 @@ export class AskThread {
1546
1533
  const update = { seq: this.seq, change };
1547
1534
 
1548
1535
  for (const listener of this.listeners) listener(update);
1549
-
1550
- if (change.type === "set" && change.status)
1551
- for (const listener of this.statusListeners) listener();
1552
1536
  }
1553
1537
  }
@@ -18,7 +18,6 @@ import {
18
18
  type AskThreadStart,
19
19
  askThreadLimits,
20
20
  } from "@review/ask/thread.js";
21
- import { ASK_THREAD_ENV } from "@review/review-api/request-origin.js";
22
21
 
23
22
  /** How long an agent may take to say what it offers. */
24
23
  const OFFER_TIMEOUT_MS = 30_000;
@@ -164,7 +163,7 @@ export function cliAskTools(
164
163
  ];
165
164
 
166
165
  return {
167
- mcpServers: (threadId) => {
166
+ mcpServers: () => {
168
167
  const cli = cliPath();
169
168
 
170
169
  return cli
@@ -173,9 +172,7 @@ export function cliAskTools(
173
172
  name: "whiteboard",
174
173
  command: process.execPath,
175
174
  args: [cli, "mcp"],
176
- // MCP clients may not pass their own env on, so the server
177
- // gets the thread it serves in its own.
178
- env: [...env(), { name: ASK_THREAD_ENV, value: threadId }],
175
+ env: env(),
179
176
  },
180
177
  ]
181
178
  : [];
@@ -221,8 +218,6 @@ export class AskThreads {
221
218
 
222
219
  private readonly mcpServers: AskMcpServers;
223
220
 
224
- private readonly statusListeners = new Set<() => void>();
225
-
226
221
  constructor(
227
222
  private readonly launch: AskAgentLauncher,
228
223
  private readonly tools: AskTools = {},
@@ -254,8 +249,6 @@ export class AskThreads {
254
249
  );
255
250
 
256
251
  this.threads.set(thread.id, thread);
257
- thread.onStatusChange(() => this.statusChanged());
258
- this.statusChanged();
259
252
  void thread.open();
260
253
  this.sweeper ??= setInterval(() => this.sweep(), SWEEP_MS);
261
254
  this.sweeper.unref?.();
@@ -298,19 +291,6 @@ export class AskThreads {
298
291
  }
299
292
  }
300
293
 
301
- /** Calls back when a thread opens, closes or changes its status. */
302
- subscribeStatus(listener: () => void) {
303
- this.statusListeners.add(listener);
304
-
305
- return () => {
306
- this.statusListeners.delete(listener);
307
- };
308
- }
309
-
310
- private statusChanged() {
311
- for (const listener of this.statusListeners) listener();
312
- }
313
-
314
294
  get(id: string) {
315
295
  return this.threads.get(id);
316
296
  }
@@ -333,10 +313,9 @@ export class AskThreads {
333
313
 
334
314
  close(id: string, save = true) {
335
315
  const thread = this.threads.get(id);
316
+ this.threads.delete(id);
336
317
  this.unwatchedSince.delete(id);
337
318
  thread?.close(save);
338
-
339
- if (this.threads.delete(id)) this.statusChanged();
340
319
  }
341
320
 
342
321
  closeDeletedReviews(isDeleted: (reviewId: string) => boolean) {
@@ -699,16 +699,11 @@ export async function writePathShim(
699
699
  cliPath: string,
700
700
  runtimePath: string | undefined,
701
701
  devHome: string,
702
- previewHome = devHome,
703
702
  ): Promise<void> {
704
703
  if (process.platform === "win32") {
705
704
  await writeFileAtomicAsync(
706
705
  shimPath,
707
- windowsCliShim(
708
- cliPath,
709
- runtimePath ?? process.execPath,
710
- devHome === previewHome ? devHome : undefined,
711
- ),
706
+ windowsCliShim(cliPath, runtimePath ?? process.execPath, devHome),
712
707
  { replaceSymlink: true },
713
708
  );
714
709
 
@@ -721,8 +716,6 @@ export async function writePathShim(
721
716
  FALLBACK_CLI=${shSingleQuote(cliPath)}
722
717
  FALLBACK_RUNTIME=${shSingleQuote(runtimePath ?? "")}
723
718
  DEFAULT_HOME=${shSingleQuote(devHome)}
724
- channel="\${DEV_FAST_REVIEW_CHANNEL:-\${DEV_REVIEW_INSTANCE:-}}"
725
- [ "$channel" = preview ] && DEFAULT_HOME=${shSingleQuote(previewHome)}
726
719
  export DEV_REVIEW_HOME="\${DEV_REVIEW_HOME:-$DEFAULT_HOME}"
727
720
  DESKTOP="$DEV_REVIEW_HOME/review-desktop"
728
721
 
@@ -843,11 +836,7 @@ export async function installReviewCommand(input: {
843
836
  shimPath,
844
837
  input.cliPath,
845
838
  input.cliRuntimePath,
846
- devReviewHome({ DEV_REVIEW_HOME: env.DEV_REVIEW_HOME }, homeDir),
847
- devReviewHome(
848
- { DEV_REVIEW_HOME: env.DEV_REVIEW_HOME, DEV_REVIEW_INSTANCE: "preview" },
849
- homeDir,
850
- ),
839
+ devReviewHome(env, homeDir),
851
840
  );
852
841
 
853
842
  if (await traceMachineEnabled({ homeDir, env })) {
package/src/cli-runner.ts CHANGED
@@ -5,7 +5,6 @@ import { createInterface } from "node:readline/promises";
5
5
  import { Writable } from "node:stream";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
- import { boardTitle } from "@dev.fast/review-protocol";
9
8
  import {
10
9
  REVIEW_CLI_INSTALL_TARGET_LABELS,
11
10
  REVIEW_REMOTE_AGENT_IDS,
@@ -837,9 +836,7 @@ export async function runReviewCli(input: ReviewCliInput): Promise<number> {
837
836
  : "Whiteboard Desktop is ready in the background. Pass --focus to bring it forward.\n",
838
837
  );
839
838
  } else {
840
- input.stdout.write(
841
- `Whiteboard Desktop is showing "${boardTitle(event.title)}".\n`,
842
- );
839
+ input.stdout.write(`Whiteboard Desktop is showing "${event.title}".\n`);
843
840
  }
844
841
  };
845
842
 
package/src/cli.ts CHANGED
@@ -84,12 +84,9 @@ async function maybeDelegateToDesktopCli(
84
84
  // This bootstrap runs before the Node floor check, so it cannot import
85
85
  // devReviewHome() from @dev.fast/trace-core: that module graph needs a modern
86
86
  // Node. Keep this copy in step with devReviewHome().
87
- const channel =
88
- env.DEV_FAST_REVIEW_CHANNEL?.trim() || env.DEV_REVIEW_INSTANCE?.trim();
89
-
90
87
  const devHome = env.DEV_REVIEW_HOME?.trim()
91
88
  ? path.resolve(env.DEV_REVIEW_HOME.trim())
92
- : path.join(os.homedir(), channel === "preview" ? ".dev-preview" : ".dev");
89
+ : path.join(os.homedir(), ".dev");
93
90
 
94
91
  let cliPath: string;
95
92
  let runtimePath: string | undefined;
@@ -1,5 +1,4 @@
1
1
  import {
2
- type JsonObject,
3
2
  type JsonValue,
4
3
  isJsonObject,
5
4
  isNumberValue,
@@ -15,63 +14,50 @@ export function migrateDiffSelections(input: unknown): JsonValue {
15
14
  return migrateNode(jsonValueSchema.parse(input));
16
15
  }
17
16
 
18
- /** Keys whose values held retired attachments. */
19
- export const ATTACHMENT_KEYS = [
20
- "source",
21
- "peek",
22
- "sources",
23
- "contextSources",
24
- "callSite",
25
- ];
26
-
27
- /** A retired attachment as a selection, or undefined for anything else. */
28
- export function retiredSelection(value: JsonObject): JsonObject | undefined {
29
- if (!isStringValue(value.file)) return undefined;
30
-
31
- if (isNumberValue(value.fromLine) && isNumberValue(value.toLine)) {
32
- const side = value.side ?? value.graph ?? "head";
33
-
34
- return {
35
- file: value.file,
36
- start: { side, line: value.fromLine },
37
- end: { side, line: value.toLine },
38
- };
39
- }
40
-
41
- if (
42
- isJsonObject(value.start) &&
43
- isJsonObject(value.end) &&
44
- "baseLine" in value.start
45
- ) {
46
- const endpoint = (row: JsonObject) =>
47
- isNumberValue(row.headLine)
48
- ? { side: "head", line: row.headLine }
49
- : { side: "base", line: row.baseLine ?? null };
50
-
51
- return {
52
- file: value.file,
53
- start: endpoint(value.start),
54
- end: endpoint(value.end),
55
- };
56
- }
57
-
58
- return undefined;
59
- }
60
-
61
17
  function migrateNode(value: JsonValue, attachment = false): JsonValue {
62
18
  if (Array.isArray(value))
63
19
  return value.map((child) => migrateNode(child, attachment));
64
20
 
65
21
  if (!isJsonObject(value)) return value;
66
22
 
67
- const selection = attachment ? retiredSelection(value) : undefined;
68
-
69
- if (selection) return selection;
23
+ if (attachment && isStringValue(value.file)) {
24
+ if (isNumberValue(value.fromLine) && isNumberValue(value.toLine)) {
25
+ const side = value.side ?? value.graph ?? "head";
26
+
27
+ return {
28
+ file: value.file,
29
+ start: { side, line: value.fromLine },
30
+ end: { side, line: value.toLine },
31
+ };
32
+ }
33
+
34
+ if (
35
+ isJsonObject(value.start) &&
36
+ isJsonObject(value.end) &&
37
+ "baseLine" in value.start
38
+ ) {
39
+ const endpoint = (row: typeof value.start) =>
40
+ isNumberValue(row.headLine)
41
+ ? { side: "head", line: row.headLine }
42
+ : { side: "base", line: row.baseLine };
43
+
44
+ return {
45
+ file: value.file,
46
+ start: endpoint(value.start),
47
+ end: endpoint(value.end),
48
+ };
49
+ }
50
+ }
70
51
 
71
52
  return Object.fromEntries(
72
53
  Object.entries(value).map(([key, child]) => [
73
54
  key,
74
- migrateNode(child, ATTACHMENT_KEYS.includes(key)),
55
+ migrateNode(
56
+ child,
57
+ ["source", "peek", "sources", "contextSources", "callSite"].includes(
58
+ key,
59
+ ),
60
+ ),
75
61
  ]),
76
62
  );
77
63
  }
@@ -24,23 +24,8 @@ const sameSideAnchor = /^(head|base)\/(.+)#L(\d+)(?:-L(\d+))?$/;
24
24
 
25
25
  const crossSideAnchor = /^diff\/(.+)#([LR])(\d+)-([LR])(\d+)$/;
26
26
 
27
- const commitLinkAnchor = /^([0-9a-f]{7,40})\/(.+#L\d+(?:-L\d+)?)$/i;
28
-
29
- /** A source link's commit form, "<sha>/path#L10-L24" (without its
30
- * `whiteboard:source/` prefix): lines of that commit of the board's own
31
- * repository, read as their head side. Undefined for any other form. */
32
- export function commitLinkTarget(
33
- target: string,
34
- ): { commit: string; anchor: Anchor } | undefined {
35
- const match = commitLinkAnchor.exec(target);
36
-
37
- return match
38
- ? { commit: match[1]!.toLowerCase(), anchor: `head/${match[2]!}` }
39
- : undefined;
40
- }
41
-
42
27
  /** An anchor's selection, as markdown source links write it too (without
43
- * their `whiteboard:source/` prefix). Undefined when it isn't one. */
28
+ * their `review-source:` scheme). Undefined when it isn't one. */
44
29
  export function parseAnchor(text: string): DiffSelection | undefined {
45
30
  const same = sameSideAnchor.exec(text);
46
31
 
@@ -210,26 +195,6 @@ export function sourceAnchor(source: LensSource): FileLineRange {
210
195
  return sourceAnchors(source)[0]!;
211
196
  }
212
197
 
213
- /** Why a selection cannot be resolved at its pins, as telemetry reports it. */
214
- export const SELECTION_UNAVAILABLE_REASONS = [
215
- "missing_file",
216
- "no_alignment",
217
- "outside_lines",
218
- "other",
219
- ] as const;
220
-
221
- export type SelectionUnavailableReason =
222
- (typeof SELECTION_UNAVAILABLE_REASONS)[number];
223
-
224
- export class SelectionUnavailableError extends Error {
225
- constructor(
226
- readonly reason: SelectionUnavailableReason,
227
- message: string,
228
- ) {
229
- super(message);
230
- }
231
- }
232
-
233
198
  /** Resolve once against the renderer's rows. No matching or context expansion. */
234
199
  export function resolveDiffSelection(
235
200
  source: LensSource,
@@ -245,8 +210,7 @@ export function resolveDiffSelection(
245
210
  const end = locate(source.end);
246
211
 
247
212
  if (start < 0 || end < start)
248
- throw new SelectionUnavailableError(
249
- "outside_lines",
213
+ throw new Error(
250
214
  "Lens endpoints do not identify an ordered interval in this diff.",
251
215
  );
252
216
  const selected = rows.slice(start, end + 1);