diffsplain 0.8.3 → 0.10.0

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 (94) hide show
  1. package/README.md +26 -5
  2. package/dist/assets/{angular-html-e4D_THVy.js → angular-html-2u-u5H-N.js} +1 -1
  3. package/dist/assets/{angular-ts-Cx3870JF.js → angular-ts-4_HVGlKp.js} +1 -1
  4. package/dist/assets/{apl-DZuu22Wh.js → apl-icaNWZX3.js} +1 -1
  5. package/dist/assets/{astro-K-NemFAk.js → astro-HvIOE4vz.js} +1 -1
  6. package/dist/assets/{blade-ConbVu3U.js → blade-BpKJV8ev.js} +1 -1
  7. package/dist/assets/{c-DtmaBqln.js → c-BNvtxyRQ.js} +1 -1
  8. package/dist/assets/{cobol-BMHoPOfB.js → cobol-8js4hfxa.js} +1 -1
  9. package/dist/assets/{coffee-DfweJ19Y.js → coffee-DXg_piQ9.js} +1 -1
  10. package/dist/assets/{cpp-CPh_zIOW.js → cpp-BZDFAjyh.js} +1 -1
  11. package/dist/assets/{crystal-CAEX7C1J.js → crystal-aCF3ira4.js} +1 -1
  12. package/dist/assets/{css-C8ihGqCA.js → css-DfRtUkhE.js} +1 -1
  13. package/dist/assets/{edge-DVZPwQ9j.js → edge-vS6_AmQv.js} +1 -1
  14. package/dist/assets/{elixir-D03TKulP.js → elixir-Ce0nUHSq.js} +1 -1
  15. package/dist/assets/{elm-JUX-2xVV.js → elm-DmaeyizZ.js} +1 -1
  16. package/dist/assets/{erb-HbktAFZJ.js → erb-DD6OUyvt.js} +1 -1
  17. package/dist/assets/{git-rebase-Ccd_ELAP.js → git-rebase-HbJMn7iS.js} +1 -1
  18. package/dist/assets/{glimmer-js-BcinUfi2.js → glimmer-js-BRuqbNaV.js} +1 -1
  19. package/dist/assets/{glimmer-ts-DuN2UkN9.js → glimmer-ts-ZBpkdEEA.js} +1 -1
  20. package/dist/assets/{glsl-BWmCEPsR.js → glsl-CrQ4GScf.js} +1 -1
  21. package/dist/assets/{graphql-DJGYoBFW.js → graphql-Tn6PBdQY.js} +1 -1
  22. package/dist/assets/{hack-ZPqBE9w3.js → hack-CoApvrbo.js} +1 -1
  23. package/dist/assets/{haml-CZDZk6Gw.js → haml-COKq8Xjp.js} +1 -1
  24. package/dist/assets/{handlebars-d23PDS0n.js → handlebars-CAna2DTA.js} +1 -1
  25. package/dist/assets/{html-BgzX2DyB.js → html-Ch4xJ7YF.js} +1 -1
  26. package/dist/assets/{html-derivative-Bq6M-ZFw.js → html-derivative-0ITK-MAI.js} +1 -1
  27. package/dist/assets/{http-DVhMl_cW.js → http-DiMWQ8Qr.js} +1 -1
  28. package/dist/assets/{hurl-CbspBAqF.js → hurl-OcXymC0J.js} +1 -1
  29. package/dist/assets/index-BmcCug2M.js +1574 -0
  30. package/dist/assets/index-CADUNiTy.css +2 -0
  31. package/dist/assets/{java--YZbq0z2.js → java-CFb3qN4r.js} +1 -1
  32. package/dist/assets/{javascript-cvRuifRJ.js → javascript-CRmk2JrF.js} +1 -1
  33. package/dist/assets/{jinja-BeZmr3LP.js → jinja-Da6lDTEy.js} +1 -1
  34. package/dist/assets/{jison-qrpGZaxs.js → jison-Bnivzmtu.js} +1 -1
  35. package/dist/assets/{json-4xUzsNJn.js → json-m8vJP_KK.js} +1 -1
  36. package/dist/assets/{jsx-BlXrTlSg.js → jsx-C3HX_Sbu.js} +1 -1
  37. package/dist/assets/{julia-IN3jzmBc.js → julia-Ckg-wSHA.js} +1 -1
  38. package/dist/assets/{just-C_50nLQH.js → just-DCjIAyvO.js} +1 -1
  39. package/dist/assets/{latex-Cd-_2IS1.js → latex-CrYSm4pl.js} +1 -1
  40. package/dist/assets/{liquid-CEJf6Xg5.js → liquid-MQUFSPYU.js} +1 -1
  41. package/dist/assets/{lua-BlaV_n8i.js → lua-DcuzJJ77.js} +1 -1
  42. package/dist/assets/{marko-Cb-NPdc_.js → marko-4JNmQaB4.js} +1 -1
  43. package/dist/assets/{mdc-Dt4ywVAS.js → mdc-C_j8HvRL.js} +1 -1
  44. package/dist/assets/{nginx-C_6SKViP.js → nginx-ta3Gn1HE.js} +1 -1
  45. package/dist/assets/{nim-BbkBjoeZ.js → nim-BwWLst9M.js} +1 -1
  46. package/dist/assets/{perl-CYPU6TVa.js → perl-DfW7Uq5O.js} +1 -1
  47. package/dist/assets/{php-B6Oqilrc.js → php-DGf5G3Cf.js} +1 -1
  48. package/dist/assets/{pug-C9Poo2KU.js → pug-CWI-NaYz.js} +1 -1
  49. package/dist/assets/{qml-DkMURMPv.js → qml-Csblgh7U.js} +1 -1
  50. package/dist/assets/{r-Bm-Gfi0p.js → r-QGooaGDI.js} +1 -1
  51. package/dist/assets/{razor-B0LqOok3.js → razor-Dp9ihLYf.js} +1 -1
  52. package/dist/assets/{regexp-jAbMHzwe.js → regexp-DwUnLcVr.js} +1 -1
  53. package/dist/assets/{rst-BlWHfg8c.js → rst-BY8GIwDJ.js} +1 -1
  54. package/dist/assets/{ruby-Bt5lKwPL.js → ruby-VgzI9htT.js} +1 -1
  55. package/dist/assets/{sas-TuY2-8EF.js → sas-DYLsm-0j.js} +1 -1
  56. package/dist/assets/{scss-Dh1SsWlK.js → scss-tIFkeUQX.js} +1 -1
  57. package/dist/assets/{shellscript-CThiwmnp.js → shellscript-CHjuQ4Pl.js} +1 -1
  58. package/dist/assets/{shellsession-DQ5iRUWd.js → shellsession-Bv0S0hj9.js} +1 -1
  59. package/dist/assets/{soy-BfM-BrZG.js → soy-D6U1tZod.js} +1 -1
  60. package/dist/assets/{sql-F617W1uN.js → sql-CGz8kjKv.js} +1 -1
  61. package/dist/assets/{stata-Ctov3ki2.js → stata-C9ytERUq.js} +1 -1
  62. package/dist/assets/{surrealql-C-i2bJGB.js → surrealql-DRfRnaYu.js} +1 -1
  63. package/dist/assets/{svelte-oZT41pTl.js → svelte-wYnk_1sI.js} +1 -1
  64. package/dist/assets/{templ-CxX9Hf3e.js → templ-DWrGVE0u.js} +1 -1
  65. package/dist/assets/{tex-GyzJq8GJ.js → tex-CYTzoUJh.js} +1 -1
  66. package/dist/assets/{ts-tags-BugzKu0x.js → ts-tags-1TIXEGea.js} +1 -1
  67. package/dist/assets/{tsx-DLqwDEUq.js → tsx-a3BoAoOh.js} +1 -1
  68. package/dist/assets/{twig-DADiR9Vq.js → twig-e3dvNv6o.js} +1 -1
  69. package/dist/assets/{typescript-BlDIG_FO.js → typescript-BBhnpnTf.js} +1 -1
  70. package/dist/assets/{vue-COXmmg2A.js → vue-DzzVbu9q.js} +1 -1
  71. package/dist/assets/{vue-html-Cy_3KYj1.js → vue-html-quBNv38K.js} +1 -1
  72. package/dist/assets/{vue-vine-CUqOY8dY.js → vue-vine-CDuR69FE.js} +1 -1
  73. package/dist/assets/{xml-C9JY7Wf6.js → xml-DMifOLSe.js} +1 -1
  74. package/dist/assets/{xsl-DBCl8hSu.js → xsl-D8Ef-5HK.js} +1 -1
  75. package/dist/assets/{yaml-BbMebKkS.js → yaml-5c38gpmA.js} +1 -1
  76. package/dist/index.html +2 -2
  77. package/package.json +12 -3
  78. package/scripts/agent-config.mjs +184 -0
  79. package/scripts/agent-exclusions.mjs +10 -0
  80. package/scripts/agent-review.mjs +94 -0
  81. package/scripts/build-diff-data.mjs +211 -49
  82. package/scripts/cli-args.mjs +63 -9
  83. package/scripts/coding-agents.mjs +255 -77
  84. package/scripts/generate-summaries.mjs +872 -426
  85. package/scripts/local-target.mjs +106 -0
  86. package/scripts/present.mjs +333 -117
  87. package/scripts/review-chat-context.mjs +510 -0
  88. package/scripts/review-chat-controller.mjs +691 -0
  89. package/scripts/review-chat-provider.mjs +363 -0
  90. package/scripts/review-chat.mjs +8 -0
  91. package/scripts/serve-built.mjs +245 -27
  92. package/scripts/summary-path.mjs +32 -11
  93. package/dist/assets/index-C2p_1iAk.css +0 -2
  94. package/dist/assets/index-divlsuKs.js +0 -1568
