@selesai/code 0.9.14 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +6 -9
  3. package/dist/defaults/models.json +79 -29
  4. package/dist/defaults/settings.json +3 -7
  5. package/dist/extensions/package.json +3 -4
  6. package/dist/extensions/pi-zentui/.editorconfig +12 -0
  7. package/dist/extensions/pi-zentui/.github/CODEOWNERS +1 -0
  8. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/bug_report.yml +110 -0
  9. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config.yml +8 -0
  10. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config_question.yml +56 -0
  11. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/feature_request.yml +63 -0
  12. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/rendering_issue.yml +106 -0
  13. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/runtime_detection.yml +103 -0
  14. package/dist/extensions/pi-zentui/.github/dependabot.yml +47 -0
  15. package/dist/extensions/pi-zentui/.github/pull_request_template.md +34 -0
  16. package/dist/extensions/pi-zentui/.github/release.yml +18 -0
  17. package/dist/extensions/pi-zentui/.github/workflows/ci.yml +79 -0
  18. package/dist/extensions/pi-zentui/.github/workflows/publish.yml +63 -0
  19. package/dist/extensions/pi-zentui/AGENTS.md +107 -0
  20. package/dist/extensions/pi-zentui/CONTRIBUTING.md +68 -0
  21. package/dist/extensions/pi-zentui/LICENSE +21 -0
  22. package/dist/extensions/pi-zentui/README.md +188 -0
  23. package/dist/extensions/pi-zentui/SECURITY.md +18 -0
  24. package/dist/extensions/pi-zentui/assets/main-cover.png +0 -0
  25. package/dist/extensions/pi-zentui/assets/screenshots/editor-accent-rail.png +0 -0
  26. package/dist/extensions/pi-zentui/assets/screenshots/editor-minimalist.png +0 -0
  27. package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode-copy-friendly.png +0 -0
  28. package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode.png +0 -0
  29. package/dist/extensions/pi-zentui/assets/screenshots/user-message-compact.png +0 -0
  30. package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed-copy-friendly.png +0 -0
  31. package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed.png +0 -0
  32. package/dist/extensions/pi-zentui/assets/screenshots/user-message-labeled.png +0 -0
  33. package/dist/extensions/pi-zentui/assets/zentui.png +0 -0
  34. package/dist/extensions/pi-zentui/biome.json +31 -0
  35. package/dist/extensions/pi-zentui/docs/configuration.md +509 -0
  36. package/dist/extensions/pi-zentui/docs/footer-format.md +162 -0
  37. package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-editor.ts +99 -0
  38. package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-layout-patch.ts +457 -0
  39. package/dist/extensions/pi-zentui/extensions/zentui/completion-menu.ts +157 -0
  40. package/dist/extensions/pi-zentui/extensions/zentui/config.ts +2100 -0
  41. package/dist/extensions/pi-zentui/extensions/zentui/editor-metadata-format.ts +321 -0
  42. package/dist/extensions/pi-zentui/extensions/zentui/editor-transfer.ts +73 -0
  43. package/dist/extensions/pi-zentui/extensions/zentui/extension-status.ts +89 -0
  44. package/dist/extensions/pi-zentui/extensions/zentui/footer-format.ts +364 -0
  45. package/dist/extensions/pi-zentui/extensions/zentui/footer-layout.ts +122 -0
  46. package/dist/extensions/pi-zentui/extensions/zentui/footer.ts +812 -0
  47. package/dist/extensions/pi-zentui/extensions/zentui/format.ts +568 -0
  48. package/dist/extensions/pi-zentui/extensions/zentui/git.ts +384 -0
  49. package/dist/extensions/pi-zentui/extensions/zentui/icons.ts +252 -0
  50. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +1406 -0
  51. package/dist/extensions/pi-zentui/extensions/zentui/interaction-summary.ts +1049 -0
  52. package/dist/extensions/pi-zentui/extensions/zentui/live-context.ts +75 -0
  53. package/dist/extensions/pi-zentui/extensions/zentui/minimalist-editor.ts +463 -0
  54. package/dist/extensions/pi-zentui/extensions/zentui/package-version.ts +651 -0
  55. package/dist/extensions/pi-zentui/extensions/zentui/project-refresh.ts +120 -0
  56. package/dist/extensions/pi-zentui/extensions/zentui/project-state.ts +59 -0
  57. package/dist/extensions/pi-zentui/extensions/zentui/prototype-patch-registry.ts +166 -0
  58. package/dist/extensions/pi-zentui/extensions/zentui/repository-root.ts +98 -0
  59. package/dist/extensions/pi-zentui/extensions/zentui/runtime.ts +841 -0
  60. package/dist/extensions/pi-zentui/extensions/zentui/selector-border.ts +93 -0
  61. package/dist/extensions/pi-zentui/extensions/zentui/session-lifecycle.ts +60 -0
  62. package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +1796 -0
  63. package/dist/extensions/pi-zentui/extensions/zentui/settings-previews.ts +128 -0
  64. package/dist/extensions/pi-zentui/extensions/zentui/state.ts +94 -0
  65. package/dist/extensions/pi-zentui/extensions/zentui/style.ts +424 -0
  66. package/dist/extensions/pi-zentui/extensions/zentui/telemetry.ts +82 -0
  67. package/dist/extensions/pi-zentui/extensions/zentui/ui.ts +1290 -0
  68. package/dist/extensions/pi-zentui/extensions/zentui/user-message-osc.ts +248 -0
  69. package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +215 -0
  70. package/dist/extensions/pi-zentui/extensions/zentui/user-message.ts +206 -0
  71. package/dist/extensions/pi-zentui/extensions/zentui/working-line-messages.ts +24 -0
  72. package/dist/extensions/pi-zentui/extensions/zentui/working-line-spinners.ts +54 -0
  73. package/dist/extensions/pi-zentui/extensions/zentui/working-line.ts +1433 -0
  74. package/dist/extensions/pi-zentui/package-lock.json +5311 -0
  75. package/dist/extensions/pi-zentui/package.json +60 -0
  76. package/dist/extensions/pi-zentui/test/accent-rail-editor.test.ts +232 -0
  77. package/dist/extensions/pi-zentui/test/accent-rail-layout-patch.test.ts +669 -0
  78. package/dist/extensions/pi-zentui/test/completion-menu.test.ts +265 -0
  79. package/dist/extensions/pi-zentui/test/config.test.ts +3119 -0
  80. package/dist/extensions/pi-zentui/test/editor-metadata-format.test.ts +405 -0
  81. package/dist/extensions/pi-zentui/test/editor-transfer.test.ts +260 -0
  82. package/dist/extensions/pi-zentui/test/editor-viewport-indicators.test.ts +1286 -0
  83. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +7196 -0
  84. package/dist/extensions/pi-zentui/test/extension-status.test.ts +182 -0
  85. package/dist/extensions/pi-zentui/test/fixtures/starship-nerd-font-symbols.toml +172 -0
  86. package/dist/extensions/pi-zentui/test/footer-format.test.ts +431 -0
  87. package/dist/extensions/pi-zentui/test/footer-layout.test.ts +113 -0
  88. package/dist/extensions/pi-zentui/test/footer-repository-path.test.ts +123 -0
  89. package/dist/extensions/pi-zentui/test/format.test.ts +865 -0
  90. package/dist/extensions/pi-zentui/test/git.test.ts +212 -0
  91. package/dist/extensions/pi-zentui/test/icons.test.ts +69 -0
  92. package/dist/extensions/pi-zentui/test/interaction-summary.test.ts +1402 -0
  93. package/dist/extensions/pi-zentui/test/live-context-integration.test.ts +378 -0
  94. package/dist/extensions/pi-zentui/test/live-context.test.ts +139 -0
  95. package/dist/extensions/pi-zentui/test/minimalist-editor.test.ts +811 -0
  96. package/dist/extensions/pi-zentui/test/package-version.test.ts +321 -0
  97. package/dist/extensions/pi-zentui/test/project-refresh.test.ts +250 -0
  98. package/dist/extensions/pi-zentui/test/project-state.test.ts +159 -0
  99. package/dist/extensions/pi-zentui/test/repository-root.test.ts +108 -0
  100. package/dist/extensions/pi-zentui/test/responsive-dependencies.test.ts +825 -0
  101. package/dist/extensions/pi-zentui/test/runtime.test.ts +183 -0
  102. package/dist/extensions/pi-zentui/test/session-lifecycle.test.ts +40 -0
  103. package/dist/extensions/pi-zentui/test/settings-command.test.ts +1381 -0
  104. package/dist/extensions/pi-zentui/test/settings-previews.test.ts +347 -0
  105. package/dist/extensions/pi-zentui/test/state.test.ts +84 -0
  106. package/dist/extensions/pi-zentui/test/telemetry-lifecycle-integration.test.ts +276 -0
  107. package/dist/extensions/pi-zentui/test/telemetry.test.ts +105 -0
  108. package/dist/extensions/pi-zentui/test/turn-summary-lifecycle.integration.test.ts +327 -0
  109. package/dist/extensions/pi-zentui/test/user-message-osc.test.ts +164 -0
  110. package/dist/extensions/pi-zentui/test/user-message-styles.test.ts +292 -0
  111. package/dist/extensions/pi-zentui/test/working-line-lifecycle.integration.test.ts +730 -0
  112. package/dist/extensions/pi-zentui/test/working-line-messages.test.ts +34 -0
  113. package/dist/extensions/pi-zentui/test/working-line.test.ts +1990 -0
  114. package/dist/extensions/pi-zentui/tsconfig.json +16 -0
  115. package/dist/extensions/tps.test.ts +38 -0
  116. package/dist/extensions/{pi-powerline-footer/tps.ts → tps.ts} +4 -0
  117. package/dist/skills/design-references/SKILL.md +31 -0
  118. package/dist/skills/design-references/references/clickhouse/DESIGN.md +544 -0
  119. package/dist/themes/adventure.json +84 -0
  120. package/dist/themes/adwaita-dark.json +84 -0
  121. package/dist/themes/arcoiris.json +84 -0
  122. package/dist/themes/arthur.json +84 -0
  123. package/dist/themes/atom.json +84 -0
  124. package/dist/themes/aura.json +84 -0
  125. package/dist/themes/black-metal-bathory.json +84 -0
  126. package/dist/themes/black-metal-burzum.json +84 -0
  127. package/dist/themes/black-metal-khold.json +84 -0
  128. package/dist/themes/box.json +84 -0
  129. package/dist/themes/brogrammer.json +84 -0
  130. package/dist/themes/carbonfox.json +84 -0
  131. package/dist/themes/catppuccin-mocha.json +84 -0
  132. package/dist/themes/citruszest.json +84 -0
  133. package/dist/themes/cursor-dark.json +84 -0
  134. package/dist/themes/cutie-pro.json +84 -0
  135. package/dist/themes/dark-modern.json +84 -0
  136. package/dist/themes/dark-pastel.json +84 -0
  137. package/dist/themes/dimmed-monokai.json +84 -0
  138. package/dist/themes/doom-peacock.json +84 -0
  139. package/dist/themes/dracula-plus.json +84 -0
  140. package/dist/themes/dracula.json +80 -0
  141. package/dist/themes/earthsong.json +84 -0
  142. package/dist/themes/everforest-dark-hard.json +84 -0
  143. package/dist/themes/everforest-dark.json +80 -0
  144. package/dist/themes/fahrenheit.json +84 -0
  145. package/dist/themes/flatland.json +84 -0
  146. package/dist/themes/flexoki-dark.json +84 -0
  147. package/dist/themes/front-end-delight.json +84 -0
  148. package/dist/themes/fun-forrest.json +84 -0
  149. package/dist/themes/galizur.json +84 -0
  150. package/dist/themes/github-dark-colorblind.json +84 -0
  151. package/dist/themes/github-dark-high-contrast.json +84 -0
  152. package/dist/themes/glacier.json +84 -0
  153. package/dist/themes/gruber-darker.json +84 -0
  154. package/dist/themes/gruvbox-dark-hard.json +84 -0
  155. package/dist/themes/gruvbox-dark.json +84 -0
  156. package/dist/themes/gruvbox-material.json +84 -0
  157. package/dist/themes/guezwhoz.json +84 -0
  158. package/dist/themes/hacktober.json +84 -0
  159. package/dist/themes/hardcore.json +84 -0
  160. package/dist/themes/havn-skumring.json +84 -0
  161. package/dist/themes/ic-orange-ppl.json +84 -0
  162. package/dist/themes/iterm2-smoooooth.json +84 -0
  163. package/dist/themes/iterm2-tango-dark.json +84 -0
  164. package/dist/themes/japanesque.json +84 -0
  165. package/dist/themes/jellybeans.json +84 -0
  166. package/dist/themes/kanagawa-wave.json +84 -0
  167. package/dist/themes/kurokula.json +84 -0
  168. package/dist/themes/later-this-evening.json +84 -0
  169. package/dist/themes/lovelace.json +84 -0
  170. package/dist/themes/material-darker.json +84 -0
  171. package/dist/themes/matte-black.json +84 -0
  172. package/dist/themes/mellow.json +84 -0
  173. package/dist/themes/miasma.json +84 -0
  174. package/dist/themes/nord.json +80 -0
  175. package/dist/themes/nvim-dark.json +84 -0
  176. package/dist/themes/one-dark.json +80 -0
  177. package/dist/themes/popping-and-locking.json +84 -0
  178. package/dist/themes/rose-pine.json +80 -0
  179. package/dist/themes/sea-shells.json +84 -0
  180. package/dist/themes/sleepy-hollow.json +84 -0
  181. package/dist/themes/smyck.json +84 -0
  182. package/dist/themes/solarized-dark.json +80 -0
  183. package/dist/themes/tokyo-night.json +80 -0
  184. package/dist/themes/tomorrow-night-bright.json +84 -0
  185. package/dist/themes/tomorrow-night-burns.json +84 -0
  186. package/dist/themes/tomorrow-night.json +84 -0
  187. package/dist/themes/twilight.json +84 -0
  188. package/dist/themes/vague.json +84 -0
  189. package/dist/themes/vesper.json +84 -0
  190. package/dist/themes/xcode-dark-hc.json +84 -0
  191. package/docs/custom-provider.md +0 -1
  192. package/docs/plans/auto-model-routing-extension.md +490 -0
  193. package/package.json +3 -3
  194. package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +0 -27
  195. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +0 -687
  196. package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +0 -16
  197. package/dist/extensions/pi-powerline-footer/README.md +0 -513
  198. package/dist/extensions/pi-powerline-footer/banner.png +0 -0
  199. package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +0 -586
  200. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +0 -583
  201. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +0 -204
  202. package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +0 -286
  203. package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +0 -108
  204. package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +0 -60
  205. package/dist/extensions/pi-powerline-footer/cd-command.ts +0 -190
  206. package/dist/extensions/pi-powerline-footer/colors.ts +0 -70
  207. package/dist/extensions/pi-powerline-footer/context-usage.ts +0 -156
  208. package/dist/extensions/pi-powerline-footer/currency-rates.ts +0 -142
  209. package/dist/extensions/pi-powerline-footer/editor-composition.ts +0 -32
  210. package/dist/extensions/pi-powerline-footer/editor-performance.ts +0 -99
  211. package/dist/extensions/pi-powerline-footer/git-status.ts +0 -329
  212. package/dist/extensions/pi-powerline-footer/guide.ts +0 -220
  213. package/dist/extensions/pi-powerline-footer/icons.ts +0 -190
  214. package/dist/extensions/pi-powerline-footer/index.ts +0 -3428
  215. package/dist/extensions/pi-powerline-footer/lifecycle.ts +0 -10
  216. package/dist/extensions/pi-powerline-footer/package.json +0 -49
  217. package/dist/extensions/pi-powerline-footer/paths.ts +0 -22
  218. package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -446
  219. package/dist/extensions/pi-powerline-footer/presets.ts +0 -102
  220. package/dist/extensions/pi-powerline-footer/queue/store.ts +0 -313
  221. package/dist/extensions/pi-powerline-footer/queue/types.ts +0 -53
  222. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +0 -29
  223. package/dist/extensions/pi-powerline-footer/segments.ts +0 -570
  224. package/dist/extensions/pi-powerline-footer/separators.ts +0 -57
  225. package/dist/extensions/pi-powerline-footer/session-usage.ts +0 -44
  226. package/dist/extensions/pi-powerline-footer/shortcuts.ts +0 -61
  227. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +0 -1905
  228. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +0 -144
  229. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +0 -156
  230. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +0 -344
  231. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +0 -48
  232. package/dist/extensions/pi-powerline-footer/tests/editor-performance.test.ts +0 -43
  233. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +0 -136
  234. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +0 -39
  235. package/dist/extensions/pi-powerline-footer/tests/git-optional-locks.test.ts +0 -64
  236. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +0 -92
  237. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +0 -129
  238. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -116
  239. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +0 -79
  240. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +0 -63
  241. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +0 -65
  242. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +0 -14
  243. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +0 -173
  244. package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +0 -47
  245. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +0 -37
  246. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +0 -76
  247. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +0 -249
  248. package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +0 -39
  249. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +0 -237
  250. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +0 -199
  251. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +0 -317
  252. package/dist/extensions/pi-powerline-footer/theme.example.json +0 -24
  253. package/dist/extensions/pi-powerline-footer/theme.json +0 -12
  254. package/dist/extensions/pi-powerline-footer/theme.ts +0 -241
  255. package/dist/extensions/pi-powerline-footer/token-stats.ts +0 -304
  256. package/dist/extensions/pi-powerline-footer/tsconfig.json +0 -13
  257. package/dist/extensions/pi-powerline-footer/types.ts +0 -235
  258. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +0 -34
  259. package/dist/extensions/pi-powerline-footer/welcome.ts +0 -709
  260. package/dist/extensions/pi-powerline-footer/working-vibes.ts +0 -721
  261. package/dist/extensions/preview-tools-disabled.test.ts +0 -37
  262. package/dist/extensions/preview-tools-disabled.ts +0 -11
  263. package/dist/themes/powerline-footer/theme.json +0 -33
