diffsplain 0.9.0 → 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.
- package/README.md +20 -4
- package/dist/assets/{angular-html-m_m91ZYB.js → angular-html-2u-u5H-N.js} +1 -1
- package/dist/assets/{angular-ts-CO5Z--Rb.js → angular-ts-4_HVGlKp.js} +1 -1
- package/dist/assets/{apl-ogYYgLUu.js → apl-icaNWZX3.js} +1 -1
- package/dist/assets/{astro-DAfY3cNR.js → astro-HvIOE4vz.js} +1 -1
- package/dist/assets/{blade-E6RzsLUV.js → blade-BpKJV8ev.js} +1 -1
- package/dist/assets/{c-kKdxhtHF.js → c-BNvtxyRQ.js} +1 -1
- package/dist/assets/{cobol-3oDDyFNf.js → cobol-8js4hfxa.js} +1 -1
- package/dist/assets/{coffee-dsiILuWe.js → coffee-DXg_piQ9.js} +1 -1
- package/dist/assets/{cpp-DYyNFq__.js → cpp-BZDFAjyh.js} +1 -1
- package/dist/assets/{crystal-B7dVp9TF.js → crystal-aCF3ira4.js} +1 -1
- package/dist/assets/{css-DlxJ0aex.js → css-DfRtUkhE.js} +1 -1
- package/dist/assets/{edge-BjEVphkC.js → edge-vS6_AmQv.js} +1 -1
- package/dist/assets/{elixir-0xTOtMcM.js → elixir-Ce0nUHSq.js} +1 -1
- package/dist/assets/{elm-iQk_Wrxq.js → elm-DmaeyizZ.js} +1 -1
- package/dist/assets/{erb-BO74ztiE.js → erb-DD6OUyvt.js} +1 -1
- package/dist/assets/{git-rebase-BN667QRY.js → git-rebase-HbJMn7iS.js} +1 -1
- package/dist/assets/{glimmer-js-DTGUIHeS.js → glimmer-js-BRuqbNaV.js} +1 -1
- package/dist/assets/{glimmer-ts-BU3oQn2d.js → glimmer-ts-ZBpkdEEA.js} +1 -1
- package/dist/assets/{glsl-KLiqLJi2.js → glsl-CrQ4GScf.js} +1 -1
- package/dist/assets/{graphql-D_idcsoJ.js → graphql-Tn6PBdQY.js} +1 -1
- package/dist/assets/{hack-BXlsgPIr.js → hack-CoApvrbo.js} +1 -1
- package/dist/assets/{haml-fP0aUg4-.js → haml-COKq8Xjp.js} +1 -1
- package/dist/assets/{handlebars-C1ZEaot-.js → handlebars-CAna2DTA.js} +1 -1
- package/dist/assets/{html-D04qIuup.js → html-Ch4xJ7YF.js} +1 -1
- package/dist/assets/{html-derivative-C6ai-9QL.js → html-derivative-0ITK-MAI.js} +1 -1
- package/dist/assets/{http-C7bz3ZvQ.js → http-DiMWQ8Qr.js} +1 -1
- package/dist/assets/{hurl-jL6B6Ahr.js → hurl-OcXymC0J.js} +1 -1
- package/dist/assets/{index-BPaJGi-m.js → index-BmcCug2M.js} +4 -4
- package/dist/assets/{index-VAyBpb3u.css → index-CADUNiTy.css} +1 -1
- package/dist/assets/{java-BVzvLCON.js → java-CFb3qN4r.js} +1 -1
- package/dist/assets/{javascript-UDbZHxCI.js → javascript-CRmk2JrF.js} +1 -1
- package/dist/assets/{jinja-DB-QKo2A.js → jinja-Da6lDTEy.js} +1 -1
- package/dist/assets/{jison-B1ZbR2FF.js → jison-Bnivzmtu.js} +1 -1
- package/dist/assets/{json-DV4WwJzT.js → json-m8vJP_KK.js} +1 -1
- package/dist/assets/{jsx-DS6_VTvW.js → jsx-C3HX_Sbu.js} +1 -1
- package/dist/assets/{julia-C4tevET0.js → julia-Ckg-wSHA.js} +1 -1
- package/dist/assets/{just-CWMjefzB.js → just-DCjIAyvO.js} +1 -1
- package/dist/assets/{latex-tyEFf_Af.js → latex-CrYSm4pl.js} +1 -1
- package/dist/assets/{liquid-C3d1X1rL.js → liquid-MQUFSPYU.js} +1 -1
- package/dist/assets/{lua-sXvrzLQY.js → lua-DcuzJJ77.js} +1 -1
- package/dist/assets/{marko-CQweucif.js → marko-4JNmQaB4.js} +1 -1
- package/dist/assets/{mdc-TrxLWF7s.js → mdc-C_j8HvRL.js} +1 -1
- package/dist/assets/{nginx-ChE2nSNJ.js → nginx-ta3Gn1HE.js} +1 -1
- package/dist/assets/{nim-BY1CTRfx.js → nim-BwWLst9M.js} +1 -1
- package/dist/assets/{perl-Cvj2PQQH.js → perl-DfW7Uq5O.js} +1 -1
- package/dist/assets/{php-CGmHp8HW.js → php-DGf5G3Cf.js} +1 -1
- package/dist/assets/{pug-DM4S0bRr.js → pug-CWI-NaYz.js} +1 -1
- package/dist/assets/{qml-DI89Al9K.js → qml-Csblgh7U.js} +1 -1
- package/dist/assets/{r-Bs5_4yYh.js → r-QGooaGDI.js} +1 -1
- package/dist/assets/{razor-DimzayhZ.js → razor-Dp9ihLYf.js} +1 -1
- package/dist/assets/{regexp-BLACz0cx.js → regexp-DwUnLcVr.js} +1 -1
- package/dist/assets/{rst-DcWzxQh2.js → rst-BY8GIwDJ.js} +1 -1
- package/dist/assets/{ruby-Dlat5E8d.js → ruby-VgzI9htT.js} +1 -1
- package/dist/assets/{sas-CCDmMaBY.js → sas-DYLsm-0j.js} +1 -1
- package/dist/assets/{scss-BPiU2nu6.js → scss-tIFkeUQX.js} +1 -1
- package/dist/assets/{shellscript-CKinVNrN.js → shellscript-CHjuQ4Pl.js} +1 -1
- package/dist/assets/{shellsession-Cea8Pu5d.js → shellsession-Bv0S0hj9.js} +1 -1
- package/dist/assets/{soy--BRAPY4-.js → soy-D6U1tZod.js} +1 -1
- package/dist/assets/{sql-DjKiCyWK.js → sql-CGz8kjKv.js} +1 -1
- package/dist/assets/{stata-CDvo3J6y.js → stata-C9ytERUq.js} +1 -1
- package/dist/assets/{surrealql-C7b7E5sh.js → surrealql-DRfRnaYu.js} +1 -1
- package/dist/assets/{svelte-DO0tKtiK.js → svelte-wYnk_1sI.js} +1 -1
- package/dist/assets/{templ-DhhmR_rv.js → templ-DWrGVE0u.js} +1 -1
- package/dist/assets/{tex-Cn_V-Gg4.js → tex-CYTzoUJh.js} +1 -1
- package/dist/assets/{ts-tags-B6c-FTmA.js → ts-tags-1TIXEGea.js} +1 -1
- package/dist/assets/{tsx-Cvtt2sJ-.js → tsx-a3BoAoOh.js} +1 -1
- package/dist/assets/{twig-B7ZbCUmB.js → twig-e3dvNv6o.js} +1 -1
- package/dist/assets/{typescript-i3C6um2B.js → typescript-BBhnpnTf.js} +1 -1
- package/dist/assets/{vue-DyPX0Vum.js → vue-DzzVbu9q.js} +1 -1
- package/dist/assets/{vue-html-D362KpK3.js → vue-html-quBNv38K.js} +1 -1
- package/dist/assets/{vue-vine-D34cBbwO.js → vue-vine-CDuR69FE.js} +1 -1
- package/dist/assets/{xml-DJEk7u4n.js → xml-DMifOLSe.js} +1 -1
- package/dist/assets/{xsl-CoTtf90f.js → xsl-D8Ef-5HK.js} +1 -1
- package/dist/assets/{yaml-Cv5epQ4-.js → yaml-5c38gpmA.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +3 -2
- package/scripts/agent-config.mjs +184 -0
- package/scripts/cli-args.mjs +29 -2
- package/scripts/coding-agents.mjs +57 -6
- package/scripts/generate-summaries.mjs +11 -4
- package/scripts/present.mjs +140 -57
- package/scripts/review-chat-controller.mjs +137 -14
- package/scripts/review-chat-provider.mjs +14 -3
- package/scripts/serve-built.mjs +39 -3
package/scripts/present.mjs
CHANGED
|
@@ -17,9 +17,11 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
17
17
|
import { createInterface } from 'node:readline';
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
19
|
import { helpText, parseCliArgs } from './cli-args.mjs';
|
|
20
|
+
import { applyAgentConfigOperation } from './agent-config.mjs';
|
|
20
21
|
import {
|
|
21
22
|
agentReadOnlyWarning,
|
|
22
23
|
assertReasoningSupported,
|
|
24
|
+
codingAgentFromSelectionError,
|
|
23
25
|
codingAgentAvailability,
|
|
24
26
|
codingAgentBinary,
|
|
25
27
|
selectCodingAgent,
|
|
@@ -126,6 +128,22 @@ if (cli.version) {
|
|
|
126
128
|
console.log(`diffsplain ${packageJson.version}`);
|
|
127
129
|
process.exit(0);
|
|
128
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
|
+
}
|
|
129
147
|
if (cli.doctor) {
|
|
130
148
|
if (cli.doctor.deep) {
|
|
131
149
|
console.error(
|
|
@@ -263,7 +281,12 @@ if (agentEnabled) {
|
|
|
263
281
|
agentArgs.push('--provider-read-only-warning-emitted');
|
|
264
282
|
}
|
|
265
283
|
} catch (error) {
|
|
266
|
-
recordSelectedProvider(
|
|
284
|
+
recordSelectedProvider(
|
|
285
|
+
codingAgentFromSelectionError(error) ||
|
|
286
|
+
selectedAgent ||
|
|
287
|
+
cli.agent ||
|
|
288
|
+
'unknown',
|
|
289
|
+
);
|
|
267
290
|
supportRecorder?.addStage(
|
|
268
291
|
'agent',
|
|
269
292
|
performance.now() - selectionStarted,
|
|
@@ -338,9 +361,7 @@ writeFileSync(accessPath, access, { mode: 0o600 });
|
|
|
338
361
|
chmodSync(accessPath, 0o600);
|
|
339
362
|
if (agentEnabled) {
|
|
340
363
|
feedArgs.push('--ignore-summary-watch');
|
|
341
|
-
|
|
342
|
-
agentArgs.push('--snapshot', rawSnapshotPath);
|
|
343
|
-
}
|
|
364
|
+
agentArgs.push('--snapshot', rawSnapshotPath);
|
|
344
365
|
}
|
|
345
366
|
|
|
346
367
|
const snapshotStarted = performance.now();
|
|
@@ -506,59 +527,91 @@ function startSite() {
|
|
|
506
527
|
child.on('error', handleSiteError);
|
|
507
528
|
}
|
|
508
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
|
+
}
|
|
567
|
+
|
|
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
|
+
}
|
|
576
|
+
|
|
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
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
509
610
|
function snapshotState() {
|
|
510
611
|
try {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
snapshot.notes.agentReviewFingerprint ||
|
|
515
|
-
snapshot.notes.reviewFingerprint;
|
|
516
|
-
const emptyReview =
|
|
517
|
-
Array.isArray(snapshot.files) && snapshot.files.length === 0;
|
|
518
|
-
return {
|
|
519
|
-
fingerprint,
|
|
520
|
-
hasCurrentAgentNotes:
|
|
521
|
-
snapshot.notes.complete &&
|
|
522
|
-
snapshot.notes.fresh &&
|
|
523
|
-
snapshot.notes.generatedFor === fingerprint &&
|
|
524
|
-
snapshot.notes.accessMode === accessMode.mode &&
|
|
525
|
-
(emptyReview ||
|
|
526
|
-
(snapshot.notes.agent === selectedAgent &&
|
|
527
|
-
(snapshot.notes.model || null) === (cli.model || null) &&
|
|
528
|
-
(snapshot.notes.reasoning || null) ===
|
|
529
|
-
(cli.reasoning || null))),
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
const reviewData = {
|
|
533
|
-
repo: {
|
|
534
|
-
name: snapshot.repo.name,
|
|
535
|
-
base: snapshot.repo.base,
|
|
536
|
-
head: snapshot.repo.head,
|
|
537
|
-
branch: snapshot.repo.branch,
|
|
538
|
-
baseBranch: snapshot.repo.baseBranch,
|
|
539
|
-
remote: snapshot.repo.remote,
|
|
540
|
-
targetKind: snapshot.repo.target?.kind,
|
|
541
|
-
},
|
|
542
|
-
change: {
|
|
543
|
-
title: snapshot.change.title,
|
|
544
|
-
number: snapshot.change.number,
|
|
545
|
-
},
|
|
546
|
-
files: snapshot.files.map((file) => ({
|
|
547
|
-
path: file.path,
|
|
548
|
-
oldPath: file.oldPath,
|
|
549
|
-
status: file.status,
|
|
550
|
-
additions: file.additions,
|
|
551
|
-
deletions: file.deletions,
|
|
552
|
-
isBinary: file.isBinary,
|
|
553
|
-
patch: file.patch,
|
|
554
|
-
})),
|
|
555
|
-
};
|
|
556
|
-
return {
|
|
557
|
-
fingerprint: createHash('sha256')
|
|
558
|
-
.update(JSON.stringify(reviewData))
|
|
559
|
-
.digest('hex'),
|
|
560
|
-
hasCurrentAgentNotes: false,
|
|
561
|
-
};
|
|
612
|
+
return snapshotStateFromSnapshot(
|
|
613
|
+
JSON.parse(readFileSync(reviewSnapshotPath, 'utf8')),
|
|
614
|
+
);
|
|
562
615
|
} catch {
|
|
563
616
|
return undefined;
|
|
564
617
|
}
|
|
@@ -699,12 +752,16 @@ function scheduleAgent(fingerprint) {
|
|
|
699
752
|
if (agent) {
|
|
700
753
|
if (selectedFingerprint !== agentFingerprint) {
|
|
701
754
|
queuedFingerprint = selectedFingerprint;
|
|
755
|
+
console.log(
|
|
756
|
+
'Review changed while notes were generated. Restarting agent notes...',
|
|
757
|
+
);
|
|
702
758
|
agent.kill('SIGTERM');
|
|
703
759
|
}
|
|
704
760
|
return;
|
|
705
761
|
}
|
|
706
762
|
clearTimeout(agentTimer);
|
|
707
763
|
const delay = agentFingerprint ? 300 : 0;
|
|
764
|
+
console.log('Preparing agent notes...');
|
|
708
765
|
agentTimer = setTimeout(() => runAgent(selectedFingerprint), delay);
|
|
709
766
|
}
|
|
710
767
|
|
|
@@ -720,11 +777,37 @@ function markSnapshotReady() {
|
|
|
720
777
|
);
|
|
721
778
|
}
|
|
722
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
|
+
|
|
723
804
|
function seedPresentationSnapshot() {
|
|
724
|
-
const
|
|
805
|
+
const snapshot = snapshotForPresentation(
|
|
806
|
+
JSON.parse(readFileSync(rawSnapshotPath, 'utf8')),
|
|
807
|
+
);
|
|
725
808
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
726
809
|
const pendingOutput = `${outputPath}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
|
|
727
|
-
writeFileSync(pendingOutput,
|
|
810
|
+
writeFileSync(pendingOutput, `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
728
811
|
renameSync(pendingOutput, outputPath);
|
|
729
812
|
}
|
|
730
813
|
|
|
@@ -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')) };
|
|
@@ -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,6 +423,7 @@ export class ReviewChatController {
|
|
|
323
423
|
thread.status = run.kind === 'compact' ? 'compacting' : 'running';
|
|
324
424
|
thread.error = undefined;
|
|
325
425
|
this.notify();
|
|
426
|
+
this.supervise(thread, run);
|
|
326
427
|
const result = executionResult(this.provider, input);
|
|
327
428
|
if (result.error) {
|
|
328
429
|
this.finishFailure(thread, run, result.error);
|
|
@@ -401,13 +502,35 @@ export class ReviewChatController {
|
|
|
401
502
|
}
|
|
402
503
|
}
|
|
403
504
|
|
|
505
|
+
timeoutRun(thread, run) {
|
|
506
|
+
if (!this.currentRun(thread, run)) return;
|
|
507
|
+
const error = timeoutError(this.runTimeoutMs);
|
|
508
|
+
if (!this.finishLifecycle(run, 'timeout', error)) return;
|
|
509
|
+
this.clearRun(thread);
|
|
510
|
+
this.cancelProvider(run);
|
|
511
|
+
thread.error = publicError(error);
|
|
512
|
+
Object.assign(thread, runState(run));
|
|
513
|
+
this.notify();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
prepareCancellation(thread, run) {
|
|
517
|
+
if (!this.finishLifecycle(run, 'cancel')) return false;
|
|
518
|
+
this.clearRun(thread);
|
|
519
|
+
this.cancelProvider(run);
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
restoreCancelledState(thread, run, stale) {
|
|
524
|
+
if (stale) return;
|
|
525
|
+
Object.assign(thread, cancelledState(run));
|
|
526
|
+
thread.error = undefined;
|
|
527
|
+
}
|
|
528
|
+
|
|
404
529
|
cancelRun(thread, { stale = false } = {}) {
|
|
405
530
|
const run = thread.run;
|
|
406
531
|
if (!run) return false;
|
|
407
|
-
this.
|
|
408
|
-
this.
|
|
409
|
-
if (!stale) Object.assign(thread, cancelledState(run));
|
|
410
|
-
if (!stale) thread.error = undefined;
|
|
532
|
+
if (!this.prepareCancellation(thread, run)) return false;
|
|
533
|
+
this.restoreCancelledState(thread, run, stale);
|
|
411
534
|
return true;
|
|
412
535
|
}
|
|
413
536
|
|
|
@@ -41,10 +41,21 @@ function childOutput(chunks) {
|
|
|
41
41
|
return Buffer.concat(chunks).toString('utf8');
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
function
|
|
44
|
+
function childStatus(status, signal) {
|
|
45
|
+
return status ?? signal;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function childDiagnosticSuffix(stdout, stderr) {
|
|
45
49
|
const detail = stderr.trim() || stdout.trim();
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
return detail ? `: ${detail.slice(-600)}` : '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function childFailure(agent, status, signal, stdout, stderr) {
|
|
54
|
+
const observedStatus = childStatus(status, signal);
|
|
55
|
+
const suffix = childDiagnosticSuffix(stdout, stderr);
|
|
56
|
+
const error = new Error(`${agent} exited with status ${observedStatus}${suffix}`);
|
|
57
|
+
error.lifecycleMessage = `${agent} exited with status ${observedStatus}`;
|
|
58
|
+
return error;
|
|
48
59
|
}
|
|
49
60
|
|
|
50
61
|
function aborted(signal) {
|
package/scripts/serve-built.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
createCodingAgentChatProvider,
|
|
12
12
|
createReviewChat,
|
|
13
13
|
} from './review-chat.mjs';
|
|
14
|
+
import { publicError } from './review-chat-context.mjs';
|
|
14
15
|
|
|
15
16
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
17
|
const clientRoot = resolve(root, 'dist');
|
|
@@ -366,6 +367,38 @@ function broadcastEvent(name, data = {}) {
|
|
|
366
367
|
}
|
|
367
368
|
}
|
|
368
369
|
|
|
370
|
+
function lifecycleKind(kind) {
|
|
371
|
+
return kind === 'compact' ? 'history compaction' : 'answer';
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function lifecycleStatus(type) {
|
|
375
|
+
return {
|
|
376
|
+
start: 'started',
|
|
377
|
+
progress: 'still running',
|
|
378
|
+
complete: 'completed',
|
|
379
|
+
failure: 'failed',
|
|
380
|
+
timeout: 'timed out',
|
|
381
|
+
cancel: 'cancelled',
|
|
382
|
+
}[type] || type;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function elapsedText(milliseconds) {
|
|
386
|
+
if (milliseconds < 1_000) return `${milliseconds}ms`;
|
|
387
|
+
return `${Math.round(milliseconds / 1_000)}s`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function logChatLifecycle(event) {
|
|
391
|
+
const elapsed = event.type === 'start' ? '' : ` (${elapsedText(event.elapsedMs)})`;
|
|
392
|
+
const error = event.error ? `: ${event.error}` : '';
|
|
393
|
+
console.log(
|
|
394
|
+
`Diffsplain chat #${event.runId}: ${lifecycleKind(event.kind)} ${lifecycleStatus(event.type)}${elapsed}${error}`,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function logObservedError(area, error) {
|
|
399
|
+
console.log(`Diffsplain ${area.replaceAll('-', ' ')} error: ${publicError(error)}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
369
402
|
const chatProviderAccess = chatAccessMode === 'checkout-read-only'
|
|
370
403
|
? { mode: chatAccessMode, root: resolve(chatAccessRoot) }
|
|
371
404
|
: { mode: chatAccessMode, reason: 'target-mismatch' };
|
|
@@ -383,6 +416,7 @@ const reviewChat = createReviewChat({
|
|
|
383
416
|
provider: chatProvider,
|
|
384
417
|
accessMode: chatProviderAccess,
|
|
385
418
|
onChange: () => broadcastEvent('chat'),
|
|
419
|
+
onLifecycle: logChatLifecycle,
|
|
386
420
|
});
|
|
387
421
|
|
|
388
422
|
function requestAuthorityError(request) {
|
|
@@ -473,6 +507,7 @@ async function sendChatCommand(request, response) {
|
|
|
473
507
|
const status = result.accepted ? 202 : 200;
|
|
474
508
|
await send(response, jsonResponse(result.state, status));
|
|
475
509
|
} catch (error) {
|
|
510
|
+
logObservedError('chat-command', error);
|
|
476
511
|
const failure = chatCommandError(error);
|
|
477
512
|
await send(response, jsonResponse({ error: failure.message }, failure.status));
|
|
478
513
|
}
|
|
@@ -565,9 +600,10 @@ async function containHandlerFailure(response) {
|
|
|
565
600
|
}
|
|
566
601
|
|
|
567
602
|
const server = createServer((request, response) => {
|
|
568
|
-
void handleRequest(request, response).catch(() =>
|
|
569
|
-
|
|
570
|
-
|
|
603
|
+
void handleRequest(request, response).catch((error) => {
|
|
604
|
+
logObservedError('request-handler', error);
|
|
605
|
+
return containHandlerFailure(response);
|
|
606
|
+
});
|
|
571
607
|
});
|
|
572
608
|
|
|
573
609
|
watchFile(output, { interval: 100 }, (current, previous) => {
|