From ccd9e6cdfbdf02f3536f289deff6a85a9cd0340a Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Tue, 19 May 2026 08:33:32 +0200 Subject: [PATCH] feat(runtimes): simplify "Add a computer" dialog (MUL-2408) (#2839) - Align Runtimes connect flow with Onboarding CLI install: install.sh + multica setup - Drop manual "I've started the daemon" step; subscribe to daemon:register WS and auto-advance - Rename Connect remote machine -> Add a computer, remove EC2-specific copy - Rework UI per web design guidelines (focus rings, aria labels, live status, footer alignment) - Fix DialogFooter negative-margin overflow with p-0 content; use outline Cancel button Co-authored-by: multica-agent --- packages/views/locales/en/runtimes.json | 47 +- packages/views/locales/zh-Hans/runtimes.json | 47 +- .../components/connect-remote-dialog.tsx | 421 ++++++++---------- 3 files changed, 220 insertions(+), 295 deletions(-) diff --git a/packages/views/locales/en/runtimes.json b/packages/views/locales/en/runtimes.json index 348ce80b1..61e152448 100644 --- a/packages/views/locales/en/runtimes.json +++ b/packages/views/locales/en/runtimes.json @@ -3,7 +3,7 @@ "title": "Runtimes", "tagline": "Machines and cloud workers running CLI sessions for your agents.", "learn_more": "Learn more →", - "connect_remote": "Connect remote machine", + "connect_remote": "Add a computer", "search_placeholder": "Search runtimes…", "scope_mine": "Mine", "scope_all": "All", @@ -13,7 +13,7 @@ "filter_all_description": "All runtimes in this view", "empty": { "title": "No runtimes yet", - "hint": "Desktop auto-scans your local machine. For AWS EC2 or other remote machines, connect them using the setup wizard." + "hint": "Desktop auto-scans this computer. To use another machine — a server, dev box, or any computer with a terminal — add it here." }, "no_matches": { "title": "No matches", @@ -139,33 +139,24 @@ "queued_one": "{{count}} queued", "queued_other": "{{count}} queued", "connect": { - "title": "Connect a remote machine", - "description": "Run these commands on your remote machine (e.g. AWS EC2) to install the Multica CLI and register it as a runtime.", - "step1": "1. Install the CLI", - "step2": "2. Configure", - "step3": "3. Login with a personal access token", - "step3_hint_prefix": "Create one in ", - "step3_hint_destination": "Settings → Tokens", - "step3_hint_suffix": ".", - "step4": "4. Start the daemon", - "security_label": "Security: ", - "security_body": "Use an EC2 IAM role or least-privilege credentials. Never put root keys into agent ", - "security_body_suffix": ". The daemon uses outbound connections only — no inbound ports needed.", - "troubleshooting": "Troubleshooting", - "trouble_check_status": "Check status: ", - "trouble_view_logs": "View logs: ", - "trouble_verify_provider": "Verify provider: ", - "trouble_remote_note_prefix": "Desktop auto-scans only your local machine. Remote machines must run ", - "trouble_remote_note_suffix": " separately.", + "title": "Add a computer", + "description": "Run these two commands on the computer you want to add. We'll detect it the moment the daemon comes online.", + "step1_label": "Install the Multica CLI", + "step2_label": "Start the daemon", + "step2_hint": "Opens a browser to sign in, then keeps the daemon running in the background.", + "live_listening": "Waiting for your computer", + "live_listening_hint": "We'll detect it as soon as the daemon starts — usually under a minute.", + "troubleshooting": "Can't open a browser on that computer?", + "trouble_intro": "Use a token instead of the browser sign-in. This replaces step 2 and works anywhere you have a terminal.", + "trouble_token_hint_prefix": "Create a token in ", + "trouble_token_hint_destination": "Settings → Tokens", + "trouble_token_hint_suffix": ". The daemon only makes outbound connections — no inbound ports required.", + "trouble_check_status": "Check status", + "trouble_view_logs": "View logs", + "copy_aria": "Copy", "cancel": "Cancel", - "started_daemon": "I've started the daemon", - "waiting_title": "Waiting for runtime…", - "waiting_description": "Listening for your remote daemon to register. This page updates automatically — no need to refresh.", - "waiting_hint_prefix": "Run ", - "waiting_hint_suffix": " on the remote machine to verify it's running.", - "back": "Back", - "success_title": "Runtime connected!", - "success_description": "Your remote machine has registered as a runtime. You can now create an agent that dispatches tasks to it.", + "success_title": "Computer connected", + "success_description": "Your computer is online and ready. Create an agent and it can start picking up tasks here.", "view_runtime": "View runtime", "create_agent": "Create an agent" }, diff --git a/packages/views/locales/zh-Hans/runtimes.json b/packages/views/locales/zh-Hans/runtimes.json index 52d2b5138..66a46a05e 100644 --- a/packages/views/locales/zh-Hans/runtimes.json +++ b/packages/views/locales/zh-Hans/runtimes.json @@ -3,7 +3,7 @@ "title": "运行时", "tagline": "为智能体跑 CLI 会话的机器和云端 worker。", "learn_more": "了解更多 →", - "connect_remote": "连接远程机器", + "connect_remote": "添加电脑", "search_placeholder": "搜索运行时...", "scope_mine": "我的", "scope_all": "全部", @@ -13,7 +13,7 @@ "filter_all_description": "当前视图下的所有运行时", "empty": { "title": "还没有运行时", - "hint": "桌面端会自动扫描本地机器。对于 AWS EC2 或其他远程机器,可使用连接向导。" + "hint": "桌面端会自动扫描这台电脑。想把另一台电脑加进来 —— 服务器、远程开发机,或任何能开终端的电脑 —— 在这里添加。" }, "no_matches": { "title": "无匹配", @@ -130,33 +130,24 @@ "running_other": "{{count}} 个运行中", "queued_other": "{{count}} 个排队中", "connect": { - "title": "连接远程机器", - "description": "在远程机器(例如 AWS EC2)上运行这些命令,安装 Multica CLI 并注册为运行时。", - "step1": "1. 安装 CLI", - "step2": "2. 配置", - "step3": "3. 用个人访问令牌登录", - "step3_hint_prefix": "可在 ", - "step3_hint_destination": "设置 → Tokens", - "step3_hint_suffix": " 中创建。", - "step4": "4. 启动守护进程", - "security_label": "安全提示: ", - "security_body": "使用 EC2 IAM 角色或最小权限凭证。切勿把 root 凭证写入智能体的 ", - "security_body_suffix": "。守护进程只使用出站连接 —— 无需开放入站端口。", - "troubleshooting": "故障排查", - "trouble_check_status": "检查状态: ", - "trouble_view_logs": "查看日志: ", - "trouble_verify_provider": "验证 provider: ", - "trouble_remote_note_prefix": "桌面端只会自动扫描本地机器。远程机器需要单独运行 ", - "trouble_remote_note_suffix": "。", + "title": "添加电脑", + "description": "在要添加的电脑上运行这两条命令。守护进程一上线,这里就会自动识别。", + "step1_label": "安装 Multica CLI", + "step2_label": "启动守护进程", + "step2_hint": "会打开浏览器登录,然后在后台保持守护进程运行。", + "live_listening": "等待你的电脑上线", + "live_listening_hint": "守护进程一启动就会被识别,通常不到一分钟。", + "troubleshooting": "那台电脑打不开浏览器?", + "trouble_intro": "改用 token 登录替代第 2 步,适用于任何有终端的环境。", + "trouble_token_hint_prefix": "在 ", + "trouble_token_hint_destination": "设置 → Tokens", + "trouble_token_hint_suffix": " 中创建 token。守护进程只使用出站连接 —— 无需开放入站端口。", + "trouble_check_status": "检查状态", + "trouble_view_logs": "查看日志", + "copy_aria": "复制", "cancel": "取消", - "started_daemon": "我已启动守护进程", - "waiting_title": "正在等待运行时...", - "waiting_description": "正在监听远程守护进程的注册。此页面会自动更新,无需刷新。", - "waiting_hint_prefix": "在远程机器上运行 ", - "waiting_hint_suffix": " 来确认它在运行。", - "back": "返回", - "success_title": "运行时已连接!", - "success_description": "你的远程机器已注册为运行时。现在可以创建一个智能体,把 task 派发到它上。", + "success_title": "电脑已连接", + "success_description": "电脑已经上线就绪。创建一个智能体,task 就可以派发到这里运行。", "view_runtime": "查看运行时", "create_agent": "创建智能体" }, diff --git a/packages/views/runtimes/components/connect-remote-dialog.tsx b/packages/views/runtimes/components/connect-remote-dialog.tsx index 6ce539f24..17557089c 100644 --- a/packages/views/runtimes/components/connect-remote-dialog.tsx +++ b/packages/views/runtimes/components/connect-remote-dialog.tsx @@ -1,16 +1,7 @@ "use client"; import { useCallback, useEffect, useRef, useState } from "react"; -import { - Check, - ChevronRight, - Copy, - Loader2, - Server, - ShieldAlert, - Terminal, - Wrench, -} from "lucide-react"; +import { Check, ChevronRight, Copy, Terminal } from "lucide-react"; import { useQueryClient } from "@tanstack/react-query"; import { useWorkspaceId } from "@multica/core/hooks"; import { runtimeKeys } from "@multica/core/runtimes/queries"; @@ -28,47 +19,41 @@ import { Button } from "@multica/ui/components/ui/button"; import { useNavigation } from "../../navigation"; import { useT } from "../../i18n"; -type Step = "instructions" | "waiting" | "success"; +type Step = "instructions" | "success"; + +const INSTALL_CMD = + "curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash"; +const SETUP_CMD = "multica setup"; +const TOKEN_CMD = `multica config set server_url https://api.multica.ai +multica config set app_url https://multica.ai +multica login --token +multica daemon start`; export function ConnectRemoteDialog({ onClose }: { onClose: () => void }) { const [step, setStep] = useState("instructions"); - const [copied, setCopied] = useState(null); const wsId = useWorkspaceId(); const slug = useWorkspaceSlug(); const qc = useQueryClient(); const navigation = useNavigation(); const newRuntimeIdRef = useRef(null); - // Listen for a new runtime registration while the dialog is open + // `multica setup` is one blocking command that handles config + login + // + daemon start; the dialog passively listens for the resulting + // `daemon:register` WS event and auto-advances to success. const handleDaemonRegister = useCallback( (payload: unknown) => { - if (step === "waiting" || step === "instructions") { - qc.invalidateQueries({ queryKey: runtimeKeys.all(wsId) }); - const p = payload as Record | null; - if (p?.runtime_id && typeof p.runtime_id === "string") { - newRuntimeIdRef.current = p.runtime_id; - } - setStep("success"); + if (step !== "instructions") return; + qc.invalidateQueries({ queryKey: runtimeKeys.all(wsId) }); + const p = payload as Record | null; + if (p?.runtime_id && typeof p.runtime_id === "string") { + newRuntimeIdRef.current = p.runtime_id; } + setStep("success"); }, [step, qc, wsId], ); useWSEvent("daemon:register", handleDaemonRegister); - const copyToClipboard = useCallback( - (text: string, key: string) => { - navigator.clipboard.writeText(text); - setCopied(key); - }, - [], - ); - - useEffect(() => { - if (!copied) return; - const t = setTimeout(() => setCopied(null), 2000); - return () => clearTimeout(t); - }, [copied]); - const handleGoToAgents = () => { onClose(); if (slug) { @@ -87,18 +72,8 @@ export function ConnectRemoteDialog({ onClose }: { onClose: () => void }) { return ( !v && onClose()}> - - {step === "instructions" && ( - setStep("waiting")} - onClose={onClose} - /> - )} - {step === "waiting" && ( - setStep("instructions")} /> - )} + + {step === "instructions" && } {step === "success" && ( void }) { } // --------------------------------------------------------------------------- -// Step 1: Installation instructions +// Copy button + code row — mirrors onboarding/CliInstallInstructions // --------------------------------------------------------------------------- -const INSTALL_CMD = "curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash"; +function CopyButton({ text, ariaLabel }: { text: string; ariaLabel: string }) { + const [copied, setCopied] = useState(false); -const CONFIGURE_CMD = `multica config set server_url https://api.multica.ai -multica config set app_url https://multica.ai`; + useEffect(() => { + if (!copied) return; + const t = setTimeout(() => setCopied(false), 2000); + return () => clearTimeout(t); + }, [copied]); -const LOGIN_CMD = "multica login --token "; + const handleCopy = () => { + navigator.clipboard.writeText(text); + setCopied(true); + }; -const START_CMD = `multica daemon start --device-name "my-ec2-instance" -multica daemon status`; - -function CodeBlock({ - code, - copyKey, - copied, - onCopy, -}: { - code: string; - copyKey: string; - copied: string | null; - onCopy: (text: string, key: string) => void; -}) { - const isCopied = copied === copyKey; return ( -
-
-        {code}
-      
- + + ); +} + +function CommandStep({ + n, + label, + cmd, + copyAria, +}: { + n: number; + label: string; + cmd: string; + copyAria: string; +}) { + return ( +
+

+ {n}. {label} +

+
+ + + {cmd} + + +
); } -function InstructionsStep({ - copied, - onCopy, - onNext, - onClose, -}: { - copied: string | null; - onCopy: (text: string, key: string) => void; - onNext: () => void; - onClose: () => void; -}) { +// --------------------------------------------------------------------------- +// Step 1: Instructions +// --------------------------------------------------------------------------- + +function InstructionsStep({ onClose }: { onClose: () => void }) { const { t } = useT("runtimes"); return ( <> - - {t(($) => $.connect.title)} - + + + {t(($) => $.connect.title)} + + {t(($) => $.connect.description)} -
-
-
-
- - {t(($) => $.connect.step1)} -
- -
+
+
+ $.connect.step1_label)} + cmd={INSTALL_CMD} + copyAria={t(($) => $.connect.copy_aria)} + />
-
- - {t(($) => $.connect.step2)} -
- $.connect.step2_label)} + cmd={SETUP_CMD} + copyAria={t(($) => $.connect.copy_aria)} /> -
- -
-
- {t(($) => $.connect.step3)} -
- -