@@ -0,0 +1,1049 @@
1
+ import type { AssistantMessage, AssistantMessageEvent } from "@earendil-works/pi-ai";
2
+ import type { Theme } from "@earendil-works/pi-coding-agent";
3
+ import { Text } from "@earendil-works/pi-tui";
4
+ import type { ColorSource, ColorSpec } from "./config";
5
+ import { formatCount, formatElapsedDuration } from "./format";
6
+ import { isSafeSgrStylePrefix } from "./style";
7
+ import { renderWorkingLineHigh } from "./working-line";
8
+
9
+ export const TURN_SUMMARY_ENTRY_TYPE = "zentui-turn-summary";
10
+ const TURN_SUMMARY_VERSION = 3;
11
+ const SGR_RESET = "\x1b[0m";
12
+
13
+ export type InteractionTokens = Readonly<{ input: number; output: number }>;
14
+ export type LiveTokenDisplay = Readonly<{
15
+ input: number;
16
+ output: number;
17
+ outputApproximate: boolean;
18
+ }>;
19
+ export type ThoughtSnapshot = Readonly<{ durationMs: number; active: boolean }>;
20
+
21
+ type SummaryBase = {
22
+ durationMs: number;
23
+ input: number;
24
+ output: number;
25
+ };
26
+
27
+ export type TurnSummaryDataV1 = SummaryBase & { version: 1 };
28
+ export type TurnSummaryDataV2 = SummaryBase & { version: 2; stylePrefix: string };
29
+ export type TurnSummaryDataV3 = SummaryBase & {
30
+ version: 3;
31
+ thoughtDurationMs: number;
32
+ stylePrefix: string;
33
+ };
34
+ export type TurnSummaryData = TurnSummaryDataV1 | TurnSummaryDataV2 | TurnSummaryDataV3;
35
+ export type TurnSummaryMetrics = SummaryBase & { thoughtDurationMs: number };
36
+
37
+ type Interval = { start: number; end: number };
38
+ type TaggedInterval = Interval & { sources: number[] };
39
+ type ThoughtCoverage = { intervals: TaggedInterval[]; incomplete: boolean };
40
+ type ThoughtBlock = { startedAt?: number; closed: boolean };
41
+
42
+ type ContentBlock = {
43
+ acceptedLength: number;
44
+ codePoints: number;
45
+ pendingHighSurrogate: boolean;
46
+ headHash: number;
47
+ headUnits: number;
48
+ tail: string;
49
+ frozen: boolean;
50
+ };
51
+
52
+ type ResponseState = {
53
+ ordinal: number;
54
+ authorized: boolean;
55
+ responseIdKey?: string;
56
+ snapshot: InteractionTokens;
57
+ hasSnapshot: boolean;
58
+ thoughtBlocks: Map<number, ThoughtBlock>;
59
+ contentBlocks: Map<string, ContentBlock>;
60
+ generatedCodePoints: number;
61
+ outputAnchor: number;
62
+ outputAnchorCodePoints: number;
63
+ liveOutputFloor: number;
64
+ lastDisplay?: LiveTokenDisplay;
65
+ estimateIncomplete: boolean;
66
+ estimateEnabled: boolean;
67
+ };
68
+
69
+ type RunState = {
70
+ ordinal: number;
71
+ startedAt: number;
72
+ ended: boolean;
73
+ committed: InteractionTokens;
74
+ responseOrdinal: number;
75
+ response?: ResponseState;
76
+ committedResponseIds: Set<string>;
77
+ lastClosedResponseId?: string;
78
+ lastClosedDisplay?: LiveTokenDisplay;
79
+ responseLessClosed: boolean;
80
+ };
81
+
82
+ type InteractionState = {
83
+ startedAt: number;
84
+ runOrdinal: number;
85
+ runs: RunState[];
86
+ activeRun?: RunState;
87
+ foldedCommitted: InteractionTokens;
88
+ thoughtCoverage: ThoughtCoverage;
89
+ };
90
+
91
+ export type MetricsUpdateResult = {
92
+ displayTokens?: LiveTokenDisplay;
93
+ usageChanged: boolean;
94
+ thoughtChanged: boolean;
95
+ };
96
+
97
+ export type MessageEndResult =
98
+ | {
99
+ status: "accepted";
100
+ tokens: InteractionTokens;
101
+ displayTokens: LiveTokenDisplay;
102
+ source: "final" | "last-snapshot" | "no-snapshot";
103
+ }
104
+ | { status: "duplicate" }
105
+ | { status: "rejected" };
106
+
107
+ export type SettlementResult = {
108
+ summary: TurnSummaryMetrics;
109
+ nextStartedAt?: number;
110
+ nextTokens?: LiveTokenDisplay;
111
+ nextThought?: ThoughtSnapshot;
112
+ };
113
+
114
+ export type InteractionMetricsDiagnostics = Readonly<{
115
+ runs: number;
116
+ responseIds: number;
117
+ activeResponses: number;
118
+ contentBlocks: number;
119
+ thoughtBlocks: number;
120
+ thoughtIntervals: number;
121
+ maxContentTailUnits: number;
122
+ retainedContentUnits: number;
123
+ estimateIncomplete: boolean;
124
+ }>;
125
+
126
+ const MAX_CONTENT_BLOCKS = 128;
127
+ const MAX_CONTENT_INDEX = 65_535;
128
+ const MAX_DELTA_UNITS = 65_536;
129
+ const MAX_SNAPSHOT_UNITS = 1_048_576;
130
+ const MAX_GENERATED_CODE_POINTS = 4_194_304;
131
+ const MAX_ESTIMATED_RESPONSES = 128;
132
+ const MAX_RUNS = 32;
133
+ const MAX_RESPONSE_IDS = 256;
134
+ const MAX_RESPONSE_ID_UNITS = 128;
135
+ const MAX_THOUGHT_INTERVALS = 256;
136
+ const HEAD_UNITS = 64;
137
+ const TAIL_UNITS = 64;
138
+ const HASH_OFFSET = 0x811c9dc5;
139
+
140
+ function isNonnegativeSafeInteger(value: unknown): value is number {
141
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
142
+ }
143
+
144
+ function safeAdd(left: number, right: number): number {
145
+ return Math.min(Number.MAX_SAFE_INTEGER, left + right);
146
+ }
147
+
148
+ function estimatedTokens(codePoints: number): number {
149
+ return Math.min(Number.MAX_SAFE_INTEGER, Math.ceil(codePoints / 4));
150
+ }
151
+
152
+ function addTokens(left: InteractionTokens, right: InteractionTokens): InteractionTokens {
153
+ return { input: safeAdd(left.input, right.input), output: safeAdd(left.output, right.output) };
154
+ }
155
+
156
+ type InteractionMessage = { role?: string; usage?: unknown; responseId?: unknown };
157
+
158
+ export function parseAssistantMessageTokens(
159
+ message: InteractionMessage,
160
+ ): InteractionTokens | undefined {
161
+ if (message.role !== "assistant") return undefined;
162
+ const usage = (message as unknown as AssistantMessage).usage;
163
+ return isNonnegativeSafeInteger(usage?.input) && isNonnegativeSafeInteger(usage?.output)
164
+ ? { input: usage.input, output: usage.output }
165
+ : undefined;
166
+ }
167
+
168
+ function responseIdKey(message: InteractionMessage): string | undefined {
169
+ if (typeof message.responseId !== "string") return undefined;
170
+ const value = message.responseId;
171
+ if (value.length > MAX_RESPONSE_ID_UNITS) return undefined;
172
+ const normalized = value.trim();
173
+ return normalized.length > 0 ? normalized : undefined;
174
+ }
175
+
176
+ function safeTime(value: number): number {
177
+ if (!Number.isFinite(value)) return value > 0 ? Number.MAX_SAFE_INTEGER : 0;
178
+ return Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, Math.floor(value)));
179
+ }
180
+
181
+ function durationMs(startedAt: number, now: number): number {
182
+ const elapsed = Math.floor(now - startedAt);
183
+ if (Number.isNaN(elapsed) || elapsed <= 0) return 0;
184
+ return Math.min(Number.MAX_SAFE_INTEGER, elapsed);
185
+ }
186
+
187
+ function unionIntervals(intervals: readonly Interval[]): Interval[] {
188
+ const sorted = intervals
189
+ .filter(({ start, end }) => end > start)
190
+ .map(({ start, end }) => ({ start: safeTime(start), end: safeTime(end) }))
191
+ .filter(({ start, end }) => end > start)
192
+ .sort((left, right) => left.start - right.start || left.end - right.end);
193
+ const union: Interval[] = [];
194
+ for (const interval of sorted) {
195
+ const previous = union.at(-1);
196
+ if (!previous || interval.start > previous.end) union.push({ ...interval });
197
+ else previous.end = Math.max(previous.end, interval.end);
198
+ }
199
+ return union;
200
+ }
201
+
202
+ function intervalDuration(intervals: readonly Interval[]): number {
203
+ return unionIntervals(intervals).reduce(
204
+ (total, interval) => safeAdd(total, interval.end - interval.start),
205
+ 0,
206
+ );
207
+ }
208
+
209
+ function sameSources(left: readonly number[], right: readonly number[]): boolean {
210
+ return left.length === right.length && left.every((source, index) => source === right[index]);
211
+ }
212
+
213
+ function normalizeTaggedIntervals(intervals: readonly TaggedInterval[]): TaggedInterval[] {
214
+ const sanitized = intervals
215
+ .map(({ start, end, sources }) => ({
216
+ start: safeTime(start),
217
+ end: safeTime(end),
218
+ sources: [...new Set(sources)].sort((left, right) => left - right),
219
+ }))
220
+ .filter(({ start, end, sources }) => end > start && sources.length > 0);
221
+ const boundaries = [...new Set(sanitized.flatMap(({ start, end }) => [start, end]))].sort(
222
+ (left, right) => left - right,
223
+ );
224
+ const result: TaggedInterval[] = [];
225
+ for (let index = 0; index + 1 < boundaries.length; index++) {
226
+ const start = boundaries[index];
227
+ const end = boundaries[index + 1];
228
+ if (start === undefined || end === undefined || end <= start) continue;
229
+ const sources = [
230
+ ...new Set(
231
+ sanitized
232
+ .filter((interval) => interval.start < end && interval.end > start)
233
+ .flatMap((interval) => interval.sources),
234
+ ),
235
+ ].sort((left, right) => left - right);
236
+ if (sources.length === 0) continue;
237
+ const previous = result.at(-1);
238
+ if (previous && previous.end === start && sameSources(previous.sources, sources)) {
239
+ previous.end = end;
240
+ } else result.push({ start, end, sources });
241
+ }
242
+ return result;
243
+ }
244
+
245
+ function addThoughtInterval(
246
+ coverage: ThoughtCoverage,
247
+ interval: Interval,
248
+ source: number,
249
+ ): boolean {
250
+ if (coverage.incomplete) return false;
251
+ const candidate = normalizeTaggedIntervals([
252
+ ...coverage.intervals,
253
+ { ...interval, sources: [source] },
254
+ ]);
255
+ if (candidate.length > MAX_THOUGHT_INTERVALS) {
256
+ coverage.incomplete = true;
257
+ return false;
258
+ }
259
+ coverage.intervals = candidate;
260
+ return true;
261
+ }
262
+
263
+ export function subtractThoughtIntervalsWithinCap(
264
+ source: readonly Interval[],
265
+ removed: readonly Interval[],
266
+ ): { intervals: Interval[]; incomplete: boolean } {
267
+ let result = unionIntervals(source);
268
+ for (const cut of unionIntervals(removed)) {
269
+ result = result.flatMap((part) => {
270
+ if (cut.end <= part.start || cut.start >= part.end) return [part];
271
+ return [
272
+ ...(cut.start > part.start ? [{ start: part.start, end: cut.start }] : []),
273
+ ...(cut.end < part.end ? [{ start: cut.end, end: part.end }] : []),
274
+ ];
275
+ });
276
+ }
277
+ return {
278
+ intervals: result.slice(0, MAX_THOUGHT_INTERVALS),
279
+ incomplete: result.length > MAX_THOUGHT_INTERVALS,
280
+ };
281
+ }
282
+
283
+ function cappedTaggedDifference(
284
+ source: readonly TaggedInterval[],
285
+ removed: readonly Interval[],
286
+ ): { intervals: TaggedInterval[]; incomplete: boolean } {
287
+ let incomplete = false;
288
+ const fragments = source.flatMap((interval) => {
289
+ const difference = subtractThoughtIntervalsWithinCap([interval], removed);
290
+ incomplete ||= difference.incomplete;
291
+ return difference.intervals.map((fragment) => ({
292
+ ...fragment,
293
+ sources: interval.sources,
294
+ }));
295
+ });
296
+ const normalized = normalizeTaggedIntervals(fragments);
297
+ return {
298
+ intervals: normalized.slice(0, MAX_THOUGHT_INTERVALS),
299
+ incomplete: incomplete || normalized.length > MAX_THOUGHT_INTERVALS,
300
+ };
301
+ }
302
+
303
+ function hashUnits(value: string, start: number, end: number, seed = HASH_OFFSET): number {
304
+ let hash = seed;
305
+ for (let index = start; index < end; index++)
306
+ hash = Math.imul(hash ^ value.charCodeAt(index), 0x01000193) >>> 0;
307
+ return hash;
308
+ }
309
+
310
+ function appendCodePoints(
311
+ value: string,
312
+ pendingHighSurrogate: boolean,
313
+ ): { codePoints: number; pendingHighSurrogate: boolean } {
314
+ let codePoints = 0;
315
+ let index = 0;
316
+ if (pendingHighSurrogate) {
317
+ if (value.length === 0) return { codePoints, pendingHighSurrogate };
318
+ const first = value.charCodeAt(0);
319
+ codePoints++;
320
+ if (first >= 0xdc00 && first <= 0xdfff) index = 1;
321
+ }
322
+ for (; index < value.length; index++) {
323
+ const unit = value.charCodeAt(index);
324
+ if (unit >= 0xd800 && unit <= 0xdbff) {
325
+ if (index + 1 === value.length) return { codePoints, pendingHighSurrogate: true };
326
+ const next = value.charCodeAt(index + 1);
327
+ codePoints++;
328
+ if (next >= 0xdc00 && next <= 0xdfff) index++;
329
+ } else codePoints++;
330
+ }
331
+ return { codePoints, pendingHighSurrogate: false };
332
+ }
333
+
334
+ function contentFingerprintMatches(block: ContentBlock, snapshot: string): boolean {
335
+ if (block.headUnits > snapshot.length) return false;
336
+ if (hashUnits(snapshot, 0, block.headUnits) !== block.headHash) return false;
337
+ const tailStart = block.acceptedLength - block.tail.length;
338
+ return snapshot.slice(tailStart, block.acceptedLength) === block.tail;
339
+ }
340
+
341
+ type ValidatedEvent = {
342
+ type: "text_delta" | "thinking_delta" | "toolcall_delta" | "thinking_start" | "thinking_end";
343
+ contentIndex: number;
344
+ delta?: string;
345
+ cumulative?: string;
346
+ oversizedDelta: boolean;
347
+ oversizedSnapshot: boolean;
348
+ };
349
+
350
+ function validateEvent(
351
+ event: AssistantMessageEvent | undefined,
352
+ ): ValidatedEvent | undefined | false {
353
+ if (!event) return undefined;
354
+ if (
355
+ event.type !== "text_delta" &&
356
+ event.type !== "thinking_delta" &&
357
+ event.type !== "toolcall_delta" &&
358
+ event.type !== "thinking_start" &&
359
+ event.type !== "thinking_end"
360
+ )
361
+ return undefined;
362
+ const record = event as unknown as Record<string, unknown>;
363
+ if (
364
+ !Number.isSafeInteger(record.contentIndex) ||
365
+ (record.contentIndex as number) < 0 ||
366
+ (record.contentIndex as number) > MAX_CONTENT_INDEX
367
+ )
368
+ return false;
369
+ const partial = record.partial;
370
+ if (!partial || typeof partial !== "object" || Array.isArray(partial)) return false;
371
+ const content = (partial as Record<string, unknown>).content;
372
+ if (!Array.isArray(content)) return false;
373
+ const contentIndex = record.contentIndex as number;
374
+ const isDelta =
375
+ event.type === "text_delta" ||
376
+ event.type === "thinking_delta" ||
377
+ event.type === "toolcall_delta";
378
+ if (isDelta && typeof record.delta !== "string") return false;
379
+ const expectedType =
380
+ event.type === "text_delta"
381
+ ? "text"
382
+ : event.type === "toolcall_delta"
383
+ ? "toolCall"
384
+ : "thinking";
385
+ let cumulative: string | undefined;
386
+ if (event.type === "toolcall_delta" && !(contentIndex in content)) return false;
387
+ if (contentIndex in content) {
388
+ const item = content[contentIndex];
389
+ if (!item || typeof item !== "object" || Array.isArray(item)) return false;
390
+ const itemRecord = item as Record<string, unknown>;
391
+ if (itemRecord.type !== expectedType) return false;
392
+ if (expectedType === "toolCall") {
393
+ if (
394
+ typeof itemRecord.id !== "string" ||
395
+ typeof itemRecord.name !== "string" ||
396
+ !itemRecord.arguments ||
397
+ typeof itemRecord.arguments !== "object" ||
398
+ Array.isArray(itemRecord.arguments)
399
+ )
400
+ return false;
401
+ } else {
402
+ const field = expectedType === "text" ? "text" : "thinking";
403
+ if (typeof itemRecord[field] !== "string") return false;
404
+ cumulative = itemRecord[field] as string;
405
+ }
406
+ }
407
+ const delta = isDelta ? (record.delta as string) : undefined;
408
+ return {
409
+ type: event.type,
410
+ contentIndex,
411
+ delta,
412
+ cumulative,
413
+ oversizedDelta: (delta?.length ?? 0) > MAX_DELTA_UNITS,
414
+ oversizedSnapshot: (cumulative?.length ?? 0) > MAX_SNAPSHOT_UNITS,
415
+ };
416
+ }
417
+
418
+ /** Owns provider-usage snapshots and thought-time partitioning for one settled interaction. */
419
+ export class InteractionMetricsTracker {
420
+ private interaction?: InteractionState;
421
+
422
+ constructor(private readonly now: () => number = Date.now) {}
423
+
424
+ agentStart(now = this.now()): { interactionStarted: boolean } {
425
+ const at = safeTime(now);
426
+ const interactionStarted = this.interaction === undefined;
427
+ if (!this.interaction) {
428
+ this.interaction = {
429
+ startedAt: at,
430
+ runOrdinal: 0,
431
+ runs: [],
432
+ foldedCommitted: { input: 0, output: 0 },
433
+ thoughtCoverage: { intervals: [], incomplete: false },
434
+ };
435
+ }
436
+ const previous = this.interaction.activeRun;
437
+ if (previous && !previous.ended) {
438
+ if (previous.response) {
439
+ this.commitSnapshot(previous, previous.response);
440
+ this.closeResponse(previous, previous.response, at);
441
+ }
442
+ previous.ended = true;
443
+ }
444
+ this.makeRunCapacity(this.interaction);
445
+ const ordinal = safeAdd(this.interaction.runOrdinal, 1);
446
+ this.interaction.runOrdinal = ordinal;
447
+ const run: RunState = {
448
+ ordinal,
449
+ startedAt: at,
450
+ ended: false,
451
+ committed: { input: 0, output: 0 },
452
+ responseOrdinal: 0,
453
+ committedResponseIds: new Set(),
454
+ responseLessClosed: false,
455
+ };
456
+ this.interaction.runs.push(run);
457
+ this.interaction.activeRun = run;
458
+ return { interactionStarted };
459
+ }
460
+
461
+ turnStart(now = this.now()): void {
462
+ const run = this.activeRun();
463
+ if (!run) return;
464
+ if (run.response) {
465
+ this.commitSnapshot(run, run.response);
466
+ this.closeResponse(run, run.response, now);
467
+ }
468
+ this.openResponse(run);
469
+ }
470
+
471
+ messageUpdate(
472
+ message: InteractionMessage,
473
+ event?: AssistantMessageEvent,
474
+ now = this.now(),
475
+ ): MetricsUpdateResult {
476
+ const unchanged: MetricsUpdateResult = {
477
+ displayTokens: this.currentDisplayTokens(),
478
+ usageChanged: false,
479
+ thoughtChanged: false,
480
+ };
481
+ const run = this.activeRun();
482
+ if (!run || message.role !== "assistant") return unchanged;
483
+ const validated = validateEvent(event);
484
+ if (validated === false) return unchanged;
485
+ const tokens = parseAssistantMessageTokens(message);
486
+ if (!tokens && !validated) return unchanged;
487
+ const response = this.responseForUpdate(run, message);
488
+ if (!response) return unchanged;
489
+ let thoughtChanged = false;
490
+ let contentChanged = false;
491
+ if (validated) {
492
+ if (validated.type.startsWith("thinking"))
493
+ thoughtChanged = this.applyThoughtEvent(run, response, validated, now);
494
+ if (
495
+ validated.type === "text_delta" ||
496
+ validated.type === "thinking_delta" ||
497
+ validated.type === "toolcall_delta"
498
+ )
499
+ contentChanged = this.applyContentEvent(response, validated);
500
+ }
501
+ const usageChanged = tokens ? this.applyLiveUsage(response, tokens) : false;
502
+ const display = this.liveDisplay(response);
503
+ const displayChanged = !this.sameDisplay(response.lastDisplay, display);
504
+ response.lastDisplay = display;
505
+ return {
506
+ displayTokens: display,
507
+ usageChanged: usageChanged || contentChanged || displayChanged,
508
+ thoughtChanged,
509
+ };
510
+ }
511
+
512
+ messageEnd(message: InteractionMessage, now = this.now()): MessageEndResult {
513
+ const run = this.activeRun();
514
+ if (!run || message.role !== "assistant") return { status: "rejected" };
515
+ const tokens = parseAssistantMessageTokens(message);
516
+ const key = responseIdKey(message);
517
+ const response = run.response;
518
+ if (!response?.authorized) return { status: "rejected" };
519
+ if (key && this.isDuplicateId(run, key)) return { status: "duplicate" };
520
+ if (response.responseIdKey && key && response.responseIdKey !== key)
521
+ return { status: "rejected" };
522
+ if (!response.responseIdKey && key) response.responseIdKey = key;
523
+ const preCloseDisplay = this.liveDisplay(response);
524
+ let source: "final" | "last-snapshot" | "no-snapshot";
525
+ if (tokens) {
526
+ response.snapshot = tokens;
527
+ response.hasSnapshot = true;
528
+ source = "final";
529
+ } else source = response.hasSnapshot ? "last-snapshot" : "no-snapshot";
530
+ this.commitSnapshot(run, response);
531
+ const committed = this.totalTokens();
532
+ const preserveEstimate = source !== "final" && response.lastDisplay?.outputApproximate === true;
533
+ const displayTokens = preserveEstimate
534
+ ? { ...preCloseDisplay, outputApproximate: true }
535
+ : { ...committed, outputApproximate: false };
536
+ run.lastClosedDisplay = preserveEstimate ? displayTokens : undefined;
537
+ this.closeResponse(run, response, now);
538
+ return { status: "accepted", tokens: committed, displayTokens, source };
539
+ }
540
+
541
+ agentEnd(now = this.now()): void {
542
+ const interaction = this.interaction;
543
+ const run = interaction?.activeRun;
544
+ if (!interaction || !run) return;
545
+ if (run.response) {
546
+ this.commitSnapshot(run, run.response);
547
+ this.closeResponse(run, run.response, now);
548
+ }
549
+ run.ended = true;
550
+ interaction.activeRun = undefined;
551
+ }
552
+
553
+ currentTokens(): InteractionTokens | undefined {
554
+ return this.interaction ? this.totalTokens() : undefined;
555
+ }
556
+
557
+ currentDisplayTokens(): LiveTokenDisplay | undefined {
558
+ if (!this.interaction) return undefined;
559
+ const run = this.interaction.activeRun;
560
+ if (run?.response) return this.liveDisplay(run.response);
561
+ return run?.lastClosedDisplay ?? { ...this.totalTokens(), outputApproximate: false };
562
+ }
563
+
564
+ currentThought(now = this.now()): ThoughtSnapshot | undefined {
565
+ if (!this.interaction) return undefined;
566
+ return this.thoughtSnapshot(this.interaction, now);
567
+ }
568
+
569
+ settle(idle: boolean, now = this.now()): SettlementResult | undefined {
570
+ const interaction = this.interaction;
571
+ if (!interaction) return undefined;
572
+ const at = safeTime(now);
573
+ const splitIndex = idle
574
+ ? interaction.runs.length
575
+ : interaction.runs.findIndex((run) => !run.ended);
576
+ const settledCount = splitIndex < 0 ? interaction.runs.length : splitIndex;
577
+ const hasFolded =
578
+ interaction.foldedCommitted.input > 0 ||
579
+ interaction.foldedCommitted.output > 0 ||
580
+ interaction.thoughtCoverage.intervals.some((interval) => interval.sources.includes(0));
581
+ if (settledCount === 0 && !hasFolded) return undefined;
582
+ const settledRuns = interaction.runs.slice(0, settledCount);
583
+ for (const run of interaction.runs) this.retainOpenThought(run, at);
584
+ for (const run of settledRuns) {
585
+ if (run.response) this.closeResponse(run, run.response, at);
586
+ }
587
+ const settledSources = new Set([0, ...settledRuns.map((run) => run.ordinal)]);
588
+ const settledIntervals = interaction.thoughtCoverage.intervals.filter((interval) =>
589
+ interval.sources.some((source) => settledSources.has(source)),
590
+ );
591
+ let tokens = interaction.foldedCommitted;
592
+ for (const run of settledRuns) tokens = addTokens(tokens, this.runTokens(run));
593
+ const summary: TurnSummaryMetrics = {
594
+ durationMs: durationMs(interaction.startedAt, now),
595
+ thoughtDurationMs: intervalDuration(settledIntervals),
596
+ input: tokens.input,
597
+ output: tokens.output,
598
+ };
599
+ const remaining = interaction.runs.slice(settledCount);
600
+ if (remaining.length === 0) {
601
+ this.interaction = undefined;
602
+ return { summary };
603
+ }
604
+ const remainingSources = new Set(remaining.map((run) => run.ordinal));
605
+ const remainingCoverage = interaction.thoughtCoverage.intervals
606
+ .map((interval) => ({
607
+ ...interval,
608
+ sources: interval.sources.filter((source) => remainingSources.has(source)),
609
+ }))
610
+ .filter((interval) => interval.sources.length > 0);
611
+ const difference = cappedTaggedDifference(remainingCoverage, settledIntervals);
612
+ for (const run of remaining) {
613
+ for (const block of run.response?.thoughtBlocks.values() ?? []) {
614
+ if (block.startedAt !== undefined && !block.closed) block.startedAt = at;
615
+ }
616
+ }
617
+ const nextStartedAt = remaining[0]?.startedAt ?? at;
618
+ this.interaction = {
619
+ startedAt: nextStartedAt,
620
+ runOrdinal: interaction.runOrdinal,
621
+ runs: remaining,
622
+ activeRun: remaining.find((run) => !run.ended),
623
+ foldedCommitted: { input: 0, output: 0 },
624
+ thoughtCoverage: {
625
+ intervals: difference.intervals,
626
+ incomplete: interaction.thoughtCoverage.incomplete || difference.incomplete,
627
+ },
628
+ };
629
+ return {
630
+ summary,
631
+ nextStartedAt,
632
+ nextTokens: this.currentDisplayTokens(),
633
+ nextThought: this.currentThought(at),
634
+ };
635
+ }
636
+
637
+ shutdown(): void {
638
+ this.interaction = undefined;
639
+ }
640
+
641
+ diagnostics(): InteractionMetricsDiagnostics {
642
+ const interaction = this.interaction;
643
+ const responses =
644
+ interaction?.runs.flatMap((run) => (run.response ? [run.response] : [])) ?? [];
645
+ const blocks = responses.flatMap((response) => [...response.contentBlocks.values()]);
646
+ return {
647
+ runs: interaction?.runs.length ?? 0,
648
+ responseIds:
649
+ interaction?.runs.reduce((total, run) => total + run.committedResponseIds.size, 0) ?? 0,
650
+ activeResponses: responses.length,
651
+ contentBlocks: blocks.length,
652
+ thoughtBlocks: responses.reduce((total, response) => total + response.thoughtBlocks.size, 0),
653
+ thoughtIntervals: interaction?.thoughtCoverage.intervals.length ?? 0,
654
+ maxContentTailUnits: blocks.reduce(
655
+ (maximum, block) => Math.max(maximum, block.tail.length),
656
+ 0,
657
+ ),
658
+ retainedContentUnits: blocks.reduce((total, block) => total + block.tail.length, 0),
659
+ estimateIncomplete: responses.some((response) => response.estimateIncomplete),
660
+ };
661
+ }
662
+
663
+ private activeRun(): RunState | undefined {
664
+ const run = this.interaction?.activeRun;
665
+ return run && !run.ended ? run : undefined;
666
+ }
667
+
668
+ private makeRunCapacity(interaction: InteractionState): void {
669
+ if (interaction.runs.length < MAX_RUNS) return;
670
+ const index = interaction.runs.findIndex((run) => run.ended);
671
+ if (index < 0) return;
672
+ const [run] = interaction.runs.splice(index, 1);
673
+ if (!run) return;
674
+ interaction.foldedCommitted = addTokens(interaction.foldedCommitted, this.runTokens(run));
675
+ interaction.thoughtCoverage.intervals = normalizeTaggedIntervals(
676
+ interaction.thoughtCoverage.intervals.map((interval) => ({
677
+ ...interval,
678
+ sources: interval.sources.map((source) => (source === run.ordinal ? 0 : source)),
679
+ })),
680
+ );
681
+ }
682
+
683
+ private openResponse(run: RunState, key?: string): ResponseState {
684
+ run.responseOrdinal = safeAdd(run.responseOrdinal, 1);
685
+ run.lastClosedDisplay = undefined;
686
+ const estimateEnabled =
687
+ run.ordinal <= MAX_RUNS && run.responseOrdinal <= MAX_ESTIMATED_RESPONSES;
688
+ const response: ResponseState = {
689
+ ordinal: run.responseOrdinal,
690
+ authorized: true,
691
+ responseIdKey: key,
692
+ snapshot: { input: 0, output: 0 },
693
+ hasSnapshot: false,
694
+ thoughtBlocks: new Map(),
695
+ contentBlocks: new Map(),
696
+ generatedCodePoints: 0,
697
+ outputAnchor: 0,
698
+ outputAnchorCodePoints: 0,
699
+ liveOutputFloor: 0,
700
+ estimateIncomplete: !estimateEnabled,
701
+ estimateEnabled,
702
+ };
703
+ run.response = response;
704
+ run.responseLessClosed = false;
705
+ return response;
706
+ }
707
+
708
+ private responseForUpdate(run: RunState, message: InteractionMessage): ResponseState | undefined {
709
+ const key = responseIdKey(message);
710
+ const response = run.response;
711
+ if (!response?.authorized || (key && this.isDuplicateId(run, key))) return undefined;
712
+ if (response.responseIdKey && key && response.responseIdKey !== key) return undefined;
713
+ if (!response.responseIdKey && key) response.responseIdKey = key;
714
+ return response;
715
+ }
716
+
717
+ private isDuplicateId(run: RunState, key: string): boolean {
718
+ return run.committedResponseIds.has(key) || run.lastClosedResponseId === key;
719
+ }
720
+
721
+ private applyContentEvent(response: ResponseState, event: ValidatedEvent): boolean {
722
+ if (!response.estimateEnabled) {
723
+ response.estimateIncomplete = true;
724
+ return false;
725
+ }
726
+ if (event.oversizedDelta || event.oversizedSnapshot) {
727
+ response.estimateIncomplete = true;
728
+ const existing = response.contentBlocks.get(
729
+ `${event.type === "text_delta" ? "text" : event.type === "toolcall_delta" ? "toolcall" : "thinking"}:${event.contentIndex}`,
730
+ );
731
+ if (existing) existing.frozen = true;
732
+ return false;
733
+ }
734
+ const contentType =
735
+ event.type === "text_delta"
736
+ ? "text"
737
+ : event.type === "toolcall_delta"
738
+ ? "toolcall"
739
+ : "thinking";
740
+ const key = `${contentType}:${event.contentIndex}`;
741
+ let block = response.contentBlocks.get(key);
742
+ if (!block) {
743
+ if (response.contentBlocks.size >= MAX_CONTENT_BLOCKS) {
744
+ response.estimateIncomplete = true;
745
+ return false;
746
+ }
747
+ block = {
748
+ acceptedLength: 0,
749
+ codePoints: 0,
750
+ pendingHighSurrogate: false,
751
+ headHash: HASH_OFFSET,
752
+ headUnits: 0,
753
+ tail: "",
754
+ frozen: false,
755
+ };
756
+ response.contentBlocks.set(key, block);
757
+ }
758
+ if (block.frozen) return false;
759
+ let suffix = event.delta ?? "";
760
+ if (event.cumulative !== undefined) {
761
+ const snapshot = event.cumulative;
762
+ if (snapshot.length < block.acceptedLength) return false;
763
+ if (!contentFingerprintMatches(block, snapshot)) {
764
+ block.frozen = true;
765
+ response.estimateIncomplete = true;
766
+ return false;
767
+ }
768
+ if (snapshot.length === block.acceptedLength) return false;
769
+ if (snapshot.length - block.acceptedLength > MAX_DELTA_UNITS) {
770
+ block.frozen = true;
771
+ response.estimateIncomplete = true;
772
+ return false;
773
+ }
774
+ suffix = snapshot.slice(block.acceptedLength);
775
+ }
776
+ if (suffix.length === 0) return false;
777
+ if (block.acceptedLength + suffix.length > MAX_SNAPSHOT_UNITS) {
778
+ block.frozen = true;
779
+ response.estimateIncomplete = true;
780
+ return false;
781
+ }
782
+ const counted = appendCodePoints(suffix, block.pendingHighSurrogate);
783
+ if (response.generatedCodePoints + counted.codePoints > MAX_GENERATED_CODE_POINTS) {
784
+ block.frozen = true;
785
+ response.estimateIncomplete = true;
786
+ return false;
787
+ }
788
+ if (block.headUnits < HEAD_UNITS) {
789
+ const taken = Math.min(HEAD_UNITS - block.headUnits, suffix.length);
790
+ block.headHash = hashUnits(suffix, 0, taken, block.headHash);
791
+ block.headUnits += taken;
792
+ }
793
+ block.tail =
794
+ suffix.length >= TAIL_UNITS
795
+ ? suffix.slice(-TAIL_UNITS)
796
+ : `${block.tail}${suffix}`.slice(-TAIL_UNITS);
797
+ block.acceptedLength += suffix.length;
798
+ block.codePoints += counted.codePoints;
799
+ block.pendingHighSurrogate = counted.pendingHighSurrogate;
800
+ response.generatedCodePoints += counted.codePoints;
801
+ return true;
802
+ }
803
+
804
+ private applyThoughtEvent(
805
+ run: RunState,
806
+ response: ResponseState,
807
+ event: ValidatedEvent,
808
+ now: number,
809
+ ): boolean {
810
+ const at = safeTime(now);
811
+ let block = response.thoughtBlocks.get(event.contentIndex);
812
+ if (!block) {
813
+ if (response.thoughtBlocks.size >= MAX_THOUGHT_INTERVALS) return false;
814
+ block = { closed: false };
815
+ response.thoughtBlocks.set(event.contentIndex, block);
816
+ }
817
+ if (event.type === "thinking_end") {
818
+ if (block.closed || block.startedAt === undefined) {
819
+ block.closed = true;
820
+ return false;
821
+ }
822
+ this.closeBlock(run, block, at);
823
+ return true;
824
+ }
825
+ if (block.closed || block.startedAt !== undefined) return false;
826
+ block.startedAt = at;
827
+ return true;
828
+ }
829
+
830
+ private closeBlock(run: RunState, block: ThoughtBlock, now: number): void {
831
+ if (block.closed) return;
832
+ block.closed = true;
833
+ if (block.startedAt === undefined) return;
834
+ const end = safeTime(now);
835
+ if (end <= block.startedAt || !this.interaction) return;
836
+ addThoughtInterval(
837
+ this.interaction.thoughtCoverage,
838
+ { start: block.startedAt, end },
839
+ run.ordinal,
840
+ );
841
+ }
842
+
843
+ private closeResponse(run: RunState, response: ResponseState, now: number): void {
844
+ if (run.response !== response) return;
845
+ for (const block of response.thoughtBlocks.values()) this.closeBlock(run, block, now);
846
+ if (response.responseIdKey) {
847
+ if (run.committedResponseIds.size < MAX_RESPONSE_IDS)
848
+ run.committedResponseIds.add(response.responseIdKey);
849
+ run.lastClosedResponseId = response.responseIdKey;
850
+ } else run.responseLessClosed = true;
851
+ response.authorized = false;
852
+ response.contentBlocks.clear();
853
+ response.thoughtBlocks.clear();
854
+ run.response = undefined;
855
+ }
856
+
857
+ private commitSnapshot(run: RunState, response: ResponseState): void {
858
+ if (!response.hasSnapshot) return;
859
+ run.committed = addTokens(run.committed, response.snapshot);
860
+ response.hasSnapshot = false;
861
+ }
862
+
863
+ private applyLiveUsage(response: ResponseState, tokens: InteractionTokens): boolean {
864
+ if (tokens.input === 0 && tokens.output === 0) return false;
865
+ if (
866
+ response.hasSnapshot &&
867
+ (tokens.input < response.snapshot.input || tokens.output < response.snapshot.output)
868
+ )
869
+ return false;
870
+ if (
871
+ response.hasSnapshot &&
872
+ tokens.input === response.snapshot.input &&
873
+ tokens.output === response.snapshot.output
874
+ )
875
+ return false;
876
+ const outputAdvanced = !response.hasSnapshot || tokens.output > response.snapshot.output;
877
+ response.snapshot = tokens;
878
+ response.hasSnapshot = true;
879
+ if (outputAdvanced) {
880
+ response.outputAnchor = tokens.output;
881
+ response.outputAnchorCodePoints = response.generatedCodePoints;
882
+ }
883
+ return true;
884
+ }
885
+
886
+ private liveDisplay(response: ResponseState): LiveTokenDisplay {
887
+ const committed = this.committedTokens();
888
+ const postAnchorCodePoints = Math.max(
889
+ 0,
890
+ response.generatedCodePoints - response.outputAnchorCodePoints,
891
+ );
892
+ const candidate = safeAdd(response.outputAnchor, estimatedTokens(postAnchorCodePoints));
893
+ const responseOutput = Math.max(response.liveOutputFloor, candidate);
894
+ const exactCoverage =
895
+ response.hasSnapshot &&
896
+ postAnchorCodePoints === 0 &&
897
+ responseOutput === response.outputAnchor &&
898
+ !response.estimateIncomplete;
899
+ response.liveOutputFloor = responseOutput;
900
+ return {
901
+ input: safeAdd(committed.input, response.hasSnapshot ? response.snapshot.input : 0),
902
+ output: safeAdd(committed.output, responseOutput),
903
+ outputApproximate: !exactCoverage,
904
+ };
905
+ }
906
+
907
+ private runTokens(run: RunState): InteractionTokens {
908
+ return run.response?.hasSnapshot
909
+ ? addTokens(run.committed, run.response.snapshot)
910
+ : run.committed;
911
+ }
912
+
913
+ private committedTokens(): InteractionTokens {
914
+ const interaction = this.interaction;
915
+ if (!interaction) return { input: 0, output: 0 };
916
+ let total = interaction.foldedCommitted;
917
+ for (const run of interaction.runs) total = addTokens(total, run.committed);
918
+ return total;
919
+ }
920
+
921
+ private totalTokens(): InteractionTokens {
922
+ const interaction = this.interaction;
923
+ if (!interaction) return { input: 0, output: 0 };
924
+ let total = interaction.foldedCommitted;
925
+ for (const run of interaction.runs) total = addTokens(total, this.runTokens(run));
926
+ return total;
927
+ }
928
+
929
+ private retainOpenThought(run: RunState, now: number): void {
930
+ const end = safeTime(now);
931
+ for (const block of run.response?.thoughtBlocks.values() ?? []) {
932
+ if (block.startedAt === undefined || block.closed || end <= block.startedAt) continue;
933
+ if (this.interaction) {
934
+ addThoughtInterval(
935
+ this.interaction.thoughtCoverage,
936
+ { start: block.startedAt, end },
937
+ run.ordinal,
938
+ );
939
+ }
940
+ block.startedAt = end;
941
+ }
942
+ }
943
+
944
+ private thoughtSnapshot(interaction: InteractionState, now: number): ThoughtSnapshot {
945
+ const coverage: ThoughtCoverage = {
946
+ intervals: interaction.thoughtCoverage.intervals.map((interval) => ({
947
+ ...interval,
948
+ sources: [...interval.sources],
949
+ })),
950
+ incomplete: interaction.thoughtCoverage.incomplete,
951
+ };
952
+ for (const run of interaction.runs) {
953
+ for (const block of run.response?.thoughtBlocks.values() ?? []) {
954
+ if (block.startedAt === undefined || block.closed) continue;
955
+ addThoughtInterval(coverage, { start: block.startedAt, end: safeTime(now) }, run.ordinal);
956
+ }
957
+ }
958
+ return {
959
+ durationMs: intervalDuration(coverage.intervals),
960
+ active: interaction.runs.some((run) =>
961
+ [...(run.response?.thoughtBlocks.values() ?? [])].some(
962
+ (block) => block.startedAt !== undefined && !block.closed,
963
+ ),
964
+ ),
965
+ };
966
+ }
967
+
968
+ private sameDisplay(
969
+ left: LiveTokenDisplay | undefined,
970
+ right: LiveTokenDisplay | undefined,
971
+ ): boolean {
972
+ return (
973
+ left?.input === right?.input &&
974
+ left?.output === right?.output &&
975
+ left?.outputApproximate === right?.outputApproximate
976
+ );
977
+ }
978
+ }
979
+
980
+ function hasExactKeys(record: Record<string, unknown>, expected: readonly string[]): boolean {
981
+ return Object.keys(record).sort().join("\0") === [...expected].sort().join("\0");
982
+ }
983
+
984
+ export function isTurnSummaryData(value: unknown): value is TurnSummaryData {
985
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
986
+ const record = value as Record<string, unknown>;
987
+ if (
988
+ !isNonnegativeSafeInteger(record.durationMs) ||
989
+ !isNonnegativeSafeInteger(record.input) ||
990
+ !isNonnegativeSafeInteger(record.output)
991
+ )
992
+ return false;
993
+ if (record.version === 1) {
994
+ return hasExactKeys(record, ["version", "durationMs", "input", "output"]);
995
+ }
996
+ if (record.version === 2) {
997
+ return (
998
+ hasExactKeys(record, ["version", "durationMs", "input", "output", "stylePrefix"]) &&
999
+ isSafeSgrStylePrefix(record.stylePrefix)
1000
+ );
1001
+ }
1002
+ return (
1003
+ record.version === TURN_SUMMARY_VERSION &&
1004
+ hasExactKeys(record, [
1005
+ "version",
1006
+ "durationMs",
1007
+ "thoughtDurationMs",
1008
+ "input",
1009
+ "output",
1010
+ "stylePrefix",
1011
+ ]) &&
1012
+ isNonnegativeSafeInteger(record.thoughtDurationMs) &&
1013
+ isSafeSgrStylePrefix(record.stylePrefix)
1014
+ );
1015
+ }
1016
+
1017
+ export function formatTurnSummary(data: TurnSummaryData | TurnSummaryMetrics): string {
1018
+ const thought =
1019
+ "thoughtDurationMs" in data && data.thoughtDurationMs > 0
1020
+ ? ` · thought for ${formatElapsedDuration(data.thoughtDurationMs)}`
1021
+ : "";
1022
+ return ` Turn took ${formatElapsedDuration(data.durationMs)}${thought} · ↑${formatCount(data.input)} ↓${formatCount(data.output)}`;
1023
+ }
1024
+
1025
+ export function renderTurnSummaryEntry(
1026
+ entry: { data?: unknown },
1027
+ themeOptions: unknown,
1028
+ theme: Pick<Theme, "fg">,
1029
+ ): Text | undefined {
1030
+ if (!isTurnSummaryData(entry.data)) return undefined;
1031
+ const text = formatTurnSummary(entry.data);
1032
+ if (entry.data.version === 2 || entry.data.version === 3) {
1033
+ return new Text(`${entry.data.stylePrefix}${text}${SGR_RESET}`, 0, 0);
1034
+ }
1035
+ const options = (themeOptions ?? {}) as {
1036
+ colorSource?: ColorSource;
1037
+ workingLineHigh?: ColorSpec;
1038
+ };
1039
+ return new Text(
1040
+ `${renderWorkingLineHigh(
1041
+ theme,
1042
+ options.colorSource === "terminal" ? "terminal" : "theme",
1043
+ options.workingLineHigh,
1044
+ text,
1045
+ )}${SGR_RESET}`,
1046
+ 0,
1047
+ 0,
1048
+ );
1049
+ }