diffsplain 0.9.0 → 0.11.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.
- package/README.md +21 -4
- package/dist/assets/{angular-html-m_m91ZYB.js → angular-html-CjYwnGyV.js} +1 -1
- package/dist/assets/{angular-ts-CO5Z--Rb.js → angular-ts-DSPf2G4O.js} +1 -1
- package/dist/assets/{apl-ogYYgLUu.js → apl-B9ytghJx.js} +1 -1
- package/dist/assets/{astro-DAfY3cNR.js → astro-BsGV0yBF.js} +1 -1
- package/dist/assets/{blade-E6RzsLUV.js → blade-Cc2eCYlt.js} +1 -1
- package/dist/assets/{c-kKdxhtHF.js → c-CWKyCnUx.js} +1 -1
- package/dist/assets/{cobol-3oDDyFNf.js → cobol-DkamOI1E.js} +1 -1
- package/dist/assets/{coffee-dsiILuWe.js → coffee-DyaEXePe.js} +1 -1
- package/dist/assets/{cpp-DYyNFq__.js → cpp-16-d2Eyo.js} +1 -1
- package/dist/assets/{crystal-B7dVp9TF.js → crystal-CavPPRhQ.js} +1 -1
- package/dist/assets/{css-DlxJ0aex.js → css-BME_wHJq.js} +1 -1
- package/dist/assets/{edge-BjEVphkC.js → edge-DeVi2hvK.js} +1 -1
- package/dist/assets/{elixir-0xTOtMcM.js → elixir-CCWgH_vc.js} +1 -1
- package/dist/assets/{elm-iQk_Wrxq.js → elm-CZ570J7x.js} +1 -1
- package/dist/assets/{erb-BO74ztiE.js → erb-CDPT5bea.js} +1 -1
- package/dist/assets/{git-rebase-BN667QRY.js → git-rebase-Dq5Z37DJ.js} +1 -1
- package/dist/assets/{glimmer-js-DTGUIHeS.js → glimmer-js-BoPVZK8p.js} +1 -1
- package/dist/assets/{glimmer-ts-BU3oQn2d.js → glimmer-ts-DazEWfg1.js} +1 -1
- package/dist/assets/{glsl-KLiqLJi2.js → glsl-DCUjYCZY.js} +1 -1
- package/dist/assets/{graphql-D_idcsoJ.js → graphql-DkCeSxy7.js} +1 -1
- package/dist/assets/{hack-BXlsgPIr.js → hack-tiY3l-W-.js} +1 -1
- package/dist/assets/{haml-fP0aUg4-.js → haml-DyQZrJg_.js} +1 -1
- package/dist/assets/{handlebars-C1ZEaot-.js → handlebars-Bvu5Rr0t.js} +1 -1
- package/dist/assets/{html-D04qIuup.js → html-C60VFgKW.js} +1 -1
- package/dist/assets/{html-derivative-C6ai-9QL.js → html-derivative-DB69gpnL.js} +1 -1
- package/dist/assets/{http-C7bz3ZvQ.js → http-DmS8hddd.js} +1 -1
- package/dist/assets/{hurl-jL6B6Ahr.js → hurl-Cdp6lmUx.js} +1 -1
- package/dist/assets/{index-BPaJGi-m.js → index-CUiLG0r9.js} +4 -4
- package/dist/assets/{index-VAyBpb3u.css → index-YnLIxcTm.css} +1 -1
- package/dist/assets/{java-BVzvLCON.js → java-CkOnwoVQ.js} +1 -1
- package/dist/assets/{javascript-UDbZHxCI.js → javascript-D2tF_Ylj.js} +1 -1
- package/dist/assets/{jinja-DB-QKo2A.js → jinja-CdWpLcff.js} +1 -1
- package/dist/assets/{jison-B1ZbR2FF.js → jison-BII58uuR.js} +1 -1
- package/dist/assets/{json-DV4WwJzT.js → json-BKlPRLrU.js} +1 -1
- package/dist/assets/{jsx-DS6_VTvW.js → jsx-Ba5UouxN.js} +1 -1
- package/dist/assets/{julia-C4tevET0.js → julia-CwdQF8Ff.js} +1 -1
- package/dist/assets/{just-CWMjefzB.js → just-Be3sr1-h.js} +1 -1
- package/dist/assets/{latex-tyEFf_Af.js → latex-CDF6t93N.js} +1 -1
- package/dist/assets/{liquid-C3d1X1rL.js → liquid-BE3LVHkw.js} +1 -1
- package/dist/assets/{lua-sXvrzLQY.js → lua-D_W8Ux9S.js} +1 -1
- package/dist/assets/{marko-CQweucif.js → marko-BeUfMD-U.js} +1 -1
- package/dist/assets/{mdc-TrxLWF7s.js → mdc-C8tRJlVU.js} +1 -1
- package/dist/assets/{nginx-ChE2nSNJ.js → nginx-CLPimMM6.js} +1 -1
- package/dist/assets/{nim-BY1CTRfx.js → nim-ikQhXKQf.js} +1 -1
- package/dist/assets/{perl-Cvj2PQQH.js → perl-C27cToFa.js} +1 -1
- package/dist/assets/{php-CGmHp8HW.js → php-Btdg-RP3.js} +1 -1
- package/dist/assets/{pug-DM4S0bRr.js → pug-Bk73ERXL.js} +1 -1
- package/dist/assets/{qml-DI89Al9K.js → qml-DhY7FDIl.js} +1 -1
- package/dist/assets/{r-Bs5_4yYh.js → r-CEwua7nj.js} +1 -1
- package/dist/assets/{razor-DimzayhZ.js → razor-sPO7Sh9d.js} +1 -1
- package/dist/assets/{regexp-BLACz0cx.js → regexp-D9DLFGXE.js} +1 -1
- package/dist/assets/{rst-DcWzxQh2.js → rst-YhauCnId.js} +1 -1
- package/dist/assets/{ruby-Dlat5E8d.js → ruby-DpMkRaoz.js} +1 -1
- package/dist/assets/{sas-CCDmMaBY.js → sas-0KdmKGRO.js} +1 -1
- package/dist/assets/{scss-BPiU2nu6.js → scss-CohvHWvq.js} +1 -1
- package/dist/assets/{shellscript-CKinVNrN.js → shellscript-C93Q8fLt.js} +1 -1
- package/dist/assets/{shellsession-Cea8Pu5d.js → shellsession-YCh2AQXu.js} +1 -1
- package/dist/assets/{soy--BRAPY4-.js → soy-CWVYdXUH.js} +1 -1
- package/dist/assets/{sql-DjKiCyWK.js → sql-CJNtax5I.js} +1 -1
- package/dist/assets/{stata-CDvo3J6y.js → stata-C7v41XAM.js} +1 -1
- package/dist/assets/{surrealql-C7b7E5sh.js → surrealql-CRUSOxDf.js} +1 -1
- package/dist/assets/{svelte-DO0tKtiK.js → svelte-Z0harRr1.js} +1 -1
- package/dist/assets/{templ-DhhmR_rv.js → templ-BplDouJX.js} +1 -1
- package/dist/assets/{tex-Cn_V-Gg4.js → tex-MsB6StN5.js} +1 -1
- package/dist/assets/{ts-tags-B6c-FTmA.js → ts-tags-BWx8aq5t.js} +1 -1
- package/dist/assets/{tsx-Cvtt2sJ-.js → tsx-DBT1gmNl.js} +1 -1
- package/dist/assets/{twig-B7ZbCUmB.js → twig-jz67rR3C.js} +1 -1
- package/dist/assets/{typescript-i3C6um2B.js → typescript-CzwGN36B.js} +1 -1
- package/dist/assets/{vue-html-D362KpK3.js → vue-html-Dhrmu7sO.js} +1 -1
- package/dist/assets/{vue-vine-D34cBbwO.js → vue-vine-BfQGXZs6.js} +1 -1
- package/dist/assets/{vue-DyPX0Vum.js → vue-xsrSiVMC.js} +1 -1
- package/dist/assets/{xml-DJEk7u4n.js → xml-W7cP4qeN.js} +1 -1
- package/dist/assets/{xsl-CoTtf90f.js → xsl-BlXf8qm_.js} +1 -1
- package/dist/assets/{yaml-Cv5epQ4-.js → yaml-BtNwxUT9.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +4 -2
- package/scripts/agent-config.mjs +184 -0
- package/scripts/agent-usage.mjs +141 -0
- package/scripts/build-diff-data.mjs +32 -12
- package/scripts/cli-args.mjs +36 -2
- package/scripts/coding-agents.mjs +265 -13
- package/scripts/generate-summaries.mjs +72 -13
- package/scripts/present.mjs +156 -58
- package/scripts/review-chat-context.mjs +6 -4
- package/scripts/review-chat-controller.mjs +142 -17
- package/scripts/review-chat-provider.mjs +40 -7
- package/scripts/serve-built.mjs +139 -7
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { performance } from 'node:perf_hooks';
|
|
2
3
|
import {
|
|
3
4
|
ReviewChatError,
|
|
4
5
|
chatInputLimit,
|
|
@@ -21,6 +22,45 @@ import {
|
|
|
21
22
|
|
|
22
23
|
const noChange = () => {};
|
|
23
24
|
const snapshotOnly = { mode: 'snapshot-only' };
|
|
25
|
+
const defaultRunTimeoutMs = 120_000;
|
|
26
|
+
const maximumProgressIntervalMs = 30_000;
|
|
27
|
+
|
|
28
|
+
function runTimeout(options) {
|
|
29
|
+
const timeoutMs = options.runTimeoutMs ?? defaultRunTimeoutMs;
|
|
30
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
|
|
31
|
+
throw new Error('Review chat run timeout must be a positive integer');
|
|
32
|
+
}
|
|
33
|
+
return timeoutMs;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function progressInterval(timeoutMs) {
|
|
37
|
+
return Math.min(maximumProgressIntervalMs, Math.max(10, Math.floor(timeoutMs / 2)));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function timeoutError(timeoutMs) {
|
|
41
|
+
const duration = timeoutMs >= 1_000
|
|
42
|
+
? `${Math.round(timeoutMs / 1_000)} seconds`
|
|
43
|
+
: `${timeoutMs}ms`;
|
|
44
|
+
return new Error(`The chat provider timed out after ${duration}.`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function lifecycleError(error) {
|
|
48
|
+
return typeof error?.lifecycleMessage === 'string'
|
|
49
|
+
? error.lifecycleMessage
|
|
50
|
+
: publicError(error);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function compactedMessages(run, answer) {
|
|
54
|
+
return [{ role: 'compacted', answer }, ...run.preserved];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function oversizedCompaction(thread, run, answer, inputLimitBytes) {
|
|
58
|
+
if (run.kind !== 'compact') return false;
|
|
59
|
+
return inputBytes(
|
|
60
|
+
compactedMessages(run, answer),
|
|
61
|
+
thread.pendingQuestion,
|
|
62
|
+
) > inputLimitBytes;
|
|
63
|
+
}
|
|
24
64
|
function snapshotRead(path) {
|
|
25
65
|
try {
|
|
26
66
|
return { value: JSON.parse(readFileSync(path, 'utf8')) };
|
|
@@ -90,9 +130,9 @@ function validatedAnswer(value, paths) {
|
|
|
90
130
|
}
|
|
91
131
|
}
|
|
92
132
|
|
|
93
|
-
function executionResult(provider, input) {
|
|
133
|
+
function executionResult(provider, input, context) {
|
|
94
134
|
try {
|
|
95
|
-
return { execution: providerExecution(provider, input) };
|
|
135
|
+
return { execution: providerExecution(provider, input, context) };
|
|
96
136
|
} catch (error) {
|
|
97
137
|
return { error };
|
|
98
138
|
}
|
|
@@ -133,6 +173,10 @@ export class ReviewChatController {
|
|
|
133
173
|
this.snapshotPath = chatSnapshotPath(options);
|
|
134
174
|
this.provider = options.provider;
|
|
135
175
|
this.onChange = typeof options.onChange === 'function' ? options.onChange : noChange;
|
|
176
|
+
this.onLifecycle = typeof options.onLifecycle === 'function'
|
|
177
|
+
? options.onLifecycle
|
|
178
|
+
: noChange;
|
|
179
|
+
this.runTimeoutMs = runTimeout(options);
|
|
136
180
|
this.inputLimitBytes = chatInputLimit(options);
|
|
137
181
|
this.accessMode = options.accessMode || snapshotOnly;
|
|
138
182
|
this.threads = new Map();
|
|
@@ -237,6 +281,53 @@ export class ReviewChatController {
|
|
|
237
281
|
thread.activeRunId = undefined;
|
|
238
282
|
}
|
|
239
283
|
|
|
284
|
+
elapsed(run) {
|
|
285
|
+
return Math.max(0, Math.round(performance.now() - run.startedAt));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
emitLifecycleSafely(type, run, details = {}) {
|
|
289
|
+
try {
|
|
290
|
+
this.onLifecycle({
|
|
291
|
+
type,
|
|
292
|
+
kind: run.kind,
|
|
293
|
+
runId: run.id,
|
|
294
|
+
elapsedMs: this.elapsed(run),
|
|
295
|
+
terminal: details.terminal === true,
|
|
296
|
+
...(details.error ? { error: lifecycleError(details.error) } : {}),
|
|
297
|
+
});
|
|
298
|
+
} catch {}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
clearSupervision(run) {
|
|
302
|
+
if (run.timeoutTimer) clearTimeout(run.timeoutTimer);
|
|
303
|
+
if (run.progressTimer) clearInterval(run.progressTimer);
|
|
304
|
+
run.timeoutTimer = undefined;
|
|
305
|
+
run.progressTimer = undefined;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
finishLifecycle(run, type, error) {
|
|
309
|
+
if (run.lifecycle !== 'active') return false;
|
|
310
|
+
run.lifecycle = type;
|
|
311
|
+
this.clearSupervision(run);
|
|
312
|
+
this.emitLifecycleSafely(type, run, { terminal: true, error });
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
supervise(thread, run) {
|
|
317
|
+
run.lifecycle = 'active';
|
|
318
|
+
run.startedAt = performance.now();
|
|
319
|
+
this.emitLifecycleSafely('start', run);
|
|
320
|
+
run.progressTimer = setInterval(() => {
|
|
321
|
+
if (run.lifecycle === 'active') this.emitLifecycleSafely('progress', run);
|
|
322
|
+
}, progressInterval(this.runTimeoutMs));
|
|
323
|
+
run.progressTimer.unref?.();
|
|
324
|
+
run.timeoutTimer = setTimeout(
|
|
325
|
+
() => this.timeoutRun(thread, run),
|
|
326
|
+
this.runTimeoutMs,
|
|
327
|
+
);
|
|
328
|
+
run.timeoutTimer.unref?.();
|
|
329
|
+
}
|
|
330
|
+
|
|
240
331
|
markFailure(thread, run, error) {
|
|
241
332
|
this.clearRun(thread);
|
|
242
333
|
thread.error = publicError(error);
|
|
@@ -244,7 +335,9 @@ export class ReviewChatController {
|
|
|
244
335
|
}
|
|
245
336
|
|
|
246
337
|
markUnavailableRun(thread, run) {
|
|
247
|
-
|
|
338
|
+
const error = new Error('The current review snapshot is not available.');
|
|
339
|
+
if (!this.finishLifecycle(run, 'failure', error)) return;
|
|
340
|
+
this.markFailure(thread, run, error);
|
|
248
341
|
this.refresh({ notify: false });
|
|
249
342
|
this.notify();
|
|
250
343
|
}
|
|
@@ -266,6 +359,7 @@ export class ReviewChatController {
|
|
|
266
359
|
|
|
267
360
|
finishFailure(thread, run, error) {
|
|
268
361
|
if (this.fenceChangedReview(thread, run)) return;
|
|
362
|
+
if (!this.finishLifecycle(run, 'failure', error)) return;
|
|
269
363
|
this.markFailure(thread, run, error);
|
|
270
364
|
this.notify();
|
|
271
365
|
}
|
|
@@ -281,15 +375,7 @@ export class ReviewChatController {
|
|
|
281
375
|
}
|
|
282
376
|
|
|
283
377
|
completeCompaction(thread, run, answer) {
|
|
284
|
-
const messages =
|
|
285
|
-
if (inputBytes(messages, thread.pendingQuestion) > this.inputLimitBytes) {
|
|
286
|
-
this.finishFailure(
|
|
287
|
-
thread,
|
|
288
|
-
run,
|
|
289
|
-
new Error('The compacted chat history still exceeds the input limit.'),
|
|
290
|
-
);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
378
|
+
const messages = compactedMessages(run, answer);
|
|
293
379
|
this.clearRun(thread);
|
|
294
380
|
thread.messages = messages;
|
|
295
381
|
thread.needsCompaction = false;
|
|
@@ -309,6 +395,20 @@ export class ReviewChatController {
|
|
|
309
395
|
this.finishFailure(thread, run, result.error);
|
|
310
396
|
return;
|
|
311
397
|
}
|
|
398
|
+
if (oversizedCompaction(
|
|
399
|
+
thread,
|
|
400
|
+
run,
|
|
401
|
+
result.answer,
|
|
402
|
+
this.inputLimitBytes,
|
|
403
|
+
)) {
|
|
404
|
+
this.finishFailure(
|
|
405
|
+
thread,
|
|
406
|
+
run,
|
|
407
|
+
new Error('The compacted chat history still exceeds the input limit.'),
|
|
408
|
+
);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (!this.finishLifecycle(run, 'complete')) return;
|
|
312
412
|
this.completeRun(thread, run, result.answer);
|
|
313
413
|
}
|
|
314
414
|
|
|
@@ -323,7 +423,10 @@ export class ReviewChatController {
|
|
|
323
423
|
thread.status = run.kind === 'compact' ? 'compacting' : 'running';
|
|
324
424
|
thread.error = undefined;
|
|
325
425
|
this.notify();
|
|
326
|
-
|
|
426
|
+
this.supervise(thread, run);
|
|
427
|
+
const result = executionResult(this.provider, input, {
|
|
428
|
+
reviewFingerprint: run.fingerprint,
|
|
429
|
+
});
|
|
327
430
|
if (result.error) {
|
|
328
431
|
this.finishFailure(thread, run, result.error);
|
|
329
432
|
return;
|
|
@@ -401,13 +504,35 @@ export class ReviewChatController {
|
|
|
401
504
|
}
|
|
402
505
|
}
|
|
403
506
|
|
|
507
|
+
timeoutRun(thread, run) {
|
|
508
|
+
if (!this.currentRun(thread, run)) return;
|
|
509
|
+
const error = timeoutError(this.runTimeoutMs);
|
|
510
|
+
if (!this.finishLifecycle(run, 'timeout', error)) return;
|
|
511
|
+
this.clearRun(thread);
|
|
512
|
+
this.cancelProvider(run);
|
|
513
|
+
thread.error = publicError(error);
|
|
514
|
+
Object.assign(thread, runState(run));
|
|
515
|
+
this.notify();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
prepareCancellation(thread, run) {
|
|
519
|
+
if (!this.finishLifecycle(run, 'cancel')) return false;
|
|
520
|
+
this.clearRun(thread);
|
|
521
|
+
this.cancelProvider(run);
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
restoreCancelledState(thread, run, stale) {
|
|
526
|
+
if (stale) return;
|
|
527
|
+
Object.assign(thread, cancelledState(run));
|
|
528
|
+
thread.error = undefined;
|
|
529
|
+
}
|
|
530
|
+
|
|
404
531
|
cancelRun(thread, { stale = false } = {}) {
|
|
405
532
|
const run = thread.run;
|
|
406
533
|
if (!run) return false;
|
|
407
|
-
this.
|
|
408
|
-
this.
|
|
409
|
-
if (!stale) Object.assign(thread, cancelledState(run));
|
|
410
|
-
if (!stale) thread.error = undefined;
|
|
534
|
+
if (!this.prepareCancellation(thread, run)) return false;
|
|
535
|
+
this.restoreCancelledState(thread, run, stale);
|
|
411
536
|
return true;
|
|
412
537
|
}
|
|
413
538
|
|
|
@@ -9,6 +9,7 @@ import { join } from 'node:path';
|
|
|
9
9
|
import {
|
|
10
10
|
agentCommand,
|
|
11
11
|
parseAgentResponse,
|
|
12
|
+
parseAgentUsage,
|
|
12
13
|
} from './coding-agents.mjs';
|
|
13
14
|
|
|
14
15
|
const maximumProviderOutputBytes = 1024 * 1024;
|
|
@@ -41,10 +42,21 @@ function childOutput(chunks) {
|
|
|
41
42
|
return Buffer.concat(chunks).toString('utf8');
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
function
|
|
45
|
+
function childStatus(status, signal) {
|
|
46
|
+
return status ?? signal;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function childDiagnosticSuffix(stdout, stderr) {
|
|
45
50
|
const detail = stderr.trim() || stdout.trim();
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
return detail ? `: ${detail.slice(-600)}` : '';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function childFailure(agent, status, signal, stdout, stderr) {
|
|
55
|
+
const observedStatus = childStatus(status, signal);
|
|
56
|
+
const suffix = childDiagnosticSuffix(stdout, stderr);
|
|
57
|
+
const error = new Error(`${agent} exited with status ${observedStatus}${suffix}`);
|
|
58
|
+
error.lifecycleMessage = `${agent} exited with status ${observedStatus}`;
|
|
59
|
+
return error;
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
function aborted(signal) {
|
|
@@ -88,8 +100,10 @@ function providerOptions(options) {
|
|
|
88
100
|
binary: providerBinary(options, agent),
|
|
89
101
|
model: options.model,
|
|
90
102
|
reasoning: options.reasoning,
|
|
103
|
+
fast: options.fast === true,
|
|
91
104
|
accessMode: options.accessMode,
|
|
92
105
|
env: providerEnvironment(options),
|
|
106
|
+
onUsage: typeof options.onUsage === 'function' ? options.onUsage : noChange,
|
|
93
107
|
};
|
|
94
108
|
}
|
|
95
109
|
|
|
@@ -113,6 +127,7 @@ function providerInvocation(options, request, paths) {
|
|
|
113
127
|
binary: options.binary,
|
|
114
128
|
model: options.model,
|
|
115
129
|
reasoning: options.reasoning,
|
|
130
|
+
fast: options.fast,
|
|
116
131
|
prompt: request.prompt,
|
|
117
132
|
schema: request.responseSchema,
|
|
118
133
|
schemaPath: paths.schemaPath,
|
|
@@ -134,12 +149,15 @@ function preparedProviderRequest(options, request) {
|
|
|
134
149
|
}
|
|
135
150
|
|
|
136
151
|
class ChildExecution {
|
|
137
|
-
constructor({ invocation, input, agent, signal, temporary }) {
|
|
152
|
+
constructor({ invocation, input, agent, signal, temporary, onUsage, usageContext }) {
|
|
138
153
|
this.invocation = invocation;
|
|
139
154
|
this.input = input;
|
|
140
155
|
this.agent = agent;
|
|
141
156
|
this.signal = signal;
|
|
142
157
|
this.temporary = temporary;
|
|
158
|
+
this.onUsage = onUsage;
|
|
159
|
+
this.usageContext = usageContext;
|
|
160
|
+
this.usageReported = false;
|
|
143
161
|
this.child = undefined;
|
|
144
162
|
this.settled = false;
|
|
145
163
|
this.closed = false;
|
|
@@ -266,11 +284,23 @@ class ChildExecution {
|
|
|
266
284
|
finishClose() {
|
|
267
285
|
if (this.closed) return;
|
|
268
286
|
this.closed = true;
|
|
287
|
+
this.reportUsage();
|
|
269
288
|
this.clearForceKill();
|
|
270
289
|
removeTemporary(this.temporary);
|
|
271
290
|
this.resolveFinished();
|
|
272
291
|
}
|
|
273
292
|
|
|
293
|
+
reportUsage() {
|
|
294
|
+
if (this.usageReported) return;
|
|
295
|
+
this.usageReported = true;
|
|
296
|
+
try {
|
|
297
|
+
this.onUsage({
|
|
298
|
+
...this.usageContext,
|
|
299
|
+
usage: parseAgentUsage(this.agent, childOutput(this.stdout)),
|
|
300
|
+
});
|
|
301
|
+
} catch {}
|
|
302
|
+
}
|
|
303
|
+
|
|
274
304
|
resolve(value) {
|
|
275
305
|
this.settle(this.resolvePromise, value);
|
|
276
306
|
}
|
|
@@ -289,6 +319,7 @@ class ChildExecution {
|
|
|
289
319
|
|
|
290
320
|
closeChild(status, signal) {
|
|
291
321
|
this.clearForceKill();
|
|
322
|
+
this.reportUsage();
|
|
292
323
|
if (!this.settled) {
|
|
293
324
|
const error = closeError(this, status, signal);
|
|
294
325
|
if (error) this.reject(error);
|
|
@@ -314,7 +345,7 @@ function trackExecution(executions, execution) {
|
|
|
314
345
|
return execution;
|
|
315
346
|
}
|
|
316
347
|
|
|
317
|
-
function runRequest(options, executions, request) {
|
|
348
|
+
function runRequest(options, executions, request, usageContext = {}) {
|
|
318
349
|
const prepared = preparedProviderRequest(options, request);
|
|
319
350
|
const controller = new AbortController();
|
|
320
351
|
const execution = providerChild({
|
|
@@ -323,6 +354,8 @@ function runRequest(options, executions, request) {
|
|
|
323
354
|
agent: options.agent,
|
|
324
355
|
signal: controller.signal,
|
|
325
356
|
temporary: prepared.temporary,
|
|
357
|
+
onUsage: options.onUsage,
|
|
358
|
+
usageContext,
|
|
326
359
|
});
|
|
327
360
|
const tracked = trackExecution(executions, execution);
|
|
328
361
|
return {
|
|
@@ -342,8 +375,8 @@ export function createCodingAgentChatProvider(options = {}) {
|
|
|
342
375
|
const configured = providerOptions(options);
|
|
343
376
|
const executions = new Set();
|
|
344
377
|
return {
|
|
345
|
-
run(request) {
|
|
346
|
-
return runRequest(configured, executions, request);
|
|
378
|
+
run(request, context) {
|
|
379
|
+
return runRequest(configured, executions, request, context);
|
|
347
380
|
},
|
|
348
381
|
close() {
|
|
349
382
|
closeExecutions(executions);
|
package/scripts/serve-built.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
4
|
-
import { watchFile, unwatchFile } from 'node:fs';
|
|
3
|
+
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
4
|
+
import { readFileSync, watchFile, unwatchFile } from 'node:fs';
|
|
5
5
|
import { readFile, stat } from 'node:fs/promises';
|
|
6
6
|
import { createServer } from 'node:http';
|
|
7
7
|
import { dirname, extname, resolve, sep } from 'node:path';
|
|
@@ -11,6 +11,14 @@ import {
|
|
|
11
11
|
createCodingAgentChatProvider,
|
|
12
12
|
createReviewChat,
|
|
13
13
|
} from './review-chat.mjs';
|
|
14
|
+
import { publicError } from './review-chat-context.mjs';
|
|
15
|
+
import {
|
|
16
|
+
emptyUsageAccumulator,
|
|
17
|
+
formatReviewUsage,
|
|
18
|
+
recordUsage,
|
|
19
|
+
reviewUsage,
|
|
20
|
+
usageSummary,
|
|
21
|
+
} from './agent-usage.mjs';
|
|
14
22
|
|
|
15
23
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
24
|
const clientRoot = resolve(root, 'dist');
|
|
@@ -37,6 +45,7 @@ const chatAgent = option('--chat-agent');
|
|
|
37
45
|
const chatBinary = option('--chat-binary', chatAgent);
|
|
38
46
|
const chatModel = option('--chat-model');
|
|
39
47
|
const chatReasoning = option('--chat-reasoning');
|
|
48
|
+
const chatFast = rawArgs.includes('--chat-fast');
|
|
40
49
|
const chatAccessMode = option('--chat-access-mode', 'snapshot-only');
|
|
41
50
|
const chatAccessRoot = option('--chat-access-root');
|
|
42
51
|
if (!/^\d+$/.test(portValue) || Number(portValue) > 65_535) {
|
|
@@ -265,7 +274,7 @@ function matchingAccess(value, expectedValue) {
|
|
|
265
274
|
|
|
266
275
|
async function fetchAsset(url) {
|
|
267
276
|
if (url.pathname === '/diff-data.json') {
|
|
268
|
-
return
|
|
277
|
+
return liveSnapshotResponse();
|
|
269
278
|
}
|
|
270
279
|
const pathname = decodeURIComponent(url.pathname);
|
|
271
280
|
const file = resolve(
|
|
@@ -278,6 +287,44 @@ async function fetchAsset(url) {
|
|
|
278
287
|
return fileResponse(file);
|
|
279
288
|
}
|
|
280
289
|
|
|
290
|
+
async function parsedLiveSnapshot() {
|
|
291
|
+
try {
|
|
292
|
+
return JSON.parse(await readFile(output, 'utf8'));
|
|
293
|
+
} catch {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function liveSnapshotVersion(snapshot, usage) {
|
|
299
|
+
return createHash('sha256')
|
|
300
|
+
.update(JSON.stringify({ version: snapshot.version, usage }))
|
|
301
|
+
.digest('hex')
|
|
302
|
+
.slice(0, 12);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function snapshotNoteSummary(snapshot) {
|
|
306
|
+
return snapshot?.usage?.agentNotes ||
|
|
307
|
+
usageSummary(emptyUsageAccumulator());
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function snapshotResponse(snapshot, usage) {
|
|
311
|
+
return jsonResponse({
|
|
312
|
+
...snapshot,
|
|
313
|
+
version: liveSnapshotVersion(snapshot, usage),
|
|
314
|
+
usage,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function liveSnapshotResponse() {
|
|
319
|
+
const snapshot = await parsedLiveSnapshot();
|
|
320
|
+
if (!snapshot) return fileResponse(output, { live: true });
|
|
321
|
+
const fingerprint = snapshot?.notes?.reviewFingerprint;
|
|
322
|
+
if (!fingerprint) return fileResponse(output, { live: true });
|
|
323
|
+
const chatSummary = currentChatUsage(fingerprint);
|
|
324
|
+
const usage = reviewUsage(snapshotNoteSummary(snapshot), chatSummary);
|
|
325
|
+
return snapshotResponse(snapshot, usage);
|
|
326
|
+
}
|
|
327
|
+
|
|
281
328
|
function jsonResponse(value, status = 200) {
|
|
282
329
|
return webResponse(JSON.stringify(value), {
|
|
283
330
|
status,
|
|
@@ -366,6 +413,85 @@ function broadcastEvent(name, data = {}) {
|
|
|
366
413
|
}
|
|
367
414
|
}
|
|
368
415
|
|
|
416
|
+
function lifecycleKind(kind) {
|
|
417
|
+
return kind === 'compact' ? 'history compaction' : 'answer';
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function lifecycleStatus(type) {
|
|
421
|
+
return {
|
|
422
|
+
start: 'started',
|
|
423
|
+
progress: 'still running',
|
|
424
|
+
complete: 'completed',
|
|
425
|
+
failure: 'failed',
|
|
426
|
+
timeout: 'timed out',
|
|
427
|
+
cancel: 'cancelled',
|
|
428
|
+
}[type] || type;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function elapsedText(milliseconds) {
|
|
432
|
+
if (milliseconds < 1_000) return `${milliseconds}ms`;
|
|
433
|
+
return `${Math.round(milliseconds / 1_000)}s`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function logChatLifecycle(event) {
|
|
437
|
+
const elapsed = event.type === 'start' ? '' : ` (${elapsedText(event.elapsedMs)})`;
|
|
438
|
+
const error = event.error ? `: ${event.error}` : '';
|
|
439
|
+
console.log(
|
|
440
|
+
`Diffsplain chat #${event.runId}: ${lifecycleKind(event.kind)} ${lifecycleStatus(event.type)}${elapsed}${error}`,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function logObservedError(area, error) {
|
|
445
|
+
console.log(`Diffsplain ${area.replaceAll('-', ' ')} error: ${publicError(error)}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
let chatUsageFingerprint;
|
|
449
|
+
let chatUsage = emptyUsageAccumulator();
|
|
450
|
+
let reviewChat;
|
|
451
|
+
|
|
452
|
+
function snapshotUsageState() {
|
|
453
|
+
try {
|
|
454
|
+
const snapshot = JSON.parse(readFileSync(output, 'utf8'));
|
|
455
|
+
return {
|
|
456
|
+
agentNotes: snapshot.usage?.agentNotes,
|
|
457
|
+
fingerprint: snapshot.notes?.reviewFingerprint,
|
|
458
|
+
};
|
|
459
|
+
} catch {
|
|
460
|
+
return {};
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function currentChatUsage(fingerprint) {
|
|
465
|
+
return fingerprint === chatUsageFingerprint
|
|
466
|
+
? usageSummary(chatUsage)
|
|
467
|
+
: usageSummary(emptyUsageAccumulator());
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function resetChatUsage(fingerprint) {
|
|
471
|
+
if (fingerprint === chatUsageFingerprint) return;
|
|
472
|
+
chatUsageFingerprint = fingerprint;
|
|
473
|
+
chatUsage = emptyUsageAccumulator();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function recordChatUsage({ reviewFingerprint, usage }) {
|
|
477
|
+
const snapshot = snapshotUsageState();
|
|
478
|
+
if (
|
|
479
|
+
!reviewFingerprint ||
|
|
480
|
+
snapshot.fingerprint !== reviewFingerprint ||
|
|
481
|
+
reviewChat?.getState().fingerprint !== reviewFingerprint
|
|
482
|
+
) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
resetChatUsage(reviewFingerprint);
|
|
486
|
+
chatUsage = recordUsage(chatUsage, usage);
|
|
487
|
+
const totals = reviewUsage(
|
|
488
|
+
snapshot.agentNotes || usageSummary(emptyUsageAccumulator()),
|
|
489
|
+
usageSummary(chatUsage),
|
|
490
|
+
);
|
|
491
|
+
console.log(formatReviewUsage(totals));
|
|
492
|
+
broadcastEvent('update');
|
|
493
|
+
}
|
|
494
|
+
|
|
369
495
|
const chatProviderAccess = chatAccessMode === 'checkout-read-only'
|
|
370
496
|
? { mode: chatAccessMode, root: resolve(chatAccessRoot) }
|
|
371
497
|
: { mode: chatAccessMode, reason: 'target-mismatch' };
|
|
@@ -375,14 +501,17 @@ const chatProvider = chatAgent
|
|
|
375
501
|
binary: chatBinary,
|
|
376
502
|
model: chatModel,
|
|
377
503
|
reasoning: chatReasoning,
|
|
504
|
+
fast: chatFast,
|
|
378
505
|
accessMode: chatProviderAccess,
|
|
506
|
+
onUsage: recordChatUsage,
|
|
379
507
|
})
|
|
380
508
|
: undefined;
|
|
381
|
-
|
|
509
|
+
reviewChat = createReviewChat({
|
|
382
510
|
snapshotPath: chatSnapshot,
|
|
383
511
|
provider: chatProvider,
|
|
384
512
|
accessMode: chatProviderAccess,
|
|
385
513
|
onChange: () => broadcastEvent('chat'),
|
|
514
|
+
onLifecycle: logChatLifecycle,
|
|
386
515
|
});
|
|
387
516
|
|
|
388
517
|
function requestAuthorityError(request) {
|
|
@@ -473,6 +602,7 @@ async function sendChatCommand(request, response) {
|
|
|
473
602
|
const status = result.accepted ? 202 : 200;
|
|
474
603
|
await send(response, jsonResponse(result.state, status));
|
|
475
604
|
} catch (error) {
|
|
605
|
+
logObservedError('chat-command', error);
|
|
476
606
|
const failure = chatCommandError(error);
|
|
477
607
|
await send(response, jsonResponse({ error: failure.message }, failure.status));
|
|
478
608
|
}
|
|
@@ -565,9 +695,10 @@ async function containHandlerFailure(response) {
|
|
|
565
695
|
}
|
|
566
696
|
|
|
567
697
|
const server = createServer((request, response) => {
|
|
568
|
-
void handleRequest(request, response).catch(() =>
|
|
569
|
-
|
|
570
|
-
|
|
698
|
+
void handleRequest(request, response).catch((error) => {
|
|
699
|
+
logObservedError('request-handler', error);
|
|
700
|
+
return containHandlerFailure(response);
|
|
701
|
+
});
|
|
571
702
|
});
|
|
572
703
|
|
|
573
704
|
watchFile(output, { interval: 100 }, (current, previous) => {
|
|
@@ -575,6 +706,7 @@ watchFile(output, { interval: 100 }, (current, previous) => {
|
|
|
575
706
|
return;
|
|
576
707
|
}
|
|
577
708
|
reviewChat.refresh();
|
|
709
|
+
resetChatUsage(reviewChat.getState().fingerprint);
|
|
578
710
|
broadcastEvent('update');
|
|
579
711
|
});
|
|
580
712
|
|