"use client"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { useCallback, useEffect, useId, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { LuArrowRight, LuCloud, LuEye, LuEyeOff, LuPlug, LuRefreshCw, LuSlidersHorizontal, LuTrash2, } from "react-icons/lu"; import { type CodeExample, CodeExamplesDialog, } from "@/components/code-examples-dialog"; import { IntegrationDiagnostics } from "@/components/integration-diagnostics"; import { LoadingButton } from "@/components/loading-button"; import { AgentIcon } from "@/components/mcp-agent-icon"; import { AnimatedDisclosureContent } from "@/components/ui/animated-disclosure"; import { AnimatedSwitch } from "@/components/ui/animated-switch"; import { AnimatedTabs, AnimatedTabsContent, AnimatedTabsList, AnimatedTabsTrigger, } from "@/components/ui/animated-tabs"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { CodeSnippet } from "@/components/ui/code-snippet"; import { ConfirmationMark } from "@/components/ui/confirmation-mark"; import { CopyToClipboard } from "@/components/ui/copy-to-clipboard"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { OperationFlow } from "@/components/ui/operation-flow"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { FeatureIcon, SECTION_LABEL_CLASS, SettingsPanel, SettingsRow, StatusLight, } from "@/components/ui/settings-panel"; import { useCloudAuth } from "@/hooks/use-cloud-auth"; import { useWayfernTerms } from "@/hooks/use-wayfern-terms"; import { curlSnippet, LOCAL_API_EXAMPLES, maskToken, mcpClientConfig, oneLine, PROFILE_ID_PLACEHOLDER, REMOTE_MCP_EXAMPLES, REQUEST_LANGUAGES, requestSnippet, } from "@/lib/api-examples"; import { translateBackendError } from "@/lib/backend-errors"; import { canUseRemoteControl } from "@/lib/entitlements"; import { type AgentCategory, CORE_TOOLSET, credentialPrefixOf, FX_AGENT_ID, fxExportLine, type McpAgentInfo, type McpRemoteCredential, type McpRemoteCredentialRotation, type McpRemoteStatus, OPTIONAL_TOOLSETS, REMOTE_MCP_URL, } from "@/lib/mcp"; import { showErrorToast, showSuccessToast } from "@/lib/toast-utils"; import { cn } from "@/lib/utils"; interface AppSettings { api_enabled: boolean; api_port: number; api_token?: string; mcp_remote_enabled: boolean; mcp_remote_key?: string | null; mcp_toolsets?: string[]; } type IntegrationsTab = "api" | "remote"; interface IntegrationsDialogProps { isOpen: boolean; onClose: () => void; subPage?: boolean; /** Which tab is displayed when the dialog mounts; defaults to "api". */ initialTab?: IntegrationsTab; } function categoryLabel( t: (k: string) => string, category: AgentCategory, ): string { switch (category) { case "desktop-app": return t("integrations.mcp.category.desktopApp"); case "editor": return t("integrations.mcp.category.editor"); case "editor-ext": return t("integrations.mcp.category.editorExt"); case "cli": return t("integrations.mcp.category.cli"); } } export function IntegrationsDialog({ isOpen, onClose, subPage, initialTab = "api", }: IntegrationsDialogProps) { const { t } = useTranslation(); const [settings, setSettings] = useState({ api_enabled: false, api_port: 10108, api_token: undefined, mcp_remote_enabled: false, }); const [apiServerPort, setApiServerPort] = useState(null); const [showApiToken, setShowApiToken] = useState(false); const [isApiStarting, setIsApiStarting] = useState(false); const [agents, setAgents] = useState([]); const [showAllClients, setShowAllClients] = useState(false); const [isAddingAll, setIsAddingAll] = useState(false); const [isSavingToolsets, setIsSavingToolsets] = useState(false); const [busyAgentIds, setBusyAgentIds] = useState>(new Set()); const [apiPortDraft, setApiPortDraft] = useState("10108"); const [remote, setRemote] = useState(null); const [isRemoteStarting, setIsRemoteStarting] = useState(false); const [credential, setCredential] = useState( null, ); const [isRotatingCredential, setIsRotatingCredential] = useState(false); const [apiExamplesOpen, setApiExamplesOpen] = useState(false); const [remoteExamplesOpen, setRemoteExamplesOpen] = useState(false); const apiSwitchId = useId(); const apiPortId = useId(); const apiTokenId = useId(); const remoteSwitchId = useId(); const [activeTab, setActiveTab] = useState(initialTab); // Mod+I re-targets an open dialog through `initialTab`. The tabs are // controlled (see `shownTab`), so a remount would not adopt the new value; // this is React's adjust-state-on-prop-change form of the same thing. const [adoptedInitialTab, setAdoptedInitialTab] = useState(initialTab); if (adoptedInitialTab !== initialTab) { setAdoptedInitialTab(initialTab); setActiveTab(initialTab); } const { termsAccepted } = useWayfernTerms(); const { user, isLoggedIn, isLoading: authLoading } = useCloudAuth(); // Asked of the SERVER, not inferred. // // Two wrong answers were tried before this one. `canUseRemoteControl(user)` // reads the cached entitlement, which is computed for this account alone, so // an entitled enterprise MEMBER was told to buy the plan they are already on. // Falling back to `remote.connected` was worse in BOTH directions: an open // socket only proves the plan is active, so every paying plan connects and // would have been handed an endpoint that answers 402; and the flag is live, // so an outage, a sleep or a slot conflict flipped an entitled member back to // the upgrade nag, permanently in the slot-taken case. // // `GET /api/mcp/status` answers the remote-control question directly and is // deliberately ungated so it can be asked before you are allowed anything. const [serverEntitled, setServerEntitled] = useState(null); const remoteEntitled = serverEntitled ?? canUseRemoteControl(user); // The local cache is only a placeholder until the server answers; treating // "not yet known" as "no" hides the endpoint from somebody who has paid. const remoteEntitlementKnown = serverEntitled !== null || (!authLoading && user !== null); const loadSettings = useCallback(async () => { try { const loaded = await invoke("get_app_settings"); setSettings(loaded); setApiPortDraft(String(loaded.api_port ?? "")); } catch (e) { console.error("Failed to load settings:", e); } }, []); const loadApiServerStatus = useCallback(async () => { try { const port = await invoke("get_api_server_status"); setApiServerPort(port); } catch (e) { console.error("Failed to get API server status:", e); } }, []); const loadAgents = useCallback(async () => { try { const list = await invoke("list_mcp_agents"); setAgents(list); } catch (e) { console.error("Failed to list MCP agents:", e); } }, []); const loadRemoteStatus = useCallback(async () => { try { setRemote(await invoke("get_mcp_remote_status")); } catch (e) { console.error("Failed to get MCP remote status:", e); } }, []); const loadCredential = useCallback(async () => { try { setCredential( await invoke("get_mcp_remote_credential"), ); } catch (e) { console.error("Failed to get MCP remote credential:", e); } }, []); /** * The server's own verdict on remote control, cached for this dialog. * * A network call, so it never gates the status line: `remoteEntitled` falls * back to the local cache until this lands, and only the endpoint panels * wait on `remoteEntitlementKnown`. */ const loadRemoteEntitlement = useCallback(async () => { if (!isLoggedIn) return; try { setServerEntitled( await invoke("get_remote_control_entitlement"), ); } catch { // Offline, or the endpoint is unreachable. Leave the local cache in // charge rather than asserting an answer we do not have. } }, [isLoggedIn]); useEffect(() => { if (isOpen) { void loadSettings(); void loadApiServerStatus(); void loadAgents(); void loadRemoteStatus(); void loadCredential(); void loadRemoteEntitlement(); } }, [ isOpen, loadSettings, loadApiServerStatus, loadAgents, loadRemoteStatus, loadCredential, loadRemoteEntitlement, ]); // The bridge reconnects on its own timer, so its state changes while this // dialog is open and nothing the user did caused it. Subscribed rather than // polled: a socket that drops and comes back should be visible immediately, // not on the next tick of an interval nobody wants to run. useEffect(() => { const unlisten = listen("mcp-remote-status", (event) => { setRemote(event.payload); }); return () => { void unlisten.then((off) => { off(); }); }; }, []); const handleApiToggle = async (enabled: boolean) => { setIsApiStarting(true); try { if (enabled) { const port = await invoke("start_api_server", { port: settings.api_port, }); setApiServerPort(port); const next = await invoke("save_app_settings", { settings: { ...settings, api_enabled: true }, }); setSettings(next); showSuccessToast(t("integrations.apiStarted", { port })); } else { await invoke("stop_api_server"); setApiServerPort(null); const next = await invoke("save_app_settings", { settings: { ...settings, api_enabled: false, api_token: null }, }); setSettings(next); showSuccessToast(t("integrations.apiStopped")); } } catch (e) { console.error("Failed to toggle API:", e); showErrorToast(t("integrations.apiToggleFailed"), { description: translateBackendError(t, e), }); } finally { setIsApiStarting(false); } }; const handleRemoteToggle = async (enabled: boolean) => { setIsRemoteStarting(true); try { const next = await invoke( enabled ? "start_mcp_remote_bridge" : "stop_mcp_remote_bridge", ); setRemote(next); setSettings((prev) => ({ ...prev, mcp_remote_enabled: enabled })); showSuccessToast( enabled ? t("integrations.remote.enabled") : t("integrations.remote.disabled"), ); } catch (e) { console.error("Failed to toggle remote control:", e); showErrorToast(t("integrations.remote.toggleFailed"), { description: translateBackendError(t, e), }); } finally { setIsRemoteStarting(false); } }; /** * A rotation succeeds once the key is stored. A client it could not be * written into is named here so the user knows which one to retry, rather * than surfacing as a failed rotation that invites a second mint. */ const reportFailedClients = (rotated: McpRemoteCredentialRotation) => { const failed = rotated.failed_clients ?? []; if (failed.length === 0) return; const names = failed.map( (id) => agents.find((agent) => agent.id === id)?.display_name ?? id, ); showErrorToast( t("integrations.remote.credentialClientsFailed", { clients: names.join(", "), }), ); }; const handleRotateCredential = async () => { const hadCredential = credential?.present ?? false; setIsRotatingCredential(true); try { const rotated = await invoke( "rotate_mcp_remote_credential", ); setCredential({ present: true, tokenPrefix: credentialPrefixOf(rotated), }); showSuccessToast( hadCredential ? t("integrations.remote.credentialRotated") : t("integrations.remote.credentialCreated"), ); reportFailedClients(rotated); // The backend reinstalls every remote client with the new key and the // plaintext behind the fx export line changes with it. void loadAgents(); void loadSettings(); } catch (e) { console.error("Failed to rotate MCP remote credential:", e); showErrorToast(t("integrations.remote.credentialRotateFailed"), { description: translateBackendError(t, e), }); } finally { setIsRotatingCredential(false); } }; const markAgentBusy = (id: string, busy: boolean) => { setBusyAgentIds((prev) => { const next = new Set(prev); if (busy) next.add(id); else next.delete(id); return next; }); }; /** * The mint that a first install triggers, shared between clicks. Two * clients added back to back would otherwise each mint a key, and the * second mint retires the first one from under the client just written. */ const pendingMint = useRef | null>(null); const ensureCredential = () => { if (credential?.present) return Promise.resolve(); if (!pendingMint.current) { pendingMint.current = invoke( "rotate_mcp_remote_credential", ) .then((minted) => { setCredential({ present: true, tokenPrefix: credentialPrefixOf(minted), }); reportFailedClients(minted); }) .finally(() => { pendingMint.current = null; // Whatever the mint answered, what is on disk is what the page // shows: a key stored before a later step failed is still a key. void loadCredential(); }); } return pendingMint.current; }; /** * The installer replaces the Donut entry wholesale, so a client whose entry * points anywhere else is fixed by the same write. */ const handleAddAgent = async (agent: McpAgentInfo) => { markAgentBusy(agent.id, true); try { // The installer writes the stored credential and refuses when there is // none, so the first client mints it here: one click for the user. await ensureCredential(); await invoke("add_mcp_to_agent", { agentId: agent.id }); showSuccessToast( t("integrations.mcp.addedToClient", { name: agent.display_name }), ); void loadCredential(); void loadSettings(); void loadAgents(); } catch (e) { showErrorToast(translateBackendError(t, e), { description: agent.display_name, }); } finally { markAgentBusy(agent.id, false); } }; /** Add Donut to several clients in one go, with one toast for all of them. */ const handleAddAll = async (targets: McpAgentInfo[]) => { setIsAddingAll(true); for (const agent of targets) markAgentBusy(agent.id, true); const failed: string[] = []; try { await ensureCredential(); for (const agent of targets) { try { await invoke("add_mcp_to_agent", { agentId: agent.id }); } catch (e) { console.error(`Failed to add MCP to ${agent.id}:`, e); failed.push(agent.display_name); } } const added = targets.length - failed.length; if (added > 0) { showSuccessToast(t("integrations.mcp.addedToMany", { count: added })); } if (failed.length > 0) { showErrorToast( t("integrations.mcp.addFailed", { names: failed.join(", ") }), ); } } catch (e) { showErrorToast(translateBackendError(t, e)); } finally { for (const agent of targets) markAgentBusy(agent.id, false); setIsAddingAll(false); void loadCredential(); void loadSettings(); void loadAgents(); } }; /** Save the toolsets; the backend rewrites every connected client. */ const handleSetToolsets = async (toolsets: string[]) => { setIsSavingToolsets(true); try { const failed = await invoke("set_mcp_toolsets", { toolsets }); setSettings((current) => ({ ...current, mcp_toolsets: toolsets })); if (failed.length > 0) { const names = failed.map( (id) => agents.find((agent) => agent.id === id)?.display_name ?? id, ); showErrorToast( t("integrations.mcp.toolsets.failed", { names: names.join(", ") }), ); } } catch (e) { showErrorToast(translateBackendError(t, e)); } finally { setIsSavingToolsets(false); void loadSettings(); } }; const handleRemoveAgent = async (agent: McpAgentInfo) => { markAgentBusy(agent.id, true); try { await invoke("remove_mcp_from_agent", { agentId: agent.id }); showSuccessToast( t("integrations.mcp.removedFromClient", { name: agent.display_name }), ); void loadAgents(); } catch (e) { showErrorToast(translateBackendError(t, e), { description: agent.display_name, }); } finally { markAgentBusy(agent.id, false); } }; const handleSavePort = async () => { const port = settings.api_port; if (port < 1 || port > 65535) { showErrorToast(t("integrations.apiInvalidPort"), { description: t("integrations.apiInvalidPortDescription"), }); return; } setIsApiStarting(true); try { await invoke("stop_api_server"); const next = await invoke("save_app_settings", { settings, }); setSettings(next); const actualPort = await invoke("start_api_server", { port }); setApiServerPort(actualPort); if (actualPort !== port) { showErrorToast(t("integrations.apiPortInUse", { port }), { description: t("integrations.apiFallbackPort", { port: actualPort }), }); } else { showSuccessToast(t("integrations.apiRunning", { port: actualPort })); } } catch (e) { showErrorToast(t("integrations.apiStartFailed"), { description: translateBackendError(t, e), }); } finally { setIsApiStarting(false); } }; const credentialPresent = credential?.present ?? false; const credentialPrefix = credentialPrefixOf(credential); // The examples name the credential they expect by its visible prefix; the // dialog never has the plaintext on screen, so the lines are templates // either way and copying one hands over exactly what is shown. const exampleBearer = credentialPrefix ? t("integrations.remote.credentialPrefix", { prefix: credentialPrefix }) : "${TOKEN}"; const remoteTarget = { baseUrl: REMOTE_MCP_URL, token: exampleBearer }; const remoteQuickStart = oneLine( curlSnippet(REMOTE_MCP_EXAMPLES[0].request, remoteTarget), ); const fxLine = fxExportLine(settings.mcp_remote_key); const apiBaseUrl = `http://127.0.0.1:${apiServerPort ?? settings.api_port}`; const apiToken = settings.api_token ?? ""; const maskedApiToken = maskToken(apiToken); // The local token is this machine's own, so a copied example carries it // and runs as pasted; the page itself shows only its ends. const apiDisplayTarget = { baseUrl: apiBaseUrl, token: maskedApiToken }; const apiCopyTarget = { baseUrl: apiBaseUrl, token: apiToken || "TOKEN" }; const apiQuickStart = LOCAL_API_EXAMPLES[0].request; const apiExamples: CodeExample[] = LOCAL_API_EXAMPLES.map((example) => ({ id: example.id, title: t(`integrations.examples.items.${example.id}.title`), description: t(`integrations.examples.items.${example.id}.description`), method: example.request.method, paid: example.paid, response: example.response, snippets: Object.fromEntries( REQUEST_LANGUAGES.map((language) => [ language, { display: requestSnippet(language, example.request, apiDisplayTarget), copy: requestSnippet(language, example.request, apiCopyTarget), }, ]), ), })); const remoteConfig = mcpClientConfig(REMOTE_MCP_URL, exampleBearer); const remoteExamples: CodeExample[] = [ ...REMOTE_MCP_EXAMPLES.map((example) => ({ id: example.id, title: t(`integrations.examples.items.${example.id}.title`), description: t(`integrations.examples.items.${example.id}.description`), method: example.request.method, response: example.response, snippets: Object.fromEntries( REQUEST_LANGUAGES.map((language) => { const code = requestSnippet(language, example.request, remoteTarget); return [language, { display: code, copy: code }]; }), ), })), { id: "clientConfig", title: t("integrations.examples.items.clientConfig.title"), description: t("integrations.examples.items.clientConfig.description"), snippets: { json: { display: remoteConfig, copy: remoteConfig } }, }, ]; // Remote control is not something a regular user is told about: the tab // exists only for an account entitled to it, or while the bridge is already // on so it can always be switched off. Nothing else on the page names it. const remoteTabShown = remoteEntitled || settings.mcp_remote_enabled || (remote?.enabled ?? false); // A tab that is not offered cannot be the one displayed, whether the caller // asked for it or it vanished under the user. const shownTab: IntegrationsTab = activeTab === "remote" && !remoteTabShown ? "api" : activeTab; const renderAgent = (agent: McpAgentInfo) => { const busy = busyAgentIds.has(agent.id); return (

{agent.display_name}

{categoryLabel(t, agent.category)}

{agent.connected ? (
{t("appFeedback.configured")}
) : ( )}
{agent.connected && (
{t("appFeedback.routeDetails")} {REMOTE_MCP_URL}, }, { id: "device", label: t("appFeedback.thisDevice"), detail: t( remote?.connected ? "appFeedback.reachable" : "appFeedback.notVerified", ), }, ]} />

{t("appFeedback.remoteProbeScope")}

)} {agent.connected && agent.id === FX_AGENT_ID && (

{t("integrations.remote.fxHint")}

{fxLine && ( )}
)}
); }; // The clients on this computer come first; the rest wait behind a toggle, // so the page leads with the apps the person actually has. const onThisComputer = agents .filter((agent) => agent.connected || agent.detected) .sort((a, b) => Number(b.connected) - Number(a.connected)); const otherClients = agents.filter( (agent) => !agent.connected && !agent.detected, ); const suggestedClients = onThisComputer.filter((agent) => !agent.connected); const clientGridClass = "grid grid-cols-1 gap-2 @2xl:grid-cols-2"; const clientsGrid = (

