diffsplain 0.10.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.
Files changed (87) hide show
  1. package/README.md +1 -0
  2. package/dist/assets/{angular-html-2u-u5H-N.js → angular-html-CjYwnGyV.js} +1 -1
  3. package/dist/assets/{angular-ts-4_HVGlKp.js → angular-ts-DSPf2G4O.js} +1 -1
  4. package/dist/assets/{apl-icaNWZX3.js → apl-B9ytghJx.js} +1 -1
  5. package/dist/assets/{astro-HvIOE4vz.js → astro-BsGV0yBF.js} +1 -1
  6. package/dist/assets/{blade-BpKJV8ev.js → blade-Cc2eCYlt.js} +1 -1
  7. package/dist/assets/{c-BNvtxyRQ.js → c-CWKyCnUx.js} +1 -1
  8. package/dist/assets/{cobol-8js4hfxa.js → cobol-DkamOI1E.js} +1 -1
  9. package/dist/assets/{coffee-DXg_piQ9.js → coffee-DyaEXePe.js} +1 -1
  10. package/dist/assets/{cpp-BZDFAjyh.js → cpp-16-d2Eyo.js} +1 -1
  11. package/dist/assets/{crystal-aCF3ira4.js → crystal-CavPPRhQ.js} +1 -1
  12. package/dist/assets/{css-DfRtUkhE.js → css-BME_wHJq.js} +1 -1
  13. package/dist/assets/{edge-vS6_AmQv.js → edge-DeVi2hvK.js} +1 -1
  14. package/dist/assets/{elixir-Ce0nUHSq.js → elixir-CCWgH_vc.js} +1 -1
  15. package/dist/assets/{elm-DmaeyizZ.js → elm-CZ570J7x.js} +1 -1
  16. package/dist/assets/{erb-DD6OUyvt.js → erb-CDPT5bea.js} +1 -1
  17. package/dist/assets/{git-rebase-HbJMn7iS.js → git-rebase-Dq5Z37DJ.js} +1 -1
  18. package/dist/assets/{glimmer-js-BRuqbNaV.js → glimmer-js-BoPVZK8p.js} +1 -1
  19. package/dist/assets/{glimmer-ts-ZBpkdEEA.js → glimmer-ts-DazEWfg1.js} +1 -1
  20. package/dist/assets/{glsl-CrQ4GScf.js → glsl-DCUjYCZY.js} +1 -1
  21. package/dist/assets/{graphql-Tn6PBdQY.js → graphql-DkCeSxy7.js} +1 -1
  22. package/dist/assets/{hack-CoApvrbo.js → hack-tiY3l-W-.js} +1 -1
  23. package/dist/assets/{haml-COKq8Xjp.js → haml-DyQZrJg_.js} +1 -1
  24. package/dist/assets/{handlebars-CAna2DTA.js → handlebars-Bvu5Rr0t.js} +1 -1
  25. package/dist/assets/{html-Ch4xJ7YF.js → html-C60VFgKW.js} +1 -1
  26. package/dist/assets/{html-derivative-0ITK-MAI.js → html-derivative-DB69gpnL.js} +1 -1
  27. package/dist/assets/{http-DiMWQ8Qr.js → http-DmS8hddd.js} +1 -1
  28. package/dist/assets/{hurl-OcXymC0J.js → hurl-Cdp6lmUx.js} +1 -1
  29. package/dist/assets/{index-BmcCug2M.js → index-CUiLG0r9.js} +4 -4
  30. package/dist/assets/{index-CADUNiTy.css → index-YnLIxcTm.css} +1 -1
  31. package/dist/assets/{java-CFb3qN4r.js → java-CkOnwoVQ.js} +1 -1
  32. package/dist/assets/{javascript-CRmk2JrF.js → javascript-D2tF_Ylj.js} +1 -1
  33. package/dist/assets/{jinja-Da6lDTEy.js → jinja-CdWpLcff.js} +1 -1
  34. package/dist/assets/{jison-Bnivzmtu.js → jison-BII58uuR.js} +1 -1
  35. package/dist/assets/{json-m8vJP_KK.js → json-BKlPRLrU.js} +1 -1
  36. package/dist/assets/{jsx-C3HX_Sbu.js → jsx-Ba5UouxN.js} +1 -1
  37. package/dist/assets/{julia-Ckg-wSHA.js → julia-CwdQF8Ff.js} +1 -1
  38. package/dist/assets/{just-DCjIAyvO.js → just-Be3sr1-h.js} +1 -1
  39. package/dist/assets/{latex-CrYSm4pl.js → latex-CDF6t93N.js} +1 -1
  40. package/dist/assets/{liquid-MQUFSPYU.js → liquid-BE3LVHkw.js} +1 -1
  41. package/dist/assets/{lua-DcuzJJ77.js → lua-D_W8Ux9S.js} +1 -1
  42. package/dist/assets/{marko-4JNmQaB4.js → marko-BeUfMD-U.js} +1 -1
  43. package/dist/assets/{mdc-C_j8HvRL.js → mdc-C8tRJlVU.js} +1 -1
  44. package/dist/assets/{nginx-ta3Gn1HE.js → nginx-CLPimMM6.js} +1 -1
  45. package/dist/assets/{nim-BwWLst9M.js → nim-ikQhXKQf.js} +1 -1
  46. package/dist/assets/{perl-DfW7Uq5O.js → perl-C27cToFa.js} +1 -1
  47. package/dist/assets/{php-DGf5G3Cf.js → php-Btdg-RP3.js} +1 -1
  48. package/dist/assets/{pug-CWI-NaYz.js → pug-Bk73ERXL.js} +1 -1
  49. package/dist/assets/{qml-Csblgh7U.js → qml-DhY7FDIl.js} +1 -1
  50. package/dist/assets/{r-QGooaGDI.js → r-CEwua7nj.js} +1 -1
  51. package/dist/assets/{razor-Dp9ihLYf.js → razor-sPO7Sh9d.js} +1 -1
  52. package/dist/assets/{regexp-DwUnLcVr.js → regexp-D9DLFGXE.js} +1 -1
  53. package/dist/assets/{rst-BY8GIwDJ.js → rst-YhauCnId.js} +1 -1
  54. package/dist/assets/{ruby-VgzI9htT.js → ruby-DpMkRaoz.js} +1 -1
  55. package/dist/assets/{sas-DYLsm-0j.js → sas-0KdmKGRO.js} +1 -1
  56. package/dist/assets/{scss-tIFkeUQX.js → scss-CohvHWvq.js} +1 -1
  57. package/dist/assets/{shellscript-CHjuQ4Pl.js → shellscript-C93Q8fLt.js} +1 -1
  58. package/dist/assets/{shellsession-Bv0S0hj9.js → shellsession-YCh2AQXu.js} +1 -1
  59. package/dist/assets/{soy-D6U1tZod.js → soy-CWVYdXUH.js} +1 -1
  60. package/dist/assets/{sql-CGz8kjKv.js → sql-CJNtax5I.js} +1 -1
  61. package/dist/assets/{stata-C9ytERUq.js → stata-C7v41XAM.js} +1 -1
  62. package/dist/assets/{surrealql-DRfRnaYu.js → surrealql-CRUSOxDf.js} +1 -1
  63. package/dist/assets/{svelte-wYnk_1sI.js → svelte-Z0harRr1.js} +1 -1
  64. package/dist/assets/{templ-DWrGVE0u.js → templ-BplDouJX.js} +1 -1
  65. package/dist/assets/{tex-CYTzoUJh.js → tex-MsB6StN5.js} +1 -1
  66. package/dist/assets/{ts-tags-1TIXEGea.js → ts-tags-BWx8aq5t.js} +1 -1
  67. package/dist/assets/{tsx-a3BoAoOh.js → tsx-DBT1gmNl.js} +1 -1
  68. package/dist/assets/{twig-e3dvNv6o.js → twig-jz67rR3C.js} +1 -1
  69. package/dist/assets/{typescript-BBhnpnTf.js → typescript-CzwGN36B.js} +1 -1
  70. package/dist/assets/{vue-html-quBNv38K.js → vue-html-Dhrmu7sO.js} +1 -1
  71. package/dist/assets/{vue-vine-CDuR69FE.js → vue-vine-BfQGXZs6.js} +1 -1
  72. package/dist/assets/{vue-DzzVbu9q.js → vue-xsrSiVMC.js} +1 -1
  73. package/dist/assets/{xml-DMifOLSe.js → xml-W7cP4qeN.js} +1 -1
  74. package/dist/assets/{xsl-D8Ef-5HK.js → xsl-BlXf8qm_.js} +1 -1
  75. package/dist/assets/{yaml-5c38gpmA.js → yaml-BtNwxUT9.js} +1 -1
  76. package/dist/index.html +2 -2
  77. package/package.json +3 -2
  78. package/scripts/agent-usage.mjs +141 -0
  79. package/scripts/build-diff-data.mjs +32 -12
  80. package/scripts/cli-args.mjs +7 -0
  81. package/scripts/coding-agents.mjs +208 -7
  82. package/scripts/generate-summaries.mjs +61 -9
  83. package/scripts/present.mjs +22 -7
  84. package/scripts/review-chat-context.mjs +6 -4
  85. package/scripts/review-chat-controller.mjs +5 -3
  86. package/scripts/review-chat-provider.mjs +26 -4
  87. package/scripts/serve-built.mjs +100 -4
