@data-club/ai-hub 0.0.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 (344) hide show
  1. package/dist/auth/passwordPolicy.d.ts +55 -0
  2. package/dist/auth/passwordPolicy.d.ts.map +1 -0
  3. package/dist/auth/passwordPolicy.js +63 -0
  4. package/dist/auth/passwordPolicy.js.map +1 -0
  5. package/dist/auth/texts.d.ts +815 -0
  6. package/dist/auth/texts.d.ts.map +1 -0
  7. package/dist/auth/texts.js +429 -0
  8. package/dist/auth/texts.js.map +1 -0
  9. package/dist/client/AIHubClient.d.ts +1058 -0
  10. package/dist/client/AIHubClient.d.ts.map +1 -0
  11. package/dist/client/AIHubClient.js +950 -0
  12. package/dist/client/AIHubClient.js.map +1 -0
  13. package/dist/client/index.d.ts +11 -0
  14. package/dist/client/index.d.ts.map +1 -0
  15. package/dist/client/index.js +9 -0
  16. package/dist/client/index.js.map +1 -0
  17. package/dist/client/sseParser.d.ts +41 -0
  18. package/dist/client/sseParser.d.ts.map +1 -0
  19. package/dist/client/sseParser.js +153 -0
  20. package/dist/client/sseParser.js.map +1 -0
  21. package/dist/client/tokenStorage.d.ts +56 -0
  22. package/dist/client/tokenStorage.d.ts.map +1 -0
  23. package/dist/client/tokenStorage.js +116 -0
  24. package/dist/client/tokenStorage.js.map +1 -0
  25. package/dist/components/AgentsPicker/index.d.ts +70 -0
  26. package/dist/components/AgentsPicker/index.d.ts.map +1 -0
  27. package/dist/components/AgentsPicker/index.js +63 -0
  28. package/dist/components/AgentsPicker/index.js.map +1 -0
  29. package/dist/components/AuthGate/index.d.ts +79 -0
  30. package/dist/components/AuthGate/index.d.ts.map +1 -0
  31. package/dist/components/AuthGate/index.js +61 -0
  32. package/dist/components/AuthGate/index.js.map +1 -0
  33. package/dist/components/CacheBadge/index.d.ts +88 -0
  34. package/dist/components/CacheBadge/index.d.ts.map +1 -0
  35. package/dist/components/CacheBadge/index.js +127 -0
  36. package/dist/components/CacheBadge/index.js.map +1 -0
  37. package/dist/components/ChangePassword/index.d.ts +109 -0
  38. package/dist/components/ChangePassword/index.d.ts.map +1 -0
  39. package/dist/components/ChangePassword/index.js +142 -0
  40. package/dist/components/ChangePassword/index.js.map +1 -0
  41. package/dist/components/ChatShell/ShellHeader.d.ts +44 -0
  42. package/dist/components/ChatShell/ShellHeader.d.ts.map +1 -0
  43. package/dist/components/ChatShell/ShellHeader.js +14 -0
  44. package/dist/components/ChatShell/ShellHeader.js.map +1 -0
  45. package/dist/components/ChatShell/UserMenu.d.ts +48 -0
  46. package/dist/components/ChatShell/UserMenu.d.ts.map +1 -0
  47. package/dist/components/ChatShell/UserMenu.js +86 -0
  48. package/dist/components/ChatShell/UserMenu.js.map +1 -0
  49. package/dist/components/ChatShell/index.d.ts +98 -0
  50. package/dist/components/ChatShell/index.d.ts.map +1 -0
  51. package/dist/components/ChatShell/index.js +124 -0
  52. package/dist/components/ChatShell/index.js.map +1 -0
  53. package/dist/components/DataClubAIHub/index.d.ts +132 -0
  54. package/dist/components/DataClubAIHub/index.d.ts.map +1 -0
  55. package/dist/components/DataClubAIHub/index.js +124 -0
  56. package/dist/components/DataClubAIHub/index.js.map +1 -0
  57. package/dist/components/DataClubAIHubAdmin/AgentEditorModal.d.ts +89 -0
  58. package/dist/components/DataClubAIHubAdmin/AgentEditorModal.d.ts.map +1 -0
  59. package/dist/components/DataClubAIHubAdmin/AgentEditorModal.js +199 -0
  60. package/dist/components/DataClubAIHubAdmin/AgentEditorModal.js.map +1 -0
  61. package/dist/components/DataClubAIHubAdmin/AgentsPage.d.ts +33 -0
  62. package/dist/components/DataClubAIHubAdmin/AgentsPage.d.ts.map +1 -0
  63. package/dist/components/DataClubAIHubAdmin/AgentsPage.js +137 -0
  64. package/dist/components/DataClubAIHubAdmin/AgentsPage.js.map +1 -0
  65. package/dist/components/DataClubAIHubAdmin/CreateUserModal.d.ts +50 -0
  66. package/dist/components/DataClubAIHubAdmin/CreateUserModal.d.ts.map +1 -0
  67. package/dist/components/DataClubAIHubAdmin/CreateUserModal.js +93 -0
  68. package/dist/components/DataClubAIHubAdmin/CreateUserModal.js.map +1 -0
  69. package/dist/components/DataClubAIHubAdmin/PasswordRevealModal.d.ts +49 -0
  70. package/dist/components/DataClubAIHubAdmin/PasswordRevealModal.d.ts.map +1 -0
  71. package/dist/components/DataClubAIHubAdmin/PasswordRevealModal.js +94 -0
  72. package/dist/components/DataClubAIHubAdmin/PasswordRevealModal.js.map +1 -0
  73. package/dist/components/DataClubAIHubAdmin/ResetPasswordModal.d.ts +37 -0
  74. package/dist/components/DataClubAIHubAdmin/ResetPasswordModal.d.ts.map +1 -0
  75. package/dist/components/DataClubAIHubAdmin/ResetPasswordModal.js +44 -0
  76. package/dist/components/DataClubAIHubAdmin/ResetPasswordModal.js.map +1 -0
  77. package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.d.ts +56 -0
  78. package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.d.ts.map +1 -0
  79. package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.js +158 -0
  80. package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.js.map +1 -0
  81. package/dist/components/DataClubAIHubAdmin/RotatingMessagesPage.d.ts +29 -0
  82. package/dist/components/DataClubAIHubAdmin/RotatingMessagesPage.d.ts.map +1 -0
  83. package/dist/components/DataClubAIHubAdmin/RotatingMessagesPage.js +183 -0
  84. package/dist/components/DataClubAIHubAdmin/RotatingMessagesPage.js.map +1 -0
  85. package/dist/components/DataClubAIHubAdmin/SessionsOverviewPage.d.ts +38 -0
  86. package/dist/components/DataClubAIHubAdmin/SessionsOverviewPage.d.ts.map +1 -0
  87. package/dist/components/DataClubAIHubAdmin/SessionsOverviewPage.js +294 -0
  88. package/dist/components/DataClubAIHubAdmin/SessionsOverviewPage.js.map +1 -0
  89. package/dist/components/DataClubAIHubAdmin/SettingsPage.d.ts +36 -0
  90. package/dist/components/DataClubAIHubAdmin/SettingsPage.d.ts.map +1 -0
  91. package/dist/components/DataClubAIHubAdmin/SettingsPage.js +373 -0
  92. package/dist/components/DataClubAIHubAdmin/SettingsPage.js.map +1 -0
  93. package/dist/components/DataClubAIHubAdmin/UsersPage.d.ts +47 -0
  94. package/dist/components/DataClubAIHubAdmin/UsersPage.d.ts.map +1 -0
  95. package/dist/components/DataClubAIHubAdmin/UsersPage.js +233 -0
  96. package/dist/components/DataClubAIHubAdmin/UsersPage.js.map +1 -0
  97. package/dist/components/DataClubAIHubAdmin/index.d.ts +87 -0
  98. package/dist/components/DataClubAIHubAdmin/index.d.ts.map +1 -0
  99. package/dist/components/DataClubAIHubAdmin/index.js +137 -0
  100. package/dist/components/DataClubAIHubAdmin/index.js.map +1 -0
  101. package/dist/components/DataClubAIHubLogin/index.d.ts +113 -0
  102. package/dist/components/DataClubAIHubLogin/index.d.ts.map +1 -0
  103. package/dist/components/DataClubAIHubLogin/index.js +123 -0
  104. package/dist/components/DataClubAIHubLogin/index.js.map +1 -0
  105. package/dist/components/EffortControl/index.d.ts +57 -0
  106. package/dist/components/EffortControl/index.d.ts.map +1 -0
  107. package/dist/components/EffortControl/index.js +84 -0
  108. package/dist/components/EffortControl/index.js.map +1 -0
  109. package/dist/components/Layout/index.d.ts +63 -0
  110. package/dist/components/Layout/index.d.ts.map +1 -0
  111. package/dist/components/Layout/index.js +45 -0
  112. package/dist/components/Layout/index.js.map +1 -0
  113. package/dist/components/MainColumn/index.d.ts +69 -0
  114. package/dist/components/MainColumn/index.d.ts.map +1 -0
  115. package/dist/components/MainColumn/index.js +47 -0
  116. package/dist/components/MainColumn/index.js.map +1 -0
  117. package/dist/components/MarkdownRenderer/index.d.ts +66 -0
  118. package/dist/components/MarkdownRenderer/index.d.ts.map +1 -0
  119. package/dist/components/MarkdownRenderer/index.js +153 -0
  120. package/dist/components/MarkdownRenderer/index.js.map +1 -0
  121. package/dist/components/MessageInput/index.d.ts +85 -0
  122. package/dist/components/MessageInput/index.d.ts.map +1 -0
  123. package/dist/components/MessageInput/index.js +139 -0
  124. package/dist/components/MessageInput/index.js.map +1 -0
  125. package/dist/components/MessageList/index.d.ts +156 -0
  126. package/dist/components/MessageList/index.d.ts.map +1 -0
  127. package/dist/components/MessageList/index.js +352 -0
  128. package/dist/components/MessageList/index.js.map +1 -0
  129. package/dist/components/ReasoningSelector/index.d.ts +77 -0
  130. package/dist/components/ReasoningSelector/index.d.ts.map +1 -0
  131. package/dist/components/ReasoningSelector/index.js +156 -0
  132. package/dist/components/ReasoningSelector/index.js.map +1 -0
  133. package/dist/components/ReasoningView/index.d.ts +94 -0
  134. package/dist/components/ReasoningView/index.d.ts.map +1 -0
  135. package/dist/components/ReasoningView/index.js +83 -0
  136. package/dist/components/ReasoningView/index.js.map +1 -0
  137. package/dist/components/ScrollToPresentButton/index.d.ts +54 -0
  138. package/dist/components/ScrollToPresentButton/index.d.ts.map +1 -0
  139. package/dist/components/ScrollToPresentButton/index.js +44 -0
  140. package/dist/components/ScrollToPresentButton/index.js.map +1 -0
  141. package/dist/components/SessionsPanel/SessionTitleModal.d.ts +79 -0
  142. package/dist/components/SessionsPanel/SessionTitleModal.d.ts.map +1 -0
  143. package/dist/components/SessionsPanel/SessionTitleModal.js +102 -0
  144. package/dist/components/SessionsPanel/SessionTitleModal.js.map +1 -0
  145. package/dist/components/SessionsPanel/index.d.ts +100 -0
  146. package/dist/components/SessionsPanel/index.d.ts.map +1 -0
  147. package/dist/components/SessionsPanel/index.js +176 -0
  148. package/dist/components/SessionsPanel/index.js.map +1 -0
  149. package/dist/components/SystemNotice/index.d.ts +66 -0
  150. package/dist/components/SystemNotice/index.d.ts.map +1 -0
  151. package/dist/components/SystemNotice/index.js +81 -0
  152. package/dist/components/SystemNotice/index.js.map +1 -0
  153. package/dist/components/ThemeSwitcher/index.d.ts +66 -0
  154. package/dist/components/ThemeSwitcher/index.d.ts.map +1 -0
  155. package/dist/components/ThemeSwitcher/index.js +73 -0
  156. package/dist/components/ThemeSwitcher/index.js.map +1 -0
  157. package/dist/components/ToolCallView/formatContent.d.ts +36 -0
  158. package/dist/components/ToolCallView/formatContent.d.ts.map +1 -0
  159. package/dist/components/ToolCallView/formatContent.js +53 -0
  160. package/dist/components/ToolCallView/formatContent.js.map +1 -0
  161. package/dist/components/ToolCallView/index.d.ts +120 -0
  162. package/dist/components/ToolCallView/index.d.ts.map +1 -0
  163. package/dist/components/ToolCallView/index.js +104 -0
  164. package/dist/components/ToolCallView/index.js.map +1 -0
  165. package/dist/components/TpmBanner/index.d.ts +53 -0
  166. package/dist/components/TpmBanner/index.d.ts.map +1 -0
  167. package/dist/components/TpmBanner/index.js +116 -0
  168. package/dist/components/TpmBanner/index.js.map +1 -0
  169. package/dist/components/TurnStatusIndicator/index.d.ts +77 -0
  170. package/dist/components/TurnStatusIndicator/index.d.ts.map +1 -0
  171. package/dist/components/TurnStatusIndicator/index.js +122 -0
  172. package/dist/components/TurnStatusIndicator/index.js.map +1 -0
  173. package/dist/components/internal/PasswordInput.d.ts +62 -0
  174. package/dist/components/internal/PasswordInput.d.ts.map +1 -0
  175. package/dist/components/internal/PasswordInput.js +42 -0
  176. package/dist/components/internal/PasswordInput.js.map +1 -0
  177. package/dist/context/AIHubContext.d.ts +220 -0
  178. package/dist/context/AIHubContext.d.ts.map +1 -0
  179. package/dist/context/AIHubContext.js +19 -0
  180. package/dist/context/AIHubContext.js.map +1 -0
  181. package/dist/context/AIHubProvider.d.ts +75 -0
  182. package/dist/context/AIHubProvider.d.ts.map +1 -0
  183. package/dist/context/AIHubProvider.js +954 -0
  184. package/dist/context/AIHubProvider.js.map +1 -0
  185. package/dist/context/index.d.ts +9 -0
  186. package/dist/context/index.d.ts.map +1 -0
  187. package/dist/context/index.js +9 -0
  188. package/dist/context/index.js.map +1 -0
  189. package/dist/context/theme.d.ts +43 -0
  190. package/dist/context/theme.d.ts.map +1 -0
  191. package/dist/context/theme.js +70 -0
  192. package/dist/context/theme.js.map +1 -0
  193. package/dist/context/useAIHub.d.ts +104 -0
  194. package/dist/context/useAIHub.d.ts.map +1 -0
  195. package/dist/context/useAIHub.js +63 -0
  196. package/dist/context/useAIHub.js.map +1 -0
  197. package/dist/context/useAIHubAuth.d.ts +44 -0
  198. package/dist/context/useAIHubAuth.d.ts.map +1 -0
  199. package/dist/context/useAIHubAuth.js +35 -0
  200. package/dist/context/useAIHubAuth.js.map +1 -0
  201. package/dist/index.d.ts +76 -0
  202. package/dist/index.d.ts.map +1 -0
  203. package/dist/index.js +57 -0
  204. package/dist/index.js.map +1 -0
  205. package/dist/primitives/index.d.ts +99 -0
  206. package/dist/primitives/index.d.ts.map +1 -0
  207. package/dist/primitives/index.js +97 -0
  208. package/dist/primitives/index.js.map +1 -0
  209. package/dist/state/affordances.d.ts +182 -0
  210. package/dist/state/affordances.d.ts.map +1 -0
  211. package/dist/state/affordances.js +189 -0
  212. package/dist/state/affordances.js.map +1 -0
  213. package/dist/state/stateReducer.d.ts +72 -0
  214. package/dist/state/stateReducer.d.ts.map +1 -0
  215. package/dist/state/stateReducer.js +122 -0
  216. package/dist/state/stateReducer.js.map +1 -0
  217. package/dist/stories/EffortControl.stories.d.ts +14 -0
  218. package/dist/stories/EffortControl.stories.d.ts.map +1 -0
  219. package/dist/stories/EffortControl.stories.js +43 -0
  220. package/dist/stories/EffortControl.stories.js.map +1 -0
  221. package/dist/stories/MarkdownRenderer.stories.d.ts +25 -0
  222. package/dist/stories/MarkdownRenderer.stories.d.ts.map +1 -0
  223. package/dist/stories/MarkdownRenderer.stories.js +178 -0
  224. package/dist/stories/MarkdownRenderer.stories.js.map +1 -0
  225. package/dist/stories/MessageBubble.stories.d.ts +21 -0
  226. package/dist/stories/MessageBubble.stories.d.ts.map +1 -0
  227. package/dist/stories/MessageBubble.stories.js +59 -0
  228. package/dist/stories/MessageBubble.stories.js.map +1 -0
  229. package/dist/stories/ReasoningView.stories.d.ts +18 -0
  230. package/dist/stories/ReasoningView.stories.d.ts.map +1 -0
  231. package/dist/stories/ReasoningView.stories.js +62 -0
  232. package/dist/stories/ReasoningView.stories.js.map +1 -0
  233. package/dist/stories/ScrollToPresentButton.stories.d.ts +18 -0
  234. package/dist/stories/ScrollToPresentButton.stories.d.ts.map +1 -0
  235. package/dist/stories/ScrollToPresentButton.stories.js +34 -0
  236. package/dist/stories/ScrollToPresentButton.stories.js.map +1 -0
  237. package/dist/stories/SystemNotice.stories.d.ts +17 -0
  238. package/dist/stories/SystemNotice.stories.d.ts.map +1 -0
  239. package/dist/stories/SystemNotice.stories.js +42 -0
  240. package/dist/stories/SystemNotice.stories.js.map +1 -0
  241. package/dist/stories/ThemeSwitcher.stories.d.ts +15 -0
  242. package/dist/stories/ThemeSwitcher.stories.d.ts.map +1 -0
  243. package/dist/stories/ThemeSwitcher.stories.js +35 -0
  244. package/dist/stories/ThemeSwitcher.stories.js.map +1 -0
  245. package/dist/stories/ToolCallView.stories.d.ts +20 -0
  246. package/dist/stories/ToolCallView.stories.d.ts.map +1 -0
  247. package/dist/stories/ToolCallView.stories.js +65 -0
  248. package/dist/stories/ToolCallView.stories.js.map +1 -0
  249. package/dist/stories/TpmBanner.stories.d.ts +24 -0
  250. package/dist/stories/TpmBanner.stories.d.ts.map +1 -0
  251. package/dist/stories/TpmBanner.stories.js +63 -0
  252. package/dist/stories/TpmBanner.stories.js.map +1 -0
  253. package/dist/stories/TurnStatusIndicator.stories.d.ts +25 -0
  254. package/dist/stories/TurnStatusIndicator.stories.d.ts.map +1 -0
  255. package/dist/stories/TurnStatusIndicator.stories.js +56 -0
  256. package/dist/stories/TurnStatusIndicator.stories.js.map +1 -0
  257. package/dist/stories/_mockProvider.d.ts +44 -0
  258. package/dist/stories/_mockProvider.d.ts.map +1 -0
  259. package/dist/stories/_mockProvider.js +84 -0
  260. package/dist/stories/_mockProvider.js.map +1 -0
  261. package/dist/types.d.ts +787 -0
  262. package/dist/types.d.ts.map +1 -0
  263. package/dist/types.js +19 -0
  264. package/dist/types.js.map +1 -0
  265. package/dist/webcomponent/defineWebComponent.d.ts +106 -0
  266. package/dist/webcomponent/defineWebComponent.d.ts.map +1 -0
  267. package/dist/webcomponent/defineWebComponent.js +126 -0
  268. package/dist/webcomponent/defineWebComponent.js.map +1 -0
  269. package/dist/webcomponent/index.d.ts +7 -0
  270. package/dist/webcomponent/index.d.ts.map +1 -0
  271. package/dist/webcomponent/index.js +7 -0
  272. package/dist/webcomponent/index.js.map +1 -0
  273. package/package.json +63 -0
  274. package/src/__lint-canary__/step1-lint-canary.ts.disabled +28 -0
  275. package/src/auth/passwordPolicy.ts +75 -0
  276. package/src/auth/texts.ts +1294 -0
  277. package/src/client/AIHubClient.ts +1718 -0
  278. package/src/client/index.ts +30 -0
  279. package/src/client/sseParser.ts +168 -0
  280. package/src/client/tokenStorage.ts +131 -0
  281. package/src/components/AgentsPicker/index.tsx +147 -0
  282. package/src/components/AuthGate/index.tsx +132 -0
  283. package/src/components/CacheBadge/index.tsx +245 -0
  284. package/src/components/ChangePassword/index.tsx +375 -0
  285. package/src/components/ChatShell/ShellHeader.tsx +80 -0
  286. package/src/components/ChatShell/UserMenu.tsx +196 -0
  287. package/src/components/ChatShell/index.tsx +307 -0
  288. package/src/components/DataClubAIHub/index.tsx +300 -0
  289. package/src/components/DataClubAIHubAdmin/AgentEditorModal.tsx +505 -0
  290. package/src/components/DataClubAIHubAdmin/AgentsPage.tsx +342 -0
  291. package/src/components/DataClubAIHubAdmin/CreateUserModal.tsx +214 -0
  292. package/src/components/DataClubAIHubAdmin/PasswordRevealModal.tsx +204 -0
  293. package/src/components/DataClubAIHubAdmin/ResetPasswordModal.tsx +122 -0
  294. package/src/components/DataClubAIHubAdmin/RotatingMessageEditorModal.tsx +408 -0
  295. package/src/components/DataClubAIHubAdmin/RotatingMessagesPage.tsx +373 -0
  296. package/src/components/DataClubAIHubAdmin/SessionsOverviewPage.tsx +639 -0
  297. package/src/components/DataClubAIHubAdmin/SettingsPage.tsx +979 -0
  298. package/src/components/DataClubAIHubAdmin/UsersPage.tsx +495 -0
  299. package/src/components/DataClubAIHubAdmin/index.tsx +277 -0
  300. package/src/components/DataClubAIHubLogin/index.tsx +282 -0
  301. package/src/components/EffortControl/index.tsx +138 -0
  302. package/src/components/Layout/index.tsx +92 -0
  303. package/src/components/MainColumn/index.tsx +131 -0
  304. package/src/components/MarkdownRenderer/index.tsx +256 -0
  305. package/src/components/MessageInput/index.tsx +283 -0
  306. package/src/components/MessageList/index.tsx +753 -0
  307. package/src/components/ReasoningSelector/index.tsx +268 -0
  308. package/src/components/ReasoningView/index.tsx +183 -0
  309. package/src/components/ScrollToPresentButton/index.tsx +85 -0
  310. package/src/components/SessionsPanel/SessionTitleModal.tsx +256 -0
  311. package/src/components/SessionsPanel/index.tsx +406 -0
  312. package/src/components/SystemNotice/index.tsx +151 -0
  313. package/src/components/ThemeSwitcher/index.tsx +136 -0
  314. package/src/components/ToolCallView/formatContent.ts +50 -0
  315. package/src/components/ToolCallView/index.tsx +274 -0
  316. package/src/components/TpmBanner/index.tsx +172 -0
  317. package/src/components/TurnStatusIndicator/index.tsx +203 -0
  318. package/src/components/internal/PasswordInput.tsx +145 -0
  319. package/src/context/AIHubContext.ts +249 -0
  320. package/src/context/AIHubProvider.tsx +1084 -0
  321. package/src/context/index.ts +9 -0
  322. package/src/context/theme.ts +76 -0
  323. package/src/context/useAIHub.ts +151 -0
  324. package/src/context/useAIHubAuth.ts +65 -0
  325. package/src/index.ts +275 -0
  326. package/src/primitives/index.ts +101 -0
  327. package/src/state/affordances.ts +321 -0
  328. package/src/state/stateReducer.ts +131 -0
  329. package/src/stories/EffortControl.stories.tsx +63 -0
  330. package/src/stories/MarkdownRenderer.stories.tsx +201 -0
  331. package/src/stories/MessageBubble.stories.tsx +101 -0
  332. package/src/stories/ReasoningView.stories.tsx +81 -0
  333. package/src/stories/ScrollToPresentButton.stories.tsx +52 -0
  334. package/src/stories/SystemNotice.stories.tsx +59 -0
  335. package/src/stories/ThemeSwitcher.stories.tsx +55 -0
  336. package/src/stories/ToolCallView.stories.tsx +95 -0
  337. package/src/stories/TpmBanner.stories.tsx +93 -0
  338. package/src/stories/TurnStatusIndicator.stories.tsx +86 -0
  339. package/src/stories/_mockProvider.tsx +116 -0
  340. package/src/styles/CLASSES.md +356 -0
  341. package/src/styles/default.css +2937 -0
  342. package/src/types.ts +867 -0
  343. package/src/webcomponent/defineWebComponent.tsx +193 -0
  344. package/src/webcomponent/index.ts +11 -0