{t("integrations.mcp.clientsLabel")}

void handleSetToolsets(next)} disabled={isSavingToolsets} />
{onThisComputer.length > 0 ? ( <>

{t("integrations.mcp.onThisComputer")}

{suggestedClients.length > 1 && ( )}
{onThisComputer.map(renderAgent)}
) : (

{t("integrations.mcp.noneFound")}

)} {otherClients.length > 0 && onThisComputer.length > 0 && ( )} {otherClients.length > 0 && (onThisComputer.length === 0 || showAllClients) && (
{otherClients.map(renderAgent)}
)}
); const examplesButton = (onClick: () => void) => ( ); return ( { if (!open) onClose(); }} subPage={subPage} > {!subPage && ( {t("integrations.title")} )}
setActiveTab(value as IntegrationsTab)} > {t("integrations.tabApi")} {remoteTabShown && ( {t("integrations.tabRemote")} )}

{t("integrations.apiEnableDescription")}

void handleApiToggle(checked) } />
{t("integrations.apiRunningOn")}
{ setApiPortDraft(e.target.value); const val = Number.parseInt(e.target.value, 10); if ( !Number.isNaN(val) && val >= 1 && val <= 65535 ) { setSettings({ ...settings, api_port: val }); } }} onBlur={() => { const val = Number.parseInt(apiPortDraft, 10); if (Number.isNaN(val) || val < 1 || val > 65535) { setApiPortDraft(String(settings.api_port)); } }} className="h-8 w-24 font-mono text-xs md:text-xs" min={1} max={65535} /> void handleSavePort()} > {t("common.buttons.save")}

