Files
openclaw/apps/linux/ui/main.js

371 lines
11 KiB
JavaScript

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"),
updateAction: document.querySelector("#update-action"),
updateBanner: document.querySelector("#update-banner"),
updateDismiss: document.querySelector("#update-dismiss"),
updateMessage: document.querySelector("#update-message"),
updateProgress: document.querySelector("#update-progress"),
updateTitle: document.querySelector("#update-title"),
};
let primaryAction = null;
let updateAction = 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 renderUpdate({ action = null, actionLabel = "", message, progress = false, title }) {
elements.updateTitle.textContent = title;
elements.updateMessage.textContent = message;
updateAction = action;
elements.updateAction.textContent = actionLabel;
show(elements.updateAction, Boolean(action));
show(elements.updateProgress, progress);
show(elements.updateBanner, true);
}
function formatBytes(bytes) {
if (!Number.isFinite(bytes)) {
return "";
}
const units = ["B", "KB", "MB", "GB"];
let value = bytes;
let unit = 0;
while (value >= 1024 && unit < units.length - 1) {
value /= 1024;
unit += 1;
}
return `${value.toFixed(unit === 0 ? 0 : 1)} ${units[unit]}`;
}
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") {
const buildInfo = await invoke("build_info").catch(() => null);
if (buildInfo?.releaseBuild === false) {
elements.channel.value = "dev";
render({
description:
"This companion is a development build, so its Gateway install should usually use the matching release channel.",
eyebrow: "FIRST-RUN SETUP",
showInstall: true,
title: "Choose a release channel",
});
return;
}
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?.();
});
elements.updateAction.addEventListener("click", () => {
void updateAction?.();
});
elements.updateDismiss.addEventListener("click", () => {
show(elements.updateBanner, false);
});
await listen("install-progress", ({ payload }) => appendLog(payload.line));
await listen("updater://not-available", () => {
renderUpdate({
message: "No update is available.",
title: "OpenClaw is up to date",
});
});
await listen("updater://available", ({ payload }) => {
elements.updateProgress.removeAttribute("value");
renderUpdate({
message: payload.notes || "Downloading in the background…",
progress: true,
title: `Update available v${payload.version} — downloading…`,
});
});
await listen("updater://progress", ({ payload }) => {
if (payload.total) {
elements.updateProgress.max = payload.total;
elements.updateProgress.value = payload.downloaded;
elements.updateMessage.textContent = `${formatBytes(payload.downloaded)} of ${formatBytes(payload.total)}`;
} else {
elements.updateProgress.removeAttribute("value");
elements.updateMessage.textContent = `${formatBytes(payload.downloaded)} downloaded`;
}
});
await listen("updater://ready", ({ payload }) => {
renderUpdate({
action: () => invoke("relaunch"),
actionLabel: "Restart to update",
message: `Version v${payload.version} is installed and ready.`,
title: "Update ready",
});
});
await listen("updater://available-manual", ({ payload }) => {
renderUpdate({
action: () =>
invoke("open_release_page").catch((error) => {
renderUpdate({
message: friendlyError(error),
title: "Could not open release page",
});
}),
actionLabel: "Open download page",
message: payload.notes || "Install the latest system package from the release page.",
title: `Update available v${payload.version}`,
});
});
await listen("updater://error", ({ payload }) => {
renderUpdate({
message: payload.message,
title: "Update check failed",
});
});
void invoke("updater_ready");
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();
}