const tauri = window["__TAURI__"]; const { invoke } = tauri.core; const { listen } = tauri.event; const elements = { activity: document.querySelector("#activity"), activityLabel: document.querySelector("#activity-label"), actionControls: document.querySelector("#action-controls"), channel: document.querySelector("#channel"), description: document.querySelector("#description"), eyebrow: document.querySelector("#eyebrow"), gatewayList: document.querySelector("#gateway-list"), discoveryStatus: document.querySelector("#discovery-status"), installButton: document.querySelector("#install-button"), installControls: document.querySelector("#install-controls"), installLog: document.querySelector("#install-log"), logStatus: document.querySelector("#log-status"), logWrap: document.querySelector("#log-wrap"), primaryAction: document.querySelector("#primary-action"), statusDot: document.querySelector("#status-dot"), title: document.querySelector("#title"), }; let primaryAction = null; let discoveryPending = false; let discoverySignature = null; function show(element, visible) { element.classList.toggle("hidden", !visible); } function render({ activity = null, description, dot = "working", eyebrow = "DESKTOP COMPANION", showInstall = false, title, }) { elements.eyebrow.textContent = eyebrow; elements.title.textContent = title; elements.description.textContent = description; elements.statusDot.className = `status-dot ${dot}`; show(elements.activity, Boolean(activity)); if (activity) { elements.activityLabel.textContent = activity; } show(elements.installControls, showInstall); show(elements.actionControls, false); } function renderAction(options, action) { render(options); primaryAction = action; elements.primaryAction.textContent = options.actionLabel; show(elements.actionControls, true); } function appendLog(line) { elements.installLog.textContent += `${line}\n`; elements.installLog.scrollTop = elements.installLog.scrollHeight; } function friendlyError(error) { if (typeof error === "string") { return error; } return error?.message || "OpenClaw could not complete the operation."; } function gatewayHost(gateway) { return (gateway.host || "").trim().replace(/\.$/, ""); } function canConnectDirect(gateway) { return ( gateway.tls || gateway.directReachable || gatewayHost(gateway).toLowerCase().endsWith(".ts.net") ); } function renderGateways(gateways) { elements.gatewayList.replaceChildren(); elements.discoveryStatus.textContent = gateways.length ? `${gateways.length} FOUND` : "SEARCHING"; if (!gateways.length) { const empty = document.createElement("p"); empty.className = "discovery-empty"; empty.textContent = "Looking for nearby OpenClaw gateways…"; elements.gatewayList.append(empty); return; } for (const gateway of gateways) { const button = document.createElement("button"); button.className = "gateway-card"; button.type = "button"; button.disabled = !canConnectDirect(gateway); if (button.disabled) { button.title = "This gateway does not advertise a direct connection."; } const copy = document.createElement("span"); copy.className = "gateway-copy"; const name = document.createElement("span"); name.className = "gateway-name"; name.textContent = gateway.name; const endpoint = document.createElement("span"); endpoint.className = "gateway-endpoint"; endpoint.textContent = `${gatewayHost(gateway)}:${gateway.port}`; copy.append(name, endpoint); const badge = document.createElement("span"); badge.className = `gateway-badge${gateway.tls ? " secure" : ""}`; badge.textContent = gateway.tls ? "TLS" : "HTTP"; button.append(copy, badge); button.addEventListener("click", () => { button.disabled = true; void invoke("connect_discovered_gateway", { host: gateway.host, port: gateway.port, tls: gateway.tls, }).catch(() => { button.disabled = false; elements.discoveryStatus.textContent = "CONNECT FAILED"; }); }); elements.gatewayList.append(button); } } async function refreshGateways() { if (discoveryPending) { return; } discoveryPending = true; try { const gateways = await invoke("discover_gateways"); const signature = JSON.stringify(gateways); if (signature !== discoverySignature) { discoverySignature = signature; renderGateways(gateways); } } catch { discoverySignature = null; elements.discoveryStatus.textContent = "UNAVAILABLE"; } finally { discoveryPending = false; } } async function connect() { render({ activity: "Checking local services…", description: "Finding your gateway and preparing the Control UI.", title: "Connecting to OpenClaw", }); try { const snapshot = await invoke("bootstrap"); if (snapshot.phase === "missingCli") { render({ activity: "Starting the bundled installer…", description: "OpenClaw is installing its managed CLI and Node runtime.", eyebrow: "FIRST-RUN SETUP", title: "Preparing OpenClaw", }); await install(); } } catch (error) { renderRetry(friendlyError(error)); } } async function install() { elements.installButton.disabled = true; elements.channel.disabled = true; elements.installLog.textContent = ""; elements.logStatus.textContent = "RUNNING"; show(elements.logWrap, true); render({ activity: "Installing OpenClaw…", description: "A managed CLI and Node runtime are being installed in your home directory.", eyebrow: "INSTALLING", title: "Preparing your companion", }); try { await invoke("install_cli", { channel: elements.channel.value }); elements.logStatus.textContent = "COMPLETE"; } catch (error) { elements.logStatus.textContent = "FAILED"; appendLog(friendlyError(error)); render({ description: "Installation did not finish. Review the final log lines, choose a release channel, then retry.", dot: "error", eyebrow: "INSTALLATION ISSUE", showInstall: true, title: "OpenClaw needs attention", }); } finally { elements.installButton.disabled = false; elements.channel.disabled = false; } } async function runGatewayAction(action) { render({ activity: `${action === "restart" ? "Restarting" : "Starting"} gateway…`, description: "OpenClaw is waiting for the local gateway to become healthy.", eyebrow: "GATEWAY", title: "One moment", }); try { await invoke("gateway_action", { action }); } catch (error) { renderRetry(friendlyError(error)); } } function renderRetry(message) { show(elements.logWrap, false); renderAction( { actionLabel: "Try again", description: message, dot: "error", eyebrow: "CONNECTION ISSUE", title: "OpenClaw needs attention", }, connect, ); } elements.installButton.addEventListener("click", () => { void install(); }); elements.primaryAction.addEventListener("click", () => { void primaryAction?.(); }); await listen("install-progress", ({ payload }) => appendLog(payload.line)); void refreshGateways(); window.setInterval(() => void refreshGateways(), 2000); const mode = new URLSearchParams(window.location.search).get("mode"); if (mode === "reconnecting") { render({ activity: "Retrying every few seconds…", description: "The gateway connection dropped. OpenClaw will restore the dashboard automatically.", eyebrow: "GATEWAY OFFLINE", title: "Reconnecting", }); } else if (mode === "stopped") { renderAction( { actionLabel: "Start Gateway", description: "The gateway is stopped. The desktop companion will remain available in the tray.", dot: "idle", eyebrow: "GATEWAY STOPPED", title: "OpenClaw is standing by", }, () => runGatewayAction("start"), ); } else if (mode === "error") { renderRetry("The last gateway action failed. Check the service, then retry."); } else { await connect(); }