@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,218 +0,0 @@
1
- import type { AggregateUsage, AIProviderType } from '@genesislcap/foundation-ai';
2
- import { emptyUsage } from './sum-usage';
3
-
4
- /** A model used during a recorded cost session. */
5
- export interface CostSessionModelEntry {
6
- model: string;
7
- /** Vendor id from `getStatus()` (e.g. `'anthropic'`, `'gemini'`). */
8
- provider?: AIProviderType;
9
- }
10
-
11
- /** Persisted usage record for one project's assistant work. */
12
- export interface CostSessionRecord {
13
- id: string;
14
- /**
15
- * Stable identity of the work this row stands for — the assistant's session key
16
- * (a project id, for hosts that key sessions per project). The upsert key: a row
17
- * is one project's lifetime usage, so re-reporting the same key REPLACES the row
18
- * rather than adding another.
19
- */
20
- projectKey: string;
21
- title: string;
22
- /**
23
- * ISO-8601 timestamp of the most recent usage recorded for this row.
24
- *
25
- * Named for what it is: the row is upserted as spend accrues, so this moves. It is
26
- * NOT an end time — a row has no terminal state, because a project can always be
27
- * worked on again.
28
- */
29
- updatedAt: string;
30
- /**
31
- * Cost plus the four token buckets — the same shape the live session tile renders, so
32
- * a history row can show the identical breakdown instead of a lone merged total that
33
- * cannot explain a large token count at a small cost.
34
- *
35
- * This is the project's **lifetime** usage: `banked` plus whatever the live transcript
36
- * currently proves.
37
- */
38
- usage: AggregateUsage;
39
- /**
40
- * Spend that predates the current transcript, and so cannot be re-derived from it.
41
- *
42
- * Without this a row could only ever report what the open transcript proves. That is
43
- * fine when the transcript is restored on load (it *is* the project's history), but
44
- * where it starts empty every refresh the row would be rewritten downwards on the next
45
- * turn and the project's earlier spend lost. Banking it keeps `usage` a lifetime figure
46
- * in both cases.
47
- *
48
- * Deliberately part of the row rather than element state: two assistant instances (a
49
- * docked bubble and a popped-out panel) share one session, and both write this row. A
50
- * value derived from the row keeps every write idempotent, whereas an element-held
51
- * accumulator would be applied once per instance.
52
- *
53
- * Optional: rows written before this existed have none, which reads as "nothing banked".
54
- */
55
- banked?: AggregateUsage;
56
- models: CostSessionModelEntry[];
57
- }
58
-
59
- const STORAGE_PREFIX = 'genesis-ai-assistant:cost-history:';
60
-
61
- /**
62
- * Every bucket must be a number before a row is trusted. Checked per field rather than
63
- * just testing for an object, because a partially-shaped `usage` would sum as `NaN` and
64
- * silently poison every total on the Usage tab.
65
- */
66
- function isAggregateUsage(value: unknown): value is AggregateUsage {
67
- if (typeof value !== 'object' || value == null) return false;
68
- const u = value as AggregateUsage;
69
- return (
70
- typeof u.costUsd === 'number' &&
71
- typeof u.uncachedInputTokens === 'number' &&
72
- typeof u.cacheReadTokens === 'number' &&
73
- typeof u.cacheWriteTokens === 'number' &&
74
- typeof u.outputTokens === 'number'
75
- );
76
- }
77
-
78
- /** localStorage key for a host-scoped cost history list. */
79
- export function costHistoryStorageKey(scope: string): string {
80
- return `${STORAGE_PREFIX}${scope}`;
81
- }
82
-
83
- /**
84
- * Load persisted cost sessions for a scope, in whatever order they were stored — this applies no
85
- * ordering of its own. Display order is the caller's business; see `sortRecordsByRecency`.
86
- */
87
- export function loadCostSessionHistory(scope: string): CostSessionRecord[] {
88
- if (typeof localStorage === 'undefined') return [];
89
- try {
90
- const raw = localStorage.getItem(costHistoryStorageKey(scope));
91
- if (!raw) return [];
92
- const parsed: unknown = JSON.parse(raw);
93
- if (!Array.isArray(parsed)) return [];
94
- return parsed.filter(isCostSessionRecord);
95
- } catch {
96
- return [];
97
- }
98
- }
99
-
100
- /** Replace the persisted list for a scope. */
101
- export function saveCostSessionHistory(scope: string, records: CostSessionRecord[]): void {
102
- if (typeof localStorage === 'undefined') return;
103
- try {
104
- localStorage.setItem(costHistoryStorageKey(scope), JSON.stringify(records));
105
- } catch {
106
- // Quota exceeded or private browsing — non-fatal.
107
- }
108
- }
109
-
110
- /**
111
- * Pure upsert — `records` with `record` replacing any row sharing its `projectKey`, or
112
- * prepended when there is none.
113
- *
114
- * An upsert, not an append, because a row stands for a **project's lifetime usage** rather
115
- * than a visit to it. Appending cut a fresh row per page-hide, so one project accumulated
116
- * a stack of near-identical rows and its spend was counted once per refresh by anything
117
- * summing the list.
118
- *
119
- * The resulting array order is incidental — it is insertion order, so a project returned to
120
- * stays where it first landed. Do not read it as recency; `sortRecordsByRecency` is what the
121
- * Usage tab renders through.
122
- *
123
- * Storage-agnostic on purpose: the same merge serves the synchronous `localStorage` path
124
- * and the async provider-backed one, so the two can never drift on what "upsert" means.
125
- * The caller owns persistence — see the assistant's `persistCostHistory`.
126
- */
127
- export function upsertRecord(
128
- records: readonly CostSessionRecord[],
129
- record: CostSessionRecord,
130
- ): CostSessionRecord[] {
131
- const at = records.findIndex((r) => r.projectKey === record.projectKey);
132
- return at === -1 ? [record, ...records] : records.map((r, i) => (i === at ? record : r));
133
- }
134
-
135
- /**
136
- * Rows most-recently-worked-on first, as a new array.
137
- *
138
- * Applied at read time rather than baked into the stored order, so it covers rows that came from
139
- * a host provider as well as from `localStorage`, and so no write path has to maintain it.
140
- *
141
- * `updatedAt` is ISO-8601, which sorts lexicographically — no date parsing, and no dependence on
142
- * the strings being valid dates. Sorting by it rather than by insertion order is what makes the
143
- * list track use: the active project moves to the top on its first upsert and stays there, since
144
- * its row is rewritten on every transcript change.
145
- */
146
- export function sortRecordsByRecency(records: readonly CostSessionRecord[]): CostSessionRecord[] {
147
- return [...records].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
148
- }
149
-
150
- /**
151
- * The spend a project's next row must carry over from before its current transcript.
152
- *
153
- * `transcriptIsAuthoritative` means the open transcript already accounts for this project's
154
- * history — true exactly when a session snapshot is restored on load. The two branches are
155
- * not interchangeable, and picking the wrong one is silently destructive:
156
- *
157
- * - Authoritative: carry the previous row's own `banked` forward. Taking `usage` instead would
158
- * re-add the restored history on every reload, the compounding double-count this replaced.
159
- * - Not authoritative: the whole previous total becomes banked, because nothing in the fresh
160
- * transcript accounts for it. Taking `banked` instead would rewrite the row down to the
161
- * current page-load and drop the project's earlier spend.
162
- *
163
- * Note both branches are idempotent — the result never includes the current transcript — which
164
- * is what lets two assistant instances sharing a session write the same row.
165
- */
166
- export function resolveBankedUsage(
167
- existing: CostSessionRecord | undefined,
168
- transcriptIsAuthoritative: boolean,
169
- ): AggregateUsage {
170
- if (!existing) return emptyUsage();
171
- const carried = transcriptIsAuthoritative ? existing.banked : existing.usage;
172
- return carried ?? emptyUsage();
173
- }
174
-
175
- /** Clear all persisted records for a scope. */
176
- export function clearCostSessionHistory(scope: string): void {
177
- if (typeof localStorage === 'undefined') return;
178
- try {
179
- localStorage.removeItem(costHistoryStorageKey(scope));
180
- } catch {
181
- // Non-fatal.
182
- }
183
- }
184
-
185
- /** Display date for a history row (e.g. "Jun 14, 2026"). */
186
- export function formatCostSessionDate(iso: string): string {
187
- return new Intl.DateTimeFormat('en-US', {
188
- month: 'short',
189
- day: 'numeric',
190
- year: 'numeric',
191
- }).format(new Date(iso));
192
- }
193
-
194
- /**
195
- * Whether a value is a usable row. Exported because rows arrive by two routes and BOTH need it:
196
- * `loadCostSessionHistory` filters the `localStorage` path here, and the assistant filters what a
197
- * host provider returns. A row that skips this check is not merely ignored downstream — a
198
- * half-shaped `usage` sums to `NaN` through every total, and a missing `updatedAt` throws inside
199
- * the sort that orders the list, during render.
200
- */
201
- export function isCostSessionRecord(value: unknown): value is CostSessionRecord {
202
- if (typeof value !== 'object' || value == null) return false;
203
- const r = value as CostSessionRecord;
204
- return (
205
- typeof r.id === 'string' &&
206
- // Rows written before the upsert model have no `projectKey` and cannot be keyed
207
- // to a project, so they are discarded on load rather than silently pooled with
208
- // the new per-project rows (where they would double-count that project).
209
- typeof r.projectKey === 'string' &&
210
- typeof r.title === 'string' &&
211
- typeof r.updatedAt === 'string' &&
212
- isAggregateUsage(r.usage) &&
213
- // Absent is valid (nothing banked / pre-dates the field); present must be whole, since
214
- // a half-shaped `banked` would poison the lifetime total exactly as a bad `usage` would.
215
- (r.banked === undefined || isAggregateUsage(r.banked)) &&
216
- Array.isArray(r.models)
217
- );
218
- }
@@ -1,63 +0,0 @@
1
- import type { ChatMessage } from '@genesislcap/foundation-ai';
2
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
3
- import {
4
- deriveCostSessionTitleFromMessages,
5
- resolveCostSessionTitle,
6
- truncateCostSessionTitle,
7
- } from './derive-cost-session-title';
8
-
9
- const suite = createLogicSuite('deriveCostSessionTitle');
10
-
11
- const userMsg = (content: string): ChatMessage => ({ role: 'user', content });
12
-
13
- suite('truncateCostSessionTitle keeps first sentence only', () => {
14
- assert.is(
15
- truncateCostSessionTitle('Build a trade blotter. With FX options and limits.'),
16
- 'Build a trade blotter',
17
- );
18
- });
19
-
20
- suite('truncateCostSessionTitle caps word count', () => {
21
- const long = 'one two three four five six seven eight nine ten eleven twelve thirteen fourteen';
22
- assert.is(truncateCostSessionTitle(long), 'one two three four five six seven eight nine ten…');
23
- });
24
-
25
- suite('truncateCostSessionTitle caps character count on one long word', () => {
26
- const word = 'a'.repeat(80);
27
- assert.is(truncateCostSessionTitle(word).length, 60);
28
- assert.ok(truncateCostSessionTitle(word).endsWith('…'));
29
- });
30
-
31
- suite('deriveCostSessionTitleFromMessages skips empty and uses first user turn', () => {
32
- assert.is(
33
- deriveCostSessionTitleFromMessages([
34
- { role: 'assistant', content: 'Hi' },
35
- { role: 'user', content: ' ' },
36
- { role: 'user', content: 'Equity options pricer for desk' },
37
- ]),
38
- 'Equity options pricer for desk',
39
- );
40
- });
41
-
42
- suite('resolveCostSessionTitle prefers app title over messages', () => {
43
- assert.is(
44
- resolveCostSessionTitle({
45
- appTitle: 'fx-trading',
46
- messages: [userMsg('Something else entirely')],
47
- headerTitle: 'Genesis Assistant',
48
- }),
49
- 'fx-trading',
50
- );
51
- });
52
-
53
- suite('resolveCostSessionTitle falls back to first user message', () => {
54
- assert.is(
55
- resolveCostSessionTitle({
56
- messages: [userMsg('Search for recent trades in APAC')],
57
- headerTitle: 'Genesis Assistant',
58
- }),
59
- 'Search for recent trades in APAC',
60
- );
61
- });
62
-
63
- suite.run();
@@ -1,76 +0,0 @@
1
- import type { ChatMessage } from '@genesislcap/foundation-ai';
2
-
3
- /** Max words kept from the first user turn when deriving a session title. */
4
- export const COST_SESSION_TITLE_MAX_WORDS = 10;
5
-
6
- /** Hard cap on derived title length (ellipsis included). */
7
- export const COST_SESSION_TITLE_MAX_CHARS = 60;
8
-
9
- const ELLIPSIS = '…';
10
-
11
- /** Plain text from a chat message `content` field. */
12
- export function getChatMessageText(content: ChatMessage['content']): string {
13
- return typeof content === 'string' ? content.trim() : '';
14
- }
15
-
16
- /**
17
- * Trim a raw user phrase to a display-friendly session title: first sentence,
18
- * then word cap, then character cap.
19
- */
20
- export function truncateCostSessionTitle(text: string): string {
21
- const normalized = text.replace(/\s+/g, ' ').trim();
22
- if (!normalized) return '';
23
-
24
- const firstSentence = normalized.split(/\n|[.!?](?:\s|$)/u)[0]?.trim() || normalized;
25
-
26
- const words = firstSentence.split(/\s+/).filter(Boolean);
27
- let title = words.slice(0, COST_SESSION_TITLE_MAX_WORDS).join(' ');
28
- if (words.length > COST_SESSION_TITLE_MAX_WORDS) {
29
- title = `${title}${ELLIPSIS}`;
30
- }
31
-
32
- if (title.length > COST_SESSION_TITLE_MAX_CHARS) {
33
- const sliceEnd = COST_SESSION_TITLE_MAX_CHARS - ELLIPSIS.length;
34
- title = `${title.slice(0, Math.max(0, sliceEnd)).trimEnd()}${ELLIPSIS}`;
35
- }
36
-
37
- return title;
38
- }
39
-
40
- /** First non-empty user message in the thread, shortened for cost history. */
41
- export function deriveCostSessionTitleFromMessages(
42
- messages: readonly ChatMessage[],
43
- ): string | undefined {
44
- for (const message of messages) {
45
- if (message.role !== 'user') continue;
46
- const title = truncateCostSessionTitle(message.content);
47
- if (title) return title;
48
- }
49
- return undefined;
50
- }
51
-
52
- export interface ResolveCostSessionTitleInput {
53
- explicit?: string;
54
- /** Host-provided label (e.g. Create `projectName`). */
55
- appTitle?: string;
56
- messages: readonly ChatMessage[];
57
- headerTitle?: string;
58
- fallback?: string;
59
- }
60
-
61
- /** Resolve the label stored on a finalized cost session record. */
62
- export function resolveCostSessionTitle(input: ResolveCostSessionTitleInput): string {
63
- const explicit = input.explicit?.trim();
64
- if (explicit) return explicit;
65
-
66
- const appTitle = input.appTitle?.trim();
67
- if (appTitle) return appTitle;
68
-
69
- const fromMessages = deriveCostSessionTitleFromMessages(input.messages);
70
- if (fromMessages) return fromMessages;
71
-
72
- const header = input.headerTitle?.trim();
73
- if (header) return header;
74
-
75
- return input.fallback?.trim() || 'Untitled session';
76
- }
@@ -1,163 +0,0 @@
1
- import type { ChatMessage } from '@genesislcap/foundation-ai';
2
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
3
- import { flattenSubAgentMessages } from './flatten-sub-agent-messages';
4
-
5
- const msg = (overrides: Partial<ChatMessage> = {}): ChatMessage => ({
6
- role: 'assistant',
7
- content: '',
8
- ...overrides,
9
- });
10
-
11
- /** A parent assistant message whose single tool call delegated to a sub-agent. */
12
- const delegating = (id: string, agentName: string, trace: ChatMessage[]): ChatMessage =>
13
- msg({
14
- agentName,
15
- toolCalls: [{ id, name: 'requestSubAgent', args: {}, subAgentTrace: trace }],
16
- });
17
-
18
- const suite = createLogicSuite('flattenSubAgentMessages');
19
-
20
- suite('returns an empty array for an empty list', () => {
21
- assert.equal(flattenSubAgentMessages([]), []);
22
- });
23
-
24
- suite('emits top-level messages unchanged and tags none as sub-agent', () => {
25
- const out = flattenSubAgentMessages([
26
- msg({ role: 'user', content: 'hi', agentName: 'UI Builder' }),
27
- msg({ content: 'hello', agentName: 'UI Builder' }),
28
- ]);
29
- assert.is(out.length, 2);
30
- assert.is(out[0].kind, 'message');
31
- assert.is(out[0].agentName, 'UI Builder');
32
- // No depth/marker fields on top-level entries.
33
- assert.is(out[0].subAgentDepth, undefined);
34
- assert.is(out[0].subAgentName, undefined);
35
- assert.is(out[1].subAgentOf, undefined);
36
- });
37
-
38
- suite('hoists a sub-agent trace to top-level entries after the spawning message', () => {
39
- const trace = [
40
- msg({
41
- agentName: 'UI Architecture Planner',
42
- content: '',
43
- toolCalls: [{ id: 'g', name: 'grep_source', args: {} }],
44
- }),
45
- msg({
46
- role: 'tool',
47
- agentName: 'UI Architecture Planner',
48
- toolResult: { toolCallId: 'g', content: 'result' },
49
- }),
50
- ];
51
- const out = flattenSubAgentMessages([
52
- delegating('tc1', 'UI Builder', trace),
53
- msg({
54
- role: 'tool',
55
- agentName: 'UI Builder',
56
- toolResult: { toolCallId: 'tc1', content: 'plan done' },
57
- }),
58
- ]);
59
-
60
- // parent tool-call msg, then 2 hoisted children, then the parent tool-result msg.
61
- assert.is(out.length, 4);
62
- assert.is(out[0].agentName, 'UI Builder'); // parent unchanged
63
- assert.is(out[1].agentName, 'UI Builder › UI Architecture Planner');
64
- assert.is(out[2].agentName, 'UI Builder › UI Architecture Planner');
65
- assert.is(out[3].agentName, 'UI Builder'); // parent tool-result, back at top level
66
- });
67
-
68
- suite('marks hoisted entries with depth, raw name, and the spawning tool-call id', () => {
69
- const out = flattenSubAgentMessages([
70
- delegating('tc1', 'UI Builder', [msg({ agentName: 'UI Architecture Planner', content: 'x' })]),
71
- ]);
72
- const child = out[1];
73
- assert.is(child.subAgentDepth, 1);
74
- assert.is(child.subAgentName, 'UI Architecture Planner');
75
- assert.is(child.subAgentOf, 'tc1');
76
- });
77
-
78
- suite(
79
- 'moves the trace out — the emitted parent tool call no longer carries it (no duplication)',
80
- () => {
81
- const trace = [msg({ agentName: 'Planner', content: 'only once' })];
82
- const out = flattenSubAgentMessages([delegating('tc1', 'UI Builder', trace)]);
83
- // The trace is stripped from the emitted tool call ...
84
- assert.is(out[0].toolCalls![0].subAgentTrace, undefined);
85
- // ... and surfaces exactly once, as a hoisted top-level entry.
86
- const hoisted = out.filter((e) => e.subAgentDepth === 1);
87
- assert.is(hoisted.length, 1);
88
- assert.is(hoisted[0].content, 'only once');
89
- },
90
- );
91
-
92
- suite('preserves per-message usage fields on hoisted entries', () => {
93
- const out = flattenSubAgentMessages([
94
- delegating('tc1', 'UI Builder', [
95
- msg({
96
- agentName: 'Planner',
97
- model: 'claude-sonnet-4-6',
98
- inputTokens: 100,
99
- outputTokens: 20,
100
- cost: 0.5,
101
- }),
102
- ]),
103
- ]);
104
- const child = out[1];
105
- assert.is(child.model, 'claude-sonnet-4-6');
106
- assert.is(child.inputTokens, 100);
107
- assert.is(child.outputTokens, 20);
108
- assert.is(child.cost, 0.5);
109
- });
110
-
111
- suite('composes the breadcrumb for nested sub-agents', () => {
112
- const grandchild = [msg({ agentName: 'Grounding', content: 'deep' })];
113
- const childTrace = [delegating('tc2', 'Planner', grandchild)];
114
- const out = flattenSubAgentMessages([delegating('tc1', 'UI Builder', childTrace)]);
115
-
116
- // top: UI Builder | depth1: Planner | depth2: Grounding
117
- const depth1 = out.find((e) => e.subAgentDepth === 1)!;
118
- const depth2 = out.find((e) => e.subAgentDepth === 2)!;
119
- assert.is(depth1.agentName, 'UI Builder › Planner');
120
- assert.is(depth2.agentName, 'UI Builder › Planner › Grounding');
121
- assert.is(depth2.subAgentName, 'Grounding');
122
- assert.is(depth2.subAgentOf, 'tc2');
123
- });
124
-
125
- suite('correlates two sub-agents spawned in the same parent turn', () => {
126
- const parent = msg({
127
- agentName: 'UI Builder',
128
- toolCalls: [
129
- {
130
- id: 'a',
131
- name: 'requestSubAgent',
132
- args: {},
133
- subAgentTrace: [msg({ agentName: 'Planner', content: 'p' })],
134
- },
135
- {
136
- id: 'b',
137
- name: 'requestSubAgent',
138
- args: {},
139
- subAgentTrace: [msg({ agentName: 'Reviewer', content: 'r' })],
140
- },
141
- ],
142
- });
143
- const out = flattenSubAgentMessages([parent]);
144
- const planner = out.find((e) => e.subAgentName === 'Planner')!;
145
- const reviewer = out.find((e) => e.subAgentName === 'Reviewer')!;
146
- assert.is(planner.subAgentOf, 'a');
147
- assert.is(reviewer.subAgentOf, 'b');
148
- assert.is(planner.agentName, 'UI Builder › Planner');
149
- assert.is(reviewer.agentName, 'UI Builder › Reviewer');
150
- });
151
-
152
- suite('falls back gracefully when the spawning message has no agentName', () => {
153
- const out = flattenSubAgentMessages([
154
- delegating('tc1', undefined as unknown as string, [
155
- msg({ agentName: 'Planner', content: 'x' }),
156
- ]),
157
- ]);
158
- // No parent name to prefix → show the sub-agent's own name unbreadcrumbed.
159
- assert.is(out[1].agentName, 'Planner');
160
- assert.is(out[1].subAgentName, 'Planner');
161
- });
162
-
163
- suite.run();
@@ -1,88 +0,0 @@
1
- import type { ChatMessage } from '@genesislcap/foundation-ai';
2
-
3
- /**
4
- * A chat message lifted into the debug-log timeline as a `kind: 'message'` entry.
5
- *
6
- * @beta
7
- */
8
- export type TimelineMessage = ChatMessage & {
9
- kind: 'message';
10
- /**
11
- * Set only on entries hoisted out of a `subAgentTrace`: the sub-agent's own
12
- * (un-breadcrumbed) `agentName`, kept for filtering since the displayed
13
- * `agentName` is rewritten to a `"<parent> › <sub-agent>"` breadcrumb.
14
- */
15
- subAgentName?: string;
16
- /** Delegation depth — absent/0 for top-level, 1 for a sub-agent, 2 for a sub-agent's sub-agent, … */
17
- subAgentDepth?: number;
18
- /** Id of the parent tool call that spawned this run — correlates a hoisted message back to its delegation. */
19
- subAgentOf?: string;
20
- };
21
-
22
- /**
23
- * Flatten sub-agent conversations into a single top-level message stream for the
24
- * debug-log timeline.
25
- *
26
- * A tool call that delegates to a sub-agent carries the sub-agent's entire
27
- * conversation on `toolCall.subAgentTrace`. The live UI reads that nested shape,
28
- * but a top-to-bottom timeline reads far better with those messages inline next to
29
- * the turn that produced them. This walks the list and, for every tool call with a
30
- * `subAgentTrace`, hoists the sub-agent's messages up as their own timeline
31
- * entries while **removing** the nested copy from the emitted parent tool call — a
32
- * move, not a copy. The data is relocated, never duplicated: the export stays the
33
- * same size and a naive `sum(timeline[].cost)` does not double-count (the
34
- * canonical `sumCosts` total still reads the un-flattened history).
35
- *
36
- * Hoisted entries are:
37
- * - **breadcrumbed**: `agentName` becomes `"<parent> › <sub-agent>"`, composing for
38
- * nested sub-agents (`"<parent> › <sub> › <subsub>"`); the raw sub-agent name is
39
- * preserved on `subAgentName`.
40
- * - **depth-tagged** (`subAgentDepth`) and **correlated** to the spawning tool call
41
- * (`subAgentOf`), so the delegation tree is reconstructable even when two
42
- * sub-agents run within the same parent turn.
43
- *
44
- * Top-level messages (depth 0) are emitted unchanged apart from the trace strip.
45
- * Order is preserved within each list; the caller sorts the whole timeline by
46
- * timestamp afterwards, and every sub-agent message carries its own timestamp, so
47
- * hoisted entries interleave chronologically between the parent's tool-call and
48
- * tool-result messages.
49
- *
50
- * @internal
51
- */
52
- export function flattenSubAgentMessages(
53
- messages: readonly ChatMessage[],
54
- prefix = '',
55
- depth = 0,
56
- subAgentOf?: string,
57
- ): TimelineMessage[] {
58
- return messages.flatMap((m) => {
59
- const breadcrumb = prefix ? `${prefix} › ${m.agentName ?? '?'}` : m.agentName;
60
- const traceCalls = m.toolCalls?.filter((tc) => tc.subAgentTrace?.length) ?? [];
61
-
62
- const entry: TimelineMessage = {
63
- kind: 'message',
64
- ...m,
65
- // Move the (about-to-be-hoisted) child conversations out of the emitted tool
66
- // calls. Setting it to `undefined` drops the key from the serialized export.
67
- ...(traceCalls.length > 0 && {
68
- toolCalls: m.toolCalls!.map((tc) =>
69
- tc.subAgentTrace ? { ...tc, subAgentTrace: undefined } : tc,
70
- ),
71
- }),
72
- // Mark + breadcrumb hoisted sub-agent messages; top-level (depth 0) stays as-is.
73
- ...(depth > 0 && {
74
- agentName: breadcrumb,
75
- subAgentName: m.agentName,
76
- subAgentDepth: depth,
77
- subAgentOf,
78
- }),
79
- };
80
-
81
- if (traceCalls.length === 0) return [entry];
82
-
83
- const hoisted = traceCalls.flatMap((tc) =>
84
- flattenSubAgentMessages(tc.subAgentTrace!, breadcrumb ?? '', depth + 1, tc.id),
85
- );
86
- return [entry, ...hoisted];
87
- });
88
- }
@@ -1,26 +0,0 @@
1
- /**
2
- * The ONE place this package renders a USD amount as text.
3
- *
4
- * Extracted (GENC-1464 budget meter) from the ad-hoc `.toFixed(2)` call sites
5
- * that had grown in `formatBlockedReason` and the settings modal's cost blocks,
6
- * so the blocked banner, the session-cost figures and the budget meter cannot
7
- * drift to different precisions. Two decimals always — money columns jitter
8
- * when "$5.1" sits above "$5.10".
9
- *
10
- * @internal
11
- */
12
-
13
- /** Decimal places for every USD figure this package renders. */
14
- export const USD_DECIMALS = 2;
15
-
16
- /**
17
- * The bare amount, no currency sign — `(12.3) => "12.30"`.
18
- *
19
- * Exists for the settings modal's cost blocks, where the `$` is a separately
20
- * styled `<span class="prefix">` and folding it into the text would change the
21
- * DOM those styles target.
22
- */
23
- export const formatUsdAmount = (value: number): string => value.toFixed(USD_DECIMALS);
24
-
25
- /** A signed USD figure — `(12.3) => "$12.30"`. */
26
- export const formatUsd = (value: number): string => `$${formatUsdAmount(value)}`;