- {t(($) => $.connect.step3_hint_prefix)} - - {t(($) => $.connect.step3_hint_destination)} - - {t(($) => $.connect.step3_hint_suffix)} +

+ {t(($) => $.connect.step2_hint)}

-
-
- {t(($) => $.connect.step4)} -
- -
+ -
-
- -
- {t(($) => $.connect.security_label)} - {t(($) => $.connect.security_body)} - - {"custom_env"} - - {t(($) => $.connect.security_body_suffix)} -
-
-
- -
- - - {t(($) => $.connect.troubleshooting)} - - -
    -
  • - {t(($) => $.connect.trouble_check_status)} - - {"multica daemon status"} - -
  • -
  • - {t(($) => $.connect.trouble_view_logs)} - - {"multica daemon logs -f"} - -
  • -
  • - {t(($) => $.connect.trouble_verify_provider)} - - {"claude --version"} - -
  • -
  • - {t(($) => $.connect.trouble_remote_note_prefix)} - - {"multica daemon"} - - {t(($) => $.connect.trouble_remote_note_suffix)} -
  • -
-
+
- - - ); } -// --------------------------------------------------------------------------- -// Step 2: Waiting for registration -// --------------------------------------------------------------------------- - -function WaitingStep({ onBack }: { onBack: () => void }) { +function TroubleshootingDetails() { const { t } = useT("runtimes"); return ( - <> - - {t(($) => $.connect.waiting_title)} - - {t(($) => $.connect.waiting_description)} - - - -
- -

