@dev.fast/whiteboard 0.2.1-preview.20261006.99 → 0.2.1-preview.20261007.101

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 (37) hide show
  1. package/dist/{account-alias-DOHN1RSH.js → account-alias-DLE9XZhi.js} +1 -1
  2. package/dist/agent-cli-DdfuiyOF.js +2 -0
  3. package/dist/{agent-cli-ChOq0ZuO.js → agent-cli-d0YJQNrR.js} +6 -6
  4. package/dist/{agent-client-CPQo7iTI.js → agent-client-DoaODH6I.js} +2 -2
  5. package/dist/{authoring-tools-Cr6Kpjsx.js → authoring-tools-Bvre8hN2.js} +2 -2
  6. package/dist/build-info.json +1 -1
  7. package/dist/{cli-hVgHcdsv.js → cli-DtolttUF.js} +1 -1
  8. package/dist/{cli-runner-CMmd6o_m.js → cli-runner-CSEH_mQN.js} +11 -11
  9. package/dist/cli.js +2 -2
  10. package/dist/{client-DFoh50W3.js → client-D01H2KOV.js} +2 -2
  11. package/dist/{desktop-discovery-DBO-PM2V.js → desktop-discovery-DK2HoBy5.js} +1 -1
  12. package/dist/{headless-host-CVMDNmLU.js → headless-host-YxYKMcI5.js} +3 -2
  13. package/dist/{local-data-CxLk75rx.js → local-data-Dy8AngNb.js} +37 -84
  14. package/dist/{mcp-QeF8vkdf.js → mcp-DGKhDPMs.js} +3 -3
  15. package/dist/{process-error-telemetry-CkiiadgB.js → process-error-telemetry-DN5BpZnc.js} +345 -13
  16. package/dist/{profile-4ry2f3AM.js → profile-B6jrb2QN.js} +1 -1
  17. package/dist/profile-D8XSrJYI.js +2 -0
  18. package/dist/{request-origin-D_4QIdoN.js → request-origin-BB2TENZ5.js} +1 -1
  19. package/dist/{review-telemetry-DEFnQsQ2.js → review-telemetry-2UlsdE4D.js} +2 -2
  20. package/dist/server/desktop-host.js +14 -303
  21. package/dist/sharing/index.d.ts +6 -10
  22. package/dist/sharing/index.js +1 -1
  23. package/dist/{src-DnwdaQ2r.js → src-DHiDJQDy.js} +7 -14
  24. package/dist/{stored-document-migration-DHeMpLMb.js → stored-document-migration-T9Uj5MkN.js} +1 -1
  25. package/dist/{tool-failure-C8zB73HV.js → tool-failure-Djeeax6q.js} +1 -1
  26. package/package.json +8 -9
  27. package/src/review-api/comparison-coverage.ts +6 -11
  28. package/src/review-api/http.ts +31 -4
  29. package/src/review-api/store.ts +0 -5
  30. package/src/server/desktop-server.ts +2 -13
  31. package/src/server/diffr-config.ts +170 -128
  32. package/src/server/headless-host.ts +3 -0
  33. package/src/server/structural-diff.ts +12 -39
  34. package/src/server/test-fixtures/diffr-ui-overrides.toml +38 -0
  35. package/dist/agent-cli-DzWfZr6G.js +0 -2
  36. package/dist/profile-Bh5FIrmf.js +0 -2
  37. package/src/server/diffr-languages.ts +0 -97
@@ -1237,11 +1237,6 @@ export class ReviewStore {
1237
1237
  ...(applied?.children && { children: applied.children }),
1238
1238
  };
1239
1239
 
1240
- if (snapshot.staleSources?.length)
1241
- warnings.push(
1242
- "Some authored source ranges changed. Update their references before presenting this review.",
1243
- );
1244
-
1245
1240
  if (warnings.length) result.warnings = warnings;
1246
1241
 
