@genesislcap/ai-assistant 15.19.0 → 15.19.1

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 (288) hide show
  1. package/package.json +17 -18
  2. package/api-extractor.json +0 -11
  3. package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts +0 -2
  4. package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts.map +0 -1
  5. package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts +0 -2
  6. package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts.map +0 -1
  7. package/dist/dts/components/chat-driver/chat-driver.test.d.ts +0 -2
  8. package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +0 -1
  9. package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts +0 -2
  10. package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts.map +0 -1
  11. package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts +0 -2
  12. package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts.map +0 -1
  13. package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts +0 -2
  14. package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts.map +0 -1
  15. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts +0 -2
  16. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts.map +0 -1
  17. package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts +0 -2
  18. package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts.map +0 -1
  19. package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts +0 -2
  20. package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts.map +0 -1
  21. package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts +0 -2
  22. package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts.map +0 -1
  23. package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts +0 -2
  24. package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts.map +0 -1
  25. package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts +0 -2
  26. package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts.map +0 -1
  27. package/dist/dts/config/define-stateful-agent.test.d.ts +0 -2
  28. package/dist/dts/config/define-stateful-agent.test.d.ts.map +0 -1
  29. package/dist/dts/config/validate-providers.test.d.ts +0 -2
  30. package/dist/dts/config/validate-providers.test.d.ts.map +0 -1
  31. package/dist/dts/main/blocked-state.test.d.ts +0 -2
  32. package/dist/dts/main/blocked-state.test.d.ts.map +0 -1
  33. package/dist/dts/main/budget-meter.test.d.ts +0 -2
  34. package/dist/dts/main/budget-meter.test.d.ts.map +0 -1
  35. package/dist/dts/main/cost-session-banking.test.d.ts +0 -2
  36. package/dist/dts/main/cost-session-banking.test.d.ts.map +0 -1
  37. package/dist/dts/main/file-attachments.test.d.ts +0 -2
  38. package/dist/dts/main/file-attachments.test.d.ts.map +0 -1
  39. package/dist/dts/main/interaction-cost.test.d.ts +0 -2
  40. package/dist/dts/main/interaction-cost.test.d.ts.map +0 -1
  41. package/dist/dts/main/main.styles.test.d.ts +0 -2
  42. package/dist/dts/main/main.styles.test.d.ts.map +0 -1
  43. package/dist/dts/main/persistence-broken-sources.test.d.ts +0 -2
  44. package/dist/dts/main/persistence-broken-sources.test.d.ts.map +0 -1
  45. package/dist/dts/main/popout-interaction-gate.test.d.ts +0 -2
  46. package/dist/dts/main/popout-interaction-gate.test.d.ts.map +0 -1
  47. package/dist/dts/state/ai-assistant-slice.test.d.ts +0 -2
  48. package/dist/dts/state/ai-assistant-slice.test.d.ts.map +0 -1
  49. package/dist/dts/state/debug-event-log.test.d.ts +0 -2
  50. package/dist/dts/state/debug-event-log.test.d.ts.map +0 -1
  51. package/dist/dts/state/driver-registry.test.d.ts +0 -2
  52. package/dist/dts/state/driver-registry.test.d.ts.map +0 -1
  53. package/dist/dts/state/interaction-context.test.d.ts +0 -2
  54. package/dist/dts/state/interaction-context.test.d.ts.map +0 -1
  55. package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts +0 -2
  56. package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts.map +0 -1
  57. package/dist/dts/state/persistence/diagnostics.test.d.ts +0 -2
  58. package/dist/dts/state/persistence/diagnostics.test.d.ts.map +0 -1
  59. package/dist/dts/state/persistence/session-persistence-provider.test.d.ts +0 -2
  60. package/dist/dts/state/persistence/session-persistence-provider.test.d.ts.map +0 -1
  61. package/dist/dts/state/persistence/session-persistence.integration.test.d.ts +0 -2
  62. package/dist/dts/state/persistence/session-persistence.integration.test.d.ts.map +0 -1
  63. package/dist/dts/state/persistence/session-persister.test.d.ts +0 -2
  64. package/dist/dts/state/persistence/session-persister.test.d.ts.map +0 -1
  65. package/dist/dts/state/persistence/session-snapshot.test.d.ts +0 -2
  66. package/dist/dts/state/persistence/session-snapshot.test.d.ts.map +0 -1
  67. package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts +0 -2
  68. package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts.map +0 -1
  69. package/dist/dts/utils/animation-exclusivity.test.d.ts +0 -2
  70. package/dist/dts/utils/animation-exclusivity.test.d.ts.map +0 -1
  71. package/dist/dts/utils/collect-session-models.test.d.ts +0 -2
  72. package/dist/dts/utils/collect-session-models.test.d.ts.map +0 -1
  73. package/dist/dts/utils/condense-history.test.d.ts +0 -2
  74. package/dist/dts/utils/condense-history.test.d.ts.map +0 -1
  75. package/dist/dts/utils/cost-session-history.test.d.ts +0 -2
  76. package/dist/dts/utils/cost-session-history.test.d.ts.map +0 -1
  77. package/dist/dts/utils/derive-cost-session-title.test.d.ts +0 -2
  78. package/dist/dts/utils/derive-cost-session-title.test.d.ts.map +0 -1
  79. package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts +0 -2
  80. package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts.map +0 -1
  81. package/dist/dts/utils/history-transform.test.d.ts +0 -2
  82. package/dist/dts/utils/history-transform.test.d.ts.map +0 -1
  83. package/dist/dts/utils/message-partition.test.d.ts +0 -2
  84. package/dist/dts/utils/message-partition.test.d.ts.map +0 -1
  85. package/dist/dts/utils/resolve-cost-history-config.test.d.ts +0 -2
  86. package/dist/dts/utils/resolve-cost-history-config.test.d.ts.map +0 -1
  87. package/dist/dts/utils/resolve-preference-baseline.test.d.ts +0 -2
  88. package/dist/dts/utils/resolve-preference-baseline.test.d.ts.map +0 -1
  89. package/dist/dts/utils/strip-agent-handlers.test.d.ts +0 -2
  90. package/dist/dts/utils/strip-agent-handlers.test.d.ts.map +0 -1
  91. package/dist/dts/utils/sum-costs.test.d.ts +0 -2
  92. package/dist/dts/utils/sum-costs.test.d.ts.map +0 -1
  93. package/dist/dts/utils/sum-tokens.test.d.ts +0 -2
  94. package/dist/dts/utils/sum-tokens.test.d.ts.map +0 -1
  95. package/dist/dts/utils/sum-usage.test.d.ts +0 -2
  96. package/dist/dts/utils/sum-usage.test.d.ts.map +0 -1
  97. package/dist/dts/utils/usage-rows.test.d.ts +0 -2
  98. package/dist/dts/utils/usage-rows.test.d.ts.map +0 -1
  99. package/dist/esm/components/chat-driver/chat-driver.compact.test.js +0 -93
  100. package/dist/esm/components/chat-driver/chat-driver.invocation-scope.test.js +0 -238
  101. package/dist/esm/components/chat-driver/chat-driver.test.js +0 -2770
  102. package/dist/esm/components/chat-driver/chat-driver.thinking-policy.test.js +0 -137
  103. package/dist/esm/components/chat-driver/chat-driver.trace-capture.test.js +0 -200
  104. package/dist/esm/components/chat-driver/chat-driver.turn-usage.test.js +0 -268
  105. package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.test.js +0 -104
  106. package/dist/esm/components/orchestrating-driver/orchestrating-driver.budget.test.js +0 -312
  107. package/dist/esm/components/orchestrating-driver/orchestrating-driver.cost.test.js +0 -76
  108. package/dist/esm/components/orchestrating-driver/orchestrating-driver.pin.test.js +0 -61
  109. package/dist/esm/components/settings-modal/settings-modal.styles.test.js +0 -80
  110. package/dist/esm/components/settings-modal/settings-modal.template.test.js +0 -91
  111. package/dist/esm/config/define-stateful-agent.test.js +0 -53
  112. package/dist/esm/config/validate-providers.test.js +0 -100
  113. package/dist/esm/main/blocked-state.test.js +0 -1065
  114. package/dist/esm/main/budget-meter.test.js +0 -359
  115. package/dist/esm/main/cost-session-banking.test.js +0 -538
  116. package/dist/esm/main/file-attachments.test.js +0 -164
  117. package/dist/esm/main/interaction-cost.test.js +0 -107
  118. package/dist/esm/main/main.styles.test.js +0 -161
  119. package/dist/esm/main/persistence-broken-sources.test.js +0 -180
  120. package/dist/esm/main/popout-interaction-gate.test.js +0 -54
  121. package/dist/esm/state/ai-assistant-slice.test.js +0 -229
  122. package/dist/esm/state/debug-event-log.test.js +0 -117
  123. package/dist/esm/state/driver-registry.test.js +0 -55
  124. package/dist/esm/state/interaction-context.test.js +0 -78
  125. package/dist/esm/state/persistence/diagnostics-cursors.test.js +0 -30
  126. package/dist/esm/state/persistence/diagnostics.test.js +0 -280
  127. package/dist/esm/state/persistence/session-persistence-provider.test.js +0 -110
  128. package/dist/esm/state/persistence/session-persistence.integration.test.js +0 -113
  129. package/dist/esm/state/persistence/session-persister.test.js +0 -613
  130. package/dist/esm/state/persistence/session-snapshot.test.js +0 -256
  131. package/dist/esm/state/persistence/stateful-restore.e2e.test.js +0 -158
  132. package/dist/esm/utils/animation-exclusivity.test.js +0 -52
  133. package/dist/esm/utils/collect-session-models.test.js +0 -53
  134. package/dist/esm/utils/condense-history.test.js +0 -547
  135. package/dist/esm/utils/cost-session-history.test.js +0 -174
  136. package/dist/esm/utils/derive-cost-session-title.test.js +0 -37
  137. package/dist/esm/utils/flatten-sub-agent-messages.test.js +0 -139
  138. package/dist/esm/utils/history-transform.test.js +0 -158
  139. package/dist/esm/utils/message-partition.test.js +0 -90
  140. package/dist/esm/utils/resolve-cost-history-config.test.js +0 -32
  141. package/dist/esm/utils/resolve-preference-baseline.test.js +0 -69
  142. package/dist/esm/utils/strip-agent-handlers.test.js +0 -81
  143. package/dist/esm/utils/sum-costs.test.js +0 -114
  144. package/dist/esm/utils/sum-tokens.test.js +0 -51
  145. package/dist/esm/utils/sum-usage.test.js +0 -164
  146. package/dist/esm/utils/usage-rows.test.js +0 -189
  147. package/dist/tsconfig.tsbuildinfo +0 -1
  148. package/license.txt +0 -46
  149. package/sandbox/tsconfig.json +0 -8
  150. package/src/channel/ai-activity-bus.ts +0 -209
  151. package/src/channel/ai-activity-channel.ts +0 -146
  152. package/src/chat-driver-node.ts +0 -159
  153. package/src/components/activity-halo/activity-halo.ts +0 -106
  154. package/src/components/agent-picker/agent-picker.constants.ts +0 -2
  155. package/src/components/agent-picker/agent-picker.styles.ts +0 -24
  156. package/src/components/agent-picker/agent-picker.template.ts +0 -54
  157. package/src/components/agent-picker/agent-picker.ts +0 -99
  158. package/src/components/agent-picker/index.ts +0 -1
  159. package/src/components/ai-driver/ai-driver.ts +0 -166
  160. package/src/components/ai-driver/index.ts +0 -1
  161. package/src/components/chat-bubble/chat-bubble.styles.ts +0 -194
  162. package/src/components/chat-bubble/chat-bubble.template.ts +0 -67
  163. package/src/components/chat-bubble/chat-bubble.ts +0 -436
  164. package/src/components/chat-bubble/index.ts +0 -1
  165. package/src/components/chat-driver/align-event-globals.ts +0 -23
  166. package/src/components/chat-driver/chat-driver.compact.test.ts +0 -131
  167. package/src/components/chat-driver/chat-driver.invocation-scope.test.ts +0 -285
  168. package/src/components/chat-driver/chat-driver.test.ts +0 -3679
  169. package/src/components/chat-driver/chat-driver.thinking-policy.test.ts +0 -185
  170. package/src/components/chat-driver/chat-driver.trace-capture.test.ts +0 -251
  171. package/src/components/chat-driver/chat-driver.ts +0 -3925
  172. package/src/components/chat-driver/chat-driver.turn-usage.test.ts +0 -362
  173. package/src/components/chat-driver/index.ts +0 -1
  174. package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts +0 -7
  175. package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts +0 -6
  176. package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts +0 -144
  177. package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts +0 -243
  178. package/src/components/chat-interaction-wrapper/index.ts +0 -3
  179. package/src/components/chat-markdown/chat-markdown.ts +0 -89
  180. package/src/components/chat-markdown/index.ts +0 -1
  181. package/src/components/flowing-waves-indicator.ts +0 -286
  182. package/src/components/halo-overlay.ts +0 -184
  183. package/src/components/orchestrating-driver/index.ts +0 -1
  184. package/src/components/orchestrating-driver/orchestrating-driver.budget.test.ts +0 -438
  185. package/src/components/orchestrating-driver/orchestrating-driver.cost.test.ts +0 -107
  186. package/src/components/orchestrating-driver/orchestrating-driver.pin.test.ts +0 -96
  187. package/src/components/orchestrating-driver/orchestrating-driver.ts +0 -813
  188. package/src/components/plasma-orb-indicator.ts +0 -281
  189. package/src/components/popout-manager/index.ts +0 -1
  190. package/src/components/popout-manager/popout-manager.ts +0 -212
  191. package/src/components/settings-modal/index.ts +0 -2
  192. package/src/components/settings-modal/settings-modal.styles.test.ts +0 -94
  193. package/src/components/settings-modal/settings-modal.styles.ts +0 -853
  194. package/src/components/settings-modal/settings-modal.template.test.ts +0 -124
  195. package/src/components/settings-modal/settings-modal.template.ts +0 -943
  196. package/src/components/waves-indicator.ts +0 -241
  197. package/src/config/config.ts +0 -608
  198. package/src/config/define-stateful-agent.test.ts +0 -64
  199. package/src/config/define-stateful-agent.ts +0 -642
  200. package/src/config/fallback-agents.ts +0 -29
  201. package/src/config/index.ts +0 -2
  202. package/src/config/validate-providers.test.ts +0 -148
  203. package/src/config/validate-providers.ts +0 -58
  204. package/src/index.ts +0 -29
  205. package/src/main/blocked-state.test.ts +0 -1444
  206. package/src/main/budget-meter.test.ts +0 -477
  207. package/src/main/cost-session-banking.test.ts +0 -679
  208. package/src/main/file-attachments.test.ts +0 -215
  209. package/src/main/index.ts +0 -3
  210. package/src/main/interaction-cost.test.ts +0 -140
  211. package/src/main/main.styles.test.ts +0 -216
  212. package/src/main/main.styles.ts +0 -995
  213. package/src/main/main.template.ts +0 -1119
  214. package/src/main/main.ts +0 -5432
  215. package/src/main/main.types.ts +0 -225
  216. package/src/main/persistence-broken-sources.test.ts +0 -219
  217. package/src/main/popout-interaction-gate.test.ts +0 -87
  218. package/src/provider/ai-provider-switcher.ts +0 -40
  219. package/src/provider/assistant-app-settings.ts +0 -84
  220. package/src/state/ai-assistant-slice.test.ts +0 -336
  221. package/src/state/ai-assistant-slice.ts +0 -604
  222. package/src/state/debug-event-log.test.ts +0 -154
  223. package/src/state/debug-event-log.ts +0 -358
  224. package/src/state/driver-registry.test.ts +0 -70
  225. package/src/state/driver-registry.ts +0 -79
  226. package/src/state/interaction-context.test.ts +0 -90
  227. package/src/state/interaction-context.ts +0 -101
  228. package/src/state/persistence/build-timeline-entries.ts +0 -66
  229. package/src/state/persistence/diagnostics-cursors.test.ts +0 -53
  230. package/src/state/persistence/diagnostics-cursors.ts +0 -80
  231. package/src/state/persistence/diagnostics.test.ts +0 -346
  232. package/src/state/persistence/diagnostics.ts +0 -220
  233. package/src/state/persistence/index.ts +0 -10
  234. package/src/state/persistence/persister-registry.ts +0 -55
  235. package/src/state/persistence/session-persistence-provider.test.ts +0 -136
  236. package/src/state/persistence/session-persistence-provider.ts +0 -226
  237. package/src/state/persistence/session-persistence.integration.test.ts +0 -141
  238. package/src/state/persistence/session-persister.test.ts +0 -787
  239. package/src/state/persistence/session-persister.ts +0 -917
  240. package/src/state/persistence/session-snapshot.test.ts +0 -352
  241. package/src/state/persistence/session-snapshot.ts +0 -344
  242. package/src/state/persistence/stateful-restore.e2e.test.ts +0 -229
  243. package/src/state/session-store.ts +0 -56
  244. package/src/styles/ai-colours.ts +0 -30
  245. package/src/styles/menu.ts +0 -187
  246. package/src/styles/settings-section.ts +0 -40
  247. package/src/suggestions/chat-suggestions.ts +0 -186
  248. package/src/tags/index.ts +0 -1
  249. package/src/types/ai-chat-widget.ts +0 -67
  250. package/src/types/interaction-context.ts +0 -82
  251. package/src/utils/animated-panel-toggle.ts +0 -62
  252. package/src/utils/animation-exclusivity.test.ts +0 -72
  253. package/src/utils/animation-exclusivity.ts +0 -40
  254. package/src/utils/banked-usage-baselines.ts +0 -90
  255. package/src/utils/collect-session-models.test.ts +0 -82
  256. package/src/utils/collect-session-models.ts +0 -65
  257. package/src/utils/condense-history.test.ts +0 -746
  258. package/src/utils/condense-history.ts +0 -448
  259. package/src/utils/cost-session-history.test.ts +0 -226
  260. package/src/utils/cost-session-history.ts +0 -218
  261. package/src/utils/derive-cost-session-title.test.ts +0 -63
  262. package/src/utils/derive-cost-session-title.ts +0 -76
  263. package/src/utils/flatten-sub-agent-messages.test.ts +0 -163
  264. package/src/utils/flatten-sub-agent-messages.ts +0 -88
  265. package/src/utils/format-usd.ts +0 -26
  266. package/src/utils/history-transform.test.ts +0 -228
  267. package/src/utils/history-transform.ts +0 -163
  268. package/src/utils/index.ts +0 -3
  269. package/src/utils/logger.ts +0 -3
  270. package/src/utils/message-partition.test.ts +0 -129
  271. package/src/utils/message-partition.ts +0 -115
  272. package/src/utils/resolve-cost-history-config.test.ts +0 -38
  273. package/src/utils/resolve-cost-history-config.ts +0 -38
  274. package/src/utils/resolve-preference-baseline.test.ts +0 -84
  275. package/src/utils/resolve-preference-baseline.ts +0 -80
  276. package/src/utils/strip-agent-handlers.test.ts +0 -99
  277. package/src/utils/strip-agent-handlers.ts +0 -53
  278. package/src/utils/sum-costs.test.ts +0 -136
  279. package/src/utils/sum-costs.ts +0 -18
  280. package/src/utils/sum-tokens.test.ts +0 -93
  281. package/src/utils/sum-tokens.ts +0 -68
  282. package/src/utils/sum-usage.test.ts +0 -191
  283. package/src/utils/sum-usage.ts +0 -208
  284. package/src/utils/tool-fold.ts +0 -181
  285. package/src/utils/usage-rows.test.ts +0 -237
  286. package/src/utils/usage-rows.ts +0 -187
  287. package/src/utils/with-timeout.ts +0 -24
  288. package/tsconfig.json +0 -9
