Bridge desktop URLs into Browser
Register the Agent Zero Browser as the Desktop URL handler, queue URL intents from the Xfce environment, and route them into Browser on the opposite canvas/modal surface. Also make floating Browser and Desktop modals pass outside clicks through while preserving interaction inside the modal window.
Alessandro committed
May 3, 2026 at 00:57 UTC
677a0c1e64aafd0566adb9f3b1d25a6d4f2e0c0f
4 files changed
+413
-14
plugins/_browser/webui/browser-store.js
+21
@@ -1242,6 +1242,27 @@ const model = {
1242
}
1243
},
1244
1245
+ async openUrlIntent(url = "", options = {}) {
1246
+ if (this._openPromise) {
1247
+ try {
1248
+ await this._openPromise;
1249
+ } catch {}
1250
+ }
1251
+ if (!this._surfaceMounted) return false;
1252
+ const targetUrl = String(url || "").trim();
1253
+ if (targetUrl) {
1254
+ await this.command("open", {
1255
+ url: targetUrl,
1256
+ source: options?.source || "desktop-url",
1257
+ });
1258
+ return true;
1259
+ }
1260
+ if (!this.activeBrowserId) {
1261
+ await this.command("open");
1262
+ }
1263
+ return true;
1264
+ },
1265
+
1266
onAddressFocus() {
1267
this.addressFocused = true;
1268
},
plugins/_office/helpers/libreoffice_desktop.py
+245
-14
@@ -1,6 +1,7 @@
1
from __future__ import annotations
2
3
import atexit
4
+import fcntl
5
import hashlib
6
import json
7
import os
@@ -57,6 +58,9 @@ DESKTOP_FOLDER_LINKS = (
58
("Agents", ("usr", "agents")),
59
("Downloads", ("usr", "downloads")),
60
)
61
+URL_INTENT_MAX_ITEMS = 50
62
+URL_INTENT_MAX_LENGTH = 8192
63
+URL_HANDLER_DESKTOP_ID = "agent-zero-browser.desktop"
64
OOR_NS = "http://openoffice.org/2001/registry"
65
XS_NS = "http://www.w3.org/2001/XMLSchema"
66
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
@@ -186,11 +190,34 @@ class LibreOfficeDesktopManager:
190
session = self.get(session_id) if session_id else self._find_by_file_id(file_id)
191
if not session:
192
return {"ok": False, "error": "LibreOffice desktop session not found."}
193
+ if not _url_bridge_script_path(session).exists():
194
+ try:
195
+ self._prepare_desktop_url_bridge(session)
196
+ self._refresh_xfce_desktop(session)
197
+ except Exception:
198
+ pass
199
+ url_intents = self.claim_url_intents(session.session_id)
200
doc = self._document_for_save(session, file_id)
201
if not doc:
191
- return {"ok": True, "session_id": session.session_id, "desktop": session.public()}
202
+ return {
203
+ "ok": True,
204
+ "session_id": session.session_id,
205
+ "desktop": session.public(),
206
+ "url_intents": url_intents,
207
+ }
208
updated = document_store.register_document(doc["path"])
193
- return {"ok": True, "session_id": session.session_id, "document": _public_doc(updated)}
209
+ return {
210
+ "ok": True,
211
+ "session_id": session.session_id,
212
+ "document": _public_doc(updated),
213
+ "url_intents": url_intents,
214
+ }
215
+
216
+ def claim_url_intents(self, session_id: str = SYSTEM_SESSION_ID) -> list[dict[str, Any]]:
217
+ session = self.get(session_id) or self.get(SYSTEM_SESSION_ID)
218
+ if not session:
219
+ return []
220
+ return _claim_url_intents(session)
221
222
def retarget_document(self, file_id: str, doc: dict[str, Any]) -> dict[str, Any]:
223
session = self._find_by_file_id(file_id)
@@ -329,6 +356,8 @@ class LibreOfficeDesktopManager:
356
def _ensure_system_desktop_locked(self) -> DesktopSession:
357
existing = self._sessions.get(SYSTEM_SESSION_ID)
358
if existing and existing.alive():
359
+ self._prepare_desktop_url_bridge(existing)
360
+ self._refresh_xfce_desktop(existing)
361
return existing
362
363
status = collect_desktop_status()
@@ -523,20 +552,9 @@ class LibreOfficeDesktopManager:
552
encoding="utf-8",
553
)
554
_write_thunar_defaults(xfce_conf_dir / "thunar.xml")
526
- helpers_rc = config_dir / "xfce4" / "helpers.rc"
527
- helpers_rc.parent.mkdir(parents=True, exist_ok=True)
528
- helpers_rc.write_text(
529
- "\n".join(
530
- [
531
- "TerminalEmulator=xfce4-terminal",
532
- "FileManager=thunar",
533
- "",
534
- ],
535
- ),
536
- encoding="utf-8",
537
- )
555
self._hide_xpra_desktop_entries(applications_dir)
556
self._hide_xfce_menu_entries(applications_dir)
557
+ self._prepare_desktop_url_bridge(session)
558
559
base_args = (
560
soffice,
@@ -608,6 +626,55 @@ class LibreOfficeDesktopManager:
626
self._prepare_xfce_panel_config(session)
627
self._prepare_xfce_profile_autostart(session)
628
629
+ def _prepare_desktop_url_bridge(self, session: DesktopSession) -> None:
630
+ desktop_dir = session.profile_dir / "Desktop"
631
+ config_dir = session.profile_dir / ".config"
632
+ data_dir = session.profile_dir / ".local" / "share"
633
+ applications_dir = data_dir / "applications"
634
+ desktop_dir.mkdir(parents=True, exist_ok=True)
635
+ config_dir.mkdir(parents=True, exist_ok=True)
636
+ applications_dir.mkdir(parents=True, exist_ok=True)
637
+
638
+ browser_bridge = _write_url_bridge_script(session)
639
+ helpers_rc = config_dir / "xfce4" / "helpers.rc"
640
+ helpers_rc.parent.mkdir(parents=True, exist_ok=True)
641
+ helpers_rc.write_text(
642
+ "\n".join(
643
+ [
644
+ "TerminalEmulator=xfce4-terminal",
645
+ "FileManager=thunar",
646
+ "WebBrowser=agent-zero-browser",
647
+ "",
648
+ ],
649
+ ),
650
+ encoding="utf-8",
651
+ )
652
+ _write_xfce_browser_helper(
653
+ config_dir / "xfce4" / "helpers" / "agent-zero-browser.desktop",
654
+ browser_bridge,
655
+ )
656
+ _write_mimeapps_defaults(config_dir / "mimeapps.list", URL_HANDLER_DESKTOP_ID)
657
+ _write_mimeapps_defaults(data_dir / "applications" / "mimeapps.list", URL_HANDLER_DESKTOP_ID)
658
+ _write_desktop_launcher(
659
+ applications_dir / URL_HANDLER_DESKTOP_ID,
660
+ name="Agent Zero Browser",
661
+ exec_line=_desktop_exec(browser_bridge, "%U"),
662
+ icon="web-browser",
663
+ categories="Network;WebBrowser;",
664
+ try_exec=str(browser_bridge),
665
+ mime_types=_url_handler_mime_types(),
666
+ no_display=True,
667
+ )
668
+ _write_desktop_launcher(
669
+ desktop_dir / "Browser.desktop",
670
+ name="Browser",
671
+ exec_line=_desktop_exec(browser_bridge),
672
+ icon="web-browser",
673
+ categories="Network;WebBrowser;",
674
+ try_exec=str(browser_bridge),
675
+ )
676
+ self._trust_desktop_launchers(session, desktop_dir)
677
+
678
def _hide_xpra_desktop_entries(self, applications_dir: Path) -> None:
679
for filename in HIDDEN_XPRA_DESKTOP_ENTRIES:
680
_write_hidden_application_entry(applications_dir / filename, "Xpra")
@@ -908,6 +975,9 @@ fi
975
"HOME": str(session.profile_dir),
976
"LANG": os.environ.get("LANG") or "C.UTF-8",
977
}
978
+ browser_bridge = _url_bridge_script_path(session)
979
+ if browser_bridge.exists():
980
+ env["BROWSER"] = str(browser_bridge)
981
env.setdefault("XDG_RUNTIME_DIR", str(STATE_DIR / "xdg-runtime"))
982
runtime_dir = Path(env["XDG_RUNTIME_DIR"])
983
runtime_dir.mkdir(parents=True, exist_ok=True)
@@ -1187,6 +1257,161 @@ def _ensure_desktop_folder_link(desktop_dir: Path, label: str, target: Path) ->
1257
return
1258
1259
1260
+def _url_bridge_dir(session: DesktopSession) -> Path:
1261
+ return session.profile_dir / ".agent-zero"
1262
+
1263
+
1264
+def _url_bridge_script_path(session: DesktopSession) -> Path:
1265
+ return _url_bridge_dir(session) / "open-url"
1266
+
1267
+
1268
+def _url_bridge_queue_path(session: DesktopSession) -> Path:
1269
+ return _url_bridge_dir(session) / "browser-url-intents.jsonl"
1270
+
1271
+
1272
+def _url_bridge_lock_path(session: DesktopSession) -> Path:
1273
+ return _url_bridge_dir(session) / "browser-url-intents.lock"
1274
+
1275
+
1276
+def _write_url_bridge_script(session: DesktopSession) -> Path:
1277
+ bridge_dir = _url_bridge_dir(session)
1278
+ bridge_dir.mkdir(parents=True, exist_ok=True)
1279
+ script = _url_bridge_script_path(session)
1280
+ queue = _url_bridge_queue_path(session)
1281
+ lock = _url_bridge_lock_path(session)
1282
+ script.write_text(
1283
+ f"""#!/usr/bin/env python3
1284
+import fcntl
1285
+import json
1286
+import os
1287
+import sys
1288
+import time
1289
+
1290
+QUEUE_PATH = {str(queue)!r}
1291
+LOCK_PATH = {str(lock)!r}
1292
+MAX_URL_LENGTH = {URL_INTENT_MAX_LENGTH}
1293
+
1294
+
1295
+def main():
1296
+ urls = [str(arg or "").strip()[:MAX_URL_LENGTH] for arg in sys.argv[1:] if str(arg or "").strip()]
1297
+ if not urls:
1298
+ urls = [""]
1299
+ os.makedirs(os.path.dirname(QUEUE_PATH), exist_ok=True)
1300
+ with open(LOCK_PATH, "a+", encoding="utf-8") as lock_file:
1301
+ fcntl.flock(lock_file, fcntl.LOCK_EX)
1302
+ with open(QUEUE_PATH, "a", encoding="utf-8") as queue_file:
1303
+ for url in urls:
1304
+ queue_file.write(json.dumps({{
1305
+ "url": url,
1306
+ "created_at": time.time(),
1307
+ "source": "desktop",
1308
+ }}, ensure_ascii=True) + "\\n")
1309
+ queue_file.flush()
1310
+ os.fsync(queue_file.fileno())
1311
+ fcntl.flock(lock_file, fcntl.LOCK_UN)
1312
+
1313
+
1314
+if __name__ == "__main__":
1315
+ main()
1316
+""",
1317
+ encoding="utf-8",
1318
+ )
1319
+ try:
1320
+ script.chmod(0o755)
1321
+ except OSError:
1322
+ pass
1323
+ return script
1324
+
1325
+
1326
+def _claim_url_intents(session: DesktopSession) -> list[dict[str, Any]]:
1327
+ queue = _url_bridge_queue_path(session)
1328
+ lock = _url_bridge_lock_path(session)
1329
+ if not queue.exists():
1330
+ return []
1331
+ lock.parent.mkdir(parents=True, exist_ok=True)
1332
+ try:
1333
+ with open(lock, "a+", encoding="utf-8") as lock_file:
1334
+ fcntl.flock(lock_file, fcntl.LOCK_EX)
1335
+ try:
1336
+ raw = queue.read_text(encoding="utf-8")
1337
+ queue.write_text("", encoding="utf-8")
1338
+ finally:
1339
+ fcntl.flock(lock_file, fcntl.LOCK_UN)
1340
+ except OSError:
1341
+ return []
1342
+
1343
+ intents: list[dict[str, Any]] = []
1344
+ for line in raw.splitlines():
1345
+ try:
1346
+ payload = json.loads(line)
1347
+ except json.JSONDecodeError:
1348
+ continue
1349
+ url = str(payload.get("url") or "").strip()
1350
+ if len(url) > URL_INTENT_MAX_LENGTH:
1351
+ url = url[:URL_INTENT_MAX_LENGTH]
1352
+ created_at = payload.get("created_at")
1353
+ try:
1354
+ created_at = float(created_at)
1355
+ except (TypeError, ValueError):
1356
+ created_at = time.time()
1357
+ intents.append(
1358
+ {
1359
+ "url": url,
1360
+ "created_at": created_at,
1361
+ "source": str(payload.get("source") or "desktop"),
1362
+ },
1363
+ )
1364
+ if len(intents) >= URL_INTENT_MAX_ITEMS:
1365
+ break
1366
+ return intents
1367
+
1368
+
1369
+def _url_handler_mime_types() -> tuple[str, ...]:
1370
+ return (
1371
+ "x-scheme-handler/http",
1372
+ "x-scheme-handler/https",
1373
+ "text/html",
1374
+ "application/xhtml+xml",
1375
+ )
1376
+
1377
+
1378
+def _write_mimeapps_defaults(path: Path, desktop_id: str) -> None:
1379
+ associations = ";".join([desktop_id, ""])
1380
+ lines = [
1381
+ "[Default Applications]",
1382
+ *(f"{mime_type}={desktop_id}" for mime_type in _url_handler_mime_types()),
1383
+ "",
1384
+ "[Added Associations]",
1385
+ *(f"{mime_type}={associations}" for mime_type in _url_handler_mime_types()),
1386
+ "",
1387
+ ]
1388
+ path.parent.mkdir(parents=True, exist_ok=True)
1389
+ path.write_text("\n".join(lines), encoding="utf-8")
1390
+
1391
+
1392
+def _write_xfce_browser_helper(path: Path, bridge_script: Path) -> None:
1393
+ command = _desktop_exec(bridge_script)
1394
+ command_with_parameter = _desktop_exec(bridge_script, "%s")
1395
+ path.parent.mkdir(parents=True, exist_ok=True)
1396
+ path.write_text(
1397
+ "\n".join(
1398
+ [
1399
+ "[Desktop Entry]",
1400
+ "NoDisplay=true",
1401
+ "Version=1.0",
1402
+ "Type=X-XFCE-Helper",
1403
+ "X-XFCE-Category=WebBrowser",
1404
+ f"X-XFCE-Commands={command}",
1405
+ f"X-XFCE-CommandsWithParameter={command_with_parameter}",
1406
+ "Icon=web-browser",
1407
+ "Name=Agent Zero Browser",
1408
+ "",
1409
+ ],
1410
+ ),
1411
+ encoding="utf-8",
1412
+ )
1413
+
1414
+
1415
def _remove_path_if_owned(path: Path) -> None:
1416
try:
1417
if path.is_symlink() or path.is_file():
@@ -1293,6 +1518,8 @@ def _write_desktop_launcher(
1518
categories: str,
1519
try_exec: str = "",
1520
working_dir: str | Path | None = None,
1521
+ mime_types: tuple[str, ...] = (),
1522
+ no_display: bool = False,
1523
) -> None:
1524
path.parent.mkdir(parents=True, exist_ok=True)
1525
lines = [
@@ -1306,6 +1533,10 @@ def _write_desktop_launcher(
1533
lines.append(f"TryExec={try_exec}")
1534
if working_dir:
1535
lines.append(f"Path={working_dir}")
1536
+ if mime_types:
1537
+ lines.append(f"MimeType={';'.join(mime_types)};")
1538
+ if no_display:
1539
+ lines.append("NoDisplay=true")
1540
lines.extend(
1541
[
1542
f"Icon={icon}",
plugins/_office/webui/office-store.js
+139
@@ -2,6 +2,8 @@ import { createStore } from "/js/AlpineStore.js";
2
import { callJsonApi } from "/js/api.js";
3
import { getNamespacedClient } from "/js/websocket.js";
4
import { store as fileBrowserStore } from "/components/modals/file-browser/file-browser-store.js";
5
+import { store as rightCanvasStore } from "/components/canvas/right-canvas-store.js";
6
+import { store as browserStore } from "/plugins/_browser/webui/browser-store.js";
7
8
const officeSocket = getNamespacedClient("/ws");
9
officeSocket.addHandlers(["ws_webui"]);
@@ -13,6 +15,9 @@ const DESKTOP_RESIZE_DELAY_MS = 80;
15
const XPRA_DESKTOP_PRIME_INTERVAL_MS = 220;
16
const XPRA_DESKTOP_PRIME_ATTEMPTS = 120;
17
const SYSTEM_DESKTOP_FILE_ID = "system-desktop";
18
+const BROWSER_MODAL_PATH = "/plugins/_browser/webui/main.html";
19
+const OFFICE_MODAL_PATH = "/plugins/_office/webui/main.html";
20
+const URL_INTENT_PANEL_TIMEOUT_MS = 5000;
21
const MAX_HISTORY = 80;
22
23
function currentContextId() {
@@ -59,6 +64,46 @@ function isEditableInputTarget(target) {
64
return !["button", "checkbox", "color", "file", "image", "radio", "range", "reset", "submit"].includes(type);
65
}
66
67
+function normalizeModalPath(path = "") {
68
+ return String(path || "").replace(/^\/+/, "");
69
+}
70
+
71
+function isModalPathOpen(path = "") {
72
+ const normalized = normalizeModalPath(path);
73
+ return Boolean(
74
+ globalThis.isModalOpen?.(path)
75
+ || globalThis.isModalOpen?.(`/${normalized}`)
76
+ || globalThis.isModalOpen?.(normalized)
77
+ );
78
+}
79
+
80
+function waitForElementByPredicate(predicate, timeoutMs = URL_INTENT_PANEL_TIMEOUT_MS) {
81
+ const found = predicate();
82
+ if (found) return Promise.resolve(found);
83
+ return new Promise((resolve) => {
84
+ const timeout = globalThis.setTimeout(() => {
85
+ observer.disconnect();
86
+ resolve(predicate());
87
+ }, timeoutMs);
88
+ const observer = new MutationObserver(() => {
89
+ const element = predicate();
90
+ if (!element) return;
91
+ globalThis.clearTimeout(timeout);
92
+ observer.disconnect();
93
+ resolve(element);
94
+ });
95
+ observer.observe(document.body, { childList: true, subtree: true });
96
+ });
97
+}
98
+
99
+function browserPanelForMode(mode = "modal") {
100
+ const panels = Array.from(document.querySelectorAll(".browser-panel"));
101
+ if (mode === "canvas") {
102
+ return panels.find((panel) => panel.closest?.('[data-surface-id="browser"]')) || null;
103
+ }
104
+ return panels.find((panel) => panel.closest?.(".modal")) || null;
105
+}
106
+
107
function placeCaretAtEnd(element) {
108
if (!element) return;
109
if (element.tagName === "TEXTAREA" || element.tagName === "INPUT") {
@@ -182,6 +227,8 @@ const model = {
227
_desktopKeyboardCleanup: null,
228
_desktopClipboardCleanup: null,
229
_desktopStarting: null,
230
+ _desktopUrlIntentBusy: false,
231
+ _desktopUrlIntentQueue: [],
232
233
async init(element = null) {
234
return await this.onMount(element, { mode: "canvas" });
@@ -1574,6 +1621,94 @@ const model = {
1621
}
1622
},
1623
1624
+ async handleDesktopUrlIntents(intents = []) {
1625
+ const incoming = Array.isArray(intents)
1626
+ ? intents.filter((intent) => intent && typeof intent === "object")
1627
+ : [];
1628
+ if (!incoming.length) return;
1629
+ this._desktopUrlIntentQueue.push(...incoming);
1630
+ if (this._desktopUrlIntentBusy) return;
1631
+
1632
+ this._desktopUrlIntentBusy = true;
1633
+ try {
1634
+ while (this._desktopUrlIntentQueue.length) {
1635
+ const intent = this._desktopUrlIntentQueue.shift();
1636
+ await this.openDesktopUrlIntent(intent);
1637
+ }
1638
+ } finally {
1639
+ this._desktopUrlIntentBusy = false;
1640
+ }
1641
+ },
1642
+
1643
+ async openDesktopUrlIntent(intent = {}) {
1644
+ const url = String(intent?.url || "").trim();
1645
+ const destination = this.browserDestinationForDesktopUrl();
1646
+ if (destination === "canvas") {
1647
+ await this.openBrowserCanvasForDesktopUrl(url);
1648
+ } else {
1649
+ await this.openBrowserModalForDesktopUrl(url);
1650
+ }
1651
+ this.setMessage(url ? "Opened link in Browser" : "Opened Browser");
1652
+ },
1653
+
1654
+ browserDestinationForDesktopUrl() {
1655
+ if (this.isDesktopInModal()) return "canvas";
1656
+ return "modal";
1657
+ },
1658
+
1659
+ isDesktopInModal() {
1660
+ if (isModalPathOpen(OFFICE_MODAL_PATH)) return true;
1661
+ const modalDesktop = Array.from(document.querySelectorAll(".office-panel"))
1662
+ .some((panel) => panel.closest?.(".modal") && panel.querySelector?.("[data-office-desktop-frame]"));
1663
+ if (modalDesktop) return true;
1664
+ if (rightCanvasStore?.isOpen && rightCanvasStore.activeSurfaceId === "office") return false;
1665
+ return this._mode === "modal";
1666
+ },
1667
+
1668
+ async openBrowserCanvasForDesktopUrl(url = "") {
1669
+ if (rightCanvasStore?.isMobileMode) {
1670
+ await this.openBrowserModalForDesktopUrl(url);
1671
+ return;
1672
+ }
1673
+ const payload = { url, source: "desktop-url" };
1674
+ let opened = false;
1675
+ if (isModalPathOpen(BROWSER_MODAL_PATH)) {
1676
+ opened = await rightCanvasStore.dockSurface?.("browser", {
1677
+ ...payload,
1678
+ modalPath: BROWSER_MODAL_PATH,
1679
+ sourceModalPath: BROWSER_MODAL_PATH,
1680
+ });
1681
+ } else {
1682
+ opened = await rightCanvasStore.open?.("browser", payload);
1683
+ }
1684
+ if (!opened) {
1685
+ await this.openBrowserModalForDesktopUrl(url);
1686
+ return;
1687
+ }
1688
+ if (browserStore?.openUrlIntent) {
1689
+ await browserStore.openUrlIntent(url);
1690
+ }
1691
+ },
1692
+
1693
+ async openBrowserModalForDesktopUrl(url = "") {
1694
+ if (rightCanvasStore?.isOpen && rightCanvasStore.activeSurfaceId === "browser") {
1695
+ await rightCanvasStore.openModalSurface?.("browser", { modalPath: BROWSER_MODAL_PATH });
1696
+ } else {
1697
+ const openModal = globalThis.ensureModalOpen || globalThis.openModal;
1698
+ const modalPromise = openModal?.(BROWSER_MODAL_PATH);
1699
+ if (modalPromise?.catch) {
1700
+ modalPromise.catch((error) => console.error("Browser modal open failed", error));
1701
+ }
1702
+ }
1703
+ const panel = await waitForElementByPredicate(() => browserPanelForMode("modal"));
1704
+ if (panel && browserStore?.onOpen) {
1705
+ await browserStore.onOpen(panel, { mode: "modal" });
1706
+ }
1707
+ if (browserStore?.openUrlIntent) {
1708
+ await browserStore.openUrlIntent(url);
1709
+ }
1710
+ },
1711
+
1712
startDesktopMonitor() {
1713
this.stopDesktopMonitor();
1714
if (!this.hasOfficialOffice()) return;
@@ -1593,6 +1728,7 @@ const model = {
1728
});
1729
if (response?.ok === false) throw new Error(response.error || "Desktop session closed.");
1730
this._desktopHeartbeatMisses = 0;
1731
+ await this.handleDesktopUrlIntents(response?.url_intents);
1732
if (response?.document) {
1733
const document = normalizeDocument(response.document);
1734
this.replaceActiveSession({
@@ -1673,12 +1809,14 @@ const model = {
1809
1810
setupFloatingModal(element = null) {
1811
const root = element || globalThis.document?.querySelector(".office-panel");
1812
+ const modal = root?.closest?.(".modal");
1813
const inner = root?.closest?.(".modal-inner");
1814
const body = root?.closest?.(".modal-bd");
1815
const header = inner?.querySelector?.(".modal-header");
1816
if (!inner || !body || !header || inner.dataset.officeModalReady === "1") return;
1817
1818
inner.dataset.officeModalReady = "1";
1819
+ modal?.classList?.add("modal-floating", "modal-no-backdrop");
1820
inner.classList.add("office-modal", "modal-no-backdrop");
1821
body.classList.add("office-modal-body");
1822
header.style.cursor = "move";
@@ -1927,6 +2065,7 @@ const model = {
2065
}
2066
this._floatingCleanup = () => {
2067
cleanup.splice(0).reverse().forEach((entry) => entry());
2068
+ modal?.classList?.remove("modal-floating", "modal-no-backdrop");
2069
inner.classList.remove("is-dragging", "is-resizing", "is-focus-mode");
2070
this._desktopResizeSuspended = false;
2071
this._desktopResizePending = false;
webui/css/modals.css
+8
@@ -18,6 +18,14 @@ the old and the new system. */
18
display: block;
19
}
20
21
+.modal.modal-floating {
22
+ pointer-events: none;
23
+}
24
+
25
+.modal.modal-floating .modal-inner {
26
+ pointer-events: auto;
27
+}
28
+
29
.modal-inner {
30
display: flex;
31
flex-direction: column;