{t("integrations.apiExampleRequest")}

{examplesButton(() => { setApiExamplesOpen(true); })}
{remoteTabShown && (

{t("integrations.remote.enableDescription")}

void handleRemoteToggle(checked) } />
{!isLoggedIn && (

{t("integrations.remote.signInRequired")}

)} {isLoggedIn && !termsAccepted && (

{t("integrations.mcpAcceptTermsFirst")}

)} {remote && (
{/* Three states, not two. A refusal the bridge will keep receiving (an unentitled plan, a taken slot, a dead credential) re-dials for ever without ever connecting, and "Connecting as" over the top of the error underneath it read as a hang rather than an answer. */} {remote.connected ? t("integrations.remote.connected") : remote.lastError ? t("integrations.remote.notConnected") : t("integrations.remote.connecting")} {/* Only under a phrase that governs it. "Connected as" and "Connecting as" are open phrases; "Not connected" is closed, and the id dangling after it read as a sentence fragment in all ten locales. */} {!(remote.lastError && !remote.connected) && ( {remote.instanceId} )}
{!remote.connected && remote.lastError && (

{translateBackendError(t, remote.lastError)}

)}
)}
{/* Gated on entitlement: handing an unentitled customer a URL, a credential or an "Add" button that can only answer 402 is the same wrong-diagnosis trap as telling them their desktop is offline. */}
{credentialPresent ? ( credentialPrefix && ( {t("integrations.remote.credentialPrefix", { prefix: credentialPrefix, })} ) ) : (

{t("integrations.remote.credentialNone")}

)} void handleRotateCredential()} > {credentialPresent && ( )} {credentialPresent ? t("integrations.remote.credentialRotate") : t("integrations.remote.credentialCreate")}

