@vertesia/ui 1.6.0-dev.20260918.090710Z → 1.6.0-dev.20260923.085325Z

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/lib/boot/branding.d.ts +13 -0
  2. package/lib/boot/branding.d.ts.map +1 -1
  3. package/lib/boot/branding.js +7 -0
  4. package/lib/boot/branding.js.map +1 -1
  5. package/lib/boot/index.d.ts +1 -1
  6. package/lib/boot/index.d.ts.map +1 -1
  7. package/lib/boot/index.js +5 -23
  8. package/lib/boot/index.js.map +1 -1
  9. package/lib/boot/loading.d.ts +4 -0
  10. package/lib/boot/loading.d.ts.map +1 -0
  11. package/lib/boot/loading.js +28 -0
  12. package/lib/boot/loading.js.map +1 -0
  13. package/lib/core/components/SelectList.d.ts.map +1 -1
  14. package/lib/core/components/SelectList.js +3 -3
  15. package/lib/core/components/SelectList.js.map +1 -1
  16. package/lib/core/components/SidePanel.d.ts.map +1 -1
  17. package/lib/core/components/SidePanel.js +2 -1
  18. package/lib/core/components/SidePanel.js.map +1 -1
  19. package/lib/core/components/shadcn/theme/ThemeProvider.d.ts.map +1 -1
  20. package/lib/core/components/shadcn/theme/ThemeProvider.js +21 -5
  21. package/lib/core/components/shadcn/theme/ThemeProvider.js.map +1 -1
  22. package/lib/env/index.d.ts +11 -0
  23. package/lib/env/index.d.ts.map +1 -1
  24. package/lib/env/index.js +16 -3
  25. package/lib/env/index.js.map +1 -1
  26. package/lib/features/agent/chat/ModernAgentConversation.js +4 -1
  27. package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
  28. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts +5 -1
  29. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts.map +1 -1
  30. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js +9 -2
  31. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js.map +1 -1
  32. package/lib/features/magic-pdf/GroundedExtractionView.js +3 -3
  33. package/lib/features/magic-pdf/GroundedExtractionView.js.map +1 -1
  34. package/lib/features/media-viewer/AudioPanel.d.ts +10 -2
  35. package/lib/features/media-viewer/AudioPanel.d.ts.map +1 -1
  36. package/lib/features/media-viewer/AudioPanel.js +78 -13
  37. package/lib/features/media-viewer/AudioPanel.js.map +1 -1
  38. package/lib/features/oauth/McpConnectionsButton.d.ts +2 -2
  39. package/lib/features/oauth/McpConnectionsButton.d.ts.map +1 -1
  40. package/lib/features/oauth/McpConnectionsButton.js +9 -7
  41. package/lib/features/oauth/McpConnectionsButton.js.map +1 -1
  42. package/lib/features/oauth/McpConnectionsDialog.d.ts.map +1 -1
  43. package/lib/features/oauth/McpConnectionsDialog.js +7 -3
  44. package/lib/features/oauth/McpConnectionsDialog.js.map +1 -1
  45. package/lib/features/oauth/useMcpConnections.d.ts +4 -2
  46. package/lib/features/oauth/useMcpConnections.d.ts.map +1 -1
  47. package/lib/features/oauth/useMcpConnections.js +15 -9
  48. package/lib/features/oauth/useMcpConnections.js.map +1 -1
  49. package/lib/features/store/objects/selection/ObjectsActionContext.d.ts.map +1 -1
  50. package/lib/features/store/objects/selection/ObjectsActionContext.js +2 -0
  51. package/lib/features/store/objects/selection/ObjectsActionContext.js.map +1 -1
  52. package/lib/features/store/objects/selection/SelectionActions.d.ts +4 -2
  53. package/lib/features/store/objects/selection/SelectionActions.d.ts.map +1 -1
  54. package/lib/features/store/objects/selection/SelectionActions.js +6 -3
  55. package/lib/features/store/objects/selection/SelectionActions.js.map +1 -1
  56. package/lib/features/store/objects/selection/actions/StartWorkflowAction.d.ts +4 -0
  57. package/lib/features/store/objects/selection/actions/StartWorkflowAction.d.ts.map +1 -0
  58. package/lib/features/store/objects/selection/actions/StartWorkflowAction.js +81 -0
  59. package/lib/features/store/objects/selection/actions/StartWorkflowAction.js.map +1 -0
  60. package/lib/features/store/objects/selection/actions/index.d.ts +1 -0
  61. package/lib/features/store/objects/selection/actions/index.d.ts.map +1 -1
  62. package/lib/features/store/objects/selection/actions/index.js +1 -0
  63. package/lib/features/store/objects/selection/actions/index.js.map +1 -1
  64. package/lib/features/store/types/TypeRegistry.d.ts +345 -155
  65. package/lib/features/store/types/TypeRegistry.d.ts.map +1 -1
  66. package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts +345 -155
  67. package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts.map +1 -1
  68. package/lib/i18n/locales/ar.json +10 -2
  69. package/lib/i18n/locales/de.json +6 -2
  70. package/lib/i18n/locales/en.json +6 -2
  71. package/lib/i18n/locales/es.json +6 -2
  72. package/lib/i18n/locales/fr.json +7 -2
  73. package/lib/i18n/locales/it.json +6 -2
  74. package/lib/i18n/locales/ja.json +6 -2
  75. package/lib/i18n/locales/ko.json +6 -2
  76. package/lib/i18n/locales/pt.json +6 -2
  77. package/lib/i18n/locales/ru.json +8 -2
  78. package/lib/i18n/locales/tr.json +6 -2
  79. package/lib/i18n/locales/zh-TW.json +6 -2
  80. package/lib/i18n/locales/zh.json +6 -2
  81. package/lib/session/UserSession.d.ts +3 -0
  82. package/lib/session/UserSession.d.ts.map +1 -1
  83. package/lib/session/UserSession.js +18 -8
  84. package/lib/session/UserSession.js.map +1 -1
  85. package/lib/session/UserSessionProvider.d.ts.map +1 -1
  86. package/lib/session/UserSessionProvider.js +34 -24
  87. package/lib/session/UserSessionProvider.js.map +1 -1
  88. package/lib/session/auth/authRoundTrip.d.ts +8 -0
  89. package/lib/session/auth/authRoundTrip.d.ts.map +1 -0
  90. package/lib/session/auth/authRoundTrip.js +22 -0
  91. package/lib/session/auth/authRoundTrip.js.map +1 -0
  92. package/lib/session/auth/composable.d.ts.map +1 -1
  93. package/lib/session/auth/composable.js +9 -7
  94. package/lib/session/auth/composable.js.map +1 -1
  95. package/lib/session/auth/domainRouting.d.ts +13 -6
  96. package/lib/session/auth/domainRouting.d.ts.map +1 -1
  97. package/lib/session/auth/domainRouting.js +2 -21
  98. package/lib/session/auth/domainRouting.js.map +1 -1
  99. package/lib/session/auth/oauth.d.ts +6 -1
  100. package/lib/session/auth/oauth.d.ts.map +1 -1
  101. package/lib/session/auth/oauth.js +145 -8
  102. package/lib/session/auth/oauth.js.map +1 -1
  103. package/lib/session/auth/tokenIssuer.d.ts +3 -0
  104. package/lib/session/auth/tokenIssuer.d.ts.map +1 -0
  105. package/lib/session/auth/tokenIssuer.js +27 -0
  106. package/lib/session/auth/tokenIssuer.js.map +1 -0
  107. package/lib/session/auth/vertesiaHosts.d.ts +47 -0
  108. package/lib/session/auth/vertesiaHosts.d.ts.map +1 -0
  109. package/lib/session/auth/vertesiaHosts.js +79 -0
  110. package/lib/session/auth/vertesiaHosts.js.map +1 -0
  111. package/lib/session/index.d.ts +1 -0
  112. package/lib/session/index.d.ts.map +1 -1
  113. package/lib/session/index.js +1 -0
  114. package/lib/session/index.js.map +1 -1
  115. package/lib/shell/AuthScreenPreview.d.ts +1 -1
  116. package/lib/shell/AuthScreenPreview.d.ts.map +1 -1
  117. package/lib/shell/AuthScreenPreview.js +2 -5
  118. package/lib/shell/AuthScreenPreview.js.map +1 -1
  119. package/lib/shell/BrandedAuthScreens.d.ts +1 -4
  120. package/lib/shell/BrandedAuthScreens.d.ts.map +1 -1
  121. package/lib/shell/BrandedAuthScreens.js +5 -26
  122. package/lib/shell/BrandedAuthScreens.js.map +1 -1
  123. package/lib/shell/BrandedLoadingIndicator.d.ts +11 -0
  124. package/lib/shell/BrandedLoadingIndicator.d.ts.map +1 -0
  125. package/lib/shell/BrandedLoadingIndicator.js +27 -0
  126. package/lib/shell/BrandedLoadingIndicator.js.map +1 -0
  127. package/lib/shell/SplashScreen.d.ts.map +1 -1
  128. package/lib/shell/SplashScreen.js +2 -1
  129. package/lib/shell/SplashScreen.js.map +1 -1
  130. package/lib/shell/apps/AppInstallationProvider.d.ts +16 -4
  131. package/lib/shell/apps/AppInstallationProvider.d.ts.map +1 -1
  132. package/lib/shell/apps/iframe-auth.d.ts +2 -0
  133. package/lib/shell/apps/iframe-auth.d.ts.map +1 -1
  134. package/lib/shell/apps/iframe-auth.js +6 -2
  135. package/lib/shell/apps/iframe-auth.js.map +1 -1
  136. package/lib/shell/index.d.ts +1 -0
  137. package/lib/shell/index.d.ts.map +1 -1
  138. package/lib/shell/index.js +1 -0
  139. package/lib/shell/index.js.map +1 -1
  140. package/lib/shell/login/SignInAuthPending.js +2 -2
  141. package/lib/shell/login/SignInAuthPending.js.map +1 -1
  142. package/lib/shell/login/SignInEmailStep.d.ts.map +1 -1
  143. package/lib/shell/login/SignInEmailStep.js +2 -2
  144. package/lib/shell/login/SignInEmailStep.js.map +1 -1
  145. package/lib/shell/login/TerminalLogin.d.ts.map +1 -1
  146. package/lib/shell/login/TerminalLogin.js +28 -21
  147. package/lib/shell/login/TerminalLogin.js.map +1 -1
  148. package/lib/vertesia-ui-boot.js +33 -29
  149. package/lib/vertesia-ui-boot.js.map +1 -1
  150. package/lib/vertesia-ui-core.js +34 -34
  151. package/lib/vertesia-ui-core.js.map +1 -1
  152. package/lib/vertesia-ui-env.js +1 -1
  153. package/lib/vertesia-ui-env.js.map +1 -1
  154. package/lib/vertesia-ui-features.js +163 -163
  155. package/lib/vertesia-ui-features.js.map +1 -1
  156. package/lib/vertesia-ui-i18n.Bl8KeZUK.chunk.js +5 -0
  157. package/lib/vertesia-ui-i18n.Bl8KeZUK.chunk.js.map +1 -0
  158. package/lib/vertesia-ui-i18n.BmT06XFI.chunk.js +5 -0
  159. package/lib/vertesia-ui-i18n.BmT06XFI.chunk.js.map +1 -0
  160. package/lib/vertesia-ui-i18n.Ch-Jjj0L.chunk.js +5 -0
  161. package/lib/vertesia-ui-i18n.Ch-Jjj0L.chunk.js.map +1 -0
  162. package/lib/vertesia-ui-i18n.CjhloW9s.chunk.js +5 -0
  163. package/lib/vertesia-ui-i18n.CjhloW9s.chunk.js.map +1 -0
  164. package/lib/vertesia-ui-i18n.CkfHdQzY.chunk.js +5 -0
  165. package/lib/vertesia-ui-i18n.CkfHdQzY.chunk.js.map +1 -0
  166. package/lib/vertesia-ui-i18n.DFuKfTe1.chunk.js +5 -0
  167. package/lib/vertesia-ui-i18n.DFuKfTe1.chunk.js.map +1 -0
  168. package/lib/vertesia-ui-i18n.DHqNVekZ.chunk.js +5 -0
  169. package/lib/vertesia-ui-i18n.DHqNVekZ.chunk.js.map +1 -0
  170. package/lib/vertesia-ui-i18n.DOaKbmaj.chunk.js +5 -0
  171. package/lib/vertesia-ui-i18n.DOaKbmaj.chunk.js.map +1 -0
  172. package/lib/vertesia-ui-i18n.Dge3Gl-h.chunk.js +5 -0
  173. package/lib/vertesia-ui-i18n.Dge3Gl-h.chunk.js.map +1 -0
  174. package/lib/vertesia-ui-i18n.GLt-10Lx.chunk.js +5 -0
  175. package/lib/vertesia-ui-i18n.GLt-10Lx.chunk.js.map +1 -0
  176. package/lib/vertesia-ui-i18n.T-XtAGbw.chunk.js +5 -0
  177. package/lib/vertesia-ui-i18n.T-XtAGbw.chunk.js.map +1 -0
  178. package/lib/vertesia-ui-i18n.VOJyTpMM.chunk.js +5 -0
  179. package/lib/vertesia-ui-i18n.VOJyTpMM.chunk.js.map +1 -0
  180. package/lib/vertesia-ui-i18n.js +3 -3
  181. package/lib/vertesia-ui-i18n.js.map +1 -1
  182. package/lib/vertesia-ui-layout.js +15 -15
  183. package/lib/vertesia-ui-layout.js.map +1 -1
  184. package/lib/vertesia-ui-rich-text.js +44 -44
  185. package/lib/vertesia-ui-rich-text.js.map +1 -1
  186. package/lib/vertesia-ui-session.js +1 -1
  187. package/lib/vertesia-ui-session.js.map +1 -1
  188. package/lib/vertesia-ui-shell.js +72 -48
  189. package/lib/vertesia-ui-shell.js.map +1 -1
  190. package/lib/vertesia-ui-widgets.js +51 -51
  191. package/lib/vertesia-ui-widgets.js.map +1 -1
  192. package/package.json +19 -19
  193. package/src/boot/branding.ts +8 -0
  194. package/src/boot/index.ts +6 -23
  195. package/src/boot/loading.ts +27 -0
  196. package/src/core/components/SelectList.tsx +5 -2
  197. package/src/core/components/SidePanel.tsx +65 -60
  198. package/src/core/components/shadcn/theme/ThemeProvider.test.tsx +101 -0
  199. package/src/core/components/shadcn/theme/ThemeProvider.tsx +21 -9
  200. package/src/env/index.test.ts +21 -0
  201. package/src/env/index.ts +33 -2
  202. package/src/features/agent/chat/ModernAgentConversation.test.tsx +81 -0
  203. package/src/features/agent/chat/ModernAgentConversation.tsx +5 -0
  204. package/src/features/agent/chat/ModernAgentOutput/MessageInput.tsx +17 -1
  205. package/src/features/magic-pdf/GroundedExtractionView.tsx +3 -3
  206. package/src/features/media-viewer/AudioPanel.test.ts +84 -0
  207. package/src/features/media-viewer/AudioPanel.tsx +97 -14
  208. package/src/features/oauth/McpConnectionsButton.test.tsx +56 -0
  209. package/src/features/oauth/McpConnectionsButton.tsx +9 -6
  210. package/src/features/oauth/McpConnectionsDialog.tsx +14 -5
  211. package/src/features/oauth/useMcpConnections.test.ts +10 -0
  212. package/src/features/oauth/useMcpConnections.ts +18 -10
  213. package/src/features/store/objects/selection/ObjectsActionContext.tsx +2 -0
  214. package/src/features/store/objects/selection/SelectionActions.tsx +13 -6
  215. package/src/features/store/objects/selection/actions/StartWorkflowAction.tsx +162 -0
  216. package/src/features/store/objects/selection/actions/index.ts +1 -0
  217. package/src/i18n/locales/ar.json +10 -2
  218. package/src/i18n/locales/de.json +6 -2
  219. package/src/i18n/locales/en.json +6 -2
  220. package/src/i18n/locales/es.json +6 -2
  221. package/src/i18n/locales/fr.json +7 -2
  222. package/src/i18n/locales/it.json +6 -2
  223. package/src/i18n/locales/ja.json +6 -2
  224. package/src/i18n/locales/ko.json +6 -2
  225. package/src/i18n/locales/pt.json +6 -2
  226. package/src/i18n/locales/ru.json +8 -2
  227. package/src/i18n/locales/tr.json +6 -2
  228. package/src/i18n/locales/zh-TW.json +6 -2
  229. package/src/i18n/locales/zh.json +6 -2
  230. package/src/session/UserSession.test.ts +48 -3
  231. package/src/session/UserSession.ts +20 -8
  232. package/src/session/UserSessionProvider.handoff.test.tsx +103 -0
  233. package/src/session/UserSessionProvider.iframe.test.tsx +178 -0
  234. package/src/session/UserSessionProvider.state-mismatch.test.tsx +43 -0
  235. package/src/session/UserSessionProvider.tsx +35 -23
  236. package/src/session/auth/auth-flow.md +18 -5
  237. package/src/session/auth/authRoundTrip.ts +24 -0
  238. package/src/session/auth/composable.test.ts +23 -1
  239. package/src/session/auth/composable.ts +10 -8
  240. package/src/session/auth/domainRouting.ts +4 -25
  241. package/src/session/auth/oauth.test.ts +121 -4
  242. package/src/session/auth/oauth.ts +172 -11
  243. package/src/session/auth/tokenIssuer.test.ts +25 -0
  244. package/src/session/auth/tokenIssuer.ts +25 -0
  245. package/src/session/auth/vertesiaHosts.test.ts +26 -0
  246. package/src/session/auth/vertesiaHosts.ts +85 -0
  247. package/src/session/index.ts +1 -0
  248. package/src/shell/AuthScreenPreview.tsx +2 -6
  249. package/src/shell/BrandedAuthScreens.test.tsx +26 -0
  250. package/src/shell/BrandedAuthScreens.tsx +4 -40
  251. package/src/shell/BrandedLoadingIndicator.tsx +42 -0
  252. package/src/shell/SplashScreen.tsx +4 -4
  253. package/src/shell/apps/iframe-auth.test.ts +8 -11
  254. package/src/shell/apps/iframe-auth.ts +10 -1
  255. package/src/shell/index.tsx +1 -0
  256. package/src/shell/login/SignInAuthPending.tsx +2 -2
  257. package/src/shell/login/SignInEmailStep.tsx +4 -2
  258. package/src/shell/login/TerminalLogin.test.tsx +111 -0
  259. package/src/shell/login/TerminalLogin.tsx +61 -37
  260. package/lib/features/store/objects/selection/actions/StartWorkflowButton.d.ts +0 -7
  261. package/lib/features/store/objects/selection/actions/StartWorkflowButton.d.ts.map +0 -1
  262. package/lib/features/store/objects/selection/actions/StartWorkflowButton.js +0 -51
  263. package/lib/features/store/objects/selection/actions/StartWorkflowButton.js.map +0 -1
  264. package/lib/vertesia-ui-i18n.B3NFclwl.chunk.js +0 -5
  265. package/lib/vertesia-ui-i18n.B3NFclwl.chunk.js.map +0 -1
  266. package/lib/vertesia-ui-i18n.BWSrNIxc.chunk.js +0 -5
  267. package/lib/vertesia-ui-i18n.BWSrNIxc.chunk.js.map +0 -1
  268. package/lib/vertesia-ui-i18n.BWvWeB1p.chunk.js +0 -5
  269. package/lib/vertesia-ui-i18n.BWvWeB1p.chunk.js.map +0 -1
  270. package/lib/vertesia-ui-i18n.BZCXvTC1.chunk.js +0 -5
  271. package/lib/vertesia-ui-i18n.BZCXvTC1.chunk.js.map +0 -1
  272. package/lib/vertesia-ui-i18n.BbJMvFZW.chunk.js +0 -5
  273. package/lib/vertesia-ui-i18n.BbJMvFZW.chunk.js.map +0 -1
  274. package/lib/vertesia-ui-i18n.CE5ldwpt.chunk.js +0 -5
  275. package/lib/vertesia-ui-i18n.CE5ldwpt.chunk.js.map +0 -1
  276. package/lib/vertesia-ui-i18n.CMC0IShY.chunk.js +0 -5
  277. package/lib/vertesia-ui-i18n.CMC0IShY.chunk.js.map +0 -1
  278. package/lib/vertesia-ui-i18n.CkcRqBKU.chunk.js +0 -5
  279. package/lib/vertesia-ui-i18n.CkcRqBKU.chunk.js.map +0 -1
  280. package/lib/vertesia-ui-i18n.D-Mq1sw_.chunk.js +0 -5
  281. package/lib/vertesia-ui-i18n.D-Mq1sw_.chunk.js.map +0 -1
  282. package/lib/vertesia-ui-i18n.DkK68-A-.chunk.js +0 -5
  283. package/lib/vertesia-ui-i18n.DkK68-A-.chunk.js.map +0 -1
  284. package/lib/vertesia-ui-i18n.XE_dcCg6.chunk.js +0 -5
  285. package/lib/vertesia-ui-i18n.XE_dcCg6.chunk.js.map +0 -1
  286. package/lib/vertesia-ui-i18n.qgNqMPvK.chunk.js +0 -5
  287. package/lib/vertesia-ui-i18n.qgNqMPvK.chunk.js.map +0 -1
  288. package/src/features/store/objects/selection/actions/StartWorkflowButton.tsx +0 -104
