@selesai/code 0.9.15 → 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 (267) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +6 -9
  3. package/dist/defaults/models.json +2 -2
  4. package/dist/defaults/settings.json +3 -7
  5. package/dist/extensions/package.json +2 -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/package.json +3 -3
  193. package/dist/extensions/auto-model/auto-model.test.ts +0 -18
  194. package/dist/extensions/auto-model/classifier.ts +0 -23
  195. package/dist/extensions/auto-model/config.ts +0 -51
  196. package/dist/extensions/auto-model/index.ts +0 -22
  197. package/dist/extensions/auto-model/lifecycle.test.ts +0 -24
  198. package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +0 -27
  199. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +0 -687
  200. package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +0 -16
  201. package/dist/extensions/pi-powerline-footer/README.md +0 -513
  202. package/dist/extensions/pi-powerline-footer/banner.png +0 -0
  203. package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +0 -586
  204. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +0 -583
  205. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +0 -204
  206. package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +0 -286
  207. package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +0 -108
  208. package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +0 -60
  209. package/dist/extensions/pi-powerline-footer/cd-command.ts +0 -190
  210. package/dist/extensions/pi-powerline-footer/colors.ts +0 -70
  211. package/dist/extensions/pi-powerline-footer/context-usage.ts +0 -156
  212. package/dist/extensions/pi-powerline-footer/currency-rates.ts +0 -142
  213. package/dist/extensions/pi-powerline-footer/editor-composition.ts +0 -32
  214. package/dist/extensions/pi-powerline-footer/editor-performance.ts +0 -99
  215. package/dist/extensions/pi-powerline-footer/git-status.ts +0 -329
  216. package/dist/extensions/pi-powerline-footer/guide.ts +0 -220
  217. package/dist/extensions/pi-powerline-footer/icons.ts +0 -190
  218. package/dist/extensions/pi-powerline-footer/index.ts +0 -3428
  219. package/dist/extensions/pi-powerline-footer/lifecycle.ts +0 -10
  220. package/dist/extensions/pi-powerline-footer/package.json +0 -49
  221. package/dist/extensions/pi-powerline-footer/paths.ts +0 -22
  222. package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -446
  223. package/dist/extensions/pi-powerline-footer/presets.ts +0 -102
  224. package/dist/extensions/pi-powerline-footer/queue/store.ts +0 -313
  225. package/dist/extensions/pi-powerline-footer/queue/types.ts +0 -53
  226. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +0 -29
  227. package/dist/extensions/pi-powerline-footer/segments.ts +0 -570
  228. package/dist/extensions/pi-powerline-footer/separators.ts +0 -57
  229. package/dist/extensions/pi-powerline-footer/session-usage.ts +0 -44
  230. package/dist/extensions/pi-powerline-footer/shortcuts.ts +0 -61
  231. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +0 -1905
  232. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +0 -144
  233. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +0 -156
  234. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +0 -344
  235. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +0 -48
  236. package/dist/extensions/pi-powerline-footer/tests/editor-performance.test.ts +0 -43
  237. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +0 -136
  238. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +0 -39
  239. package/dist/extensions/pi-powerline-footer/tests/git-optional-locks.test.ts +0 -64
  240. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +0 -92
  241. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +0 -129
  242. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -116
  243. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +0 -79
  244. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +0 -63
  245. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +0 -65
  246. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +0 -14
  247. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +0 -173
  248. package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +0 -47
  249. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +0 -37
  250. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +0 -76
  251. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +0 -249
  252. package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +0 -39
  253. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +0 -237
  254. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +0 -199
  255. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +0 -317
  256. package/dist/extensions/pi-powerline-footer/theme.example.json +0 -24
  257. package/dist/extensions/pi-powerline-footer/theme.json +0 -12
  258. package/dist/extensions/pi-powerline-footer/theme.ts +0 -241
  259. package/dist/extensions/pi-powerline-footer/token-stats.ts +0 -304
  260. package/dist/extensions/pi-powerline-footer/tsconfig.json +0 -13
  261. package/dist/extensions/pi-powerline-footer/types.ts +0 -235
  262. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +0 -34
  263. package/dist/extensions/pi-powerline-footer/welcome.ts +0 -709
  264. package/dist/extensions/pi-powerline-footer/working-vibes.ts +0 -721
  265. package/dist/extensions/preview-tools-disabled.test.ts +0 -37
  266. package/dist/extensions/preview-tools-disabled.ts +0 -11
  267. 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
+ }