@@ -1,38 +0,0 @@
1
- import type { ChatConfig } from '@genesislcap/foundation-ai';
2
-
3
- /** Resolved cost-history settings from `chatConfig.costHistory`. */
4
- export interface ResolvedCostHistoryConfig {
5
- /** When `true`, sessions are persisted automatically and history is shown. */
6
- enabled: boolean;
7
- /** Host label for the active session (e.g. project name). */
8
- title?: string;
9
- /** `localStorage` scope key; falls back to element `id` then `'default'`. */
10
- scope?: string;
11
- /** Show running session USD on the Usage tab. Default: `true`. */
12
- showCost: boolean;
13
- /** Show cumulative session tokens on the Usage tab. Default: `true`. */
14
- showTokens: boolean;
15
- /** Live-session badge label on the Usage tab summary. */
16
- badgeLabel: string;
17
- /**
18
- * Host-supplied disclosure line for the Usage tab. Stays optional — unlike
19
- * `badgeLabel` there is no default, because only the host knows what its `scope`
20
- * spans, and the library must not assert it.
21
- */
22
- summaryHint?: string;
23
- }
24
-
25
- /** Resolve `chatConfig.costHistory` with defaults applied. */
26
- export function resolveCostHistoryConfig(chatConfig: ChatConfig): ResolvedCostHistoryConfig {
27
- const cost = chatConfig.costHistory;
28
-
29
- return {
30
- enabled: cost?.enabled === true,
31
- title: cost?.title?.trim() || undefined,
32
- scope: cost?.scope?.trim() || undefined,
33
- showCost: cost?.showCost !== false,
34
- showTokens: cost?.showTokens !== false,
35
- badgeLabel: cost?.badgeLabel ?? 'Currently building',
36
- summaryHint: cost?.summaryHint?.trim() || undefined,
37
- };
38
- }
@@ -1,84 +0,0 @@
1
- import type { ChatConfig } from '@genesislcap/foundation-ai';
2
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
3
- import type { SessionPreferences } from '../state/persistence/session-persistence-provider';
4
- import { resolvePreferenceBaseline } from './resolve-preference-baseline';
5
-
6
- // GENC-1351 #2: the preference baseline resolves with precedence
7
- // slice defaults < chatConfig.ui defaults < stored user preferences.
8
- // The regression this guards: when stored prefs are absent (first load / wiped
9
- // backend) the result must fall through to the chatConfig.ui defaults, NOT to
10
- // everything-disabled.
11
-
12
- const FULL_UI: ChatConfig['ui'] = {
13
- showToolCalls: true,
14
- showThinkingSteps: true,
15
- showNarration: true,
16
- showAgentSwitchIndicator: true,
17
- animations: { userConfigurable: true },
18
- };
19
-
20
- const Suite = createLogicSuite('resolvePreferenceBaseline');
21
-
22
- Suite('with no stored prefs, falls back to chatConfig.ui defaults (the nuke case)', () => {
23
- const r = resolvePreferenceBaseline(FULL_UI, null);
24
- assert.is(r.showToolCalls, true);
25
- assert.is(r.showThinkingSteps, true);
26
- assert.is(r.showNarration, true);
27
- assert.is(r.showAgentSwitchIndicator, true);
28
- assert.ok(r.enabledAnimations.length > 0, 'config animations enabled by default');
29
- assert.is(r.enabled, undefined, 'no stored enabled → leave host default untouched');
30
- });
31
-
32
- Suite('with no stored prefs AND empty/unbound chatConfig, everything is off', () => {
33
- // The transient state during connect before chatConfig binds — all-false is
34
- // correct HERE; the element re-runs the baseline once chatConfig arrives.
35
- const r = resolvePreferenceBaseline(undefined, null);
36
- assert.equal(
37
- [r.showToolCalls, r.showThinkingSteps, r.showNarration, r.showAgentSwitchIndicator],
38
- [false, false, false, false],
39
- );
40
- assert.equal(r.enabledAnimations, []);
41
- });
42
-
43
- Suite('stored prefs override the config defaults field-by-field', () => {
44
- const stored: SessionPreferences = {
45
- enabled: true,
46
- showToolCalls: false,
47
- showThinkingSteps: false,
48
- showNarration: true,
49
- showAgentSwitchIndicator: false,
50
- animations: ['halo'],
51
- };
52
- const r = resolvePreferenceBaseline(FULL_UI, stored);
53
- assert.is(r.showToolCalls, false, 'stored false beats config true');
54
- assert.is(r.showThinkingSteps, false);
55
- assert.is(r.showNarration, true);
56
- assert.is(r.showAgentSwitchIndicator, false);
57
- assert.equal(r.enabledAnimations, ['halo'], 'stored animations applied as-is');
58
- assert.is(r.enabled, true, 'stored enabled surfaces');
59
- });
60
-
61
- Suite('a partial stored blob overrides only present fields; others keep config default', () => {
62
- const stored = { showToolCalls: false } as SessionPreferences;
63
- const r = resolvePreferenceBaseline(FULL_UI, stored);
64
- assert.is(r.showToolCalls, false, 'present field overridden');
65
- assert.is(r.showThinkingSteps, true, 'absent field keeps config default');
66
- assert.is(r.showAgentSwitchIndicator, true);
67
- assert.is(r.enabled, undefined, 'absent enabled stays undefined');
68
- });
69
-
70
- Suite('stored prefs over an empty config still apply (config layer is all-false)', () => {
71
- const stored: SessionPreferences = { showToolCalls: true, showThinkingSteps: true };
72
- const r = resolvePreferenceBaseline(undefined, stored);
73
- assert.is(r.showToolCalls, true);
74
- assert.is(r.showThinkingSteps, true);
75
- assert.is(r.showNarration, false, 'not in stored, not in config → false');
76
- });
77
-
78
- Suite('de-dupes stored animations (heals blobs from the old settings-modal append bug)', () => {
79
- const stored = { animations: ['loading', 'halo', 'loading', 'halo'] } as SessionPreferences;
80
- const r = resolvePreferenceBaseline(FULL_UI, stored);
81
- assert.equal(r.enabledAnimations, ['loading', 'halo'], 'duplicates collapsed, order preserved');
82
- });
83
-
84
- Suite.run();
@@ -1,80 +0,0 @@
1
- import type { ChatConfig } from '@genesislcap/foundation-ai';
2
- import { DEFAULT_ANIMATIONS, type AiAssistantAnimation } from '../main/main.types';
3
- import type { SessionPreferences } from '../state/persistence/session-persistence-provider';
4
- import { resolveExclusiveLoadingStyle } from './animation-exclusivity';
5
-
6
- /**
7
- * The effective user-facing preference state for a freshly created session store
8
- * (GENC-1351 #2), resolved from the two inputs that feed it.
9
- *
10
- * @internal
11
- */
12
- export interface ResolvedPreferenceBaseline {
13
- showToolCalls: boolean;
14
- showThinkingSteps: boolean;
15
- showNarration: boolean;
16
- showAgentSwitchIndicator: boolean;
17
- enabledAnimations: AiAssistantAnimation[];
18
- /**
19
- * The remembered "remember chat" toggle — present only when stored prefs carried
20
- * an `enabled` value. Left `undefined` otherwise so the caller keeps the
21
- * host-configured default rather than forcing a value.
22
- */
23
- enabled?: boolean;
24
- }
25
-
26
- /**
27
- * Resolve the effective preference baseline for a fresh store, applying the
28
- * precedence (lowest → highest):
29
- *
30
- * slice defaults < `chatConfig.ui` defaults < stored user preferences
31
- *
32
- * The `chatConfig.ui` layer is the fallback when the user has no stored prefs
33
- * (e.g. a first-ever load, or after the persistence backend is wiped) — the case
34
- * the old "read config once on connect" path got wrong whenever `chatConfig` had
35
- * not bound yet. Stored prefs, when present, override only the fields they carry;
36
- * absent fields keep the config default. Pure, so the precedence is unit-testable
37
- * independent of the element/store wiring.
38
- *
39
- * Config-supplied animations are normalised through {@link resolveExclusiveLoadingStyle}
40
- * (a raw config list may name both loading styles); stored animations are applied
41
- * as-is, since they were already normalised when saved.
42
- *
43
- * @internal
44
- */
45
- export function resolvePreferenceBaseline(
46
- ui: ChatConfig['ui'],
47
- stored: SessionPreferences | null | undefined,
48
- ): ResolvedPreferenceBaseline {
49
- const u = ui ?? {};
50
- // Layer 1 — chatConfig.ui defaults.
51
- const baseline: ResolvedPreferenceBaseline = {
52
- showToolCalls: u.showToolCalls === true,
53
- showThinkingSteps: u.showThinkingSteps === true,
54
- showNarration: u.showNarration === true,
55
- showAgentSwitchIndicator: u.showAgentSwitchIndicator === true,
56
- enabledAnimations: resolveExclusiveLoadingStyle(
57
- (u.animations?.enabled as AiAssistantAnimation[]) ??
58
- (u.animations ? [...DEFAULT_ANIMATIONS] : []),
59
- ),
60
- };
61
-
62
- // Layer 2 — stored user preferences override the fields they carry.
63
- if (stored) {
64
- if (stored.enabled !== undefined) baseline.enabled = stored.enabled;
65
- if (stored.showToolCalls !== undefined) baseline.showToolCalls = stored.showToolCalls;
66
- if (stored.showThinkingSteps !== undefined)
67
- baseline.showThinkingSteps = stored.showThinkingSteps;
68
- if (stored.showNarration !== undefined) baseline.showNarration = stored.showNarration;
69
- if (stored.showAgentSwitchIndicator !== undefined) {
70
- baseline.showAgentSwitchIndicator = stored.showAgentSwitchIndicator;
71
- }
72
- if (Array.isArray(stored.animations)) {
73
- // De-dupe defensively — historical blobs may hold duplicates from the old
74
- // settings-modal append bug (now fixed at the `setEnabledAnimations` source).
75
- baseline.enabledAnimations = [...new Set(stored.animations)] as AiAssistantAnimation[];
76
- }
77
- }
78
-
79
- return baseline;
80
- }
@@ -1,99 +0,0 @@
1
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
- import type { AgentConfig } from '../config/config';
3
- import { stripAgentHandlers } from './strip-agent-handlers';
4
-
5
- const cfg = (overrides: Record<string, unknown>): AgentConfig =>
6
- ({ name: 'agent', ...overrides }) as unknown as AgentConfig;
7
-
8
- /** Mirrors what redux's serializable-state check does: find any function anywhere. */
9
- const hasFunctionDeep = (value: unknown): boolean => {
10
- if (typeof value === 'function') return true;
11
- if (Array.isArray(value)) return value.some(hasFunctionDeep);
12
- if (value !== null && typeof value === 'object') {
13
- return Object.values(value).some(hasFunctionDeep);
14
- }
15
- return false;
16
- };
17
-
18
- const suite = createLogicSuite('stripAgentHandlers');
19
-
20
- suite('drops object-form toolHandlers (the redux-leak regression)', () => {
21
- const stripped = stripAgentHandlers(
22
- cfg({ toolHandlers: { vfs_list: async () => 'x', vfs_read: async () => 'y' } }),
23
- ) as Record<string, unknown>;
24
- // The whole handler bag is gone — not left behind with live functions inside.
25
- assert.not.ok(stripped.toolHandlers);
26
- assert.not.ok(hasFunctionDeep(stripped));
27
- });
28
-
29
- suite('drops factory-function toolHandlers and the lifecycle/dispatch hooks', () => {
30
- const stripped = stripAgentHandlers(
31
- cfg({
32
- toolHandlers: () => ({ t: async () => 'x' }),
33
- onActivate: async () => {},
34
- onDeactivate: async () => {},
35
- getDebugSnapshot: () => ({}),
36
- onUnresolvedTool: () => 'redirect',
37
- }),
38
- ) as Record<string, unknown>;
39
- assert.not.ok(stripped.toolHandlers);
40
- assert.not.ok(stripped.onActivate);
41
- assert.not.ok(stripped.onDeactivate);
42
- assert.not.ok(stripped.getDebugSnapshot);
43
- assert.not.ok(stripped.onUnresolvedTool);
44
- });
45
-
46
- suite('drops function-form resolvers but keeps static forms and plain data', () => {
47
- const stripped = stripAgentHandlers(
48
- cfg({
49
- systemPrompt: () => 'dynamic', // function → dropped
50
- displayName: 'Static Name', // string → kept
51
- temperature: 0.5, // number → kept
52
- toolChoice: 'auto', // string → kept
53
- toolDefinitions: [{ name: 'x', description: 'd', parameters: {} }], // data array → kept
54
- manualSelection: { enabled: true, hint: 'pick me' }, // data object → kept
55
- }),
56
- ) as Record<string, unknown>;
57
- assert.not.ok(stripped.systemPrompt);
58
- assert.is(stripped.displayName, 'Static Name');
59
- assert.is(stripped.temperature, 0.5);
60
- assert.is(stripped.toolChoice, 'auto');
61
- assert.equal(stripped.toolDefinitions, [{ name: 'x', description: 'd', parameters: {} }]);
62
- assert.equal(stripped.manualSelection, { enabled: true, hint: 'pick me' });
63
- });
64
-
65
- suite('recurses into subAgents, stripping their object-form toolHandlers', () => {
66
- const stripped = stripAgentHandlers(
67
- cfg({
68
- name: 'boss',
69
- toolHandlers: () => ({}),
70
- subAgents: [
71
- cfg({
72
- name: 'worker',
73
- toolHandlers: { vfs_list: async () => 'x', vfs_read: async () => 'y' },
74
- }),
75
- ],
76
- }),
77
- ) as Record<string, unknown>;
78
- const subs = stripped.subAgents as Array<Record<string, unknown>>;
79
- assert.is(subs.length, 1);
80
- assert.is(subs[0].name, 'worker');
81
- assert.not.ok(subs[0].toolHandlers, 'sub-agent toolHandlers must be stripped (the reported bug)');
82
- // Nothing function-valued survives anywhere in the projection.
83
- assert.not.ok(hasFunctionDeep(stripped));
84
- });
85
-
86
- suite('produces a projection with no function at any depth', () => {
87
- const stripped = stripAgentHandlers(
88
- cfg({
89
- name: 'boss',
90
- onActivate: async () => {},
91
- subAgents: [cfg({ name: 'w', toolHandlers: { t: async () => {} } })],
92
- }),
93
- );
94
- assert.not.ok(hasFunctionDeep(stripped));
95
- // Round-trips to the expected serializable shape.
96
- assert.equal(JSON.parse(JSON.stringify(stripped)), { name: 'boss', subAgents: [{ name: 'w' }] });
97
- });
98
-
99
- suite.run();
@@ -1,53 +0,0 @@
1
- import type { AgentConfig } from '../config/config';
2
-
3
- /**
4
- * Project an `AgentConfig` down to a JSON-serializable shape for the redux
5
- * session store. Drops two kinds of field:
6
- *
7
- * 1. **Directly function-valued** — the lifecycle/dispatch hooks (`onActivate`,
8
- * `onDeactivate`, `getDebugSnapshot`, `onUnresolvedTool`) and the function
9
- * form of the per-turn resolvers (`systemPrompt`, `toolDefinitions`,
10
- * `displayName`, `provider`, `temperature`, `toolChoice`, `cachePolicy`,
11
- * `thinkingPolicy`, `toolHandlers`).
12
- * 2. **Object "handler bags" whose *values* are functions** — `toolHandlers` in
13
- * its object form is `{ name: handler }`, so `typeof` is `'object'`, not
14
- * `'function'`. A by-value check on the field alone misses it, leaking a live
15
- * handler into store state and tripping redux's serializability check (the
16
- * regression this guards against — top-level agents tend to use the factory
17
- * function form, but sub-agents commonly declare an object literal).
18
- *
19
- * `subAgents` are projected recursively. Static forms (string / number / array /
20
- * plain data object) pass through unchanged.
21
- *
22
- * Filtering by value rather than an explicit field list means a new
23
- * function-valued field on `AgentConfig` is handled automatically — no denylist
24
- * to maintain. The live config on the driver stays the source of truth; the
25
- * slice only holds this serializable projection and functions are never read
26
- * back from it.
27
- *
28
- * @internal
29
- */
30
- export function stripAgentHandlers(agent: AgentConfig): Omit<AgentConfig, 'toolHandlers'> {
31
- const serializable: Record<string, unknown> = {};
32
- for (const [key, value] of Object.entries(agent)) {
33
- // Projected recursively below.
34
- if (key === 'subAgents') continue;
35
- // Directly function-valued field.
36
- if (typeof value === 'function') continue;
37
- // Object/array whose values include a function (e.g. `toolHandlers` in object
38
- // form). Drop the whole field — the projection type omits it and a function
39
- // must never reach the store.
40
- if (
41
- value !== null &&
42
- typeof value === 'object' &&
43
- Object.values(value).some((v) => typeof v === 'function')
44
- ) {
45
- continue;
46
- }
47
- serializable[key] = value;
48
- }
49
- if (agent.subAgents?.length) {
50
- serializable.subAgents = agent.subAgents.map(stripAgentHandlers);
51
- }
52
- return serializable as unknown as Omit<AgentConfig, 'toolHandlers'>;
53
- }
@@ -1,136 +0,0 @@
1
- import type { ChatMessage } from '@genesislcap/foundation-ai';
2
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
3
- import { sumCosts } from './sum-costs';
4
-
5
- const userMsg = (overrides: Partial<ChatMessage> = {}): ChatMessage => ({
6
- role: 'user',
7
- content: '',
8
- ...overrides,
9
- });
10
-
11
- const assistantMsg = (overrides: Partial<ChatMessage> = {}): ChatMessage => ({
12
- role: 'assistant',
13
- content: '',
14
- ...overrides,
15
- });
16
-
17
- const suite = createLogicSuite('sumCosts');
18
-
19
- suite('returns 0 for an empty message list', () => {
20
- assert.is(sumCosts([]), 0);
21
- });
22
-
23
- suite('returns 0 when no message carries cost (e.g. all from a no-usage provider)', () => {
24
- assert.is(sumCosts([userMsg(), assistantMsg({ content: 'hi' })]), 0);
25
- });
26
-
27
- suite('sums top-level message costs', () => {
28
- // Use integer-cents arithmetic to avoid float-equality flake; the production
29
- // path only ever adds floats here, so any rounding lives in display, not sum.
30
- const cost = sumCosts([
31
- assistantMsg({ cost: 0.5 }),
32
- assistantMsg({ cost: 1.5 }),
33
- userMsg(), // no cost — ignored
34
- ]);
35
- assert.is(cost, 2);
36
- });
37
-
38
- suite('includes sub-agent trace costs attached to a tool call', () => {
39
- const parentToolCallMsg = assistantMsg({
40
- cost: 0.1,
41
- toolCalls: [
42
- {
43
- id: 'tc1',
44
- name: 'spawn_sub',
45
- args: {},
46
- subAgentTrace: [assistantMsg({ cost: 0.5 }), assistantMsg({ cost: 0.4 })],
47
- },
48
- ],
49
- });
50
- // 0.1 (parent) + 0.5 + 0.4 (sub) = 1.0
51
- assert.is(sumCosts([parentToolCallMsg]), 1);
52
- });
53
-
54
- suite('recurses into nested sub-agent traces', () => {
55
- const innerTrace: ChatMessage[] = [assistantMsg({ cost: 0.25 })];
56
- const middleTrace: ChatMessage[] = [
57
- assistantMsg({
58
- cost: 0.5,
59
- toolCalls: [{ id: 'inner', name: 'spawn_inner', args: {}, subAgentTrace: innerTrace }],
60
- }),
61
- ];
62
- const top = assistantMsg({
63
- cost: 1,
64
- toolCalls: [{ id: 'middle', name: 'spawn_middle', args: {}, subAgentTrace: middleTrace }],
65
- });
66
- // 1 + 0.5 + 0.25 = 1.75
67
- assert.is(sumCosts([top]), 1.75);
68
- });
69
-
70
- suite('handles a mix of tool calls with and without subAgentTrace', () => {
71
- const cost = sumCosts([
72
- assistantMsg({
73
- cost: 1,
74
- toolCalls: [
75
- { id: 'a', name: 'plain_tool', args: {} }, // no trace — skipped
76
- {
77
- id: 'b',
78
- name: 'sub_tool',
79
- args: {},
80
- subAgentTrace: [assistantMsg({ cost: 2 })],
81
- },
82
- ],
83
- }),
84
- ]);
85
- assert.is(cost, 3);
86
- });
87
-
88
- suite('ignores missing cost fields inside an otherwise-populated trace', () => {
89
- const cost = sumCosts([
90
- assistantMsg({
91
- toolCalls: [
92
- {
93
- id: 'tc',
94
- name: 'sub',
95
- args: {},
96
- subAgentTrace: [
97
- userMsg(), // role=user, no cost
98
- assistantMsg({ content: 'partial', cost: 0.3 }),
99
- assistantMsg({ content: 'unbilled' }), // no cost field
100
- ],
101
- },
102
- ],
103
- }),
104
- ]);
105
- assert.is(cost, 0.3);
106
- });
107
-
108
- suite('adds widget-reported externalCostUsd alongside provider cost', () => {
109
- // A message can carry both: provider LLM `cost` and a widget's `externalCostUsd`.
110
- const cost = sumCosts([
111
- assistantMsg({ cost: 0.5 }), // provider only
112
- assistantMsg({ externalCostUsd: 0.25 }), // external only (e.g. an interaction message)
113
- assistantMsg({ cost: 0.1, externalCostUsd: 0.4 }), // both
114
- userMsg(), // neither — ignored
115
- ]);
116
- // 0.5 + 0.25 + 0.1 + 0.4 = 1.25
117
- assert.is(cost, 1.25);
118
- });
119
-
120
- suite('includes externalCostUsd inside a sub-agent trace', () => {
121
- const cost = sumCosts([
122
- assistantMsg({
123
- toolCalls: [
124
- {
125
- id: 'tc',
126
- name: 'sub',
127
- args: {},
128
- subAgentTrace: [assistantMsg({ cost: 0.5, externalCostUsd: 0.5 })],
129
- },
130
- ],
131
- }),
132
- ]);
133
- assert.is(cost, 1);
134
- });
135
-
136
- suite.run();
@@ -1,18 +0,0 @@
1
- import type { ChatMessage } from '@genesislcap/foundation-ai';
2
- import { sumUsage } from './sum-usage';
3
-
4
- /**
5
- * Sum cost across a message list: each message's provider-reported LLM `cost`
6
- * plus any `externalCostUsd` a widget reported for its own external service
7
- * calls, recursing into each tool call's `subAgentTrace` so LLM calls made by
8
- * sub-agents (potentially on a different provider with different rates)
9
- * contribute to the total.
10
- *
11
- * Returns 0 when no messages carry a cost — providers that don't report usage
12
- * metadata (e.g. Chrome built-in) are skipped silently.
13
- *
14
- * @internal
15
- */
16
- export function sumCosts(messages: readonly ChatMessage[]): number {
17
- return sumUsage(messages).costUsd;
18
- }
@@ -1,93 +0,0 @@
1
- import type { ChatMessage } from '@genesislcap/foundation-ai';
2
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
3
- import { formatCompactTokenCount, formatCompactTokenCountSafe, sumTokens } from './sum-tokens';
4
-
5
- const assistantMsg = (overrides: Partial<ChatMessage> = {}): ChatMessage => ({
6
- role: 'assistant',
7
- content: '',
8
- ...overrides,
9
- });
10
-
11
- const suite = createLogicSuite('sumTokens');
12
-
13
- suite('returns 0 for an empty message list', () => {
14
- assert.is(sumTokens([]), 0);
15
- });
16
-
17
- suite('sums input and output tokens on top-level messages', () => {
18
- assert.is(
19
- sumTokens([
20
- assistantMsg({ inputTokens: 100, outputTokens: 50 }),
21
- assistantMsg({ inputTokens: 200, outputTokens: 0 }),
22
- ]),
23
- 350,
24
- );
25
- });
26
-
27
- suite('includes sub-agent trace tokens attached to a tool call', () => {
28
- assert.is(
29
- sumTokens([
30
- assistantMsg({
31
- inputTokens: 10,
32
- outputTokens: 5,
33
- toolCalls: [
34
- {
35
- id: 'tc1',
36
- name: 'spawn_sub',
37
- args: {},
38
- subAgentTrace: [assistantMsg({ inputTokens: 1000, outputTokens: 400 })],
39
- },
40
- ],
41
- }),
42
- ]),
43
- 1415,
44
- );
45
- });
46
-
47
- const formatSuite = createLogicSuite('formatCompactTokenCount');
48
-
49
- formatSuite('formats millions with one decimal when needed', () => {
50
- assert.is(
51
- formatCompactTokenCount(1_400_000),
52
- '<span class="value">1.4</span><span class="suffix">M</span>',
53
- );
54
- });
55
-
56
- formatSuite('formats whole millions without a decimal', () => {
57
- assert.is(
58
- formatCompactTokenCount(2_000_000),
59
- '<span class="value">2</span><span class="suffix">M</span>',
60
- );
61
- });
62
-
63
- formatSuite('formats thousands compactly', () => {
64
- assert.is(
65
- formatCompactTokenCount(3_200),
66
- '<span class="value">3.2</span><span class="suffix">K</span>',
67
- );
68
- });
69
-
70
- formatSuite('formats small counts with grouping', () => {
71
- assert.is(formatCompactTokenCount(842), '842');
72
- });
73
-
74
- formatSuite('rolls K suffix up to M when rounding would yield 1000K', () => {
75
- assert.is(
76
- formatCompactTokenCount(999_950),
77
- '<span class="value">1</span><span class="suffix">M</span>',
78
- );
79
- assert.is(
80
- formatCompactTokenCount(999_949),
81
- '<span class="value">999.9</span><span class="suffix">K</span>',
82
- );
83
- });
84
-
85
- formatSuite('safe formatter preserves compact markup', () => {
86
- assert.is(
87
- formatCompactTokenCountSafe(1_400_000),
88
- '<span class="value">1.4</span><span class="suffix">M</span>',
89
- );
90
- });
91
-
92
- suite.run();
93
- formatSuite.run();
@@ -1,68 +0,0 @@
1
- import type { ChatMessage } from '@genesislcap/foundation-ai';
2
- import DOMPurify from 'dompurify';
3
- import { sumUsage, totalTokens } from './sum-usage';
4
-
5
- /**
6
- * Total tokens across a message list — the headline figure, with the per-bucket
7
- * split available from {@link sumUsage} for anything that needs to show *why* a
8
- * large token count cost little.
9
- *
10
- * Identical arithmetic to the old per-message `inputTokens + outputTokens` sum:
11
- * the three input buckets partition `inputTokens`, so they re-add to it exactly.
12
- * It does now additionally count usage banked by a compaction, which is the point
13
- * — that spend used to vanish when the summarised turns left the transcript.
14
- *
15
- * Returns 0 when no message carries token metadata.
16
- *
17
- * @internal
18
- */
19
- export function sumTokens(messages: readonly ChatMessage[]): number {
20
- return totalTokens(sumUsage(messages));
21
- }
22
-
23
- const TOKEN_SCALE_DECIMAL_FACTOR = 10;
24
- const TOKENS_PER_MILLION = 1_000_000;
25
- const TOKENS_PER_THOUSAND = 1_000;
26
- const SUFFIX_ROLLOVER_THRESHOLD = 1000;
27
-
28
- const roundScaled = (scaled: number): number =>
29
- Math.round(scaled * TOKEN_SCALE_DECIMAL_FACTOR) / TOKEN_SCALE_DECIMAL_FACTOR;
30
-
31
- const formatScaledTokenMarkup = (rounded: number, suffix: string): string => {
32
- const value = Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
33
- return `<span class="value">${value}</span><span class="suffix">${suffix}</span>`;
34
- };
35
-
36
- const scaledSuffixLabel = (tokens: number): string | undefined => {
37
- if (tokens >= TOKENS_PER_MILLION) return 'M';
38
- if (tokens >= TOKENS_PER_THOUSAND) {
39
- const kRounded = roundScaled(tokens / TOKENS_PER_THOUSAND);
40
- if (kRounded >= SUFFIX_ROLLOVER_THRESHOLD) return 'M';
41
- return 'K';
42
- }
43
- return undefined;
44
- };
45
-
46
- const scaledNumericValue = (tokens: number): number | undefined => {
47
- const suffix = scaledSuffixLabel(tokens);
48
- if (!suffix) return undefined;
49
- if (suffix === 'M') return roundScaled(tokens / TOKENS_PER_MILLION);
50
- return roundScaled(tokens / TOKENS_PER_THOUSAND);
51
- };
52
-
53
- /**
54
- * Compact display for large token counts (e.g. 1_400_000 → value "1.4" + suffix "M").
55
- * Returns HTML markup so the suffix can be styled separately in the settings modal.
56
- *
57
- * @internal
58
- */
59
- export function formatCompactTokenCount(tokens: number): string {
60
- const suffix = scaledSuffixLabel(tokens);
61
- if (!suffix) return tokens.toLocaleString();
62
- return formatScaledTokenMarkup(scaledNumericValue(tokens)!, suffix);
63
- }
64
-
65
- /** {@link formatCompactTokenCount} passed through DOMPurify for `:innerHTML` bindings. */
66
- export function formatCompactTokenCountSafe(tokens: number): string {
67
- return DOMPurify.sanitize(formatCompactTokenCount(tokens));
68
- }