import { useEffect, useMemo, useState, useRef } from "react"; import { useTranslation } from "react-i18next"; import { motion, AnimatePresence } from "framer-motion"; import { toast } from "sonner"; import { invoke } from "@tauri-apps/api/core"; import { useQueryClient } from "@tanstack/react-query"; import { Plus, Settings, ArrowLeft, Minus, Maximize2, Minimize2, X, Book, Brain, Wrench, History, BarChart2, Download, FolderArchive, Search, FolderOpen, KeyRound, Shield, Cpu, LayoutDashboard, } from "lucide-react"; import { getCurrentWindow } from "@tauri-apps/api/window"; import type { Provider, VisibleApps } from "@/types"; import type { EnvConflict } from "@/types/env"; import { useProvidersQuery, useSettingsQuery } from "@/lib/query"; import { providersApi, settingsApi, type AppId, type ProviderSwitchEvent, } from "@/lib/api"; import { checkAllEnvConflicts, checkEnvConflicts } from "@/lib/api/env"; import { useProviderActions } from "@/hooks/useProviderActions"; import { openclawKeys, useOpenClawHealth } from "@/hooks/useOpenClaw"; import { hermesKeys, useOpenHermesWebUI } from "@/hooks/useHermes"; import { hermesApi } from "@/lib/api/hermes"; import { useProxyStatus } from "@/hooks/useProxyStatus"; import { useAutoCompact } from "@/hooks/useAutoCompact"; import { useUsageCacheBridge } from "@/hooks/useUsageCacheBridge"; import { useTauriEvent } from "@/hooks/useTauriEvent"; import { useLastValidValue } from "@/hooks/useLastValidValue"; import { useScanUnmanagedSkills } from "@/hooks/useSkills"; import { extractErrorMessage } from "@/utils/errorUtils"; import { isTextEditableTarget } from "@/utils/domUtils"; import { deepClone } from "@/utils/deepClone"; import { cn } from "@/lib/utils"; import { isWindows, isLinux, DRAG_REGION_ATTR, DRAG_REGION_STYLE, } from "@/lib/platform"; import { AppSwitcher } from "@/components/AppSwitcher"; import { ProfileSwitcher } from "@/components/profiles/ProfileSwitcher"; import { ProviderList } from "@/components/providers/ProviderList"; import { AddProviderDialog } from "@/components/providers/AddProviderDialog"; import { EditProviderDialog } from "@/components/providers/EditProviderDialog"; import { ConfirmDialog } from "@/components/ConfirmDialog"; import { SettingsPage } from "@/components/settings/SettingsPage"; import { UpdateBadge } from "@/components/UpdateBadge"; import { EnvWarningBanner } from "@/components/env/EnvWarningBanner"; import { ProxyToggle } from "@/components/proxy/ProxyToggle"; import { ClaudeDesktopRouteToggle } from "@/components/proxy/ClaudeDesktopRouteToggle"; import { FailoverToggle } from "@/components/proxy/FailoverToggle"; import UsageScriptModal from "@/components/UsageScriptModal"; import UnifiedMcpPanel from "@/components/mcp/UnifiedMcpPanel"; import PromptPanel from "@/components/prompts/PromptPanel"; import { SkillsPage, getSkillsPageHeaderActions, type SkillsPageSource, } from "@/components/skills/SkillsPage"; import UnifiedSkillsPanel from "@/components/skills/UnifiedSkillsPanel"; import { DeepLinkImportDialog } from "@/components/DeepLinkImportDialog"; import { FirstRunNoticeDialog } from "@/components/FirstRunNoticeDialog"; import { AgentsPanel } from "@/components/agents/AgentsPanel"; import { UniversalProviderPanel } from "@/components/universal"; import { McpIcon } from "@/components/BrandIcons"; import { Button } from "@/components/ui/button"; import { SessionManagerPage } from "@/components/sessions/SessionManagerPage"; import { useDisableCurrentOmo, useDisableCurrentOmoSlim, } from "@/lib/query/omo"; import WorkspaceFilesPanel from "@/components/workspace/WorkspaceFilesPanel"; import EnvPanel from "@/components/openclaw/EnvPanel"; import ToolsPanel from "@/components/openclaw/ToolsPanel"; import AgentsDefaultsPanel from "@/components/openclaw/AgentsDefaultsPanel"; import OpenClawHealthBanner from "@/components/openclaw/OpenClawHealthBanner"; import HermesMemoryPanel from "@/components/hermes/HermesMemoryPanel"; type View = | "providers" | "settings" | "prompts" | "skills" | "skillsDiscovery" | "mcp" | "agents" | "universal" | "sessions" | "workspace" | "openclawEnv" | "openclawTools" | "openclawAgents" | "hermesMemory"; interface SyncStatusUpdatedPayload { source?: string; status?: string; error?: string; } const DEFAULT_DRAG_BAR_HEIGHT = isWindows() || isLinux() ? 0 : 28; // px const HEADER_HEIGHT = 64; // px const STORAGE_KEY = "cc-switch-last-app"; const VALID_APPS: AppId[] = [ "claude", "claude-desktop", "codex", "gemini", "grokbuild", "opencode", "openclaw", "hermes", ]; const getInitialApp = (): AppId => { const saved = localStorage.getItem(STORAGE_KEY) as AppId | null; if (saved && VALID_APPS.includes(saved)) { return saved; } return "claude"; }; const VIEW_STORAGE_KEY = "cc-switch-last-view"; const VALID_VIEWS: View[] = [ "providers", "settings", "prompts", "skills", "skillsDiscovery", "mcp", "agents", "universal", "sessions", "workspace", "openclawEnv", "openclawTools", "openclawAgents", "hermesMemory", ]; const getInitialView = (): View => { const saved = localStorage.getItem(VIEW_STORAGE_KEY) as View | null; if (saved && VALID_VIEWS.includes(saved)) { return saved; } return "providers"; }; function App() { const { t } = useTranslation(); const queryClient = useQueryClient(); const [activeApp, setActiveApp] = useState(getInitialApp); const sharedFeatureApp: AppId = activeApp === "claude-desktop" ? "claude" : activeApp; const [currentView, setCurrentView] = useState(getInitialView); const [skillsDiscoverySource, setSkillsDiscoverySource] = useState("repos"); const [settingsDefaultTab, setSettingsDefaultTab] = useState("general"); const [isAddOpen, setIsAddOpen] = useState(false); const [isWindowMaximized, setIsWindowMaximized] = useState(false); useEffect(() => { localStorage.setItem(VIEW_STORAGE_KEY, currentView); }, [currentView]); const { data: settingsData } = useSettingsQuery(); const useAppWindowControls = isLinux() && (settingsData?.useAppWindowControls ?? false); const dragBarHeight = useAppWindowControls ? 32 : DEFAULT_DRAG_BAR_HEIGHT; const contentTopOffset = dragBarHeight + HEADER_HEIGHT; const visibleApps: VisibleApps = settingsData?.visibleApps ?? { claude: true, "claude-desktop": true, codex: true, gemini: true, grokbuild: true, opencode: true, openclaw: true, hermes: true, }; const getFirstVisibleApp = (): AppId => { if (visibleApps.claude) return "claude"; if (visibleApps["claude-desktop"]) return "claude-desktop"; if (visibleApps.codex) return "codex"; if (visibleApps.gemini) return "gemini"; if (visibleApps.grokbuild) return "grokbuild"; if (visibleApps.opencode) return "opencode"; if (visibleApps.openclaw) return "openclaw"; if (visibleApps.hermes) return "hermes"; return "claude"; // fallback }; useEffect(() => { if (!visibleApps[activeApp]) { setActiveApp(getFirstVisibleApp()); } }, [visibleApps, activeApp]); // Fallback from sessions view when switching to an app without session support useEffect(() => { if ( currentView === "sessions" && sharedFeatureApp !== "claude" && sharedFeatureApp !== "codex" && sharedFeatureApp !== "grokbuild" && sharedFeatureApp !== "opencode" && sharedFeatureApp !== "openclaw" && sharedFeatureApp !== "gemini" && sharedFeatureApp !== "hermes" ) { setCurrentView("providers"); } }, [sharedFeatureApp, currentView]); const [editingProvider, setEditingProvider] = useState(null); const [usageProvider, setUsageProvider] = useState(null); const [confirmAction, setConfirmAction] = useState<{ provider: Provider; action: "remove" | "delete"; } | null>(null); const [envConflicts, setEnvConflicts] = useState([]); const [showEnvBanner, setShowEnvBanner] = useState(false); const effectiveEditingProvider = useLastValidValue(editingProvider); const effectiveUsageProvider = useLastValidValue(usageProvider); const toolbarRef = useRef(null); const isToolbarCompact = useAutoCompact(toolbarRef); useUsageCacheBridge(); const promptPanelRef = useRef(null); const mcpPanelRef = useRef(null); const skillsPageRef = useRef(null); const unifiedSkillsPanelRef = useRef(null); // 订阅未管理 Skill 的共享缓存(实际扫描由 UnifiedSkillsPanel 进入页面时触发)。 // 这里 enabled 默认 false,仅用于「导入」按钮的绿点提示,不主动发起扫描。 const { data: unmanagedSkills } = useScanUnmanagedSkills(); const hasUnmanagedSkills = (unmanagedSkills?.length ?? 0) > 0; const addActionButtonClass = "bg-orange-500 hover:bg-orange-600 dark:bg-orange-500 dark:hover:bg-orange-600 text-white shadow-lg shadow-orange-500/30 dark:shadow-orange-500/40 rounded-full w-8 h-8"; const { isRunning: isProxyRunning, takeoverStatus, status: proxyStatus, } = useProxyStatus(); const isCurrentAppTakeoverActive = takeoverStatus?.[activeApp] || false; const activeProviderId = useMemo(() => { const target = proxyStatus?.active_targets?.find( (t) => t.app_type === activeApp, ); return target?.provider_id; }, [proxyStatus?.active_targets, activeApp]); const { data, isLoading, refetch } = useProvidersQuery(activeApp, { isProxyRunning, }); const providers = useMemo(() => data?.providers ?? {}, [data]); const currentProviderId = data?.currentProviderId ?? ""; const isOpenClawView = activeApp === "openclaw" && (currentView === "providers" || currentView === "workspace" || currentView === "sessions" || currentView === "openclawEnv" || currentView === "openclawTools" || currentView === "openclawAgents"); const { data: openclawHealthWarnings = [] } = useOpenClawHealth(isOpenClawView); const hasSkillsSupport = sharedFeatureApp !== "openclaw"; const hasSessionSupport = sharedFeatureApp === "claude" || sharedFeatureApp === "codex" || sharedFeatureApp === "grokbuild" || sharedFeatureApp === "opencode" || sharedFeatureApp === "openclaw" || sharedFeatureApp === "gemini" || sharedFeatureApp === "hermes"; const { addProvider, updateProvider, switchProvider, deleteProvider, saveUsageScript, setAsDefaultModel, } = useProviderActions( activeApp, isProxyRunning, isProxyRunning && isCurrentAppTakeoverActive, ); const disableOmoMutation = useDisableCurrentOmo(); const handleDisableOmo = () => { disableOmoMutation.mutate(undefined, { onSuccess: () => { toast.success(t("omo.disabled", { defaultValue: "OMO 已停用" })); }, onError: (error: Error) => { toast.error( t("omo.disableFailed", { defaultValue: "停用 OMO 失败: {{error}}", error: extractErrorMessage(error), }), ); }, }); }; const disableOmoSlimMutation = useDisableCurrentOmoSlim(); const handleDisableOmoSlim = () => { disableOmoSlimMutation.mutate(undefined, { onSuccess: () => { toast.success(t("omo.disabled", { defaultValue: "OMO 已停用" })); }, onError: (error: Error) => { toast.error( t("omo.disableFailed", { defaultValue: "停用 OMO 失败: {{error}}", error: extractErrorMessage(error), }), ); }, }); }; useEffect(() => { let unsubscribe: (() => void) | undefined; let active = true; const setupListener = async () => { try { const off = await providersApi.onSwitched( async (event: ProviderSwitchEvent) => { if (event.appType === activeApp) { await refetch(); } }, ); if (!active) { off(); return; } unsubscribe = off; } catch (error) { console.error("[App] Failed to subscribe provider switch event", error); } }; void setupListener(); return () => { active = false; unsubscribe?.(); }; }, [activeApp, refetch]); useTauriEvent("universal-provider-synced", async () => { await queryClient.invalidateQueries({ queryKey: ["providers"] }); try { await providersApi.updateTrayMenu(); } catch (error) { console.error("[App] Failed to update tray menu", error); } }); // 应用项目后刷新相关缓存(providers 由既有 provider-switched 监听承接; // proxy 状态由后端直接改 DB,不走 mutation,必须显式刷新) useTauriEvent("profile-applied", async () => { await queryClient.invalidateQueries({ queryKey: ["profiles"] }); await queryClient.invalidateQueries({ queryKey: ["mcp", "all"] }); await queryClient.invalidateQueries({ queryKey: ["skills"] }); await queryClient.invalidateQueries({ queryKey: ["proxyTakeoverStatus"] }); await queryClient.invalidateQueries({ queryKey: ["proxyStatus"] }); await queryClient.invalidateQueries({ queryKey: ["providers", "claude-desktop"], }); }); useTauriEvent( "webdav-sync-status-updated", async (payload) => { const statusPayload = payload ?? {}; await queryClient.invalidateQueries({ queryKey: ["settings"] }); if (statusPayload.source !== "auto" || statusPayload.status !== "error") { return; } toast.error( t("settings.webdavSync.autoSyncFailedToast", { error: statusPayload.error || t("common.unknown"), }), ); }, ); useTauriEvent( "s3-sync-status-updated", async (payload) => { const statusPayload = payload ?? {}; await queryClient.invalidateQueries({ queryKey: ["settings"] }); if (statusPayload.source !== "auto" || statusPayload.status !== "error") { return; } toast.error( t("settings.s3Sync.autoSyncFailedToast", { error: statusPayload.error || t("common.unknown"), }), ); }, ); useTauriEvent<{ appType: string; providerName: string }>( "proxy-official-warning", (payload) => { toast.warning( t("notifications.proxyOfficialWarning", { name: payload.providerName, defaultValue: `当前供应商 ${payload.providerName} 是官方供应商,建议切换到第三方供应商后再使用代理接管`, }), { duration: 8000 }, ); }, ); useEffect(() => { let active = true; let unlistenResize: (() => void) | undefined; const setupWindowStateSync = async () => { try { const currentWindow = getCurrentWindow(); const syncWindowMaximizedState = async () => { const maximized = await currentWindow.isMaximized(); if (active) { setIsWindowMaximized(maximized); } }; await syncWindowMaximizedState(); unlistenResize = await currentWindow.onResized(() => { void syncWindowMaximizedState(); }); } catch (error) { console.error("[App] Failed to sync window maximized state", error); } }; void setupWindowStateSync(); return () => { active = false; unlistenResize?.(); }; }, []); useEffect(() => { // settingsData 未加载时跳过,避免用 fallback false 覆盖 Rust 侧已设好的装饰状态 if (!settingsData) return; const syncWindowDecorations = async () => { try { await getCurrentWindow().setDecorations(!useAppWindowControls); } catch (error) { console.error("[App] Failed to update window decorations", error); } }; void syncWindowDecorations(); }, [useAppWindowControls, settingsData]); useEffect(() => { const checkEnvOnStartup = async () => { try { const allConflicts = await checkAllEnvConflicts(); const flatConflicts = Object.values(allConflicts).flat(); if (flatConflicts.length > 0) { setEnvConflicts(flatConflicts); const dismissed = sessionStorage.getItem("env_banner_dismissed"); if (!dismissed) { setShowEnvBanner(true); } } } catch (error) { console.error( "[App] Failed to check environment conflicts on startup:", error, ); } }; checkEnvOnStartup(); }, []); useEffect(() => { const checkMigration = async () => { try { const migrated = await invoke("get_migration_result"); if (migrated) { toast.success( t("migration.success", { defaultValue: "配置迁移成功" }), { closeButton: true }, ); } } catch (error) { console.error("[App] Failed to check migration result:", error); } }; checkMigration(); }, [t]); useEffect(() => { const checkSkillsMigration = async () => { try { const result = await invoke<{ count: number; error?: string } | null>( "get_skills_migration_result", ); if (result?.error) { toast.error(t("migration.skillsFailed"), { description: t("migration.skillsFailedDescription"), closeButton: true, }); console.error("[App] Skills SSOT migration failed:", result.error); return; } if (result && result.count > 0) { toast.success(t("migration.skillsSuccess", { count: result.count }), { closeButton: true, }); await queryClient.invalidateQueries({ queryKey: ["skills"] }); } } catch (error) { console.error("[App] Failed to check skills migration result:", error); } }; checkSkillsMigration(); }, [t, queryClient]); useEffect(() => { const checkEnvOnSwitch = async () => { try { const conflicts = await checkEnvConflicts(activeApp); if (conflicts.length > 0) { setEnvConflicts((prev) => { const existingKeys = new Set( prev.map((c) => `${c.varName}:${c.sourcePath}`), ); const newConflicts = conflicts.filter( (c) => !existingKeys.has(`${c.varName}:${c.sourcePath}`), ); return [...prev, ...newConflicts]; }); const dismissed = sessionStorage.getItem("env_banner_dismissed"); if (!dismissed) { setShowEnvBanner(true); } } } catch (error) { console.error( "[App] Failed to check environment conflicts on app switch:", error, ); } }; checkEnvOnSwitch(); }, [activeApp]); const currentViewRef = useRef(currentView); useEffect(() => { currentViewRef.current = currentView; }, [currentView]); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "," && (event.metaKey || event.ctrlKey)) { event.preventDefault(); setCurrentView("settings"); return; } if (event.key !== "Escape" || event.defaultPrevented) return; if (document.body.style.overflow === "hidden") return; const view = currentViewRef.current; if (view === "providers") return; if (isTextEditableTarget(event.target)) return; event.preventDefault(); setCurrentView(view === "skillsDiscovery" ? "skills" : "providers"); }; window.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("keydown", handleKeyDown); }; }, []); const [launchDashboardOpen, setLaunchDashboardOpen] = useState(false); const openHermesWebUI = useOpenHermesWebUI(() => setLaunchDashboardOpen(true), ); const handleOpenWebsite = async (url: string) => { try { await settingsApi.openExternal(url); } catch (error) { const detail = extractErrorMessage(error) || t("notifications.openLinkFailed", { defaultValue: "链接打开失败", }); toast.error(detail); } }; const handleEditProvider = async ({ provider, originalId, }: { provider: Provider; originalId?: string; }) => { await updateProvider(provider, originalId); setEditingProvider(null); }; const handleConfirmAction = async () => { if (!confirmAction) return; const { provider, action } = confirmAction; if (action === "remove") { // Remove from live config only (for additive mode apps like OpenCode/OpenClaw) // Does NOT delete from database - provider remains in the list await providersApi.removeFromLiveConfig(provider.id, activeApp); // Invalidate queries to refresh the isInConfig state if (activeApp === "opencode") { await queryClient.invalidateQueries({ queryKey: ["opencodeLiveProviderIds"], }); } else if (activeApp === "openclaw") { await queryClient.invalidateQueries({ queryKey: openclawKeys.liveProviderIds, }); await queryClient.invalidateQueries({ queryKey: openclawKeys.health, }); } else if (activeApp === "hermes") { await queryClient.invalidateQueries({ queryKey: hermesKeys.liveProviderIds, }); } toast.success( t("notifications.removeFromConfigSuccess", { defaultValue: "已从配置移除", }), { closeButton: true }, ); } else { await deleteProvider(provider.id); } setConfirmAction(null); }; const generateUniqueProviderCopyKey = ( originalKey: string, existingKeys: string[], ): string => { const baseKey = `${originalKey}-copy`; if (!existingKeys.includes(baseKey)) { return baseKey; } let counter = 2; while (existingKeys.includes(`${baseKey}-${counter}`)) { counter++; } return `${baseKey}-${counter}`; }; const handleDuplicateProvider = async (provider: Provider) => { const newSortIndex = provider.sortIndex !== undefined ? provider.sortIndex + 1 : undefined; const duplicatedProvider: Omit & { providerKey?: string; addToLive?: boolean; } = { name: `${provider.name} copy`, settingsConfig: deepClone(provider.settingsConfig), websiteUrl: provider.websiteUrl, category: provider.category, sortIndex: newSortIndex, // 复制原 sortIndex + 1 meta: provider.meta ? deepClone(provider.meta) : undefined, icon: provider.icon, iconColor: provider.iconColor, }; if ( activeApp === "opencode" || activeApp === "openclaw" || activeApp === "hermes" ) { let liveProviderIds: string[] = []; try { liveProviderIds = activeApp === "opencode" ? await queryClient.ensureQueryData({ queryKey: ["opencodeLiveProviderIds"], queryFn: () => providersApi.getOpenCodeLiveProviderIds(), }) : activeApp === "openclaw" ? await queryClient.ensureQueryData({ queryKey: openclawKeys.liveProviderIds, queryFn: () => providersApi.getOpenClawLiveProviderIds(), }) : await queryClient.ensureQueryData({ queryKey: hermesKeys.liveProviderIds, queryFn: () => providersApi.getHermesLiveProviderIds(), }); } catch (error) { console.error( "[App] Failed to load live provider IDs for duplication", error, ); const errorMessage = extractErrorMessage(error); toast.error( t("provider.duplicateLiveIdsLoadFailed", { defaultValue: "读取配置中的供应商标识失败,请先修复配置后再试", }) + (errorMessage ? `: ${errorMessage}` : ""), ); return; } const existingKeys = Array.from( new Set([...Object.keys(providers), ...liveProviderIds]), ); duplicatedProvider.providerKey = generateUniqueProviderCopyKey( provider.id, existingKeys, ); duplicatedProvider.addToLive = false; } if (provider.sortIndex !== undefined) { const updates = Object.values(providers) .filter( (p) => p.sortIndex !== undefined && p.sortIndex >= newSortIndex! && p.id !== provider.id, ) .map((p) => ({ id: p.id, sortIndex: p.sortIndex! + 1, })); if (updates.length > 0) { try { await providersApi.updateSortOrder(updates, activeApp); } catch (error) { console.error("[App] Failed to update sort order", error); toast.error( t("provider.sortUpdateFailed", { defaultValue: "排序更新失败", }), ); return; // 如果排序更新失败,不继续添加 } } } await addProvider(duplicatedProvider); }; const handleOpenTerminal = async (provider: Provider) => { try { const selectedDir = await settingsApi.pickDirectory(); if (!selectedDir) { return; } await providersApi.openTerminal(provider.id, activeApp, { cwd: selectedDir, }); toast.success( t("provider.terminalOpened", { defaultValue: "终端已打开", }), ); } catch (error) { console.error("[App] Failed to open terminal", error); const errorMessage = extractErrorMessage(error); toast.error( t("provider.terminalOpenFailed", { defaultValue: "打开终端失败", }) + (errorMessage ? `: ${errorMessage}` : ""), ); } }; const handleImportSuccess = async () => { try { await queryClient.invalidateQueries({ queryKey: ["providers"], refetchType: "all", }); await queryClient.refetchQueries({ queryKey: ["providers"], type: "all", }); } catch (error) { console.error("[App] Failed to refresh providers after import", error); await refetch(); } try { await providersApi.updateTrayMenu(); } catch (error) { console.error("[App] Failed to refresh tray menu", error); } }; const notifyWindowControlError = (error: unknown) => { toast.error( t("notifications.windowControlFailed", { defaultValue: "窗口控制失败:{{error}}", error: extractErrorMessage(error), }), ); }; const handleWindowMinimize = async () => { try { await getCurrentWindow().minimize(); } catch (error) { console.error("[App] Failed to minimize window", error); notifyWindowControlError(error); } }; const handleWindowToggleMaximize = async () => { try { const currentWindow = getCurrentWindow(); await currentWindow.toggleMaximize(); setIsWindowMaximized(await currentWindow.isMaximized()); } catch (error) { console.error("[App] Failed to toggle maximize", error); notifyWindowControlError(error); } }; const handleWindowClose = async () => { try { await getCurrentWindow().close(); } catch (error) { console.error("[App] Failed to close window", error); notifyWindowControlError(error); } }; const handleOpenSkillsDiscovery = () => { setSkillsDiscoverySource("repos"); setCurrentView("skillsDiscovery"); }; const renderContent = () => { const content = (() => { switch (currentView) { case "settings": return ( setCurrentView("providers")} onImportSuccess={handleImportSuccess} defaultTab={settingsDefaultTab} /> ); case "prompts": return ( setCurrentView("providers")} appId={sharedFeatureApp} /> ); case "hermesMemory": return ; case "skills": return ( ); case "skillsDiscovery": return ( ); case "mcp": return ( setCurrentView("providers")} /> ); case "agents": return ( setCurrentView("providers")} /> ); case "universal": return (
); case "sessions": return ( ); case "workspace": return ; case "openclawEnv": return ; case "openclawTools": return ; case "openclawAgents": return ; default: return (
{ setEditingProvider(provider); }} onDelete={(provider) => setConfirmAction({ provider, action: "delete" }) } onRemoveFromConfig={ activeApp === "opencode" || activeApp === "openclaw" || activeApp === "hermes" ? (provider) => setConfirmAction({ provider, action: "remove" }) : undefined } onDisableOmo={ activeApp === "opencode" ? handleDisableOmo : undefined } onDisableOmoSlim={ activeApp === "opencode" ? handleDisableOmoSlim : undefined } onDuplicate={handleDuplicateProvider} onConfigureUsage={setUsageProvider} onOpenWebsite={handleOpenWebsite} onOpenTerminal={ activeApp === "claude" ? handleOpenTerminal : undefined } onCreate={() => setIsAddOpen(true)} onSetAsDefault={ activeApp === "openclaw" ? setAsDefaultModel : activeApp === "hermes" ? switchProvider : undefined } />
); } })(); return ( {content} ); }; return (
{(dragBarHeight > 0 || useAppWindowControls) && (
{useAppWindowControls && (
)}
)} {showEnvBanner && envConflicts.length > 0 && ( { setShowEnvBanner(false); sessionStorage.setItem("env_banner_dismissed", "true"); }} onDeleted={async () => { try { const allConflicts = await checkAllEnvConflicts(); const flatConflicts = Object.values(allConflicts).flat(); setEnvConflicts(flatConflicts); if (flatConflicts.length === 0) { setShowEnvBanner(false); } } catch (error) { console.error( "[App] Failed to re-check conflicts after deletion:", error, ); } }} /> )}
{currentView !== "providers" ? (

{currentView === "settings" && t("settings.title")} {currentView === "prompts" && t("prompts.title", { appName: t(`apps.${sharedFeatureApp}`), })} {currentView === "skills" && t("skills.title")} {currentView === "skillsDiscovery" && t("skills.title")} {currentView === "mcp" && t("mcp.unifiedPanel.title")} {currentView === "agents" && t("agents.title")} {currentView === "universal" && t("universalProvider.title", { defaultValue: "统一供应商", })} {currentView === "sessions" && t("sessionManager.title")} {currentView === "workspace" && t("workspace.title")} {currentView === "openclawEnv" && t("openclaw.env.title")} {currentView === "openclawTools" && t("openclaw.tools.title")} {currentView === "openclawAgents" && t("openclaw.agents.title")} {currentView === "hermesMemory" && t("hermes.memory.title")}

) : (
{ setSettingsDefaultTab("about"); setCurrentView("settings"); }} /> {isCurrentAppTakeoverActive && ( )}
)}
{currentView === "providers" && activeApp !== "opencode" && activeApp !== "openclaw" && activeApp !== "hermes" && (
{activeApp === "claude-desktop" ? ( ) : ( settingsData?.enableLocalProxy && ( ) )} {activeApp !== "claude-desktop" && settingsData?.enableFailoverToggle && ( )}
)} {currentView === "providers" && (settingsData?.showProfileSwitcher ?? true) && (
)}
{currentView === "prompts" && ( )} {currentView === "mcp" && ( <> )} {currentView === "skills" && ( <> )} {currentView === "skillsDiscovery" && ( <> {getSkillsPageHeaderActions(skillsDiscoverySource).map( ({ key, labelKey, Icon, execute }) => ( ), )} )} {currentView === "providers" && ( <>
{activeApp === "hermes" ? ( <> ) : activeApp === "openclaw" ? ( <> ) : ( <> )}
)}
{isOpenClawView && openclawHealthWarnings.length > 0 && ( )} {renderContent()}
{ if (!open) { setEditingProvider(null); } }} onSubmit={handleEditProvider} appId={activeApp} isProxyTakeover={isCurrentAppTakeoverActive} /> {effectiveUsageProvider && ( setUsageProvider(null)} onSave={(script) => { if (usageProvider) { void saveUsageScript(usageProvider, script); } }} /> )} void handleConfirmAction()} onCancel={() => setConfirmAction(null)} /> { setLaunchDashboardOpen(false); void (async () => { try { await hermesApi.launchDashboard(); toast.success(t("hermes.webui.launching")); } catch (error) { toast.error(t("hermes.webui.launchFailed"), { description: extractErrorMessage(error) || undefined, }); } })(); }} onCancel={() => setLaunchDashboardOpen(false)} />
); } export default App;