agent-zero/plugins/_browser/webui/browser-config-store.js
Alessandro 9fc3ff20a4 Add browser extension uninstall controls
Expose extension deletion from the Browser internal settings page and keep the compact Browser dropdown focused on quick enable/install actions.\n\nAdd a guarded uninstall API that only deletes Browser-managed extension folders, updates enabled extension paths, refreshes the settings UI, and covers managed versus external paths with regression tests.
2026-05-02 17:05:33 +02:00

188 lines
5.8 KiB
JavaScript

import { createStore } from "/js/AlpineStore.js";
import { callJsonApi } from "/js/api.js";
const BROWSER_EXTENSIONS_API = "/plugins/_browser/extensions";
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.model_preset = String(config.model_preset || "").trim();
delete config.model;
return config;
}
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;
}
export const store = createStore("browserConfig", {
config: null,
extensionsList: [],
extensionsLoading: false,
extensionsError: "",
extensionsMessage: "",
extensionDeleteLoadingPath: "",
async init(config) {
this.bindConfig(config);
await this.loadExtensionsList();
},
cleanup() {
this.config = null;
this.extensionsList = [];
this.extensionsError = "";
this.extensionsMessage = "";
this.extensionDeleteLoadingPath = "";
},
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";
},
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";
},
});