"use client"; import { invoke } from "@tauri-apps/api/core"; import { Eye, EyeOff } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { LuAppWindow, LuCheck, LuCodeXml, LuPlug, LuTerminal, LuTrash2, LuZap, } from "react-icons/lu"; import { AnimatedSwitch } from "@/components/ui/animated-switch"; import { AnimatedTabs, AnimatedTabsContent, AnimatedTabsList, AnimatedTabsTrigger, } from "@/components/ui/animated-tabs"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useWayfernTerms } from "@/hooks/use-wayfern-terms"; import { translateBackendError } from "@/lib/backend-errors"; import { showErrorToast, showSuccessToast } from "@/lib/toast-utils"; import { CopyToClipboard } from "./ui/copy-to-clipboard"; interface AppSettings { api_enabled: boolean; api_port: number; api_token?: string; mcp_enabled: boolean; mcp_port?: number; mcp_token?: string; } interface McpConfig { port: number; token: string; } type AgentCategory = "desktop-app" | "cli" | "editor" | "editor-ext"; interface McpAgentInfo { id: string; display_name: string; category: AgentCategory; connected: boolean; detected: boolean; } interface IntegrationsDialogProps { isOpen: boolean; onClose: () => void; subPage?: boolean; /** Which tab is displayed when the dialog mounts; defaults to "api". */ initialTab?: "api" | "mcp"; } function AgentIcon({ category }: { category: AgentCategory }) { const className = "size-4 text-muted-foreground"; switch (category) { case "desktop-app": return ; case "editor": return ; case "editor-ext": return ; case "cli": return ; } } 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_enabled: false, mcp_port: undefined, mcp_token: undefined, }); const [apiServerPort, setApiServerPort] = useState(null); const [mcpConfig, setMcpConfig] = useState(null); const [, setMcpRunning] = useState(false); const [showApiToken, setShowApiToken] = useState(false); const [showMcpUrl, setShowMcpUrl] = useState(false); const [isApiStarting, setIsApiStarting] = useState(false); const [isMcpStarting, setIsMcpStarting] = useState(false); const [agents, setAgents] = useState([]); const [busyAgentIds, setBusyAgentIds] = useState>(new Set()); const [apiPortDraft, setApiPortDraft] = useState("10108"); const { termsAccepted } = useWayfernTerms(); 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 loadMcpConfig = useCallback(async () => { try { const config = await invoke("get_mcp_config"); setMcpConfig(config); } catch (e) { console.error("Failed to get MCP config:", e); } }, []); const loadMcpServerStatus = useCallback(async () => { try { const isRunning = await invoke("get_mcp_server_status"); setMcpRunning(isRunning); } catch (e) { console.error("Failed to get MCP server status:", 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); } }, []); useEffect(() => { if (isOpen) { void loadSettings(); void loadApiServerStatus(); void loadMcpConfig(); void loadMcpServerStatus(); void loadAgents(); } }, [ isOpen, loadSettings, loadApiServerStatus, loadMcpConfig, loadMcpServerStatus, loadAgents, ]); 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: e instanceof Error ? e.message : t("integrations.apiUnknownError"), }); } finally { setIsApiStarting(false); } }; const handleMcpToggle = async (enabled: boolean) => { setIsMcpStarting(true); try { if (enabled) { const port = await invoke("start_mcp_server"); const next = await invoke("save_app_settings", { settings: { ...settings, mcp_enabled: true, mcp_port: port }, }); setSettings(next); void loadMcpConfig(); void loadAgents(); showSuccessToast(t("integrations.mcpStarted", { port })); } else { await invoke("stop_mcp_server"); const next = await invoke("save_app_settings", { settings: { ...settings, mcp_enabled: false }, }); setSettings(next); setMcpConfig(null); showSuccessToast(t("integrations.mcpStopped")); } } catch (e) { console.error("Failed to toggle MCP server:", e); showErrorToast(t("integrations.mcpToggleFailed"), { description: e instanceof Error ? e.message : t("integrations.apiUnknownError"), }); } finally { setIsMcpStarting(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; }); }; const handleAddAgent = async (agent: McpAgentInfo) => { markAgentBusy(agent.id, true); try { await invoke("add_mcp_to_agent", { agentId: agent.id }); showSuccessToast( t("integrations.mcp.addedToClient", { name: agent.display_name }), ); void loadAgents(); } catch (e) { showErrorToast(translateBackendError(t, e), { description: agent.display_name, }); } finally { markAgentBusy(agent.id, false); } }; 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 mcpUrl = mcpConfig ? `http://127.0.0.1:${mcpConfig.port}/mcp/${mcpConfig.token}` : ""; return ( { if (!open) onClose(); }} subPage={subPage} > {!subPage && ( {t("integrations.title")} )} {t("integrations.tabApi")} {t("integrations.tabMcp")} {t("integrations.apiEnableLabel")} {t("integrations.apiEnableDescription")} void handleApiToggle(checked)} /> {apiServerPort && ( {t("integrations.apiRunningOn")} http://127.0.0.1:{apiServerPort} )} {settings.api_enabled && ( <> {t("integrations.apiPortLabel")} { 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="w-24 font-mono" min={1} max={65535} /> { 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: e instanceof Error ? e.message : t("integrations.apiUnknownError"), }); } finally { setIsApiStarting(false); } }} > {t("common.buttons.save")} {t("integrations.apiTokenLabel")} { setShowApiToken(!showApiToken); }} > {showApiToken ? ( ) : ( )} {t("integrations.apiExampleRequest")} {`curl -H "Authorization: Bearer \${TOKEN}" \\ http://127.0.0.1:${apiServerPort ?? settings.api_port}/v1/profiles`} > )} {t("integrations.mcpEnableLabel")} {t("integrations.mcpEnableDescription")} {!termsAccepted && ( {t("integrations.mcpAcceptTermsFirst")} )} void handleMcpToggle(checked)} /> {mcpConfig && ( <> {t("integrations.mcp.url")} { setShowMcpUrl(!showMcpUrl); }} > {showMcpUrl ? ( ) : ( )} {t("integrations.mcp.clientsLabel")} {agents.map((agent) => { const busy = busyAgentIds.has(agent.id); return ( {agent.display_name} {categoryLabel(t, agent.category)} {agent.connected ? ( {t("integrations.mcp.connected")} void handleRemoveAgent(agent)} aria-label={t( "integrations.mcp.removeAriaLabel", { name: agent.display_name, }, )} > ) : ( void handleAddAgent(agent)} > {t("integrations.mcp.add")} )} ); })} > )} ); }
{t("integrations.apiEnableDescription")}
http://127.0.0.1:{apiServerPort}
{`curl -H "Authorization: Bearer \${TOKEN}" \\ http://127.0.0.1:${apiServerPort ?? settings.api_port}/v1/profiles`}
{t("integrations.mcpEnableDescription")} {!termsAccepted && ( {t("integrations.mcpAcceptTermsFirst")} )}
{agent.display_name}
{categoryLabel(t, agent.category)}