@@ -61,6 +61,8 @@ vi.mock('./ModernAgentOutput/Header', () => ({
61
61
  vi.mock('./ModernAgentOutput/MessageInput', () => ({
62
62
  default: (props: {
63
63
  onSend: (message: string) => void;
64
+ value?: string;
65
+ onValueChange?: (value: string) => void;
64
66
  activeTaskCount?: number;
65
67
  activeWorkstreams?: Array<{ workstream_id: string; status: string }>;
66
68
  onSelectWorkstream?: (workstreamId: string) => void;
@@ -81,6 +83,11 @@ vi.mock('./ModernAgentOutput/MessageInput', () => ({
81
83
  return (
82
84
  <div>
83
85
  {props.approvalModeSlot}
86
+ <textarea
87
+ aria-label="composer draft"
88
+ value={props.value}
89
+ onChange={(event) => props.onValueChange?.(event.target.value)}
90
+ />
84
91
  <button type="button" onClick={() => props.onSend('follow up')}>
85
92
  composer send
86
93
  </button>
@@ -1706,6 +1713,80 @@ describe('ModernAgentConversation send handling', () => {
1706
1713
  );
1707
1714
  });
1708
1715
 
1716
+ it('restores the composer draft after an option response without sending it as part of that response', async () => {
1717
+ const requestMessage = {
1718
+ ...createMessage(AgentMessageType.REQUEST_INPUT, 'What is your favorite color?'),
1719
+ details: {
1720
+ request_id: 'ask-user-1',
1721
+ ux: {
1722
+ options: [
1723
+ { id: 'red', label: 'Red' },
1724
+ { id: 'blue', label: 'Blue' },
1725
+ ],
1726
+ },
1727
+ },
1728
+ };
1729
+ mockStreamState({ messages: [], isCompleted: false, agentRunStatus: 'RUNNING' });
1730
+
1731
+ const view = renderConversation({ hideMessageInput: false });
1732
+ fireEvent.change(screen.getByRole('textbox', { name: 'composer draft' }), {
1733
+ target: { value: 'My pending message' },
1734
+ });
1735
+
1736
+ mockStreamState({ messages: [requestMessage], isCompleted: false, agentRunStatus: 'RUNNING' });
1737
+ view.rerender(
1738
+ <ModernAgentConversation
1739
+ agentRunId="agent-run-1"
1740
+ title="Agent"
1741
+ hideHeader
1742
+ hideMessageInput={false}
1743
+ showRightPanel={false}
1744
+ />,
1745
+ );
1746
+
1747
+ await act(async () => {
1748
+ fireEvent.click(screen.getByRole('button', { name: 'Blue' }));
1749
+ await Promise.resolve();
1750
+ });
1751
+ await waitFor(() => {
1752
+ expect(mocks.sendSignal).toHaveBeenCalledWith(
1753
+ 'agent-run-1',
1754
+ 'UserInput',
1755
+ expect.objectContaining({ message: 'blue' }),
1756
+ );
1757
+ });
1758
+
1759
+ mockStreamState({
1760
+ messages: [
1761
+ requestMessage,
1762
+ {
1763
+ ...createMessage(AgentMessageType.QUESTION, 'blue'),
1764
+ details: { request_input_response: { request_id: 'ask-user-1' } },
1765
+ },
1766
+ ],
1767
+ isCompleted: false,
1768
+ agentRunStatus: 'RUNNING',
1769
+ });
1770
+ view.rerender(
1771
+ <ModernAgentConversation
1772
+ agentRunId="agent-run-1"
1773
+ title="Agent"
1774
+ hideHeader
1775
+ hideMessageInput={false}
1776
+ showRightPanel={false}
1777
+ />,
1778
+ );
1779
+
1780
+ expect((screen.getByRole('textbox', { name: 'composer draft' }) as HTMLTextAreaElement).value).toBe(
1781
+ 'My pending message',
1782
+ );
1783
+ expect(mocks.sendSignal).not.toHaveBeenCalledWith(
1784
+ 'agent-run-1',
1785
+ 'UserInput',
1786
+ expect.objectContaining({ message: 'My pending message' }),
1787
+ );
1788
+ });
1789
+
1709
1790
  it('does not resurrect an answered request overlay after remounting before the persisted echo arrives', async () => {
1710
1791
  const requestMessage = {
1711
1792
  ...createMessage(AgentMessageType.REQUEST_INPUT, 'What is your favorite color?'),
@@ -1586,6 +1586,9 @@ function ModernAgentConversationInner({
1586
1586
  const conversationRef = useRef<HTMLDivElement | null>(null);
1587
1587
  const conversationLayoutRef = useRef<HTMLDivElement | null>(null);
1588
1588
  const [isSending, setIsSending] = useState(false);
1589
+ // Request-input overlays replace the composer while the user chooses a response. Keep the
1590
+ // ordinary composer draft here so that temporary unmount does not discard it.
1591
+ const [composerValue, setComposerValue] = useState('');
1589
1592
  const [isCompactingContext, setIsCompactingContext] = useState(false);
1590
1593
  const [internalViewMode, setInternalViewMode] = useState<AgentConversationViewMode>('sliding');
1591
1594
  const viewMode = controlledViewMode ?? internalViewMode;
@@ -2908,6 +2911,8 @@ function ModernAgentConversationInner({
2908
2911
  {composerContext}
2909
2912
  <MessageInput
2910
2913
  onSend={handleSendMessage}
2914
+ value={composerValue}
2915
+ onValueChange={setComposerValue}
2911
2916
  onStop={allowWorkflowControl ? handleStopWorkflow : undefined}
2912
2917
  approvalModeSlot={
2913
2918
  interactive && toolApprovalMode ? (
@@ -55,6 +55,10 @@ function formatTokenCountInK(tokens: number): string {
55
55
 
56
56
  interface MessageInputProps {
57
57
  onSend: (message: string) => void;
58
+ /** Controlled composer draft. When omitted, the composer manages its own draft. */
59
+ value?: string;
60
+ /** Called whenever the composer draft changes. */
61
+ onValueChange?: (value: string) => void;
58
62
  onStop?: () => void;
59
63
  disabled?: boolean;
60
64
  isSending?: boolean;
@@ -121,6 +125,8 @@ interface MessageInputProps {
121
125
 
122
126
  export default function MessageInput({
123
127
  onSend,
128
+ value: controlledValue,
129
+ onValueChange,
124
130
  onStop,
125
131
  approvalModeSlot,
126
132
  mcpSlot,
@@ -163,7 +169,17 @@ export default function MessageInput({
163
169
  const resolvedPlaceholder = placeholder ?? t('agent.typeYourMessage');
164
170
  const ref = useRef<HTMLTextAreaElement | null>(null);
165
171
  const fileInputRef = useRef<HTMLInputElement | null>(null);
166
- const [value, setValue] = useState('');
172
+ const [uncontrolledValue, setUncontrolledValue] = useState('');
173
+ const value = controlledValue ?? uncontrolledValue;
174
+ const setValue = useCallback(
175
+ (nextValue: string) => {
176
+ if (controlledValue === undefined) {
177
+ setUncontrolledValue(nextValue);
178
+ }
179
+ onValueChange?.(nextValue);
180
+ },
181
+ [controlledValue, onValueChange],
182
+ );
167
183
  const [isObjectModalOpen, setIsObjectModalOpen] = useState(false);
168
184
  const [isDocSearchOpen, setIsDocSearchOpen] = useState(false);
169
185
  const [isDragOver, setIsDragOver] = useState(false);
@@ -659,7 +659,7 @@ function PageWithOverlay({
659
659
  'absolute cursor-pointer border rounded-[1px] transition-colors',
660
660
  citation.misaligned && 'border-dashed',
661
661
  isSelected
662
- ? 'border-2 border-info bg-info z-10'
662
+ ? 'border-2 border-info bg-info/20 z-10'
663
663
  : citation.verified
664
664
  ? 'border-success/70 hover:bg-success/20'
665
665
  : citation.reviewed
@@ -853,7 +853,7 @@ function ArrayTable({
853
853
  className={cn(
854
854
  'text-start rounded px-1 w-full',
855
855
  isSelected
856
- ? 'bg-info ring-1 ring-primary'
856
+ ? 'bg-info/15 ring-1 ring-primary'
857
857
  : citation
858
858
  ? 'hover:bg-muted cursor-pointer'
859
859
  : 'text-muted cursor-default',
@@ -909,7 +909,7 @@ function LeafRow({
909
909
  onClick={() => citation && onSelect(path)}
910
910
  className={cn(
911
911
  'flex items-center gap-2 w-full text-start text-sm rounded px-1.5 py-0.5',
912
- isSelected ? 'bg-info ring-1 ring-primary' : citation ? 'hover:bg-muted' : '',
912
+ isSelected ? 'bg-info/15 ring-1 ring-primary' : citation ? 'hover:bg-muted' : '',
913
913
  !citation && 'cursor-default',
914
914
  )}
915
915
  >
@@ -0,0 +1,84 @@
1
+ import { act, render } from '@testing-library/react';
2
+ import type { AudioResult } from '@vertesia/common';
3
+ import { createElement } from 'react';
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
5
+ import { AudioPanel, pcm16LeToWav } from './AudioPanel.js';
6
+
7
+ describe('pcm16LeToWav', () => {
8
+ it('adds a WAV header for 16-bit little-endian PCM', async () => {
9
+ const pcm = new Uint8Array([0x01, 0x00, 0xff, 0x7f]).buffer;
10
+ const wav = new DataView(await pcm16LeToWav(pcm, { sampleRate: 24_000, channels: 1 }).arrayBuffer());
11
+
12
+ expect(String.fromCharCode(...new Uint8Array(wav.buffer, 0, 4))).toBe('RIFF');
13
+ expect(String.fromCharCode(...new Uint8Array(wav.buffer, 8, 4))).toBe('WAVE');
14
+ expect(wav.getUint16(22, true)).toBe(1);
15
+ expect(wav.getUint32(24, true)).toBe(24_000);
16
+ expect(wav.getUint16(34, true)).toBe(16);
17
+ expect(String.fromCharCode(...new Uint8Array(wav.buffer, 36, 4))).toBe('data');
18
+ expect(wav.getUint32(40, true)).toBe(4);
19
+ expect([...new Uint8Array(wav.buffer, 44)]).toEqual([0x01, 0x00, 0xff, 0x7f]);
20
+ });
21
+ });
22
+
23
+ const { client } = vi.hoisted(() => ({ client: { files: { getDownloadUrl: vi.fn() } } }));
24
+ vi.mock('@vertesia/ui/core', () => ({ Spinner: () => null }));
25
+ vi.mock('@vertesia/ui/i18n', () => ({ useUITranslation: () => ({ t: (key: string) => key }) }));
26
+ vi.mock('@vertesia/ui/session', () => ({ useUserSession: () => ({ client }) }));
27
+
28
+ const audio: AudioResult = {
29
+ type: 'audio',
30
+ value: 'artifact:sample.pcm',
31
+ mime_type: 'audio/pcm',
32
+ container: 'raw',
33
+ codec: 'pcm',
34
+ sample_rate: 24000,
35
+ channels: 1,
36
+ sample_encoding: 'int16',
37
+ byte_order: 'little',
38
+ };
39
+
40
+ afterEach(() => {
41
+ vi.restoreAllMocks();
42
+ vi.unstubAllGlobals();
43
+ client.files.getDownloadUrl.mockReset();
44
+ });
45
+
46
+ describe('AudioPanel cancellation', () => {
47
+ it('aborts an in-flight PCM download on unmount without reporting an error', async () => {
48
+ let downloadSignal: AbortSignal | undefined;
49
+ const fetchMock = vi.fn(
50
+ (_url: string, init: RequestInit) =>
51
+ new Promise<Response>((_resolve, reject) => {
52
+ downloadSignal = init.signal ?? undefined;
53
+ downloadSignal?.addEventListener('abort', () => reject(downloadSignal?.reason), { once: true });
54
+ }),
55
+ );
56
+ vi.stubGlobal('fetch', fetchMock);
57
+ const log = vi.spyOn(console, 'error').mockImplementation(() => {});
58
+ const view = render(createElement(AudioPanel, { url: '/sample.pcm', audio }));
59
+ expect(fetchMock).toHaveBeenCalledOnce();
60
+
61
+ await act(async () => view.unmount());
62
+
63
+ expect(downloadSignal?.aborted).toBe(true);
64
+ expect(log).not.toHaveBeenCalled();
65
+ });
66
+
67
+ it('does not start a PCM download when a signed URL resolves after unmount', async () => {
68
+ let resolveUrl!: (value: { url: string }) => void;
69
+ client.files.getDownloadUrl.mockReturnValue(
70
+ new Promise<{ url: string }>((resolve) => {
71
+ resolveUrl = resolve;
72
+ }),
73
+ );
74
+ const fetchMock = vi.fn();
75
+ vi.stubGlobal('fetch', fetchMock);
76
+ const view = render(createElement(AudioPanel, { source: audio.value, audio }));
77
+ expect(client.files.getDownloadUrl).toHaveBeenCalledWith(audio.value);
78
+ view.unmount();
79
+
80
+ await act(async () => resolveUrl({ url: '/sample.pcm' }));
81
+
82
+ expect(fetchMock).not.toHaveBeenCalled();
83
+ });
84
+ });
@@ -1,4 +1,10 @@
1
- import { AUDIO_RENDITION_NAME, type AudioMetadata, ContentNature, type ContentObject } from '@vertesia/common';
1
+ import {
2
+ AUDIO_RENDITION_NAME,
3
+ type AudioMetadata,
4
+ type AudioResult,
5
+ ContentNature,
6
+ type ContentObject,
7
+ } from '@vertesia/common';
2
8
  import { Spinner } from '@vertesia/ui/core';
3
9
  import { useUITranslation } from '@vertesia/ui/i18n';
4
10
  import { useUserSession } from '@vertesia/ui/session';
@@ -12,15 +18,66 @@ interface AudioPanelProps {
12
18
  source?: string;
13
19
  /** ContentObject — uses the audio rendition or falls back to the original if web-supported. */
14
20
  object?: ContentObject;
21
+ /** Audio result metadata used to make raw PCM playable in browser media controls. */
22
+ audio?: AudioResult;
15
23
  /** Extra classes for the wrapper. */
16
24
  className?: string;
17
25
  }
18
26
 
27
+ export interface PcmFormat {
28
+ sampleRate: number;
29
+ channels: number;
30
+ }
31
+
32
+ function pcmFormat(audio?: AudioResult): PcmFormat | undefined {
33
+ if (
34
+ audio?.container !== 'raw' ||
35
+ audio.codec !== 'pcm' ||
36
+ audio.sample_encoding !== 'int16' ||
37
+ audio.byte_order !== 'little' ||
38
+ !audio.sample_rate ||
39
+ !audio.channels
40
+ ) {
41
+ return undefined;
42
+ }
43
+
44
+ return { sampleRate: audio.sample_rate, channels: audio.channels };
45
+ }
46
+
47
+ /** Wrap provider PCM16-LE bytes in a WAV header so browser media controls can decode them. */
48
+ export function pcm16LeToWav(pcm: ArrayBuffer, format: PcmFormat): Blob {
49
+ const headerSize = 44;
50
+ const bytesPerSample = 2;
51
+ const blockAlign = format.channels * bytesPerSample;
52
+ const bytesPerSecond = format.sampleRate * blockAlign;
53
+ const wav = new ArrayBuffer(headerSize);
54
+ const view = new DataView(wav);
55
+ const writeAscii = (offset: number, value: string) => {
56
+ for (let index = 0; index < value.length; index++) view.setUint8(offset + index, value.charCodeAt(index));
57
+ };
58
+
59
+ writeAscii(0, 'RIFF');
60
+ view.setUint32(4, 36 + pcm.byteLength, true);
61
+ writeAscii(8, 'WAVE');
62
+ writeAscii(12, 'fmt ');
63
+ view.setUint32(16, 16, true);
64
+ view.setUint16(20, 1, true);
65
+ view.setUint16(22, format.channels, true);
66
+ view.setUint32(24, format.sampleRate, true);
67
+ view.setUint32(28, bytesPerSecond, true);
68
+ view.setUint16(32, blockAlign, true);
69
+ view.setUint16(34, 16, true);
70
+ writeAscii(36, 'data');
71
+ view.setUint32(40, pcm.byteLength, true);
72
+
73
+ return new Blob([wav, pcm], { type: 'audio/wav' });
74
+ }
75
+
19
76
  /**
20
77
  * Renders an audio player from a direct URL, a storage source path, or a Vertesia ContentObject.
21
78
  * Resolution priority: `url` > `source` > `object`. Duration is shown only in object mode.
22
79
  */
23
- export function AudioPanel({ url, source, object, className }: AudioPanelProps) {
80
+ export function AudioPanel({ url, source, object, audio, className }: AudioPanelProps) {
24
81
  const { t } = useUITranslation();
25
82
  const { client } = useUserSession();
26
83
  const [audioUrl, setAudioUrl] = useState<string | undefined>(url);
@@ -34,19 +91,37 @@ export function AudioPanel({ url, source, object, className }: AudioPanelProps)
34
91
  !!object && object.metadata?.type === ContentNature.Audio && !audioRendition && !isOriginalWebSupported;
35
92
 
36
93
  useEffect(() => {
37
- if (url) {
38
- setAudioUrl(url);
39
- setIsLoading(false);
40
- return;
41
- }
42
-
94
+ let generatedUrl: string | undefined;
95
+ const controller = new AbortController();
96
+ const { signal } = controller;
43
97
  setAudioUrl(undefined);
44
98
 
99
+ const setPlayableUrl = async (downloadUrl: string) => {
100
+ signal.throwIfAborted();
101
+ const format = pcmFormat(audio);
102
+ if (!format) {
103
+ if (!signal.aborted) setAudioUrl(downloadUrl);
104
+ return;
105
+ }
106
+
107
+ const response = await fetch(downloadUrl, { signal });
108
+ if (!response.ok) throw new Error(`Failed to fetch PCM audio: ${response.status}`);
109
+ const pcm = await response.arrayBuffer();
110
+ signal.throwIfAborted();
111
+ generatedUrl = URL.createObjectURL(pcm16LeToWav(pcm, format));
112
+ setAudioUrl(generatedUrl);
113
+ };
114
+
45
115
  const load = async () => {
46
116
  try {
117
+ if (url) {
118
+ await setPlayableUrl(url);
119
+ return;
120
+ }
121
+
47
122
  if (source) {
48
123
  const downloadUrl = await client.files.getDownloadUrl(source);
49
- setAudioUrl(downloadUrl.url);
124
+ await setPlayableUrl(downloadUrl.url);
50
125
  return;
51
126
  }
52
127
 
@@ -60,22 +135,30 @@ export function AudioPanel({ url, source, object, className }: AudioPanelProps)
60
135
  downloadUrl = await client.files.getDownloadUrl(object.content.source);
61
136
  }
62
137
  if (downloadUrl) {
63
- setAudioUrl(downloadUrl.url);
138
+ await setPlayableUrl(downloadUrl.url);
64
139
  }
65
140
  } catch (error) {
66
- console.error('Failed to get audio URL', error);
141
+ if (!signal.aborted) console.error('Failed to get audio URL', error);
67
142
  } finally {
68
- setIsLoading(false);
143
+ if (!signal.aborted) {
144
+ setIsLoading(false);
145
+ }
69
146
  }
70
147
  };
71
148
 
72
- if (source || object) {
149
+ if (url || source || object) {
73
150
  setIsLoading(true);
74
151
  void load();
75
152
  } else {
76
153
  setIsLoading(false);
77
154
  }
78
- }, [url, source, object, audioRendition, isOriginalWebSupported, client]);
155
+ return () => {
156
+ controller.abort();
157
+ if (generatedUrl) {
158
+ URL.revokeObjectURL(generatedUrl);
159
+ }
160
+ };
161
+ }, [url, source, object, audio, audioRendition, isOriginalWebSupported, client]);
79
162
 
80
163
  if (showsObjectFallbackEmpty) {
81
164
  return (
@@ -183,6 +183,62 @@ describe('MCP connection controls', () => {
183
183
  expect(onChange).toHaveBeenCalledWith(['linear']);
184
184
  });
185
185
 
186
+ it.each([{ oauthIds: [] }, { oauthIds: ['anonymous'] }])(
187
+ 'shows anonymous servers without status requests, including stale OAuth ids %j',
188
+ async ({ oauthIds }) => {
189
+ mocks.getInstalledApps.mockResolvedValue([
190
+ {
191
+ id: 'anonymous-app',
192
+ manifest: {
193
+ name: 'anonymous-app',
194
+ tool_collections: [
195
+ {
196
+ type: 'mcp',
197
+ id: 'anonymous',
198
+ name: 'Anonymous MCP',
199
+ namespace: 'anonymous',
200
+ url: 'https://anonymous.example.com/mcp',
201
+ auth: 'none',
202
+ oauth_app: 'stale-oauth-app',
203
+ },
204
+ ],
205
+ },
206
+ oauth_collection_ids: oauthIds,
207
+ },
208
+ ]);
209
+ const onChange = vi.fn();
210
+ const { rerender } = renderWithProviders(
211
+ <McpConnectionsInlineList disabledCollections={['other']} onChange={onChange} />,
212
+ );
213
+
214
+ expect(await screen.findByText('Anonymous MCP')).not.toBeNull();
215
+ expect(screen.getAllByText('No authentication')).toHaveLength(1);
216
+ expect(screen.queryByRole('button', { name: /connect/i })).toBeNull();
217
+ expect(mocks.getStatus).not.toHaveBeenCalled();
218
+ expect(mocks.getCollectionStatus).not.toHaveBeenCalled();
219
+
220
+ fireEvent.click(screen.getByRole('switch', { name: 'Deactivate Anonymous MCP for this conversation' }));
221
+ expect(onChange).toHaveBeenLastCalledWith(['other', 'anonymous']);
222
+ rerender(<McpConnectionsInlineList disabledCollections={['other', 'anonymous']} onChange={onChange} />);
223
+ fireEvent.click(screen.getByRole('switch', { name: 'Activate Anonymous MCP for this conversation' }));
224
+ expect(onChange).toHaveBeenLastCalledWith(['other']);
225
+ rerender(<McpConnectionsInlineList onChange={onChange} readOnly />);
226
+ expect(screen.getByRole('switch')).toHaveProperty('disabled', true);
227
+
228
+ rerender(<McpConnectionsActionMenu onChange={onChange} />);
229
+ fireEvent.pointerDown(await screen.findByRole('button', { name: /settings/i }));
230
+ const menuItem = await screen.findByText('MCP');
231
+ expect(screen.getByText('1')).not.toBeNull();
232
+ fireEvent.click(menuItem);
233
+ expect(await screen.findByText('Anonymous MCP')).not.toBeNull();
234
+ expect(screen.getByText('No authentication')).not.toBeNull();
235
+ expect(screen.queryByRole('button', { name: /connect/i })).toBeNull();
236
+ fireEvent.click(screen.getByRole('switch', { name: 'Deactivate Anonymous MCP for this conversation' }));
237
+ expect(onChange).toHaveBeenLastCalledWith(['anonymous']);
238
+ expect(mocks.getStatus).not.toHaveBeenCalled();
239
+ },
240
+ );
241
+
186
242
  it('renders MCP rows before connection statuses finish loading', async () => {
187
243
  const statusRequest =
188
244
  deferred<
@@ -7,6 +7,7 @@ import { RemoteMcpConnectionButton } from './RemoteMcpConnectionButton.js';
7
7
  import {
8
8
  countConnectedActiveGroups,
9
9
  getConnectedActiveGroupLabels,
10
+ isGroupConnected,
10
11
  isGroupDisabled,
11
12
  toggleGroupDisabled,
12
13
  useMcpConnections,
@@ -29,7 +30,7 @@ export interface McpConnectionsButtonProps {
29
30
  /**
30
31
  * Toolbar button that opens the MCP connections dialog. The badge shows how many MCP
31
32
  * servers are connected and active for this conversation.
32
- * Renders nothing when the project has no OAuth MCP servers installed.
33
+ * Renders nothing when the project has no MCP servers installed.
33
34
  */
34
35
  export function McpConnectionsButton({
35
36
  disabledCollections,
@@ -93,7 +94,7 @@ export function McpConnectionsButton({
93
94
  }
94
95
 
95
96
  /**
96
- * Inline MCP status list for forms. It shows all available authenticated MCP groups
97
+ * Inline MCP status list for forms. It shows all available MCP groups
97
98
  * without requiring the user to open the management dialog.
98
99
  */
99
100
  export function McpConnectionsInlineList({
@@ -127,7 +128,7 @@ export function McpConnectionsInlineList({
127
128
  <div className="space-y-1">
128
129
  {groups.map((group) => {
129
130
  const active = !isGroupDisabled(group, disabledCollections);
130
- const connected = group.authStatus?.authenticated === true;
131
+ const connected = isGroupConnected(group);
131
132
  const interactiveOAuth = group.authType === 'oauth' && group.oauthGrantType !== 'client_credentials';
132
133
  const StatusIcon = connected ? Link2 : Link2Off;
133
134
  return (
@@ -179,9 +180,11 @@ export function McpConnectionsInlineList({
179
180
  />
180
181
  ) : (
181
182
  <Badge variant="outline" className="h-6 w-32 justify-center px-2 text-xs">
182
- {group.authType === 'api_key'
183
- ? t('mcpConnections.apiKey')
184
- : t('mcpConnections.managedByApp')}
183
+ {group.authType === 'none'
184
+ ? t('mcpConnections.noAuthentication')
185
+ : group.authType === 'api_key'
186
+ ? t('mcpConnections.apiKey')
187
+ : t('mcpConnections.managedByApp')}
185
188
  </Badge>
186
189
  )}
187
190
  {onChange && (
@@ -2,7 +2,12 @@ import { useUITranslation } from '@vertesia/ui/i18n';
2
2
  import { Link2, Link2Off } from 'lucide-react';
3
3
  import { Badge, Button, Modal, ModalBody, ModalTitle, Spinner, Switch, VTooltip } from '../../core/index.js';
4
4
  import { RemoteMcpConnectionButton } from './RemoteMcpConnectionButton.js';
5
- import { isGroupDisabled, type McpConnectionGroup, toggleGroupDisabled } from './useMcpConnections.js';
5
+ import {
6
+ isGroupConnected,
7
+ isGroupDisabled,
8
+ type McpConnectionGroup,
9
+ toggleGroupDisabled,
10
+ } from './useMcpConnections.js';
6
11
 
7
12
  export interface McpConnectionsDialogProps {
8
13
  isOpen: boolean;
@@ -61,7 +66,7 @@ export function McpConnectionsDialog({
61
66
  <div className="space-y-1">
62
67
  {groups.map((group) => {
63
68
  const active = !isGroupDisabled(group, disabledCollections);
64
- const connected = group.authStatus?.authenticated === true;
69
+ const connected = isGroupConnected(group);
65
70
  const interactiveOAuth =
66
71
  group.authType === 'oauth' && group.oauthGrantType !== 'client_credentials';
67
72
  const StatusIcon = connected ? Link2 : Link2Off;
@@ -115,11 +120,15 @@ export function McpConnectionsDialog({
115
120
  showDisconnect
116
121
  readOnly={readOnly}
117
122
  />
118
- ) : group.authType === 'oauth' ? (
123
+ ) : (
119
124
  <Badge variant="outline" className="h-6 w-32 justify-center px-2 text-xs">
120
- {t('mcpConnections.managedByApp')}
125
+ {group.authType === 'none'
126
+ ? t('mcpConnections.noAuthentication')
127
+ : group.authType === 'api_key'
128
+ ? t('mcpConnections.apiKey')
129
+ : t('mcpConnections.managedByApp')}
121
130
  </Badge>
122
- ) : null}
131
+ )}
123
132
  {onChange && (
124
133
  <Switch
125
134
  size="sm"
@@ -93,3 +93,13 @@ describe('toggleGroupDisabled', () => {
93
93
  expect(toggleGroupDisabled(g, disabled, true)).toEqual([]);
94
94
  });
95
95
  });
96
+
97
+ describe('anonymous MCP groups', () => {
98
+ it('counts anonymous servers without credential status and respects disabled collections', () => {
99
+ const anonymous = { ...group(['anonymous'], false, 'Anonymous'), authType: 'none' as const };
100
+ expect(countConnectedActiveGroups([anonymous])).toBe(1);
101
+ expect(getConnectedActiveGroupLabels([anonymous])).toEqual(['Anonymous']);
102
+ expect(countConnectedActiveGroups([anonymous], ['anonymous'])).toBe(0);
103
+ expect(getConnectedActiveGroupLabels([anonymous], ['anonymous'])).toEqual([]);
104
+ });
105
+ });