1247
1242
  this.commitCommand(
@@ -57,12 +57,12 @@ import { z } from "zod";
57
57
  import { aliasInstallationToAccount } from "./account-alias";
58
58
  import { CrashReportRequestSchema, reportCrashDump } from "./crash-report";
59
59
  import {
60
+ migrateDiffrConfig,
60
61
  readDiffrConfig,
61
62
  saveDiffrSummarizer,
62
63
  setDiffrConfigValue,
63
64
  testDiffrSummarizer,
64
65
  } from "./diffr-config";
65
- import { installFullDiffr } from "./diffr-languages.js";
66
66
  import {
67
67
  GlobalReviewDesktopVerbRelay,
68
68
  type ReviewDesktopVerbRelay,
@@ -397,18 +397,6 @@ export function createGlobalReviewServer(
397
397
 
398
398
  return serverJson(200, { ok: true });
399
399
  });
400
- app.post("/diffr-languages/install", async () => {
401
- try {
402
- await installFullDiffr();
403
- invalidateStructuralComparisons();
404
-
405
- return serverJson(200, { installed: true });
406
- } catch (error) {
407
- return serverJson(503, {
408
- error: error instanceof Error ? error.message : String(error),
409
- });
410
- }
411
- });
412
400
 
413
401
  app.get("/diffr-config", async () =>
414
402
  serverJson(200, await readDiffrConfig()),
@@ -580,6 +568,7 @@ export function createGlobalReviewServer(
580
568
  return urlForBoundPort();
581
569
  },
582
570
  listen: async () => {
571
+ await migrateDiffrConfig();
583
572
  scratchpadEnabled = await readScratchpadEnabled();
584
573
  boundPort = await listen(httpServer, input.port);
585
574
  discovery.url = urlForBoundPort();
@@ -15,6 +15,7 @@ import {
15
15
  type ReviewDiffrConfig,
16
16
  type ReviewDiffrProvider,
17
17
  type ReviewDiffrSummarizerInput,
18
+ type StructuralRegion,
18
19
  decodeStructuralDiffEvent,
19
20
  reviewDiffrSummarizerInputSchema,
20
21
  } from "@dev.fast/review-protocol";
@@ -36,29 +37,91 @@ function serialized<T>(operation: () => Promise<T>): Promise<T> {
36
37
  return next;
37
38
  }
38
39
 
40
+ type DiffrArguments =
41
+ | { command: "schema" }
42
+ | { command: "show"; reveal: boolean }
43
+ | { command: "set"; patch: JsonObject }
44
+ | { command: "migrate" }
45
+ | { command: "diff"; before: string; after: string };
46
+
47
+ function diffrArgv(args: DiffrArguments): string[] {
48
+ switch (args.command) {
49
+ case "schema":
50
+ return ["config", "schema"];
51
+ case "show":
52
+ return ["config", "show", "--json", ...(args.reveal ? ["--reveal"] : [])];
53
+ case "set":
54
+ return ["config", "set", "--stdin", "--json"];
55
+ case "migrate":
56
+ return ["config", "migrate", "--json"];
57
+ case "diff":
58
+ return [
59
+ "--no-index",
60
+ "--format",
61
+ "ndjson",
62
+ "--",
63
+ args.before,
64
+ args.after,
65
+ ];
66
+ }
67
+ }
68
+
39
69
  async function diffr(
40
- args: string[],
70
+ args: DiffrArguments,
41
71
  rootPath?: string,
42
72
  options: { signal?: AbortSignal; env?: NodeJS.ProcessEnv } = {},
43
73
  ): Promise<string> {
74
+ const executable = diffrExecutable();
75
+
44
76
  try {
45
- const { stdout } = await execFileAsync(diffrExecutable(), args, {
77
+ const operation = execFileAsync(executable, diffrArgv(args), {
46
78
  cwd: rootPath,
47
79
  maxBuffer: 16 * 1024 * 1024,
48
80
  signal: options.signal ?? AbortSignal.timeout(30_000),
49
81
  env: options.env ?? process.env,
50
82
  });
51
83
 
84
+ if (args.command === "set")
85
+ operation.child.stdin?.end(JSON.stringify(args.patch));
86
+ const { stdout } = await operation;
87
+
52
88
  return stdout;
53
89
  } catch (error) {
54
- // SAFETY: execFile rejects with an Error carrying its exit or spawn code.
90
+ // SAFETY: execFile adds the exit or spawn code and stdout to its Error.
55
91
  // Do not forward its message: it includes command arguments.
56
- const failure = error as NodeJS.ErrnoException;
92
+ const failure = error as NodeJS.ErrnoException & { stdout?: string };
57
93
 
58
94
  if (failure.code === "ENOENT") throw diffrMissingError();
59
95
 
60
96
  if (failure.name === "AbortError")
61
97
  throw new Error("diffr timed out or was cancelled.");
98
+
99
+ if (
100
+ (args.command === "set" || args.command === "migrate") &&
101
+ failure.stdout
102
+ ) {
103
+ let details: JsonValue | undefined;
104
+
105
+ try {
106
+ details = parseJsonText(failure.stdout);
107
+ } catch {
108
+ /* Do not echo raw output in parse errors. */
109
+ }
110
+
111
+ if (
112
+ isJsonObject(details) &&
113
+ isJsonObject(details.error) &&
114
+ isStringValue(details.error.message)
115
+ ) {
116
+ const { message, repair_prompt: prompt } = details.error;
117
+ throw new Error(
118
+ isStringValue(prompt)
119
+ ? `${message}\n\nCopy this as a prompt to your coding agent:\n${prompt}`
120
+ : message,
121
+ );
122
+ }
123
+ }
124
+
62
125
  throw new Error(
63
126
  "diffr could not complete the operation. Check its configuration and credentials.",
64
127
  );
@@ -70,13 +133,7 @@ async function values(
70
133
  reveal = false,
71
134
  signal?: AbortSignal,
72
135
  ): Promise<JsonObject> {
73
- return json(
74
- await diffr(
75
- ["config", "show", "--json", ...(reveal ? ["--reveal"] : [])],
76
- rootPath,
77
- { signal },
78
- ),
79
- );
136
+ return json(await diffr({ command: "show", reveal }, rootPath, { signal }));
80
137
  }
81
138
 
82
139
  function json(output: string): JsonObject {
@@ -100,7 +157,7 @@ function valueAt(object: JsonObject, key: string): JsonValue | undefined {
100
157
  return value;
101
158
  }
102
159
 
103
- const prefix = "plugins.bundled.summarize";
160
+ const prefix = "plugins.shape.bundled.summarize";
104
161
 
105
162
  function savedProvider(config: JsonObject): string {
106
163
  const value = valueAt(config, `${prefix}.provider`);
@@ -108,20 +165,16 @@ function savedProvider(config: JsonObject): string {
108
165
  return isStringValue(value) ? value : "";
109
166
  }
110
167
 
111
- function savedEndpoint(config: JsonObject): string {
112
- const value = valueAt(config, `${prefix}.endpoint`);
113
-
114
- return isStringValue(value) ? value : "";
115
- }
116
-
117
168
  /** Whether a saved key belongs to a different destination than the draft's. */
118
169
  function movesKey(
119
170
  config: JsonObject,
120
171
  draft: Pick<ReviewDiffrSummarizerInput, "provider" | "endpoint">,
121
172
  ): boolean {
173
+ const endpoint = valueAt(config, `${prefix}.endpoint`);
174
+
122
175
  return (
123
176
  draft.provider !== savedProvider(config) ||
124
- draft.endpoint !== savedEndpoint(config)
177
+ draft.endpoint !== (isStringValue(endpoint) ? endpoint : "")
125
178
  );
126
179
  }
127
180
 
@@ -151,27 +204,35 @@ function strings(value: JsonValue | undefined): string[] {
151
204
  }
152
205
 
153
206
  /**
154
- * What diffr's schema says about summaries: each provider's defaults, and
155
- * the default prompt with the link its description gives. Nothing when
156
- * diffr cannot describe its schema: the rest of Settings still works.
207
+ * Defaults supplied by diffr's schema for tag hiding and summary controls.
208
+ * If the schema cannot be read, resolved settings still work.
157
209
  */
158
- async function summaryDefaults(
210
+ async function configDefaults(
159
211
  rootPath?: string,
160
- ): Promise<Pick<ReviewDiffrConfig, "providers" | "defaultPrompt">> {
212
+ ): Promise<
213
+ Pick<ReviewDiffrConfig, "providers" | "defaultPrompt" | "defaultHiddenTags">
214
+ > {
161
215
  let schema: JsonObject;
162
216
 
163
217
  try {
164
- schema = json(await diffr(["config", "schema"], rootPath));
218
+ schema = json(await diffr({ command: "schema" }, rootPath));
165
219
  } catch {
166
220
  return {};
167
221
  }
168
222
 
169
223
  const options = valueAt(
170
224
  schema,
171
- "properties.plugins.properties.bundled.properties.summarize.properties",
225
+ "properties.plugins.properties.shape.properties.bundled.properties.summarize.properties",
172
226
  );
173
227
 
174
- if (!isJsonObject(options)) return {};
228
+ const defaultHiddenTags = strings(
229
+ valueAt(
230
+ schema,
231
+ "properties.plugins.properties.classify.properties.bundled.properties.hide.default",
232
+ ),
233
+ );
234
+
235
+ if (!isJsonObject(options)) return { defaultHiddenTags };
175
236
 
176
237
  const option = (name: string) =>
177
238
  isJsonObject(options[name]) ? options[name] : {};
@@ -200,6 +261,7 @@ async function summaryDefaults(
200
261
  const prompt = option("system_prompt");
201
262
 
202
263
  return {
264
+ defaultHiddenTags,
203
265
  providers,
204
266
  defaultPrompt: isStringValue(prompt.default) ? prompt.default : undefined,
205
267
  };
@@ -207,7 +269,7 @@ async function summaryDefaults(
207
269
 
208
270
  async function read(rootPath?: string): Promise<ReviewDiffrConfig> {
209
271
  const config = await values(rootPath);
210
- const defaults = await summaryDefaults(rootPath);
272
+ const defaults = await configDefaults(rootPath);
211
273
  const saved = valueAt(config, `${prefix}.api_key`);
212
274
 
213
275
  const credentialSource =
@@ -217,16 +279,15 @@ async function read(rootPath?: string): Promise<ReviewDiffrConfig> {
217
279
  ? "environment"
218
280
  : "missing";
219
281
 
220
- const plugins = config.plugins;
282
+ const plugins = valueAt(config, "plugins.shape");
221
283
 
222
284
  if (isJsonObject(plugins)) {
223
- for (const namespace of Object.values(plugins)) {
224
- if (!isJsonObject(namespace)) continue;
285
+ const entries = isJsonObject(plugins.bundled)
286
+ ? [...Object.values(plugins), ...Object.values(plugins.bundled)]
287
+ : Object.values(plugins);
225
288
 
226
- for (const plugin of Object.values(namespace)) {
227
- if (isJsonObject(plugin)) delete plugin.api_key;
228
- }
229
- }
289
+ for (const plugin of entries)
290
+ if (isJsonObject(plugin)) delete plugin.api_key;
230
291
  }
231
292
 
232
293
  return { values: config, credentialSource, ...defaults };
@@ -236,35 +297,35 @@ export function readDiffrConfig(rootPath?: string): Promise<ReviewDiffrConfig> {
236
297
  return serialized(() => read(rootPath));
237
298
  }
238
299
 
239
- export function diffrConfigValueText(value: JsonValue): string {
240
- return isStringValue(value) ? value : JSON.stringify(value);
300
+ export async function migrateDiffrConfig(
301
+ signal?: AbortSignal,
302
+ ): Promise<boolean> {
303
+ const result = json(
304
+ await diffr({ command: "migrate" }, undefined, { signal }),
305
+ );
306
+
307
+ if (result.changed === true) invalidateStructuralComparisons();
308
+
309
+ return result.changed === true;
241
310
  }
242
311
 
243
312
  async function writeSettings(
244
- entries: [string, JsonValue][],
313
+ patch: JsonObject,
245
314
  rootPath?: string,
246
315
  ): Promise<ReviewDiffrConfig> {
247
316
  let changed = false;
248
317
  let error: string | undefined;
249
- let current = await values(rootPath);
250
318
 
251
319
  try {
252
- for (const [key, value] of entries) {
253
- if (valueAt(current, key) === value) continue;
254
- await diffr(
255
- ["config", "set", key, diffrConfigValueText(value)],
256
- rootPath,
257
- );
258
- changed = true;
259
- // A write can clear others, such as a provider's endpoint and key.
260
- current = await values(rootPath);
261
- }
262
- } catch {
263
- error = changed
264
- ? "Some settings were saved before diffr rejected a change. Check the current values and try again."
265
- : "diffr could not save the setting. Check its configuration and the entered value.";
266
- } finally {
320
+ const result = json(await diffr({ command: "set", patch }, rootPath));
321
+ changed = result.changed === true;
322
+
267
323
  if (changed) invalidateStructuralComparisons();
324
+ } catch (cause) {
325
+ error =
326
+ cause instanceof Error
327
+ ? cause.message
328
+ : "diffr could not save the settings.";
268
329
  }
269
330
 
270
331
  return { ...(await read(rootPath)), changed, error };
@@ -275,13 +336,16 @@ export function setDiffrConfigValue(
275
336
  value: JsonValue,
276
337
  rootPath?: string,
277
338
  ): Promise<ReviewDiffrConfig> {
278
- if (
279
- !/^[A-Za-z0-9_][A-Za-z0-9_-]*(\.[A-Za-z0-9_][A-Za-z0-9_-]*)*$/.test(key)
280
- ) {
339
+ if (!/^[A-Za-z0-9_][A-Za-z0-9_-]*(\.[^.\r\n\0]+)*$/u.test(key)) {
281
340
  throw new Error("Invalid diffr config key.");
282
341
  }
283
342
 
284
- return serialized(() => writeSettings([[key, value]], rootPath));
343
+ const parts = key.split(".");
344
+ let patch: JsonObject = { [parts.pop()!]: value };
345
+
346
+ for (const part of parts.reverse()) patch = { [part]: patch };
347
+
348
+ return serialized(() => writeSettings(patch, rootPath));
285
349
  }
286
350
 
287
351
  export function saveDiffrSummarizer(
@@ -313,40 +377,24 @@ export function saveDiffrSummarizer(
313
377
  };
314
378
  }
315
379
 
316
- const entries: [string, JsonValue][] = [];
317
-
318
- if (!draft.enabled) entries.push([`${prefix}.enabled`, false]);
319
-
320
- // Clear a key before its destination changes and write a new one only
321
- // after, so a failure part way never pairs a key with another vendor.
322
- if (moving && savedKey) entries.push([`${prefix}.api_key`, ""]);
323
- entries.push([`${prefix}.provider`, draft.provider]);
380
+ const summarize: JsonObject = {
381
+ enabled: draft.enabled,
382
+ provider: draft.provider,
383
+ endpoint: draft.endpoint,
384
+ model: draft.model,
385
+ tests: draft.tests,
386
+ };
324
387
 
325
- // diffr clears a provider's endpoint when the provider changes, so a
326
- // custom one kept across a switch is written again.
327
- if (
328
- draft.endpoint !== savedEndpoint(current.values) ||
329
- (draft.endpoint && draft.provider !== savedProvider(current.values))
330
- )
331
- entries.push([`${prefix}.endpoint`, draft.endpoint]);
388
+ if (draft.apiKey) summarize.api_key = draft.apiKey;
389
+ else if (moving && savedKey) summarize.api_key = "";
332
390
 
333
- if (draft.apiKey) entries.push([`${prefix}.api_key`, draft.apiKey]);
391
+ if (draft.systemPrompt.trim()) summarize.system_prompt = draft.systemPrompt;
334
392
 
335
- // A blank prompt leaves diffr's own in place.
336
- if (
337
- draft.systemPrompt.trim() &&
338
- draft.systemPrompt !== valueAt(current.values, `${prefix}.system_prompt`)
339
- )
340
- entries.push([`${prefix}.system_prompt`, draft.systemPrompt]);
341
-
342
- entries.push(
343
- [`${prefix}.model`, draft.model],
344
- [`${prefix}.tests`, draft.tests],
393
+ return writeSettings(
394
+ // oxlint-disable-next-line anti-slop/no-shape-in-symbol-names -- Native diffr config key.
395
+ { plugins: { shape: { bundled: { summarize } } } },
396
+ rootPath,
345
397
  );
346
-
347
- if (draft.enabled) entries.push([`${prefix}.enabled`, true]);
348
-
349
- return writeSettings(entries, rootPath);
350
398
  });
351
399
  }
352
400
 
@@ -373,7 +421,7 @@ export async function testDiffrSummarizer(
373
421
  throw new Error("The summarizer is not available in this configuration.");
374
422
 
375
423
  const { provider, endpoint, model, systemPrompt } = parsed.data;
376
- const providers = (await summaryDefaults(rootPath)).providers ?? [];
424
+ const providers = (await configDefaults(rootPath)).providers ?? [];
377
425
 
378
426
  const variables =
379
427
  providers.find((known) => known.id === provider)?.keyVariables ?? [];
@@ -410,10 +458,13 @@ export async function testDiffrSummarizer(
410
458
  await writeFile(
411
459
  join(directory, "diffr", "config.toml"),
412
460
  stringify({
413
- version: 1,
461
+ version: 2,
414
462
  plugins: {
415
- order: ["bundled.summarize"],
416
- bundled: { summarize: options },
463
+ // oxlint-disable-next-line anti-slop/no-shape-in-symbol-names -- Native diffr v2 config key.
464
+ shape: {
465
+ order: ["bundled.summarize"],
466
+ bundled: { summarize: options },
467
+ },
417
468
  },
418
469
  }),
419
470
  { mode: 0o600 },
@@ -425,56 +476,47 @@ export async function testDiffrSummarizer(
425
476
  await writeFile(before, "");
426
477
  await writeFile(after, SUMMARY_SAMPLE);
427
478
 
428
- const output = await diffr(
429
- [
430
- "--no-index",
431
- "--format",
432
- "ndjson",
433
- "--stream-annotations",
434
- "--",
435
- before,
436
- after,
437
- ],
438
- directory,
439
- {
440
- env: {
441
- ...process.env,
442
- ...Object.fromEntries(
443
- providers
444
- .flatMap((known) => known.keyVariables)
445
- .map((name) => [name, ""]),
446
- ),
447
- ...(apiKey && variables[0] && { [variables[0]]: apiKey }),
448
- XDG_CONFIG_HOME: directory,
449
- },
450
- signal: deadline,
479
+ const output = await diffr({ command: "diff", before, after }, directory, {
480
+ env: {
481
+ ...process.env,
482
+ ...Object.fromEntries(
483
+ providers
484
+ .flatMap((known) => known.keyVariables)
485
+ .map((name) => [name, ""]),
486
+ ),
487
+ ...(apiKey && variables[0] && { [variables[0]]: apiKey }),
488
+ XDG_CONFIG_HOME: directory,
451
489
  },
452
- );
490
+ signal: deadline,
491
+ });
453
492
 
454
493
  try {
455
494
  const events = output.trim().split("\n").map(decodeStructuralDiffEvent);
456
495
  const complete = events.at(-1);
457
496
 
458
- const annotation = events
497
+ const labels = (region: StructuralRegion): string[] => [
498
+ ...(region.visibility?.label ? [region.visibility.label] : []),
499
+ ...(region.kind === "fold" ? region.children.flatMap(labels) : []),
500
+ ];
501
+
502
+ const summary = events
459
503
  .flatMap((event) =>
460
- event.type === "annotations" ? event.annotations : [],
504
+ event.type === "file" && event.diff?.type === "text"
505
+ ? [event.diff.rhs, event.diff.lhs].flatMap((source) =>
506
+ source ? labels(source.root) : [],
507
+ )
508
+ : [],
461
509
  )
462
- .find((item) => item.label.trim());
510
+ .find((label) => label.trim());
463
511
 
464
512
  if (
465
513
  complete?.type === "complete" &&
466
514
  complete.failed === 0 &&
467
515
  !complete.aborted &&
468
- !events.some(
469
- (event) =>
470
- (event.type === "annotations" || event.type === "file") &&
471
- event.error,
472
- ) &&
473
- annotation
516
+ !events.some((event) => event.type === "file" && event.error) &&
517
+ summary
474
518
  ) {
475
- return apiKey
476
- ? annotation.label.split(apiKey).join("[redacted]")
477
- : annotation.label;
519
+ return apiKey ? summary.split(apiKey).join("[redacted]") : summary;
478
520
  }
479
521
  } catch {
480
522
  /* Never return provider output or parser causes. */
@@ -12,6 +12,7 @@ import {
12
12
  reviewServerDiscoveryPath,
13
13
  } from "@review/server-discovery.js";
14
14
 
15
+ import { migrateDiffrConfig } from "./diffr-config.js";
15
16
  import { GlobalReviewDesktopVerbRelay } from "./global-verb-relay.js";
16
17
  import { createNodeRequestListener } from "./hono-http.js";
17
18
  import {
@@ -72,6 +73,8 @@ export function withHeadlessServerLock<T>(
72
73
  async function serve(input: HeadlessServerInput) {
73
74
  if (input.signal.aborted) return;
74
75
 
76
+ await migrateDiffrConfig(input.signal);
77
+
75
78
  if (input.telemetry) await drainServerCrashReport(input.telemetry);
76
79
 
77
80
  const local = await openReviewProfile(input.stateDir, {
@@ -1,17 +1,13 @@
1
1
  import { spawn } from "node:child_process";
2
- import { existsSync } from "node:fs";
3
- import path from "node:path";
4
2
  import { createInterface } from "node:readline";
5
3
 
4
+ import { diffrBinaryPath } from "@dev.fast/diffr";
6
5
  import {
7
6
  STRUCTURAL_DIFF_WIRE_VERSION,
8
7
  type StructuralDiffEvent,
9
8
  type StructuralProblem,
10
9
  decodeStructuralDiffEvent,
11
10
  } from "@dev.fast/review-protocol";
12
- import { findReviewPackageRoot } from "@review/package-paths";
13
-
14
- import { installedFullDiffr } from "./diffr-languages.js";
15
11
 
16
12
  export type DiffComparison =
17
13
  | { kind: "trees"; base: string; head: string }
@@ -26,27 +22,17 @@ export interface StructuralDiffRequest {
26
22
  signal: AbortSignal;
27
23
  }
28
24
 
29
- export function diffrExecutable(
30
- packageRoot = findReviewPackageRoot(import.meta.url),
31
- ): string {
32
- if (process.env.REVIEW_DIFFR_BINARY) return process.env.REVIEW_DIFFR_BINARY;
33
-
34
- const full = installedFullDiffr(packageRoot);
25
+ export function diffrExecutable(): string {
26
+ const executable = diffrBinaryPath();
35
27
 
36
- if (full) return full;
28
+ if (!executable) throw diffrMissingError();
37
29
 
38
- const bundled = path.join(
39
- packageRoot,
40
- "bin",
41
- process.platform === "win32" ? "diffr.exe" : "diffr",
42
- );
43
-
44
- return existsSync(bundled) ? bundled : "diffr";
30
+ return executable;
45
31
  }
46
32
 
47
33
  export function diffrMissingError(): Error {
48
34
  return new Error(
49
- `Cannot find diffr at ${diffrExecutable()}. Whiteboard Desktop bundles it at bin/diffr under its runtime; in a checkout, run \`pnpm --filter @dev.fast/whiteboard ensure:diffr\` or install diffr on PATH, or set REVIEW_DIFFR_BINARY to its executable.`,
35
+ "The bundled diffr executable is missing. Reinstall Whiteboard.",
50
36
  );
51
37
  }
52
38
 
@@ -59,15 +45,10 @@ export async function* structuralDiff(
59
45
  input: StructuralDiffRequest,
60
46
  ): AsyncGenerator<StructuralDiffEvent> {
61
47
  input.signal.throwIfAborted();
48
+ const executable = diffrExecutable();
62
49
  const comparison = input.comparison;
63
50
 
64
- const args = [
65
- "--repo",
66
- input.repositoryPath,
67
- "--format",
68
- "ndjson",
69
- "--stream-annotations",
70
- ];
51
+ const args = ["--repo", input.repositoryPath, "--format", "ndjson"];
71
52
 
72
53
  switch (comparison.kind) {
73
54
  case "worktree":
@@ -88,11 +69,9 @@ export async function* structuralDiff(
88
69
 
89
70
  // The host inherits its own environment and runs from the repository, so
90
71
  // diffr reads the user's config and keys exactly as it would from a shell.
91
- console.info(
92
- `[Review] structural diff: ${diffrExecutable()} ${args.join(" ")}`,
93
- );
72
+ console.info(`[Review] structural diff: ${executable} ${args.join(" ")}`);
94
73
 
95
- const child = spawn(diffrExecutable(), args, {
74
+ const child = spawn(executable, args, {
96
75
  cwd: input.repositoryPath,
97
76
  stdio: ["ignore", "pipe", "pipe"],
98
77
  signal,
@@ -120,7 +99,6 @@ export async function* structuralDiff(
120
99
  let completed = false;
121
100
  let aborted: StructuralProblem | undefined;
122
101
  let failed = 0;
123
- let annotationFailed = false;
124
102
  const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
125
103
 
126
104
  try {
@@ -152,8 +130,6 @@ export async function* structuralDiff(
152
130
 
153
131
  failed = event.failed;
154
132
  aborted = event.aborted;
155
- } else if (event.type === "annotations") {
156
- annotationFailed ||= event.error !== undefined;
157
133
  } else if (event.type !== "file") {
158
134
  throw new Error(`Unexpected diffr event: ${event.type}`);
159
135
  }
@@ -168,13 +144,10 @@ export async function* structuralDiff(
168
144
  throw new Error("diffr stream ended before completion.");
169
145
  }
170
146
 
171
- // diffr exits 2 for file or enrichment failures already reported by the stream.
147
+ // diffr exits 2 for file failures already reported by the stream.
172
148
  const code = await exited;
173
149
 
174
- if (
175
- code !== 0 &&
176
- !(code === 2 && (failed > 0 || annotationFailed || aborted !== undefined))
177
- )
150
+ if (code !== 0 && !(code === 2 && (failed > 0 || aborted !== undefined)))
178
151
  throw exitError(code);
179
152
  } finally {
180
153
  clearTimeout(idle);