- {t(($) => $.connect.waiting_hint_prefix)} - - {"multica daemon status"} - - {t(($) => $.connect.waiting_hint_suffix)} +

+ + + {t(($) => $.connect.troubleshooting)} + +
+

{t(($) => $.connect.trouble_intro)}

+ $.connect.step2_label)} + cmd={TOKEN_CMD} + copyAria={t(($) => $.connect.copy_aria)} + /> +

+ {t(($) => $.connect.trouble_token_hint_prefix)} + + {t(($) => $.connect.trouble_token_hint_destination)} + + {t(($) => $.connect.trouble_token_hint_suffix)}

+
    +
  • + {t(($) => $.connect.trouble_check_status)} + + multica daemon status + +
  • +
  • + {t(($) => $.connect.trouble_view_logs)} + + multica daemon logs -f + +
  • +
- - - - - +
); } // --------------------------------------------------------------------------- -// Step 3: Success +// Live-listening indicator +// --------------------------------------------------------------------------- + +function LiveListening() { + const { t } = useT("runtimes"); + return ( +
+ + + + + + {t(($) => $.connect.live_listening)} + + + {t(($) => $.connect.live_listening_hint)} + +
+ ); +} + +// --------------------------------------------------------------------------- +// Step 2: Success // --------------------------------------------------------------------------- function SuccessStep({ @@ -351,28 +289,33 @@ function SuccessStep({ const { t } = useT("runtimes"); return ( <> - - {t(($) => $.connect.success_title)} - + + + {t(($) => $.connect.success_title)} + + {t(($) => $.connect.success_description)} -
-
+
+
- + {onGoToRuntime && ( - )} -