@@ -0,0 +1,106 @@
1
+ import { spawnSync } from 'node:child_process';
2
+
3
+ export function isBaseWorktreeTarget({
4
+ base,
5
+ branch,
6
+ checkout,
7
+ head,
8
+ pullRequest,
9
+ worktree,
10
+ }) {
11
+ return Boolean(base) && [branch, checkout, head, pullRequest, worktree]
12
+ .every((value) => !value);
13
+ }
14
+
15
+ const checkoutTargetKinds = new Set([
16
+ 'checkout',
17
+ 'worktree',
18
+ 'base-worktree',
19
+ ]);
20
+
21
+ function localTargetKind({ checkout, worktree }) {
22
+ if (checkout) return 'checkout';
23
+ if (worktree) return 'worktree';
24
+ return undefined;
25
+ }
26
+
27
+ function remoteTargetKind({ branch, pullRequest }) {
28
+ if (pullRequest) return 'pr';
29
+ if (branch) return 'branch';
30
+ return undefined;
31
+ }
32
+
33
+ function hasRangeTarget({ base, head, range }) {
34
+ return Boolean(range || base || head);
35
+ }
36
+
37
+ function gitTargetKind(options) {
38
+ const { base, branch, checkout, head, pullRequest, worktree } = options;
39
+ if (isBaseWorktreeTarget({
40
+ base,
41
+ branch,
42
+ checkout,
43
+ head,
44
+ pullRequest,
45
+ worktree,
46
+ })) return 'base-worktree';
47
+ if (hasRangeTarget(options)) return 'range';
48
+ return undefined;
49
+ }
50
+
51
+ function explicitTargetKind(options) {
52
+ return localTargetKind(options) ||
53
+ remoteTargetKind(options) ||
54
+ gitTargetKind(options);
55
+ }
56
+
57
+ function resolvedTargetKind(options) {
58
+ const explicit = explicitTargetKind(options);
59
+ if (explicit) return explicit;
60
+ if (options.snapshotSupplied) return undefined;
61
+ return 'worktree';
62
+ }
63
+
64
+ export function reviewAccessMode({
65
+ repo,
66
+ base,
67
+ branch,
68
+ checkout,
69
+ head,
70
+ noCheckoutAccess = false,
71
+ pullRequest,
72
+ range,
73
+ snapshotSupplied = false,
74
+ worktree,
75
+ }) {
76
+ if (noCheckoutAccess) {
77
+ return { mode: 'snapshot-only', reason: 'disabled' };
78
+ }
79
+ const targetKind = resolvedTargetKind({
80
+ base,
81
+ branch,
82
+ checkout,
83
+ head,
84
+ pullRequest,
85
+ range,
86
+ snapshotSupplied,
87
+ worktree,
88
+ });
89
+ if (checkoutTargetKinds.has(targetKind)) {
90
+ return { mode: 'checkout-read-only', root: repo };
91
+ }
92
+ return { mode: 'snapshot-only', reason: 'target-mismatch' };
93
+ }
94
+
95
+ export function resolveBaseWorktreeCommit(repo, base) {
96
+ const result = spawnSync(
97
+ 'git',
98
+ ['-C', repo, 'rev-parse', '--verify', `${base}^{commit}`],
99
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
100
+ );
101
+ const oid = result.status === 0 ? result.stdout.trim() : '';
102
+ if (!oid) {
103
+ throw new Error(`Could not resolve base ref "${base}" to a commit.`);
104
+ }
105
+ return oid;
106
+ }
@@ -7,6 +7,7 @@ import {
7
7
  mkdirSync,
8
8
  mkdtempSync,
9
9
  readFileSync,
10
+ renameSync,
10
11
  rmSync,
11
12
  statSync,
12
13
  writeFileSync,
@@ -16,12 +17,20 @@ import { dirname, join, resolve } from 'node:path';
16
17
  import { createInterface } from 'node:readline';
17
18
  import { fileURLToPath } from 'node:url';
18
19
  import { helpText, parseCliArgs } from './cli-args.mjs';
20
+ import { applyAgentConfigOperation } from './agent-config.mjs';
19
21
  import {
22
+ agentReadOnlyWarning,
20
23
  assertReasoningSupported,
24
+ codingAgentFromSelectionError,
21
25
  codingAgentAvailability,
22
26
  codingAgentBinary,
23
27
  selectCodingAgent,
24
28
  } from './coding-agents.mjs';
29
+ import {
30
+ isBaseWorktreeTarget,
31
+ reviewAccessMode,
32
+ resolveBaseWorktreeCommit,
33
+ } from './local-target.mjs';
25
34
  import { doctorReport } from './doctor.mjs';
26
35
  import { accessTokenDirectory } from './access-token.mjs';
27
36
  import { cacheStatus, clearCache, formatCacheStatus, pruneCache } from './cache.mjs';
@@ -119,6 +128,22 @@ if (cli.version) {
119
128
  console.log(`diffsplain ${packageJson.version}`);
120
129
  process.exit(0);
121
130
  }
131
+ if (cli.config) {
132
+ try {
133
+ const result = applyAgentConfigOperation(cli.config);
134
+ if (result.kind === 'show') {
135
+ console.log(result.agent ?? 'No default coding agent is configured.');
136
+ } else if (result.kind === 'set') {
137
+ console.log(`Default coding agent set to "${result.agent}".`);
138
+ } else {
139
+ console.log('Default coding agent unset.');
140
+ }
141
+ process.exit(0);
142
+ } catch (error) {
143
+ console.error(error instanceof Error ? error.message : String(error));
144
+ process.exit(1);
145
+ }
146
+ }
122
147
  if (cli.doctor) {
123
148
  if (cli.doctor.deep) {
124
149
  console.error(
@@ -178,33 +203,90 @@ function emitSupportRecord(code = 1) {
178
203
  }
179
204
  }
180
205
 
206
+ function targetOption(args, name) {
207
+ const index = args.indexOf(name);
208
+ return index === -1 ? undefined : args[index + 1];
209
+ }
210
+
181
211
  const { agentEnabled, browserEnabled, host, port } = cli;
182
212
  const feedArgs = [...cli.feedArgs];
183
213
  const agentArgs = [...cli.agentArgs];
214
+ const base = targetOption(feedArgs, '--base');
215
+ const head = targetOption(feedArgs, '--head');
216
+ const branch = targetOption(feedArgs, '--branch');
217
+ const checkout = feedArgs.includes('--checkout');
218
+ const reviewRepo = targetOption(feedArgs, '--repo');
219
+ const accessMode = reviewAccessMode({
220
+ repo: reviewRepo,
221
+ base,
222
+ branch,
223
+ checkout,
224
+ head,
225
+ noCheckoutAccess: cli.noCheckoutAccess,
226
+ pullRequest: targetOption(feedArgs, '--pr'),
227
+ worktree: feedArgs.includes('--worktree'),
228
+ });
229
+ if (isBaseWorktreeTarget({
230
+ base,
231
+ branch,
232
+ checkout,
233
+ head,
234
+ pullRequest: targetOption(feedArgs, '--pr'),
235
+ worktree: feedArgs.includes('--worktree'),
236
+ })) {
237
+ try {
238
+ resolveBaseWorktreeCommit(targetOption(feedArgs, '--repo'), base);
239
+ } catch (error) {
240
+ console.error(error.message);
241
+ process.exit(2);
242
+ }
243
+ }
184
244
  let selectedAgent;
245
+ let selectedAgentBinary;
185
246
  beginSupportRecord();
186
247
  if (agentEnabled) {
187
248
  const selectionStarted = performance.now();
188
249
  try {
189
250
  selectedAgent = await selectCodingAgent(
190
251
  cli.agent,
191
- (agent) =>
192
- codingAgentAvailability(agent, {
193
- binary: codingAgentBinary(agent, { codexBin: cli.codexBin }),
194
- }),
252
+ {
253
+ available: (agent) =>
254
+ codingAgentAvailability(agent, {
255
+ binary: codingAgentBinary(agent, { codexBin: cli.codexBin }),
256
+ }),
257
+ },
195
258
  );
196
259
  assertReasoningSupported(selectedAgent, cli.reasoning);
197
- const agentBinary = codingAgentBinary(selectedAgent, {
260
+ selectedAgentBinary = codingAgentBinary(selectedAgent, {
198
261
  codexBin: cli.codexBin,
199
262
  });
200
- recordSelectedProvider(selectedAgent, agentBinary);
263
+ recordSelectedProvider(selectedAgent, selectedAgentBinary);
201
264
  supportRecorder?.addStage(
202
265
  'agent',
203
266
  performance.now() - selectionStarted,
204
267
  );
205
268
  agentArgs.push('--agent', selectedAgent);
269
+ if (
270
+ accessMode.mode === 'snapshot-only' &&
271
+ accessMode.reason === 'target-mismatch'
272
+ ) {
273
+ console.log(
274
+ 'Warning: This target does not map to the live checkout. Agent notes will use the supplied snapshot only.',
275
+ );
276
+ agentArgs.push('--access-warning-emitted');
277
+ }
278
+ const readOnlyWarning = agentReadOnlyWarning(selectedAgent, accessMode);
279
+ if (readOnlyWarning) {
280
+ console.log(readOnlyWarning);
281
+ agentArgs.push('--provider-read-only-warning-emitted');
282
+ }
206
283
  } catch (error) {
207
- recordSelectedProvider(cli.agent || 'unknown');
284
+ recordSelectedProvider(
285
+ codingAgentFromSelectionError(error) ||
286
+ selectedAgent ||
287
+ cli.agent ||
288
+ 'unknown',
289
+ );
208
290
  supportRecorder?.addStage(
209
291
  'agent',
210
292
  performance.now() - selectionStarted,
@@ -217,6 +299,12 @@ if (agentEnabled) {
217
299
  } else {
218
300
  feedArgs.push('--no-summaries');
219
301
  }
302
+ if (agentEnabled) {
303
+ agentArgs.push('--access-mode', accessMode.mode);
304
+ if (accessMode.mode === 'snapshot-only') {
305
+ agentArgs.push('--access-reason', accessMode.reason);
306
+ }
307
+ }
220
308
  const outputIndex = feedArgs.indexOf('--output');
221
309
  let runtimeDirectory;
222
310
 
@@ -226,16 +314,27 @@ if (outputIndex === -1) {
226
314
  feedArgs.push('--output', liveOutput);
227
315
  agentArgs.push('--output', liveOutput);
228
316
  }
317
+ const outputPath = resolve(
318
+ callerDirectory,
319
+ feedArgs[feedArgs.indexOf('--output') + 1],
320
+ );
321
+ let rawSnapshotPath = outputPath;
322
+ if (agentEnabled) {
323
+ if (!runtimeDirectory) {
324
+ runtimeDirectory = mkdtempSync(join(tmpdir(), 'diffsplain-live-'));
325
+ }
326
+ rawSnapshotPath = resolve(runtimeDirectory, 'raw-diff-data.json');
327
+ const feedOutputIndex = feedArgs.indexOf('--output');
328
+ feedArgs[feedOutputIndex + 1] = rawSnapshotPath;
329
+ feedArgs.push('--exclude-output', outputPath);
330
+ }
331
+ const reviewSnapshotPath = agentEnabled ? rawSnapshotPath : outputPath;
229
332
  process.on('exit', () => {
230
333
  if (runtimeDirectory) {
231
334
  rmSync(runtimeDirectory, { recursive: true, force: true });
232
335
  }
233
336
  });
234
337
  if (!feedArgs.includes('--watch')) feedArgs.push('--watch');
235
- const outputPath = resolve(
236
- callerDirectory,
237
- feedArgs[feedArgs.indexOf('--output') + 1],
238
- );
239
338
  const repoIndex = feedArgs.indexOf('--repo');
240
339
  const remoteIndex = feedArgs.indexOf('--remote');
241
340
  const projectKey = createHash('sha256')
@@ -262,7 +361,7 @@ writeFileSync(accessPath, access, { mode: 0o600 });
262
361
  chmodSync(accessPath, 0o600);
263
362
  if (agentEnabled) {
264
363
  feedArgs.push('--ignore-summary-watch');
265
- agentArgs.push('--snapshot', outputPath);
364
+ agentArgs.push('--snapshot', rawSnapshotPath);
266
365
  }
267
366
 
268
367
  const snapshotStarted = performance.now();
@@ -334,118 +433,185 @@ function scheduleBrowserOpen(match) {
334
433
  }, 750);
335
434
  }
336
435
 
436
+ function optionalSiteArgument(flag, value) {
437
+ return value ? [flag, value] : [];
438
+ }
439
+
440
+ function chatAccessArguments() {
441
+ if (accessMode.mode !== 'checkout-read-only') return [];
442
+ return ['--chat-access-root', accessMode.root];
443
+ }
444
+
445
+ function chatAgentArguments() {
446
+ if (!agentEnabled) return [];
447
+ return [
448
+ '--chat-agent',
449
+ selectedAgent,
450
+ '--chat-binary',
451
+ selectedAgentBinary,
452
+ '--chat-access-mode',
453
+ accessMode.mode,
454
+ ...chatAccessArguments(),
455
+ ...optionalSiteArgument('--chat-model', cli.model),
456
+ ...optionalSiteArgument('--chat-reasoning', cli.reasoning),
457
+ ];
458
+ }
459
+
460
+ function siteArguments() {
461
+ return [
462
+ resolve(root, 'scripts/serve-built.mjs'),
463
+ '--output',
464
+ outputPath,
465
+ '--port',
466
+ String(port),
467
+ '--host',
468
+ host,
469
+ '--project',
470
+ projectKey,
471
+ '--access',
472
+ access,
473
+ ...optionalSiteArgument('--previous-access', previousAccess),
474
+ '--chat-snapshot',
475
+ outputPath,
476
+ ...chatAgentArguments(),
477
+ ...(!cli.portWasPassed ? ['--increment-port'] : []),
478
+ ];
479
+ }
480
+
481
+ function handleSiteLine(line) {
482
+ if (handleConnectedTab(line)) return;
483
+ console.log(line);
484
+ const match = line.match(/^Diffsplain: (http:\/\/\S+)$/);
485
+ markSiteReady(match);
486
+ scheduleBrowserOpen(match);
487
+ }
488
+
489
+ function listenForSiteLines(child) {
490
+ if (!child.stdout) return;
491
+ createInterface({ input: child.stdout }).on('line', handleSiteLine);
492
+ }
493
+
494
+ function siteFailed(code, signal) {
495
+ return Boolean(code || signal);
496
+ }
497
+
498
+ function recordSiteFailure(code) {
499
+ supportRecorder?.addStage('serve', performance.now() - siteStarted, 'failed');
500
+ emitSupportRecord(code || 1);
501
+ }
502
+
503
+ function handleSiteExit(code, signal) {
504
+ if (closing) return;
505
+ if (siteFailed(code, signal)) recordSiteFailure(code);
506
+ stop(code || (signal ? 1 : 0));
507
+ }
508
+
509
+ function handleSiteError(error) {
510
+ if (closing) return;
511
+ supportRecorder?.addStage('serve', performance.now() - siteStarted, 'failed');
512
+ console.error(`Could not start the local page: ${error.message}`);
513
+ emitSupportRecord();
514
+ stop(1);
515
+ }
516
+
337
517
  function startSite() {
338
518
  if (closing || site) return;
339
519
  siteStarted = performance.now();
340
- const child = spawn(
341
- process.execPath,
342
- [
343
- resolve(root, 'scripts/serve-built.mjs'),
344
- '--output',
345
- outputPath,
346
- '--port',
347
- String(port),
348
- '--host',
349
- host,
350
- '--project',
351
- projectKey,
352
- '--access',
353
- access,
354
- ...(previousAccess ? ['--previous-access', previousAccess] : []),
355
- ...(!cli.portWasPassed ? ['--increment-port'] : []),
356
- ],
357
- { cwd: root, stdio: ['ignore', 'pipe', 'inherit'] },
358
- );
520
+ const child = spawn(process.execPath, siteArguments(), {
521
+ cwd: root,
522
+ stdio: ['ignore', 'pipe', 'inherit'],
523
+ });
359
524
  site = child;
525
+ listenForSiteLines(child);
526
+ child.on('exit', handleSiteExit);
527
+ child.on('error', handleSiteError);
528
+ }
529
+
530
+ function savedReviewFingerprint(notes) {
531
+ if (!notes) return undefined;
532
+ if (notes.agentReviewFingerprint) return notes.agentReviewFingerprint;
533
+ return notes.reviewFingerprint;
534
+ }
535
+
536
+ function snapshotReviewFingerprint(snapshot) {
537
+ const savedFingerprint = savedReviewFingerprint(snapshot.notes);
538
+ if (savedFingerprint) return savedFingerprint;
539
+ const reviewData = {
540
+ repo: {
541
+ name: snapshot.repo.name,
542
+ base: snapshot.repo.base,
543
+ head: snapshot.repo.head,
544
+ branch: snapshot.repo.branch,
545
+ baseBranch: snapshot.repo.baseBranch,
546
+ remote: snapshot.repo.remote,
547
+ targetKind: snapshot.repo.target?.kind,
548
+ },
549
+ change: {
550
+ title: snapshot.change.title,
551
+ number: snapshot.change.number,
552
+ },
553
+ files: snapshot.files.map((file) => ({
554
+ path: file.path,
555
+ oldPath: file.oldPath,
556
+ status: file.status,
557
+ additions: file.additions,
558
+ deletions: file.deletions,
559
+ isBinary: file.isBinary,
560
+ patch: file.patch,
561
+ })),
562
+ };
563
+ return createHash('sha256')
564
+ .update(JSON.stringify(reviewData))
565
+ .digest('hex');
566
+ }
360
567
 
361
- if (child.stdout) {
362
- const siteLines = createInterface({ input: child.stdout });
363
- siteLines.on('line', (line) => {
364
- if (handleConnectedTab(line)) return;
365
- console.log(line);
366
- const match = line.match(/^Diffsplain: (http:\/\/\S+)$/);
367
- markSiteReady(match);
368
- scheduleBrowserOpen(match);
369
- });
370
- }
568
+ function noteMetadataMatches(notes, fingerprint) {
569
+ return [
570
+ notes.complete,
571
+ notes.fresh,
572
+ notes.generatedFor === fingerprint,
573
+ notes.accessMode === accessMode.mode,
574
+ ].every(Boolean);
575
+ }
371
576
 
372
- child.on('exit', (code, signal) => {
373
- if (!closing) {
374
- if (code || signal) {
375
- supportRecorder?.addStage(
376
- 'serve',
377
- performance.now() - siteStarted,
378
- 'failed',
379
- );
380
- emitSupportRecord(code || 1);
381
- }
382
- stop(code || (signal ? 1 : 0));
383
- }
384
- });
385
- child.on('error', (error) => {
386
- if (!closing) {
387
- supportRecorder?.addStage(
388
- 'serve',
389
- performance.now() - siteStarted,
390
- 'failed',
391
- );
392
- console.error(`Could not start the local page: ${error.message}`);
393
- emitSupportRecord();
394
- stop(1);
395
- }
396
- });
577
+ function normalizedAgentSetting(value) {
578
+ return value ?? null;
579
+ }
580
+
581
+ function agentSettingsMatch(notes) {
582
+ return [
583
+ notes.agent === selectedAgent,
584
+ normalizedAgentSetting(notes.model) === normalizedAgentSetting(cli.model),
585
+ normalizedAgentSetting(notes.reasoning) ===
586
+ normalizedAgentSetting(cli.reasoning),
587
+ ].every(Boolean);
588
+ }
589
+
590
+ function emptyReview(files) {
591
+ return Array.isArray(files) && files.length === 0;
592
+ }
593
+
594
+ function hasCurrentAgentNotes(snapshot, fingerprint) {
595
+ const notes = snapshot.notes;
596
+ if (!savedReviewFingerprint(notes)) return false;
597
+ if (!noteMetadataMatches(notes, fingerprint)) return false;
598
+ if (emptyReview(snapshot.files)) return true;
599
+ return agentSettingsMatch(notes);
600
+ }
601
+
602
+ function snapshotStateFromSnapshot(snapshot) {
603
+ const fingerprint = snapshotReviewFingerprint(snapshot);
604
+ return {
605
+ fingerprint,
606
+ hasCurrentAgentNotes: hasCurrentAgentNotes(snapshot, fingerprint),
607
+ };
397
608
  }
398
609
 
399
610
  function snapshotState() {
400
611
  try {
401
- const snapshot = JSON.parse(readFileSync(outputPath, 'utf8'));
402
- if (snapshot.notes?.reviewFingerprint) {
403
- const fingerprint = snapshot.notes.reviewFingerprint;
404
- const emptyReview =
405
- Array.isArray(snapshot.files) && snapshot.files.length === 0;
406
- return {
407
- fingerprint,
408
- hasCurrentAgentNotes:
409
- snapshot.notes.complete &&
410
- snapshot.notes.fresh &&
411
- snapshot.notes.generatedFor === fingerprint &&
412
- (emptyReview ||
413
- (snapshot.notes.agent === selectedAgent &&
414
- (snapshot.notes.model || null) === (cli.model || null) &&
415
- (snapshot.notes.reasoning || null) ===
416
- (cli.reasoning || null))),
417
- };
418
- }
419
- const reviewData = {
420
- repo: {
421
- name: snapshot.repo.name,
422
- base: snapshot.repo.base,
423
- head: snapshot.repo.head,
424
- branch: snapshot.repo.branch,
425
- baseBranch: snapshot.repo.baseBranch,
426
- remote: snapshot.repo.remote,
427
- targetKind: snapshot.repo.target?.kind,
428
- },
429
- change: {
430
- title: snapshot.change.title,
431
- number: snapshot.change.number,
432
- },
433
- files: snapshot.files.map((file) => ({
434
- path: file.path,
435
- oldPath: file.oldPath,
436
- status: file.status,
437
- additions: file.additions,
438
- deletions: file.deletions,
439
- isBinary: file.isBinary,
440
- patch: file.patch,
441
- })),
442
- };
443
- return {
444
- fingerprint: createHash('sha256')
445
- .update(JSON.stringify(reviewData))
446
- .digest('hex'),
447
- hasCurrentAgentNotes: false,
448
- };
612
+ return snapshotStateFromSnapshot(
613
+ JSON.parse(readFileSync(reviewSnapshotPath, 'utf8')),
614
+ );
449
615
  } catch {
450
616
  return undefined;
451
617
  }
@@ -586,12 +752,16 @@ function scheduleAgent(fingerprint) {
586
752
  if (agent) {
587
753
  if (selectedFingerprint !== agentFingerprint) {
588
754
  queuedFingerprint = selectedFingerprint;
755
+ console.log(
756
+ 'Review changed while notes were generated. Restarting agent notes...',
757
+ );
589
758
  agent.kill('SIGTERM');
590
759
  }
591
760
  return;
592
761
  }
593
762
  clearTimeout(agentTimer);
594
763
  const delay = agentFingerprint ? 300 : 0;
764
+ console.log('Preparing agent notes...');
595
765
  agentTimer = setTimeout(() => runAgent(selectedFingerprint), delay);
596
766
  }
597
767
 
@@ -599,7 +769,7 @@ function markSnapshotReady() {
599
769
  if (snapshotReady) return;
600
770
  snapshotReady = true;
601
771
  try {
602
- supportRecorder?.addBytes('snapshot', statSync(outputPath).size);
772
+ supportRecorder?.addBytes('snapshot', statSync(reviewSnapshotPath).size);
603
773
  } catch {}
604
774
  supportRecorder?.addStage(
605
775
  'snapshot',
@@ -607,11 +777,55 @@ function markSnapshotReady() {
607
777
  );
608
778
  }
609
779
 
780
+ function snapshotForPresentation(snapshot) {
781
+ if (snapshotStateFromSnapshot(snapshot).hasCurrentAgentNotes) {
782
+ return snapshot;
783
+ }
784
+ const content = {
785
+ ...snapshot,
786
+ notes: {
787
+ ...snapshot.notes,
788
+ complete: false,
789
+ status: 'generating',
790
+ },
791
+ };
792
+ delete content.version;
793
+ delete content.generatedAt;
794
+ return {
795
+ version: createHash('sha256')
796
+ .update(JSON.stringify(content))
797
+ .digest('hex')
798
+ .slice(0, 12),
799
+ generatedAt: new Date().toISOString(),
800
+ ...content,
801
+ };
802
+ }
803
+
804
+ function seedPresentationSnapshot() {
805
+ const snapshot = snapshotForPresentation(
806
+ JSON.parse(readFileSync(rawSnapshotPath, 'utf8')),
807
+ );
808
+ mkdirSync(dirname(outputPath), { recursive: true });
809
+ const pendingOutput = `${outputPath}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
810
+ writeFileSync(pendingOutput, `${JSON.stringify(snapshot, null, 2)}\n`);
811
+ renameSync(pendingOutput, outputPath);
812
+ }
813
+
610
814
  function isSnapshotLine(line) {
611
815
  return line.startsWith('Wrote ') || line === 'No diff-data changes';
612
816
  }
613
817
 
614
- function startSnapshotDependents() {
818
+ function startSnapshotDependents({ rawSnapshotWritten = false } = {}) {
819
+ if (agentEnabled && rawSnapshotWritten) {
820
+ try {
821
+ seedPresentationSnapshot();
822
+ } catch (error) {
823
+ console.error(`Could not publish the live snapshot: ${error.message}`);
824
+ emitSupportRecord();
825
+ stop(1);
826
+ return;
827
+ }
828
+ }
615
829
  markSnapshotReady();
616
830
  startSite();
617
831
  if (agentEnabled) scheduleAgent();
@@ -622,7 +836,9 @@ function shouldPrintFeedLine(line) {
622
836
  }
623
837
 
624
838
  function handleFeedLine(line) {
625
- if (isSnapshotLine(line)) startSnapshotDependents();
839
+ if (isSnapshotLine(line)) {
840
+ startSnapshotDependents({ rawSnapshotWritten: line.startsWith('Wrote ') });
841
+ }
626
842
  if (shouldPrintFeedLine(line)) console.log(line);
627
843
  }
628
844