diffsplain 0.5.0 → 0.7.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 (92) hide show
  1. package/README.md +17 -12
  2. package/dist/assets/{angular-html-CoQle5-T.js → angular-html-CphzFC_v.js} +1 -1
  3. package/dist/assets/{angular-ts-BB_A1Lpy.js → angular-ts-Am0-mbFw.js} +1 -1
  4. package/dist/assets/{apl-B7_4_FNq.js → apl-Cs2rPKDN.js} +1 -1
  5. package/dist/assets/{astro-CXVo4VE4.js → astro-CxKUUn9h.js} +1 -1
  6. package/dist/assets/{blade-BVX41dq7.js → blade-DqeCa6v5.js} +1 -1
  7. package/dist/assets/{c-Dhyi8Nd2.js → c-DdI6lknk.js} +1 -1
  8. package/dist/assets/{cobol-CU5oLeZj.js → cobol-COuAGE2X.js} +1 -1
  9. package/dist/assets/{coffee-D_FU6gC_.js → coffee-BFlYT3Lp.js} +1 -1
  10. package/dist/assets/{cpp-CxoYLSi3.js → cpp-nywLU0Np.js} +1 -1
  11. package/dist/assets/{crystal-BdgqBN3z.js → crystal-BWp4dz1s.js} +1 -1
  12. package/dist/assets/{css-wLk1vVwJ.js → css-CRb9tUpU.js} +1 -1
  13. package/dist/assets/{edge-DMylWoGl.js → edge-rUYSbJy4.js} +1 -1
  14. package/dist/assets/{elixir-Djmgto_Z.js → elixir-DOq9ybIH.js} +1 -1
  15. package/dist/assets/{elm-D7B-eBLA.js → elm-C8e38E5-.js} +1 -1
  16. package/dist/assets/{erb-g1sso1GW.js → erb-BR9Q37Et.js} +1 -1
  17. package/dist/assets/{git-rebase-DEAhxutZ.js → git-rebase-Coo2LUtB.js} +1 -1
  18. package/dist/assets/{glimmer-js-CHvGFGMp.js → glimmer-js-C7hX9jg6.js} +1 -1
  19. package/dist/assets/{glimmer-ts-DvhL705p.js → glimmer-ts-CZLT0F0k.js} +1 -1
  20. package/dist/assets/{glsl-C8mS_t8I.js → glsl-BDZRv6e-.js} +1 -1
  21. package/dist/assets/{graphql-DW2pnvqi.js → graphql-DyzFc3y3.js} +1 -1
  22. package/dist/assets/{hack-DYc1d3he.js → hack-CWnO7WsF.js} +1 -1
  23. package/dist/assets/{haml-_5OmzH31.js → haml-CvDj-ghO.js} +1 -1
  24. package/dist/assets/{handlebars-rN-CaXUf.js → handlebars-BnU_YcVA.js} +1 -1
  25. package/dist/assets/{html-DQnaJUjl.js → html-CjB2TKKa.js} +1 -1
  26. package/dist/assets/{html-derivative-BovOubry.js → html-derivative-CYQqEXS6.js} +1 -1
  27. package/dist/assets/{http-BKTgwOYc.js → http-NNQbnqIx.js} +1 -1
  28. package/dist/assets/{hurl-ChS3_IgD.js → hurl-DHaAA1BS.js} +1 -1
  29. package/dist/assets/{index-BLtRBc2n.css → index-C2p_1iAk.css} +1 -1
  30. package/dist/assets/{index-CEglkSs-.js → index-f9ekN94b.js} +11 -11
  31. package/dist/assets/{java-Dgnyk--z.js → java-D1J5V2CD.js} +1 -1
  32. package/dist/assets/{javascript-BfnrwkRN.js → javascript-C3pX-HsE.js} +1 -1
  33. package/dist/assets/{jinja-CD2AIvOi.js → jinja-BY9CIY8g.js} +1 -1
  34. package/dist/assets/{jison-DmKNw3Pl.js → jison-Cr0H89gS.js} +1 -1
  35. package/dist/assets/{json-CKB5a8aj.js → json-CA3kKNfq.js} +1 -1
  36. package/dist/assets/{jsx-jJXrDRBM.js → jsx-BuMkR-Lj.js} +1 -1
  37. package/dist/assets/{julia-G8vVBW-t.js → julia-C2sLwg3f.js} +1 -1
  38. package/dist/assets/{just-DMXLrLZL.js → just-pXpkLBXv.js} +1 -1
  39. package/dist/assets/{latex-DS3xuxld.js → latex-7v3EPOdF.js} +1 -1
  40. package/dist/assets/{liquid-COQnD_C4.js → liquid-CJZq0Z0t.js} +1 -1
  41. package/dist/assets/{lua-BwMKwosd.js → lua-BimBzvTH.js} +1 -1
  42. package/dist/assets/{marko-cQCK_WIk.js → marko-CCZHVnQN.js} +1 -1
  43. package/dist/assets/{mdc-DQUjY13N.js → mdc-SLECNw2A.js} +1 -1
  44. package/dist/assets/{nginx-C4Kv3DJi.js → nginx-DUwtUge2.js} +1 -1
  45. package/dist/assets/{nim-ChzLzuYD.js → nim-HD8o_Omp.js} +1 -1
  46. package/dist/assets/{perl-z5xUiWAY.js → perl-BGMeIkNk.js} +1 -1
  47. package/dist/assets/{php-BHqm-Xeh.js → php-BL5xuY-v.js} +1 -1
  48. package/dist/assets/{pug-C9C_Llft.js → pug-C8G1U_u8.js} +1 -1
  49. package/dist/assets/{qml-dtpuwXJZ.js → qml-BGVSyw1x.js} +1 -1
  50. package/dist/assets/{r-BicQMs_c.js → r-BDnRcclC.js} +1 -1
  51. package/dist/assets/{razor-D5h00YTZ.js → razor-I8tbEIVJ.js} +1 -1
  52. package/dist/assets/{regexp-CTa8S86v.js → regexp-D0yakB9Q.js} +1 -1
  53. package/dist/assets/{rst-Bg-b-7HK.js → rst-DbTCn07T.js} +1 -1
  54. package/dist/assets/{ruby-BiWYm056.js → ruby-BwgPFIcO.js} +1 -1
  55. package/dist/assets/{sas-bJTdfgUz.js → sas-dN4ePfXU.js} +1 -1
  56. package/dist/assets/{scss-0qkNyHr1.js → scss-T7M1A4iM.js} +1 -1
  57. package/dist/assets/{shellscript-3sDkDE9z.js → shellscript-HFjBHS7_.js} +1 -1
  58. package/dist/assets/{shellsession-D4EBhEmz.js → shellsession-DCAGskrx.js} +1 -1
  59. package/dist/assets/{soy-DG9hUwLL.js → soy-BgT96TF9.js} +1 -1
  60. package/dist/assets/{sql-B0UoKHkx.js → sql-DCIQeRzx.js} +1 -1
  61. package/dist/assets/{stata-CsQbuXQp.js → stata-Dn0tad0_.js} +1 -1
  62. package/dist/assets/{surrealql-D63VLxos.js → surrealql-CCgFEQfW.js} +1 -1
  63. package/dist/assets/{svelte-BApdnAma.js → svelte-ITXimG8u.js} +1 -1
  64. package/dist/assets/{templ-B64pN6Lz.js → templ-DJVZZqPw.js} +1 -1
  65. package/dist/assets/{tex-DR2UVIPF.js → tex-Bn4N-OtH.js} +1 -1
  66. package/dist/assets/{ts-tags-IwhomvQv.js → ts-tags-CxQ4PoC4.js} +1 -1
  67. package/dist/assets/{tsx-DROdxqzS.js → tsx-CxwgcjkC.js} +1 -1
  68. package/dist/assets/{twig-BtbUqpR4.js → twig-CtbyTyMH.js} +1 -1
  69. package/dist/assets/{typescript-CHpSjYZL.js → typescript-d6ru12L8.js} +1 -1
  70. package/dist/assets/{vue-DCvEu_ug.js → vue-RvqAsYDm.js} +1 -1
  71. package/dist/assets/{vue-html-BgL4Rf-L.js → vue-html-BkHDVENe.js} +1 -1
  72. package/dist/assets/{vue-vine-CRNdhPTn.js → vue-vine-DvS3Mlkn.js} +1 -1
  73. package/dist/assets/{xml-CkfPxZuz.js → xml-lzTq5A-V.js} +1 -1
  74. package/dist/assets/{xsl-CteaoFSV.js → xsl-BL0KXNWI.js} +1 -1
  75. package/dist/assets/{yaml-CUr3TDRN.js → yaml-CF4XlBJS.js} +1 -1
  76. package/dist/demo-diff-data.json +6 -1
  77. package/dist/index.html +2 -2
  78. package/package.json +50 -23
  79. package/scripts/access-token.mjs +21 -0
  80. package/scripts/build-diff-data.mjs +260 -39
  81. package/scripts/cache.mjs +301 -0
  82. package/scripts/cli-args.mjs +206 -57
  83. package/scripts/coding-agents.mjs +279 -162
  84. package/scripts/dev.mjs +218 -0
  85. package/scripts/doctor.mjs +265 -46
  86. package/scripts/generate-summaries.mjs +760 -260
  87. package/scripts/mock-agent.mjs +165 -0
  88. package/scripts/present.mjs +394 -85
  89. package/scripts/presenter-runtime.mjs +148 -0
  90. package/scripts/serve-built.mjs +343 -71
  91. package/scripts/summary-path.mjs +24 -7
  92. package/scripts/support-record.mjs +242 -0
