From 022b6f031f890fb0b43bc4552adb15db34406fdb Mon Sep 17 00:00:00 2001 From: Alessandro <155005371+3clyp50@users.noreply.github.com> Date: Thu, 7 May 2026 00:14:31 +0200 Subject: [PATCH] Split live surfaces out of modals Introduce the shared surfaces frontend service and stylesheet so Browser and Desktop can register docked or floating live UI without special cases in modals.js. Update Browser and right-canvas integration to preserve active viewers across canvas/modal switches and avoid creating blank tabs unless explicitly requested. --- plugins/_browser/api/ws_browser.py | 29 +- plugins/_browser/default_config.yaml | 2 +- .../browser-button.html | 2 +- .../browser-tool-handler.js | 39 +- .../auto-open-browser-results.js | 13 +- .../surfaces_register/register-browser.js | 104 ++++ plugins/_browser/helpers/extension_manager.py | 6 +- .../prompts/agent.system.tool.browser.md | 6 +- plugins/_browser/webui/browser-panel.html | 11 + plugins/_browser/webui/browser-store.js | 225 +++++++-- plugins/_browser/webui/config.html | 4 +- plugins/_browser/webui/main.html | 4 + webui/components/canvas/right-canvas-store.js | 104 ++-- webui/components/canvas/right-canvas.css | 7 + webui/css/modals.css | 66 --- webui/css/surfaces.css | 83 ++++ webui/index.html | 1 + webui/js/initFw.js | 3 +- webui/js/modals.js | 276 +++-------- webui/js/surfaces.js | 445 ++++++++++++++++++ 20 files changed, 1002 insertions(+), 428 deletions(-) create mode 100644 plugins/_browser/extensions/webui/surfaces_register/register-browser.js create mode 100644 webui/css/surfaces.css create mode 100644 webui/js/surfaces.js diff --git a/plugins/_browser/api/ws_browser.py b/plugins/_browser/api/ws_browser.py index 9f4f49acc..039dc7ebe 100644 --- a/plugins/_browser/api/ws_browser.py +++ b/plugins/_browser/api/ws_browser.py @@ -62,10 +62,14 @@ class WsBrowser(WsHandler): if not AgentContext.get(context_id): return self._error("CONTEXT_NOT_FOUND", f"Context '{context_id}' was not found", data) - runtime = await get_runtime(context_id) - listing = await runtime.call("list") - browsers = listing.get("browsers") or [] - if not browsers: + create_browser = self._bool(data.get("create_browser", data.get("createBrowser"))) + runtime = await get_runtime(context_id, create=create_browser) + listing = {"browsers": [], "last_interacted_browser_id": None} + browsers: list[dict[str, Any]] = [] + if runtime: + listing = await runtime.call("list") + browsers = listing.get("browsers") or [] + if runtime and not browsers and create_browser: opened = await runtime.call("open", "") listing = await runtime.call("list") browsers = listing.get("browsers") or [] @@ -73,7 +77,7 @@ class WsBrowser(WsHandler): listing["last_interacted_browser_id"] = opened.get("id") active_id = self._active_browser_id(listing, data.get("browser_id")) initial_viewport = self._viewport_from_data(data) - if active_id and initial_viewport: + if runtime and active_id and initial_viewport: await runtime.call( "set_viewport", active_id, @@ -88,9 +92,10 @@ class WsBrowser(WsHandler): if existing: existing.cancel() viewer_id = str(data.get("viewer_id") or "") - self._streams[stream_key] = asyncio.create_task( - self._stream_frames(sid, context_id, active_id, viewer_id) - ) + if runtime: + self._streams[stream_key] = asyncio.create_task( + self._stream_frames(sid, context_id, active_id, viewer_id) + ) return { "context_id": context_id, @@ -498,6 +503,14 @@ class WsBrowser(WsHandler): def _context_id(data: dict[str, Any]) -> str: return str(data.get("context_id") or data.get("context") or "").strip() + @staticmethod + def _bool(value: Any) -> bool: + if isinstance(value, bool): + return value + if isinstance(value, (int, float)): + return bool(value) + return str(value or "").strip().lower() in {"1", "true", "yes", "on"} + @staticmethod def _error(code: str, message: str, data: dict[str, Any]) -> WsResult: return WsResult.error( diff --git a/plugins/_browser/default_config.yaml b/plugins/_browser/default_config.yaml index b78e07845..9972d8908 100644 --- a/plugins/_browser/default_config.yaml +++ b/plugins/_browser/default_config.yaml @@ -5,7 +5,7 @@ extension_paths: [] # Page opened by new Browser sessions when no URL is provided. default_homepage: "about:blank" -# When the Browser canvas is already open, keep it synced to agent Browser tool results. +# When the Browser surface is already open, keep it synced to agent Browser tool results. autofocus_active_page: true # Optional _model_config preset used by Browser-owned model helpers. diff --git a/plugins/_browser/extensions/webui/chat-input-bottom-actions-start/browser-button.html b/plugins/_browser/extensions/webui/chat-input-bottom-actions-start/browser-button.html index fbcb6491b..61935624d 100644 --- a/plugins/_browser/extensions/webui/chat-input-bottom-actions-start/browser-button.html +++ b/plugins/_browser/extensions/webui/chat-input-bottom-actions-start/browser-button.html @@ -5,7 +5,7 @@ aria-label="Open Browser" data-bs-placement="top" data-bs-trigger="hover" - @click="$store.rightCanvas ? $store.rightCanvas.open('browser') : (window.ensureModalOpen ? window.ensureModalOpen('/plugins/_browser/webui/main.html') : (window.openModal && window.openModal('/plugins/_browser/webui/main.html')))" + @click="import('/js/surfaces.js').then(({ open }) => open('browser'))" > '; + const updateFocusButton = (active) => { + const label = active ? "Restore size" : "Focus mode"; + focusButton.setAttribute("aria-label", label); + focusButton.setAttribute("title", label); + focusButton.querySelector(".material-symbols-outlined").textContent = active ? "fullscreen_exit" : "fullscreen"; + }; + const setFocusMode = (enabled) => { + if (enabled) { + beforeFocusBounds = currentBounds(); + inner.classList.add("is-focus-mode"); + setBounds(focusBounds()); + updateFocusButton(true); + return; + } + inner.classList.remove("is-focus-mode"); + setBounds(beforeFocusBounds || currentBounds()); + beforeFocusBounds = null; + updateFocusButton(false); + }; + updateFocusButton(false); + const closeButton = inner.querySelector(".modal-close"); + if (closeButton) { + closeButton.insertAdjacentElement("beforebegin", focusButton); + } else { + header.appendChild(focusButton); + } + const onFocusClick = () => setFocusMode(!inner.classList.contains("is-focus-mode")); + focusButton.addEventListener("click", onFocusClick); + globalThis.addEventListener("resize", clampGeometry); if (globalThis.ResizeObserver) { resizeObserver = new ResizeObserver(clampGeometry); @@ -2537,6 +2697,7 @@ const model = { const onPointerDown = (event) => { if (event.button !== 0) return; if (event.target?.closest?.("button, input, select, textarea, a")) return; + if (inner.classList.contains("is-focus-mode")) return; const current = inner.getBoundingClientRect(); drag = { x: event.clientX, @@ -2553,6 +2714,8 @@ const model = { header.addEventListener("pointerdown", onPointerDown); this._floatingCleanup = () => { + focusButton.removeEventListener("click", onFocusClick); + focusButton.remove(); header.removeEventListener("pointerdown", onPointerDown); globalThis.removeEventListener("pointermove", onPointerMove); globalThis.removeEventListener("pointerup", onPointerUp); @@ -2560,6 +2723,7 @@ const model = { resizeObserver?.disconnect?.(); this._stageResizeObserver?.disconnect?.(); this._stageResizeObserver = null; + inner.classList.remove("is-focus-mode"); }; }, @@ -2601,3 +2765,10 @@ const model = { }; export const store = createStore("browserPage", model); + +registerUrlHandler(async (intent = {}) => { + const url = String(intent.url || "").trim(); + const payload = { url, source: intent.source || "surface-url-intent" }; + await openLatestSurface("browser", payload); + return await store.openUrlIntent(url, { source: payload.source }); +}); diff --git a/plugins/_browser/webui/config.html b/plugins/_browser/webui/config.html index 072ebae73..9f52e105b 100644 --- a/plugins/_browser/webui/config.html +++ b/plugins/_browser/webui/config.html @@ -18,7 +18,7 @@
Browsing
- Set how new Browser sessions start and how an already-open Browser canvas follows agent activity. + Set how new Browser sessions start and how an already-open Browser surface follows agent activity.