@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,67 +0,0 @@
1
- import { html, ref, repeat, slotted, ViewTemplate, when } from '@genesislcap/web-core';
2
- import type { AiChatBubble } from './chat-bubble';
3
-
4
- const genesisIconTemplate = html`
5
- <svg
6
- viewBox="-4 0 30 26"
7
- fill="none"
8
- xmlns="http://www.w3.org/2000/svg"
9
- style="width:60%;height:60%"
10
- >
11
- <path
12
- d="M18.7702 23.0437C18.7702 22.4675 18.8526 21.8089 19.0995 21.3149C19.3465 20.7386 19.6758 20.2447 20.0874 19.833C20.4991 19.4214 20.993 19.0921 21.5693 18.8451C22.1456 18.5982 22.7219 18.5158 23.2981 18.5158H25.9326V11.6828H19.1819V14.3172C19.1819 15.5521 18.6879 16.7047 17.8647 17.5279C17.0414 18.3512 15.8888 18.8451 14.7363 18.9275H6.7507V6.99025H16.4651V0.157227H3.54C2.63442 0.157227 1.72884 0.568855 1.07023 1.22746C0.411628 1.88606 0 2.79165 0 3.77955V22.2205C0 23.1261 0.411628 24.0316 1.07023 24.7726C1.72884 25.4312 2.63442 25.8428 3.54 25.8428H18.7702V23.0437Z"
13
- fill="currentColor"
14
- />
15
- </svg>
16
- `;
17
-
18
- export const AiChatBubbleTemplate = (): ViewTemplate<AiChatBubble> => {
19
- return html<AiChatBubble>`
20
- ${when(
21
- (x) => !x.dialogOpen,
22
- html<AiChatBubble>`
23
- <div
24
- class="bubble ${(x) => (x.hovered ? 'hovered' : '')}"
25
- style="${(x) => `translate: ${x.bubbleLeft}px ${x.bubbleTop}px`}"
26
- ${ref('bubbleEl')}
27
- @mousedown="${(x, c) => x.onBubbleMouseDown(c.event as MouseEvent)}"
28
- @mouseenter="${(x) => x.onBubbleMouseEnter()}"
29
- @mouseleave="${(x) => x.onBubbleMouseLeave()}"
30
- >
31
- ${when(
32
- (x) => !!x.imageSrc,
33
- html<AiChatBubble>`
34
- <img src="${(x) => x.imageSrc}" alt="AI" draggable="false" />
35
- `,
36
- )}
37
- ${when((x) => !x.imageSrc, genesisIconTemplate)}
38
- <div class="pulse-ring"></div>
39
- <div class="tooltip" style="${(x) => x.tooltipStyle}" ${ref('tooltipEl')}>
40
- ${(x) => x.title}
41
- </div>
42
- </div>
43
- `,
44
- )}
45
- ${repeat(
46
- // Pass the component instance as the sole array item so inner bindings
47
- // can use x directly. recycle: false ensures a fresh element on every
48
- // open, preventing stale .closing class or animationend listeners from
49
- // a previous close persisting on the reused view.
50
- (x) => (x.dialogOpen ? [x] : []),
51
- html<AiChatBubble>`
52
- <div
53
- class="dialog"
54
- style="${(x) =>
55
- `translate: ${x.dialogLeft}px ${x.dialogTop}px; width: ${x.dialogWidth}px; height: ${x.dialogHeight}px`}"
56
- @chat-header-mousedown="${(x, c) => x.onDialogHeaderMouseDown(c.event as CustomEvent)}"
57
- ${ref('dialogEl')}
58
- >
59
- <div class="dialog-content">
60
- <slot name="dialog-content" ${slotted('dialogContent')}></slot>
61
- </div>
62
- </div>
63
- `,
64
- { recycle: false },
65
- )}
66
- `;
67
- };
@@ -1,436 +0,0 @@
1
- import { attr, customElement, GenesisElement, html, observable } from '@genesislcap/web-core';
2
- import { agenticActivityBus } from '../../channel/ai-activity-bus';
3
- import { styles } from './chat-bubble.styles';
4
- import { AiChatBubbleTemplate } from './chat-bubble.template';
5
-
6
- /** Default dimensions of the dialog panel */
7
- const DIALOG_DEFAULT_WIDTH = 400;
8
- const DIALOG_DEFAULT_HEIGHT = 520;
9
-
10
- /** Initial position: bubble starts this far from the right/bottom viewport edges */
11
- const BUBBLE_INITIAL_RIGHT_OFFSET = 90;
12
-
13
- /** Initial position: dialog starts this far from the right/bottom viewport edges */
14
- const DIALOG_INITIAL_RIGHT_OFFSET = 440;
15
- const DIALOG_INITIAL_BOTTOM_OFFSET = 570;
16
-
17
- /** Minimum distance the dialog is allowed from any viewport edge */
18
- const VIEWPORT_EDGE_MARGIN = 20;
19
-
20
- /** Minimum pointer movement (px) to classify an interaction as a drag rather than a click */
21
- const DRAG_THRESHOLD = 4;
22
-
23
- /** Keeps the bubble fully inside the viewport when dragged (accounts for bubble size + border) */
24
- const BUBBLE_DRAG_MARGIN = 64;
25
-
26
- /** Horizontal offset between bubble center and dialog left edge when toggling open/close */
27
- const DIALOG_BUBBLE_HORIZONTAL_OFFSET = 340;
28
-
29
- /** How far above the bubble the dialog opens */
30
- const DIALOG_OPEN_TOP_OFFSET = 470;
31
-
32
- /** How far below the dialog top the bubble snaps when closing via the X button */
33
- const BUBBLE_SNAP_BOTTOM_OFFSET = 458;
34
-
35
- /** Fallback tooltip width used before the element has been measured in the DOM */
36
- const TOOLTIP_FALLBACK_WIDTH = 155;
37
-
38
- /**
39
- * Floating chat bubble component that provides a draggable orb and dialog panel
40
- * for embedding an AI assistant without a full-page layout.
41
- *
42
- * @remarks
43
- * This component is optional — apps can use `foundation-ai-assistant` directly
44
- * in a layout panel instead, depending on their AI interaction flow.
45
- *
46
- * @beta
47
- */
48
- @customElement({
49
- name: 'foundation-ai-chat-bubble',
50
- template: html`
51
- ${() => AiChatBubbleTemplate()}
52
- `,
53
- styles,
54
- })
55
- export class AiChatBubble extends GenesisElement {
56
- @attr({ attribute: 'title' }) title: string = 'AI Assistant';
57
- @attr({ attribute: 'image-src' }) imageSrc?: string;
58
- @attr({ attribute: 'design-system-prefix' }) designSystemPrefix: string = 'rapid';
59
-
60
- /** @internal */
61
- @observable dialogContent: HTMLElement[] = [];
62
- /** @internal */
63
- @observable tooltipMeasuredWidth: number = 0;
64
- /** Current left position of the bubble orb (px from viewport left). @internal */
65
- @observable bubbleLeft: number = 0;
66
- /** Current top position of the bubble orb (px from viewport top). @internal */
67
- @observable bubbleTop: number = 0;
68
- /** Current left position of the dialog panel (px from viewport left). @internal */
69
- @observable dialogLeft: number = 0;
70
- /** Current top position of the dialog panel (px from viewport top). @internal */
71
- @observable dialogTop: number = 0;
72
- /** Current rendered width of the dialog panel (px). @internal */
73
- @observable dialogWidth: number = DIALOG_DEFAULT_WIDTH;
74
- /** Current rendered height of the dialog panel (px). @internal */
75
- @observable dialogHeight: number = DIALOG_DEFAULT_HEIGHT;
76
- /** Whether the dialog panel is currently open. @internal */
77
- @observable dialogOpen: boolean = false;
78
- /** Whether the bubble orb is currently hovered. @internal */
79
- @observable hovered: boolean = false;
80
-
81
- /** @internal */
82
- bubbleEl?: HTMLElement;
83
- /** @internal */
84
- tooltipEl?: HTMLElement;
85
- /** @internal */
86
- dialogEl?: HTMLElement;
87
-
88
- private unsubBus?: () => void;
89
- private injectedCloseBtn?: Element;
90
- private resizeObserver?: ResizeObserver;
91
- private dragging: boolean = false;
92
- private dragTarget: 'bubble' | 'dialog' | null = null;
93
- private startMouseX: number = 0;
94
- private startMouseY: number = 0;
95
- private startElemLeft: number = 0;
96
- private startElemTop: number = 0;
97
- private hasDragged: boolean = false;
98
- private latestMouseX: number = 0;
99
- private latestMouseY: number = 0;
100
- private rafHandle?: number;
101
- private closeAbort?: AbortController;
102
-
103
- connectedCallback() {
104
- super.connectedCallback();
105
- this.bubbleLeft = window.innerWidth - BUBBLE_INITIAL_RIGHT_OFFSET;
106
- this.bubbleTop = window.innerHeight - 100;
107
- this.dialogLeft = Math.max(
108
- VIEWPORT_EDGE_MARGIN,
109
- window.innerWidth - DIALOG_INITIAL_RIGHT_OFFSET,
110
- );
111
- this.dialogTop = Math.max(
112
- VIEWPORT_EDGE_MARGIN,
113
- window.innerHeight - DIALOG_INITIAL_BOTTOM_OFFSET,
114
- );
115
- document.addEventListener('mousemove', this.onDocMouseMove);
116
- document.addEventListener('mouseup', this.onDocMouseUp);
117
- const unsubPopout = agenticActivityBus.subscribe('chat-popout', () => {
118
- this.startCloseDialog();
119
- this.hidden = true;
120
- });
121
- const unsubPopin = agenticActivityBus.subscribe('chat-popin', () => {
122
- this.cancelClose();
123
- this.hidden = false;
124
- this.dialogOpen = true;
125
- });
126
- this.unsubBus = () => {
127
- unsubPopout();
128
- unsubPopin();
129
- };
130
- // Measure tooltip width once after first render. The bubble is visible on connect
131
- // (dialogOpen starts false), so the element is in the DOM by the time rAF fires.
132
- // This value persists through open/close cycles — no re-measurement needed unless
133
- // the title attribute changes.
134
- requestAnimationFrame(() => {
135
- if (this.tooltipEl) this.tooltipMeasuredWidth = this.tooltipEl.offsetWidth;
136
- });
137
- }
138
-
139
- onBubbleMouseEnter() {
140
- this.hovered = true;
141
- // Ensure we have a real measured width even on first-ever hover.
142
- // The ref may not be available yet during connectedCallback's first rAF.
143
- if (!this.tooltipMeasuredWidth) {
144
- requestAnimationFrame(() => {
145
- if (this.tooltipEl) this.tooltipMeasuredWidth = this.tooltipEl.offsetWidth;
146
- });
147
- }
148
- }
149
-
150
- onBubbleMouseLeave() {
151
- this.hovered = false;
152
- }
153
-
154
- disconnectedCallback() {
155
- super.disconnectedCallback();
156
- document.removeEventListener('mousemove', this.onDocMouseMove);
157
- document.removeEventListener('mouseup', this.onDocMouseUp);
158
- this.unsubBus?.();
159
- this.unsubBus = undefined;
160
- this.injectedCloseBtn?.removeEventListener('click', this.onCloseBtnClick);
161
- this.injectedCloseBtn?.remove();
162
- this.injectedCloseBtn = undefined;
163
- this.resizeObserver?.disconnect();
164
- this.resizeObserver = undefined;
165
- if (this.rafHandle !== undefined) {
166
- cancelAnimationFrame(this.rafHandle);
167
- this.rafHandle = undefined;
168
- }
169
- this.closeAbort?.abort();
170
- this.closeAbort = undefined;
171
- }
172
-
173
- dialogOpenChanged(_: boolean, next: boolean) {
174
- if (next) {
175
- // Ensure we never observe a stale element from a prior open.
176
- this.resizeObserver?.disconnect();
177
- this.resizeObserver = undefined;
178
- requestAnimationFrame(() => {
179
- const el = this.dialogEl;
180
- if (!el || !el.isConnected) return;
181
-
182
- // Immediately sync actual size on open. This prevents cases where a stale
183
- // ResizeObserver entry (or a not-yet-laid-out element) would temporarily
184
- // drive the observables to 0 and shrink the dialog.
185
- const w = el.offsetWidth;
186
- const h = el.offsetHeight;
187
- if (w > 0) this.dialogWidth = w;
188
- if (h > 0) this.dialogHeight = h;
189
-
190
- this.resizeObserver = new ResizeObserver((entries) => {
191
- const entry = entries[0];
192
- if (!entry) return;
193
- const borderBox = (entry as ResizeObserverEntry).borderBoxSize?.[0] as
194
- | { inlineSize: number; blockSize: number }
195
- | undefined;
196
- const nextW = Math.round(borderBox?.inlineSize ?? entry.contentRect.width);
197
- const nextH = Math.round(borderBox?.blockSize ?? entry.contentRect.height);
198
- if (nextW > 0) this.dialogWidth = nextW;
199
- if (nextH > 0) this.dialogHeight = nextH;
200
- });
201
- this.resizeObserver.observe(el);
202
- });
203
- } else {
204
- this.resizeObserver?.disconnect();
205
- this.resizeObserver = undefined;
206
- }
207
- }
208
-
209
- dialogContentChanged(_: HTMLElement[], next: HTMLElement[]) {
210
- if (!next?.length || this.injectedCloseBtn) return;
211
- for (const el of next) {
212
- const assistant =
213
- el.tagName.toLowerCase() === 'foundation-ai-assistant'
214
- ? el
215
- : el.shadowRoot?.querySelector('foundation-ai-assistant');
216
- if (assistant) {
217
- this.injectCloseButton(assistant);
218
- break;
219
- }
220
- }
221
- }
222
-
223
- private injectCloseButton(assistant: Element) {
224
- const prefix = this.designSystemPrefix;
225
- const btn = document.createElement(`${prefix}-button`);
226
- btn.setAttribute('slot', 'header-actions-extra');
227
- btn.setAttribute('appearance', 'stealth');
228
- btn.setAttribute('title', 'Close');
229
- const icon = document.createElement(`${prefix}-icon`);
230
- icon.setAttribute('name', 'times');
231
- btn.appendChild(icon);
232
- btn.addEventListener('click', this.onCloseBtnClick);
233
- assistant.appendChild(btn);
234
- this.injectedCloseBtn = btn;
235
- }
236
-
237
- onBubbleMouseDown(e: MouseEvent) {
238
- e.preventDefault();
239
- this.dragging = true;
240
- this.dragTarget = 'bubble';
241
- this.startMouseX = e.clientX;
242
- this.startMouseY = e.clientY;
243
- this.latestMouseX = e.clientX;
244
- this.latestMouseY = e.clientY;
245
- this.startElemLeft = this.bubbleLeft;
246
- this.startElemTop = this.bubbleTop;
247
- this.hasDragged = false;
248
- }
249
-
250
- onDialogHeaderMouseDown(e: CustomEvent<{ clientX: number; clientY: number }>) {
251
- // Sync actual rendered size before dragging — the browser's CSS resize handle
252
- // updates the element's size but our observables may not have caught up yet.
253
- if (this.dialogEl) {
254
- this.dialogWidth = this.dialogEl.offsetWidth;
255
- this.dialogHeight = this.dialogEl.offsetHeight;
256
- }
257
- this.dragging = true;
258
- this.dragTarget = 'dialog';
259
- this.startMouseX = e.detail.clientX;
260
- this.startMouseY = e.detail.clientY;
261
- this.latestMouseX = e.detail.clientX;
262
- this.latestMouseY = e.detail.clientY;
263
- this.startElemLeft = this.dialogLeft;
264
- this.startElemTop = this.dialogTop;
265
- this.hasDragged = false;
266
- }
267
-
268
- private readonly onCloseBtnClick = () => this.onCloseDialog();
269
-
270
- // Cancels an in-flight close animation and restores the dialog to its open state.
271
- // Needed for the chat-popin case: startCloseDialog() may have been called (adding
272
- // .closing and an animationend listener) but the host was hidden before the animation
273
- // finished. When the host reappears via chat-popin, we cancel the pending close so
274
- // the dialog stays open rather than disappearing once animationend eventually fires.
275
- private cancelClose() {
276
- this.closeAbort?.abort();
277
- this.closeAbort = undefined;
278
- this.dialogEl?.classList.remove('closing');
279
- }
280
-
281
- // Plays the closing animation before removing the dialog from the DOM.
282
- // dialogOpen must stay true while the animation runs — setting it to false
283
- // immediately would cause the repeat directive to remove the element before
284
- // the animation has a chance to play. We only set dialogOpen = false inside
285
- // the animationend callback, at which point FAST removes the element cleanly.
286
- // If the user prefers reduced motion we skip the animation and close immediately.
287
- private startCloseDialog() {
288
- const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
289
- if (!this.dialogEl || reducedMotion) {
290
- this.dialogOpen = false;
291
- return;
292
- }
293
- this.closeAbort = new AbortController();
294
- this.dialogEl.classList.add('closing');
295
- this.dialogEl.addEventListener(
296
- 'animationend',
297
- () => {
298
- this.dialogOpen = false;
299
- },
300
- {
301
- once: true,
302
- signal: this.closeAbort.signal,
303
- },
304
- );
305
- }
306
-
307
- // Capture the latest pointer coords and schedule one rAF per display frame.
308
- // Pointer events can fire far faster than the display refreshes (up to 1000Hz on
309
- // gaming mice), so we only store the coords here and let applyDragFrame do the
310
- // actual work. If a frame is already scheduled we just overwrite the coords —
311
- // the pending frame will pick up the freshest values when it runs.
312
- private onDocMouseMove = (e: MouseEvent) => {
313
- if (!this.dragging) return;
314
- this.latestMouseX = e.clientX;
315
- this.latestMouseY = e.clientY;
316
- if (this.rafHandle === undefined) {
317
- this.rafHandle = requestAnimationFrame(this.applyDragFrame);
318
- }
319
- };
320
-
321
- // Runs once per display frame during a drag. Writes the new position directly to
322
- // the element's style.translate rather than updating FAST observables, which keeps
323
- // the compositor thread doing the work instead of triggering FAST's reactivity
324
- // pipeline on every frame. This means the element's visual position is temporarily
325
- // ahead of the observables — they are only synced back on mouseup (see onDocMouseUp).
326
- // Note: style.translate is an independent CSS property and does not conflict with
327
- // the transform used for the bubble's hover scale or the dialog's open animation.
328
- private applyDragFrame = () => {
329
- this.rafHandle = undefined;
330
- const dx = this.latestMouseX - this.startMouseX;
331
- const dy = this.latestMouseY - this.startMouseY;
332
- if (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD) this.hasDragged = true;
333
- if (this.dragTarget === 'bubble') {
334
- const left = Math.max(
335
- 0,
336
- Math.min(window.innerWidth - BUBBLE_DRAG_MARGIN, this.startElemLeft + dx),
337
- );
338
- const top = Math.max(
339
- 0,
340
- Math.min(window.innerHeight - BUBBLE_DRAG_MARGIN, this.startElemTop + dy),
341
- );
342
- if (this.bubbleEl) this.bubbleEl.style.translate = `${left}px ${top}px`;
343
- } else if (this.dragTarget === 'dialog') {
344
- const left = Math.max(
345
- 0,
346
- Math.min(window.innerWidth - DIALOG_DEFAULT_WIDTH, this.startElemLeft + dx),
347
- );
348
- const top = Math.max(0, Math.min(window.innerHeight - 100, this.startElemTop + dy));
349
- if (this.dialogEl) this.dialogEl.style.translate = `${left}px ${top}px`;
350
- }
351
- };
352
-
353
- // Commits the final drag position back to the FAST observables, bringing them back
354
- // in sync with what the user sees. This triggers a single FAST re-render which writes
355
- // the same value already on the element — no visible jump. Any pending rAF is
356
- // cancelled first; its work is redundant since we recompute from latestMouseX/Y here.
357
- private onDocMouseUp = () => {
358
- if (this.rafHandle !== undefined) {
359
- cancelAnimationFrame(this.rafHandle);
360
- this.rafHandle = undefined;
361
- }
362
- if (this.dragging) {
363
- const dx = this.latestMouseX - this.startMouseX;
364
- const dy = this.latestMouseY - this.startMouseY;
365
- if (this.dragTarget === 'bubble') {
366
- this.bubbleLeft = Math.max(
367
- 0,
368
- Math.min(window.innerWidth - BUBBLE_DRAG_MARGIN, this.startElemLeft + dx),
369
- );
370
- this.bubbleTop = Math.max(
371
- 0,
372
- Math.min(window.innerHeight - BUBBLE_DRAG_MARGIN, this.startElemTop + dy),
373
- );
374
- if (!this.hasDragged) {
375
- if (this.dialogOpen) {
376
- this.startCloseDialog();
377
- } else {
378
- this.dialogLeft = Math.max(
379
- VIEWPORT_EDGE_MARGIN,
380
- this.bubbleLeft - DIALOG_BUBBLE_HORIZONTAL_OFFSET,
381
- );
382
- this.dialogTop = Math.max(
383
- VIEWPORT_EDGE_MARGIN,
384
- this.bubbleTop - DIALOG_OPEN_TOP_OFFSET,
385
- );
386
- this.dialogOpen = true;
387
- }
388
- }
389
- } else if (this.dragTarget === 'dialog') {
390
- this.dialogLeft = Math.max(
391
- 0,
392
- Math.min(window.innerWidth - DIALOG_DEFAULT_WIDTH, this.startElemLeft + dx),
393
- );
394
- this.dialogTop = Math.max(0, Math.min(window.innerHeight - 100, this.startElemTop + dy));
395
- }
396
- }
397
- this.dragging = false;
398
- this.dragTarget = null;
399
- };
400
-
401
- onPopout() {
402
- this.startCloseDialog();
403
- }
404
-
405
- onCloseDialog() {
406
- // Snap the bubble orb to the bottom-right of wherever the dialog currently is,
407
- // so it reappears near where the user last positioned the window.
408
- this.bubbleLeft = Math.max(
409
- 0,
410
- Math.min(
411
- window.innerWidth - BUBBLE_DRAG_MARGIN,
412
- this.dialogLeft + DIALOG_BUBBLE_HORIZONTAL_OFFSET,
413
- ),
414
- );
415
- this.bubbleTop = Math.max(
416
- 0,
417
- Math.min(window.innerHeight - BUBBLE_DRAG_MARGIN, this.dialogTop + BUBBLE_SNAP_BOTTOM_OFFSET),
418
- );
419
- this.startCloseDialog();
420
- }
421
-
422
- get tooltipStyle(): string {
423
- const tooltipWidth = this.tooltipMeasuredWidth || TOOLTIP_FALLBACK_WIDTH;
424
- const bubbleWidth = 62; // Fixed in CSS
425
- const margin = 8;
426
- const naturalLeft = (bubbleWidth - tooltipWidth) / 2;
427
- const vpLeft = this.bubbleLeft + naturalLeft;
428
- const vpRight = vpLeft + tooltipWidth;
429
- let shift = 0;
430
- if (vpLeft < margin) shift = margin - vpLeft;
431
- else if (vpRight > window.innerWidth - margin) shift = window.innerWidth - margin - vpRight;
432
- const tooltipLeft = naturalLeft + shift;
433
- const arrowLeft = bubbleWidth / 2 - tooltipLeft;
434
- return `left: ${tooltipLeft}px; --arrow-left: ${arrowLeft}px`;
435
- }
436
- }
@@ -1 +0,0 @@
1
- export * from './chat-bubble';
@@ -1,23 +0,0 @@
1
- /**
2
- * Test-only side effect: align the global `EventTarget` with jsdom's before any
3
- * module that `extends EventTarget` is evaluated.
4
- *
5
- * The node test runner's jsdom setup installs `globalThis.CustomEvent` from
6
- * jsdom but leaves `globalThis.EventTarget` as Node's native class. A class that
7
- * `extends EventTarget` (e.g. {@link ChatDriver}) then inherits Node's native
8
- * `dispatchEvent`, which rejects the jsdom `CustomEvent` instances it is handed
9
- * ("The 'event' argument must be an instance of Event. Received an instance of
10
- * CustomEvent"). Pointing `EventTarget` at jsdom's keeps the whole event family
11
- * in one realm.
12
- *
13
- * No-op in a real browser, where `window.EventTarget === globalThis.EventTarget`
14
- * already. Import this BEFORE importing anything that subclasses `EventTarget`.
15
- */
16
- const jsdomWindow = (globalThis as { window?: { EventTarget?: typeof EventTarget } }).window;
17
- if (jsdomWindow?.EventTarget && globalThis.EventTarget !== jsdomWindow.EventTarget) {
18
- Object.defineProperty(globalThis, 'EventTarget', {
19
- value: jsdomWindow.EventTarget,
20
- configurable: true,
21
- writable: true,
22
- });
23
- }
@@ -1,131 +0,0 @@
1
- import type { AIProviderRegistry, ChatMessage } from '@genesislcap/foundation-ai';
2
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
3
- // Side-effect import — MUST come before `./chat-driver` so the driver subclasses
4
- // jsdom's EventTarget rather than Node's native one (CustomEvent dispatch then
5
- // works). Mirrors `chat-driver.test.ts`.
6
- import './align-event-globals';
7
- import { ChatDriver } from './chat-driver';
8
-
9
- // GENC-1351 §5.7 — driver-level compaction: the atomic, destructive replace.
10
- // A minimal fake registry stands in for a provider, so no network is involved.
11
-
12
- const asMsg = (o: Record<string, unknown>): ChatMessage => o as unknown as ChatMessage;
13
-
14
- /** A registry whose default provider summarizes via `prompt` (undefined = unsupported). */
15
- function registryWith(prompt: ((m: string) => Promise<string>) | undefined): AIProviderRegistry {
16
- const provider = { prompt, getStatus: async () => ({ model: 'test-model' }) };
17
- return { default: () => provider } as unknown as AIProviderRegistry;
18
- }
19
-
20
- /** 10 messages alternating user/assistant (user at even indices); last carries usage. */
21
- function tenTurnHistory(): ChatMessage[] {
22
- return Array.from({ length: 10 }, (_, i) =>
23
- asMsg({
24
- role: i % 2 === 0 ? 'user' : 'assistant',
25
- content: `m${i}`,
26
- timestamp: `2026-01-01T00:00:${String(i).padStart(2, '0')}.000Z`,
27
- ...(i === 9 ? { inputTokens: 1234 } : {}),
28
- }),
29
- );
30
- }
31
-
32
- const Suite = createLogicSuite('chat-driver-compact');
33
-
34
- Suite('compact() destructively replaces older turns with one head summary + metadata', async () => {
35
- const driver = new ChatDriver(registryWith(async () => 'THE SUMMARY'));
36
- driver.loadHistory(tenTurnHistory());
37
- const before = driver.getHistory().length;
38
-
39
- const summary = await driver.compact();
40
- assert.ok(summary, 'returns the created summary message');
41
- assert.is(summary!.role, 'compacted-summary');
42
- assert.is(summary!.content, 'THE SUMMARY');
43
- assert.is(summary!.compaction?.tokensBefore, 1234, 'captures pre-compaction input tokens');
44
- assert.is(summary!.compaction?.model, 'test-model', 'records the summarizing model');
45
-
46
- const after = driver.getHistory();
47
- assert.ok(after.length < before, 'history shrank (destructive)');
48
- assert.is(after[0].role, 'compacted-summary', 'summary sits at the head');
49
- assert.is(
50
- after[0].timestamp,
51
- '2026-01-01T00:00:00.000Z',
52
- 'summary inherits the covered region start → sorts to the head in time-ordered views',
53
- );
54
- assert.is(after[0].compaction?.compactedCount, before - (after.length - 1));
55
- assert.ok(
56
- after.slice(1).every((m) => m.role !== 'compacted-summary'),
57
- 'exactly one summary — no marker chain',
58
- );
59
- });
60
-
61
- Suite('compact() is atomic on failure — no splice, surfaces a "sorry" message', async () => {
62
- const driver = new ChatDriver(
63
- registryWith(async () => {
64
- throw new Error('network');
65
- }),
66
- );
67
- const original = tenTurnHistory();
68
- driver.loadHistory(original);
69
-
70
- const result = await driver.compact();
71
- assert.is(result, null, 'returns null on failure');
72
- const after = driver.getHistory();
73
- assert.ok(
74
- after.every((m) => m.role !== 'compacted-summary'),
75
- 'nothing compacted',
76
- );
77
- // The covered turns are untouched (atomic); only a brief apology is appended so
78
- // the user knows it failed (GENC-1351 follow-up — surfaced like a turn error).
79
- assert.is(after.length, original.length + 1, 'only an apology appended');
80
- assert.is(after[after.length - 1].role, 'assistant');
81
- assert.ok(/sorry/i.test(after[after.length - 1].content), 'apology is an agent message');
82
- });
83
-
84
- Suite('compact() rolls a prior summary into a single fresh one', async () => {
85
- const driver = new ChatDriver(registryWith(async () => 'COMBINED'));
86
- driver.loadHistory([
87
- asMsg({ role: 'compacted-summary', content: 'old', compaction: { compactedCount: 5 } }),
88
- ...tenTurnHistory(),
89
- ]);
90
- const summary = await driver.compact();
91
- assert.ok(summary);
92
- const after = driver.getHistory();
93
- assert.is(
94
- after.filter((m) => m.role === 'compacted-summary').length,
95
- 1,
96
- 'still exactly one summary after re-compaction',
97
- );
98
- assert.is(after[0].content, 'COMBINED');
99
- });
100
-
101
- Suite('compact() no-ops when there is nothing worth compacting', async () => {
102
- const driver = new ChatDriver(registryWith(async () => 'unused'));
103
- driver.loadHistory([
104
- asMsg({ role: 'user', content: 'hi' }),
105
- asMsg({ role: 'assistant', content: 'yo' }),
106
- ]);
107
- assert.is(await driver.compact(), null);
108
- assert.is(driver.getHistory().length, 2, 'short history untouched');
109
- });
110
-
111
- Suite('compact() no-ops when the default provider cannot summarize', async () => {
112
- const driver = new ChatDriver(registryWith(undefined));
113
- driver.loadHistory(tenTurnHistory());
114
- assert.is(await driver.compact(), null);
115
- assert.is(driver.getHistory().length, 10, 'history untouched without prompt support');
116
- });
117
-
118
- Suite("canCompact reflects compact()-would-act, keyed on the driver's own history", () => {
119
- const rich = new ChatDriver(registryWith(async () => 'x'));
120
- rich.loadHistory(tenTurnHistory());
121
- assert.is(rich.canCompact(), true, 'enough history → gate open');
122
-
123
- const thin = new ChatDriver(registryWith(async () => 'x'));
124
- thin.loadHistory([
125
- asMsg({ role: 'user', content: 'hi' }),
126
- asMsg({ role: 'assistant', content: 'yo' }),
127
- ]);
128
- assert.is(thin.canCompact(), false, 'too short → gate closed (same source compact() uses)');
129
- });
130
-
131
- Suite.run();