@@ -1,12 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { spawn, spawnSync } from 'node:child_process';
4
- import { createHash } from 'node:crypto';
3
+ import { spawn } from 'node:child_process';
4
+ import { createHash, randomBytes } from 'node:crypto';
5
5
  import {
6
- existsSync,
6
+ chmodSync,
7
+ mkdirSync,
7
8
  mkdtempSync,
8
9
  readFileSync,
9
10
  rmSync,
11
+ statSync,
12
+ writeFileSync,
10
13
  } from 'node:fs';
11
14
  import { tmpdir } from 'node:os';
12
15
  import { dirname, join, resolve } from 'node:path';
@@ -14,14 +17,88 @@ import { createInterface } from 'node:readline';
14
17
  import { fileURLToPath } from 'node:url';
15
18
  import { helpText, parseCliArgs } from './cli-args.mjs';
16
19
  import {
20
+ assertReasoningSupported,
17
21
  codingAgentBinary,
18
22
  commandAvailable,
19
23
  selectCodingAgent,
20
24
  } from './coding-agents.mjs';
21
25
  import { doctorReport } from './doctor.mjs';
26
+ import { accessTokenDirectory } from './access-token.mjs';
27
+ import { cacheStatus, clearCache, formatCacheStatus, pruneCache } from './cache.mjs';
28
+ import {
29
+ agentFallbackRecordNeeded,
30
+ agentRunCompleted,
31
+ agentRunFailed,
32
+ agentRunNeeded,
33
+ agentRunSuperseded,
34
+ ensureBuiltAssets,
35
+ failedAgentRunForFingerprint,
36
+ nextAgentFingerprint,
37
+ openBrowser,
38
+ } from './presenter-runtime.mjs';
39
+ import {
40
+ createSupportRecorder,
41
+ formatSupportRecord,
42
+ safeCommandVersion,
43
+ writeSupportRecord,
44
+ } from './support-record.mjs';
22
45
 
