Files
Moltbot/ui/src/ui/app-settings.ts
2026-02-06 22:17:09 -08:00

433 lines
13 KiB
TypeScript

import type { OpenClawApp } from "./app.ts";
import type { AgentsListResult } from "./types.ts";
import { refreshChat } from "./app-chat.ts";
import {
startLogsPolling,
stopLogsPolling,
startDebugPolling,
stopDebugPolling,
} from "./app-polling.ts";
import { scheduleChatScroll, scheduleLogsScroll } from "./app-scroll.ts";
import { loadAgentIdentities, loadAgentIdentity } from "./controllers/agent-identity.ts";
import { loadAgentSkills } from "./controllers/agent-skills.ts";
import { loadAgents } from "./controllers/agents.ts";
import { loadChannels } from "./controllers/channels.ts";
import { loadConfig, loadConfigSchema } from "./controllers/config.ts";
import { loadCronJobs, loadCronStatus } from "./controllers/cron.ts";
import { loadDebug } from "./controllers/debug.ts";
import { loadDevices } from "./controllers/devices.ts";
import { loadExecApprovals } from "./controllers/exec-approvals.ts";
import { loadLogs } from "./controllers/logs.ts";
import { loadNodes } from "./controllers/nodes.ts";
import { loadPresence } from "./controllers/presence.ts";
import { loadSessions } from "./controllers/sessions.ts";
import { loadSkills } from "./controllers/skills.ts";
import {
inferBasePathFromPathname,
normalizeBasePath,
normalizePath,
pathForTab,
tabFromPath,
type Tab,
} from "./navigation.ts";
import { saveSettings, type UiSettings } from "./storage.ts";
import { startThemeTransition, type ThemeTransitionContext } from "./theme-transition.ts";
import { resolveTheme, type ResolvedTheme, type ThemeMode } from "./theme.ts";
type SettingsHost = {
settings: UiSettings;
password?: string;
theme: ThemeMode;
themeResolved: ResolvedTheme;
applySessionKey: string;
sessionKey: string;
tab: Tab;
connected: boolean;
chatHasAutoScrolled: boolean;
logsAtBottom: boolean;
eventLog: unknown[];
eventLogBuffer: unknown[];
basePath: string;
agentsList?: AgentsListResult | null;
agentsSelectedId?: string | null;
agentsPanel?: "overview" | "files" | "tools" | "skills" | "channels" | "cron";
themeMedia: MediaQueryList | null;
themeMediaHandler: ((event: MediaQueryListEvent) => void) | null;
pendingGatewayUrl?: string | null;
};
export function applySettings(host: SettingsHost, next: UiSettings) {
const normalized = {
...next,
lastActiveSessionKey: next.lastActiveSessionKey?.trim() || next.sessionKey.trim() || "main",
};
host.settings = normalized;
saveSettings(normalized);
if (next.theme !== host.theme) {
host.theme = next.theme;
applyResolvedTheme(host, resolveTheme(next.theme));
}
host.applySessionKey = host.settings.lastActiveSessionKey;
}
export function setLastActiveSessionKey(host: SettingsHost, next: string) {
const trimmed = next.trim();
if (!trimmed) {
return;
}
if (host.settings.lastActiveSessionKey === trimmed) {
return;
}
applySettings(host, { ...host.settings, lastActiveSessionKey: trimmed });
}
export function applySettingsFromUrl(host: SettingsHost) {
if (!window.location.search && !window.location.hash) {
return;
}
const url = new URL(window.location.href);
const params = new URLSearchParams(url.search);
const hashParams = new URLSearchParams(url.hash.startsWith("#") ? url.hash.slice(1) : url.hash);
const tokenRaw = params.get("token") ?? hashParams.get("token");
const passwordRaw = params.get("password") ?? hashParams.get("password");
const sessionRaw = params.get("session") ?? hashParams.get("session");
const gatewayUrlRaw = params.get("gatewayUrl") ?? hashParams.get("gatewayUrl");
let shouldCleanUrl = false;
if (tokenRaw != null) {
const token = tokenRaw.trim();
if (token && token !== host.settings.token) {
applySettings(host, { ...host.settings, token });
}
params.delete("token");
hashParams.delete("token");
shouldCleanUrl = true;
}
if (passwordRaw != null) {
const password = passwordRaw.trim();
if (password) {
(host as { password: string }).password = password;
}
params.delete("password");
hashParams.delete("password");
shouldCleanUrl = true;
}
if (sessionRaw != null) {
const session = sessionRaw.trim();
if (session) {
host.sessionKey = session;
applySettings(host, {
...host.settings,
sessionKey: session,
lastActiveSessionKey: session,
});
}
}
if (gatewayUrlRaw != null) {
const gatewayUrl = gatewayUrlRaw.trim();
if (gatewayUrl && gatewayUrl !== host.settings.gatewayUrl) {
host.pendingGatewayUrl = gatewayUrl;
}
params.delete("gatewayUrl");
hashParams.delete("gatewayUrl");
shouldCleanUrl = true;
}
if (!shouldCleanUrl) {
return;
}
url.search = params.toString();
const nextHash = hashParams.toString();
url.hash = nextHash ? `#${nextHash}` : "";
window.history.replaceState({}, "", url.toString());
}
export function setTab(host: SettingsHost, next: Tab) {
if (host.tab !== next) {
host.tab = next;
}
if (next === "chat") {
host.chatHasAutoScrolled = false;
}
if (next === "logs") {
startLogsPolling(host as unknown as Parameters<typeof startLogsPolling>[0]);
} else {
stopLogsPolling(host as unknown as Parameters<typeof stopLogsPolling>[0]);
}
if (next === "debug") {
startDebugPolling(host as unknown as Parameters<typeof startDebugPolling>[0]);
} else {
stopDebugPolling(host as unknown as Parameters<typeof stopDebugPolling>[0]);
}
void refreshActiveTab(host);
syncUrlWithTab(host, next, false);
}
export function setTheme(host: SettingsHost, next: ThemeMode, context?: ThemeTransitionContext) {
const applyTheme = () => {
host.theme = next;
applySettings(host, { ...host.settings, theme: next });
applyResolvedTheme(host, resolveTheme(next));
};
startThemeTransition({
nextTheme: next,
applyTheme,
context,
currentTheme: host.theme,
});
}
export async function refreshActiveTab(host: SettingsHost) {
if (host.tab === "overview") {
await loadOverview(host);
}
if (host.tab === "channels") {
await loadChannelsTab(host);
}
if (host.tab === "instances") {
await loadPresence(host as unknown as OpenClawApp);
}
if (host.tab === "sessions") {
await loadSessions(host as unknown as OpenClawApp);
}
if (host.tab === "cron") {
await loadCron(host);
}
if (host.tab === "skills") {
await loadSkills(host as unknown as OpenClawApp);
}
if (host.tab === "agents") {
await loadAgents(host as unknown as OpenClawApp);
await loadConfig(host as unknown as OpenClawApp);
const agentIds = host.agentsList?.agents?.map((entry) => entry.id) ?? [];
if (agentIds.length > 0) {
void loadAgentIdentities(host as unknown as OpenClawApp, agentIds);
}
const agentId =
host.agentsSelectedId ?? host.agentsList?.defaultId ?? host.agentsList?.agents?.[0]?.id;
if (agentId) {
void loadAgentIdentity(host as unknown as OpenClawApp, agentId);
if (host.agentsPanel === "skills") {
void loadAgentSkills(host as unknown as OpenClawApp, agentId);
}
if (host.agentsPanel === "channels") {
void loadChannels(host as unknown as OpenClawApp, false);
}
if (host.agentsPanel === "cron") {
void loadCron(host);
}
}
}
if (host.tab === "nodes") {
await loadNodes(host as unknown as OpenClawApp);
await loadDevices(host as unknown as OpenClawApp);
await loadConfig(host as unknown as OpenClawApp);
await loadExecApprovals(host as unknown as OpenClawApp);
}
if (host.tab === "chat") {
await refreshChat(host as unknown as Parameters<typeof refreshChat>[0]);
scheduleChatScroll(
host as unknown as Parameters<typeof scheduleChatScroll>[0],
!host.chatHasAutoScrolled,
);
}
if (host.tab === "config") {
await loadConfigSchema(host as unknown as OpenClawApp);
await loadConfig(host as unknown as OpenClawApp);
}
if (host.tab === "debug") {
await loadDebug(host as unknown as OpenClawApp);
host.eventLog = host.eventLogBuffer;
}
if (host.tab === "logs") {
host.logsAtBottom = true;
await loadLogs(host as unknown as OpenClawApp, { reset: true });
scheduleLogsScroll(host as unknown as Parameters<typeof scheduleLogsScroll>[0], true);
}
}
export function inferBasePath() {
if (typeof window === "undefined") {
return "";
}
const configured = window.__OPENCLAW_CONTROL_UI_BASE_PATH__;
if (typeof configured === "string" && configured.trim()) {
return normalizeBasePath(configured);
}
return inferBasePathFromPathname(window.location.pathname);
}
export function syncThemeWithSettings(host: SettingsHost) {
host.theme = host.settings.theme ?? "system";
applyResolvedTheme(host, resolveTheme(host.theme));
}
export function applyResolvedTheme(host: SettingsHost, resolved: ResolvedTheme) {
host.themeResolved = resolved;
if (typeof document === "undefined") {
return;
}
const root = document.documentElement;
root.dataset.theme = resolved;
root.style.colorScheme = resolved;
}
export function attachThemeListener(host: SettingsHost) {
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
return;
}
host.themeMedia = window.matchMedia("(prefers-color-scheme: dark)");
host.themeMediaHandler = (event) => {
if (host.theme !== "system") {
return;
}
applyResolvedTheme(host, event.matches ? "dark" : "light");
};
if (typeof host.themeMedia.addEventListener === "function") {
host.themeMedia.addEventListener("change", host.themeMediaHandler);
return;
}
const legacy = host.themeMedia as MediaQueryList & {
addListener: (cb: (event: MediaQueryListEvent) => void) => void;
};
legacy.addListener(host.themeMediaHandler);
}
export function detachThemeListener(host: SettingsHost) {
if (!host.themeMedia || !host.themeMediaHandler) {
return;
}
if (typeof host.themeMedia.removeEventListener === "function") {
host.themeMedia.removeEventListener("change", host.themeMediaHandler);
return;
}
const legacy = host.themeMedia as MediaQueryList & {
removeListener: (cb: (event: MediaQueryListEvent) => void) => void;
};
legacy.removeListener(host.themeMediaHandler);
host.themeMedia = null;
host.themeMediaHandler = null;
}
export function syncTabWithLocation(host: SettingsHost, replace: boolean) {
if (typeof window === "undefined") {
return;
}
const resolved = tabFromPath(window.location.pathname, host.basePath) ?? "chat";
setTabFromRoute(host, resolved);
syncUrlWithTab(host, resolved, replace);
}
export function onPopState(host: SettingsHost) {
if (typeof window === "undefined") {
return;
}
const resolved = tabFromPath(window.location.pathname, host.basePath);
if (!resolved) {
return;
}
const url = new URL(window.location.href);
const session = url.searchParams.get("session")?.trim();
if (session) {
host.sessionKey = session;
applySettings(host, {
...host.settings,
sessionKey: session,
lastActiveSessionKey: session,
});
}
setTabFromRoute(host, resolved);
}
export function setTabFromRoute(host: SettingsHost, next: Tab) {
if (host.tab !== next) {
host.tab = next;
}
if (next === "chat") {
host.chatHasAutoScrolled = false;
}
if (next === "logs") {
startLogsPolling(host as unknown as Parameters<typeof startLogsPolling>[0]);
} else {
stopLogsPolling(host as unknown as Parameters<typeof stopLogsPolling>[0]);
}
if (next === "debug") {
startDebugPolling(host as unknown as Parameters<typeof startDebugPolling>[0]);
} else {
stopDebugPolling(host as unknown as Parameters<typeof stopDebugPolling>[0]);
}
if (host.connected) {
void refreshActiveTab(host);
}
}
export function syncUrlWithTab(host: SettingsHost, tab: Tab, replace: boolean) {
if (typeof window === "undefined") {
return;
}
const targetPath = normalizePath(pathForTab(tab, host.basePath));
const currentPath = normalizePath(window.location.pathname);
const url = new URL(window.location.href);
if (tab === "chat" && host.sessionKey) {
url.searchParams.set("session", host.sessionKey);
} else {
url.searchParams.delete("session");
}
if (currentPath !== targetPath) {
url.pathname = targetPath;
}
if (replace) {
window.history.replaceState({}, "", url.toString());
} else {
window.history.pushState({}, "", url.toString());
}
}
export function syncUrlWithSessionKey(host: SettingsHost, sessionKey: string, replace: boolean) {
if (typeof window === "undefined") {
return;
}
const url = new URL(window.location.href);
url.searchParams.set("session", sessionKey);
if (replace) {
window.history.replaceState({}, "", url.toString());
} else {
window.history.pushState({}, "", url.toString());
}
}
export async function loadOverview(host: SettingsHost) {
await Promise.all([
loadChannels(host as unknown as OpenClawApp, false),
loadPresence(host as unknown as OpenClawApp),
loadSessions(host as unknown as OpenClawApp),
loadCronStatus(host as unknown as OpenClawApp),
loadDebug(host as unknown as OpenClawApp),
]);
}
export async function loadChannelsTab(host: SettingsHost) {
await Promise.all([
loadChannels(host as unknown as OpenClawApp, true),
loadConfigSchema(host as unknown as OpenClawApp),
loadConfig(host as unknown as OpenClawApp),
]);
}
export async function loadCron(host: SettingsHost) {
await Promise.all([
loadChannels(host as unknown as OpenClawApp, false),
loadCronStatus(host as unknown as OpenClawApp),
loadCronJobs(host as unknown as OpenClawApp),
]);
}