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.