23
46
  const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
24
47
  const callerDirectory = process.cwd();
48
+ const cacheArgs = process.argv.slice(2);
49
+
50
+ function cacheUsage() {
51
+ console.error(
52
+ 'Use: diffsplain cache [status|prune --age DAYS|prune --size BYTES|clear --yes]',
53
+ );
54
+ return 2;
55
+ }
56
+
57
+ function printCacheChange(result) {
58
+ console.log(
59
+ `Removed ${result.removed.length} inactive cache entries; kept ${result.retainedActive.length} active.`,
60
+ );
61
+ return 0;
62
+ }
63
+
64
+ function statusCommand(args) {
65
+ if (args.length !== 1 && args.length !== 2) return cacheUsage();
66
+ console.log(formatCacheStatus(cacheStatus()));
67
+ return 0;
68
+ }
69
+
70
+ function clearCommand(args) {
71
+ if (args.length !== 3 || args[2] !== '--yes') return cacheUsage();
72
+ return printCacheChange(clearCache());
73
+ }
74
+
75
+ function pruneOptions(flag, rawValue) {
76
+ const value = Number(rawValue);
77
+ if (!Number.isFinite(value) || value < 0) return undefined;
78
+ if (flag === '--age') return { maxAgeMs: value * 86_400_000 };
79
+ if (flag === '--size') return { maxBytes: value };
80
+ return undefined;
81
+ }
82
+
83
+ function pruneCommand(args) {
84
+ if (args.length !== 4) return cacheUsage();
85
+ const options = pruneOptions(args[2], args[3]);
86
+ return options ? printCacheChange(pruneCache(options)) : cacheUsage();
87
+ }
88
+
89
+ function runCacheCommand(args) {
90
+ const commands = {
91
+ status: statusCommand,
92
+ clear: clearCommand,
93
+ prune: pruneCommand,
94
+ };
95
+ const command = args[1] || 'status';
96
+ return commands[command]?.(args) ?? cacheUsage();
97
+ }
98
+
99
+ if (cacheArgs[0] === 'cache') {
100
+ process.exit(runCacheCommand(cacheArgs));
101
+ }
25
102
  let cli;
