@devicai/ui 0.57.0 → 0.58.0
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/README.md +162 -0
- package/dist/cjs/api/types.js.map +1 -1
- package/dist/cjs/components/AICommandBar/AICommandBar.js +10 -7
- package/dist/cjs/components/AICommandBar/AICommandBar.js.map +1 -1
- package/dist/cjs/components/AICommandBar/useAICommandBar.js +7 -5
- package/dist/cjs/components/AICommandBar/useAICommandBar.js.map +1 -1
- package/dist/cjs/components/AIElementWrapper/AIElementWrapper.js +17 -14
- package/dist/cjs/components/AIElementWrapper/AIElementWrapper.js.map +1 -1
- package/dist/cjs/components/AIElementWrapper/useAIElementWrapper.js +7 -5
- package/dist/cjs/components/AIElementWrapper/useAIElementWrapper.js.map +1 -1
- package/dist/cjs/components/AIGenerationButton/AIGenerationButton.js +11 -6
- package/dist/cjs/components/AIGenerationButton/AIGenerationButton.js.map +1 -1
- package/dist/cjs/components/AIGenerationButton/useAIGenerationButton.js +6 -4
- package/dist/cjs/components/AIGenerationButton/useAIGenerationButton.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/ChatDrawer.js +26 -12
- package/dist/cjs/components/ChatDrawer/ChatDrawer.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/ChatInput.js +21 -15
- package/dist/cjs/components/ChatDrawer/ChatInput.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/ChatMessages.js +54 -13
- package/dist/cjs/components/ChatDrawer/ChatMessages.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/CompactionWidget.js +13 -4
- package/dist/cjs/components/ChatDrawer/CompactionWidget.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/ConversationSelector.js +6 -4
- package/dist/cjs/components/ChatDrawer/ConversationSelector.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/GuardrailNotice.js +40 -0
- package/dist/cjs/components/ChatDrawer/GuardrailNotice.js.map +1 -0
- package/dist/cjs/components/ChatDrawer/HandoffSubagentWidget.js +3 -1
- package/dist/cjs/components/ChatDrawer/HandoffSubagentWidget.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/LimitBanner.js +19 -9
- package/dist/cjs/components/ChatDrawer/LimitBanner.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/QueueNotice.js +19 -7
- package/dist/cjs/components/ChatDrawer/QueueNotice.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/RecalledMemoriesWidget.js +17 -5
- package/dist/cjs/components/ChatDrawer/RecalledMemoriesWidget.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/ReferenceChip.js +3 -1
- package/dist/cjs/components/ChatDrawer/ReferenceChip.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/UsageBar.js +23 -18
- package/dist/cjs/components/ChatDrawer/UsageBar.js.map +1 -1
- package/dist/cjs/components/CoreMemoryModal/CoreMemoryModal.js +20 -9
- package/dist/cjs/components/CoreMemoryModal/CoreMemoryModal.js.map +1 -1
- package/dist/cjs/components/Feedback/FeedbackModal.js +5 -1
- package/dist/cjs/components/Feedback/FeedbackModal.js.map +1 -1
- package/dist/cjs/components/Feedback/MessageActions.js +3 -1
- package/dist/cjs/components/Feedback/MessageActions.js.map +1 -1
- package/dist/cjs/components/IntegrationsModal/ConnectFieldsForm.js +12 -8
- package/dist/cjs/components/IntegrationsModal/ConnectFieldsForm.js.map +1 -1
- package/dist/cjs/components/IntegrationsModal/IntegrationsHint.js +5 -3
- package/dist/cjs/components/IntegrationsModal/IntegrationsHint.js.map +1 -1
- package/dist/cjs/components/IntegrationsModal/IntegrationsLauncher.js +12 -3
- package/dist/cjs/components/IntegrationsModal/IntegrationsLauncher.js.map +1 -1
- package/dist/cjs/components/IntegrationsModal/IntegrationsPanel.js +34 -24
- package/dist/cjs/components/IntegrationsModal/IntegrationsPanel.js.map +1 -1
- package/dist/cjs/components/IntegrationsModal/IntegrationsToggle.js +15 -6
- package/dist/cjs/components/IntegrationsModal/IntegrationsToggle.js.map +1 -1
- package/dist/cjs/components/IntegrationsModal/McpConnectForm.js +15 -7
- package/dist/cjs/components/IntegrationsModal/McpConnectForm.js.map +1 -1
- package/dist/cjs/components/IntegrationsModal/McpServersSection.js +32 -19
- package/dist/cjs/components/IntegrationsModal/McpServersSection.js.map +1 -1
- package/dist/cjs/components/IntegrationsModal/useIntegrations.js +3 -1
- package/dist/cjs/components/IntegrationsModal/useIntegrations.js.map +1 -1
- package/dist/cjs/components/IntegrationsModal/useTenantMcp.js +3 -1
- package/dist/cjs/components/IntegrationsModal/useTenantMcp.js.map +1 -1
- package/dist/cjs/components/ThreadStateTag/ThreadStateTag.js +63 -40
- package/dist/cjs/components/ThreadStateTag/ThreadStateTag.js.map +1 -1
- package/dist/cjs/hooks/useDevicChat.js +11 -5
- package/dist/cjs/hooks/useDevicChat.js.map +1 -1
- package/dist/cjs/hooks/useSpeechRecording.js +8 -4
- package/dist/cjs/hooks/useSpeechRecording.js.map +1 -1
- package/dist/cjs/i18n/translate.js +48 -0
- package/dist/cjs/i18n/translate.js.map +1 -0
- package/dist/cjs/i18n/useTranslations.js +57 -0
- package/dist/cjs/i18n/useTranslations.js.map +1 -0
- package/dist/cjs/index.js +11 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/provider/DevicProvider.js +3 -1
- package/dist/cjs/provider/DevicProvider.js.map +1 -1
- package/dist/cjs/styles.css +1 -1
- package/dist/esm/api/types.d.ts +10 -1
- package/dist/esm/api/types.js.map +1 -1
- package/dist/esm/components/AICommandBar/AICommandBar.js +11 -8
- package/dist/esm/components/AICommandBar/AICommandBar.js.map +1 -1
- package/dist/esm/components/AICommandBar/AICommandBar.types.d.ts +10 -0
- package/dist/esm/components/AICommandBar/useAICommandBar.js +7 -5
- package/dist/esm/components/AICommandBar/useAICommandBar.js.map +1 -1
- package/dist/esm/components/AIElementWrapper/AIElementWrapper.js +17 -14
- package/dist/esm/components/AIElementWrapper/AIElementWrapper.js.map +1 -1
- package/dist/esm/components/AIElementWrapper/AIElementWrapper.types.d.ts +10 -0
- package/dist/esm/components/AIElementWrapper/useAIElementWrapper.js +7 -5
- package/dist/esm/components/AIElementWrapper/useAIElementWrapper.js.map +1 -1
- package/dist/esm/components/AIGenerationButton/AIGenerationButton.js +12 -7
- package/dist/esm/components/AIGenerationButton/AIGenerationButton.js.map +1 -1
- package/dist/esm/components/AIGenerationButton/AIGenerationButton.types.d.ts +10 -0
- package/dist/esm/components/AIGenerationButton/useAIGenerationButton.js +6 -4
- package/dist/esm/components/AIGenerationButton/useAIGenerationButton.js.map +1 -1
- package/dist/esm/components/ChatDrawer/ChatDrawer.js +27 -13
- package/dist/esm/components/ChatDrawer/ChatDrawer.js.map +1 -1
- package/dist/esm/components/ChatDrawer/ChatDrawer.types.d.ts +33 -0
- package/dist/esm/components/ChatDrawer/ChatInput.js +21 -15
- package/dist/esm/components/ChatDrawer/ChatInput.js.map +1 -1
- package/dist/esm/components/ChatDrawer/ChatMessages.d.ts +1 -1
- package/dist/esm/components/ChatDrawer/ChatMessages.js +54 -13
- package/dist/esm/components/ChatDrawer/ChatMessages.js.map +1 -1
- package/dist/esm/components/ChatDrawer/CompactionWidget.js +13 -4
- package/dist/esm/components/ChatDrawer/CompactionWidget.js.map +1 -1
- package/dist/esm/components/ChatDrawer/ConversationSelector.js +6 -4
- package/dist/esm/components/ChatDrawer/ConversationSelector.js.map +1 -1
- package/dist/esm/components/ChatDrawer/GuardrailNotice.d.ts +36 -0
- package/dist/esm/components/ChatDrawer/GuardrailNotice.js +38 -0
- package/dist/esm/components/ChatDrawer/GuardrailNotice.js.map +1 -0
- package/dist/esm/components/ChatDrawer/HandoffSubagentWidget.js +3 -1
- package/dist/esm/components/ChatDrawer/HandoffSubagentWidget.js.map +1 -1
- package/dist/esm/components/ChatDrawer/LimitBanner.js +19 -9
- package/dist/esm/components/ChatDrawer/LimitBanner.js.map +1 -1
- package/dist/esm/components/ChatDrawer/QueueNotice.js +19 -7
- package/dist/esm/components/ChatDrawer/QueueNotice.js.map +1 -1
- package/dist/esm/components/ChatDrawer/RecalledMemoriesWidget.js +17 -5
- package/dist/esm/components/ChatDrawer/RecalledMemoriesWidget.js.map +1 -1
- package/dist/esm/components/ChatDrawer/ReferenceChip.js +3 -1
- package/dist/esm/components/ChatDrawer/ReferenceChip.js.map +1 -1
- package/dist/esm/components/ChatDrawer/UsageBar.js +23 -18
- package/dist/esm/components/ChatDrawer/UsageBar.js.map +1 -1
- package/dist/esm/components/ChatDrawer/index.d.ts +2 -0
- package/dist/esm/components/CoreMemoryModal/CoreMemoryModal.js +20 -9
- package/dist/esm/components/CoreMemoryModal/CoreMemoryModal.js.map +1 -1
- package/dist/esm/components/Feedback/FeedbackModal.js +5 -1
- package/dist/esm/components/Feedback/FeedbackModal.js.map +1 -1
- package/dist/esm/components/Feedback/MessageActions.js +3 -1
- package/dist/esm/components/Feedback/MessageActions.js.map +1 -1
- package/dist/esm/components/IntegrationsModal/ConnectFieldsForm.js +12 -8
- package/dist/esm/components/IntegrationsModal/ConnectFieldsForm.js.map +1 -1
- package/dist/esm/components/IntegrationsModal/IntegrationsHint.js +5 -3
- package/dist/esm/components/IntegrationsModal/IntegrationsHint.js.map +1 -1
- package/dist/esm/components/IntegrationsModal/IntegrationsLauncher.js +12 -3
- package/dist/esm/components/IntegrationsModal/IntegrationsLauncher.js.map +1 -1
- package/dist/esm/components/IntegrationsModal/IntegrationsPanel.d.ts +1 -1
- package/dist/esm/components/IntegrationsModal/IntegrationsPanel.js +34 -24
- package/dist/esm/components/IntegrationsModal/IntegrationsPanel.js.map +1 -1
- package/dist/esm/components/IntegrationsModal/IntegrationsToggle.d.ts +1 -1
- package/dist/esm/components/IntegrationsModal/IntegrationsToggle.js +15 -6
- package/dist/esm/components/IntegrationsModal/IntegrationsToggle.js.map +1 -1
- package/dist/esm/components/IntegrationsModal/McpConnectForm.js +15 -7
- package/dist/esm/components/IntegrationsModal/McpConnectForm.js.map +1 -1
- package/dist/esm/components/IntegrationsModal/McpServersSection.js +32 -19
- package/dist/esm/components/IntegrationsModal/McpServersSection.js.map +1 -1
- package/dist/esm/components/IntegrationsModal/useIntegrations.js +3 -1
- package/dist/esm/components/IntegrationsModal/useIntegrations.js.map +1 -1
- package/dist/esm/components/IntegrationsModal/useTenantMcp.js +3 -1
- package/dist/esm/components/IntegrationsModal/useTenantMcp.js.map +1 -1
- package/dist/esm/components/ThreadStateTag/ThreadStateTag.js +63 -40
- package/dist/esm/components/ThreadStateTag/ThreadStateTag.js.map +1 -1
- package/dist/esm/hooks/useDevicChat.js +12 -6
- package/dist/esm/hooks/useDevicChat.js.map +1 -1
- package/dist/esm/hooks/useSpeechRecording.js +8 -4
- package/dist/esm/hooks/useSpeechRecording.js.map +1 -1
- package/dist/esm/i18n/index.d.ts +4 -0
- package/dist/esm/i18n/translate.d.ts +23 -0
- package/dist/esm/i18n/translate.js +43 -0
- package/dist/esm/i18n/translate.js.map +1 -0
- package/dist/esm/i18n/types.d.ts +27 -0
- package/dist/esm/i18n/useTranslations.d.ts +32 -0
- package/dist/esm/i18n/useTranslations.js +53 -0
- package/dist/esm/i18n/useTranslations.js.map +1 -0
- package/dist/esm/index.d.ts +4 -2
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/provider/DevicProvider.d.ts +1 -1
- package/dist/esm/provider/DevicProvider.js +3 -1
- package/dist/esm/provider/DevicProvider.js.map +1 -1
- package/dist/esm/provider/types.d.ts +33 -0
- package/dist/esm/styles.css +1 -1
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
|
2
2
|
import { useState, useMemo } from 'react';
|
|
3
3
|
import { createPortal } from 'react-dom';
|
|
4
4
|
import { isDarkTheme, themeVars } from '../theme.js';
|
|
5
|
+
import { useTranslations } from '../../i18n/useTranslations.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* What an MCP server asks the end user for.
|
|
@@ -19,6 +20,7 @@ import { isDarkTheme, themeVars } from '../theme.js';
|
|
|
19
20
|
*/
|
|
20
21
|
function McpConnectForm({ server, submitting, error, callbackUrl, needsClientCredentials, theme, onCancel, onSubmit, }) {
|
|
21
22
|
const custom = !server;
|
|
23
|
+
const t = useTranslations();
|
|
22
24
|
const [url, setUrl] = useState(server?.url ?? "");
|
|
23
25
|
const [name, setName] = useState(server?.name ?? "");
|
|
24
26
|
const [key, setKey] = useState("");
|
|
@@ -32,8 +34,8 @@ function McpConnectForm({ server, submitting, error, callbackUrl, needsClientCre
|
|
|
32
34
|
// authenticates, and asking again would only invite a wrong answer.
|
|
33
35
|
const [mode, setMode] = useState(server?.authMode ?? "oauth");
|
|
34
36
|
const headerLabel = server?.headerName
|
|
35
|
-
?
|
|
36
|
-
: "API key";
|
|
37
|
+
? t("{header} value", { header: server.headerName })
|
|
38
|
+
: t("API key");
|
|
37
39
|
const urlInvalid = useMemo(() => custom && touched && !/^https:\/\/.+/i.test(url.trim()), [custom, touched, url]);
|
|
38
40
|
const keyInvalid = touched && mode === "header" && !key.trim();
|
|
39
41
|
const handleSubmit = (event) => {
|
|
@@ -60,17 +62,23 @@ function McpConnectForm({ server, submitting, error, callbackUrl, needsClientCre
|
|
|
60
62
|
},
|
|
61
63
|
});
|
|
62
64
|
};
|
|
63
|
-
const title = custom
|
|
65
|
+
const title = custom
|
|
66
|
+
? t("Add an MCP server")
|
|
67
|
+
: t("Connect {server}", { server: server.name });
|
|
64
68
|
return createPortal(jsx("div", { className: "devic-int-overlay devic-int-overlay-stacked", style: themeVars(theme), "data-dark": isDarkTheme(theme), onClick: onCancel, children: jsxs("form", { className: "devic-int-connect-dialog", role: "dialog", "aria-modal": "true", "aria-label": title, onClick: (e) => e.stopPropagation(), onSubmit: handleSubmit, children: [jsxs("div", { className: "devic-int-connect-header", children: [server?.logoUrl ? (jsx("img", { className: "devic-int-mcp-logo", src: server.logoUrl, alt: "", "aria-hidden": "true" })) : (jsx("span", { className: "devic-int-mcp-logo devic-int-mcp-logo-fallback", children: (server?.name ?? "M").slice(0, 1).toUpperCase() })), jsxs("div", { className: "devic-int-connect-heading", children: [jsx("strong", { children: title }), jsx("span", { children: mode === "oauth"
|
|
65
|
-
? "You'll be sent to the server to finish authorising."
|
|
69
|
+
? t("You'll be sent to the server to finish authorising.")
|
|
66
70
|
: mode === "header"
|
|
67
|
-
? "Your key goes straight to the server — only you can use it."
|
|
68
|
-
: "This server needs no credentials." })] }), jsx("button", { type: "button", className: "devic-int-close", onClick: onCancel, "aria-label": "Close", children: "\u00D7" })] }), jsxs("div", { className: "devic-int-connect-body", children: [error && jsx("div", { className: "devic-int-error", children: error }), custom && (jsxs(Fragment, { children: [jsxs("label", { className: "devic-int-connect-field", children: [jsx("span", { className: "devic-int-connect-label", children: "Server URL" }), jsx("input", { autoFocus: true, type: "url", value: url, onChange: (e) => setUrl(e.target.value), placeholder: "https://mcp.example.com/sse", spellCheck: false, autoComplete: "off", "aria-invalid": urlInvalid || undefined, "data-invalid": urlInvalid || undefined }), jsx("small", { className: "devic-int-connect-hint", children: "Must be reachable over https on a public address." })] }), jsxs("label", { className: "devic-int-connect-field", children: [jsxs("span", { className: "devic-int-connect-label", children: ["Name ", jsx("em", { children: "(optional)" })] }), jsx("input", { type: "text", value: name, onChange: (e) => setName(e.target.value), placeholder: "What you want to call it", autoComplete: "off" })] }), jsxs("label", { className: "devic-int-connect-field", children: [jsx("span", { className: "devic-int-connect-label", children: "Authentication" }), jsxs("select", { value: mode, onChange: (e) => setMode(e.target.value), children: [jsx("option", { value: "oauth", children: "Sign in (OAuth)" }), jsx("option", { value: "header", children: "API key" }), jsx("option", { value: "none", children: "None" })] })] })] })), mode === "header" && (jsxs("label", { className: "devic-int-connect-field", children: [jsx("span", { className: "devic-int-connect-label", children: headerLabel }), jsx("input", { autoFocus: !custom, type: "password", value: key, onChange: (e) => setKey(e.target.value), autoComplete: "new-password", spellCheck: false, "aria-invalid": keyInvalid || undefined, "data-invalid": keyInvalid || undefined })] })), mode === "oauth" && (custom || server?.allowClientCredentials) && (jsxs("div", { className: "devic-int-mcp-advanced", children: [needsClientCredentials && (jsx("div", { className: "devic-int-notice", children: "This server needs its own OAuth application. Register one with it and paste the details below." })), jsxs("button", { type: "button", className: "devic-int-mcp-disclosure", onClick: () => setShowClient((prev) => !prev), "aria-expanded": showClient, children: [showClient ? "▾" : "▸", " Use my own OAuth application"] }), showClient && (jsxs(Fragment, { children: [jsxs("label", { className: "devic-int-connect-field", children: [jsxs("span", { className: "devic-int-connect-label", children: ["Client ID ", jsx("em", { children: "(optional)" })] }), jsx("input", { type: "text", value: clientId, onChange: (e) => setClientId(e.target.value), autoComplete: "off", spellCheck: false })] }), jsxs("label", { className: "devic-int-connect-field", children: [jsxs("span", { className: "devic-int-connect-label", children: ["Client secret ", jsx("em", { children: "(optional)" })] }), jsx("input", { type: "password", value: clientSecret, onChange: (e) => setClientSecret(e.target.value), autoComplete: "new-password", spellCheck: false })] }), callbackUrl && (jsxs("label", { className: "devic-int-connect-field", children: [jsx("span", { className: "devic-int-connect-label", children: "Authorised redirect URI" }), jsx("input", { readOnly: true, value: callbackUrl, spellCheck: false }), jsxs("small", { className: "devic-int-connect-hint", children: ["Add this to your OAuth application, exactly as shown.", " ", jsx("button", { type: "button", className: "devic-int-mcp-copy", onClick: () => {
|
|
71
|
+
? t("Your key goes straight to the server — only you can use it.")
|
|
72
|
+
: t("This server needs no credentials.") })] }), jsx("button", { type: "button", className: "devic-int-close", onClick: onCancel, "aria-label": t("Close"), children: "\u00D7" })] }), jsxs("div", { className: "devic-int-connect-body", children: [error && jsx("div", { className: "devic-int-error", children: error }), custom && (jsxs(Fragment, { children: [jsxs("label", { className: "devic-int-connect-field", children: [jsx("span", { className: "devic-int-connect-label", children: t("Server URL") }), jsx("input", { autoFocus: true, type: "url", value: url, onChange: (e) => setUrl(e.target.value), placeholder: "https://mcp.example.com/sse", spellCheck: false, autoComplete: "off", "aria-invalid": urlInvalid || undefined, "data-invalid": urlInvalid || undefined }), jsx("small", { className: "devic-int-connect-hint", children: t("Must be reachable over https on a public address.") })] }), jsxs("label", { className: "devic-int-connect-field", children: [jsxs("span", { className: "devic-int-connect-label", children: [t("Name"), " ", jsx("em", { children: t("(optional)") })] }), jsx("input", { type: "text", value: name, onChange: (e) => setName(e.target.value), placeholder: t("What you want to call it"), autoComplete: "off" })] }), jsxs("label", { className: "devic-int-connect-field", children: [jsx("span", { className: "devic-int-connect-label", children: t("Authentication") }), jsxs("select", { value: mode, onChange: (e) => setMode(e.target.value), children: [jsx("option", { value: "oauth", children: t("Sign in (OAuth)") }), jsx("option", { value: "header", children: t("API key") }), jsx("option", { value: "none", children: t("None") })] })] })] })), mode === "header" && (jsxs("label", { className: "devic-int-connect-field", children: [jsx("span", { className: "devic-int-connect-label", children: headerLabel }), jsx("input", { autoFocus: !custom, type: "password", value: key, onChange: (e) => setKey(e.target.value), autoComplete: "new-password", spellCheck: false, "aria-invalid": keyInvalid || undefined, "data-invalid": keyInvalid || undefined })] })), mode === "oauth" && (custom || server?.allowClientCredentials) && (jsxs("div", { className: "devic-int-mcp-advanced", children: [needsClientCredentials && (jsx("div", { className: "devic-int-notice", children: t("This server needs its own OAuth application. Register one with it and paste the details below.") })), jsxs("button", { type: "button", className: "devic-int-mcp-disclosure", onClick: () => setShowClient((prev) => !prev), "aria-expanded": showClient, children: [showClient ? "▾" : "▸", " ", t("Use my own OAuth application")] }), showClient && (jsxs(Fragment, { children: [jsxs("label", { className: "devic-int-connect-field", children: [jsxs("span", { className: "devic-int-connect-label", children: [t("Client ID"), " ", jsx("em", { children: t("(optional)") })] }), jsx("input", { type: "text", value: clientId, onChange: (e) => setClientId(e.target.value), autoComplete: "off", spellCheck: false })] }), jsxs("label", { className: "devic-int-connect-field", children: [jsxs("span", { className: "devic-int-connect-label", children: [t("Client secret"), " ", jsx("em", { children: t("(optional)") })] }), jsx("input", { type: "password", value: clientSecret, onChange: (e) => setClientSecret(e.target.value), autoComplete: "new-password", spellCheck: false })] }), callbackUrl && (jsxs("label", { className: "devic-int-connect-field", children: [jsx("span", { className: "devic-int-connect-label", children: t("Authorised redirect URI") }), jsx("input", { readOnly: true, value: callbackUrl, spellCheck: false }), jsxs("small", { className: "devic-int-connect-hint", children: [t("Add this to your OAuth application, exactly as shown."), " ", jsx("button", { type: "button", className: "devic-int-mcp-copy", onClick: () => {
|
|
69
73
|
void navigator.clipboard
|
|
70
74
|
?.writeText(callbackUrl)
|
|
71
75
|
.then(() => setCopied(true))
|
|
72
76
|
.catch(() => undefined);
|
|
73
|
-
}, children: copied ? "Copied" : "Copy" })] })] }))] }))] }))] }), jsxs("div", { className: "devic-int-connect-actions", children: [jsx("button", { type: "button", className: "devic-int-btn", onClick: onCancel, disabled: submitting, children: "Cancel" }), jsx("button", { type: "submit", className: "devic-int-btn devic-int-btn-primary", disabled: submitting, children: submitting
|
|
77
|
+
}, children: copied ? t("Copied") : t("Copy") })] })] }))] }))] }))] }), jsxs("div", { className: "devic-int-connect-actions", children: [jsx("button", { type: "button", className: "devic-int-btn", onClick: onCancel, disabled: submitting, children: t("Cancel") }), jsx("button", { type: "submit", className: "devic-int-btn devic-int-btn-primary", disabled: submitting, children: submitting
|
|
78
|
+
? t("Connecting…")
|
|
79
|
+
: mode === "oauth"
|
|
80
|
+
? t("Continue")
|
|
81
|
+
: t("Connect") })] })] }) }), document.body);
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
export { McpConnectForm };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"McpConnectForm.js","sources":["../../../../src/components/IntegrationsModal/McpConnectForm.tsx"],"sourcesContent":["import { useMemo, useState, type FormEvent, type JSX } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport type { TenantMcpAuthInput, TenantMcpServer } from \"../../api/types\";\nimport { isDarkTheme, themeVars, type DevicTheme } from \"../theme\";\n\nexport interface McpConnectFormProps {\n /**\n * The server being connected. `null` is the \"add your own\" case, where the\n * url and the name are the end user's to type.\n */\n server: TenantMcpServer | null;\n submitting?: boolean;\n error?: string | null;\n /**\n * The redirect URI the end user must authorise on their own OAuth\n * application, when the server told us it has no dynamic registration.\n */\n callbackUrl?: string;\n /** Open the client-credentials fields already expanded. */\n needsClientCredentials?: boolean;\n theme?: DevicTheme;\n onCancel: () => void;\n onSubmit: (values: {\n url?: string;\n name?: string;\n auth: TenantMcpAuthInput;\n }) => void;\n}\n\n/**\n * What an MCP server asks the end user for.\n *\n * Both ways in end here — a server the developer listed and one the user\n * brings — because the difference between them is only who typed the url.\n * Everything after that is the same question: a key, or a trip to a consent\n * screen.\n *\n * A dialog rather than something inside a card: the OAuth-application fields\n * carry a redirect URI the user has to copy somewhere else, which does not fit\n * in a column of a grid without becoming a stack of clipped text.\n *\n * Values go straight into the connection and are never kept here.\n */\nexport function McpConnectForm({\n server,\n submitting,\n error,\n callbackUrl,\n needsClientCredentials,\n theme,\n onCancel,\n onSubmit,\n}: McpConnectFormProps): JSX.Element {\n const custom = !server;\n const [url, setUrl] = useState(server?.url ?? \"\");\n const [name, setName] = useState(server?.name ?? \"\");\n const [key, setKey] = useState(\"\");\n const [clientId, setClientId] = useState(\"\");\n const [clientSecret, setClientSecret] = useState(\"\");\n const [showClient, setShowClient] = useState(!!needsClientCredentials);\n const [copied, setCopied] = useState(false);\n const [touched, setTouched] = useState(false);\n\n // A server the user brings could be anything, so the form offers both ways\n // and lets them say. One the developer listed already declared how it\n // authenticates, and asking again would only invite a wrong answer.\n const [mode, setMode] = useState<TenantMcpAuthInput[\"mode\"]>(\n server?.authMode ?? \"oauth\"\n );\n\n const headerLabel = server?.headerName\n ? `${server.headerName} value`\n : \"API key\";\n\n const urlInvalid = useMemo(\n () => custom && touched && !/^https:\\/\\/.+/i.test(url.trim()),\n [custom, touched, url]\n );\n const keyInvalid = touched && mode === \"header\" && !key.trim();\n\n const handleSubmit = (event: FormEvent) => {\n event.preventDefault();\n setTouched(true);\n if (submitting) return;\n if (custom && !/^https:\\/\\/.+/i.test(url.trim())) return;\n if (mode === \"header\" && !key.trim()) return;\n\n onSubmit({\n url: custom ? url.trim() : undefined,\n name: name.trim() || undefined,\n auth: {\n mode,\n headerValue: mode === \"header\" ? key.trim() : undefined,\n upstreamOAuth:\n mode === \"oauth\" && clientId.trim()\n ? {\n clientId: clientId.trim(),\n clientSecret: clientSecret.trim() || undefined,\n }\n : undefined,\n },\n });\n };\n\n const title = custom ? \"Add an MCP server\" : `Connect ${server.name}`;\n\n return createPortal(\n <div\n className=\"devic-int-overlay devic-int-overlay-stacked\"\n style={themeVars(theme)}\n data-dark={isDarkTheme(theme)}\n onClick={onCancel}\n >\n <form\n className=\"devic-int-connect-dialog\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={title}\n onClick={(e) => e.stopPropagation()}\n onSubmit={handleSubmit}\n >\n <div className=\"devic-int-connect-header\">\n {server?.logoUrl ? (\n <img\n className=\"devic-int-mcp-logo\"\n src={server.logoUrl}\n alt=\"\"\n aria-hidden=\"true\"\n />\n ) : (\n <span className=\"devic-int-mcp-logo devic-int-mcp-logo-fallback\">\n {(server?.name ?? \"M\").slice(0, 1).toUpperCase()}\n </span>\n )}\n <div className=\"devic-int-connect-heading\">\n <strong>{title}</strong>\n <span>\n {mode === \"oauth\"\n ? \"You'll be sent to the server to finish authorising.\"\n : mode === \"header\"\n ? \"Your key goes straight to the server — only you can use it.\"\n : \"This server needs no credentials.\"}\n </span>\n </div>\n <button\n type=\"button\"\n className=\"devic-int-close\"\n onClick={onCancel}\n aria-label=\"Close\"\n >\n ×\n </button>\n </div>\n\n <div className=\"devic-int-connect-body\">\n {error && <div className=\"devic-int-error\">{error}</div>}\n\n {custom && (\n <>\n <label className=\"devic-int-connect-field\">\n <span className=\"devic-int-connect-label\">Server URL</span>\n <input\n autoFocus\n type=\"url\"\n value={url}\n onChange={(e) => setUrl(e.target.value)}\n placeholder=\"https://mcp.example.com/sse\"\n spellCheck={false}\n autoComplete=\"off\"\n aria-invalid={urlInvalid || undefined}\n data-invalid={urlInvalid || undefined}\n />\n <small className=\"devic-int-connect-hint\">\n Must be reachable over https on a public address.\n </small>\n </label>\n\n <label className=\"devic-int-connect-field\">\n <span className=\"devic-int-connect-label\">\n Name <em>(optional)</em>\n </span>\n <input\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n placeholder=\"What you want to call it\"\n autoComplete=\"off\"\n />\n </label>\n\n <label className=\"devic-int-connect-field\">\n <span className=\"devic-int-connect-label\">Authentication</span>\n <select\n value={mode}\n onChange={(e) =>\n setMode(e.target.value as TenantMcpAuthInput[\"mode\"])\n }\n >\n <option value=\"oauth\">Sign in (OAuth)</option>\n <option value=\"header\">API key</option>\n <option value=\"none\">None</option>\n </select>\n </label>\n </>\n )}\n\n {mode === \"header\" && (\n <label className=\"devic-int-connect-field\">\n <span className=\"devic-int-connect-label\">{headerLabel}</span>\n <input\n autoFocus={!custom}\n type=\"password\"\n value={key}\n onChange={(e) => setKey(e.target.value)}\n autoComplete=\"new-password\"\n spellCheck={false}\n aria-invalid={keyInvalid || undefined}\n data-invalid={keyInvalid || undefined}\n />\n </label>\n )}\n\n {mode === \"oauth\" && (custom || server?.allowClientCredentials) && (\n <div className=\"devic-int-mcp-advanced\">\n {/* Not a detail to bury: a server whose authorization server has\n no dynamic client registration cannot be connected at all\n without these, and the message that says so arrives only\n after the first attempt. */}\n {needsClientCredentials && (\n <div className=\"devic-int-notice\">\n This server needs its own OAuth application. Register one with\n it and paste the details below.\n </div>\n )}\n <button\n type=\"button\"\n className=\"devic-int-mcp-disclosure\"\n onClick={() => setShowClient((prev) => !prev)}\n aria-expanded={showClient}\n >\n {showClient ? \"▾\" : \"▸\"} Use my own OAuth application\n </button>\n\n {showClient && (\n <>\n <label className=\"devic-int-connect-field\">\n <span className=\"devic-int-connect-label\">\n Client ID <em>(optional)</em>\n </span>\n <input\n type=\"text\"\n value={clientId}\n onChange={(e) => setClientId(e.target.value)}\n autoComplete=\"off\"\n spellCheck={false}\n />\n </label>\n <label className=\"devic-int-connect-field\">\n <span className=\"devic-int-connect-label\">\n Client secret <em>(optional)</em>\n </span>\n <input\n type=\"password\"\n value={clientSecret}\n onChange={(e) => setClientSecret(e.target.value)}\n autoComplete=\"new-password\"\n spellCheck={false}\n />\n </label>\n {callbackUrl && (\n <label className=\"devic-int-connect-field\">\n <span className=\"devic-int-connect-label\">\n Authorised redirect URI\n </span>\n <input readOnly value={callbackUrl} spellCheck={false} />\n <small className=\"devic-int-connect-hint\">\n Add this to your OAuth application, exactly as shown.{\" \"}\n <button\n type=\"button\"\n className=\"devic-int-mcp-copy\"\n onClick={() => {\n void navigator.clipboard\n ?.writeText(callbackUrl)\n .then(() => setCopied(true))\n .catch(() => undefined);\n }}\n >\n {copied ? \"Copied\" : \"Copy\"}\n </button>\n </small>\n </label>\n )}\n </>\n )}\n </div>\n )}\n </div>\n\n <div className=\"devic-int-connect-actions\">\n <button\n type=\"button\"\n className=\"devic-int-btn\"\n onClick={onCancel}\n disabled={submitting}\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n className=\"devic-int-btn devic-int-btn-primary\"\n disabled={submitting}\n >\n {submitting ? \"Connecting…\" : mode === \"oauth\" ? \"Continue\" : \"Connect\"}\n </button>\n </div>\n </form>\n </div>,\n document.body\n );\n}\n\nexport default McpConnectForm;\n"],"names":["_jsx","_jsxs","_Fragment"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"McpConnectForm.js","sources":["../../../../src/components/IntegrationsModal/McpConnectForm.tsx"],"sourcesContent":["import { useMemo, useState, type FormEvent, type JSX } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport type { TenantMcpAuthInput, TenantMcpServer } from \"../../api/types\";\nimport { isDarkTheme, themeVars, type DevicTheme } from \"../theme\";\nimport { useTranslations } from \"../../i18n\";\n\nexport interface McpConnectFormProps {\n /**\n * The server being connected. `null` is the \"add your own\" case, where the\n * url and the name are the end user's to type.\n */\n server: TenantMcpServer | null;\n submitting?: boolean;\n error?: string | null;\n /**\n * The redirect URI the end user must authorise on their own OAuth\n * application, when the server told us it has no dynamic registration.\n */\n callbackUrl?: string;\n /** Open the client-credentials fields already expanded. */\n needsClientCredentials?: boolean;\n theme?: DevicTheme;\n onCancel: () => void;\n onSubmit: (values: {\n url?: string;\n name?: string;\n auth: TenantMcpAuthInput;\n }) => void;\n}\n\n/**\n * What an MCP server asks the end user for.\n *\n * Both ways in end here — a server the developer listed and one the user\n * brings — because the difference between them is only who typed the url.\n * Everything after that is the same question: a key, or a trip to a consent\n * screen.\n *\n * A dialog rather than something inside a card: the OAuth-application fields\n * carry a redirect URI the user has to copy somewhere else, which does not fit\n * in a column of a grid without becoming a stack of clipped text.\n *\n * Values go straight into the connection and are never kept here.\n */\nexport function McpConnectForm({\n server,\n submitting,\n error,\n callbackUrl,\n needsClientCredentials,\n theme,\n onCancel,\n onSubmit,\n}: McpConnectFormProps): JSX.Element {\n const custom = !server;\n const t = useTranslations();\n const [url, setUrl] = useState(server?.url ?? \"\");\n const [name, setName] = useState(server?.name ?? \"\");\n const [key, setKey] = useState(\"\");\n const [clientId, setClientId] = useState(\"\");\n const [clientSecret, setClientSecret] = useState(\"\");\n const [showClient, setShowClient] = useState(!!needsClientCredentials);\n const [copied, setCopied] = useState(false);\n const [touched, setTouched] = useState(false);\n\n // A server the user brings could be anything, so the form offers both ways\n // and lets them say. One the developer listed already declared how it\n // authenticates, and asking again would only invite a wrong answer.\n const [mode, setMode] = useState<TenantMcpAuthInput[\"mode\"]>(\n server?.authMode ?? \"oauth\"\n );\n\n const headerLabel = server?.headerName\n ? t(\"{header} value\", { header: server.headerName })\n : t(\"API key\");\n\n const urlInvalid = useMemo(\n () => custom && touched && !/^https:\\/\\/.+/i.test(url.trim()),\n [custom, touched, url]\n );\n const keyInvalid = touched && mode === \"header\" && !key.trim();\n\n const handleSubmit = (event: FormEvent) => {\n event.preventDefault();\n setTouched(true);\n if (submitting) return;\n if (custom && !/^https:\\/\\/.+/i.test(url.trim())) return;\n if (mode === \"header\" && !key.trim()) return;\n\n onSubmit({\n url: custom ? url.trim() : undefined,\n name: name.trim() || undefined,\n auth: {\n mode,\n headerValue: mode === \"header\" ? key.trim() : undefined,\n upstreamOAuth:\n mode === \"oauth\" && clientId.trim()\n ? {\n clientId: clientId.trim(),\n clientSecret: clientSecret.trim() || undefined,\n }\n : undefined,\n },\n });\n };\n\n const title = custom\n ? t(\"Add an MCP server\")\n : t(\"Connect {server}\", { server: server.name });\n\n return createPortal(\n <div\n className=\"devic-int-overlay devic-int-overlay-stacked\"\n style={themeVars(theme)}\n data-dark={isDarkTheme(theme)}\n onClick={onCancel}\n >\n <form\n className=\"devic-int-connect-dialog\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={title}\n onClick={(e) => e.stopPropagation()}\n onSubmit={handleSubmit}\n >\n <div className=\"devic-int-connect-header\">\n {server?.logoUrl ? (\n <img\n className=\"devic-int-mcp-logo\"\n src={server.logoUrl}\n alt=\"\"\n aria-hidden=\"true\"\n />\n ) : (\n <span className=\"devic-int-mcp-logo devic-int-mcp-logo-fallback\">\n {(server?.name ?? \"M\").slice(0, 1).toUpperCase()}\n </span>\n )}\n <div className=\"devic-int-connect-heading\">\n <strong>{title}</strong>\n <span>\n {mode === \"oauth\"\n ? t(\"You'll be sent to the server to finish authorising.\")\n : mode === \"header\"\n ? t(\n \"Your key goes straight to the server — only you can use it.\"\n )\n : t(\"This server needs no credentials.\")}\n </span>\n </div>\n <button\n type=\"button\"\n className=\"devic-int-close\"\n onClick={onCancel}\n aria-label={t(\"Close\")}\n >\n ×\n </button>\n </div>\n\n <div className=\"devic-int-connect-body\">\n {error && <div className=\"devic-int-error\">{error}</div>}\n\n {custom && (\n <>\n <label className=\"devic-int-connect-field\">\n <span className=\"devic-int-connect-label\">\n {t(\"Server URL\")}\n </span>\n <input\n autoFocus\n type=\"url\"\n value={url}\n onChange={(e) => setUrl(e.target.value)}\n placeholder=\"https://mcp.example.com/sse\"\n spellCheck={false}\n autoComplete=\"off\"\n aria-invalid={urlInvalid || undefined}\n data-invalid={urlInvalid || undefined}\n />\n <small className=\"devic-int-connect-hint\">\n {t(\"Must be reachable over https on a public address.\")}\n </small>\n </label>\n\n <label className=\"devic-int-connect-field\">\n <span className=\"devic-int-connect-label\">\n {t(\"Name\")} <em>{t(\"(optional)\")}</em>\n </span>\n <input\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n placeholder={t(\"What you want to call it\")}\n autoComplete=\"off\"\n />\n </label>\n\n <label className=\"devic-int-connect-field\">\n <span className=\"devic-int-connect-label\">\n {t(\"Authentication\")}\n </span>\n <select\n value={mode}\n onChange={(e) =>\n setMode(e.target.value as TenantMcpAuthInput[\"mode\"])\n }\n >\n <option value=\"oauth\">{t(\"Sign in (OAuth)\")}</option>\n <option value=\"header\">{t(\"API key\")}</option>\n <option value=\"none\">{t(\"None\")}</option>\n </select>\n </label>\n </>\n )}\n\n {mode === \"header\" && (\n <label className=\"devic-int-connect-field\">\n <span className=\"devic-int-connect-label\">{headerLabel}</span>\n <input\n autoFocus={!custom}\n type=\"password\"\n value={key}\n onChange={(e) => setKey(e.target.value)}\n autoComplete=\"new-password\"\n spellCheck={false}\n aria-invalid={keyInvalid || undefined}\n data-invalid={keyInvalid || undefined}\n />\n </label>\n )}\n\n {mode === \"oauth\" && (custom || server?.allowClientCredentials) && (\n <div className=\"devic-int-mcp-advanced\">\n {/* Not a detail to bury: a server whose authorization server has\n no dynamic client registration cannot be connected at all\n without these, and the message that says so arrives only\n after the first attempt. */}\n {needsClientCredentials && (\n <div className=\"devic-int-notice\">\n {t(\n \"This server needs its own OAuth application. Register one with it and paste the details below.\"\n )}\n </div>\n )}\n <button\n type=\"button\"\n className=\"devic-int-mcp-disclosure\"\n onClick={() => setShowClient((prev) => !prev)}\n aria-expanded={showClient}\n >\n {showClient ? \"▾\" : \"▸\"} {t(\"Use my own OAuth application\")}\n </button>\n\n {showClient && (\n <>\n <label className=\"devic-int-connect-field\">\n <span className=\"devic-int-connect-label\">\n {t(\"Client ID\")} <em>{t(\"(optional)\")}</em>\n </span>\n <input\n type=\"text\"\n value={clientId}\n onChange={(e) => setClientId(e.target.value)}\n autoComplete=\"off\"\n spellCheck={false}\n />\n </label>\n <label className=\"devic-int-connect-field\">\n <span className=\"devic-int-connect-label\">\n {t(\"Client secret\")} <em>{t(\"(optional)\")}</em>\n </span>\n <input\n type=\"password\"\n value={clientSecret}\n onChange={(e) => setClientSecret(e.target.value)}\n autoComplete=\"new-password\"\n spellCheck={false}\n />\n </label>\n {callbackUrl && (\n <label className=\"devic-int-connect-field\">\n <span className=\"devic-int-connect-label\">\n {t(\"Authorised redirect URI\")}\n </span>\n <input readOnly value={callbackUrl} spellCheck={false} />\n <small className=\"devic-int-connect-hint\">\n {t(\n \"Add this to your OAuth application, exactly as shown.\"\n )}{\" \"}\n <button\n type=\"button\"\n className=\"devic-int-mcp-copy\"\n onClick={() => {\n void navigator.clipboard\n ?.writeText(callbackUrl)\n .then(() => setCopied(true))\n .catch(() => undefined);\n }}\n >\n {copied ? t(\"Copied\") : t(\"Copy\")}\n </button>\n </small>\n </label>\n )}\n </>\n )}\n </div>\n )}\n </div>\n\n <div className=\"devic-int-connect-actions\">\n <button\n type=\"button\"\n className=\"devic-int-btn\"\n onClick={onCancel}\n disabled={submitting}\n >\n {t(\"Cancel\")}\n </button>\n <button\n type=\"submit\"\n className=\"devic-int-btn devic-int-btn-primary\"\n disabled={submitting}\n >\n {submitting\n ? t(\"Connecting…\")\n : mode === \"oauth\"\n ? t(\"Continue\")\n : t(\"Connect\")}\n </button>\n </div>\n </form>\n </div>,\n document.body\n );\n}\n\nexport default McpConnectForm;\n"],"names":["_jsx","_jsxs","_Fragment"],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
3
3
|
import { McpConnectForm } from './McpConnectForm.js';
|
|
4
|
+
import { useTranslations } from '../../i18n/useTranslations.js';
|
|
4
5
|
|
|
5
6
|
/** A random value tying an OAuth round trip to the window that started it. */
|
|
6
7
|
function newNonce() {
|
|
@@ -9,15 +10,16 @@ function newNonce() {
|
|
|
9
10
|
return c.randomUUID();
|
|
10
11
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
11
12
|
}
|
|
12
|
-
function stateOf(server) {
|
|
13
|
+
function stateOf(t, server) {
|
|
13
14
|
const status = server.connection?.status;
|
|
14
15
|
if (status === "active")
|
|
15
|
-
return { key: "connected", label: "Connected" };
|
|
16
|
-
if (status === "error")
|
|
17
|
-
return { key: "reconnect", label: "Needs attention" };
|
|
16
|
+
return { key: "connected", label: t("Connected") };
|
|
17
|
+
if (status === "error") {
|
|
18
|
+
return { key: "reconnect", label: t("Needs attention") };
|
|
19
|
+
}
|
|
18
20
|
if (status === "pending_auth")
|
|
19
|
-
return { key: "reconnect", label: "Waiting for sign-in" };
|
|
20
|
-
return { key: "disconnected", label: "Not connected" };
|
|
21
|
+
return { key: "reconnect", label: t("Waiting for sign-in") };
|
|
22
|
+
return { key: "disconnected", label: t("Not connected") };
|
|
21
23
|
}
|
|
22
24
|
function matches(server, query) {
|
|
23
25
|
const q = query.trim().toLowerCase();
|
|
@@ -46,6 +48,7 @@ function keyOf(server) {
|
|
|
46
48
|
* cancelled consent screen posts nothing back, so the close is watched too.
|
|
47
49
|
*/
|
|
48
50
|
function McpServersSection({ state, theme, query = "", }) {
|
|
51
|
+
const t = useTranslations();
|
|
49
52
|
const { servers, allowCustom, limits, loading, client, scope, refresh } = state;
|
|
50
53
|
const [actionError, setActionError] = useState(null);
|
|
51
54
|
const [busyKey, setBusyKey] = useState(null);
|
|
@@ -141,7 +144,7 @@ function McpServersSection({ state, theme, query = "", }) {
|
|
|
141
144
|
if (result.status === "error") {
|
|
142
145
|
popup?.close();
|
|
143
146
|
setBusyKey(null);
|
|
144
|
-
setFormError(result.error ?? "The server could not be reached.");
|
|
147
|
+
setFormError(result.error ?? t("The server could not be reached."));
|
|
145
148
|
return;
|
|
146
149
|
}
|
|
147
150
|
if (!result.authorizationUrl) {
|
|
@@ -197,32 +200,42 @@ function McpServersSection({ state, theme, query = "", }) {
|
|
|
197
200
|
// over an empty box, which says less than saying nothing.
|
|
198
201
|
if (!servers.length && !allowCustom)
|
|
199
202
|
return null;
|
|
200
|
-
return (jsxs("div", { className: "devic-int-mcp-section", children: [jsxs("div", { className: "devic-int-mcp-heading", children: [jsx("h4", { children: "MCP servers" }), jsx("span", { children: "Connect a server and its tools become available in this chat." })] }), actionError && jsx("div", { className: "devic-int-error", children: actionError }), blockedUrl && (jsxs("div", { className: "devic-int-notice", children: ["Your browser blocked the pop-up.", " ", jsx("a", { href: blockedUrl.url, target: "_blank", rel: "noopener noreferrer", onClick: () => {
|
|
203
|
+
return (jsxs("div", { className: "devic-int-mcp-section", children: [jsxs("div", { className: "devic-int-mcp-heading", children: [jsx("h4", { children: t("MCP servers") }), jsx("span", { children: t("Connect a server and its tools become available in this chat.") })] }), actionError && jsx("div", { className: "devic-int-error", children: actionError }), blockedUrl && (jsxs("div", { className: "devic-int-notice", children: [t("Your browser blocked the pop-up."), " ", jsx("a", { href: blockedUrl.url, target: "_blank", rel: "noopener noreferrer", onClick: () => {
|
|
201
204
|
pendingRef.current = null;
|
|
202
205
|
setBlockedUrl(null);
|
|
203
|
-
}, children: "Open the authorisation page" }), " ", "and come back
|
|
204
|
-
const cardState = stateOf(server);
|
|
206
|
+
}, children: t("Open the authorisation page") }), " ", t("and come back — then use Refresh.")] })), visible.length > 0 && (jsx("div", { className: "devic-int-grid", children: visible.map((server) => {
|
|
207
|
+
const cardState = stateOf(t, server);
|
|
205
208
|
const rowKey = keyOf(server);
|
|
206
209
|
const busy = busyKey === rowKey;
|
|
207
210
|
const connection = server.connection;
|
|
208
211
|
return (jsxs("div", { className: "devic-int-card", "data-state": cardState.key, children: [jsxs("div", { className: "devic-int-card-head", children: [server.logoUrl ? (jsx("img", { className: "devic-int-mcp-logo", src: server.logoUrl, alt: "", "aria-hidden": "true" })) : (jsx("span", { className: "devic-int-mcp-logo devic-int-mcp-logo-fallback", children: server.name.slice(0, 1).toUpperCase() })), jsxs("span", { className: "devic-int-state", children: [jsx("span", { className: "devic-int-dot", "data-ok": cardState.key === "connected", "data-off": cardState.key === "disconnected", "aria-hidden": "true" }), cardState.label] })] }), jsx("div", { className: "devic-int-name", title: server.name, children: server.name }), jsx("div", { className: "devic-int-description", title: server.description ?? server.url, children: server.description ?? server.url }), connection?.shared && (
|
|
209
212
|
// Worth saying before they wonder why they cannot remove it.
|
|
210
|
-
jsx("div", { className: "devic-int-mcp-note", children: "Connected for everyone on this account" })), connection?.status === "active" &&
|
|
211
|
-
typeof connection.toolCount === "number" && (
|
|
213
|
+
jsx("div", { className: "devic-int-mcp-note", children: t("Connected for everyone on this account") })), connection?.status === "active" &&
|
|
214
|
+
typeof connection.toolCount === "number" && (jsx("div", { className: "devic-int-mcp-note", children: connection.toolCount === 1
|
|
215
|
+
? t("1 tool available")
|
|
216
|
+
: t("{count} tools available", {
|
|
217
|
+
count: connection.toolCount,
|
|
218
|
+
}) })), connection?.lastProbeError && (jsx("div", { className: "devic-int-mcp-note devic-int-mcp-note-warn", title: connection.lastProbeError, children: connection.lastProbeError })), jsx("button", { type: "button", className: `devic-int-btn devic-int-btn-block${cardState.key === "connected" ? "" : " devic-int-btn-primary"}`, onClick: () => openPrompt(server), disabled: busy ||
|
|
212
219
|
!!busyKey ||
|
|
213
220
|
connection?.readOnly ||
|
|
214
221
|
(!connection && atLimit), title: connection?.readOnly
|
|
215
|
-
? "This server was connected for the whole account."
|
|
222
|
+
? t("This server was connected for the whole account.")
|
|
216
223
|
: !connection && atLimit
|
|
217
|
-
?
|
|
224
|
+
? t("You can have {max} connected at a time.", {
|
|
225
|
+
max: limits.maxServers,
|
|
226
|
+
})
|
|
218
227
|
: undefined, children: busy
|
|
219
|
-
? "Waiting…"
|
|
228
|
+
? t("Waiting…")
|
|
220
229
|
: cardState.key === "disconnected"
|
|
221
|
-
? "Connect"
|
|
222
|
-
: "Reconnect" }), connection && !connection.readOnly && (jsx("button", { type: "button", className: "devic-int-btn devic-int-btn-block devic-int-mcp-remove", onClick: () => void disconnect(server), disabled: !!busyKey, children: "Disconnect" }))] }, rowKey));
|
|
230
|
+
? t("Connect")
|
|
231
|
+
: t("Reconnect") }), connection && !connection.readOnly && (jsx("button", { type: "button", className: "devic-int-btn devic-int-btn-block devic-int-mcp-remove", onClick: () => void disconnect(server), disabled: !!busyKey, children: t("Disconnect") }))] }, rowKey));
|
|
223
232
|
}) })), allowCustom && (jsx("button", { type: "button", className: "devic-int-btn devic-int-mcp-add", onClick: () => openPrompt(null), disabled: loading || !!busyKey || atLimit, title: atLimit
|
|
224
|
-
?
|
|
225
|
-
|
|
233
|
+
? limits.maxServers === 1
|
|
234
|
+
? t("You can have 1 MCP server connected at a time.")
|
|
235
|
+
: t("You can have {max} MCP servers connected at a time.", {
|
|
236
|
+
max: limits.maxServers,
|
|
237
|
+
})
|
|
238
|
+
: undefined, children: t("+ Add your own MCP") })), prompt && (jsx(McpConnectForm, { server: prompt.server, callbackUrl: prompt.callbackUrl, needsClientCredentials: prompt.needsClientCredentials, submitting: busyKey === (prompt.server ? keyOf(prompt.server) : "new"), error: formError, theme: theme, onCancel: () => {
|
|
226
239
|
setPrompt(null);
|
|
227
240
|
setFormError(null);
|
|
228
241
|
}, onSubmit: submit }))] }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"McpServersSection.js","sources":["../../../../src/components/IntegrationsModal/McpServersSection.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState, type JSX } from \"react\";\nimport type {\n TenantMcpAuthInput,\n TenantMcpConnectResult,\n TenantMcpServer,\n} from \"../../api/types\";\nimport type { DevicTheme } from \"../theme\";\nimport { McpConnectForm } from \"./McpConnectForm\";\nimport type { TenantMcpState } from \"./useTenantMcp\";\n\nexport interface McpServersSectionProps {\n state: TenantMcpState;\n theme?: DevicTheme;\n /** Filter typed in the modal's search box. */\n query?: string;\n}\n\n/** Message the OAuth callback page posts back when an MCP connection is done. */\ninterface McpCallbackMessage {\n source?: string;\n type?: string;\n status?: string;\n returnTo?: string;\n}\n\n/** A random value tying an OAuth round trip to the window that started it. */\nfunction newNonce(): string {\n const c = typeof crypto !== \"undefined\" ? crypto : undefined;\n if (c?.randomUUID) return c.randomUUID();\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;\n}\n\nfunction stateOf(server: TenantMcpServer): {\n key: \"connected\" | \"reconnect\" | \"disconnected\";\n label: string;\n} {\n const status = server.connection?.status;\n if (status === \"active\") return { key: \"connected\", label: \"Connected\" };\n if (status === \"error\") return { key: \"reconnect\", label: \"Needs attention\" };\n if (status === \"pending_auth\")\n return { key: \"reconnect\", label: \"Waiting for sign-in\" };\n return { key: \"disconnected\", label: \"Not connected\" };\n}\n\nfunction matches(server: TenantMcpServer, query: string): boolean {\n const q = query.trim().toLowerCase();\n if (!q) return true;\n return (\n server.name.toLowerCase().includes(q) ||\n server.url.toLowerCase().includes(q) ||\n (server.description ?? \"\").toLowerCase().includes(q)\n );\n}\n\n/** Identity of a row, stable whether or not it has been connected yet. */\nfunction keyOf(server: TenantMcpServer): string {\n return server.connection?.id ?? server.templateId ?? server.url;\n}\n\n/**\n * The MCP servers section of the connected-apps panel.\n *\n * The end user sees one list: the servers their developer left ready to\n * connect, the ones they added themselves, and — unless it was switched off —\n * a button to add another. Both routes open the same form, because the only\n * difference between them is who typed the url.\n *\n * Connecting opens a consent screen in a popup. The popup is opened empty\n * *before* the request that produces its URL, because browsers only honour\n * `window.open` inside the gesture that triggered it; opening it after the\n * round trip is what gets it blocked. Two endings are watched, not one — a\n * cancelled consent screen posts nothing back, so the close is watched too.\n */\nexport function McpServersSection({\n state,\n theme,\n query = \"\",\n}: McpServersSectionProps): JSX.Element | null {\n const { servers, allowCustom, limits, loading, client, scope, refresh } = state;\n\n const [actionError, setActionError] = useState<string | null>(null);\n const [busyKey, setBusyKey] = useState<string | null>(null);\n const [blockedUrl, setBlockedUrl] = useState<{ url: string } | null>(null);\n /** The row whose form is open. `null` inside the object is \"add your own\". */\n const [prompt, setPrompt] = useState<{\n server: TenantMcpServer | null;\n callbackUrl?: string;\n needsClientCredentials?: boolean;\n } | null>(null);\n const [formError, setFormError] = useState<string | null>(null);\n\n const pendingRef = useRef<{ returnTo: string } | null>(null);\n const popupRef = useRef<Window | null>(null);\n const pollRef = useRef<number | null>(null);\n\n const finish = useCallback(() => {\n if (pollRef.current !== null) {\n window.clearInterval(pollRef.current);\n pollRef.current = null;\n }\n pendingRef.current = null;\n popupRef.current = null;\n setBusyKey(null);\n void refresh();\n }, [refresh]);\n\n // The callback page says it is done. A nudge, never the result: what is shown\n // comes from re-reading the server, so a forged message costs one extra fetch\n // and nothing else.\n useEffect(() => {\n const onMessage = (event: MessageEvent) => {\n const data = event.data as McpCallbackMessage | undefined;\n if (!data || data.source !== \"devic\") return;\n if (data.type !== \"mcp-connected\") return;\n const pending = pendingRef.current;\n if (!pending || data.returnTo !== pending.returnTo) return;\n popupRef.current?.close();\n finish();\n };\n window.addEventListener(\"message\", onMessage);\n return () => window.removeEventListener(\"message\", onMessage);\n }, [finish]);\n\n useEffect(\n () => () => {\n if (pollRef.current !== null) window.clearInterval(pollRef.current);\n },\n []\n );\n\n const openPrompt = (server: TenantMcpServer | null) => {\n setFormError(null);\n setActionError(null);\n setBlockedUrl(null);\n setPrompt({ server });\n };\n\n const submit = async (values: {\n url?: string;\n name?: string;\n auth: TenantMcpAuthInput;\n }) => {\n if (!client || !prompt) return;\n const server = prompt.server;\n const rowKey = server ? keyOf(server) : \"new\";\n setBusyKey(rowKey);\n setFormError(null);\n\n const nonce = newNonce();\n const returnTo = `${window.location.origin}/?devic_mcp=${nonce}`;\n // Opened empty inside the submit gesture and navigated once the URL is\n // known. Skipped when no browser is involved at all.\n const popup =\n values.auth.mode === \"oauth\"\n ? window.open(\n \"\",\n \"devic-mcp-oauth\",\n \"width=520,height=680,menubar=no,toolbar=no\"\n )\n : null;\n\n try {\n const existing = server?.connection;\n const result: TenantMcpConnectResult = existing\n ? await client.reconnectMcpServer(existing.id, {\n ...scope,\n returnTo,\n auth: values.auth,\n })\n : await client.connectMcpServer({\n ...scope,\n returnTo,\n templateId: server?.templateId,\n url: values.url,\n name: values.name,\n auth: values.auth,\n });\n\n if (result.requiresClientCredentials) {\n // Not a failure the user can do nothing about: they own the server, so\n // the form reopens with the fields and the redirect URI to authorise.\n popup?.close();\n setBusyKey(null);\n setPrompt({\n server,\n callbackUrl: result.callbackUrl,\n needsClientCredentials: true,\n });\n setFormError(result.error ?? null);\n return;\n }\n\n if (result.status === \"error\") {\n popup?.close();\n setBusyKey(null);\n setFormError(result.error ?? \"The server could not be reached.\");\n return;\n }\n\n if (!result.authorizationUrl) {\n // Connected outright: the key the user typed is the credential.\n popup?.close();\n setPrompt(null);\n finish();\n return;\n }\n\n pendingRef.current = { returnTo };\n setPrompt(null);\n if (popup && !popup.closed) {\n popupRef.current = popup;\n popup.location.href = result.authorizationUrl;\n pollRef.current = window.setInterval(() => {\n if (popup.closed) finish();\n }, 700);\n } else {\n // Blocked (Safari, in-app browsers, extensions): hand over the URL.\n setBlockedUrl({ url: result.authorizationUrl });\n setBusyKey(null);\n }\n } catch (err) {\n popup?.close();\n pendingRef.current = null;\n setBusyKey(null);\n setFormError(err instanceof Error ? err.message : String(err));\n }\n };\n\n const disconnect = async (server: TenantMcpServer) => {\n const connection = server.connection;\n if (!client || !connection || busyKey) return;\n setBusyKey(keyOf(server));\n setActionError(null);\n try {\n await client.disconnectMcpServer(connection.id, scope);\n await refresh();\n } catch (err) {\n setActionError(err instanceof Error ? err.message : String(err));\n } finally {\n setBusyKey(null);\n }\n };\n\n const visible = servers.filter((s) => matches(s, query));\n const atLimit = limits.used >= limits.maxServers;\n\n // Nothing offered and nothing addable: the whole section would be a heading\n // over an empty box, which says less than saying nothing.\n if (!servers.length && !allowCustom) return null;\n\n return (\n <div className=\"devic-int-mcp-section\">\n <div className=\"devic-int-mcp-heading\">\n <h4>MCP servers</h4>\n <span>Connect a server and its tools become available in this chat.</span>\n </div>\n\n {actionError && <div className=\"devic-int-error\">{actionError}</div>}\n\n {blockedUrl && (\n <div className=\"devic-int-notice\">\n Your browser blocked the pop-up.{\" \"}\n <a\n href={blockedUrl.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={() => {\n pendingRef.current = null;\n setBlockedUrl(null);\n }}\n >\n Open the authorisation page\n </a>{\" \"}\n and come back — then use Refresh.\n </div>\n )}\n\n {visible.length > 0 && (\n <div className=\"devic-int-grid\">\n {visible.map((server) => {\n const cardState = stateOf(server);\n const rowKey = keyOf(server);\n const busy = busyKey === rowKey;\n const connection = server.connection;\n return (\n <div\n key={rowKey}\n className=\"devic-int-card\"\n data-state={cardState.key}\n >\n <div className=\"devic-int-card-head\">\n {server.logoUrl ? (\n <img\n className=\"devic-int-mcp-logo\"\n src={server.logoUrl}\n alt=\"\"\n aria-hidden=\"true\"\n />\n ) : (\n <span className=\"devic-int-mcp-logo devic-int-mcp-logo-fallback\">\n {server.name.slice(0, 1).toUpperCase()}\n </span>\n )}\n <span className=\"devic-int-state\">\n <span\n className=\"devic-int-dot\"\n data-ok={cardState.key === \"connected\"}\n data-off={cardState.key === \"disconnected\"}\n aria-hidden=\"true\"\n />\n {cardState.label}\n </span>\n </div>\n\n <div className=\"devic-int-name\" title={server.name}>\n {server.name}\n </div>\n\n <div\n className=\"devic-int-description\"\n title={server.description ?? server.url}\n >\n {server.description ?? server.url}\n </div>\n\n {connection?.shared && (\n // Worth saying before they wonder why they cannot remove it.\n <div className=\"devic-int-mcp-note\">\n Connected for everyone on this account\n </div>\n )}\n\n {connection?.status === \"active\" &&\n typeof connection.toolCount === \"number\" && (\n <div className=\"devic-int-mcp-note\">\n {connection.toolCount} tool\n {connection.toolCount === 1 ? \"\" : \"s\"} available\n </div>\n )}\n\n {connection?.lastProbeError && (\n <div\n className=\"devic-int-mcp-note devic-int-mcp-note-warn\"\n title={connection.lastProbeError}\n >\n {connection.lastProbeError}\n </div>\n )}\n\n <button\n type=\"button\"\n className={`devic-int-btn devic-int-btn-block${\n cardState.key === \"connected\" ? \"\" : \" devic-int-btn-primary\"\n }`}\n onClick={() => openPrompt(server)}\n disabled={\n busy ||\n !!busyKey ||\n connection?.readOnly ||\n (!connection && atLimit)\n }\n title={\n connection?.readOnly\n ? \"This server was connected for the whole account.\"\n : !connection && atLimit\n ? `You can have ${limits.maxServers} connected at a time.`\n : undefined\n }\n >\n {busy\n ? \"Waiting…\"\n : cardState.key === \"disconnected\"\n ? \"Connect\"\n : \"Reconnect\"}\n </button>\n\n {connection && !connection.readOnly && (\n <button\n type=\"button\"\n className=\"devic-int-btn devic-int-btn-block devic-int-mcp-remove\"\n onClick={() => void disconnect(server)}\n disabled={!!busyKey}\n >\n Disconnect\n </button>\n )}\n </div>\n );\n })}\n </div>\n )}\n\n {allowCustom && (\n <button\n type=\"button\"\n className=\"devic-int-btn devic-int-mcp-add\"\n onClick={() => openPrompt(null)}\n disabled={loading || !!busyKey || atLimit}\n title={\n atLimit\n ? `You can have ${limits.maxServers} MCP server${\n limits.maxServers === 1 ? \"\" : \"s\"\n } connected at a time.`\n : undefined\n }\n >\n + Add your own MCP\n </button>\n )}\n\n {prompt && (\n <McpConnectForm\n server={prompt.server}\n callbackUrl={prompt.callbackUrl}\n needsClientCredentials={prompt.needsClientCredentials}\n submitting={\n busyKey === (prompt.server ? keyOf(prompt.server) : \"new\")\n }\n error={formError}\n theme={theme}\n onCancel={() => {\n setPrompt(null);\n setFormError(null);\n }}\n onSubmit={submit}\n />\n )}\n </div>\n );\n}\n\nexport default McpServersSection;\n"],"names":["_jsxs","_jsx"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"McpServersSection.js","sources":["../../../../src/components/IntegrationsModal/McpServersSection.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState, type JSX } from \"react\";\nimport type {\n TenantMcpAuthInput,\n TenantMcpConnectResult,\n TenantMcpServer,\n} from \"../../api/types\";\nimport type { DevicTheme } from \"../theme\";\nimport { McpConnectForm } from \"./McpConnectForm\";\nimport type { TenantMcpState } from \"./useTenantMcp\";\nimport { useTranslations, type Translator } from \"../../i18n\";\n\nexport interface McpServersSectionProps {\n state: TenantMcpState;\n theme?: DevicTheme;\n /** Filter typed in the modal's search box. */\n query?: string;\n}\n\n/** Message the OAuth callback page posts back when an MCP connection is done. */\ninterface McpCallbackMessage {\n source?: string;\n type?: string;\n status?: string;\n returnTo?: string;\n}\n\n/** A random value tying an OAuth round trip to the window that started it. */\nfunction newNonce(): string {\n const c = typeof crypto !== \"undefined\" ? crypto : undefined;\n if (c?.randomUUID) return c.randomUUID();\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;\n}\n\nfunction stateOf(\n t: Translator,\n server: TenantMcpServer\n): {\n key: \"connected\" | \"reconnect\" | \"disconnected\";\n label: string;\n} {\n const status = server.connection?.status;\n if (status === \"active\") return { key: \"connected\", label: t(\"Connected\") };\n if (status === \"error\") {\n return { key: \"reconnect\", label: t(\"Needs attention\") };\n }\n if (status === \"pending_auth\")\n return { key: \"reconnect\", label: t(\"Waiting for sign-in\") };\n return { key: \"disconnected\", label: t(\"Not connected\") };\n}\n\nfunction matches(server: TenantMcpServer, query: string): boolean {\n const q = query.trim().toLowerCase();\n if (!q) return true;\n return (\n server.name.toLowerCase().includes(q) ||\n server.url.toLowerCase().includes(q) ||\n (server.description ?? \"\").toLowerCase().includes(q)\n );\n}\n\n/** Identity of a row, stable whether or not it has been connected yet. */\nfunction keyOf(server: TenantMcpServer): string {\n return server.connection?.id ?? server.templateId ?? server.url;\n}\n\n/**\n * The MCP servers section of the connected-apps panel.\n *\n * The end user sees one list: the servers their developer left ready to\n * connect, the ones they added themselves, and — unless it was switched off —\n * a button to add another. Both routes open the same form, because the only\n * difference between them is who typed the url.\n *\n * Connecting opens a consent screen in a popup. The popup is opened empty\n * *before* the request that produces its URL, because browsers only honour\n * `window.open` inside the gesture that triggered it; opening it after the\n * round trip is what gets it blocked. Two endings are watched, not one — a\n * cancelled consent screen posts nothing back, so the close is watched too.\n */\nexport function McpServersSection({\n state,\n theme,\n query = \"\",\n}: McpServersSectionProps): JSX.Element | null {\n const t = useTranslations();\n const { servers, allowCustom, limits, loading, client, scope, refresh } = state;\n\n const [actionError, setActionError] = useState<string | null>(null);\n const [busyKey, setBusyKey] = useState<string | null>(null);\n const [blockedUrl, setBlockedUrl] = useState<{ url: string } | null>(null);\n /** The row whose form is open. `null` inside the object is \"add your own\". */\n const [prompt, setPrompt] = useState<{\n server: TenantMcpServer | null;\n callbackUrl?: string;\n needsClientCredentials?: boolean;\n } | null>(null);\n const [formError, setFormError] = useState<string | null>(null);\n\n const pendingRef = useRef<{ returnTo: string } | null>(null);\n const popupRef = useRef<Window | null>(null);\n const pollRef = useRef<number | null>(null);\n\n const finish = useCallback(() => {\n if (pollRef.current !== null) {\n window.clearInterval(pollRef.current);\n pollRef.current = null;\n }\n pendingRef.current = null;\n popupRef.current = null;\n setBusyKey(null);\n void refresh();\n }, [refresh]);\n\n // The callback page says it is done. A nudge, never the result: what is shown\n // comes from re-reading the server, so a forged message costs one extra fetch\n // and nothing else.\n useEffect(() => {\n const onMessage = (event: MessageEvent) => {\n const data = event.data as McpCallbackMessage | undefined;\n if (!data || data.source !== \"devic\") return;\n if (data.type !== \"mcp-connected\") return;\n const pending = pendingRef.current;\n if (!pending || data.returnTo !== pending.returnTo) return;\n popupRef.current?.close();\n finish();\n };\n window.addEventListener(\"message\", onMessage);\n return () => window.removeEventListener(\"message\", onMessage);\n }, [finish]);\n\n useEffect(\n () => () => {\n if (pollRef.current !== null) window.clearInterval(pollRef.current);\n },\n []\n );\n\n const openPrompt = (server: TenantMcpServer | null) => {\n setFormError(null);\n setActionError(null);\n setBlockedUrl(null);\n setPrompt({ server });\n };\n\n const submit = async (values: {\n url?: string;\n name?: string;\n auth: TenantMcpAuthInput;\n }) => {\n if (!client || !prompt) return;\n const server = prompt.server;\n const rowKey = server ? keyOf(server) : \"new\";\n setBusyKey(rowKey);\n setFormError(null);\n\n const nonce = newNonce();\n const returnTo = `${window.location.origin}/?devic_mcp=${nonce}`;\n // Opened empty inside the submit gesture and navigated once the URL is\n // known. Skipped when no browser is involved at all.\n const popup =\n values.auth.mode === \"oauth\"\n ? window.open(\n \"\",\n \"devic-mcp-oauth\",\n \"width=520,height=680,menubar=no,toolbar=no\"\n )\n : null;\n\n try {\n const existing = server?.connection;\n const result: TenantMcpConnectResult = existing\n ? await client.reconnectMcpServer(existing.id, {\n ...scope,\n returnTo,\n auth: values.auth,\n })\n : await client.connectMcpServer({\n ...scope,\n returnTo,\n templateId: server?.templateId,\n url: values.url,\n name: values.name,\n auth: values.auth,\n });\n\n if (result.requiresClientCredentials) {\n // Not a failure the user can do nothing about: they own the server, so\n // the form reopens with the fields and the redirect URI to authorise.\n popup?.close();\n setBusyKey(null);\n setPrompt({\n server,\n callbackUrl: result.callbackUrl,\n needsClientCredentials: true,\n });\n setFormError(result.error ?? null);\n return;\n }\n\n if (result.status === \"error\") {\n popup?.close();\n setBusyKey(null);\n setFormError(result.error ?? t(\"The server could not be reached.\"));\n return;\n }\n\n if (!result.authorizationUrl) {\n // Connected outright: the key the user typed is the credential.\n popup?.close();\n setPrompt(null);\n finish();\n return;\n }\n\n pendingRef.current = { returnTo };\n setPrompt(null);\n if (popup && !popup.closed) {\n popupRef.current = popup;\n popup.location.href = result.authorizationUrl;\n pollRef.current = window.setInterval(() => {\n if (popup.closed) finish();\n }, 700);\n } else {\n // Blocked (Safari, in-app browsers, extensions): hand over the URL.\n setBlockedUrl({ url: result.authorizationUrl });\n setBusyKey(null);\n }\n } catch (err) {\n popup?.close();\n pendingRef.current = null;\n setBusyKey(null);\n setFormError(err instanceof Error ? err.message : String(err));\n }\n };\n\n const disconnect = async (server: TenantMcpServer) => {\n const connection = server.connection;\n if (!client || !connection || busyKey) return;\n setBusyKey(keyOf(server));\n setActionError(null);\n try {\n await client.disconnectMcpServer(connection.id, scope);\n await refresh();\n } catch (err) {\n setActionError(err instanceof Error ? err.message : String(err));\n } finally {\n setBusyKey(null);\n }\n };\n\n const visible = servers.filter((s) => matches(s, query));\n const atLimit = limits.used >= limits.maxServers;\n\n // Nothing offered and nothing addable: the whole section would be a heading\n // over an empty box, which says less than saying nothing.\n if (!servers.length && !allowCustom) return null;\n\n return (\n <div className=\"devic-int-mcp-section\">\n <div className=\"devic-int-mcp-heading\">\n <h4>{t(\"MCP servers\")}</h4>\n <span>\n {t(\"Connect a server and its tools become available in this chat.\")}\n </span>\n </div>\n\n {actionError && <div className=\"devic-int-error\">{actionError}</div>}\n\n {blockedUrl && (\n <div className=\"devic-int-notice\">\n {t(\"Your browser blocked the pop-up.\")}{\" \"}\n <a\n href={blockedUrl.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={() => {\n pendingRef.current = null;\n setBlockedUrl(null);\n }}\n >\n {t(\"Open the authorisation page\")}\n </a>{\" \"}\n {t(\"and come back — then use Refresh.\")}\n </div>\n )}\n\n {visible.length > 0 && (\n <div className=\"devic-int-grid\">\n {visible.map((server) => {\n const cardState = stateOf(t, server);\n const rowKey = keyOf(server);\n const busy = busyKey === rowKey;\n const connection = server.connection;\n return (\n <div\n key={rowKey}\n className=\"devic-int-card\"\n data-state={cardState.key}\n >\n <div className=\"devic-int-card-head\">\n {server.logoUrl ? (\n <img\n className=\"devic-int-mcp-logo\"\n src={server.logoUrl}\n alt=\"\"\n aria-hidden=\"true\"\n />\n ) : (\n <span className=\"devic-int-mcp-logo devic-int-mcp-logo-fallback\">\n {server.name.slice(0, 1).toUpperCase()}\n </span>\n )}\n <span className=\"devic-int-state\">\n <span\n className=\"devic-int-dot\"\n data-ok={cardState.key === \"connected\"}\n data-off={cardState.key === \"disconnected\"}\n aria-hidden=\"true\"\n />\n {cardState.label}\n </span>\n </div>\n\n <div className=\"devic-int-name\" title={server.name}>\n {server.name}\n </div>\n\n <div\n className=\"devic-int-description\"\n title={server.description ?? server.url}\n >\n {server.description ?? server.url}\n </div>\n\n {connection?.shared && (\n // Worth saying before they wonder why they cannot remove it.\n <div className=\"devic-int-mcp-note\">\n {t(\"Connected for everyone on this account\")}\n </div>\n )}\n\n {connection?.status === \"active\" &&\n typeof connection.toolCount === \"number\" && (\n <div className=\"devic-int-mcp-note\">\n {connection.toolCount === 1\n ? t(\"1 tool available\")\n : t(\"{count} tools available\", {\n count: connection.toolCount,\n })}\n </div>\n )}\n\n {connection?.lastProbeError && (\n <div\n className=\"devic-int-mcp-note devic-int-mcp-note-warn\"\n title={connection.lastProbeError}\n >\n {connection.lastProbeError}\n </div>\n )}\n\n <button\n type=\"button\"\n className={`devic-int-btn devic-int-btn-block${\n cardState.key === \"connected\" ? \"\" : \" devic-int-btn-primary\"\n }`}\n onClick={() => openPrompt(server)}\n disabled={\n busy ||\n !!busyKey ||\n connection?.readOnly ||\n (!connection && atLimit)\n }\n title={\n connection?.readOnly\n ? t(\"This server was connected for the whole account.\")\n : !connection && atLimit\n ? t(\"You can have {max} connected at a time.\", {\n max: limits.maxServers,\n })\n : undefined\n }\n >\n {busy\n ? t(\"Waiting…\")\n : cardState.key === \"disconnected\"\n ? t(\"Connect\")\n : t(\"Reconnect\")}\n </button>\n\n {connection && !connection.readOnly && (\n <button\n type=\"button\"\n className=\"devic-int-btn devic-int-btn-block devic-int-mcp-remove\"\n onClick={() => void disconnect(server)}\n disabled={!!busyKey}\n >\n {t(\"Disconnect\")}\n </button>\n )}\n </div>\n );\n })}\n </div>\n )}\n\n {allowCustom && (\n <button\n type=\"button\"\n className=\"devic-int-btn devic-int-mcp-add\"\n onClick={() => openPrompt(null)}\n disabled={loading || !!busyKey || atLimit}\n title={\n atLimit\n ? limits.maxServers === 1\n ? t(\"You can have 1 MCP server connected at a time.\")\n : t(\"You can have {max} MCP servers connected at a time.\", {\n max: limits.maxServers,\n })\n : undefined\n }\n >\n {t(\"+ Add your own MCP\")}\n </button>\n )}\n\n {prompt && (\n <McpConnectForm\n server={prompt.server}\n callbackUrl={prompt.callbackUrl}\n needsClientCredentials={prompt.needsClientCredentials}\n submitting={\n busyKey === (prompt.server ? keyOf(prompt.server) : \"new\")\n }\n error={formError}\n theme={theme}\n onCancel={() => {\n setPrompt(null);\n setFormError(null);\n }}\n onSubmit={submit}\n />\n )}\n </div>\n );\n}\n\nexport default McpServersSection;\n"],"names":["_jsxs","_jsx"],"mappings":"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"}
|
|
@@ -2,6 +2,7 @@ import { useMemo, useState, useCallback, useRef, useEffect } from 'react';
|
|
|
2
2
|
import { DevicApiClient } from '../../api/client.js';
|
|
3
3
|
import 'react/jsx-runtime';
|
|
4
4
|
import { useOptionalDevicContext } from '../../provider/DevicContext.js';
|
|
5
|
+
import { useTranslations } from '../../i18n/useTranslations.js';
|
|
5
6
|
|
|
6
7
|
/** A 4xx here means "not for you", not "something broke". */
|
|
7
8
|
function isRefusal(err) {
|
|
@@ -19,6 +20,7 @@ function isRefusal(err) {
|
|
|
19
20
|
*/
|
|
20
21
|
function useIntegrations(options) {
|
|
21
22
|
const { assistantId, tenantId, subtenantId, apiKey, baseUrl, enabled = true, } = options;
|
|
23
|
+
const t = useTranslations();
|
|
22
24
|
const context = useOptionalDevicContext();
|
|
23
25
|
const resolvedBaseUrl = baseUrl || context?.baseUrl || "https://api.devic.ai";
|
|
24
26
|
const resolvedTenantId = tenantId || context?.tenantId;
|
|
@@ -46,7 +48,7 @@ function useIntegrations(options) {
|
|
|
46
48
|
const [settled, setSettled] = useState(false);
|
|
47
49
|
const refresh = useCallback(async (dropCache = false) => {
|
|
48
50
|
if (!client) {
|
|
49
|
-
setError("No credentials configured");
|
|
51
|
+
setError(t("No credentials configured"));
|
|
50
52
|
setSettled(true);
|
|
51
53
|
return;
|
|
52
54
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useIntegrations.js","sources":["../../../../src/components/IntegrationsModal/useIntegrations.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { DevicApiError } from \"../../api/client\";\nimport { DevicApiClient } from \"../../api/client\";\nimport type { Integration } from \"../../api/types\";\nimport { useOptionalDevicContext } from \"../../provider\";\n\n/** What identifies the end user in front of the widget. */\nexport interface IntegrationsScope {\n assistantId: string;\n tenantId?: string;\n subtenantId?: string;\n}\n\nexport interface UseIntegrationsOptions extends IntegrationsScope {\n /** API key override (falls back to the provider's). */\n apiKey?: string;\n /** Base URL override (falls back to the provider's). */\n baseUrl?: string;\n /**\n * Fetch when this turns true, and only then. The launcher passes the drawer's\n * open state so a widget nobody opens costs no request at all.\n */\n enabled?: boolean;\n}\n\nexport interface IntegrationsState {\n integrations: Integration[];\n loading: boolean;\n error: string | null;\n /**\n * Whether this assistant offers connected apps to its tenants at all.\n *\n * False before the first answer arrives, and false again if the server said\n * no — which it does for a disabled catalogue, a missing tenant, and an\n * assistant that does not exist, deliberately without distinguishing them.\n * Anything that hangs off this flag stays hidden until the server has said\n * yes, so nothing ever appears and then disappears.\n */\n offered: boolean;\n /** True once a first answer (or refusal) has arrived. */\n settled: boolean;\n refresh: (dropCache?: boolean) => Promise<void>;\n client: DevicApiClient | null;\n scope: IntegrationsScope;\n}\n\n/** A 4xx here means \"not for you\", not \"something broke\". */\nfunction isRefusal(err: unknown): boolean {\n const status = (err as DevicApiError)?.statusCode;\n return typeof status === \"number\" && status >= 400 && status < 500;\n}\n\n/**\n * The apps an assistant offers to its tenants, with the accounts THIS tenant\n * has connected — loaded once and shared.\n *\n * Both the header button and the modal need the same listing: the button to\n * know whether it should exist and which logos to show, the modal to fill\n * itself. Fetching it in each of them would double every round trip and let the\n * two disagree for a moment after connecting an account.\n */\nexport function useIntegrations(\n options: UseIntegrationsOptions\n): IntegrationsState {\n const {\n assistantId,\n tenantId,\n subtenantId,\n apiKey,\n baseUrl,\n enabled = true,\n } = options;\n\n const context = useOptionalDevicContext();\n const resolvedBaseUrl = baseUrl || context?.baseUrl || \"https://api.devic.ai\";\n const resolvedTenantId = tenantId || context?.tenantId;\n const resolvedSubtenantId = subtenantId || context?.subtenantId;\n\n const resolvedApiKey = apiKey || context?.apiKey;\n const resolvedTenantSession = context?.getTenantSession;\n const onSessionExpired = context?.onSessionExpired;\n\n const client = useMemo(\n () =>\n resolvedApiKey || resolvedTenantSession\n ? new DevicApiClient({\n apiKey: resolvedApiKey,\n baseUrl: resolvedBaseUrl,\n getTenantSession: resolvedTenantSession,\n onSessionExpired,\n })\n : null,\n [resolvedApiKey, resolvedTenantSession, resolvedBaseUrl]\n );\n\n const scope = useMemo(\n () => ({\n assistantId,\n tenantId: resolvedTenantId || undefined,\n subtenantId: resolvedSubtenantId || undefined,\n }),\n [assistantId, resolvedTenantId, resolvedSubtenantId]\n );\n\n const [integrations, setIntegrations] = useState<Integration[]>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [offered, setOffered] = useState(false);\n const [settled, setSettled] = useState(false);\n\n const refresh = useCallback(\n async (dropCache = false) => {\n if (!client) {\n setError(\"No credentials configured\");\n setSettled(true);\n return;\n }\n setLoading(true);\n setError(null);\n try {\n // The server caches a scope's connections briefly; after an OAuth round\n // trip that cache is exactly one step behind, so drop it first.\n if (dropCache) {\n await client.refreshIntegrations(scope).catch(() => undefined);\n }\n const list = await client.getIntegrations(scope);\n setIntegrations(list);\n setOffered(list.length > 0);\n } catch (err) {\n setIntegrations([]);\n setOffered(false);\n setError(err instanceof Error ? err.message : String(err));\n if (!isRefusal(err)) {\n // A refusal is an ordinary answer and stays quiet. A 500 or a dead\n // network is not, and swallowing it entirely would leave an\n // integrator with a button that never appears and no reason why.\n console.warn(\"[devic-ui] could not load connected apps\", err);\n }\n } finally {\n setLoading(false);\n setSettled(true);\n }\n },\n [client, scope]\n );\n\n // Once per scope, not once per render of whatever switched it on. A new\n // assistant or tenant is a different listing altogether, so it loads again —\n // and the previous tenant's apps are dropped first, because showing them\n // while the new ones arrive would name accounts that are not this user's.\n const key = `${client ? \"k\" : \"-\"}|${scope.assistantId}|${scope.tenantId ?? \"\"}|${scope.subtenantId ?? \"\"}`;\n const loadedKeyRef = useRef<string | null>(null);\n useEffect(() => {\n if (!enabled || loadedKeyRef.current === key) return;\n if (loadedKeyRef.current !== null) {\n setIntegrations([]);\n setOffered(false);\n setSettled(false);\n }\n loadedKeyRef.current = key;\n void refresh();\n }, [enabled, key, refresh]);\n\n return {\n integrations,\n loading,\n error,\n offered,\n settled,\n refresh,\n client,\n scope,\n };\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useIntegrations.js","sources":["../../../../src/components/IntegrationsModal/useIntegrations.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { DevicApiError } from \"../../api/client\";\nimport { DevicApiClient } from \"../../api/client\";\nimport type { Integration } from \"../../api/types\";\nimport { useOptionalDevicContext } from \"../../provider\";\nimport { useTranslations } from \"../../i18n\";\n\n/** What identifies the end user in front of the widget. */\nexport interface IntegrationsScope {\n assistantId: string;\n tenantId?: string;\n subtenantId?: string;\n}\n\nexport interface UseIntegrationsOptions extends IntegrationsScope {\n /** API key override (falls back to the provider's). */\n apiKey?: string;\n /** Base URL override (falls back to the provider's). */\n baseUrl?: string;\n /**\n * Fetch when this turns true, and only then. The launcher passes the drawer's\n * open state so a widget nobody opens costs no request at all.\n */\n enabled?: boolean;\n}\n\nexport interface IntegrationsState {\n integrations: Integration[];\n loading: boolean;\n error: string | null;\n /**\n * Whether this assistant offers connected apps to its tenants at all.\n *\n * False before the first answer arrives, and false again if the server said\n * no — which it does for a disabled catalogue, a missing tenant, and an\n * assistant that does not exist, deliberately without distinguishing them.\n * Anything that hangs off this flag stays hidden until the server has said\n * yes, so nothing ever appears and then disappears.\n */\n offered: boolean;\n /** True once a first answer (or refusal) has arrived. */\n settled: boolean;\n refresh: (dropCache?: boolean) => Promise<void>;\n client: DevicApiClient | null;\n scope: IntegrationsScope;\n}\n\n/** A 4xx here means \"not for you\", not \"something broke\". */\nfunction isRefusal(err: unknown): boolean {\n const status = (err as DevicApiError)?.statusCode;\n return typeof status === \"number\" && status >= 400 && status < 500;\n}\n\n/**\n * The apps an assistant offers to its tenants, with the accounts THIS tenant\n * has connected — loaded once and shared.\n *\n * Both the header button and the modal need the same listing: the button to\n * know whether it should exist and which logos to show, the modal to fill\n * itself. Fetching it in each of them would double every round trip and let the\n * two disagree for a moment after connecting an account.\n */\nexport function useIntegrations(\n options: UseIntegrationsOptions\n): IntegrationsState {\n const {\n assistantId,\n tenantId,\n subtenantId,\n apiKey,\n baseUrl,\n enabled = true,\n } = options;\n\n const t = useTranslations();\n const context = useOptionalDevicContext();\n const resolvedBaseUrl = baseUrl || context?.baseUrl || \"https://api.devic.ai\";\n const resolvedTenantId = tenantId || context?.tenantId;\n const resolvedSubtenantId = subtenantId || context?.subtenantId;\n\n const resolvedApiKey = apiKey || context?.apiKey;\n const resolvedTenantSession = context?.getTenantSession;\n const onSessionExpired = context?.onSessionExpired;\n\n const client = useMemo(\n () =>\n resolvedApiKey || resolvedTenantSession\n ? new DevicApiClient({\n apiKey: resolvedApiKey,\n baseUrl: resolvedBaseUrl,\n getTenantSession: resolvedTenantSession,\n onSessionExpired,\n })\n : null,\n [resolvedApiKey, resolvedTenantSession, resolvedBaseUrl]\n );\n\n const scope = useMemo(\n () => ({\n assistantId,\n tenantId: resolvedTenantId || undefined,\n subtenantId: resolvedSubtenantId || undefined,\n }),\n [assistantId, resolvedTenantId, resolvedSubtenantId]\n );\n\n const [integrations, setIntegrations] = useState<Integration[]>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [offered, setOffered] = useState(false);\n const [settled, setSettled] = useState(false);\n\n const refresh = useCallback(\n async (dropCache = false) => {\n if (!client) {\n setError(t(\"No credentials configured\"));\n setSettled(true);\n return;\n }\n setLoading(true);\n setError(null);\n try {\n // The server caches a scope's connections briefly; after an OAuth round\n // trip that cache is exactly one step behind, so drop it first.\n if (dropCache) {\n await client.refreshIntegrations(scope).catch(() => undefined);\n }\n const list = await client.getIntegrations(scope);\n setIntegrations(list);\n setOffered(list.length > 0);\n } catch (err) {\n setIntegrations([]);\n setOffered(false);\n setError(err instanceof Error ? err.message : String(err));\n if (!isRefusal(err)) {\n // A refusal is an ordinary answer and stays quiet. A 500 or a dead\n // network is not, and swallowing it entirely would leave an\n // integrator with a button that never appears and no reason why.\n console.warn(\"[devic-ui] could not load connected apps\", err);\n }\n } finally {\n setLoading(false);\n setSettled(true);\n }\n },\n [client, scope]\n );\n\n // Once per scope, not once per render of whatever switched it on. A new\n // assistant or tenant is a different listing altogether, so it loads again —\n // and the previous tenant's apps are dropped first, because showing them\n // while the new ones arrive would name accounts that are not this user's.\n const key = `${client ? \"k\" : \"-\"}|${scope.assistantId}|${scope.tenantId ?? \"\"}|${scope.subtenantId ?? \"\"}`;\n const loadedKeyRef = useRef<string | null>(null);\n useEffect(() => {\n if (!enabled || loadedKeyRef.current === key) return;\n if (loadedKeyRef.current !== null) {\n setIntegrations([]);\n setOffered(false);\n setSettled(false);\n }\n loadedKeyRef.current = key;\n void refresh();\n }, [enabled, key, refresh]);\n\n return {\n integrations,\n loading,\n error,\n offered,\n settled,\n refresh,\n client,\n scope,\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -2,6 +2,7 @@ import { useMemo, useState, useCallback, useRef, useEffect } from 'react';
|
|
|
2
2
|
import { DevicApiClient } from '../../api/client.js';
|
|
3
3
|
import 'react/jsx-runtime';
|
|
4
4
|
import { useOptionalDevicContext } from '../../provider/DevicContext.js';
|
|
5
|
+
import { useTranslations } from '../../i18n/useTranslations.js';
|
|
5
6
|
|
|
6
7
|
/** A 4xx here means "not for you", not "something broke". */
|
|
7
8
|
function isRefusal(err) {
|
|
@@ -19,6 +20,7 @@ const NO_LIMITS = { maxServers: 0, maxToolsPerServer: 0, used: 0 };
|
|
|
19
20
|
*/
|
|
20
21
|
function useTenantMcp(options) {
|
|
21
22
|
const { assistantId, tenantId, subtenantId, apiKey, baseUrl, enabled = true } = options;
|
|
23
|
+
const t = useTranslations();
|
|
22
24
|
const context = useOptionalDevicContext();
|
|
23
25
|
const resolvedBaseUrl = baseUrl || context?.baseUrl || "https://api.devic.ai";
|
|
24
26
|
const resolvedTenantId = tenantId || context?.tenantId;
|
|
@@ -45,7 +47,7 @@ function useTenantMcp(options) {
|
|
|
45
47
|
const [settled, setSettled] = useState(false);
|
|
46
48
|
const refresh = useCallback(async () => {
|
|
47
49
|
if (!client) {
|
|
48
|
-
setError("No credentials configured");
|
|
50
|
+
setError(t("No credentials configured"));
|
|
49
51
|
setSettled(true);
|
|
50
52
|
return;
|
|
51
53
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useTenantMcp.js","sources":["../../../../src/components/IntegrationsModal/useTenantMcp.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { DevicApiError } from \"../../api/client\";\nimport { DevicApiClient } from \"../../api/client\";\nimport type { TenantMcpListing, TenantMcpServer } from \"../../api/types\";\nimport { useOptionalDevicContext } from \"../../provider\";\nimport type { IntegrationsScope } from \"./useIntegrations\";\n\nexport interface UseTenantMcpOptions extends IntegrationsScope {\n apiKey?: string;\n baseUrl?: string;\n /**\n * Fetch when this turns true, and only then. The modal passes its own open\n * state, so a panel nobody opens costs no request at all.\n */\n enabled?: boolean;\n}\n\nexport interface TenantMcpState {\n servers: TenantMcpServer[];\n loading: boolean;\n error: string | null;\n /** Whether this assistant offers MCP servers to its tenants at all. */\n offered: boolean;\n /** Whether the end user may add a server of their own. */\n allowCustom: boolean;\n limits: { maxServers: number; maxToolsPerServer: number; used: number };\n /** True once a first answer (or refusal) has arrived. */\n settled: boolean;\n refresh: () => Promise<void>;\n client: DevicApiClient | null;\n scope: IntegrationsScope;\n}\n\n/** A 4xx here means \"not for you\", not \"something broke\". */\nfunction isRefusal(err: unknown): boolean {\n const status = (err as DevicApiError)?.statusCode;\n return typeof status === \"number\" && status >= 400 && status < 500;\n}\n\nconst NO_LIMITS = { maxServers: 0, maxToolsPerServer: 0, used: 0 };\n\n/**\n * The MCP servers an assistant offers its tenants, together with the ones this\n * tenant has connected — loaded once and shared.\n *\n * Deliberately a sibling of `useIntegrations` rather than part of it: the two\n * answer different endpoints, and folding them together would mean an assistant\n * that offers only one of them pays for both on every open.\n */\nexport function useTenantMcp(options: UseTenantMcpOptions): TenantMcpState {\n const { assistantId, tenantId, subtenantId, apiKey, baseUrl, enabled = true } =\n options;\n\n const context = useOptionalDevicContext();\n const resolvedBaseUrl = baseUrl || context?.baseUrl || \"https://api.devic.ai\";\n const resolvedTenantId = tenantId || context?.tenantId;\n const resolvedSubtenantId = subtenantId || context?.subtenantId;\n const resolvedApiKey = apiKey || context?.apiKey;\n const resolvedTenantSession = context?.getTenantSession;\n const onSessionExpired = context?.onSessionExpired;\n\n const client = useMemo(\n () =>\n resolvedApiKey || resolvedTenantSession\n ? new DevicApiClient({\n apiKey: resolvedApiKey,\n baseUrl: resolvedBaseUrl,\n getTenantSession: resolvedTenantSession,\n onSessionExpired,\n })\n : null,\n [resolvedApiKey, resolvedTenantSession, resolvedBaseUrl]\n );\n\n const scope = useMemo(\n () => ({\n assistantId,\n tenantId: resolvedTenantId || undefined,\n subtenantId: resolvedSubtenantId || undefined,\n }),\n [assistantId, resolvedTenantId, resolvedSubtenantId]\n );\n\n const [listing, setListing] = useState<TenantMcpListing | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [settled, setSettled] = useState(false);\n\n const refresh = useCallback(async () => {\n if (!client) {\n setError(\"No credentials configured\");\n setSettled(true);\n return;\n }\n setLoading(true);\n setError(null);\n try {\n setListing(await client.getMcpServers(scope));\n } catch (err) {\n setListing(null);\n setError(err instanceof Error ? err.message : String(err));\n if (!isRefusal(err)) {\n // A refusal is an ordinary answer — a disabled feature, a missing\n // tenant — and stays quiet. A 500 is not, and swallowing it would\n // leave an integrator with a panel that never appears and no reason.\n console.warn(\"[devic-ui] could not load MCP servers\", err);\n }\n } finally {\n setLoading(false);\n setSettled(true);\n }\n }, [client, scope]);\n\n // Once per scope, not once per render of whatever switched it on. A different\n // tenant is a different listing, and the previous one is dropped first:\n // showing it while the new one arrives would name servers that are not this\n // user's.\n const key = `${client ? \"k\" : \"-\"}|${scope.assistantId}|${scope.tenantId ?? \"\"}|${scope.subtenantId ?? \"\"}`;\n const loadedKeyRef = useRef<string | null>(null);\n useEffect(() => {\n if (!enabled || loadedKeyRef.current === key) return;\n if (loadedKeyRef.current !== null) {\n setListing(null);\n setSettled(false);\n }\n loadedKeyRef.current = key;\n void refresh();\n }, [enabled, key, refresh]);\n\n return {\n servers: listing?.servers ?? [],\n loading,\n error,\n // Offered only once the server has said so. Anything hanging off this stays\n // hidden until then, so nothing appears and then disappears.\n offered: !!listing?.offered,\n allowCustom: !!listing?.allowCustom,\n limits: listing?.limits ?? NO_LIMITS,\n settled,\n refresh,\n client,\n scope,\n };\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useTenantMcp.js","sources":["../../../../src/components/IntegrationsModal/useTenantMcp.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { DevicApiError } from \"../../api/client\";\nimport { DevicApiClient } from \"../../api/client\";\nimport type { TenantMcpListing, TenantMcpServer } from \"../../api/types\";\nimport { useOptionalDevicContext } from \"../../provider\";\nimport type { IntegrationsScope } from \"./useIntegrations\";\nimport { useTranslations } from \"../../i18n\";\n\nexport interface UseTenantMcpOptions extends IntegrationsScope {\n apiKey?: string;\n baseUrl?: string;\n /**\n * Fetch when this turns true, and only then. The modal passes its own open\n * state, so a panel nobody opens costs no request at all.\n */\n enabled?: boolean;\n}\n\nexport interface TenantMcpState {\n servers: TenantMcpServer[];\n loading: boolean;\n error: string | null;\n /** Whether this assistant offers MCP servers to its tenants at all. */\n offered: boolean;\n /** Whether the end user may add a server of their own. */\n allowCustom: boolean;\n limits: { maxServers: number; maxToolsPerServer: number; used: number };\n /** True once a first answer (or refusal) has arrived. */\n settled: boolean;\n refresh: () => Promise<void>;\n client: DevicApiClient | null;\n scope: IntegrationsScope;\n}\n\n/** A 4xx here means \"not for you\", not \"something broke\". */\nfunction isRefusal(err: unknown): boolean {\n const status = (err as DevicApiError)?.statusCode;\n return typeof status === \"number\" && status >= 400 && status < 500;\n}\n\nconst NO_LIMITS = { maxServers: 0, maxToolsPerServer: 0, used: 0 };\n\n/**\n * The MCP servers an assistant offers its tenants, together with the ones this\n * tenant has connected — loaded once and shared.\n *\n * Deliberately a sibling of `useIntegrations` rather than part of it: the two\n * answer different endpoints, and folding them together would mean an assistant\n * that offers only one of them pays for both on every open.\n */\nexport function useTenantMcp(options: UseTenantMcpOptions): TenantMcpState {\n const { assistantId, tenantId, subtenantId, apiKey, baseUrl, enabled = true } =\n options;\n\n const t = useTranslations();\n const context = useOptionalDevicContext();\n const resolvedBaseUrl = baseUrl || context?.baseUrl || \"https://api.devic.ai\";\n const resolvedTenantId = tenantId || context?.tenantId;\n const resolvedSubtenantId = subtenantId || context?.subtenantId;\n const resolvedApiKey = apiKey || context?.apiKey;\n const resolvedTenantSession = context?.getTenantSession;\n const onSessionExpired = context?.onSessionExpired;\n\n const client = useMemo(\n () =>\n resolvedApiKey || resolvedTenantSession\n ? new DevicApiClient({\n apiKey: resolvedApiKey,\n baseUrl: resolvedBaseUrl,\n getTenantSession: resolvedTenantSession,\n onSessionExpired,\n })\n : null,\n [resolvedApiKey, resolvedTenantSession, resolvedBaseUrl]\n );\n\n const scope = useMemo(\n () => ({\n assistantId,\n tenantId: resolvedTenantId || undefined,\n subtenantId: resolvedSubtenantId || undefined,\n }),\n [assistantId, resolvedTenantId, resolvedSubtenantId]\n );\n\n const [listing, setListing] = useState<TenantMcpListing | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [settled, setSettled] = useState(false);\n\n const refresh = useCallback(async () => {\n if (!client) {\n setError(t(\"No credentials configured\"));\n setSettled(true);\n return;\n }\n setLoading(true);\n setError(null);\n try {\n setListing(await client.getMcpServers(scope));\n } catch (err) {\n setListing(null);\n setError(err instanceof Error ? err.message : String(err));\n if (!isRefusal(err)) {\n // A refusal is an ordinary answer — a disabled feature, a missing\n // tenant — and stays quiet. A 500 is not, and swallowing it would\n // leave an integrator with a panel that never appears and no reason.\n console.warn(\"[devic-ui] could not load MCP servers\", err);\n }\n } finally {\n setLoading(false);\n setSettled(true);\n }\n }, [client, scope]);\n\n // Once per scope, not once per render of whatever switched it on. A different\n // tenant is a different listing, and the previous one is dropped first:\n // showing it while the new one arrives would name servers that are not this\n // user's.\n const key = `${client ? \"k\" : \"-\"}|${scope.assistantId}|${scope.tenantId ?? \"\"}|${scope.subtenantId ?? \"\"}`;\n const loadedKeyRef = useRef<string | null>(null);\n useEffect(() => {\n if (!enabled || loadedKeyRef.current === key) return;\n if (loadedKeyRef.current !== null) {\n setListing(null);\n setSettled(false);\n }\n loadedKeyRef.current = key;\n void refresh();\n }, [enabled, key, refresh]);\n\n return {\n servers: listing?.servers ?? [],\n loading,\n error,\n // Offered only once the server has said so. Anything hanging off this stays\n // hidden until then, so nothing appears and then disappears.\n offered: !!listing?.offered,\n allowCustom: !!listing?.allowCustom,\n limits: listing?.limits ?? NO_LIMITS,\n settled,\n refresh,\n client,\n scope,\n };\n}\n"],"names":[],"mappings":"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"}
|