{t("integrations.remote.exampleRequest")}

{examplesButton(() => { setRemoteExamplesOpen(true); })}
{clientsGrid}
)}
); } /** Which tool groups agents list, besides the core tools. */ function ToolsetsPicker({ value, onChange, disabled, }: { value: string[]; onChange: (next: string[]) => void; disabled: boolean; }) { const { t } = useTranslation(); const everything = value.length === 0; const chosen: string[] = everything ? [...OPTIONAL_TOOLSETS] : value.filter((name) => name !== CORE_TOOLSET); const label = everything ? t("integrations.mcp.toolsets.all") : chosen.length === 0 ? t("integrations.mcp.toolsets.coreOnly") : t("integrations.mcp.toolsets.some", { count: chosen.length }); const toggle = (name: string, on: boolean) => { const next = OPTIONAL_TOOLSETS.filter((set) => set === name ? on : chosen.includes(set), ); onChange( next.length === OPTIONAL_TOOLSETS.length ? [] : next.length === 0 ? [CORE_TOOLSET] : next, ); }; return (

{t("integrations.mcp.toolsets.title")}

{t("integrations.mcp.toolsets.description")}

{OPTIONAL_TOOLSETS.map((name) => (
toggle(name, checked === true)} className="mt-0.5" />
))}

{t("integrations.mcp.toolsets.core")}

); }