@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.
- package/lib/boot/branding.d.ts +13 -0
- package/lib/boot/branding.d.ts.map +1 -1
- package/lib/boot/branding.js +7 -0
- package/lib/boot/branding.js.map +1 -1
- package/lib/boot/index.d.ts +1 -1
- package/lib/boot/index.d.ts.map +1 -1
- package/lib/boot/index.js +5 -23
- package/lib/boot/index.js.map +1 -1
- package/lib/boot/loading.d.ts +4 -0
- package/lib/boot/loading.d.ts.map +1 -0
- package/lib/boot/loading.js +28 -0
- package/lib/boot/loading.js.map +1 -0
- package/lib/core/components/SelectList.d.ts.map +1 -1
- package/lib/core/components/SelectList.js +3 -3
- package/lib/core/components/SelectList.js.map +1 -1
- package/lib/core/components/SidePanel.d.ts.map +1 -1
- package/lib/core/components/SidePanel.js +2 -1
- package/lib/core/components/SidePanel.js.map +1 -1
- package/lib/core/components/shadcn/theme/ThemeProvider.d.ts.map +1 -1
- package/lib/core/components/shadcn/theme/ThemeProvider.js +21 -5
- package/lib/core/components/shadcn/theme/ThemeProvider.js.map +1 -1
- package/lib/env/index.d.ts +11 -0
- package/lib/env/index.d.ts.map +1 -1
- package/lib/env/index.js +16 -3
- package/lib/env/index.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentConversation.js +4 -1
- package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts +5 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js +9 -2
- package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js.map +1 -1
- package/lib/features/magic-pdf/GroundedExtractionView.js +3 -3
- package/lib/features/magic-pdf/GroundedExtractionView.js.map +1 -1
- package/lib/features/media-viewer/AudioPanel.d.ts +10 -2
- package/lib/features/media-viewer/AudioPanel.d.ts.map +1 -1
- package/lib/features/media-viewer/AudioPanel.js +78 -13
- package/lib/features/media-viewer/AudioPanel.js.map +1 -1
- package/lib/features/oauth/McpConnectionsButton.d.ts +2 -2
- package/lib/features/oauth/McpConnectionsButton.d.ts.map +1 -1
- package/lib/features/oauth/McpConnectionsButton.js +9 -7
- package/lib/features/oauth/McpConnectionsButton.js.map +1 -1
- package/lib/features/oauth/McpConnectionsDialog.d.ts.map +1 -1
- package/lib/features/oauth/McpConnectionsDialog.js +7 -3
- package/lib/features/oauth/McpConnectionsDialog.js.map +1 -1
- package/lib/features/oauth/useMcpConnections.d.ts +4 -2
- package/lib/features/oauth/useMcpConnections.d.ts.map +1 -1
- package/lib/features/oauth/useMcpConnections.js +15 -9
- package/lib/features/oauth/useMcpConnections.js.map +1 -1
- package/lib/features/store/objects/selection/ObjectsActionContext.d.ts.map +1 -1
- package/lib/features/store/objects/selection/ObjectsActionContext.js +2 -0
- package/lib/features/store/objects/selection/ObjectsActionContext.js.map +1 -1
- package/lib/features/store/objects/selection/SelectionActions.d.ts +4 -2
- package/lib/features/store/objects/selection/SelectionActions.d.ts.map +1 -1
- package/lib/features/store/objects/selection/SelectionActions.js +6 -3
- package/lib/features/store/objects/selection/SelectionActions.js.map +1 -1
- package/lib/features/store/objects/selection/actions/StartWorkflowAction.d.ts +4 -0
- package/lib/features/store/objects/selection/actions/StartWorkflowAction.d.ts.map +1 -0
- package/lib/features/store/objects/selection/actions/StartWorkflowAction.js +81 -0
- package/lib/features/store/objects/selection/actions/StartWorkflowAction.js.map +1 -0
- package/lib/features/store/objects/selection/actions/index.d.ts +1 -0
- package/lib/features/store/objects/selection/actions/index.d.ts.map +1 -1
- package/lib/features/store/objects/selection/actions/index.js +1 -0
- package/lib/features/store/objects/selection/actions/index.js.map +1 -1
- package/lib/features/store/types/TypeRegistry.d.ts +345 -155
- package/lib/features/store/types/TypeRegistry.d.ts.map +1 -1
- package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts +345 -155
- package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts.map +1 -1
- package/lib/i18n/locales/ar.json +10 -2
- package/lib/i18n/locales/de.json +6 -2
- package/lib/i18n/locales/en.json +6 -2
- package/lib/i18n/locales/es.json +6 -2
- package/lib/i18n/locales/fr.json +7 -2
- package/lib/i18n/locales/it.json +6 -2
- package/lib/i18n/locales/ja.json +6 -2
- package/lib/i18n/locales/ko.json +6 -2
- package/lib/i18n/locales/pt.json +6 -2
- package/lib/i18n/locales/ru.json +8 -2
- package/lib/i18n/locales/tr.json +6 -2
- package/lib/i18n/locales/zh-TW.json +6 -2
- package/lib/i18n/locales/zh.json +6 -2
- package/lib/session/UserSession.d.ts +3 -0
- package/lib/session/UserSession.d.ts.map +1 -1
- package/lib/session/UserSession.js +18 -8
- package/lib/session/UserSession.js.map +1 -1
- package/lib/session/UserSessionProvider.d.ts.map +1 -1
- package/lib/session/UserSessionProvider.js +34 -24
- package/lib/session/UserSessionProvider.js.map +1 -1
- package/lib/session/auth/authRoundTrip.d.ts +8 -0
- package/lib/session/auth/authRoundTrip.d.ts.map +1 -0
- package/lib/session/auth/authRoundTrip.js +22 -0
- package/lib/session/auth/authRoundTrip.js.map +1 -0
- package/lib/session/auth/composable.d.ts.map +1 -1
- package/lib/session/auth/composable.js +9 -7
- package/lib/session/auth/composable.js.map +1 -1
- package/lib/session/auth/domainRouting.d.ts +13 -6
- package/lib/session/auth/domainRouting.d.ts.map +1 -1
- package/lib/session/auth/domainRouting.js +2 -21
- package/lib/session/auth/domainRouting.js.map +1 -1
- package/lib/session/auth/oauth.d.ts +6 -1
- package/lib/session/auth/oauth.d.ts.map +1 -1
- package/lib/session/auth/oauth.js +145 -8
- package/lib/session/auth/oauth.js.map +1 -1
- package/lib/session/auth/tokenIssuer.d.ts +3 -0
- package/lib/session/auth/tokenIssuer.d.ts.map +1 -0
- package/lib/session/auth/tokenIssuer.js +27 -0
- package/lib/session/auth/tokenIssuer.js.map +1 -0
- package/lib/session/auth/vertesiaHosts.d.ts +47 -0
- package/lib/session/auth/vertesiaHosts.d.ts.map +1 -0
- package/lib/session/auth/vertesiaHosts.js +79 -0
- package/lib/session/auth/vertesiaHosts.js.map +1 -0
- package/lib/session/index.d.ts +1 -0
- package/lib/session/index.d.ts.map +1 -1
- package/lib/session/index.js +1 -0
- package/lib/session/index.js.map +1 -1
- package/lib/shell/AuthScreenPreview.d.ts +1 -1
- package/lib/shell/AuthScreenPreview.d.ts.map +1 -1
- package/lib/shell/AuthScreenPreview.js +2 -5
- package/lib/shell/AuthScreenPreview.js.map +1 -1
- package/lib/shell/BrandedAuthScreens.d.ts +1 -4
- package/lib/shell/BrandedAuthScreens.d.ts.map +1 -1
- package/lib/shell/BrandedAuthScreens.js +5 -26
- package/lib/shell/BrandedAuthScreens.js.map +1 -1
- package/lib/shell/BrandedLoadingIndicator.d.ts +11 -0
- package/lib/shell/BrandedLoadingIndicator.d.ts.map +1 -0
- package/lib/shell/BrandedLoadingIndicator.js +27 -0
- package/lib/shell/BrandedLoadingIndicator.js.map +1 -0
- package/lib/shell/SplashScreen.d.ts.map +1 -1
- package/lib/shell/SplashScreen.js +2 -1
- package/lib/shell/SplashScreen.js.map +1 -1
- package/lib/shell/apps/AppInstallationProvider.d.ts +16 -4
- package/lib/shell/apps/AppInstallationProvider.d.ts.map +1 -1
- package/lib/shell/apps/iframe-auth.d.ts +2 -0
- package/lib/shell/apps/iframe-auth.d.ts.map +1 -1
- package/lib/shell/apps/iframe-auth.js +6 -2
- package/lib/shell/apps/iframe-auth.js.map +1 -1
- package/lib/shell/index.d.ts +1 -0
- package/lib/shell/index.d.ts.map +1 -1
- package/lib/shell/index.js +1 -0
- package/lib/shell/index.js.map +1 -1
- package/lib/shell/login/SignInAuthPending.js +2 -2
- package/lib/shell/login/SignInAuthPending.js.map +1 -1
- package/lib/shell/login/SignInEmailStep.d.ts.map +1 -1
- package/lib/shell/login/SignInEmailStep.js +2 -2
- package/lib/shell/login/SignInEmailStep.js.map +1 -1
- package/lib/shell/login/TerminalLogin.d.ts.map +1 -1
- package/lib/shell/login/TerminalLogin.js +28 -21
- package/lib/shell/login/TerminalLogin.js.map +1 -1
- package/lib/vertesia-ui-boot.js +33 -29
- package/lib/vertesia-ui-boot.js.map +1 -1
- package/lib/vertesia-ui-core.js +34 -34
- package/lib/vertesia-ui-core.js.map +1 -1
- package/lib/vertesia-ui-env.js +1 -1
- package/lib/vertesia-ui-env.js.map +1 -1
- package/lib/vertesia-ui-features.js +163 -163
- package/lib/vertesia-ui-features.js.map +1 -1
- package/lib/vertesia-ui-i18n.Bl8KeZUK.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.Bl8KeZUK.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.BmT06XFI.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BmT06XFI.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.Ch-Jjj0L.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.Ch-Jjj0L.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.CjhloW9s.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.CjhloW9s.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.CkfHdQzY.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.CkfHdQzY.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.DFuKfTe1.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.DFuKfTe1.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.DHqNVekZ.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.DHqNVekZ.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.DOaKbmaj.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.DOaKbmaj.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.Dge3Gl-h.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.Dge3Gl-h.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.GLt-10Lx.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.GLt-10Lx.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.T-XtAGbw.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.T-XtAGbw.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.VOJyTpMM.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.VOJyTpMM.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.js +3 -3
- package/lib/vertesia-ui-i18n.js.map +1 -1
- package/lib/vertesia-ui-layout.js +15 -15
- package/lib/vertesia-ui-layout.js.map +1 -1
- package/lib/vertesia-ui-rich-text.js +44 -44
- package/lib/vertesia-ui-rich-text.js.map +1 -1
- package/lib/vertesia-ui-session.js +1 -1
- package/lib/vertesia-ui-session.js.map +1 -1
- package/lib/vertesia-ui-shell.js +72 -48
- package/lib/vertesia-ui-shell.js.map +1 -1
- package/lib/vertesia-ui-widgets.js +51 -51
- package/lib/vertesia-ui-widgets.js.map +1 -1
- package/package.json +19 -19
- package/src/boot/branding.ts +8 -0
- package/src/boot/index.ts +6 -23
- package/src/boot/loading.ts +27 -0
- package/src/core/components/SelectList.tsx +5 -2
- package/src/core/components/SidePanel.tsx +65 -60
- package/src/core/components/shadcn/theme/ThemeProvider.test.tsx +101 -0
- package/src/core/components/shadcn/theme/ThemeProvider.tsx +21 -9
- package/src/env/index.test.ts +21 -0
- package/src/env/index.ts +33 -2
- package/src/features/agent/chat/ModernAgentConversation.test.tsx +81 -0
- package/src/features/agent/chat/ModernAgentConversation.tsx +5 -0
- package/src/features/agent/chat/ModernAgentOutput/MessageInput.tsx +17 -1
- package/src/features/magic-pdf/GroundedExtractionView.tsx +3 -3
- package/src/features/media-viewer/AudioPanel.test.ts +84 -0
- package/src/features/media-viewer/AudioPanel.tsx +97 -14
- package/src/features/oauth/McpConnectionsButton.test.tsx +56 -0
- package/src/features/oauth/McpConnectionsButton.tsx +9 -6
- package/src/features/oauth/McpConnectionsDialog.tsx +14 -5
- package/src/features/oauth/useMcpConnections.test.ts +10 -0
- package/src/features/oauth/useMcpConnections.ts +18 -10
- package/src/features/store/objects/selection/ObjectsActionContext.tsx +2 -0
- package/src/features/store/objects/selection/SelectionActions.tsx +13 -6
- package/src/features/store/objects/selection/actions/StartWorkflowAction.tsx +162 -0
- package/src/features/store/objects/selection/actions/index.ts +1 -0
- package/src/i18n/locales/ar.json +10 -2
- package/src/i18n/locales/de.json +6 -2
- package/src/i18n/locales/en.json +6 -2
- package/src/i18n/locales/es.json +6 -2
- package/src/i18n/locales/fr.json +7 -2
- package/src/i18n/locales/it.json +6 -2
- package/src/i18n/locales/ja.json +6 -2
- package/src/i18n/locales/ko.json +6 -2
- package/src/i18n/locales/pt.json +6 -2
- package/src/i18n/locales/ru.json +8 -2
- package/src/i18n/locales/tr.json +6 -2
- package/src/i18n/locales/zh-TW.json +6 -2
- package/src/i18n/locales/zh.json +6 -2
- package/src/session/UserSession.test.ts +48 -3
- package/src/session/UserSession.ts +20 -8
- package/src/session/UserSessionProvider.handoff.test.tsx +103 -0
- package/src/session/UserSessionProvider.iframe.test.tsx +178 -0
- package/src/session/UserSessionProvider.state-mismatch.test.tsx +43 -0
- package/src/session/UserSessionProvider.tsx +35 -23
- package/src/session/auth/auth-flow.md +18 -5
- package/src/session/auth/authRoundTrip.ts +24 -0
- package/src/session/auth/composable.test.ts +23 -1
- package/src/session/auth/composable.ts +10 -8
- package/src/session/auth/domainRouting.ts +4 -25
- package/src/session/auth/oauth.test.ts +121 -4
- package/src/session/auth/oauth.ts +172 -11
- package/src/session/auth/tokenIssuer.test.ts +25 -0
- package/src/session/auth/tokenIssuer.ts +25 -0
- package/src/session/auth/vertesiaHosts.test.ts +26 -0
- package/src/session/auth/vertesiaHosts.ts +85 -0
- package/src/session/index.ts +1 -0
- package/src/shell/AuthScreenPreview.tsx +2 -6
- package/src/shell/BrandedAuthScreens.test.tsx +26 -0
- package/src/shell/BrandedAuthScreens.tsx +4 -40
- package/src/shell/BrandedLoadingIndicator.tsx +42 -0
- package/src/shell/SplashScreen.tsx +4 -4
- package/src/shell/apps/iframe-auth.test.ts +8 -11
- package/src/shell/apps/iframe-auth.ts +10 -1
- package/src/shell/index.tsx +1 -0
- package/src/shell/login/SignInAuthPending.tsx +2 -2
- package/src/shell/login/SignInEmailStep.tsx +4 -2
- package/src/shell/login/TerminalLogin.test.tsx +111 -0
- package/src/shell/login/TerminalLogin.tsx +61 -37
- package/lib/features/store/objects/selection/actions/StartWorkflowButton.d.ts +0 -7
- package/lib/features/store/objects/selection/actions/StartWorkflowButton.d.ts.map +0 -1
- package/lib/features/store/objects/selection/actions/StartWorkflowButton.js +0 -51
- package/lib/features/store/objects/selection/actions/StartWorkflowButton.js.map +0 -1
- package/lib/vertesia-ui-i18n.B3NFclwl.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.B3NFclwl.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BWSrNIxc.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BWSrNIxc.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BWvWeB1p.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BWvWeB1p.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BZCXvTC1.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BZCXvTC1.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BbJMvFZW.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BbJMvFZW.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.CE5ldwpt.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.CE5ldwpt.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.CMC0IShY.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.CMC0IShY.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.CkcRqBKU.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.CkcRqBKU.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.D-Mq1sw_.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.D-Mq1sw_.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DkK68-A-.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DkK68-A-.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.XE_dcCg6.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.XE_dcCg6.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.qgNqMPvK.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.qgNqMPvK.chunk.js.map +0 -1
- 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 [
|
|
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 {
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 === '
|
|
183
|
-
? t('mcpConnections.
|
|
184
|
-
:
|
|
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 {
|
|
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
|
|
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
|
-
) :
|
|
123
|
+
) : (
|
|
119
124
|
<Badge variant="outline" className="h-6 w-32 justify-center px-2 text-xs">
|
|
120
|
-
{
|
|
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
|
-
)
|
|
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
|
+
});
|