@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,477 +0,0 @@
1
- import type { ChatMessage } from '@genesislcap/foundation-ai';
2
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
3
- import type { VendorBudgetFigures } from '../state/ai-assistant-slice';
4
- import type { SessionStoreReturn } from '../state/session-store';
5
- import { clearAllSessionStores, getSessionStore } from '../state/session-store';
6
- import { logger } from '../utils/logger';
7
- import { budgetPercentOf, FoundationAiAssistant } from './main';
8
-
9
- // Hold a reference so the custom-element registration isn't tree-shaken.
10
- FoundationAiAssistant;
11
-
12
- // GENC-1464 — the settings-modal per-vendor budget meter.
13
- //
14
- // Like `blocked-state.test.ts`, this pins the GETTER LOGIC the template bindings
15
- // depend on rather than the rendered DOM, and for the same reason: mounting the
16
- // assistant runs `connectedCallback`, which subscribes to `agenticActivityBus`
17
- // and leaves the runner's event loop open. `document.createElement` only
18
- // upgrades the element — it does not connect — so nothing subscribes here.
19
- //
20
- // The bindings under test in `settings-modal.template.ts` are:
21
- // ${when((x) => x.settingsBudgetUsageVisible, …)} (the whole meter)
22
- // ${repeat((x) => x.settingsBudgetRows, …)} (one row per vendor)
23
- // <…-progress value="${(row) => row.barValue}"> (the clamped bar)
24
- // plus `settingsModelSectionVisible`, which decides whether the "AI Model
25
- // Settings" section renders at all.
26
-
27
- const Suite = createLogicSuite('FoundationAiAssistant budget meter');
28
-
29
- let storeSeq = 0;
30
-
31
- /** A fresh (unconnected) element wired to its own real session store. */
32
- function element(): FoundationAiAssistant {
33
- const el = document.createElement('foundation-ai-assistant') as FoundationAiAssistant;
34
- storeSeq += 1;
35
- (el as unknown as { _sessionRef: unknown })._sessionRef = getSessionStore(
36
- `budget-meter-test-${storeSeq}`,
37
- false,
38
- );
39
- return el;
40
- }
41
-
42
- /** The raw stored figures, read the way the rows getter reads them. */
43
- const storedBudgets = (el: FoundationAiAssistant) =>
44
- (el as unknown as { _sessionRef: SessionStoreReturn })._sessionRef.store.aiAssistant
45
- .vendorBudgets;
46
-
47
- /** The store half of "Clear" / "New chat" — see blocked-state.test.ts. */
48
- const resetSession = (el: FoundationAiAssistant): void =>
49
- (
50
- el as unknown as { _sessionRef: SessionStoreReturn }
51
- )._sessionRef.actions.aiAssistant.resetSession();
52
-
53
- /** Count `logger.warn` calls during `run` — the boundary warns ONCE per element. */
54
- const countWarns = (run: () => void): number => {
55
- const original = logger.warn;
56
- let calls = 0;
57
- logger.warn = ((...args: unknown[]) => {
58
- calls += 1;
59
- void args;
60
- }) as typeof logger.warn;
61
- try {
62
- run();
63
- } finally {
64
- logger.warn = original;
65
- }
66
- return calls;
67
- };
68
-
69
- /**
70
- * Count reads of `settingsBudgetRows` during `run`.
71
- *
72
- * Building the rows formats every vendor's money text — a `formatUsd` pair and
73
- * a `budgetPercentOf` each — so a gate that answers THROUGH the rows pays for
74
- * all of it. Counting reads of the getter is the direct probe of that; the spy
75
- * goes on the prototype (where the accessor lives) and is put back after.
76
- */
77
- const countRowBuilds = (run: () => void): number => {
78
- const proto = FoundationAiAssistant.prototype;
79
- const original = Object.getOwnPropertyDescriptor(proto, 'settingsBudgetRows')!;
80
- let calls = 0;
81
- Object.defineProperty(proto, 'settingsBudgetRows', {
82
- ...original,
83
- get(this: FoundationAiAssistant) {
84
- calls += 1;
85
- return original.get!.call(this);
86
- },
87
- });
88
- try {
89
- run();
90
- } finally {
91
- Object.defineProperty(proto, 'settingsBudgetRows', original);
92
- }
93
- return calls;
94
- };
95
-
96
- Suite.after(() => {
97
- clearAllSessionStores();
98
- });
99
-
100
- // ── Feeding figures ─────────────────────────────────────────────────────────
101
-
102
- Suite('feeding two vendors renders two rows with the real money text', () => {
103
- const el = element();
104
-
105
- assert.is(el.setVendorBudget('anthropic', { budgetUsd: 50, spentUsd: 45 }), true);
106
- assert.is(el.setVendorBudget('gemini', { budgetUsd: 200, spentUsd: 30.5 }), true);
107
-
108
- const rows = el.settingsBudgetRows;
109
- assert.is(rows.length, 2);
110
- assert.is(rows[0].vendor, 'anthropic');
111
- assert.is(rows[0].label, 'Anthropic', 'display name comes from VENDOR_LABELS');
112
- assert.is(rows[0].figures, '$45.00 / $50.00 (90%)');
113
- assert.is(rows[0].barValue, 90);
114
- assert.is(rows[1].vendor, 'gemini');
115
- assert.is(rows[1].figures, '$30.50 / $200.00 (15%)', 'two decimals always, percent rounded');
116
- });
117
-
118
- Suite('rows keep BUDGETED_VENDORS order however the host ordered its feed', () => {
119
- // The feed loops over a server response whose order is the server's business;
120
- // the meter must not reorder itself between turns because of it.
121
- const el = element();
122
- el.setVendorBudget('gemini', { budgetUsd: 10, spentUsd: 1 });
123
- el.setVendorBudget('anthropic', { budgetUsd: 10, spentUsd: 2 });
124
-
125
- assert.equal(
126
- el.settingsBudgetRows.map((r) => r.vendor),
127
- ['anthropic', 'gemini'],
128
- );
129
- });
130
-
131
- Suite('a vendor with no figures gets no row — that is how "unlimited" renders', () => {
132
- // Display rule: hosts simply do not feed a vendor the server marks
133
- // `unlimited: true` / `limitUsd: null`, and the meter shows only what it was
134
- // fed. No "∞" row, no empty bar.
135
- const el = element();
136
- el.setVendorBudget('anthropic', { budgetUsd: 50, spentUsd: 15 });
137
-
138
- const rows = el.settingsBudgetRows;
139
- assert.is(rows.length, 1);
140
- assert.is(rows[0].vendor, 'anthropic');
141
- });
142
-
143
- // ── Overshoot: the text tells the truth, the bar clamps ─────────────────────
144
-
145
- Suite('spend past the cap shows the real figures but clamps the bar at 100', () => {
146
- // Real overshoot exists — the last allowed call can cross the cap (the demo
147
- // "Exhaust" lands at $62.34 of $50 on purpose). A bar past its end renders
148
- // nonsense, so only the bar clamps.
149
- const el = element();
150
- el.setVendorBudget('anthropic', { budgetUsd: 50, spentUsd: 62.34 });
151
-
152
- const [row] = el.settingsBudgetRows;
153
- assert.is(row.figures, '$62.34 / $50.00 (125%)', 'the text is the truth, unclamped');
154
- assert.is(row.barValue, 100, 'the bar is full, not broken');
155
- });
156
-
157
- Suite('a $0 cap reads as a full meter, not a division by zero', () => {
158
- // The platform treats a $0 budget as "no spend allowed" (not "unlimited"), so
159
- // it has no headroom by definition.
160
- assert.is(budgetPercentOf({ budgetUsd: 0, spentUsd: 0 }), 100);
161
- assert.is(budgetPercentOf({ budgetUsd: 0, spentUsd: 3 }), 100);
162
- const el = element();
163
- el.setVendorBudget('gemini', { budgetUsd: 0, spentUsd: 0 });
164
- assert.is(el.settingsBudgetRows[0].figures, '$0.00 / $0.00 (100%)');
165
- assert.is(el.settingsBudgetRows[0].barValue, 100);
166
- });
167
-
168
- // ── Clearing ────────────────────────────────────────────────────────────────
169
-
170
- Suite('setVendorBudget(vendor, null) clears that row and only that row', () => {
171
- const el = element();
172
- el.setVendorBudget('anthropic', { budgetUsd: 50, spentUsd: 45 });
173
- el.setVendorBudget('gemini', { budgetUsd: 50, spentUsd: 15 });
174
-
175
- assert.is(el.setVendorBudget('anthropic', null), true);
176
-
177
- assert.equal(
178
- el.settingsBudgetRows.map((r) => r.vendor),
179
- ['gemini'],
180
- );
181
- });
182
-
183
- Suite('clearing a vendor that has no figures is a true no-op', () => {
184
- const el = element();
185
- assert.is(el.setVendorBudget('anthropic', null), true, 'clearing nothing is not an error');
186
- assert.is(el.settingsBudgetRows.length, 0);
187
- });
188
-
189
- Suite("undefined clears too — an untyped host's missing lookup is not a crash", () => {
190
- // The `| null` parameter type is for typed callers; the runtime is more
191
- // forgiving on purpose. Untyped JS hosts feed this straight from
192
- // GET /api/budget, where a per-vendor lookup that MISSES yields `undefined`,
193
- // not `null` — and a boundary whose whole contract is "return false, never
194
- // throw" must not TypeError on it. "No figures" means "no row", either way.
195
- const el = element();
196
- el.setVendorBudget('anthropic', { budgetUsd: 50, spentUsd: 45 });
197
- el.setVendorBudget('gemini', { budgetUsd: 50, spentUsd: 15 });
198
-
199
- const warns = countWarns(() => {
200
- assert.is(el.setVendorBudget('anthropic', undefined as never), true, 'accepted, not refused');
201
- });
202
-
203
- assert.is(warns, 0, 'a missing lookup is not garbage input — nothing to warn about');
204
- assert.equal(
205
- el.settingsBudgetRows.map((r) => r.vendor),
206
- ['gemini'],
207
- 'it cleared that vendor and only that vendor, exactly as null does',
208
- );
209
- });
210
-
211
- // ── Invalid input: false + warn once, never a throw, never a write ──────────
212
-
213
- Suite('an unbudgeted vendor is refused with false and no state change', () => {
214
- const el = element();
215
- el.setVendorBudget('anthropic', { budgetUsd: 50, spentUsd: 15 });
216
-
217
- // 'none' is the no-provider sentinel; 'chrome' is on-device (no pot to
218
- // meter); 'openai' is refused by the proxy up front — none can carry figures.
219
- for (const vendor of ['none', 'chrome', 'openai', 'acme-ai']) {
220
- assert.is(el.setVendorBudget(vendor, { budgetUsd: 1, spentUsd: 0 }), false, vendor);
221
- }
222
-
223
- assert.equal(
224
- el.settingsBudgetRows.map((r) => r.vendor),
225
- ['anthropic'],
226
- 'nothing was stored for any of them',
227
- );
228
- });
229
-
230
- Suite('non-finite or negative figures are refused with false and no state change', () => {
231
- const el = element();
232
- el.setVendorBudget('anthropic', { budgetUsd: 50, spentUsd: 15 });
233
-
234
- const bad: Array<{ budgetUsd: number; spentUsd: number }> = [
235
- { budgetUsd: NaN, spentUsd: 1 },
236
- { budgetUsd: 1, spentUsd: NaN },
237
- { budgetUsd: Infinity, spentUsd: 1 },
238
- { budgetUsd: 1, spentUsd: -0.01 },
239
- { budgetUsd: -50, spentUsd: 1 },
240
- // The endpoint's `limitUsd: null` (unlimited) must be dropped by the host,
241
- // not forwarded; forwarded anyway, it is garbage, not a clear.
242
- { budgetUsd: null as unknown as number, spentUsd: 1 },
243
- ];
244
- for (const figures of bad) {
245
- assert.is(el.setVendorBudget('anthropic', figures), false, JSON.stringify(figures));
246
- }
247
-
248
- assert.is(el.settingsBudgetRows[0].figures, '$15.00 / $50.00 (30%)', 'the good figures survive');
249
- });
250
-
251
- Suite('garbage warns once per element, not once per call', () => {
252
- // The documented feed re-runs after every turn, so a host bug would repeat
253
- // the same warning on every send for the life of the page.
254
- const el = element();
255
-
256
- const warns = countWarns(() => {
257
- assert.is(el.setVendorBudget('openai', { budgetUsd: 1, spentUsd: 0 }), false);
258
- assert.is(el.setVendorBudget('openai', { budgetUsd: 1, spentUsd: 0 }), false);
259
- assert.is(el.setVendorBudget('anthropic', { budgetUsd: NaN, spentUsd: 0 }), false);
260
- });
261
-
262
- assert.is(warns, 1, 'one warning covers the whole broken feed');
263
- });
264
-
265
- // ── Not ready yet: the other reason a feed is refused ───────────────────────
266
-
267
- Suite('a feed before the element has a session store is refused, not silently dropped', () => {
268
- // The lifecycle window: a host feeding ahead of the first append, or during a
269
- // pop-out remount, has no store to write to. The dispatch no-ops there, so
270
- // returning true reported a write that never happened and the caller — which
271
- // reads the boolean as "accepted" — would never retry.
272
- const el = document.createElement('foundation-ai-assistant') as FoundationAiAssistant;
273
-
274
- const warns = countWarns(() => {
275
- assert.is(el.setVendorBudget('anthropic', { budgetUsd: 50, spentUsd: 45 }), false);
276
- });
277
-
278
- assert.is(warns, 0, 'a lifecycle window is not bad input — the warn-once channel is for garbage');
279
- assert.is(el.settingsBudgetRows.length, 0, 'nothing was stored');
280
- assert.is(el.settingsBudgetUsageVisible, false, 'and nothing is shown');
281
-
282
- // The recovery is the documented per-turn re-feed, which lands as soon as the
283
- // store exists — the connected path is unchanged.
284
- const ready = element();
285
- assert.is(ready.setVendorBudget('anthropic', { budgetUsd: 50, spentUsd: 45 }), true);
286
- assert.is(ready.settingsBudgetRows.length, 1);
287
- });
288
-
289
- // ── Idempotence ─────────────────────────────────────────────────────────────
290
-
291
- Suite('re-feeding identical figures leaves the stored object untouched', () => {
292
- // The feed re-runs unconditionally after every turn; identical dollars must
293
- // not publish a new store reference (and re-render an unchanged meter).
294
- const el = element();
295
- el.setVendorBudget('anthropic', { budgetUsd: 50, spentUsd: 15 });
296
- const before = storedBudgets(el).anthropic;
297
-
298
- assert.is(el.setVendorBudget('anthropic', { budgetUsd: 50, spentUsd: 15 }), true);
299
-
300
- assert.is(storedBudgets(el).anthropic, before, 'same dollars, same object');
301
- });
302
-
303
- Suite('the store keeps its own copy, not the host object', () => {
304
- // The store must not adopt an object the host still owns: immer finalizes
305
- // whatever the reducer assigns into the draft and FREEZES it — so adoption
306
- // would freeze state the host believes is its own, turning the host's next
307
- // `figures.spentUsd += x` into a strict-mode TypeError (or a silent no-op).
308
- // The frozen-ness probe is the observable that catches adoption directly;
309
- // the mutation-isolation assertion is the user-visible consequence.
310
- const el = element();
311
- const hostFigures: VendorBudgetFigures = { budgetUsd: 50, spentUsd: 15 };
312
- el.setVendorBudget('anthropic', hostFigures);
313
-
314
- assert.is(Object.isFrozen(hostFigures), false, 'the host object was not adopted-and-frozen');
315
-
316
- hostFigures.spentUsd = 999;
317
-
318
- assert.is(el.settingsBudgetRows[0].figures, '$15.00 / $50.00 (30%)');
319
- });
320
-
321
- // ── Lifetime ────────────────────────────────────────────────────────────────
322
-
323
- Suite('the figures survive "Clear" / "New chat" — budget is not conversation state', () => {
324
- // Same side of the resetSession preserve-list as the blocked latch: a new
325
- // chat refills no budget, so wiping the meter would blank real dollars until
326
- // the host's next re-feed. Opposite side from contextTokens, which IS
327
- // conversation state (a fresh chat genuinely has an empty context window).
328
- const el = element();
329
- el.setVendorBudget('anthropic', { budgetUsd: 50, spentUsd: 45 });
330
- el.messages = [{ id: '1', role: 'user', content: 'hello' } as ChatMessage];
331
-
332
- resetSession(el);
333
-
334
- assert.is(el.messages.length, 0, 'the transcript is gone');
335
- assert.is(el.contextTokens, undefined, 'context usage reset with the conversation');
336
- assert.is(el.settingsBudgetRows[0].figures, '$45.00 / $50.00 (90%)', 'the meter is not');
337
- });
338
-
339
- Suite('the figures are per session store — a different stateKey starts empty', () => {
340
- const first = element();
341
- const second = element();
342
-
343
- first.setVendorBudget('anthropic', { budgetUsd: 50, spentUsd: 45 });
344
-
345
- assert.is(first.settingsBudgetRows.length, 1);
346
- assert.is(second.settingsBudgetRows.length, 0, 'a different session store is unaffected');
347
- });
348
-
349
- // ── Visibility ──────────────────────────────────────────────────────────────
350
-
351
- Suite('showBudgetUsage: false hides the meter even with figures fed', () => {
352
- const el = element();
353
- el.setVendorBudget('anthropic', { budgetUsd: 50, spentUsd: 45 });
354
- assert.is(el.settingsBudgetUsageVisible, true, 'default ON — undefined shows');
355
-
356
- el.chatConfig = { ui: { showBudgetUsage: false } };
357
-
358
- assert.is(el.settingsBudgetUsageVisible, false);
359
- assert.is(el.settingsBudgetRows.length, 1, 'the state itself is kept, only the render is off');
360
-
361
- el.chatConfig = { ui: { showBudgetUsage: true } };
362
- assert.is(el.settingsBudgetUsageVisible, true, 'explicit true shows too');
363
- });
364
-
365
- Suite('no figures → the meter behaves like the context indicator with no data', () => {
366
- // The absent case mirrors settingsContextUsageVisible exactly: nothing to
367
- // show means no meter AND no "AI Model Settings" section on its account.
368
- const el = element();
369
-
370
- assert.is(el.settingsContextUsageVisible, false, 'the sibling gate this one mirrors');
371
- assert.is(el.settingsBudgetUsageVisible, false);
372
- assert.is(el.settingsModelSectionVisible, false, 'no slot content, no data — no section');
373
- });
374
-
375
- Suite('clearing the last vendor takes the meter back out of view', () => {
376
- // The gate reads the RAW slice state rather than counting formatted rows, so
377
- // this is the case that keeps the two readings honest: a vendor that was fed
378
- // and then cleared must leave NO trace the gate could mistake for data.
379
- const el = element();
380
- el.setVendorBudget('anthropic', { budgetUsd: 50, spentUsd: 45 });
381
- assert.is(el.settingsBudgetUsageVisible, true);
382
-
383
- el.setVendorBudget('anthropic', null);
384
-
385
- assert.is(el.settingsBudgetUsageVisible, false, 'cleared is invisible, not an empty meter');
386
- assert.is(el.settingsBudgetRows.length, 0, 'the rows agree');
387
- assert.is(el.settingsModelSectionVisible, false, 'and the section goes with it');
388
- });
389
-
390
- Suite('the visibility gate answers without building a single row', () => {
391
- // The gate is read several times per render pass — this getter, the section
392
- // gate, the template's `when` — and answering it through
393
- // `settingsBudgetRows.length` formatted every vendor's money text each time
394
- // just to ask whether any rows exist. It reads the raw slice state instead;
395
- // the context sibling it mirrors gates on a bare `!= null` for the same
396
- // reason. The rendering path still builds rows, of course — the control below.
397
- const el = element();
398
- el.setVendorBudget('anthropic', { budgetUsd: 50, spentUsd: 45 });
399
- el.setVendorBudget('gemini', { budgetUsd: 200, spentUsd: 30.5 });
400
-
401
- assert.is(
402
- countRowBuilds(() => void el.settingsBudgetUsageVisible),
403
- 0,
404
- 'the gate builds nothing',
405
- );
406
- assert.is(
407
- countRowBuilds(() => void el.settingsBudgetRows),
408
- 1,
409
- 'the rendering path does (the control that the probe can see a read at all)',
410
- );
411
- });
412
-
413
- Suite('figures alone are enough to surface the AI Model Settings section', () => {
414
- // The wiring into settingsModelSectionVisible, the same way
415
- // settingsContextUsageVisible is wired in.
416
- const el = element();
417
- assert.is(el.settingsModelSectionVisible, false);
418
-
419
- el.setVendorBudget('gemini', { budgetUsd: 50, spentUsd: 15 });
420
-
421
- assert.is(el.settingsModelSectionVisible, true);
422
-
423
- el.chatConfig = { ui: { showBudgetUsage: false } };
424
- assert.is(el.settingsModelSectionVisible, false, 'and the config flag takes it back out');
425
- });
426
-
427
- // ── Freshness stamp + refresh request (GENC-1464 refresh affordance) ─────────
428
-
429
- Suite('every feed stamps budgetMeterLastFedAt — identical figures included', () => {
430
- const el = element();
431
- assert.is(el.budgetMeterLastFedAt, null, 'null before any feed');
432
- assert.is(el.settingsBudgetUpdatedLabel, undefined, 'no label before any feed');
433
-
434
- el.setVendorBudget('anthropic', { budgetUsd: 50, spentUsd: 10 });
435
- const first = el.budgetMeterLastFedAt;
436
- assert.ok(typeof first === 'number', 'stamped on feed');
437
- assert.ok(el.settingsBudgetUpdatedLabel!.startsWith('Updated '), 'label renders');
438
-
439
- // The store is idempotent by value, but a refresh that CONFIRMS the same
440
- // dollars is still a refresh — the stamp must move even when the rows don't.
441
- el.budgetMeterLastFedAt = first! - 5000; // age it so a same-ms re-feed still differs
442
- el.setVendorBudget('anthropic', { budgetUsd: 50, spentUsd: 10 });
443
- assert.ok(el.budgetMeterLastFedAt! > first! - 5000, 'identical re-feed still stamps');
444
-
445
- // Clearing figures is not a refresh — the label describes shown figures.
446
- const before = el.budgetMeterLastFedAt;
447
- el.setVendorBudget('anthropic', null);
448
- assert.is(el.budgetMeterLastFedAt, before, 'a clear does not stamp');
449
- });
450
-
451
- Suite('requestBudgetRefresh emits budget-refresh-requested, and settings open asks too', () => {
452
- const el = element();
453
- let fired = 0;
454
- el.addEventListener('budget-refresh-requested', () => {
455
- fired += 1;
456
- });
457
-
458
- el.requestBudgetRefresh();
459
- assert.is(fired, 1, 'the button path emits');
460
-
461
- // The element cannot fetch — the event is the whole contract. Hosts without
462
- // a listener simply keep the per-turn cadence, so firing is unconditional.
463
- //
464
- // The modal stub is load-bearing: openSettingsModal's `show` closure
465
- // re-queues itself via DOM.queueUpdate until `settingsModal` exists, and on
466
- // an unconnected element that is never — an infinite queue that keeps the
467
- // test runner alive (this file's documented hang class). `open: true` makes
468
- // the first queued run return immediately.
469
- (el as unknown as { settingsModal: { open: boolean; show(): void } }).settingsModal = {
470
- open: true,
471
- show() {},
472
- };
473
- el.openSettingsModal();
474
- assert.is(fired, 2, 'opening settings requests fresh figures');
475
- });
476
-
477
- Suite.run();