diff --git a/src-tauri/src/core/system/commands.rs b/src-tauri/src/core/system/commands.rs index 2a56b18bc..09a07bf2d 100644 --- a/src-tauri/src/core/system/commands.rs +++ b/src-tauri/src/core/system/commands.rs @@ -3590,6 +3590,27 @@ pub fn open_agent_terminal( Ok(()) } +/// Open a URL in the user's default browser (cross-platform). +fn open_web_url(url: &str) -> Result<(), String> { + use std::process::Command; + + let result = if cfg!(target_os = "windows") { + Command::new("cmd") + .args(["/C", "start", "", url]) + .status() + } else if cfg!(target_os = "macos") { + Command::new("open").arg(url).status() + } else { + Command::new("xdg-open").arg(url).status() + }; + + match result { + Ok(status) if status.success() => Ok(()), + Ok(_) => Err(format!("Failed to open {}", url)), + Err(e) => Err(format!("Failed to open {}: {}", url, e)), + } +} + /// Launch a GUI editor for the "IDEs & Editors" integrations (VS Code, /// JetBrains, Xcode). /// @@ -3641,6 +3662,12 @@ pub fn launch_editor(editor_id: String) -> Result<(), String> { // Xcode is macOS-only and ships no general-purpose launcher binary // (`xed` needs a file argument), so we open the app directly. "xcode" => (&[], Some("Xcode")), + // Self-hosted web UIs — open the default local URL in the browser. + "onyx" => { + open_web_url("http://localhost:3000")?; + log::info!("Opened Onyx at http://localhost:3000"); + return Ok(()); + } other => return Err(format!("Unknown editor: {}", other)), }; diff --git a/web-app/src/constants/integrations.ts b/web-app/src/constants/integrations.ts index e5b58fbae..4cc83cb60 100644 --- a/web-app/src/constants/integrations.ts +++ b/web-app/src/constants/integrations.ts @@ -1,4 +1,18 @@ -export type IntegrationKind = 'coding' | 'assistant' | 'editor' +export type IntegrationKind = + | 'coding' + | 'assistant' + | 'editor' + | 'chat' + | 'automation' + | 'notebook' + +/** Integrations that use copy-settings + launch + manual steps (no config file). */ +export const MANUAL_SETUP_KINDS: IntegrationKind[] = [ + 'editor', + 'chat', + 'automation', + 'notebook', +] /** * Ordered manual-setup instructions shown inside an editor card. These mirror @@ -121,20 +135,6 @@ export const INTEGRATION_AGENTS: IntegrationAgent[] = [ requiresModel: true, endpointWithPrefix: true, }, - { - id: 'zed', - name: 'Zed', - description: 'High-performance code editor with a built-in AI agent.', - kind: 'coding', - detectBin: 'zed', - docsUrl: 'https://zed.dev/docs/ai/llm-providers', - installable: true, - configurable: true, - requiresModel: true, - // Zed's native Atomic Chat provider expects the OpenAI-compatible base URL - // with the `/v1` prefix (matches its built-in default). - endpointWithPrefix: true, - }, { id: 'mimo', name: 'MiMo Code', @@ -285,4 +285,26 @@ export const INTEGRATION_AGENTS: IntegrationAgent[] = [ ], }, }, + { + id: 'onyx', + name: 'Onyx', + description: + 'Self-hostable chat UI with RAG, agents, connectors, and deep research.', + kind: 'chat', + detectBin: 'docker', + docsUrl: 'https://docs.onyx.app/deployment/quickstart', + installable: false, + configurable: false, + requiresModel: false, + endpointWithPrefix: true, + editor: { + launchId: 'onyx', + steps: [ + 'Deploy Onyx (see Docs) and sign in to your instance.', + 'During setup, add an OpenAI-compatible LLM provider.', + 'Paste the copied Base URL and API key (use host.docker.internal instead of 127.0.0.1 if Onyx runs in Docker).', + 'Select your Atomic Chat model and send a test message.', + ], + }, + }, ] diff --git a/web-app/src/locales/en/launch.json b/web-app/src/locales/en/launch.json index 043c1e13c..b85ec46c5 100644 --- a/web-app/src/locales/en/launch.json +++ b/web-app/src/locales/en/launch.json @@ -9,6 +9,12 @@ "assistantsDesc": "AI assistants that help with everyday tasks.", "editors": "Editors", "editorsDesc": "Code editors that let you chat and code with your local models.", + "chatRag": "Chat & RAG", + "chatRagDesc": "Chat interfaces and retrieval-augmented generation platforms.", + "automation": "Automation", + "automationDesc": "Workflow automation platforms with AI integration.", + "notebooks": "Notebooks", + "notebooksDesc": "Interactive notebooks with AI chat and code completion.", "copySettings": "Copy settings", "manualSetup": "Manual setup", "serverNotRunningHint": "Start the local server above first so {{name}} can reach your models.", @@ -18,8 +24,7 @@ "setCustomPath": "Set binary path", "customPathSet": "Binary path set", "savePath": "Save", - "customPathPlaceholder": "/path/to/binary", - "customPathPlaceholderWindows": "C:\\path\\to\\binary.exe", + "customPathPlaceholder": "/path/to/binary or C:\\path\\to\\binary.exe", "customPathHint": "Point at the agent's executable if it's installed in a non-standard location that auto-detection misses. This only affects the Installed status here.", "install": "Install", "installing": "Installing...", @@ -44,10 +49,8 @@ "installSuccess": "{{name}} installed", "installSuccessDesc": "{{name}} was installed successfully.", "installFailed": "Failed to install {{name}}", - "installFailedNetworkDesc": "The installer couldn't reach the network. If you're behind a region block, set an HTTP/HTTPS proxy in Settings → HTTPS Proxy and try again. Note: SOCKS proxies often don't work for npm/PowerShell-based installers.", "configured": "{{name}} configured", "configuredDesc": "{{name}} now points at your local server — opening a terminal.", - "configuredDescEditor": "{{name}} now points at your local server — opening the editor.", "configureFailed": "Failed to configure {{name}}", "terminalFailed": "Couldn't open a terminal for {{name}}", "settingsCopied": "Connection settings copied", diff --git a/web-app/src/routes/launch/index.tsx b/web-app/src/routes/launch/index.tsx index 511cb821b..6352951bb 100644 --- a/web-app/src/routes/launch/index.tsx +++ b/web-app/src/routes/launch/index.tsx @@ -16,6 +16,7 @@ import { import { route } from '@/constants/routes' import { INTEGRATION_AGENTS, + MANUAL_SETUP_KINDS, type IntegrationAgent, } from '@/constants/integrations' import HeaderPage from '@/containers/HeaderPage' @@ -25,9 +26,7 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { useTranslation } from '@/i18n/react-i18next-compat' import { useLocalApiServer } from '@/hooks/useLocalApiServer' -import { useProxyConfig } from '@/hooks/useProxyConfig' import { useAppState } from '@/hooks/useAppState' -import { useGeneralSetting } from '@/hooks/useGeneralSetting' import { useServiceHub } from '@/hooks/useServiceHub' import { useLaunchStore } from '@/stores/launch-store' import { useLaunchSettings } from '@/stores/launch-settings-store' @@ -42,29 +41,6 @@ export const Route = createFileRoute(route.launch.index as any)({ // than this; near-instant config writes finish first and never flash it. const SPINNER_DELAY_MS = 350 -// Snapshot the app proxy config (if enabled) into the payload the Rust -// `install_agent` / `open_agent_terminal` commands expect, so spawned installers -// and the launched agent can reach the network behind a region block. Returns -// undefined when no proxy is configured, so the commands omit proxy env. -function buildProxyPayload(): - | { url: string; username?: string; password?: string; no_proxy?: string } - | undefined { - const { - proxyEnabled, - proxyUrl, - proxyUsername, - proxyPassword, - noProxy, - } = useProxyConfig.getState() - if (!proxyEnabled || !proxyUrl.trim()) return undefined - return { - url: proxyUrl.trim(), - username: proxyUsername.trim() || undefined, - password: proxyPassword || undefined, - no_proxy: noProxy.trim() || undefined, - } -} - function IconBox({ children, bg, @@ -140,27 +116,6 @@ function AgentIcon({ agent }: { agent: IntegrationAgent }) { ) - case 'zed': - // Official Zed brand mark (assets/images/zed_logo.svg from zed-industries/zed), - // rendered light-on-dark to match Zed's app icon. - return ( - - - - - - ) case 'mimo': return ( @@ -304,6 +259,12 @@ function AgentIcon({ agent }: { agent: IntegrationAgent }) { /> ) + case 'onyx': + return ( + + O + + ) default: return ( @@ -370,11 +331,7 @@ function CustomPathRow({ onKeyDown={(e) => { if (e.key === 'Enter') onSave(draft) }} - placeholder={t( - IS_WINDOWS - ? 'launch:customPathPlaceholderWindows' - : 'launch:customPathPlaceholder' - )} + placeholder={t('launch:customPathPlaceholder')} spellCheck={false} autoCapitalize="off" autoCorrect="off" @@ -471,11 +428,6 @@ function LaunchPage() { INTEGRATION_AGENTS.forEach((agent) => detect(agent)) }, [detect]) - // First visit clears the sidebar "New" pill on Integrations. - useEffect(() => { - useGeneralSetting.getState().markIntegrationsBadgeSeen() - }, []) - useEffect(() => { refreshRunningModels() }, [refreshRunningModels, serverStatus]) @@ -534,10 +486,7 @@ function LaunchPage() { })) } ) - await invoke('install_agent', { - agentId: agent.id, - proxy: buildProxyPayload(), - }) + await invoke('install_agent', { agentId: agent.id }) toast.success(t('launch:toast.installSuccess', { name: agent.name }), { description: t('launch:toast.installSuccessDesc', { name: agent.name, @@ -547,15 +496,8 @@ function LaunchPage() { return true } catch (err) { const msg = err instanceof Error ? err.message : String(err) - // Surface a localized, actionable hint for network/DNS failures - // (commonly a region block with no proxy); keep the raw detail otherwise. - const isNetworkFailure = /dns|tunnel|proxy|os error 11001|wsahost_not_found|could not reach the network|resolve host|getaddrinfo|name resolution/i.test( - msg - ) toast.error(t('launch:toast.installFailed', { name: agent.name }), { - description: isNetworkFailure - ? t('launch:toast.installFailedNetworkDesc') - : msg, + description: msg, }) return false } finally { @@ -608,9 +550,6 @@ function LaunchPage() { case 'mimo': await invoke('configure_mimo', { apiUrl, model, apiKey: key }) break - case 'zed': - await invoke('configure_zed', { apiUrl, model, apiKey: key }) - break case 'copilot': await invoke('configure_copilot', { apiUrl, model, apiKey: key }) break @@ -647,12 +586,7 @@ function LaunchPage() { } toast.success(t('launch:toast.configured', { name: agent.name }), { - description: t( - agent.id === 'zed' - ? 'launch:toast.configuredDescEditor' - : 'launch:toast.configuredDesc', - { name: agent.name } - ), + description: t('launch:toast.configuredDesc', { name: agent.name }), duration: 8000, }) }, @@ -696,14 +630,6 @@ function LaunchPage() { // Config is written; open a terminal running the agent so the user can // start immediately. A terminal failure must not fail the whole Run. try { - // Zed is a desktop editor, not a terminal agent: its AI agent lives - // in its own window. Launch the app directly (no terminal) — the - // config we just wrote points its native Atomic Chat provider at the - // local server, and the user drives the Agent Panel from there. - if (agent.id === 'zed') { - await invoke('launch_zed') - return - } // OpenClaw's bare `openclaw` entry is the Crestodian setup/repair // helper (deterministic commands), not a chat. `openclaw chat` runs // the embedded local agent runtime, so the user lands straight in a @@ -722,10 +648,7 @@ function LaunchPage() { } else { command = agent.detectBin } - await invoke('open_agent_terminal', { - command, - proxy: buildProxyPayload(), - }) + await invoke('open_agent_terminal', { command }) } catch (termErr) { const tmsg = termErr instanceof Error ? termErr.message : String(termErr) @@ -836,9 +759,14 @@ function LaunchPage() { const coding = INTEGRATION_AGENTS.filter((a) => a.kind === 'coding') const assistants = INTEGRATION_AGENTS.filter((a) => a.kind === 'assistant') - const editors = INTEGRATION_AGENTS.filter((a) => a.kind === 'editor') + const manualIntegrations = INTEGRATION_AGENTS.filter((a) => + MANUAL_SETUP_KINDS.includes(a.kind) + ) + const chatRag = INTEGRATION_AGENTS.filter((a) => a.kind === 'chat') + const automation = INTEGRATION_AGENTS.filter((a) => a.kind === 'automation') + const notebooks = INTEGRATION_AGENTS.filter((a) => a.kind === 'notebook') - const renderEditor = (agent: IntegrationAgent) => { + const renderManualIntegration = (agent: IntegrationAgent) => { const isInstalled = installed[agent.id] const isBusy = editorBusy[agent.id] const steps = agent.editor?.steps ?? [] @@ -1113,7 +1041,7 @@ function LaunchPage() { {coding.map(renderAgent)} - {editors.length > 0 && ( + {manualIntegrations.length > 0 && (

@@ -1123,7 +1051,51 @@ function LaunchPage() { {t('launch:editorsDesc')}

- {editors.map(renderEditor)} + {manualIntegrations + .filter((a) => a.kind === 'editor') + .map(renderManualIntegration)} +
+ )} + + {chatRag.length > 0 && ( +
+
+

+ {t('launch:chatRag')} +

+

+ {t('launch:chatRagDesc')} +

+
+ {chatRag.map(renderManualIntegration)} +
+ )} + + {automation.length > 0 && ( +
+
+

+ {t('launch:automation')} +

+

+ {t('launch:automationDesc')} +

+
+ {automation.map(renderManualIntegration)} +
+ )} + + {notebooks.length > 0 && ( +
+
+

+ {t('launch:notebooks')} +

+

+ {t('launch:notebooksDesc')} +

+
+ {notebooks.map(renderManualIntegration)}
)}