@@ -0,0 +1,753 @@
1
+ /**
2
+ * `<MessageList />` — the scrollable list of message bubbles in the
3
+ * active session.
4
+ *
5
+ * Step 10 layers in:
6
+ *
7
+ * - `--system` / `--tool` / `--failed` bubble modifiers (Step 9 shipped
8
+ * `--user` / `--assistant`).
9
+ * - The `customMessageActions` slot — Tier-2 / Tier-3 wrappers render
10
+ * their own action bar inside every assistant bubble (e.g. 👍 / 👎).
11
+ * - The `customMessageBubble` slot — Tier-3 escape hatch that replaces
12
+ * the entire bubble for a message.
13
+ * - The `<TpmBanner />` — sits above the streaming bubble during a
14
+ * `waiting_provider` phase + the brief resumed-flash.
15
+ * - The `<ScrollToPresentButton />` — appears when the user has scrolled
16
+ * away from the bottom during a running turn (auto-scroll is
17
+ * inhibited while they're scrolled up).
18
+ * - Per-row Retry button on the failed-turn `--failed` assistant bubble.
19
+ *
20
+ * Server toggles (§8.4) gate via the resolved prop stack the monolith
21
+ * passes in:
22
+ *
23
+ * - `systemMessagesVisible` — when false, `system` and `tool`-role
24
+ * messages are filtered out of the list.
25
+ * - `tpmNoticeVisible` — when false, `<TpmBanner />` is suppressed.
26
+ * - `scrollToPresentVisible` — when false, the scroll-to-present
27
+ * button is suppressed.
28
+ *
29
+ * Exported under `AIHub.MessageList`.
30
+ *
31
+ * @see SPECIFICATIONS.md §7.1, §8.4, §10.4
32
+ * @see ui-sketches/03-chat.md — message_bubble + system_notice
33
+ */
34
+
35
+ import {
36
+ useCallback,
37
+ useEffect,
38
+ useMemo,
39
+ useRef,
40
+ useState,
41
+ type ReactNode,
42
+ } from "react";
43
+
44
+ import {
45
+ resolveAIHubTexts,
46
+ type AIHubTexts,
47
+ type DeepPartial,
48
+ } from "../../auth/texts.js";
49
+ import { useAIHub } from "../../context/useAIHub.js";
50
+ import { chatAffordances } from "../../state/affordances.js";
51
+ import type {
52
+ AIHubMessage,
53
+ AIHubMessageActionRenderer,
54
+ AIHubMessageBubbleRenderer,
55
+ } from "../../types.js";
56
+
57
+ import { CacheBadge } from "../CacheBadge/index.js";
58
+ import { MarkdownRenderer } from "../MarkdownRenderer/index.js";
59
+ import { ReasoningView } from "../ReasoningView/index.js";
60
+ import { ScrollToPresentButton } from "../ScrollToPresentButton/index.js";
61
+ import { ToolCallView } from "../ToolCallView/index.js";
62
+ import { TpmBanner } from "../TpmBanner/index.js";
63
+
64
+ /** Distance from bottom (in pixels) that still counts as "at the latest". */
65
+ const BOTTOM_EPSILON_PX = 32;
66
+
67
+ /* ────────────────────────────────────────────────────────────────────────── */
68
+ /* Block-expansion stable IDs */
69
+ /* ────────────────────────────────────────────────────────────────────────── */
70
+
71
+ /**
72
+ * Stable id for the reasoning sub-block of a *historical* assistant
73
+ * message. Used as the controlled-mode key into the user-expanded
74
+ * accordion state owned by `<MessageList />`.
75
+ */
76
+ function reasoningBlockId(messageId: string): string {
77
+ return `reasoning:${messageId}`;
78
+ }
79
+
80
+ /** Stable id for the reasoning sub-block of the *currently-streaming* bubble. */
81
+ const STREAMING_REASONING_BLOCK_ID = "reasoning:streaming";
82
+
83
+ /**
84
+ * Stable id for a tool-call card. The accordion key is namespaced by the
85
+ * **owning bubble** (`tool:<parent>:<tool_call_id>`) — empirically Letta
86
+ * has been observed to reuse `tool_call_id` values across logical
87
+ * messages within a single multi-step run, and even when it doesn't, the
88
+ * historical projector's per-step attribution could surface a record in
89
+ * two bubbles if it misfires. Without the namespace, two cards sharing
90
+ * an id would expand together because the accordion state lives in
91
+ * `<MessageList />` (cross-bubble). The namespace makes each bubble's
92
+ * card an independent accordion slot regardless of upstream id quirks.
93
+ *
94
+ * Pass `"streaming"` as `parentId` for the currently-streaming bubble's
95
+ * tool cards; pass the historical assistant message's id otherwise.
96
+ */
97
+ function toolCallBlockId(parentId: string, toolCallId: string): string {
98
+ return `tool:${parentId}:${toolCallId}`;
99
+ }
100
+
101
+ /**
102
+ * BEM class names emitted by `<MessageList />`.
103
+ */
104
+ export interface MessageListClassNames {
105
+ /**
106
+ * Outer wrapper. NOT the scroll container — that's `root` (a child of
107
+ * this element). The wrapper exists so `<ScrollToPresentButton />`
108
+ * can sit as a sibling of the scroll container and stay anchored to
109
+ * the visible chat area as the user scrolls.
110
+ */
111
+ wrapper: string;
112
+ /** Scroll container — the actual element that overflows + scrolls. */
113
+ root: string;
114
+ /** Inner block; targets `max-width` for readable line lengths. */
115
+ inner: string;
116
+ /** Empty-state placeholder (zero messages in the active session). */
117
+ empty: string;
118
+ /** Loading state. */
119
+ loading: string;
120
+ /** Placeholder when no session is selected. */
121
+ noSession: string;
122
+ /**
123
+ * Error banner shown above the streaming-assistant placeholder when
124
+ * the active turn failed without producing any assistant output.
125
+ */
126
+ errorBanner: string;
127
+ /** Per-bubble base class. */
128
+ bubble: string;
129
+ /** Bubble modifier — `--user`. */
130
+ bubbleUser: string;
131
+ /** Bubble modifier — `--assistant`. */
132
+ bubbleAssistant: string;
133
+ /** Bubble modifier — `--system`. */
134
+ bubbleSystem: string;
135
+ /** Bubble modifier — `--tool`. */
136
+ bubbleTool: string;
137
+ /** Bubble modifier — `--failed` (applied to the assistant bubble of a failed turn). */
138
+ bubbleFailed: string;
139
+ /** Bubble modifier — `--cancelled` (for the "Stopped" stamp). */
140
+ bubbleCancelled: string;
141
+ /** Wrapper for the role label inside the bubble. */
142
+ bubbleRole: string;
143
+ /** The text body inside the bubble. */
144
+ bubbleBody: string;
145
+ /** The action bar inside the bubble (slot for custom actions). */
146
+ bubbleActions: string;
147
+ /** The retry-button on `--failed` bubbles. */
148
+ bubbleRetryButton: string;
149
+ /** The "Stopped" stamp on `--cancelled` bubbles. */
150
+ bubbleCancelledStamp: string;
151
+ /** Streaming caret while a turn is in flight. */
152
+ bubbleCaret: string;
153
+ }
154
+
155
+ const DEFAULT_CLASS_NAMES: MessageListClassNames = Object.freeze({
156
+ wrapper: "data-club-ai-hub__messages-wrapper",
157
+ root: "data-club-ai-hub__messages",
158
+ inner: "data-club-ai-hub__messages__inner",
159
+ empty: "data-club-ai-hub__messages__empty",
160
+ loading: "data-club-ai-hub__messages__loading",
161
+ noSession: "data-club-ai-hub__messages__no-session",
162
+ errorBanner: "data-club-ai-hub__messages__error",
163
+ bubble: "data-club-ai-hub__message-bubble",
164
+ bubbleUser: "data-club-ai-hub__message-bubble--user",
165
+ bubbleAssistant: "data-club-ai-hub__message-bubble--assistant",
166
+ bubbleSystem: "data-club-ai-hub__message-bubble--system",
167
+ bubbleTool: "data-club-ai-hub__message-bubble--tool",
168
+ bubbleFailed: "data-club-ai-hub__message-bubble--failed",
169
+ bubbleCancelled: "data-club-ai-hub__message-bubble--cancelled",
170
+ bubbleRole: "data-club-ai-hub__message-bubble__role",
171
+ bubbleBody: "data-club-ai-hub__message-bubble__body",
172
+ bubbleActions: "data-club-ai-hub__message-bubble__actions",
173
+ bubbleRetryButton: "data-club-ai-hub__message-bubble__retry",
174
+ bubbleCancelledStamp: "data-club-ai-hub__message-bubble__cancelled-stamp",
175
+ bubbleCaret: "data-club-ai-hub__message-bubble__caret",
176
+ }) as MessageListClassNames;
177
+
178
+ /**
179
+ * Props for `<MessageList />`.
180
+ */
181
+ export interface MessageListProps {
182
+ /**
183
+ * Show system / tool-role bubbles. Default true. When false, those
184
+ * messages are filtered out (the user-facing list shows user +
185
+ * assistant only).
186
+ */
187
+ showSystemMessages?: boolean;
188
+ /**
189
+ * Render `<TpmBanner />` during `waiting_provider` and on
190
+ * tpm_exhausted. Default true. The monolith passes the resolved
191
+ * §8.4 flag here.
192
+ */
193
+ showTpmNotice?: boolean;
194
+ /**
195
+ * Render `<ReasoningView />` inside assistant bubbles when the
196
+ * message carries `reasoning`. Default true. Monolith forwards the
197
+ * resolved `reasoning_visible` flag.
198
+ */
199
+ showReasoning?: boolean;
200
+ /**
201
+ * Render `<ToolCallView />` cards inside assistant bubbles when the
202
+ * message carries `tool_calls`. Default true. Monolith forwards the
203
+ * resolved `tool_calls_visible` flag.
204
+ */
205
+ showToolCalls?: boolean;
206
+ /**
207
+ * Render `<ScrollToPresentButton />` when the user has scrolled away
208
+ * from the bottom of the list during a running turn. Default true.
209
+ */
210
+ showScrollToPresentButton?: boolean;
211
+ /**
212
+ * Render a "Retry" button on `--failed` assistant bubbles. Default
213
+ * true. When false the bubble still renders with the failed modifier
214
+ * but no action is offered.
215
+ */
216
+ showRetryButton?: boolean;
217
+ /**
218
+ * Tier 2 / Tier 3 slot — render the action bar of every assistant
219
+ * message bubble. The renderer receives the full `AIHubMessage` and
220
+ * returns React-renderable content (or null to suppress the bar
221
+ * for that message).
222
+ */
223
+ renderMessageActions?: AIHubMessageActionRenderer;
224
+ /**
225
+ * Tier 3 escape hatch — fully replace the message bubble for any
226
+ * message. The renderer is wrapped in the list's flex container; the
227
+ * caller is responsible for choosing variant modifiers.
228
+ */
229
+ renderMessageBubble?: AIHubMessageBubbleRenderer;
230
+ /** Extra class merged onto the root. */
231
+ className?: string;
232
+ /** Per-element class overrides. */
233
+ classNames?: Partial<MessageListClassNames>;
234
+ /** Wrapper-supplied i18n. */
235
+ texts?: DeepPartial<AIHubTexts>;
236
+ }
237
+
238
+ /**
239
+ * Renders the message list.
240
+ *
241
+ * @param props - see `MessageListProps`.
242
+ * @returns the rendered list.
243
+ */
244
+ export function MessageList(props: MessageListProps): ReactNode {
245
+ const {
246
+ messages,
247
+ messagesLoading,
248
+ activeSessionId,
249
+ streamingAssistantText,
250
+ streamingReasoning,
251
+ streamingToolCalls,
252
+ lastTurnError,
253
+ state,
254
+ sendMessage,
255
+ } = useAIHub();
256
+ const texts = resolveAIHubTexts(props.texts);
257
+ const cls = { ...DEFAULT_CLASS_NAMES, ...(props.classNames ?? {}) };
258
+ const showSystemMessages = props.showSystemMessages ?? true;
259
+ const showTpmNotice = props.showTpmNotice ?? true;
260
+ const showScrollToPresent = props.showScrollToPresentButton ?? true;
261
+ const showRetryButton = props.showRetryButton ?? true;
262
+ const showReasoning = props.showReasoning ?? true;
263
+ const showToolCalls = props.showToolCalls ?? true;
264
+ const rootClass = props.className
265
+ ? `${cls.root} ${props.className}`
266
+ : cls.root;
267
+ const affordances = chatAffordances(state);
268
+
269
+ // Block-expansion accordion (reasoning + tool-call blocks). At rest,
270
+ // at most one block in the entire conversation is user-expanded —
271
+ // clicking a second block collapses the first. During generation,
272
+ // the currently-streaming block is additionally auto-expanded, and
273
+ // the user-accordion ignores it (so up to TWO blocks can be visible:
274
+ // the auto-expanded streaming one + one user-expanded). When
275
+ // generation finishes, the streaming auto-expand falls away and the
276
+ // user's manual accordion remains.
277
+ const [userExpandedBlockId, setUserExpandedBlockId] = useState<string | null>(null);
278
+ // The currently-streaming block (reasoning while phase ==
279
+ // model_reasoning; the most recent still-running tool call while
280
+ // phase == model_tool_input or tool_executing). Null otherwise.
281
+ const currentlyStreamingBlockId = useMemo<string | null>(() => {
282
+ if (state.status !== "running") return null;
283
+ if (state.phase === "model_reasoning") return STREAMING_REASONING_BLOCK_ID;
284
+ if (state.phase === "model_tool_input" || state.phase === "tool_executing") {
285
+ // Walk newest-first; "running" = not yet finished. The translator
286
+ // computes `status === "running"` when neither output nor error is
287
+ // set yet, but that derivation happens in the streaming-bubble
288
+ // render below — here we mirror it.
289
+ for (let i = streamingToolCalls.length - 1; i >= 0; i--) {
290
+ const c = streamingToolCalls[i];
291
+ if (!c) continue;
292
+ const isRunning = c.status !== "error" && c.output == null;
293
+ if (isRunning) return toolCallBlockId("streaming", c.tool_call_id);
294
+ }
295
+ }
296
+ return null;
297
+ }, [state.status, state.phase, streamingToolCalls]);
298
+ // `expanded` resolver: auto-expand wins (it's the visible-during-stream
299
+ // contract); user-accordion is the fallback.
300
+ const isBlockExpanded = useCallback(
301
+ (blockId: string): boolean =>
302
+ blockId === currentlyStreamingBlockId || blockId === userExpandedBlockId,
303
+ [currentlyStreamingBlockId, userExpandedBlockId],
304
+ );
305
+ // Toggle: clicking the streaming-locked block is a no-op (the rule is
306
+ // "the latest in-progress block is always visible"). For every other
307
+ // block, the accordion replaces: clicking an already-user-expanded
308
+ // block collapses it to null; clicking any other expands it.
309
+ const handleToggleBlock = useCallback(
310
+ (blockId: string): void => {
311
+ if (blockId === currentlyStreamingBlockId) return;
312
+ setUserExpandedBlockId((current) => (current === blockId ? null : blockId));
313
+ },
314
+ [currentlyStreamingBlockId],
315
+ );
316
+
317
+ // Filter messages: assistant + user always, system + tool gated by
318
+ // showSystemMessages. Dedupe by id defensively even though the
319
+ // provider already does it on its own. Empty/whitespace-only messages
320
+ // are skipped — they appear when the messages-list route applies an
321
+ // empty `user_override` row (the rendered chip would otherwise be a
322
+ // tiny empty bubble), or when Letta's `<memory_metadata>`-stripped
323
+ // system message leaves nothing behind. Assistant bubbles keep
324
+ // rendering when they carry `reasoning` or `tool_calls` even with
325
+ // empty text, since those affordances are the message body.
326
+ const visibleMessages = useMemo(() => {
327
+ const seen = new Set<string>();
328
+ const out: AIHubMessage[] = [];
329
+ for (const msg of messages) {
330
+ if (seen.has(msg.id)) continue;
331
+ if ((msg.role === "system" || msg.role === "tool") && !showSystemMessages) {
332
+ continue;
333
+ }
334
+ if (isMessageRenderEmpty(msg)) continue;
335
+ seen.add(msg.id);
336
+ out.push(msg);
337
+ }
338
+ return out;
339
+ }, [messages, showSystemMessages]);
340
+
341
+ // Track whether the user is currently at-the-bottom (within
342
+ // BOTTOM_EPSILON_PX). When at-bottom, new content auto-scrolls. When
343
+ // scrolled up, auto-scroll is suppressed and the
344
+ // ScrollToPresentButton renders. Recomputed on every scroll event
345
+ // through the scroll container.
346
+ const scrollRef = useRef<HTMLDivElement | null>(null);
347
+ const [atBottom, setAtBottom] = useState(true);
348
+
349
+ const checkAtBottom = useCallback((): boolean => {
350
+ const el = scrollRef.current;
351
+ if (!el) return true;
352
+ return el.scrollHeight - el.scrollTop - el.clientHeight < BOTTOM_EPSILON_PX;
353
+ }, []);
354
+
355
+ function handleScroll(): void {
356
+ setAtBottom(checkAtBottom());
357
+ }
358
+
359
+ // Auto-scroll only when the user is at-the-bottom. On session change
360
+ // we always scroll to the bottom (loading a new session shouldn't
361
+ // strand the user mid-history).
362
+ useEffect(() => {
363
+ const el = scrollRef.current;
364
+ if (!el) return;
365
+ if (atBottom) {
366
+ el.scrollTop = el.scrollHeight;
367
+ }
368
+ }, [messages, streamingAssistantText, atBottom]);
369
+ useEffect(() => {
370
+ const el = scrollRef.current;
371
+ if (!el) return;
372
+ el.scrollTop = el.scrollHeight;
373
+ setAtBottom(true);
374
+ }, [activeSessionId]);
375
+
376
+ const handleScrollToPresent = useCallback((): void => {
377
+ const el = scrollRef.current;
378
+ if (!el) return;
379
+ el.scrollTop = el.scrollHeight;
380
+ setAtBottom(true);
381
+ }, []);
382
+
383
+ const handleRetry = useCallback((): void => {
384
+ // Find the last user message in the visible list (the failed
385
+ // turn's input). Sending it re-issues the turn.
386
+ for (let i = visibleMessages.length - 1; i >= 0; i--) {
387
+ const msg = visibleMessages[i];
388
+ if (msg && msg.role === "user") {
389
+ void sendMessage(msg.content);
390
+ return;
391
+ }
392
+ }
393
+ }, [visibleMessages, sendMessage]);
394
+
395
+ if (!activeSessionId) {
396
+ return (
397
+ <div className={cls.wrapper} data-testid="message-list-wrapper">
398
+ <div className={rootClass} data-testid="message-list">
399
+ <div className={cls.noSession} data-testid="message-list-no-session">
400
+ {texts.chat.noActiveSessionHint}
401
+ </div>
402
+ </div>
403
+ </div>
404
+ );
405
+ }
406
+
407
+ return (
408
+ <div className={cls.wrapper} data-testid="message-list-wrapper">
409
+ <div
410
+ className={rootClass}
411
+ ref={scrollRef}
412
+ onScroll={handleScroll}
413
+ data-testid="message-list"
414
+ >
415
+ <div className={cls.inner} aria-live="polite">
416
+ {showTpmNotice ? (
417
+ <TpmBanner
418
+ onRetry={handleRetry}
419
+ {...(props.texts !== undefined ? { texts: props.texts } : {})}
420
+ />
421
+ ) : null}
422
+ {messagesLoading ? (
423
+ <p className={cls.loading} data-testid="message-list-loading">
424
+ {texts.chat.sessionsLoading}
425
+ </p>
426
+ ) : null}
427
+ {!messagesLoading &&
428
+ visibleMessages.length === 0 &&
429
+ streamingAssistantText === null &&
430
+ lastTurnError === null ? (
431
+ <p className={cls.empty} data-testid="message-list-empty">
432
+ {texts.chat.emptyConversationHint}
433
+ </p>
434
+ ) : null}
435
+ {visibleMessages.map((msg) =>
436
+ props.renderMessageBubble ? (
437
+ // The Tier-3 escape hatch returns arbitrary content; we
438
+ // wrap with a stable React key.
439
+ <div key={msg.id} data-testid="message-bubble-custom">
440
+ {props.renderMessageBubble(msg) as ReactNode}
441
+ </div>
442
+ ) : (
443
+ <MessageBubble
444
+ key={msg.id}
445
+ message={msg}
446
+ classNames={cls}
447
+ isFailedAssistant={
448
+ state.status === "failed" &&
449
+ msg.role === "assistant" &&
450
+ isLastAssistant(visibleMessages, msg)
451
+ }
452
+ isCancelledAssistant={
453
+ state.status === "cancelled" &&
454
+ msg.role === "assistant" &&
455
+ isLastAssistant(visibleMessages, msg)
456
+ }
457
+ showRetryButton={showRetryButton}
458
+ showReasoning={showReasoning}
459
+ showToolCalls={showToolCalls}
460
+ retryLabel={texts.chat.retryFailedButton}
461
+ failedLabel={texts.chat.streamFailedText}
462
+ cancelledStampLabel={texts.chat.cancelledStampText}
463
+ onRetry={handleRetry}
464
+ isBlockExpanded={isBlockExpanded}
465
+ onToggleBlock={handleToggleBlock}
466
+ {...(props.texts !== undefined ? { texts: props.texts } : {})}
467
+ {...(props.renderMessageActions !== undefined
468
+ ? { renderMessageActions: props.renderMessageActions }
469
+ : {})}
470
+ />
471
+ ),
472
+ )}
473
+ {lastTurnError !== null && state.status === "failed" ? (
474
+ <div
475
+ className={`${cls.bubble} ${cls.bubbleAssistant} ${cls.bubbleFailed}`}
476
+ data-testid="message-bubble-failed-error"
477
+ data-role="assistant"
478
+ >
479
+ <span className={cls.bubbleBody}>
480
+ {texts.chat.streamFailedText}
481
+ </span>
482
+ {showRetryButton ? (
483
+ <button
484
+ type="button"
485
+ className={cls.bubbleRetryButton}
486
+ onClick={handleRetry}
487
+ data-testid="message-list-retry"
488
+ >
489
+ {texts.chat.retryFailedButton}
490
+ </button>
491
+ ) : null}
492
+ </div>
493
+ ) : lastTurnError !== null ? (
494
+ <div
495
+ className={cls.errorBanner}
496
+ data-testid="message-list-error"
497
+ role="alert"
498
+ >
499
+ {lastTurnError}
500
+ </div>
501
+ ) : null}
502
+ {streamingAssistantText !== null ||
503
+ (streamingReasoning !== null && showReasoning) ||
504
+ (streamingToolCalls.length > 0 && showToolCalls) ? (
505
+ <div
506
+ className={`${cls.bubble} ${cls.bubbleAssistant}`}
507
+ data-testid="message-bubble"
508
+ data-role="assistant"
509
+ data-streaming="true"
510
+ >
511
+ {streamingReasoning !== null && showReasoning ? (
512
+ <ReasoningView
513
+ content={streamingReasoning}
514
+ streaming={state.phase === "model_reasoning"}
515
+ expanded={isBlockExpanded(STREAMING_REASONING_BLOCK_ID)}
516
+ onToggle={() => handleToggleBlock(STREAMING_REASONING_BLOCK_ID)}
517
+ {...(props.texts !== undefined ? { texts: props.texts } : {})}
518
+ />
519
+ ) : null}
520
+ {showToolCalls
521
+ ? streamingToolCalls.map((call) => {
522
+ const blockId = toolCallBlockId("streaming", call.tool_call_id);
523
+ return (
524
+ <ToolCallView
525
+ key={call.tool_call_id}
526
+ toolName={call.tool_name}
527
+ status={
528
+ call.status === "error"
529
+ ? "error"
530
+ : call.output != null
531
+ ? "done"
532
+ : "running"
533
+ }
534
+ input={call.input}
535
+ expanded={isBlockExpanded(blockId)}
536
+ onToggle={() => handleToggleBlock(blockId)}
537
+ {...(call.output != null ? { output: call.output } : {})}
538
+ {...(call.error != null ? { error: call.error } : {})}
539
+ {...(props.texts !== undefined ? { texts: props.texts } : {})}
540
+ />
541
+ );
542
+ })
543
+ : null}
544
+ {streamingAssistantText !== null ? (
545
+ <span className={cls.bubbleBody}>
546
+ <MarkdownRenderer
547
+ content={streamingAssistantText.replace(/^\s+|\s+$/g, "")}
548
+ />
549
+ {affordances.streamingDots ? (
550
+ <span className={cls.bubbleCaret} aria-hidden="true" />
551
+ ) : null}
552
+ </span>
553
+ ) : null}
554
+ </div>
555
+ ) : null}
556
+ </div>
557
+ </div>
558
+ {showScrollToPresent ? (
559
+ <ScrollToPresentButton
560
+ visible={affordances.scrollToPresentVisible && !atBottom}
561
+ onClick={handleScrollToPresent}
562
+ {...(props.texts !== undefined ? { texts: props.texts } : {})}
563
+ />
564
+ ) : null}
565
+ </div>
566
+ );
567
+ }
568
+
569
+ /**
570
+ * Returns true when `msg` is the last assistant-role entry in `list`.
571
+ * Used to scope the failed/cancelled modifiers to the latest assistant
572
+ * turn only (mid-history assistant messages are not retroactively
573
+ * failed when a *new* turn fails).
574
+ */
575
+ function isLastAssistant(
576
+ list: ReadonlyArray<AIHubMessage>,
577
+ msg: AIHubMessage,
578
+ ): boolean {
579
+ for (let i = list.length - 1; i >= 0; i--) {
580
+ const item = list[i];
581
+ if (!item) continue;
582
+ if (item.role !== "assistant") continue;
583
+ return item.id === msg.id;
584
+ }
585
+ return false;
586
+ }
587
+
588
+ /**
589
+ * True when `msg` has nothing meaningful to render — its `content` is
590
+ * empty or whitespace-only AND it carries no `reasoning` text or
591
+ * `tool_calls`. Used by the visibility filter so messages overridden to
592
+ * empty (e.g. an empty `user_override` row applied to the persisted
593
+ * `system_message`, or Letta's `<memory_metadata>`-stripped system row
594
+ * after the strip leaves the slot empty) don't render as tiny empty
595
+ * bubbles.
596
+ *
597
+ * Assistant bubbles with `reasoning` or `tool_calls` are NEVER empty for
598
+ * this purpose — those affordances are the body. A regular text-only
599
+ * assistant message with whitespace content is still hidden.
600
+ */
601
+ function isMessageRenderEmpty(msg: AIHubMessage): boolean {
602
+ const text = (msg.content ?? "").trim();
603
+ if (text !== "") return false;
604
+ if (msg.role === "assistant") {
605
+ if (msg.reasoning && msg.reasoning.trim() !== "") return false;
606
+ if (msg.tool_calls && msg.tool_calls.length > 0) return false;
607
+ }
608
+ return true;
609
+ }
610
+
611
+ /**
612
+ * Single message bubble. Variant modifier is driven by `message.role`,
613
+ * with `--failed` / `--cancelled` overrides for the most-recent
614
+ * assistant bubble of the corresponding terminal status.
615
+ *
616
+ * For assistant bubbles, renders nested `<ReasoningView />` (when the
617
+ * message carries `reasoning`) and `<ToolCallView />` cards (one per
618
+ * `tool_calls` entry) before the visible text — matching the
619
+ * ui-sketches/03-chat.md layout.
620
+ */
621
+ function MessageBubble(props: {
622
+ message: AIHubMessage;
623
+ classNames: MessageListClassNames;
624
+ isFailedAssistant: boolean;
625
+ isCancelledAssistant: boolean;
626
+ showRetryButton: boolean;
627
+ showReasoning: boolean;
628
+ showToolCalls: boolean;
629
+ retryLabel: string;
630
+ failedLabel: string;
631
+ cancelledStampLabel: string;
632
+ onRetry: () => void;
633
+ /**
634
+ * Resolver for the accordion + auto-expand state owned by
635
+ * `<MessageList />`. Returns whether a given block (`reasoning:<id>`
636
+ * or `tool:<tool_call_id>`) should render expanded.
637
+ */
638
+ isBlockExpanded: (blockId: string) => boolean;
639
+ /** Toggle handler for the accordion. */
640
+ onToggleBlock: (blockId: string) => void;
641
+ texts?: DeepPartial<AIHubTexts>;
642
+ renderMessageActions?: AIHubMessageActionRenderer;
643
+ }): ReactNode {
644
+ const { message, classNames: cls } = props;
645
+ const baseModifier =
646
+ message.role === "user"
647
+ ? cls.bubbleUser
648
+ : message.role === "assistant"
649
+ ? cls.bubbleAssistant
650
+ : message.role === "system"
651
+ ? cls.bubbleSystem
652
+ : cls.bubbleTool;
653
+ const stateModifier = props.isFailedAssistant
654
+ ? cls.bubbleFailed
655
+ : props.isCancelledAssistant
656
+ ? cls.bubbleCancelled
657
+ : null;
658
+ const klass = [cls.bubble, baseModifier, stateModifier]
659
+ .filter(Boolean)
660
+ .join(" ");
661
+
662
+ const actions =
663
+ props.renderMessageActions && message.role === "assistant"
664
+ ? (props.renderMessageActions(message) as ReactNode)
665
+ : null;
666
+
667
+ const isAssistant = message.role === "assistant";
668
+ const reasoning = isAssistant && props.showReasoning ? message.reasoning : null;
669
+ const toolCalls = isAssistant && props.showToolCalls ? message.tool_calls : null;
670
+
671
+ return (
672
+ <div
673
+ className={klass}
674
+ data-testid="message-bubble"
675
+ data-role={message.role}
676
+ data-message-id={message.id}
677
+ data-failed={props.isFailedAssistant ? "true" : "false"}
678
+ data-cancelled={props.isCancelledAssistant ? "true" : "false"}
679
+ >
680
+ {reasoning ? (
681
+ (() => {
682
+ const blockId = reasoningBlockId(message.id);
683
+ return (
684
+ <ReasoningView
685
+ content={reasoning}
686
+ expanded={props.isBlockExpanded(blockId)}
687
+ onToggle={() => props.onToggleBlock(blockId)}
688
+ {...(props.texts !== undefined ? { texts: props.texts } : {})}
689
+ />
690
+ );
691
+ })()
692
+ ) : null}
693
+ {toolCalls?.map((call) => {
694
+ const blockId = toolCallBlockId(message.id, call.tool_call_id);
695
+ return (
696
+ <ToolCallView
697
+ key={call.tool_call_id}
698
+ toolName={call.tool_name}
699
+ status={call.status === "error" ? "error" : "done"}
700
+ input={call.input}
701
+ expanded={props.isBlockExpanded(blockId)}
702
+ onToggle={() => props.onToggleBlock(blockId)}
703
+ {...(call.output != null ? { output: call.output } : {})}
704
+ {...(call.error != null ? { error: call.error } : {})}
705
+ {...(props.texts !== undefined ? { texts: props.texts } : {})}
706
+ />
707
+ );
708
+ })}
709
+ <span className={cls.bubbleBody}>
710
+ {isAssistant ? (
711
+ <MarkdownRenderer content={message.content} />
712
+ ) : (
713
+ message.content.trim()
714
+ )}
715
+ </span>
716
+ {isAssistant && message.usage ? (
717
+ <CacheBadge usage={message.usage} />
718
+ ) : null}
719
+ {props.isFailedAssistant ? (
720
+ <div className={cls.bubbleActions}>
721
+ <span>{props.failedLabel}</span>
722
+ {props.showRetryButton ? (
723
+ <button
724
+ type="button"
725
+ className={cls.bubbleRetryButton}
726
+ onClick={props.onRetry}
727
+ data-testid="message-bubble-retry"
728
+ >
729
+ {props.retryLabel}
730
+ </button>
731
+ ) : null}
732
+ </div>
733
+ ) : props.isCancelledAssistant ? (
734
+ <div className={cls.bubbleActions}>
735
+ <span
736
+ className={cls.bubbleCancelledStamp}
737
+ data-testid="message-bubble-cancelled-stamp"
738
+ >
739
+ {props.cancelledStampLabel}
740
+ </span>
741
+ </div>
742
+ ) : null}
743
+ {actions ? (
744
+ <div
745
+ className={cls.bubbleActions}
746
+ data-testid="message-bubble-actions"
747
+ >
748
+ {actions}
749
+ </div>
750
+ ) : null}
751
+ </div>
752
+ );
753
+ }