@@ -11,11 +11,26 @@ import {
11
11
  import { readConfiguredAgent } from './agent-config.mjs';
12
12
 
13
13
  export const codingAgentCapabilities = {
14
- codex: { binary: 'codex', model: true, reasoning: true },
15
- claude: { binary: 'claude', model: true, reasoning: false },
16
- copilot: { binary: 'copilot', model: true, reasoning: false },
17
- cursor: { binary: 'cursor-agent', model: true, reasoning: false },
18
- opencode: { binary: 'opencode', model: true, reasoning: true },
14
+ codex: {
15
+ binary: 'codex',
16
+ model: true,
17
+ reasoning: true,
18
+ fast: { label: 'Codex CLI', minimumVersion: '0.108.0' },
19
+ },
20
+ claude: {
21
+ binary: 'claude',
22
+ model: true,
23
+ reasoning: false,
24
+ fast: { label: 'Claude Code', minimumVersion: '2.1.36' },
25
+ },
26
+ copilot: { binary: 'copilot', model: true, reasoning: false, fast: false },
27
+ cursor: {
28
+ binary: 'cursor-agent',
29
+ model: true,
30
+ reasoning: false,
31
+ fast: false,
32
+ },
33
+ opencode: { binary: 'opencode', model: true, reasoning: true, fast: false },
19
34
  };
20
35
 
21
36
  export const codingAgents = Object.keys(codingAgentCapabilities);
@@ -24,6 +39,10 @@ export function agentSupportsReasoning(agent) {
24
39
  return codingAgentCapabilities[agent]?.reasoning === true;
25
40
  }
26
41
 
42
+ export function agentSupportsFast(agent) {
43
+ return Boolean(codingAgentCapabilities[agent]?.fast);
44
+ }
45
+
27
46
  export function assertReasoningSupported(agent, reasoning) {
28
47
  if (reasoning && !agentSupportsReasoning(agent)) {
29
48
  throw new Error(
@@ -89,6 +108,67 @@ function versionAtLeast(current, minimum) {
89
108
  return true;
90
109
  }
91
110
 
111
+ function semanticVersion(value) {
112
+ const match = value?.match(/(?:^|\D)(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?/);
113
+ if (!match) return undefined;
114
+ return {
115
+ parts: match.slice(1, 4).map(Number),
116
+ prerelease: Boolean(match[4]),
117
+ };
118
+ }
119
+
120
+ export function inspectFastCompatibility(
121
+ agent,
122
+ command,
123
+ {
124
+ env = process.env,
125
+ timeout = 5_000,
126
+ run = spawnSync,
127
+ } = {},
128
+ ) {
129
+ const capability = codingAgentCapabilities[agent]?.fast;
130
+ if (!capability) {
131
+ return {
132
+ compatible: false,
133
+ reason: `--fast is supported only by codex and claude; ${agent} does not support it.`,
134
+ };
135
+ }
136
+ const result = run(command, ['--version'], {
137
+ encoding: 'utf8',
138
+ env,
139
+ timeout,
140
+ windowsHide: true,
141
+ });
142
+ const version = firstLine(`${result.stdout || ''}\n${result.stderr || ''}`);
143
+ const current = semanticVersion(version);
144
+ const minimum = semanticVersion(capability.minimumVersion);
145
+ const compatible = current && minimum &&
146
+ (versionAtLeast(current.parts, minimum.parts) &&
147
+ !(current.prerelease &&
148
+ current.parts.every((part, index) => part === minimum.parts[index])));
149
+ if (result.error || result.status !== 0 || !version || !current) {
150
+ return {
151
+ compatible: false,
152
+ version,
153
+ reason: `${capability.label} ${capability.minimumVersion} or newer is required for --fast, but its version could not be checked. Upgrade ${capability.label}.`,
154
+ };
155
+ }
156
+ if (!compatible) {
157
+ return {
158
+ compatible: false,
159
+ version,
160
+ reason: `${capability.label} ${capability.minimumVersion} or newer is required for --fast; found ${version}. Upgrade ${capability.label}.`,
161
+ };
162
+ }
163
+ return { compatible: true, version };
164
+ }
165
+
166
+ export function assertFastCompatible(agent, command, fast, options) {
167
+ if (!fast) return;
168
+ const inspection = inspectFastCompatibility(agent, command, options);
169
+ if (!inspection.compatible) throw new Error(inspection.reason);
170
+ }
171
+
92
172
  function cursorCompatibilityError(detail) {
93
173
  return `Cursor Agent is incompatible: ${detail} ${cursorRequirementSummary} Upgrade Cursor Agent.`;
94
174
  }
@@ -463,6 +543,27 @@ function parseClaudeResponse(stdout) {
463
543
  return envelope;
464
544
  }
465
545
 
546
+ function jsonLines(stdout) {
547
+ const lines = stdout.trim().split('\n').filter(Boolean);
548
+ if (!lines.length) return undefined;
549
+ const events = lines.map(parseEvent);
550
+ return events.every(Boolean) ? events : undefined;
551
+ }
552
+
553
+ function parseCodexResponse(stdout) {
554
+ const events = jsonLines(stdout);
555
+ const message = events && [...events]
556
+ .reverse()
557
+ .find((event) =>
558
+ event.type === 'item.completed' &&
559
+ event.item?.type === 'agent_message' &&
560
+ typeof event.item.text === 'string',
561
+ );
562
+ return message
563
+ ? parseJsonText(message.item.text, 'Codex')
564
+ : parseJsonText(stdout, 'Codex');
565
+ }
566
+
466
567
  function parseEvent(line) {
467
568
  try {
468
569
  return JSON.parse(line);
@@ -521,8 +622,94 @@ export function parseAgentResponse(agent, stdout) {
521
622
  if (agent === 'claude') return parseClaudeResponse(stdout);
522
623
  if (agent === 'opencode') return parseOpenCodeResponse(stdout);
523
624
  if (agent === 'cursor') return parseCursorResponse(stdout);
524
- const label = agent === 'copilot' ? 'Copilot' : 'Codex';
525
- return parseJsonText(stdout, label);
625
+ if (agent === 'codex') return parseCodexResponse(stdout);
626
+ return parseJsonText(stdout, 'Copilot');
627
+ }
628
+
629
+ function addUsageField(totals, [field, count]) {
630
+ if (!Number.isSafeInteger(count) || count < 0) return;
631
+ totals[field] = (totals[field] || 0) + count;
632
+ }
633
+
634
+ function addProviderUsage(totals, usage) {
635
+ Object.entries(usage).forEach((entry) => addUsageField(totals, entry));
636
+ return totals;
637
+ }
638
+
639
+ function sumProviderUsage(usages) {
640
+ const totals = usages.reduce(addProviderUsage, {});
641
+ return Object.keys(totals).length ? totals : undefined;
642
+ }
643
+
644
+ function providerUsage(inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens) {
645
+ return { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens };
646
+ }
647
+
648
+ function codexEventUsage(event) {
649
+ if (event.type !== 'turn.completed' || !event.usage) return undefined;
650
+ const usage = event.usage;
651
+ return providerUsage(
652
+ usage.input_tokens,
653
+ usage.output_tokens,
654
+ usage.cached_input_tokens,
655
+ usage.cache_write_input_tokens,
656
+ );
657
+ }
658
+
659
+ function eventPartTokens(event, type) {
660
+ if (event.type !== type) return undefined;
661
+ return event.part?.tokens;
662
+ }
663
+
664
+ function openCodeEventUsage(event) {
665
+ const tokens = eventPartTokens(event, 'step_finish');
666
+ if (!tokens) return undefined;
667
+ const cache = tokens.cache || {};
668
+ return providerUsage(
669
+ tokens.input,
670
+ tokens.output,
671
+ cache.read,
672
+ cache.write,
673
+ );
674
+ }
675
+
676
+ function eventUsage(stdout, extractUsage) {
677
+ const events = jsonLines(stdout) || [];
678
+ return sumProviderUsage(events.map(extractUsage).filter(Boolean));
679
+ }
680
+
681
+ function codexUsage(stdout) {
682
+ return eventUsage(stdout, codexEventUsage);
683
+ }
684
+
685
+ function safeProviderEnvelope(stdout, label) {
686
+ try {
687
+ return parseJsonText(stdout, label);
688
+ } catch {
689
+ return undefined;
690
+ }
691
+ }
692
+
693
+ function claudeUsage(stdout) {
694
+ const usage = safeProviderEnvelope(stdout, 'Claude')?.usage;
695
+ if (!usage || typeof usage !== 'object') return undefined;
696
+ return sumProviderUsage([providerUsage(
697
+ usage.input_tokens,
698
+ usage.output_tokens,
699
+ usage.cache_read_input_tokens,
700
+ usage.cache_creation_input_tokens,
701
+ )]);
702
+ }
703
+
704
+ function openCodeUsage(stdout) {
705
+ return eventUsage(stdout, openCodeEventUsage);
706
+ }
707
+
708
+ export function parseAgentUsage(agent, stdout) {
709
+ if (agent === 'codex') return codexUsage(stdout);
710
+ if (agent === 'claude') return claudeUsage(stdout);
711
+ if (agent === 'opencode') return openCodeUsage(stdout);
712
+ return undefined;
526
713
  }
527
714
 
528
715
  function codexIsolationArgs(snapshotOnly) {
@@ -554,6 +741,7 @@ function codexRepoArgs(snapshotOnly) {
554
741
 
555
742
  function codexCommand({
556
743
  binary,
744
+ fast,
557
745
  model,
558
746
  reasoning,
559
747
  prompt,
@@ -565,6 +753,7 @@ function codexCommand({
565
753
  const args = [
566
754
  'exec',
567
755
  '--ephemeral',
756
+ '--json',
568
757
  '--sandbox',
569
758
  'read-only',
570
759
  ...codexIsolationArgs(snapshotOnly),
@@ -577,6 +766,14 @@ function codexCommand({
577
766
  schemaPath,
578
767
  ...codexConfigArgs(snapshotOnly),
579
768
  ];
769
+ if (fast) {
770
+ args.push(
771
+ '--config',
772
+ 'service_tier="fast"',
773
+ '--config',
774
+ 'features.fast_mode=true',
775
+ );
776
+ }
580
777
  if (model) args.push('--model', model);
581
778
  if (reasoning) {
582
779
  args.push(
@@ -596,6 +793,7 @@ function codexCommand({
596
793
 
597
794
  function claudeCommand({
598
795
  binary,
796
+ fast,
599
797
  model,
600
798
  prompt,
601
799
  schema,
@@ -612,6 +810,7 @@ function claudeCommand({
612
810
  ...(snapshotOnly ? ['--tools', ''] : ['--permission-mode', 'plan']),
613
811
  '--no-session-persistence',
614
812
  ];
813
+ if (fast) args.push('--settings', JSON.stringify({ fastMode: true }));
615
814
  if (model) args.push('--model', model);
616
815
  args.push(prompt);
617
816
  return {
@@ -764,6 +963,7 @@ export function agentCommand({
764
963
  binary = agent,
765
964
  model,
766
965
  reasoning,
966
+ fast = false,
767
967
  prompt,
768
968
  schema,
769
969
  schemaPath,
@@ -775,6 +975,7 @@ export function agentCommand({
775
975
  const checkoutAccess = accessMode?.mode === 'checkout-read-only';
776
976
  const options = {
777
977
  binary,
978
+ fast,
778
979
  inputPath,
779
980
  model,
780
981
  prompt,
@@ -15,13 +15,22 @@ import { fileURLToPath } from 'node:url';
15
15
  import {
16
16
  agentCommand,
17
17
  agentReadOnlyWarning,
18
+ assertFastCompatible,
18
19
  assertReasoningSupported,
19
20
  codingAgentFromSelectionError,
20
21
  codingAgentAvailability,
21
22
  codingAgentBinary,
22
23
  parseAgentResponse,
24
+ parseAgentUsage,
23
25
  selectCodingAgent,
24
26
  } from './coding-agents.mjs';
27
+ import {
28
+ emptyUsageAccumulator,
29
+ formatReviewUsage,
30
+ recordUsage,
31
+ reviewUsage,
32
+ usageSummary,
33
+ } from './agent-usage.mjs';
25
34
  import {
26
35
  isBaseWorktreeTarget,
27
36
  reviewAccessMode,
@@ -76,6 +85,7 @@ const valueFlags = new Set([
76
85
  const booleanFlags = new Set([
77
86
  '--checkout',
78
87
  '--force',
88
+ '--fast',
79
89
  '--support-record',
80
90
  '--worktree',
81
91
  '--no-checkout-access',
@@ -140,6 +150,7 @@ Options:
140
150
  --codex-bin FILE Codex CLI path (default: codex)
141
151
  --model NAME Model passed to the coding agent
142
152
  --reasoning LEVEL Agent reasoning effort when supported
153
+ --fast Enable provider Fast mode for every agent call
143
154
  --batch-size COUNT Maximum files per agent pass (default: 12)
144
155
  --jobs COUNT Agent passes to run at once (default: 3)
145
156
  --support-record Print a safe record if this run fails
@@ -178,6 +189,7 @@ let selectedAgent;
178
189
  let agentBinary;
179
190
  const model = option('--model');
180
191
  const reasoning = option('--reasoning');
192
+ const fast = rawArgs.includes('--fast');
181
193
  const batchSizeValue = option('--batch-size') || '12';
182
194
  const reasoningLevels = new Set([
183
195
  'minimal',
@@ -377,6 +389,18 @@ const summariesPath = summaryPath({
377
389
  const ownershipPath = `${summariesPath}.lock`;
378
390
  let ownership;
379
391
  let ownershipHeartbeat;
392
+ let agentUsage = emptyUsageAccumulator();
393
+
394
+ function currentAgentUsage() {
395
+ return usageSummary(agentUsage);
396
+ }
397
+
398
+ function recordAgentUsage(usage) {
399
+ agentUsage = recordUsage(agentUsage, usage);
400
+ const emptyChatUsage = usageSummary(emptyUsageAccumulator());
401
+ console.log(formatReviewUsage(reviewUsage(currentAgentUsage(), emptyChatUsage)));
402
+ }
403
+
380
404
  function acquireOwnership() {
381
405
  ownership = acquireLease(ownershipPath);
382
406
  ownershipHeartbeat = setInterval(() => {
@@ -1039,8 +1063,13 @@ function publishSnapshot(
1039
1063
  ...(state.errors.length ? { errors: state.errors } : {}),
1040
1064
  ...(model ? { model } : {}),
1041
1065
  ...(reasoning ? { reasoning } : {}),
1066
+ fast,
1042
1067
  accessMode: accessMode.mode,
1043
1068
  },
1069
+ usage: reviewUsage(
1070
+ currentAgentUsage(),
1071
+ usageSummary(emptyUsageAccumulator()),
1072
+ ),
1044
1073
  };
1045
1074
  delete content.version;
1046
1075
  delete content.generatedAt;
@@ -1065,6 +1094,7 @@ function summariesForSnapshot(summaries, snapshot) {
1065
1094
  meta: {
1066
1095
  ...summaries.meta,
1067
1096
  reviewFingerprint: snapshot.notes.reviewFingerprint,
1097
+ usage: currentAgentUsage(),
1068
1098
  },
1069
1099
  };
1070
1100
  }
@@ -1208,8 +1238,9 @@ async function selectAgentForNotes() {
1208
1238
  signal: selectionAbortController.signal,
1209
1239
  },
1210
1240
  );
1211
- assertReasoningSupported(selectedAgent, reasoning);
1212
1241
  agentBinary = codingAgentBinary(selectedAgent, { codexBin });
1242
+ assertReasoningSupported(selectedAgent, reasoning);
1243
+ assertFastCompatible(selectedAgent, agentBinary, fast);
1213
1244
  supportRecorder?.setProvider(
1214
1245
  selectedAgent,
1215
1246
  safeCommandVersion(selectedAgent, agentBinary),
@@ -1265,9 +1296,12 @@ function runAgent(invocation, input, { timeoutMs } = {}) {
1265
1296
  const timeout = timeoutMs
1266
1297
  ? setTimeout(() => {
1267
1298
  child.kill('SIGTERM');
1268
- rejectPromise(
1269
- new Error(`${selectedAgent} timed out`),
1299
+ const error = new Error(`${selectedAgent} timed out`);
1300
+ error.providerUsage = parseAgentUsage(
1301
+ selectedAgent,
1302
+ Buffer.concat(stdout).toString('utf8'),
1270
1303
  );
1304
+ rejectPromise(error);
1271
1305
  }, timeoutMs)
1272
1306
  : undefined;
1273
1307
  timeout?.unref();
@@ -1316,14 +1350,18 @@ function runAgent(invocation, input, { timeoutMs } = {}) {
1316
1350
  .filter((line) => line.trim() && line.length < 600)
1317
1351
  .slice(-8)
1318
1352
  .join('\n');
1319
- rejectPromise(
1320
- new Error(
1321
- `${selectedAgent} exited with status ${status ?? signal}${detail ? `\n${detail}` : ''}`,
1322
- ),
1353
+ const error = new Error(
1354
+ `${selectedAgent} exited with status ${status ?? signal}${detail ? `\n${detail}` : ''}`,
1323
1355
  );
1356
+ error.providerUsage = parseAgentUsage(selectedAgent, stdoutText);
1357
+ rejectPromise(error);
1324
1358
  return;
1325
1359
  }
1326
- resolvePromise({ stdout: stdoutText, stderr: stderrText });
1360
+ resolvePromise({
1361
+ stdout: stdoutText,
1362
+ stderr: stderrText,
1363
+ providerUsage: parseAgentUsage(selectedAgent, stdoutText),
1364
+ });
1327
1365
  });
1328
1366
  if (invocation.input === 'stdin') child.stdin.end(input);
1329
1367
  else child.stdin.end();
@@ -1333,8 +1371,16 @@ function runAgent(invocation, input, { timeoutMs } = {}) {
1333
1371
  async function requestAgent(invocation, input, normalize, snapshot) {
1334
1372
  supportRecorder?.addBytes('agentInput', Buffer.byteLength(input));
1335
1373
  return recordAsyncStage('agent', async () => {
1336
- const result = await runAgent(invocation, input);
1374
+ let result;
1375
+ try {
1376
+ result = await runAgent(invocation, input);
1377
+ } catch (error) {
1378
+ assertAgentReviewFresh(snapshot);
1379
+ recordAgentUsage(error?.providerUsage);
1380
+ throw error;
1381
+ }
1337
1382
  assertAgentReviewFresh(snapshot);
1383
+ recordAgentUsage(result.providerUsage);
1338
1384
  if (result.stderr.trim()) {
1339
1385
  console.error(
1340
1386
  `${selectedAgent} wrote diagnostic output:\n${result.stderr.trim()}`,
@@ -1567,6 +1613,7 @@ try {
1567
1613
  generatedAt: new Date().toISOString(),
1568
1614
  ...(model ? { model } : {}),
1569
1615
  ...(reasoning ? { reasoning } : {}),
1616
+ fast,
1570
1617
  accessMode: accessMode.mode,
1571
1618
  },
1572
1619
  };
@@ -1613,6 +1660,7 @@ try {
1613
1660
  generatedAt: new Date().toISOString(),
1614
1661
  ...(model ? { model } : {}),
1615
1662
  ...(reasoning ? { reasoning } : {}),
1663
+ fast,
1616
1664
  accessMode: accessMode.mode,
1617
1665
  },
1618
1666
  };
@@ -1694,6 +1742,7 @@ try {
1694
1742
  : 'complete',
1695
1743
  ...(model ? { model } : {}),
1696
1744
  ...(reasoning ? { reasoning } : {}),
1745
+ fast,
1697
1746
  accessMode: accessMode.mode,
1698
1747
  },
1699
1748
  },
@@ -1749,6 +1798,7 @@ try {
1749
1798
  binary: agentBinary,
1750
1799
  model,
1751
1800
  reasoning,
1801
+ fast,
1752
1802
  prompt: promptFor(batchPaths, { accessMode, includeChange: false }),
1753
1803
  schema: outputSchema(batchPaths, { includeChange: false }),
1754
1804
  schemaPath,
@@ -1875,6 +1925,7 @@ try {
1875
1925
  binary: agentBinary,
1876
1926
  model,
1877
1927
  reasoning,
1928
+ fast,
1878
1929
  prompt: promptFor([], { accessMode }),
1879
1930
  schema,
1880
1931
  schemaPath,
@@ -1945,6 +1996,7 @@ try {
1945
1996
  } catch (error) {
1946
1997
  if (!(error instanceof ReviewChangedError)) throw error;
1947
1998
  reviewChanged = true;
1999
+ agentUsage = emptyUsageAccumulator();
1948
2000
  console.log('The review changed while notes were generated. Rebuilding the snapshot.');
1949
2001
  }
1950
2002
  } while (reviewChanged && !interrupted);
@@ -20,6 +20,7 @@ import { helpText, parseCliArgs } from './cli-args.mjs';
20
20
  import { applyAgentConfigOperation } from './agent-config.mjs';
21
21
  import {
22
22
  agentReadOnlyWarning,
23
+ assertFastCompatible,
23
24
  assertReasoningSupported,
24
25
  codingAgentFromSelectionError,
25
26
  codingAgentAvailability,
@@ -33,6 +34,11 @@ import {
33
34
  } from './local-target.mjs';
34
35
  import { doctorReport } from './doctor.mjs';
35
36
  import { accessTokenDirectory } from './access-token.mjs';
37
+ import {
38
+ emptyUsageAccumulator,
39
+ reviewUsage,
40
+ usageSummary,
41
+ } from './agent-usage.mjs';
36
42
  import { cacheStatus, clearCache, formatCacheStatus, pruneCache } from './cache.mjs';
37
43
  import {
38
44
  agentFallbackRecordNeeded,
@@ -256,10 +262,11 @@ if (agentEnabled) {
256
262
  }),
257
263
  },
258
264
  );
259
- assertReasoningSupported(selectedAgent, cli.reasoning);
260
265
  selectedAgentBinary = codingAgentBinary(selectedAgent, {
261
266
  codexBin: cli.codexBin,
262
267
  });
268
+ assertReasoningSupported(selectedAgent, cli.reasoning);
269
+ assertFastCompatible(selectedAgent, selectedAgentBinary, cli.fast);
263
270
  recordSelectedProvider(selectedAgent, selectedAgentBinary);
264
271
  supportRecorder?.addStage(
265
272
  'agent',
@@ -454,6 +461,7 @@ function chatAgentArguments() {
454
461
  ...chatAccessArguments(),
455
462
  ...optionalSiteArgument('--chat-model', cli.model),
456
463
  ...optionalSiteArgument('--chat-reasoning', cli.reasoning),
464
+ ...(cli.fast ? ['--chat-fast'] : []),
457
465
  ];
458
466
  }
459
467
 
@@ -778,15 +786,22 @@ function markSnapshotReady() {
778
786
  }
779
787
 
780
788
  function snapshotForPresentation(snapshot) {
781
- if (snapshotStateFromSnapshot(snapshot).hasCurrentAgentNotes) {
782
- return snapshot;
783
- }
784
- const content = {
789
+ const zeroUsage = usageSummary(emptyUsageAccumulator());
790
+ const hasCurrentNotes = snapshotStateFromSnapshot(snapshot)
791
+ .hasCurrentAgentNotes;
792
+ const current = {
785
793
  ...snapshot,
794
+ usage: reviewUsage(zeroUsage, zeroUsage),
795
+ };
796
+ if (hasCurrentNotes && snapshot.notes?.fast === cli.fast) return current;
797
+ const content = {
798
+ ...current,
786
799
  notes: {
787
800
  ...snapshot.notes,
788
- complete: false,
789
- status: 'generating',
801
+ fast: cli.fast,
802
+ ...(hasCurrentNotes
803
+ ? {}
804
+ : { complete: false, status: 'generating' }),
790
805
  },
791
806
  };
792
807
  delete content.version;
@@ -307,8 +307,10 @@ export function makeInput({ kind, context, messages, question, accessMode }) {
307
307
  };
308
308
  }
309
309
 
310
- function providerResult(provider, input) {
311
- return typeof provider === 'function' ? provider(input) : provider.run(input);
310
+ function providerResult(provider, input, context) {
311
+ return typeof provider === 'function'
312
+ ? provider(input, context)
313
+ : provider.run(input, context);
312
314
  }
313
315
 
314
316
  function isExecution(value) {
@@ -324,8 +326,8 @@ function executionPromise(value) {
324
326
  return isExecution(value) ? value.promise : value;
325
327
  }
326
328
 
327
- export function providerExecution(provider, input) {
328
- const result = providerResult(provider, input);
329
+ export function providerExecution(provider, input, context) {
330
+ const result = providerResult(provider, input, context);
329
331
  return {
330
332
  promise: Promise.resolve(executionPromise(result)),
331
333
  cancel: cancellationFor(result),
@@ -130,9 +130,9 @@ function validatedAnswer(value, paths) {
130
130
  }
131
131
  }
132
132
 
133
- function executionResult(provider, input) {
133
+ function executionResult(provider, input, context) {
134
134
  try {
135
- return { execution: providerExecution(provider, input) };
135
+ return { execution: providerExecution(provider, input, context) };
136
136
  } catch (error) {
137
137
  return { error };
138
138
  }
@@ -424,7 +424,9 @@ export class ReviewChatController {
424
424
  thread.error = undefined;
425
425
  this.notify();
426
426
  this.supervise(thread, run);
427
- const result = executionResult(this.provider, input);
427
+ const result = executionResult(this.provider, input, {
428
+ reviewFingerprint: run.fingerprint,
429
+ });
428
430
  if (result.error) {
429
431
  this.finishFailure(thread, run, result.error);
430
432
  return;