agent-zero/plugins/_browser/webui/browser-config-store.js
2026-05-09 17:36:15 +02:00

291 lines
9.8 KiB
JavaScript

import { createStore } from "/js/AlpineStore.js";
import { callJsonApi } from "/js/api.js";
const BROWSER_EXTENSIONS_API = "/plugins/_browser/extensions";
const BROWSER_STATUS_API = "/plugins/_browser/status";
const RUNTIME_BACKENDS = new Set(["container", "host_required"]);
const HOST_PRIVACY_POLICIES = new Set(["enforce_local", "warn", "allow"]);
const HOST_PROFILE_MODES = new Set(["existing", "agent"]);
function normalizePathList(value) {
const source = Array.isArray(value)
? value
: String(value || "").split(/\r?\n/);
const seen = new Set();
const paths = [];
for (const item of source) {
const path = String(item || "").trim();
if (!path || seen.has(path)) continue;
seen.add(path);
paths.push(path);
}
return paths;
}
function ensureConfig(config) {
if (!config || typeof config !== "object") return null;
config.extension_paths = normalizePathList(config.extension_paths);
config.default_homepage = String(config.default_homepage || "about:blank").trim() || "about:blank";
config.autofocus_active_page = normalizeBoolean(config.autofocus_active_page, true);
config.runtime_backend = normalizeRuntimeBackend(config.runtime_backend);
config.host_browser_privacy_policy = normalizeChoice(
config.host_browser_privacy_policy,
HOST_PRIVACY_POLICIES,
"allow",
);
config.host_browser_profile_mode = normalizeChoice(
config.host_browser_profile_mode,
HOST_PROFILE_MODES,
"existing",
);
config.model_preset = String(config.model_preset || "").trim();
delete config.model;
return config;
}
function normalizeChoice(value, allowed, fallback) {
const normalized = String(value || "").trim().toLowerCase().replace(/-/g, "_");
return allowed.has(normalized) ? normalized : fallback;
}
function normalizeRuntimeBackend(value) {
const normalized = String(value || "").trim().toLowerCase().replace(/-/g, "_");
if (normalized === "host_when_available") return "host_required";
return RUNTIME_BACKENDS.has(normalized) ? normalized : "container";
}
function normalizeBoolean(value, fallback = true) {
if (value === undefined || value === null || value === "") return fallback;
if (typeof value === "boolean") return value;
if (typeof value === "number") return Boolean(value);
const normalized = String(value).trim().toLowerCase();
if (["1", "true", "yes", "on", "enabled"].includes(normalized)) return true;
if (["0", "false", "no", "off", "disabled"].includes(normalized)) return false;
return fallback;
}
function hostBrowserFamilyLabel(value) {
const family = String(value || "").trim().toLowerCase();
const a0Profile = family.endsWith("-a0");
const remoteDebugging = family.endsWith("-cdp");
const base = a0Profile ? family.slice(0, -3) : remoteDebugging ? family.slice(0, -4) : family;
const labels = {
chrome: "Chrome",
chromium: "Chromium",
edge: "Edge",
"edge-dev": "Edge Dev",
};
const label = labels[base] || "Host browser";
if (remoteDebugging) return `${label} (allowed)`;
return a0Profile ? `${label} (A0 profile)` : label;
}
function hostBrowserStatusLabel(value) {
const status = String(value || "").trim().toLowerCase();
if (status === "active") return "open";
if (status === "ready") return "ready";
if (status === "disabled") return "will open on first use";
if (status === "relaunch_required") return "close browser and retry";
if (status === "unsupported") return "unavailable";
return status || "ready";
}
export const store = createStore("browserConfig", {
config: null,
extensionsList: [],
extensionsLoading: false,
extensionsError: "",
extensionsMessage: "",
extensionDeleteLoadingPath: "",
hostBrowserStatus: null,
hostBrowserStatusLoading: false,
async init(config) {
this.bindConfig(config);
await Promise.all([this.loadExtensionsList(), this.loadHostBrowserStatus()]);
},
cleanup() {
this.config = null;
this.extensionsList = [];
this.extensionsError = "";
this.extensionsMessage = "";
this.extensionDeleteLoadingPath = "";
this.hostBrowserStatus = null;
this.hostBrowserStatusLoading = false;
},
bindConfig(config) {
const safeConfig = ensureConfig(config);
if (!safeConfig) return;
if (this.config === safeConfig) return;
this.config = safeConfig;
},
setAutofocusActivePage(enabled) {
const safeConfig = ensureConfig(this.config);
if (!safeConfig) return;
safeConfig.autofocus_active_page = Boolean(enabled);
},
autofocusLabel() {
return this.config?.autofocus_active_page === false ? "Off" : "On";
},
runtimeBackendLabel() {
const value = this.config?.runtime_backend || "container";
if (value === "host_required") return "Bring Your Own Browser";
return "Docker Browser";
},
privacyPolicyLabel() {
const value = this.config?.host_browser_privacy_policy || "allow";
if (value === "warn") return "Warn When Using Cloud";
if (value === "allow") return "Allow";
return "Local Models Only";
},
hostBrowserProfileModeLabel() {
const value = this.config?.host_browser_profile_mode || "existing";
if (value === "agent") return "Clean Agent Profile";
return "Existing Browser Profile";
},
async loadHostBrowserStatus() {
if (this.hostBrowserStatusLoading) return;
this.hostBrowserStatusLoading = true;
try {
const response = await callJsonApi(BROWSER_STATUS_API, {});
this.hostBrowserStatus = response?.host_browser || { connectors: [] };
} catch (_error) {
this.hostBrowserStatus = { connectors: [] };
} finally {
this.hostBrowserStatusLoading = false;
}
},
hostBrowserConnectorLabel() {
const connectors = Array.isArray(this.hostBrowserStatus?.connectors)
? this.hostBrowserStatus.connectors
: [];
const active = connectors.find((item) => item?.supported && item?.enabled);
if (active) {
const profile = active.profile_label ? ` - ${active.profile_label}` : "";
return `${hostBrowserFamilyLabel(active.browser_family)}${profile}: ${hostBrowserStatusLabel(active.status)}`;
}
const preparable = connectors.find((item) => item?.can_prepare || item?.supported);
if (preparable) return "A0 CLI connected - browser will open on first use";
if (connectors.length) return "A0 CLI connected - host browser unavailable";
return "Connect A0 CLI to use a host browser";
},
hasPaths() {
return this.pathCount() > 0;
},
pathCount() {
return normalizePathList(this.config?.extension_paths).length;
},
pathCountLabel() {
const count = this.pathCount();
if (!count) return "No extensions enabled";
return `${count} extension${count === 1 ? "" : "s"} enabled`;
},
extensionModeReady() {
return this.pathCount() > 0;
},
async loadExtensionsList() {
if (this.extensionsLoading) return;
this.extensionsLoading = true;
this.extensionsError = "";
try {
const response = await callJsonApi(BROWSER_EXTENSIONS_API, { action: "list" });
if (!response?.ok) {
throw new Error(response?.error || "Could not load browser extensions.");
}
this.applyExtensionPayload(response);
} catch (error) {
this.extensionsList = [];
this.extensionsError = error instanceof Error ? error.message : String(error);
} finally {
this.extensionsLoading = false;
}
},
applyExtensionPayload(response = {}) {
this.extensionsList = Array.isArray(response.extensions) ? response.extensions : [];
if (Array.isArray(response.extension_paths) && this.config) {
this.config.extension_paths = normalizePathList(response.extension_paths);
}
},
extensionEnabled(extension) {
const path = typeof extension === "string" ? extension : extension?.path;
return normalizePathList(this.config?.extension_paths).includes(String(path || ""));
},
setExtensionEnabled(extension, enabled) {
const path = String((typeof extension === "string" ? extension : extension?.path) || "").trim();
if (!path) return;
const safeConfig = ensureConfig(this.config);
if (!safeConfig) return;
const paths = normalizePathList(safeConfig.extension_paths);
if (enabled && !paths.includes(path)) {
paths.push(path);
} else if (!enabled) {
const index = paths.indexOf(path);
if (index >= 0) paths.splice(index, 1);
}
safeConfig.extension_paths = paths;
},
extensionCanDelete(extension) {
return Boolean(extension?.can_delete);
},
extensionDeleteTitle(extension) {
return this.extensionCanDelete(extension)
? "Delete extension"
: "Only Browser-managed extensions can be deleted";
},
async deleteExtension(extension) {
const path = String(extension?.path || "").trim();
if (!path) return;
this.extensionsError = "";
this.extensionsMessage = "";
if (!this.extensionCanDelete(extension)) {
this.extensionsError = "Only Browser-managed extensions can be deleted.";
return;
}
const name = String(extension?.name || "this extension").trim();
if (globalThis.confirm && !globalThis.confirm(`Delete ${name}? This removes the extension folder from Browser.`)) {
return;
}
this.extensionDeleteLoadingPath = path;
try {
const response = await callJsonApi(BROWSER_EXTENSIONS_API, {
action: "uninstall_extension",
path,
});
if (!response?.ok) {
throw new Error(response?.error || "Could not delete extension.");
}
this.applyExtensionPayload(response);
this.extensionsMessage = `Deleted ${response.name || name}.`;
} catch (error) {
this.extensionsError = error instanceof Error ? error.message : String(error);
} finally {
this.extensionDeleteLoadingPath = "";
}
},
extensionVersionLabel(extension) {
const version = String(extension?.version || "").trim();
return version ? `v${version}` : "Unpacked extension";
},
});