mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-10-19 15:33:54 +00:00
Keep quick settings focused on the model preset, Host/Container choice, native page zoom, and installed Chrome extensions. Show a dimmed host configuration link only in Host mode, and match the Browser and Zoom labels to the preset label. Move extension creation, installation, and review into Browser config. Open Chromium management pages for browsing data, history, passwords, autofill, extensions, and downloads through the existing Browser surface. Honor native download folder and save-dialog preferences, and add annotation screenshots as an opt-in setting that defaults off. Update the Browser contracts and regression coverage. Validation: 201 browser/tool tests and 29 host-connector tests passed, with the host suite isolated from existing module stubs. Live checks on localhost:32083 covered settings navigation, runtime selection, zoom, annotation attachments, download preferences, matching typography, and narrow layouts.
681 lines
26 KiB
JavaScript
681 lines
26 KiB
JavaScript
import { createStore } from "/js/AlpineStore.js";
|
|
import { callJsonApi } from "/js/api.js";
|
|
import { showConfirmDialog } from "/js/confirmDialog.js";
|
|
import { store as notificationStore } from "/components/notifications/notification-store.js";
|
|
|
|
const BROWSER_PAGES = {
|
|
history: "chrome://history/",
|
|
clearData: "chrome://settings/clearBrowserData",
|
|
passwords: "chrome://password-manager/passwords",
|
|
addresses: "chrome://settings/addresses",
|
|
extensions: "chrome://extensions/",
|
|
downloadSettings: "chrome://settings/downloads",
|
|
downloads: "chrome://downloads/",
|
|
};
|
|
const EXTENSIONS_ROOT = "/a0/usr/_browser/extensions";
|
|
const BROWSER_EXTENSIONS_API = "/plugins/_browser/extensions";
|
|
const BROWSER_SETUP_API = "/plugins/_browser/host_browser_setup";
|
|
const BROWSER_STATUS_API = "/plugins/_browser/status";
|
|
const RUNTIME_BACKENDS = new Set(["container", "host_required"]);
|
|
const BROWSER_TAB_SCOPES = new Set(["per_context", "shared"]);
|
|
const HOST_PRIVACY_POLICIES = new Set(["enforce_local", "warn", "allow"]);
|
|
const HOST_PROFILE_MODES = new Set(["existing", "agent"]);
|
|
const DEFAULT_MAX_OPEN_TABS = 32;
|
|
const MIN_MAX_OPEN_TABS = 1;
|
|
const HARD_MAX_OPEN_TABS = 50;
|
|
const HOST_BROWSER_STATUS_REFRESH_MS = 1000;
|
|
const CUSTOM_HOST_BROWSER_SELECTION = "__custom_endpoint__";
|
|
|
|
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.annotation_screenshots = normalizeBoolean(config.annotation_screenshots, false);
|
|
config.browser_tab_scope = normalizeChoice(config.browser_tab_scope, BROWSER_TAB_SCOPES, "per_context");
|
|
config.max_open_tabs = normalizeInt(config.max_open_tabs, DEFAULT_MAX_OPEN_TABS, MIN_MAX_OPEN_TABS, HARD_MAX_OPEN_TABS);
|
|
config.evaluate_timeout_seconds ??= 30;
|
|
config.runtime_backend = normalizeRuntimeBackend(config.runtime_backend);
|
|
config.proxy_server = String(config.proxy_server || "").trim();
|
|
config.proxy_bypass = String(config.proxy_bypass || "").trim();
|
|
config.proxy_username = String(config.proxy_username || "");
|
|
config.proxy_password = String(config.proxy_password || "");
|
|
config.keyboard_layout = normalizeXkbToken(config.keyboard_layout);
|
|
config.keyboard_variant = normalizeXkbToken(config.keyboard_variant);
|
|
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.host_browser_selection = normalizeHostBrowserSelection(config.host_browser_selection);
|
|
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 normalizeInt(value, fallback, minimum, maximum) {
|
|
const number = Number.parseInt(value, 10);
|
|
if (!Number.isFinite(number)) return fallback;
|
|
return Math.max(minimum, Math.min(maximum, number));
|
|
}
|
|
|
|
function normalizeXkbToken(value) {
|
|
return String(value || "")
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9_-]/g, "")
|
|
.slice(0, 32);
|
|
}
|
|
|
|
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 normalizeHostBrowserSelection(value) {
|
|
const raw = String(value || "").trim();
|
|
if (raw.includes("://") || /^(?:\[[^\]]+\]|[^/:\s]+):\d+$/.test(raw)) {
|
|
return raw.replace(/\s+/g, "").slice(0, 2048);
|
|
}
|
|
return raw.toLowerCase().replace(/\s+/g, "_").slice(0, 200);
|
|
}
|
|
|
|
function normalizeCustomHostBrowserEndpoint(value) {
|
|
const raw = String(value || "").trim();
|
|
if (!raw) return "";
|
|
const candidate = raw.includes("://") ? raw : `http://${raw}`;
|
|
try {
|
|
const url = new URL(candidate);
|
|
if (!url.host) return "";
|
|
if (["http:", "https:"].includes(url.protocol)) {
|
|
if (!["/", "/json/version"].includes(url.pathname)) return "";
|
|
const path = url.pathname === "/" ? "" : url.pathname;
|
|
return normalizeHostBrowserSelection(`${url.protocol}//${url.host}${path}${url.search || ""}`);
|
|
}
|
|
if (!["ws:", "wss:"].includes(url.protocol)) return "";
|
|
return normalizeHostBrowserSelection(`${url.protocol}//${url.host}${url.pathname === "/" ? "" : url.pathname}${url.search || ""}`);
|
|
} catch (_error) {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function isCustomHostBrowserEndpoint(value) {
|
|
return Boolean(normalizeCustomHostBrowserEndpoint(value));
|
|
}
|
|
|
|
function stableHostBrowserSelection(value, status) {
|
|
const selection = normalizeHostBrowserSelection(value);
|
|
if (!selection) return "";
|
|
const connectors = Array.isArray(status?.connectors) ? status.connectors : [];
|
|
for (const connector of connectors) {
|
|
const candidates = [
|
|
...(Array.isArray(connector?.available_browsers) ? connector.available_browsers : []),
|
|
connector,
|
|
];
|
|
for (const candidate of candidates) {
|
|
const endpoint = normalizeCustomHostBrowserEndpoint(candidate?.cdp_endpoint);
|
|
const browserId = normalizeHostBrowserSelection(candidate?.id || candidate?.browser_id);
|
|
if (endpoint && endpoint === selection && browserId) return browserId;
|
|
}
|
|
}
|
|
return selection;
|
|
}
|
|
|
|
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",
|
|
brave: "Brave",
|
|
opera: "Opera",
|
|
safari: "Safari",
|
|
vivaldi: "Vivaldi",
|
|
};
|
|
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,
|
|
extensionInstallUrl: "",
|
|
extensionActionLoading: false,
|
|
extensionsRoot: "",
|
|
openingBrowserPage: "",
|
|
extensionDeleteLoadingPath: "",
|
|
hostBrowserStatus: null,
|
|
hostBrowserStatusLoading: false,
|
|
hostBrowserStatusRefreshTimer: null,
|
|
hostBrowserSetupOpening: "",
|
|
hostBrowserCustomEndpoint: "",
|
|
hostBrowserCustomMode: false,
|
|
|
|
async init(config) {
|
|
this.bindConfig(config);
|
|
await Promise.all([this.loadExtensionsList(), this.loadHostBrowserStatus()]);
|
|
this.startHostBrowserStatusRefresh();
|
|
},
|
|
|
|
cleanup() {
|
|
this.stopHostBrowserStatusRefresh();
|
|
this.config = null;
|
|
this.extensionsList = [];
|
|
this.extensionInstallUrl = "";
|
|
this.openingBrowserPage = "";
|
|
this.extensionDeleteLoadingPath = "";
|
|
this.hostBrowserStatus = null;
|
|
this.hostBrowserStatusLoading = false;
|
|
this.hostBrowserSetupOpening = "";
|
|
this.hostBrowserCustomEndpoint = "";
|
|
this.hostBrowserCustomMode = false;
|
|
},
|
|
|
|
startHostBrowserStatusRefresh() {
|
|
this.stopHostBrowserStatusRefresh();
|
|
this.hostBrowserStatusRefreshTimer = window.setInterval(
|
|
() => this.loadHostBrowserStatus(),
|
|
HOST_BROWSER_STATUS_REFRESH_MS,
|
|
);
|
|
},
|
|
|
|
stopHostBrowserStatusRefresh() {
|
|
if (!this.hostBrowserStatusRefreshTimer) return;
|
|
window.clearInterval(this.hostBrowserStatusRefreshTimer);
|
|
this.hostBrowserStatusRefreshTimer = null;
|
|
},
|
|
|
|
bindConfig(config) {
|
|
const safeConfig = ensureConfig(config);
|
|
if (!safeConfig) return;
|
|
if (this.config === safeConfig) return;
|
|
this.config = safeConfig;
|
|
if (isCustomHostBrowserEndpoint(safeConfig.host_browser_selection)) {
|
|
this.hostBrowserCustomEndpoint = safeConfig.host_browser_selection;
|
|
}
|
|
},
|
|
|
|
async openBrowserPage(section) {
|
|
const url = BROWSER_PAGES[section];
|
|
if (!url || this.openingBrowserPage) return;
|
|
this.openingBrowserPage = section;
|
|
try {
|
|
const { store: browser } = await import("/plugins/_browser/webui/browser-store.js");
|
|
const { store: canvas } = await import("/components/canvas/right-canvas-store.js");
|
|
await browser.command("open", { url });
|
|
if (browser.error) throw new Error(browser.error);
|
|
await canvas.openModalSurface("browser");
|
|
} catch (error) {
|
|
notificationStore.addFrontendToastOnly("error", error instanceof Error ? error.message : String(error));
|
|
} finally {
|
|
this.openingBrowserPage = "";
|
|
}
|
|
},
|
|
|
|
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";
|
|
},
|
|
|
|
setBrowserTabScope(value) {
|
|
const safeConfig = ensureConfig(this.config);
|
|
if (!safeConfig) return;
|
|
safeConfig.browser_tab_scope = normalizeChoice(value, BROWSER_TAB_SCOPES, "per_context");
|
|
},
|
|
|
|
browserTabScopeLabel() {
|
|
return this.config?.browser_tab_scope === "shared" ? "Shared" : "Per chat";
|
|
},
|
|
|
|
normalizeMaxOpenTabs() {
|
|
const safeConfig = ensureConfig(this.config);
|
|
if (!safeConfig) return;
|
|
safeConfig.max_open_tabs = normalizeInt(
|
|
safeConfig.max_open_tabs,
|
|
DEFAULT_MAX_OPEN_TABS,
|
|
MIN_MAX_OPEN_TABS,
|
|
HARD_MAX_OPEN_TABS,
|
|
);
|
|
},
|
|
|
|
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";
|
|
},
|
|
|
|
hostBrowserOptions() {
|
|
const connectors = Array.isArray(this.hostBrowserStatus?.connectors)
|
|
? this.hostBrowserStatus.connectors
|
|
: [];
|
|
const options = [{ value: "", label: "Automatic (first available)" }];
|
|
const seen = new Set([""]);
|
|
for (const connector of connectors) {
|
|
const advertised = Array.isArray(connector?.available_browsers)
|
|
? connector.available_browsers
|
|
: [];
|
|
for (const browser of advertised) {
|
|
const endpoint = normalizeCustomHostBrowserEndpoint(browser?.cdp_endpoint);
|
|
const browserId = normalizeHostBrowserSelection(browser?.id || browser?.browser_id);
|
|
const family = normalizeHostBrowserSelection(browser?.family || browser?.browser_family);
|
|
const value = endpoint
|
|
? browserId || endpoint
|
|
: family === "safari" ? browserId : "";
|
|
if (!value || seen.has(value)) continue;
|
|
seen.add(value);
|
|
const label = browser?.label || hostBrowserFamilyLabel(browser?.family || value);
|
|
const status = browser?.status ? ` - ${hostBrowserStatusLabel(browser.status)}` : "";
|
|
options.push({ value, label: `${label}${status}` });
|
|
}
|
|
const fallbackEndpoint = normalizeCustomHostBrowserEndpoint(connector?.cdp_endpoint);
|
|
const fallbackValue = fallbackEndpoint
|
|
? normalizeHostBrowserSelection(connector?.browser_id) || fallbackEndpoint
|
|
: "";
|
|
if (fallbackValue && !seen.has(fallbackValue)) {
|
|
seen.add(fallbackValue);
|
|
const label = connector?.browser_label || hostBrowserFamilyLabel(connector?.browser_family || fallbackValue);
|
|
options.push({ value: fallbackValue, label });
|
|
}
|
|
}
|
|
const selected = normalizeHostBrowserSelection(this.config?.host_browser_selection);
|
|
if (selected && !seen.has(selected) && !isCustomHostBrowserEndpoint(selected)) {
|
|
seen.add(selected);
|
|
options.push({ value: selected, label: `Saved: ${selected}` });
|
|
}
|
|
options.push({ value: CUSTOM_HOST_BROWSER_SELECTION, label: "Custom endpoint" });
|
|
return options;
|
|
},
|
|
|
|
hostBrowserSelectValue() {
|
|
if (this.hostBrowserCustomMode) return CUSTOM_HOST_BROWSER_SELECTION;
|
|
const selected = normalizeHostBrowserSelection(this.config?.host_browser_selection);
|
|
if (!selected) return "";
|
|
if (this.hostBrowserOptions().some((option) => option.value === selected)) return selected;
|
|
if (isCustomHostBrowserEndpoint(selected)) return CUSTOM_HOST_BROWSER_SELECTION;
|
|
return selected;
|
|
},
|
|
|
|
setHostBrowserSelection(value) {
|
|
const safeConfig = ensureConfig(this.config);
|
|
if (!safeConfig) return;
|
|
if (value === CUSTOM_HOST_BROWSER_SELECTION) {
|
|
this.hostBrowserCustomMode = true;
|
|
if (isCustomHostBrowserEndpoint(safeConfig.host_browser_selection)) {
|
|
this.hostBrowserCustomEndpoint = safeConfig.host_browser_selection;
|
|
} else {
|
|
safeConfig.host_browser_selection = "";
|
|
}
|
|
return;
|
|
}
|
|
this.hostBrowserCustomMode = false;
|
|
safeConfig.host_browser_selection = normalizeHostBrowserSelection(value);
|
|
},
|
|
|
|
showCustomHostBrowserEndpoint() {
|
|
return this.hostBrowserSelectValue() === CUSTOM_HOST_BROWSER_SELECTION;
|
|
},
|
|
|
|
setCustomHostBrowserEndpoint(value) {
|
|
this.hostBrowserCustomMode = true;
|
|
this.hostBrowserCustomEndpoint = String(value || "").trim();
|
|
const safeConfig = ensureConfig(this.config);
|
|
if (!safeConfig) return;
|
|
const endpoint = normalizeCustomHostBrowserEndpoint(this.hostBrowserCustomEndpoint);
|
|
safeConfig.host_browser_selection = endpoint
|
|
|| normalizeHostBrowserSelection(this.hostBrowserCustomEndpoint);
|
|
},
|
|
|
|
customHostBrowserEndpointDiagnostic() {
|
|
if (!this.hostBrowserCustomEndpoint) {
|
|
return "Paste a ws://.../devtools/browser/... endpoint from the browser inspect page.";
|
|
}
|
|
const endpoint = normalizeCustomHostBrowserEndpoint(this.hostBrowserCustomEndpoint);
|
|
if (endpoint) return `Using ${endpoint}`;
|
|
return "Use host:port, an http(s):// discovery address, or a ws(s):// browser endpoint.";
|
|
},
|
|
|
|
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: [] };
|
|
const safeConfig = ensureConfig(this.config);
|
|
if (safeConfig) {
|
|
const stable = stableHostBrowserSelection(
|
|
safeConfig.host_browser_selection,
|
|
this.hostBrowserStatus,
|
|
);
|
|
if (stable !== safeConfig.host_browser_selection) {
|
|
safeConfig.host_browser_selection = stable;
|
|
this.hostBrowserCustomEndpoint = "";
|
|
this.hostBrowserCustomMode = false;
|
|
}
|
|
}
|
|
} catch (_error) {
|
|
this.hostBrowserStatus = { connectors: [] };
|
|
} finally {
|
|
this.hostBrowserStatusLoading = false;
|
|
}
|
|
},
|
|
|
|
async openHostBrowserSetup(browserFamily) {
|
|
if (this.hostBrowserSetupOpening) return;
|
|
const family = String(browserFamily || "").trim().toLowerCase();
|
|
if (!["chrome", "opera", "edge"].includes(family)) return;
|
|
this.hostBrowserSetupOpening = family;
|
|
try {
|
|
await callJsonApi(BROWSER_SETUP_API, { browser_family: family });
|
|
const label = family === "edge" ? "Edge" : `${family[0].toUpperCase()}${family.slice(1)}`;
|
|
notificationStore.addFrontendToastOnly(
|
|
"success",
|
|
`${label} remote-debugging setup opened on the connected host.`,
|
|
"",
|
|
4,
|
|
);
|
|
} catch (error) {
|
|
console.error("Failed to open host browser setup:", error);
|
|
notificationStore.addFrontendToastOnly(
|
|
"error",
|
|
"Connect or update Launcher or A0 CLI, enable host Browser access, and try again.",
|
|
"Could not open browser setup",
|
|
7,
|
|
);
|
|
} finally {
|
|
this.hostBrowserSetupOpening = "";
|
|
}
|
|
},
|
|
|
|
hostBrowserSetupAvailable(browserFamily) {
|
|
const family = String(browserFamily || "").trim().toLowerCase();
|
|
const connectors = Array.isArray(this.hostBrowserStatus?.connectors)
|
|
? this.hostBrowserStatus.connectors
|
|
: [];
|
|
return connectors.some((connector) => {
|
|
if (!Array.isArray(connector?.features) || !connector.features.includes("open_remote_debugging")) {
|
|
return false;
|
|
}
|
|
const browsers = Array.isArray(connector?.available_browsers)
|
|
? connector.available_browsers
|
|
: [];
|
|
return browsers.some((browser) => {
|
|
const available = String(browser?.family || browser?.browser_family || "")
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/-(?:a0|cdp)$/, "");
|
|
return available === family || (family === "edge" && available === "edge-dev");
|
|
});
|
|
});
|
|
},
|
|
|
|
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 "Host connected - browser will open on first use";
|
|
if (connectors.length) return "Host connected - host browser unavailable";
|
|
return "Connect through Launcher or A0 CLI to use a host browser";
|
|
},
|
|
|
|
browserRuntimeStatusLabel() {
|
|
if (this.config?.runtime_backend !== "host_required") {
|
|
return "Docker browser runs inside Agent Zero; host-browser connection status does not affect it.";
|
|
}
|
|
const label = this.hostBrowserConnectorLabel();
|
|
if (label.startsWith("Connect through Launcher") || label.includes("unavailable")) {
|
|
return `${label}. Switch Browser location to Internal Docker browser to browse without a host connection.`;
|
|
}
|
|
return label;
|
|
},
|
|
|
|
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;
|
|
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 = [];
|
|
notificationStore.addFrontendToastOnly("error", error instanceof Error ? error.message : String(error));
|
|
} finally {
|
|
this.extensionsLoading = false;
|
|
}
|
|
},
|
|
|
|
applyExtensionPayload(response = {}) {
|
|
this.extensionsRoot = response.root || EXTENSIONS_ROOT;
|
|
this.extensionsList = Array.isArray(response.extensions) ? response.extensions : [];
|
|
},
|
|
|
|
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;
|
|
},
|
|
|
|
createExtensionWithAgent(context) {
|
|
this.prefillExtensionPrompt(context,
|
|
[
|
|
"Use the browser-extension-control skill to create a new Chrome extension for Agent Zero's Browser.",
|
|
"Start by asking me for the extension name, purpose, target websites, and required permissions.",
|
|
`Create it under ${this.extensionsRoot || EXTENSIONS_ROOT}/<extension-slug> and keep permissions minimal.`,
|
|
].join("\n")
|
|
);
|
|
},
|
|
|
|
askAgentInstallExtension(context) {
|
|
const url = String(this.extensionInstallUrl || "").trim();
|
|
const prompt = url
|
|
? [
|
|
"Use the browser-extension-control skill to review and optionally install this Chrome Web Store extension for Agent Zero's Browser.",
|
|
`Chrome Web Store URL or id: ${url}`,
|
|
"Explain the permissions and any sandbox risk before enabling it.",
|
|
].join("\n")
|
|
: [
|
|
"Use the browser-extension-control skill to help me install and review a Chrome Web Store extension for Agent Zero's Browser.",
|
|
"Ask me for the Chrome Web Store URL or extension id first.",
|
|
"Explain the permissions and any sandbox risk before enabling it.",
|
|
].join("\n");
|
|
this.prefillExtensionPrompt(context, prompt);
|
|
},
|
|
|
|
async installExtensionFromUrl() {
|
|
if (this.extensionActionLoading) return;
|
|
const url = String(this.extensionInstallUrl || "").trim();
|
|
if (!url) {
|
|
notificationStore.addFrontendToastOnly("info", "Paste a Chrome Web Store URL or extension id first.");
|
|
return;
|
|
}
|
|
|
|
this.extensionActionLoading = true;
|
|
notificationStore.addFrontendToastOnly("info", "Installing extension… Large packages may take a few minutes.");
|
|
try {
|
|
const response = await callJsonApi("/plugins/_browser/extensions", {
|
|
action: "install_web_store",
|
|
url,
|
|
});
|
|
if (!response?.ok) {
|
|
throw new Error(response?.error || "Install failed.");
|
|
}
|
|
this.applyExtensionPayload(response);
|
|
this.setExtensionEnabled(response.path, true);
|
|
this.extensionInstallUrl = "";
|
|
notificationStore.addFrontendToastOnly("success", `Installed ${response.name || response.id}.`);
|
|
} catch (error) {
|
|
notificationStore.addFrontendToastOnly("error", error instanceof Error ? error.message : String(error));
|
|
} finally {
|
|
this.extensionActionLoading = false;
|
|
}
|
|
},
|
|
|
|
async prefillExtensionPrompt(context, prompt) {
|
|
if (context?.confirmDiscardUnsavedChanges && !context.confirmDiscardUnsavedChanges()) return;
|
|
const { store: chat } = await import("/components/chat/input/input-store.js");
|
|
await globalThis.closeModal?.();
|
|
chat.message = prompt;
|
|
chat.adjustTextareaHeight?.();
|
|
chat.focus?.();
|
|
},
|
|
|
|
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;
|
|
if (!this.extensionCanDelete(extension)) {
|
|
notificationStore.addFrontendToastOnly("error", "Only Browser-managed extensions can be deleted.");
|
|
return;
|
|
}
|
|
const name = String(extension?.name || "this extension").trim();
|
|
const safeName = name.replace(/[&<>"']/g, (character) => `&#${character.charCodeAt(0)};`);
|
|
const confirmed = await showConfirmDialog({
|
|
title: "Delete extension",
|
|
message: `Delete ${safeName}? This removes the extension folder from Browser.`,
|
|
confirmText: "Delete",
|
|
type: "danger",
|
|
});
|
|
if (!confirmed) 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.setExtensionEnabled(path, false);
|
|
notificationStore.addFrontendToastOnly("success", `Deleted ${response.name || name}.`);
|
|
} catch (error) {
|
|
notificationStore.addFrontendToastOnly("error", error instanceof Error ? error.message : String(error));
|
|
} finally {
|
|
this.extensionDeleteLoadingPath = "";
|
|
}
|
|
},
|
|
|
|
extensionVersionLabel(extension) {
|
|
const version = String(extension?.version || "").trim();
|
|
return version ? `v${version}` : "Unpacked extension";
|
|
},
|
|
});
|