@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,1119 +0,0 @@
1
- /**
2
- * Whitespace in FAST templates
3
- * ────────────────────────────
4
- * The formatter adds newlines/indentation to static template string parts,
5
- * which FAST renders as real text nodes. With `white-space: pre-wrap` these
6
- * are visible as blank lines. To avoid this:
7
- *
8
- * - Apply `white-space: pre-wrap` only on leaf elements whose *content* needs
9
- * it, never on containers.
10
- * - Inside pre-wrap elements keep content on the same line as the opening tag:
11
- * GOOD `<div>${expr}</div>` / BAD `<div>\n ${expr}\n</div>`
12
- * - Extract shared or deeply-nested templates as named module-level consts to
13
- * keep indentation shallow.
14
- * - Chain adjacent `when()` calls as `${when(a,tA)}${when(b,tB)}` — separate
15
- * lines let the formatter inject whitespace between them.
16
- */
17
-
18
- import type {
19
- ChatAttachment,
20
- ChatMessage,
21
- ChatToolCall,
22
- InteractionPresentation,
23
- } from '@genesislcap/foundation-ai';
24
- import { isChatToolCallUnknown } from '@genesislcap/foundation-ai';
25
- import { classNames, html, ref, repeat, when, ViewTemplate } from '@genesislcap/web-core';
26
- import { SettingsModalTemplate } from '../components/settings-modal';
27
- import { compactionDividerLabel } from '../utils/history-transform';
28
- import type { FoundationAiAssistant } from './main';
29
- import { LOADING_STYLE_ANIMATIONS } from './main.types';
30
-
31
- function unknownToolPayload(tc: ChatToolCall): string {
32
- if (!isChatToolCallUnknown(tc)) return '';
33
- // A stale tool was real earlier in this agent's lifetime but isn't available
34
- // in the current state — distinct from a hallucinated name that never existed.
35
- const lines = [
36
- tc.stale ? `${tc.name} — no longer available at this step` : `${tc.name} — tool does not exist`,
37
- ];
38
- if (tc.availableTools.length > 0) {
39
- lines.push('Available tools:');
40
- tc.availableTools.forEach((t) => lines.push(` • ${t}`));
41
- }
42
- return lines.join('\n');
43
- }
44
-
45
- const HALO_SPEED_DEFAULT = 1.5;
46
- const HALO_SPEED_ORCHESTRATING = 0.4;
47
- const HALO_BORDER_SIZE_DEFAULT = 3;
48
-
49
- // Avatar markup is owned by the component (`assistantIconSafe` / `userIconSafe`),
50
- // which holds the sanitized SVG string for the default or any consumer override
51
- // and is injected via `:innerHTML`. A raster `image-src` / `user-image-src`
52
- // takes precedence over the inline icon. The exported `*IconSvg` string
53
- // constants in `./main` let apps opt back into a specific built-in mark.
54
-
55
- const assistantAvatarTemplate = html<ChatMessage, FoundationAiAssistant>`
56
- ${(m, c) => {
57
- const parent = c.parent as FoundationAiAssistant;
58
- return parent.imageSrc
59
- ? html<ChatMessage, FoundationAiAssistant>`
60
- <img src="${() => parent.imageSrc}" alt="Assistant" class="avatar-img" />
61
- `
62
- : html<ChatMessage, FoundationAiAssistant>`
63
- <span class="avatar-icon" :innerHTML="${() => parent.assistantIconSafe}"></span>
64
- `;
65
- }}
66
- `;
67
-
68
- const userAvatarTemplate = html<ChatMessage, FoundationAiAssistant>`
69
- ${(m, c) => {
70
- const parent = c.parent as FoundationAiAssistant;
71
- return parent.userImageSrc
72
- ? html<ChatMessage, FoundationAiAssistant>`
73
- <img src="${() => parent.userImageSrc}" alt="You" class="avatar-img" />
74
- `
75
- : html<ChatMessage, FoundationAiAssistant>`
76
- <span class="avatar-icon" :innerHTML="${() => parent.userIconSafe}"></span>
77
- `;
78
- }}
79
- `;
80
-
81
- /**
82
- * @beta
83
- */
84
- const messageType = (m: ChatMessage): string => {
85
- switch (true) {
86
- case m.role === 'system-event':
87
- return 'agent-switch';
88
- case m.role === 'user':
89
- // A synthetic-user message is a display-only echo of an interaction
90
- // outcome; render it identically to a real user message.
91
- case m.role === 'synthetic-user':
92
- return 'user';
93
- // Reasoning (chain-of-thought summary) and narration (interstitial prose the model emits
94
- // alongside a tool call) both render as the "Thinking" bubble — visually set apart from a real
95
- // final agent reply, even though they stay separate categories (own log entry + own toggle:
96
- // `showThinkingSteps` vs `showNarration`). `thinking` is the legacy flag for pre-split sessions.
97
- case m.category === 'reasoning':
98
- case m.category === 'narration':
99
- case !!m.thinking:
100
- return 'ai-thinking';
101
- case !!m.toolCalls:
102
- return 'ai-function';
103
- case !!m.interaction:
104
- return 'interaction';
105
- default:
106
- return 'ai';
107
- }
108
- };
109
-
110
- const senderLabel: Record<string, string> = {
111
- user: 'You',
112
- 'ai-thinking': 'Thinking',
113
- 'ai-function': 'Tool Call',
114
- interaction: 'Assistant',
115
- ai: 'Assistant',
116
- };
117
-
118
- /**
119
- * Sender label for a message. Reasoning and narration share the "Thinking" bubble, so the label is
120
- * the only thing that tells them apart: narration reads "Working" (the action it narrates alongside
121
- * its tool call), reasoning stays "Thinking". Everything else uses the {@link messageType} map.
122
- */
123
- const senderLabelFor = (m: ChatMessage): string =>
124
- m.category === 'narration' ? 'Working' : senderLabel[messageType(m)];
125
-
126
- /**
127
- * Resolves how the host frames an interaction widget, defaulting to `'label'`
128
- * (the historical "Assistant" label, no bubble). Callers guard on
129
- * `m.interaction` first; non-interaction messages never consult this.
130
- */
131
- const interactionPresentation = (m: ChatMessage): InteractionPresentation =>
132
- m.interaction?.presentation ?? 'label';
133
-
134
- /**
135
- * Space-separated `part` names for a message bubble, so a host can style bubbles
136
- * from its own stylesheet via `::part()`. Every bubble gets `message` plus its
137
- * type (`message-user`, `message-ai`, `message-ai-thinking`, `message-ai-function`,
138
- * `message-interaction`). Interaction bubbles additionally carry a
139
- * presentation-specific part (`message-interaction-label` / `-bare` / `-bubble`)
140
- * so a host can target, say, only the bubble-framed widgets.
141
- */
142
- const messageParts = (m: ChatMessage): string => {
143
- const type = messageType(m);
144
- return type === 'interaction'
145
- ? `message message-interaction message-interaction-${interactionPresentation(m)}`
146
- : `message message-${type}`;
147
- };
148
-
149
- // ─── Sub-agent trace fragments ────────────────────────────────────────────────
150
-
151
- const subAgentAssistantTemplate = html<ChatMessage>`
152
- <div class="sub-agent-message sub-agent-assistant">${(m) => m.content}</div>
153
- `;
154
-
155
- const subAgentToolCallTemplate = html<ChatMessage>`
156
- <div class="sub-agent-message sub-agent-tool-call">
157
- ${repeat(
158
- (m) => m.toolCalls!,
159
- html<ChatToolCall>`
160
- <span class="sub-agent-tool-name">${(tc) => tc.name}</span>
161
- `,
162
- )}
163
- </div>
164
- `;
165
-
166
- const subAgentToolResultTemplate = html<ChatMessage>`
167
- <div class="sub-agent-message sub-agent-tool-result">${(m) => m.toolResult!.content}</div>
168
- `;
169
-
170
- /** Renders one row of a sub-agent trace (assistant text, tool call, or tool result). */
171
- const subAgentMessageRowTemplate = html<ChatMessage>`
172
- ${when(
173
- (m) => m.role === 'assistant' && !m.toolCalls?.length && !!m.content,
174
- subAgentAssistantTemplate,
175
- )}${when((m) => !!m.toolCalls?.length, subAgentToolCallTemplate)}${when(
176
- (m) => m.role === 'tool' && !!m.toolResult?.content,
177
- subAgentToolResultTemplate,
178
- )}
179
- `;
180
-
181
- /**
182
- * Label for a trace block: the distinct agents that appear in it.
183
- *
184
- * One tool call can invoke several sub-agents — a scheduler dispatching a dependency
185
- * graph, or a retry — and their conversations arrive concatenated in one trace. Naming
186
- * only `[0]` would then label a block "gen_A trace" while `gen_B`'s turns sit inside
187
- * it. Two agents read as "gen_A, gen_B"; beyond that it degrades to a count rather
188
- * than growing an unbounded summary line.
189
- */
190
- const subAgentTraceLabel = (trace: readonly ChatMessage[]): string => {
191
- const names = [...new Set(trace.map((m) => m.agentName).filter(Boolean))] as string[];
192
- if (names.length === 0) return 'Sub-agent trace';
193
- if (names.length <= 2) return `${names.join(', ')} trace`;
194
- // Owns the whole string rather than returning a fragment the template suffixes, so the noun
195
- // agrees with the count — a shared ` trace` suffix rendered "3 sub-agents trace".
196
- return `${names.length} sub-agent traces`;
197
- };
198
-
199
- /** Collapsed <details> trace shown inside a tool-call card once the sub-agent finishes. */
200
- const subAgentTraceTemplate = html<ChatToolCall>`
201
- <details class="sub-agent-trace">
202
- <summary class="sub-agent-trace-summary">
203
- ${(tc) => subAgentTraceLabel(tc.subAgentTrace!)}
204
- </summary>
205
- ${repeat(
206
- (tc) => tc.subAgentTrace!.filter((m) => m.role !== 'user'),
207
- subAgentMessageRowTemplate,
208
- )}
209
- </details>
210
- `;
211
-
212
- /** Live streaming trace panel shown while a sub-agent is actively running. */
213
- const liveSubAgentTraceTemplate = html<FoundationAiAssistant>`
214
- <div class="live-sub-agent-trace" part="live-sub-agent-trace">
215
- <span class="live-sub-agent-name">${(x) => x.liveSubAgentName ?? 'Sub-agent'}</span>
216
- ${repeat(
217
- (x) => x.liveSubAgentTrace.filter((m) => m.role !== 'user'),
218
- subAgentMessageRowTemplate,
219
- )}
220
- </div>
221
- `;
222
-
223
- // The interchangeable loading indicators. These MUST be stable module-level
224
- // instances: the binding that selects between them reads `enabledAnimations`,
225
- // which is backed by the redux store proxy and re-evaluates on every change to
226
- // the aiAssistant slice (i.e. every new message, including hidden tool-call and
227
- // thinking-step messages). Returning a fresh `html` instance from that binding
228
- // would make FAST tear down and rebuild the indicator DOM each time, restarting
229
- // the CSS animations and rAF loops. Stable references let FAST reuse the
230
- // existing view so the animation runs uninterrupted.
231
- const thinkingDotsTemplate = html<FoundationAiAssistant>`
232
- <div class="thinking-dots">
233
- <div class="dot dot-1"></div>
234
- <div class="dot dot-2"></div>
235
- <div class="dot dot-3"></div>
236
- <div class="dot dot-4"></div>
237
- </div>
238
- `;
239
-
240
- const thinkingWavesTemplate = html<FoundationAiAssistant>`
241
- <div class="thinking-waves" part="thinking-waves">
242
- <ai-waves-indicator></ai-waves-indicator>
243
- <span class="thinking-caption">Thinking...</span>
244
- </div>
245
- `;
246
-
247
- const thinkingFlowingWavesTemplate = html<FoundationAiAssistant>`
248
- <div class="thinking-flowing-waves" part="thinking-flowing-waves">
249
- <ai-flowing-waves-indicator></ai-flowing-waves-indicator>
250
- <span class="thinking-caption">Thinking...</span>
251
- </div>
252
- `;
253
-
254
- const thinkingPlasmaTemplate = html<FoundationAiAssistant>`
255
- <div class="thinking-plasma" part="thinking-plasma">
256
- <ai-plasma-orb-indicator></ai-plasma-orb-indicator>
257
- <span class="thinking-caption">Thinking...</span>
258
- </div>
259
- `;
260
-
261
- /**
262
- * Picks the loading indicator for the currently enabled style, falling back to
263
- * the dots (also the default when no animations config is supplied). Returns a
264
- * stable template instance per style — see the note above.
265
- */
266
- const selectThinkingTemplate = (x: FoundationAiAssistant): ViewTemplate<FoundationAiAssistant> => {
267
- const enabled = x.enabledAnimations;
268
- if (enabled.includes('waves')) return thinkingWavesTemplate;
269
- if (enabled.includes('flowingWaves')) return thinkingFlowingWavesTemplate;
270
- if (enabled.includes('plasma')) return thinkingPlasmaTemplate;
271
- return thinkingDotsTemplate;
272
- };
273
-
274
- /** Shown in place of the transcript while a manual compaction runs (GENC-1351 §5.7). */
275
- const compactingTemplate = html<FoundationAiAssistant>`
276
- <div class="compacting-indicator" part="compacting-indicator">
277
- ${thinkingDotsTemplate}
278
- <span class="compacting-caption">Compacting…</span>
279
- </div>
280
- `;
281
-
282
- /**
283
- * Shown in place of the transcript while a page-reload restore is in flight
284
- * (GENC-1351 §6) — a slow, server-backed `provider.load()`. Reuses the compaction
285
- * indicator's visual (same centered dotted loader) with its own `part` for host
286
- * styling. Invisible for synchronous providers, which resolve before paint.
287
- */
288
- const restoringTemplate = html<FoundationAiAssistant>`
289
- <div class="compacting-indicator" part="restoring-indicator">
290
- ${thinkingDotsTemplate}
291
- <span class="compacting-caption">Restoring…</span>
292
- </div>
293
- `;
294
-
295
- /**
296
- * Class list for the banner below, joined rather than interpolated so an
297
- * inapplicable modifier contributes nothing. Two interpolations directly in the
298
- * attribute emitted `class="blocked-banner "` in the common (unblocked) case —
299
- * harmless to the browser, but it shows up in every DOM snapshot and every
300
- * innerHTML assertion a host writes against this element.
301
- *
302
- * The two modifiers name what they actually gate, which is why neither is
303
- * `is-blocked`: `is-visible` means the banner has something to say, and that
304
- * includes PARTIAL exhaustion — one vendor walled, composer still live, `blocked`
305
- * false. `is-partial` then softens the treatment for exactly that case. Naming
306
- * the first after `blocked` read as a contradiction beside the second, and made
307
- * the styles say `.blocked-banner.is-blocked` to mean "visible".
308
- *
309
- * Exported for the unit test that pins the attribute; not part of the element
310
- * API.
311
- *
312
- * @internal
313
- */
314
- export const blockedBannerClasses = (x: FoundationAiAssistant): string =>
315
- [
316
- 'blocked-banner',
317
- x.bannerVisible ? 'is-visible' : '',
318
- x.bannerVisible && !x.blocked ? 'is-partial' : '',
319
- ]
320
- .filter(Boolean)
321
- .join(' ');
322
-
323
- /**
324
- * Id of the blocked banner, referenced by the composer controls'
325
- * `aria-describedby`. Shadow-DOM-scoped, so a fixed string cannot collide with
326
- * the host page — and IDREF resolution is same-root, which is exactly where both
327
- * ends of this reference live.
328
- *
329
- * @internal
330
- */
331
- export const BLOCKED_BANNER_ID = 'blocked-banner';
332
-
333
- /**
334
- * `aria-describedby` for the composer's textarea, send button and attach button:
335
- * the banner's id whenever the banner has something to say, otherwise `null` (so
336
- * the attribute is omitted rather than emitted empty).
337
- *
338
- * Keyed on `bannerVisible`, NOT on `blocked`, and that is the point. The
339
- * PARTIAL state — some vendor walled, composer still live — is the state this
340
- * feature exists to create, and it was the one state with no accessible
341
- * explanation at all: `aria-disabled` and `aria-label` bind only on `blocked`, and
342
- * a live composer keeps the host's own placeholder, so a screen-reader user
343
- * arriving at the textarea heard "Type a message" with no hint that the next turn
344
- * might be refused. The banner's `role="status"` announces the text when it
345
- * CHANGES; this is what makes the same explanation reachable afterwards, on
346
- * demand, from the control it is about.
347
- *
348
- * Applied in the fully blocked state too, where it is additive: the `aria-label`
349
- * there states the reason as the control's name, and this restates it as its
350
- * description for the send/attach buttons, which carry neither.
351
- *
352
- * Exported for the unit test that pins it; not part of the element API.
353
- *
354
- * @internal
355
- */
356
- export const composerDescribedBy = (x: FoundationAiAssistant): string | null =>
357
- x.bannerVisible ? BLOCKED_BANNER_ID : null;
358
-
359
- /**
360
- * Persistent banner shown above the composer while a backend condition blocks
361
- * the assistant — today an exhausted AI budget (GENC-1464).
362
- *
363
- * Deliberately NOT a transcript replacement like `compactingTemplate` /
364
- * `restoringTemplate`: those cover a transient rewrite where the history is
365
- * mid-flight and unsafe to show, whereas a block is indefinite. Hiding the
366
- * conversation behind it would strand the user's work behind a wall they
367
- * cannot clear, so the banner sits alongside a fully readable transcript.
368
- *
369
- * `role="status"` (not `alert`) — it is a standing condition the user can also
370
- * see in the disabled composer, not an interruption.
371
- *
372
- * **Rendered unconditionally, empty when unblocked.** This is an accessibility
373
- * requirement, not a stylistic choice: `role="status"` announces MUTATIONS to a
374
- * region the assistive technology was already observing. A region INSERTED into
375
- * the DOM already carrying its text is widely missed across NVDA, JAWS and
376
- * VoiceOver — and it is the only announcement a screen-reader user gets, since
377
- * the textarea simultaneously flips to `disabled`, which drops it out of the tab
378
- * order and means the swapped "AI usage limit reached" placeholder is never
379
- * voiced (`maybeAutoFocusChatInput` also no-ops against a disabled control). So
380
- * the region stays mounted and only its TEXT changes.
381
- *
382
- * It is deliberately **not** collapsed with `display: none` when unblocked —
383
- * that would drop it out of the accessibility tree and make the flip an
384
- * insertion again, defeating the point. Instead the visual treatment (padding,
385
- * border, background) hangs off the `is-visible` class, so the empty container
386
- * occupies no space while remaining a live, registered region.
387
- *
388
- * It carries {@link BLOCKED_BANNER_ID} so the composer controls can point
389
- * `aria-describedby` at it — see {@link composerDescribedBy}.
390
- */
391
- const blockedBannerTemplate = html<FoundationAiAssistant>`
392
- <div
393
- id="${BLOCKED_BANNER_ID}"
394
- class="${blockedBannerClasses}"
395
- part="blocked-banner"
396
- role="status"
397
- >
398
- ${when(
399
- (x) => x.bannerVisible,
400
- html<FoundationAiAssistant>`
401
- <span class="blocked-banner-text">${(x) => x.effectiveBlockedReason}</span>
402
- `,
403
- )}
404
- </div>
405
- `;
406
-
407
- // ─── Public factory ───────────────────────────────────────────────────────────
408
-
409
- /** @internal */
410
-
411
- /**
412
- * GENC-1511 — blocking "not saving" modal.
413
- *
414
- * Deliberately has NO way out but Reload. The failure it reports is invisible by nature — a
415
- * prod session wrote nothing for 3h17m with a normal-looking UI throughout — and anything the
416
- * user can dismiss is an invitation to carry on producing work that will be lost. So:
417
- * `show-close-icon` off, our own `bottom` slot replacing the default close button, and the
418
- * native dialog's Escape route cancelled on `mainElement` in `persistenceBrokenChanged`.
419
- *
420
- * It is only ever shown at a point where reloading is SAFE — the persister gates on the active
421
- * agent being resumable — so the advice cannot cost the user in-flight work it is unable to carry.
422
- *
423
- * ⚠ `showCloseIcon` is set as a PROPERTY (`:showCloseIcon`), not via the `?show-close-icon`
424
- * boolean attribute. It is an `@attr({ mode: 'boolean' })` whose FIELD default is `true`, so the
425
- * property reflects the attribute ON at construction and the boolean binding's "absent means
426
- * false" never wins. Measured live 2026-08-20: the close icon rendered anyway
427
- * (`showCloseIcon=true`, `hasAttribute=true`) until the property was set directly. Do not
428
- * "simplify" it back to `?show-close-icon` — this modal must not be dismissible.
429
- */
430
- const persistenceBrokenModalTemplate = (
431
- designSystemPrefix: string,
432
- ): ViewTemplate<FoundationAiAssistant> => {
433
- const modalTag = `${designSystemPrefix}-modal`;
434
- const buttonTag = `${designSystemPrefix}-button`;
435
- const iconTag = `${designSystemPrefix}-icon`;
436
- return html<FoundationAiAssistant>`
437
- <${modalTag}
438
- class="persistence-broken-modal"
439
- part="persistence-broken-modal"
440
- ${ref('persistenceModal')}
441
- initial-width="480"
442
- :showCloseIcon=${() => false}
443
- >
444
- <div slot="top" class="persistence-broken-header" part="persistence-broken-header">
445
- <h2 class="persistence-broken-title">
446
- <${iconTag} name="triangle-exclamation"></${iconTag}> Your work isn't being saved
447
- </h2>
448
- </div>
449
- <div class="persistence-broken-content" part="persistence-broken-content">
450
- <p>
451
- ${(x) =>
452
- x.persistenceBrokenMessage ??
453
- "We've encountered a problem and this session has stopped saving to the server. " +
454
- 'Anything you do from here will be lost, so please reload before carrying on.'}
455
- </p>
456
- <!--
457
- Quantified where the reporter can manage it, because "some work" is not actionable.
458
- Composed by the CALLER, not here: the assistant's persister counts unsaved messages, a
459
- host counting something else has its own sentence, and a template that knew about
460
- "messages" could only ever serve the first. Omitted entirely when nobody supplied one.
461
- -->
462
- ${when(
463
- (x) => !!x.persistenceBrokenDetail,
464
- html<FoundationAiAssistant>`
465
- <p class="persistence-broken-detail">${(x) => x.persistenceBrokenDetail}</p>
466
- `,
467
- )}
468
- <p class="persistence-broken-detail">
469
- Reloading restores the last saved point.${when(
470
- (x) => x.chatConfig.ui?.allowDebugDownload === true,
471
- html<FoundationAiAssistant>`
472
- You can download a record of this conversation first.
473
- `,
474
- )}
475
- </p>
476
- </div>
477
- <div slot="bottom" class="persistence-broken-actions" part="persistence-broken-actions">
478
- <!--
479
- The download lives HERE, not in Settings. The copy used to point at the Settings panel,
480
- which this modal makes unreachable by design — offering a record the user cannot get to.
481
- Gated on the host's own allowDebugDownload flag, same as the Settings entry, so a host
482
- that turned it off does not get it smuggled back in through the failure path; the
483
- sentence above is gated on the same flag so the copy never promises a missing button.
484
- -->
485
- ${when(
486
- (x) => x.chatConfig.ui?.allowDebugDownload === true,
487
- html<FoundationAiAssistant>`
488
- <${buttonTag}
489
- appearance="outline"
490
- part="persistence-broken-download"
491
- ?disabled=${(x) => x.downloadingDebugLog}
492
- @click=${(x) => x.downloadDebugLog()}
493
- >
494
- ${(x) => (x.downloadingDebugLog ? 'Preparing…' : 'Download agent log')}
495
- </${buttonTag}>
496
- `,
497
- )}
498
- <${buttonTag}
499
- appearance="accent"
500
- part="persistence-broken-reload"
501
- @click=${(x) => x.reloadForPersistence()}
502
- >
503
- Reload the page
504
- </${buttonTag}>
505
- </div>
506
- </${modalTag}>
507
- `;
508
- };
509
-
510
- export const FoundationAiAssistantTemplate = (
511
- designSystemPrefix: string,
512
- ): ViewTemplate<FoundationAiAssistant> => {
513
- const buttonTag = `${designSystemPrefix}-button`;
514
- const textareaTag = `${designSystemPrefix}-text-area`;
515
- const iconTag = `${designSystemPrefix}-icon`;
516
-
517
- // ── Tool call item ──────────────────────────────────────────────────────────
518
-
519
- const toolCallItemTemplate = html<ChatToolCall>`
520
- ${when(
521
- (tc) => tc.foldEvent === 'open',
522
- html<ChatToolCall>`
523
- <pre class="payload fold-event fold-open">&#9658; ${(tc) => tc.name}</pre>
524
- `,
525
- )}
526
- ${when(
527
- (tc) => tc.foldEvent === 'close',
528
- html<ChatToolCall>`
529
- <pre class="payload fold-event fold-close">&#9668; ${(tc) => tc.name}</pre>
530
- `,
531
- )}
532
- ${when(
533
- (tc) => !tc.foldEvent && !isChatToolCallUnknown(tc),
534
- html<ChatToolCall>`
535
- <pre class="payload">
536
- ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<strong>${(tc) =>
537
- tc.name}</strong>(${(tc) => JSON.stringify(tc.args, null, 2)})</pre
538
- >
539
- `,
540
- )}
541
- ${when(
542
- isChatToolCallUnknown,
543
- html<ChatToolCall>`
544
- <pre class="payload unknown-tool">${(tc) => unknownToolPayload(tc)}</pre>
545
- `,
546
- )}
547
- ${when((tc) => !!tc.subAgentTrace?.length, subAgentTraceTemplate)}
548
- `;
549
-
550
- // ── Message row ─────────────────────────────────────────────────────────────
551
-
552
- const messageRowTemplate = html<ChatMessage, FoundationAiAssistant>`
553
- ${when(
554
- (m) => m.role === 'system-event',
555
- html<ChatMessage>`
556
- <div class="agent-switch-indicator" part="agent-switch-indicator">
557
- <span class="agent-switch-label">${(m) => m.content}</span>
558
- </div>
559
- `,
560
- )}
561
- ${when(
562
- (m) => m.role === 'compacted-summary',
563
- html<ChatMessage>`
564
- <div class="agent-switch-indicator compaction-divider" part="compaction-divider">
565
- <span class="agent-switch-label">${(m) => compactionDividerLabel(m)}</span>
566
- </div>
567
- `,
568
- )}
569
- ${when(
570
- (m) => m.role !== 'system-event' && m.role !== 'compacted-summary',
571
- html<ChatMessage, FoundationAiAssistant>`
572
- <div
573
- class="message-row ${(m) => messageType(m)} ${(m) =>
574
- m.interaction ? `present-${interactionPresentation(m)}` : ''}"
575
- >
576
- <div class="avatar ${(m) => messageType(m)}">
577
- ${when(
578
- // Keyed on messageType (not raw role) so a synthetic-user message,
579
- // which renders as 'user', gets the user avatar rather than the
580
- // assistant one.
581
- (m) => messageType(m) === 'user',
582
- userAvatarTemplate,
583
- )}${when((m) => messageType(m) !== 'user', assistantAvatarTemplate)}
584
- </div>
585
- <div
586
- class="message ${(m) => messageType(m)} ${(m) =>
587
- classNames(['narration', m.category === 'narration'])}"
588
- part="${(m) => messageParts(m)}"
589
- >
590
- ${when(
591
- // A 'bare' interaction owns its full presentation — suppress the
592
- // host sender label. Every other message keeps it.
593
- (m) => !(m.interaction && interactionPresentation(m) === 'bare'),
594
- html<ChatMessage, FoundationAiAssistant>`
595
- <div class="sender">
596
- ${(m, c) =>
597
- messageType(m) === 'ai-function' &&
598
- m.agentName &&
599
- (c.parent as FoundationAiAssistant).showAgentSwitchIndicator
600
- ? `Tool Call · ${m.agentLabel ?? m.agentName}`
601
- : senderLabelFor(m)}
602
- </div>
603
- `,
604
- )}
605
- <div class="content">
606
- ${when(
607
- (m) => m.content,
608
- html<ChatMessage>`
609
- <ai-chat-markdown :content="${(m) => m.content}"></ai-chat-markdown>
610
- `,
611
- )}
612
- ${when(
613
- (m) => m.toolCalls,
614
- html<ChatMessage>`
615
- ${repeat((m) => m.toolCalls ?? [], toolCallItemTemplate)}
616
- `,
617
- )}
618
- ${when(
619
- (m) => m.interaction,
620
- html<ChatMessage, FoundationAiAssistant>`
621
- <ai-chat-interaction-wrapper
622
- :componentName=${(m) => m.interaction!.componentName}
623
- :data=${(m) => m.interaction!.data}
624
- :interactionId=${(m) => m.interaction!.interactionId}
625
- :resolved=${(m) => m.interaction!.resolved}
626
- @interaction-completed=${(m, c) => c.parent.handleInteractionCompleted(c.event)}
627
- @interaction-cost=${(m, c) => c.parent.handleInteractionCost(c.event)}
628
- ></ai-chat-interaction-wrapper>
629
- `,
630
- )}
631
- </div>
632
- </div>
633
- </div>
634
- `,
635
- )}
636
- `;
637
-
638
- // ── Composer control pills ──────────────────────────────────────────────────
639
- //
640
- // The three leading controls share one round-icon-button shape (`composer-pill`)
641
- // and sit INSIDE the composer's box, on a bar under the textarea. The agent
642
- // pill additionally carries a label, so it is the only one that grows.
643
-
644
- const agentToggleButtonTemplate = html<FoundationAiAssistant>`
645
- <${buttonTag}
646
- class="composer-pill composer-pill-labelled agent-toggle-button"
647
- part="agent-toggle-button"
648
- appearance="stealth"
649
- aria-haspopup="menu"
650
- aria-expanded=${(x) => (x.agentPickerOpen ? 'true' : 'false')}
651
- title=${(x) => x.agentToggleTitle}
652
- ?disabled=${(x) => x.busy || x.pinLocked}
653
- @click=${(x) => x.toggleAgentPicker()}
654
- ><span
655
- class="pill-glyph"
656
- style="${(x) => (x.pinnedAgentColour ? `color: ${x.pinnedAgentColour}` : '')}"
657
- ><${iconTag}
658
- name="${(x) => (x.pinnedAgentName === null ? 'a' : 'thumbtack')}"
659
- ></${iconTag}></span
660
- ><span class="pill-label">${(x) => x.agentButtonLabel}</span></${buttonTag}>
661
- `;
662
-
663
- // The attach button shares the textarea's disable gate. Attaching a file that
664
- // can never be sent is a dead end — and `blocked`, `compacting` and `restoring`
665
- // were all missing from it, so files could be attached during a compaction or
666
- // against an exhausted budget and then never sent.
667
- const attachButtonTemplate = html<FoundationAiAssistant>`
668
- <${buttonTag}
669
- class="composer-pill attach-button"
670
- part="attach-button"
671
- appearance="stealth"
672
- title=${(x) => `Attach file (${x.chatConfig.ui?.acceptedFiles})`}
673
- aria-label="Attach file"
674
- ?disabled=${(x) => x.busy || x.compacting || x.restoring || x.blocked}
675
- aria-describedby=${composerDescribedBy}
676
- @click=${(x) => x.triggerFileInput()}
677
- ><${iconTag} name="paperclip"></${iconTag}></${buttonTag}>
678
- `;
679
-
680
- const sessionMenuButtonTemplate = html<FoundationAiAssistant>`
681
- <${buttonTag}
682
- class="composer-pill session-menu-button"
683
- part="session-menu-button"
684
- appearance="stealth"
685
- title="Session options"
686
- aria-label="Session options"
687
- aria-haspopup="menu"
688
- aria-expanded=${(x) => (x.sessionMenuOpen ? 'true' : 'false')}
689
- ?disabled=${(x) => x.busy || x.compacting || x.restoring}
690
- @click=${(x) => x.toggleSessionMenu()}
691
- ><${iconTag} name="ellipsis"></${iconTag}></${buttonTag}>
692
- `;
693
-
694
- // The agent control is LAST in the cluster because it is the only one whose
695
- // width changes: pinning an agent swaps `AUTO` for a name, and with it in
696
- // front the fixed-size buttons would slide sideways every time the pin
697
- // changed. Trailing, it grows into the empty middle of the bar instead.
698
- const composerBarStartTemplate = html<FoundationAiAssistant>`
699
- <div class="composer-bar-start" part="composer-bar-start">
700
- ${when((x) => !!x.chatConfig.ui?.acceptedFiles, attachButtonTemplate)}${when(
701
- (x) => x.sessionMenuEnabled,
702
- sessionMenuButtonTemplate,
703
- )}${when((x) => x.agentPickerEnabled, agentToggleButtonTemplate)}
704
- </div>
705
- `;
706
-
707
- // ── Send / Stop ─────────────────────────────────────────────────────────────
708
- //
709
- // One slot, two mutually exclusive actions. While a turn runs, Stop takes the
710
- // slot outright rather than appearing alongside a greyed-out Send — a disabled
711
- // control next to a live one is just something for the eye to skip.
712
-
713
- const stopButtonTemplate = html<FoundationAiAssistant>`
714
- <${buttonTag}
715
- class="composer-pill stop-button"
716
- part="stop-button"
717
- appearance="stealth"
718
- title="${(x) => (x.state === 'cancelling' ? 'Stopping…' : 'Stop')}"
719
- aria-label="${(x) => (x.state === 'cancelling' ? 'Stopping' : 'Stop')}"
720
- ?disabled=${(x) => x.state === 'cancelling'}
721
- @click=${(x) => x.handleStopClick()}
722
- ><${iconTag}
723
- class="${(x) => classNames('stop-icon', ['stop-icon-spinning', x.state === 'cancelling'])}"
724
- name="${(x) => (x.state === 'cancelling' ? 'spinner' : 'stop')}"
725
- ></${iconTag}></${buttonTag}>
726
- `;
727
-
728
- // The shell exists so a host can paint a gradient RING around Send without
729
- // owning the button's own surface: it is an inert zero-padding wrapper by
730
- // default, and ::part(send-shell) given a padding plus a background becomes
731
- // that ring. Styling ::part(send-button) alone cannot do it — the design-system
732
- // button paints its own background over any gradient set on it.
733
- const sendButtonTemplate = html<FoundationAiAssistant>`
734
- <span class="send-shell" part="send-shell">
735
- <${buttonTag}
736
- class="send-button"
737
- part="send-button"
738
- ?disabled=${(x) => x.busy || x.compacting || x.restoring || x.blocked || (!x.inputValue.trim() && !x.attachments.length)}
739
- aria-describedby=${composerDescribedBy}
740
- @click=${(x) => x.handleSendClick()}
741
- >Send</${buttonTag}>
742
- </span>
743
- `;
744
-
745
- // ── Composer menus ──────────────────────────────────────────────────────────
746
- //
747
- // Both menus are the same object: a `composer-menu` panel absolutely
748
- // positioned above the composer box, edge-aligned to the button that opened it
749
- // (`-start` / `-end`) and animated from that corner so it reads as growing out
750
- // of the button rather than appearing overhead. Only the contents differ — the
751
- // rows themselves share `menuItemStyles` across the two shadow roots.
752
-
753
- const agentPickerPanelTemplate = html<FoundationAiAssistant>`
754
- <div class="composer-menu agent-picker-panel" part="agent-picker-panel">
755
- <foundation-ai-agent-picker
756
- part="agent-picker"
757
- :mode="${(x) => x.agentPicker}"
758
- :agents="${(x) => x.agents ?? []}"
759
- :pinnedAgentName="${(x) => x.pinnedAgentName}"
760
- :disablePinColours="${(x) => !!x.chatConfig.picker?.disablePinColours}"
761
- @agent-pinned="${(x, c) => {
762
- x.pinnedAgentName = (c.event as CustomEvent<string | null>).detail;
763
- x.toggleAgentPicker();
764
- }}"
765
- ></foundation-ai-agent-picker>
766
- </div>
767
- `;
768
-
769
- // ── Session lifecycle menu (GENC-1351 §5.5) ──────────────────────────────────
770
-
771
- // Clear and Compact both confirm in place: the row you picked is replaced by a
772
- // danger-toned Confirm plus a Cancel, so the destructive step is a second
773
- // deliberate click and the menu never changes size enough to move under the
774
- // cursor.
775
- const sessionMenuIdleTemplate = html<FoundationAiAssistant>`
776
- <button
777
- class="menu-item"
778
- role="menuitem"
779
- type="button"
780
- ?disabled=${(x) => !x.compactable}
781
- title=${(x) =>
782
- x.compactable
783
- ? 'Summarize older messages to free up context'
784
- : 'Not enough conversation yet to be worth compacting'}
785
- @click=${(x) => (x.confirmingCompact = true)}
786
- >
787
- <span class="menu-item-icon" aria-hidden="true"><${iconTag} name="compress"></${iconTag}></span>
788
- <span class="menu-item-body">
789
- <span class="menu-item-name">Compact</span>
790
- <span class="menu-item-hint">Summarize older messages to free up context</span>
791
- </span>
792
- </button>
793
- <button
794
- class="menu-item"
795
- role="menuitem"
796
- type="button"
797
- @click=${(x) => (x.confirmingClear = true)}
798
- >
799
- <span class="menu-item-icon" aria-hidden="true"><${iconTag} name="trash"></${iconTag}></span>
800
- <span class="menu-item-body">
801
- <span class="menu-item-name">Clear</span>
802
- <span class="menu-item-hint">Start a new conversation</span>
803
- </span>
804
- </button>
805
- `;
806
-
807
- const sessionMenuCancelTemplate = html<FoundationAiAssistant>`
808
- <button
809
- class="menu-item"
810
- role="menuitem"
811
- type="button"
812
- @click=${(x) => {
813
- x.confirmingCompact = false;
814
- x.confirmingClear = false;
815
- }}
816
- >
817
- <span class="menu-item-icon" aria-hidden="true"><${iconTag} name="xmark"></${iconTag}></span>
818
- <span class="menu-item-body"><span class="menu-item-name">Cancel</span></span>
819
- </button>
820
- `;
821
-
822
- const sessionMenuConfirmCompactTemplate = html<FoundationAiAssistant>`
823
- <button
824
- class="menu-item"
825
- role="menuitem"
826
- type="button"
827
- @click=${(x) => {
828
- void x.compactSession();
829
- }}
830
- >
831
- <span class="menu-item-icon" aria-hidden="true"><${iconTag} name="compress"></${iconTag}></span>
832
- <span class="menu-item-body">
833
- <span class="menu-item-name">Compact now</span>
834
- <span class="menu-item-hint">Older turns are replaced by a summary</span>
835
- </span>
836
- </button>
837
- ${sessionMenuCancelTemplate}
838
- `;
839
-
840
- const sessionMenuConfirmClearTemplate = html<FoundationAiAssistant>`
841
- <button
842
- class="menu-item is-danger"
843
- role="menuitem"
844
- type="button"
845
- @click=${(x) => {
846
- void x.clearSession();
847
- }}
848
- >
849
- <span class="menu-item-icon" aria-hidden="true"><${iconTag} name="trash"></${iconTag}></span>
850
- <span class="menu-item-body">
851
- <span class="menu-item-name">Clear everything</span>
852
- <span class="menu-item-hint">This conversation is deleted permanently</span>
853
- </span>
854
- </button>
855
- ${sessionMenuCancelTemplate}
856
- `;
857
-
858
- const sessionMenuPanelTemplate = html<FoundationAiAssistant>`
859
- <div
860
- class="composer-menu session-menu-panel"
861
- part="session-menu-panel"
862
- role="menu"
863
- aria-label="Session options"
864
- >
865
- ${when((x) => !x.confirmingClear && !x.confirmingCompact, sessionMenuIdleTemplate)}${when(
866
- (x) => x.confirmingCompact,
867
- sessionMenuConfirmCompactTemplate,
868
- )}${when((x) => x.confirmingClear, sessionMenuConfirmClearTemplate)}
869
- </div>
870
- `;
871
-
872
- // ── Root template ───────────────────────────────────────────────────────────
873
-
874
- return html<FoundationAiAssistant>`
875
- <div class="chat-wrapper" part="chat-wrapper">
876
- ${when(
877
- (x) => !!x.chatConfig.ui?.acceptedFiles,
878
- html<FoundationAiAssistant>`
879
- <input
880
- class="file-input"
881
- type="file"
882
- multiple
883
- accept=${(x) => x.chatConfig.ui?.acceptedFiles}
884
- @change=${(x, c) => x.handleFileSelect(c.event)}
885
- />
886
- `,
887
- )}
888
- ${when(
889
- (x) => x.settingsCogVisible,
890
- html<FoundationAiAssistant>`
891
- <div
892
- class="chat-header ${(x) => (x.popoutMode !== 'collapse' ? 'draggable' : '')}"
893
- part="chat-header"
894
- @mousedown="${(x, c) => x.onChatHeaderMouseDown(c.event as MouseEvent)}"
895
- >
896
- ${when(
897
- (x) => !!x.headerTitle,
898
- html<FoundationAiAssistant>`
899
- <span class="chat-title" part="chat-title">${(x) => x.headerTitle}</span>
900
- `,
901
- )}
902
- <div class="header-actions" part="header-actions">
903
- <slot name="header-actions-extra"></slot>
904
- ${when(
905
- (x) => !!x.popoutMode,
906
- html<FoundationAiAssistant>`
907
- <${buttonTag}
908
- class="popout-button"
909
- part="popout-button"
910
- appearance="stealth"
911
- title="${(x) => (x.popoutMode === 'expand' ? 'Expand to panel' : 'Pop back to bubble')}"
912
- ?disabled=${(x) => x.hasActivePendingInteraction}
913
- @click=${(x) => x.handlePopout()}
914
- ><${iconTag} name="${(x) => (x.popoutMode === 'expand' ? 'external-link-alt' : 'compress-alt')}"></${iconTag}></${buttonTag}>
915
- `,
916
- )}
917
- <${buttonTag}
918
- class="settings-button"
919
- part="settings-button"
920
- appearance="stealth"
921
- title="Settings"
922
- aria-haspopup="dialog"
923
- @click=${(x) => x.openSettingsModal()}
924
- ><${iconTag} name="cog"></${iconTag}></${buttonTag}>
925
- </div>
926
- </div>
927
- `,
928
- )}
929
-
930
- <slot name="below-header"></slot>
931
-
932
- <div class="splash-wrapper" part="splash">
933
- <slot name="splash"></slot>
934
- </div>
935
-
936
- <div class="messages" part="messages" ${ref('messagesEl')}>
937
- <div class="messages-content" part="messages-content">
938
- ${when((x) => x.compacting, compactingTemplate)}
939
- ${when((x) => x.restoring, restoringTemplate)}
940
- ${repeat(
941
- (x) => (x.compacting || x.restoring ? [] : x.visibleMessages),
942
- messageRowTemplate,
943
- )}
944
- ${
945
- /* GENC-1429: recycle:false so each pending interaction gets a FRESH wrapper. With
946
- recycling, the trailing slot's wrapper is reused across interactions (e.g. a tool that
947
- shows the code-diff approval, then the preview-status card): the reused wrapper still
948
- carries the prior interaction's truthy `resolved` (bound last, after componentName/
949
- interactionId trigger the re-render), so the completion listener's `if (this.resolved)
950
- return` guard swallows the next auto-resolve widget's completion and the driver hangs
951
- awaiting it. A fresh wrapper starts with resolved=undefined, so the guard is correct. */ ''
952
- }
953
- ${repeat((x) => x.activeInteractionRow, messageRowTemplate, { recycle: false })}
954
- ${when(
955
- (x) => x.liveSubAgentTrace.length > 0 && x.showToolCalls,
956
- liveSubAgentTraceTemplate,
957
- )}
958
- ${when(
959
- (x) =>
960
- x.showLoadingIndicator &&
961
- (x.chatConfig.ui?.animations == null ||
962
- LOADING_STYLE_ANIMATIONS.some((s) => x.enabledAnimations.includes(s))),
963
- html<FoundationAiAssistant>`
964
- <div class="message-row ai" part="thinking">
965
- <div class="avatar">
966
- ${(x) =>
967
- x.imageSrc
968
- ? html<FoundationAiAssistant>`
969
- <img src="${() => x.imageSrc}" alt="Assistant" class="avatar-img" />
970
- `
971
- : html<FoundationAiAssistant>`
972
- <span
973
- class="avatar-icon"
974
- :innerHTML="${() => x.assistantIconSafe}"
975
- ></span>
976
- `}
977
- </div>
978
- ${(x) => selectThinkingTemplate(x)}
979
- </div>
980
- `,
981
- )}
982
- </div>
983
- </div>
984
-
985
- ${when(
986
- (x) =>
987
- !x.composerHiddenByConfig && (x.attachments.length > 0 || x.attachmentErrors.length > 0),
988
- html<FoundationAiAssistant>`
989
- <div class="attachment-chips" part="attachment-chips">
990
- ${repeat(
991
- (x) => x.attachments,
992
- html<ChatAttachment, FoundationAiAssistant>`
993
- <span class="attachment-chip" part="attachment-chip">
994
- ${(a) => a.name}
995
- <button
996
- class="chip-remove"
997
- type="button"
998
- @click=${(a, c) => c.parent.removeAttachment(a)}
999
- >
1000
- &times;
1001
- </button>
1002
- </span>
1003
- `,
1004
- )}
1005
- ${repeat(
1006
- (x) => x.attachmentErrors,
1007
- html<string, FoundationAiAssistant>`
1008
- <span class="attachment-chip attachment-chip-error" part="attachment-chip-error">
1009
- ${(err) => err}
1010
- <button
1011
- class="chip-remove"
1012
- type="button"
1013
- @click=${(err, c) => c.parent.removeAttachmentError(err)}
1014
- >
1015
- &times;
1016
- </button>
1017
- </span>
1018
- `,
1019
- )}
1020
- </div>
1021
- `,
1022
- )}
1023
- ${when(
1024
- (x) => x.chatConfig.suggestions?.behavior !== 'never' && !x.suggestionsBlocked,
1025
- html<FoundationAiAssistant>`
1026
- <chat-suggestions
1027
- :state="${(x) => x.suggestionsState}"
1028
- @suggestion-clicked="${(x, c) =>
1029
- x.handleSuggestionClick((c.event as CustomEvent).detail)}"
1030
- ></chat-suggestions>
1031
- `,
1032
- )}
1033
- ${
1034
- /* Rendered above the composer and independently of it — a host that hides the
1035
- built-in composer (`composerHiddenByConfig`) still needs to be told why its own
1036
- input is being refused. */ ''
1037
- }
1038
- ${blockedBannerTemplate}
1039
- ${when(
1040
- (x) =>
1041
- !x.composerHiddenByConfig &&
1042
- !(x.busy && x.effectiveChatInputDuringExecution === 'hidden'),
1043
- html<FoundationAiAssistant>`
1044
- ${when(
1045
- (x) => x.isComposerResizable,
1046
- html<FoundationAiAssistant>`
1047
- <div
1048
- class="composer-resize-handle"
1049
- part="composer-resize-handle"
1050
- role="separator"
1051
- aria-orientation="horizontal"
1052
- title="Drag to resize the message box"
1053
- @pointerdown=${(x, c) => x.onComposerResizeStart(c.event as PointerEvent)}
1054
- @pointermove=${(x, c) => x.onComposerResizeMove(c.event as PointerEvent)}
1055
- @pointerup=${(x, c) => x.onComposerResizeEnd(c.event as PointerEvent)}
1056
- @pointercancel=${(x, c) => x.onComposerResizeEnd(c.event as PointerEvent)}
1057
- ></div>
1058
- `,
1059
- )}
1060
- <div class="composer" part="composer">
1061
- <${textareaTag}
1062
- ${ref('chatInputEl')}
1063
- class="chat-input"
1064
- part="input"
1065
- style=${(x) => x.composerHeightStyle}
1066
- placeholder=${(x) => x.effectivePlaceholder}
1067
- :value=${(x) => x.inputValue}
1068
- ?disabled=${(x) => x.busy || x.compacting || x.restoring || x.blocked}
1069
- aria-disabled=${(x) => (x.blocked ? 'true' : null)}
1070
- aria-label=${(x) => (x.blocked ? x.effectiveBlockedReason : null)}
1071
- aria-describedby=${composerDescribedBy}
1072
- @input=${(x, c) => (x.inputValue = (c.event.target as any).value)}
1073
- @keydown=${(x, c) => {
1074
- if (
1075
- (c.event as KeyboardEvent).key === 'Enter' &&
1076
- !(c.event as KeyboardEvent).shiftKey
1077
- ) {
1078
- c.event.preventDefault();
1079
- x.handleSendClick();
1080
- return false;
1081
- }
1082
- return true;
1083
- }}
1084
- ></${textareaTag}>
1085
- <div class="composer-bar" part="composer-bar">
1086
- ${composerBarStartTemplate}
1087
- ${
1088
- /* Stop and Send are mutually exclusive and share this slot — see
1089
- `stopButtonVisible`. The only state with neither is a host that
1090
- suppressed Stop, which keeps a disabled Send instead. */ ''
1091
- }
1092
- <div class="composer-bar-end" part="composer-bar-end">
1093
- ${when((x) => x.stopButtonVisible, stopButtonTemplate)}${when(
1094
- (x) => x.sendButtonVisible,
1095
- sendButtonTemplate,
1096
- )}
1097
- </div>
1098
- </div>
1099
- ${when(
1100
- (x) => x.agentPickerEnabled && x.agentPickerOpen,
1101
- agentPickerPanelTemplate,
1102
- )}${when((x) => x.sessionMenuOpen && !x.busy, sessionMenuPanelTemplate)}
1103
- </div>
1104
- </div>
1105
- `,
1106
- )}
1107
- <ai-halo-overlay
1108
- part="halo-overlay"
1109
- ?active=${(x) => x.showHalo !== 'no' && x.enabledAnimations.includes('halo')}
1110
- :speed="${(x) =>
1111
- x.showHalo === 'orchestrating' ? HALO_SPEED_ORCHESTRATING : HALO_SPEED_DEFAULT}"
1112
- direction="${(x) => (x.showHalo === 'orchestrating' ? 'ccw' : 'cw')}"
1113
- :borderSize="${(x) => (x.showHalo === 'orchestrating' ? 2 : HALO_BORDER_SIZE_DEFAULT)}"
1114
- ></ai-halo-overlay>
1115
- </div>
1116
- ${SettingsModalTemplate(designSystemPrefix)}
1117
- ${persistenceBrokenModalTemplate(designSystemPrefix)}
1118
- `;
1119
- };