Add Browser extension UI open action

Detect openable Chrome extension UI pages from manifests and expose resolved chrome-extension URLs to the Browser UI.

Render an Open button in the compact Browser extension dropdown and cover manifest UI metadata with regression tests.
This commit is contained in:
Alessandro 2026-05-02 20:02:28 +02:00
parent 92ae20da2c
commit c553e91c03
4 changed files with 250 additions and 7 deletions

View file

@ -1,5 +1,7 @@
from __future__ import annotations
import base64
import hashlib
import json
import os
import re
@ -22,6 +24,7 @@ WEB_STORE_ID_RE = re.compile(r"(?<![a-p])([a-p]{32})(?![a-p])")
CHROME_VERSION_RE = re.compile(r"(\d+(?:\.\d+){0,3})")
CHROME_I18N_MESSAGE_RE = re.compile(r"__MSG_([A-Za-z0-9_@.-]+)__")
DEFAULT_CHROME_PRODVERSION = "140.0.0.0"
EXTENSION_ID_ALPHABET = "abcdefghijklmnop"
CHROME_VERSION_COMMANDS = (
("google-chrome", "--version"),
("chromium", "--version"),
@ -301,12 +304,17 @@ def _extension_entry(extension_dir: Path, enabled_paths: set[str]) -> dict[str,
manifest = _read_manifest(extension_dir)
extension_path = str(extension_dir)
can_delete = _is_managed_extension_dir(extension_dir)
extension_id = _extension_runtime_id(extension_dir, manifest)
source_id = _extension_source_id(extension_dir)
ui = _extension_ui(extension_id, manifest)
name = (
_manifest_label(extension_dir, manifest, "name")
or _manifest_label(extension_dir, manifest, "short_name")
or extension_dir.name
)
return {
"id": extension_id,
"source_id": source_id,
"name": name,
"raw_name": manifest.get("name") or "",
"description": _manifest_label(extension_dir, manifest, "description"),
@ -315,6 +323,10 @@ def _extension_entry(extension_dir: Path, enabled_paths: set[str]) -> dict[str,
"enabled": extension_path in enabled_paths,
"managed": can_delete,
"can_delete": can_delete,
"has_ui": bool(ui["open_url"]),
"open_url": ui["open_url"],
"open_label": ui["open_label"],
"ui": ui,
}
@ -335,6 +347,118 @@ def _read_manifest(extension_path: Path) -> dict[str, Any]:
return {}
def _extension_runtime_id(extension_dir: Path, manifest: dict[str, Any]) -> str:
key_id = _extension_id_from_manifest_key(str(manifest.get("key") or ""))
if key_id:
return key_id
return _extension_id_from_path(extension_dir)
def _extension_source_id(extension_dir: Path) -> str:
name = extension_dir.name
if not EXTENSION_ID_RE.fullmatch(name):
return ""
try:
if extension_dir.parent.name == "chrome-web-store":
return name
except OSError:
return ""
return ""
def _extension_id_from_manifest_key(key: str) -> str:
raw_key = str(key or "").strip()
if not raw_key:
return ""
try:
padding = "=" * (-len(raw_key) % 4)
public_key = base64.b64decode(raw_key + padding, validate=True)
except Exception:
return ""
return _extension_id_from_hash_input(public_key)
def _extension_id_from_path(extension_dir: Path) -> str:
return _extension_id_from_hash_input(str(extension_dir).encode("utf-8"))
def _extension_id_from_hash_input(value: bytes) -> str:
digest = hashlib.sha256(value).hexdigest()[:32]
return "".join(EXTENSION_ID_ALPHABET[int(char, 16)] for char in digest)
def _extension_ui(extension_id: str, manifest: dict[str, Any]) -> dict[str, Any]:
targets = _extension_ui_targets(extension_id, manifest)
primary = targets[0] if targets else {}
return {
"open_url": primary.get("url", ""),
"open_label": primary.get("label", ""),
"targets": targets,
}
def _extension_ui_targets(extension_id: str, manifest: dict[str, Any]) -> list[dict[str, str]]:
candidates: list[tuple[str, str, Any]] = [
("options", "Options", _manifest_nested_value(manifest, "options_ui", "page")),
("options", "Options", manifest.get("options_page")),
("popup", "Popup", _manifest_nested_value(manifest, "action", "default_popup")),
("popup", "Popup", _manifest_nested_value(manifest, "browser_action", "default_popup")),
("popup", "Popup", _manifest_nested_value(manifest, "page_action", "default_popup")),
("side_panel", "Side panel", _manifest_nested_value(manifest, "side_panel", "default_path")),
("devtools", "DevTools", manifest.get("devtools_page")),
]
chrome_url_overrides = manifest.get("chrome_url_overrides")
if isinstance(chrome_url_overrides, dict):
candidates.extend(
(
(f"chrome_url_override_{name}", _extension_override_label(name), page)
for name, page in chrome_url_overrides.items()
)
)
targets: list[dict[str, str]] = []
seen_urls: set[str] = set()
for kind, label, page in candidates:
url = _extension_page_url(extension_id, page)
if not url or url in seen_urls:
continue
seen_urls.add(url)
targets.append(
{
"kind": kind,
"label": label,
"page": str(page or "").strip(),
"url": url,
}
)
return targets
def _manifest_nested_value(manifest: dict[str, Any], key: str, nested_key: str) -> Any:
value = manifest.get(key)
if not isinstance(value, dict):
return ""
return value.get(nested_key)
def _extension_override_label(name: str) -> str:
normalized = str(name or "").replace("_", " ").strip()
return normalized[:1].upper() + normalized[1:] if normalized else "Extension page"
def _extension_page_url(extension_id: str, page: Any) -> str:
page_path = str(page or "").strip()
if not extension_id or not page_path:
return ""
if re.match(r"^[a-z][a-z0-9+.-]*:", page_path, flags=re.IGNORECASE):
return ""
page_path = page_path.lstrip("/")
if not page_path:
return ""
return f"chrome-extension://{extension_id}/{page_path}"
def _manifest_label(extension_dir: Path, manifest: dict[str, Any], key: str) -> str:
value = str(manifest.get(key) or "").strip()
if not value:

View file

@ -117,19 +117,30 @@
x-show="$store.browserPage.extensionsListLoading">progress_activity</span>
</div>
<template x-for="extension in $store.browserPage.extensionsList" :key="extension.path">
<label class="browser-extension-row" :title="extension.path">
<div class="browser-extension-row" :title="extension.path">
<span class="browser-extension-row-text">
<span class="browser-extension-name" x-text="extension.name || 'Unnamed extension'"></span>
<span class="browser-extension-meta"
x-text="$store.browserPage.extensionVersionLabel(extension)"></span>
</span>
<span class="browser-extension-toggle">
<input type="checkbox" :checked="extension.enabled"
:disabled="$store.browserPage.extensionToggleLoadingPath === extension.path"
@change="$store.browserPage.setExtensionEnabled(extension, $event.target.checked, $event.target)" />
<span class="browser-extension-switch"></span>
<span class="browser-extension-row-actions">
<button type="button" class="browser-extension-open"
x-show="$store.browserPage.extensionHasOpenUi(extension)"
:title="$store.browserPage.extensionOpenTitle(extension)"
:aria-label="$store.browserPage.extensionOpenTitle(extension)"
:disabled="!extension.enabled || $store.browserPage.isBusy()"
@click.stop="$store.browserPage.openExtensionUi(extension)">
<span class="material-symbols-outlined">open_in_new</span>
<span>Open</span>
</button>
<label class="browser-extension-toggle">
<input type="checkbox" :checked="extension.enabled"
:disabled="$store.browserPage.extensionToggleLoadingPath === extension.path"
@change="$store.browserPage.setExtensionEnabled(extension, $event.target.checked, $event.target)" />
<span class="browser-extension-switch"></span>
</label>
</span>
</label>
</div>
</template>
</div>
<button type="button" class="dropdown-item" @click="$store.browserPage.openExtensionsSettings()">
@ -849,6 +860,44 @@
color: var(--color-text);
}
.browser-extension-row-actions {
display: inline-flex;
align-items: center;
gap: 7px;
}
.browser-extension-open {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
min-width: 0;
min-height: 28px;
padding: 0 8px;
border: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent);
border-radius: 6px;
background: color-mix(in srgb, var(--color-panel) 70%, transparent);
color: var(--color-text);
font: inherit;
font-size: 0.76rem;
font-weight: 650;
cursor: pointer;
}
.browser-extension-open:hover:not(:disabled) {
border-color: color-mix(in srgb, var(--color-primary) 42%, var(--color-border));
background: color-mix(in srgb, var(--color-primary) 13%, var(--color-background));
}
.browser-extension-open:disabled {
cursor: not-allowed;
opacity: 0.52;
}
.browser-extension-open .material-symbols-outlined {
font-size: 15px;
}
.browser-extension-toggle {
position: relative;
display: inline-flex;

View file

@ -526,6 +526,36 @@ const model = {
return version ? `v${version}` : "Unpacked extension";
},
extensionOpenUrl(extension) {
return String(extension?.open_url || extension?.ui?.open_url || "").trim();
},
extensionHasOpenUi(extension) {
return Boolean(this.extensionOpenUrl(extension));
},
extensionOpenTitle(extension) {
const label = String(extension?.open_label || extension?.ui?.open_label || "Extension UI").trim();
const name = String(extension?.name || "extension").trim();
if (!extension?.enabled) {
return `Enable ${name} before opening ${label}.`;
}
return `Open ${label} for ${name}`;
},
async openExtensionUi(extension) {
const url = this.extensionOpenUrl(extension);
if (!url) return;
this.extensionActionMessage = "";
this.extensionActionError = "";
if (!extension?.enabled) {
this.extensionActionError = `Enable ${extension?.name || "this extension"} before opening it.`;
return;
}
this.closeExtensionsMenu();
await this.command("open", { url });
},
_prefillAgentPrompt(prompt) {
chatInputStore.message = prompt;
chatInputStore.adjustTextareaHeight?.();

View file

@ -344,6 +344,43 @@ def test_browser_extension_manager_uninstalls_only_managed_extensions(monkeypatc
assert external_extension.exists()
def test_browser_extension_manager_exposes_openable_manifest_ui(monkeypatch, tmp_path):
monkeypatch.setattr(
browser_extension_manager_module.files,
"get_abs_path",
lambda *parts: str(tmp_path.joinpath(*parts)),
)
extension_dir = get_extensions_root() / "local-options"
extension_dir.mkdir(parents=True)
(extension_dir / "manifest.json").write_text(
json.dumps(
{
"manifest_version": 3,
"name": "Openable Extension",
"version": "1.0.0",
"options_ui": {"page": "options/index.html"},
"action": {"default_popup": "popup.html"},
}
),
encoding="utf-8",
)
monkeypatch.setattr(
browser_extension_manager_module,
"get_browser_config",
lambda: {"extension_paths": [str(extension_dir)]},
)
entry = browser_extension_manager_module.list_browser_extensions()[0]
expected_id = browser_extension_manager_module._extension_id_from_path(extension_dir)
assert entry["id"] == expected_id
assert entry["has_ui"] is True
assert entry["open_label"] == "Options"
assert entry["open_url"] == f"chrome-extension://{expected_id}/options/index.html"
assert entry["ui"]["targets"][1]["url"] == f"chrome-extension://{expected_id}/popup.html"
def test_browser_extension_manager_parses_web_store_urls():
extension_id = "a" * 32
@ -402,6 +439,9 @@ def test_browser_extension_menu_exposes_agent_and_url_paths():
assert "No extensions installed yet." not in html
assert "Browser Extension Settings" not in html
assert "<span>Settings</span>" in html
assert "extensionHasOpenUi(extension)" in html
assert "openExtensionUi(extension)" in html
assert "<span>Open</span>" in html
assert "hasExtensionInstallUrl()" in html
assert "malicious or buggy extensions" in html
assert skill.exists()