26
103
  try {
27
104
  cli = parseCliArgs(process.argv.slice(2), { callerDirectory });
@@ -43,28 +120,102 @@ if (cli.version) {
43
120
  process.exit(0);
44
121
  }
45
122
  if (cli.doctor) {
46
- const report = await doctorReport();
47
- console.log(report.text);
123
+ if (cli.doctor.deep) {
124
+ console.error(
125
+ 'Warning: deep checks run local provider commands. They do not send a provider prompt.',
126
+ );
127
+ }
128
+ const report = await doctorReport({ deep: cli.doctor.deep });
129
+ console.log(cli.doctor.json ? JSON.stringify(report.json, null, 2) : report.text);
48
130
  process.exit(report.ready ? 0 : 1);
49
131
  }
50
132
 
51
- const { agentEnabled, port } = cli;
133
+ let supportRecorder;
134
+ let supportRecordEmitted = false;
135
+
136
+ function wantsSupportRecord() {
137
+ return cli.supportRecord || Boolean(cli.supportRecordFile);
138
+ }
139
+
140
+ function selectedProviderVersion(provider, binary) {
141
+ if (!binary) return null;
142
+ return safeCommandVersion(provider, binary);
143
+ }
144
+
145
+ function beginSupportRecord() {
146
+ if (supportRecorder) return;
147
+ if (!wantsSupportRecord()) return;
148
+ supportRecorder = createSupportRecorder();
149
+ }
150
+
151
+ function recordSelectedProvider(provider, binary) {
152
+ supportRecorder?.setProvider(
153
+ provider,
154
+ selectedProviderVersion(provider, binary),
155
+ );
156
+ }
157
+
158
+ function deliverSupportRecord(record) {
159
+ if (cli.supportRecordFile) {
160
+ writeSupportRecord(cli.supportRecordFile, record);
161
+ console.error(`Wrote support record to ${cli.supportRecordFile}`);
162
+ return;
163
+ }
164
+ process.stderr.write(
165
+ `Diffsplain support record:\n${formatSupportRecord(record)}`,
166
+ );
167
+ }
168
+
169
+ function emitSupportRecord(code = 1) {
170
+ if (!supportRecorder) return;
171
+ if (supportRecordEmitted) return;
172
+ supportRecordEmitted = true;
173
+ const record = supportRecorder.failure(code);
174
+ try {
175
+ deliverSupportRecord(record);
176
+ } catch {
177
+ console.error('Could not write the support record.');
178
+ }
179
+ }
180
+
181
+ const { agentEnabled, browserEnabled, host, port } = cli;
52
182
  const feedArgs = [...cli.feedArgs];
53
183
  const agentArgs = [...cli.agentArgs];
184
+ let selectedAgent;
185
+ beginSupportRecord();
54
186
  if (agentEnabled) {
187
+ const selectionStarted = performance.now();
55
188
  try {
56
- const selectedAgent = await selectCodingAgent(
189
+ selectedAgent = await selectCodingAgent(
57
190
  cli.agent,
58
191
  (agent) =>
59
192
  commandAvailable(
60
193
  codingAgentBinary(agent, { codexBin: cli.codexBin }),
61
194
  ),
62
195
  );
196
+ assertReasoningSupported(selectedAgent, cli.reasoning);
197
+ const agentBinary = codingAgentBinary(selectedAgent, {
198
+ codexBin: cli.codexBin,
199
+ });
200
+ recordSelectedProvider(selectedAgent, agentBinary);
201
+ supportRecorder?.addStage(
202
+ 'agent',
203
+ performance.now() - selectionStarted,
204
+ );
63
205
  agentArgs.push('--agent', selectedAgent);
64
206
  } catch (error) {
207
+ recordSelectedProvider(cli.agent || 'unknown');
208
+ supportRecorder?.addStage(
209
+ 'agent',
210
+ performance.now() - selectionStarted,
211
+ 'failed',
212
+ );
65
213
  console.error(error.message);
214
+ emitSupportRecord();
66
215
  process.exit(1);
67
216
  }
217
+ } else {
218
+ feedArgs.push('--no-summaries');
68
219
  }
69
220
  const outputIndex = feedArgs.indexOf('--output');
70
221
  let runtimeDirectory;
@@ -95,23 +246,39 @@ const projectKey = createHash('sha256')
95
246
  )
96
247
  .digest('hex')
97
248
  .slice(0, 12);
249
+ const accessDirectory = accessTokenDirectory();
250
+ const accessPath = join(accessDirectory, `${projectKey}.token`);
251
+ mkdirSync(accessDirectory, { recursive: true, mode: 0o700 });
252
+ chmodSync(accessDirectory, 0o700);
253
+ let previousAccess;
254
+ try {
255
+ const savedAccess = readFileSync(accessPath, 'utf8').trim();
256
+ if (/^[A-Za-z0-9_-]{43}$/.test(savedAccess)) previousAccess = savedAccess;
257
+ } catch {
258
+ // The first run for a project has no prior tab access value.
259
+ }
260
+ const access = randomBytes(32).toString('base64url');
261
+ writeFileSync(accessPath, access, { mode: 0o600 });
262
+ chmodSync(accessPath, 0o600);
98
263
  if (agentEnabled) {
99
264
  feedArgs.push('--ignore-summary-watch');
100
265
  agentArgs.push('--snapshot', outputPath);
101
266
  }
102
267
 
103
- const builtPage = resolve(root, 'dist/index.html');
104
- if (!existsSync(builtPage)) {
105
- const result = spawnSync(
106
- process.platform === 'win32' ? 'npm.cmd' : 'npm',
107
- ['run', 'build'],
108
- { cwd: root, stdio: 'inherit' },
268
+ const snapshotStarted = performance.now();
269
+ let snapshotReady = false;
270
+ try {
271
+ ensureBuiltAssets({ root });
272
+ } catch (error) {
273
+ const exitCode = Number.isInteger(error.exitCode) ? error.exitCode : 1;
274
+ supportRecorder?.addStage(
275
+ 'snapshot',
276
+ performance.now() - snapshotStarted,
277
+ 'failed',
109
278
  );
110
- if (result.error) {
111
- console.error(`Could not build the local page: ${result.error.message}`);
112
- process.exit(1);
113
- }
114
- if (result.status !== 0) process.exit(result.status || 1);
279
+ console.error(error.message);
280
+ emitSupportRecord(exitCode);
281
+ process.exit(exitCode);
115
282
  }
116
283
 
117
284
  const feed = spawn(
@@ -127,39 +294,49 @@ let site;
127
294
  let agent;
128
295
  let agentTimer;
129
296
  let agentFingerprint;
297
+ let completedAgentFingerprint;
298
+ let failedAgentFingerprint;
130
299
  let queuedFingerprint;
131
300
  let browserOpened = false;
132
301
  let browserOpenTimer;
302
+ let siteReady = false;
303
+ let siteStarted;
133
304
 
134
- function openBrowser(url) {
135
- let command;
136
- let args;
137
- if (process.env.BROWSER) {
138
- command = process.env.BROWSER;
139
- args = [url];
140
- } else if (process.platform === 'darwin') {
141
- command = 'open';
142
- args = [url];
143
- } else if (process.platform === 'win32') {
144
- command = 'cmd.exe';
145
- args = ['/d', '/s', '/c', 'start', '', url];
146
- } else {
147
- command = 'xdg-open';
148
- args = [url];
305
+ function handleConnectedTab(line) {
306
+ if (line !== 'Diffsplain tab: connected') return false;
307
+ if (!browserOpened && browserOpenTimer) {
308
+ clearTimeout(browserOpenTimer);
309
+ browserOpenTimer = undefined;
310
+ browserOpened = true;
311
+ console.log('Reusing the open Diffsplain tab.');
149
312
  }
313
+ return true;
314
+ }
150
315
 
151
- const opener = spawn(command, args, {
152
- detached: true,
153
- stdio: 'ignore',
154
- });
155
- opener.once('error', (error) => {
156
- console.error(`Could not open the browser: ${error.message}`);
157
- });
158
- opener.unref();
316
+ function markSiteReady(match) {
317
+ if (siteReady || !match) return;
318
+ siteReady = true;
319
+ supportRecorder?.addStage(
320
+ 'serve',
321
+ performance.now() - siteStarted,
322
+ );
323
+ }
324
+
325
+ function scheduleBrowserOpen(match) {
326
+ if (!browserEnabled || browserOpened || browserOpenTimer || !match) return;
327
+ browserOpenTimer = setTimeout(() => {
328
+ browserOpenTimer = undefined;
329
+ browserOpened = true;
330
+ openBrowser(match[1], {
331
+ onError: (error) =>
332
+ console.error(`Could not open the browser: ${error.message}`),
333
+ });
334
+ }, 750);
159
335
  }
160
336
 
161
337
  function startSite() {
162
338
  if (closing || site) return;
339
+ siteStarted = performance.now();
163
340
  const child = spawn(
164
341
  process.execPath,
165
342
  [
@@ -168,8 +345,13 @@ function startSite() {
168
345
  outputPath,
169
346
  '--port',
170
347
  String(port),
348
+ '--host',
349
+ host,
171
350
  '--project',
172
351
  projectKey,
352
+ '--access',
353
+ access,
354
+ ...(previousAccess ? ['--previous-access', previousAccess] : []),
173
355
  ...(!cli.portWasPassed ? ['--increment-port'] : []),
174
356
  ],
175
357
  { cwd: root, stdio: ['ignore', 'pipe', 'inherit'] },
@@ -179,33 +361,36 @@ function startSite() {
179
361
  if (child.stdout) {
180
362
  const siteLines = createInterface({ input: child.stdout });
181
363
  siteLines.on('line', (line) => {
182
- if (line === 'Diffsplain tab: connected') {
183
- if (!browserOpened && browserOpenTimer) {
184
- clearTimeout(browserOpenTimer);
185
- browserOpenTimer = undefined;
186
- browserOpened = true;
187
- console.log('Reusing the open Diffsplain tab.');
188
- }
189
- return;
190
- }
364
+ if (handleConnectedTab(line)) return;
191
365
  console.log(line);
192
366
  const match = line.match(/^Diffsplain: (http:\/\/\S+)$/);
193
- if (!browserOpened && !browserOpenTimer && match) {
194
- browserOpenTimer = setTimeout(() => {
195
- browserOpenTimer = undefined;
196
- browserOpened = true;
197
- openBrowser(match[1]);
198
- }, 750);
199
- }
367
+ markSiteReady(match);
368
+ scheduleBrowserOpen(match);
200
369
  });
201
370
  }
202
371
 
203
372
  child.on('exit', (code, signal) => {
204
- if (!closing) stop(code || (signal ? 1 : 0));
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
+ }
205
384
  });
206
385
  child.on('error', (error) => {
207
386
  if (!closing) {
387
+ supportRecorder?.addStage(
388
+ 'serve',
389
+ performance.now() - siteStarted,
390
+ 'failed',
391
+ );
208
392
  console.error(`Could not start the local page: ${error.message}`);
393
+ emitSupportRecord();
209
394
  stop(1);
210
395
  }
211
396
  });
@@ -216,12 +401,19 @@ function snapshotState() {
216
401
  const snapshot = JSON.parse(readFileSync(outputPath, 'utf8'));
217
402
  if (snapshot.notes?.reviewFingerprint) {
218
403
  const fingerprint = snapshot.notes.reviewFingerprint;
404
+ const emptyReview =
405
+ Array.isArray(snapshot.files) && snapshot.files.length === 0;
219
406
  return {
220
407
  fingerprint,
221
408
  hasCurrentAgentNotes:
222
409
  snapshot.notes.complete &&
223
410
  snapshot.notes.fresh &&
224
- snapshot.notes.generatedFor === fingerprint,
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))),
225
417
  };
226
418
  }
227
419
  const reviewData = {
@@ -263,6 +455,56 @@ function snapshotFingerprint() {
263
455
  return snapshotState()?.fingerprint;
264
456
  }
265
457
 
458
+ function recordAgentFallbackStage(needed, startedAt) {
459
+ if (!needed) return;
460
+ supportRecorder?.addStage(
461
+ 'agent',
462
+ performance.now() - startedAt,
463
+ 'failed',
464
+ );
465
+ }
466
+
467
+ function releaseAgentChild(child) {
468
+ if (agent === child) agent = undefined;
469
+ agentFingerprint = undefined;
470
+ }
471
+
472
+ function recordAgentRunResult({
473
+ closing,
474
+ code,
475
+ error,
476
+ signal,
477
+ superseded,
478
+ finishedFingerprint,
479
+ }) {
480
+ if (agentRunCompleted({ code, error, signal, superseded })) {
481
+ completedAgentFingerprint = finishedFingerprint;
482
+ }
483
+ if (!agentRunFailed({ closing, code, error, signal, superseded })) return;
484
+ failedAgentFingerprint = finishedFingerprint;
485
+ if (error) console.error(error.message);
486
+ console.error(
487
+ 'The coding agent could not write notes. It will retry after the diff changes or Diffsplain restarts.',
488
+ );
489
+ }
490
+
491
+ function scheduleNextAgentFingerprint({
492
+ pendingFingerprint,
493
+ finishedFingerprint,
494
+ }) {
495
+ const nextFingerprint = nextAgentFingerprint({
496
+ queuedFingerprint: pendingFingerprint,
497
+ observedFingerprint: snapshotFingerprint(),
498
+ finishedFingerprint,
499
+ });
500
+ queuedFingerprint = undefined;
501
+ if (nextFingerprint) scheduleAgent(nextFingerprint);
502
+ }
503
+
504
+ function emitAgentFallbackRecord(needed, code) {
505
+ if (needed) emitSupportRecord(code || 1);
506
+ }
507
+
266
508
  function runAgent(fingerprint) {
267
509
  if (closing || !agentEnabled) return;
268
510
  if (agent) {
@@ -273,6 +515,7 @@ function runAgent(fingerprint) {
273
515
  return;
274
516
  }
275
517
  agentFingerprint = fingerprint;
518
+ const agentStarted = performance.now();
276
519
  const child = spawn(
277
520
  process.execPath,
278
521
  [resolve(root, 'scripts/generate-summaries.mjs'), ...agentArgs],
@@ -284,17 +527,32 @@ function runAgent(fingerprint) {
284
527
  if (settled) return;
285
528
  settled = true;
286
529
  const finishedFingerprint = agentFingerprint;
287
- if (agent === child) agent = undefined;
288
- agentFingerprint = finishedFingerprint;
289
- if (!closing && (error || code || signal)) {
290
- if (error) console.error(error.message);
291
- console.error(
292
- 'The coding agent could not write notes. The diff page will stay open.',
293
- );
294
- }
295
- const latest = queuedFingerprint || snapshotFingerprint();
296
- queuedFingerprint = undefined;
297
- if (latest && latest !== finishedFingerprint) scheduleAgent(latest);
530
+ const pendingFingerprint = queuedFingerprint;
531
+ const superseded = agentRunSuperseded(
532
+ pendingFingerprint,
533
+ finishedFingerprint,
534
+ );
535
+ const needsFallbackRecord = agentFallbackRecordNeeded({
536
+ closing,
537
+ queuedFingerprint: pendingFingerprint,
538
+ error,
539
+ signal,
540
+ });
541
+ recordAgentFallbackStage(needsFallbackRecord, agentStarted);
542
+ releaseAgentChild(child);
543
+ recordAgentRunResult({
544
+ closing,
545
+ code,
546
+ error,
547
+ signal,
548
+ superseded,
549
+ finishedFingerprint,
550
+ });
551
+ scheduleNextAgentFingerprint({
552
+ pendingFingerprint,
553
+ finishedFingerprint,
554
+ });
555
+ emitAgentFallbackRecord(needsFallbackRecord, code);
298
556
  };
299
557
  child.on('error', (error) => finish(1, undefined, error));
300
558
  child.on('exit', (code, signal) => finish(code, signal));
@@ -303,16 +561,28 @@ function runAgent(fingerprint) {
303
561
  function scheduleAgent(fingerprint) {
304
562
  const state = snapshotState();
305
563
  const selectedFingerprint = fingerprint || state?.fingerprint;
564
+ failedAgentFingerprint = failedAgentRunForFingerprint(
565
+ failedAgentFingerprint,
566
+ selectedFingerprint,
567
+ );
306
568
  if (
307
569
  !cli.forceSummaryRegeneration &&
308
570
  !agentFingerprint &&
309
571
  state?.hasCurrentAgentNotes &&
310
572
  selectedFingerprint === state.fingerprint
311
573
  ) {
312
- agentFingerprint = selectedFingerprint;
574
+ console.log('Reusing current agent notes.');
575
+ return;
576
+ }
577
+ if (
578
+ !agentRunNeeded(selectedFingerprint, {
579
+ activeFingerprint: agentFingerprint,
580
+ completedFingerprint: completedAgentFingerprint,
581
+ failedFingerprint: failedAgentFingerprint,
582
+ })
583
+ ) {
313
584
  return;
314
585
  }
315
- if (!selectedFingerprint || selectedFingerprint === agentFingerprint) return;
316
586
  if (agent) {
317
587
  if (selectedFingerprint !== agentFingerprint) {
318
588
  queuedFingerprint = selectedFingerprint;
@@ -325,17 +595,44 @@ function scheduleAgent(fingerprint) {
325
595
  agentTimer = setTimeout(() => runAgent(selectedFingerprint), delay);
326
596
  }
327
597
 
598
+ function markSnapshotReady() {
599
+ if (snapshotReady) return;
600
+ snapshotReady = true;
601
+ try {
602
+ supportRecorder?.addBytes('snapshot', statSync(outputPath).size);
603
+ } catch {}
604
+ supportRecorder?.addStage(
605
+ 'snapshot',
606
+ performance.now() - snapshotStarted,
607
+ );
608
+ }
609
+
610
+ function isSnapshotLine(line) {
611
+ return line.startsWith('Wrote ') || line === 'No diff-data changes';
612
+ }
613
+
614
+ function startSnapshotDependents() {
615
+ markSnapshotReady();
616
+ startSite();
617
+ if (agentEnabled) scheduleAgent();
618
+ }
619
+
620
+ function shouldPrintFeedLine(line) {
621
+ return line !== 'No diff-data changes' || !agentEnabled;
622
+ }
623
+
624
+ function handleFeedLine(line) {
625
+ if (isSnapshotLine(line)) startSnapshotDependents();
626
+ if (shouldPrintFeedLine(line)) console.log(line);
627
+ }
628
+
328
629
  if (feed.stdout) {
329
630
  const feedLines = createInterface({ input: feed.stdout });
330
- feedLines.on('line', (line) => {
331
- const snapshotReady =
332
- line.startsWith('Wrote ') || line === 'No diff-data changes';
333
- if (snapshotReady) {
334
- startSite();
335
- if (agentEnabled) scheduleAgent();
336
- }
337
- if (line !== 'No diff-data changes' || !agentEnabled) console.log(line);
338
- });
631
+ feedLines.on('line', handleFeedLine);
632
+ }
633
+
634
+ function stopChild(child) {
635
+ if (child && !child.killed) child.kill('SIGTERM');
339
636
  }
340
637
 
341
638
  function stop(code = 0) {
@@ -343,18 +640,30 @@ function stop(code = 0) {
343
640
  closing = true;
344
641
  clearTimeout(browserOpenTimer);
345
642
  clearTimeout(agentTimer);
346
- if (!feed.killed) feed.kill('SIGTERM');
347
- if (site && !site.killed) site.kill('SIGTERM');
348
- if (agent && !agent.killed) agent.kill('SIGTERM');
643
+ [feed, site, agent].forEach(stopChild);
349
644
  process.exitCode = code;
350
645
  }
351
646
 
352
647
  feed.on('exit', (code, signal) => {
353
- if (!closing && (code || signal)) stop(code || 1);
648
+ if (!closing && (code || signal)) {
649
+ supportRecorder?.addStage(
650
+ 'snapshot',
651
+ performance.now() - snapshotStarted,
652
+ 'failed',
653
+ );
654
+ emitSupportRecord(code || 1);
655
+ stop(code || 1);
656
+ }
354
657
  });
355
658
  feed.on('error', (error) => {
356
659
  if (!closing) {
660
+ supportRecorder?.addStage(
661
+ 'snapshot',
662
+ performance.now() - snapshotStarted,
663
+ 'failed',
664
+ );
357
665
  console.error(`Could not start the diff watcher: ${error.message}`);
666
+ emitSupportRecord();
358
667
  stop(1);
359
668
  }
360
669
  });