diff --git a/static/panes/pane-desktop-host.js b/static/panes/pane-desktop-host.js
new file mode 100644
index 0000000..af2366b
--- /dev/null
+++ b/static/panes/pane-desktop-host.js
@@ -0,0 +1,108 @@
+/*
+ * fee[dB]ack — the desktop pane host.
+ *
+ * When running inside the desktop app, a popped-out pane gets a real
+ * BrowserWindow instead of a browser pop-up: it remembers where you put it, it
+ * can float above everything, it minimizes to the system tray, and the tray
+ * lists every pane you have.
+ *
+ * Registers as the `desktop` host at priority 20 — above `window` (10, the
+ * browser pop-up) and `dock` (0) — so `panes.detach()` picks it whenever the
+ * desktop bridge is present. In a plain browser this file registers nothing and
+ * the browser pop-up host takes over. Nothing else in the pane system changes;
+ * that is what the host registry is for.
+ *
+ * The renderer stays the authority on what a pane IS. The main process only owns
+ * the window. So this file pushes the pane registry up to the tray, and answers
+ * the tray when it asks for a pane to be toggled.
+ */
+(function () {
+ 'use strict';
+
+ const panes = window.feedBack && window.feedBack.panes;
+ const bus = window.feedBack;
+ // `feedBackDesktop.panes` is absent in a browser, and also in an OLDER
+ // desktop build that predates this feature — in both cases we simply don't
+ // register, and the browser pop-out host handles detach as before.
+ const desktop = window.feedBackDesktop && window.feedBackDesktop.panes;
+ if (!panes || !bus || !desktop) return;
+
+ const open = new Set();
+
+ function mount(spec) {
+ const url = new URL(window.location.origin + '/pane');
+ url.searchParams.set('pane', spec.id);
+ url.searchParams.set('script', spec.script);
+
+ open.add(spec.id);
+ // Fire-and-forget: the pane's contents arrive over BroadcastChannel from
+ // pane-hub.js, not through this call. All the main process does is put a
+ // window on screen pointing at our own origin.
+ desktop.open({
+ paneId: spec.id,
+ url: url.toString(),
+ title: spec.title,
+ width: spec.width,
+ height: spec.height,
+ }).then((ok) => {
+ if (ok) return;
+ console.error('[panes] desktop refused to open a window for', spec.id);
+ open.delete(spec.id);
+ panes.close(spec.id);
+ }).catch((err) => {
+ console.error('[panes] desktop pane open failed', spec.id, err);
+ open.delete(spec.id);
+ panes.close(spec.id);
+ });
+
+ return { paneId: spec.id }; // an opaque handle; the manager only checks it's truthy
+ }
+
+ function unmount(id) {
+ open.delete(id);
+ desktop.close(id).catch(() => { /* window already gone */ });
+ }
+
+ panes.registerHost({
+ id: 'desktop',
+ priority: 20,
+ remote: true,
+ // Unlike a browser pop-up, this needs no user gesture — so a pane the user
+ // left popped out comes back popped out, where they left it, on next launch.
+ autoRestore: true,
+ available: () => typeof BroadcastChannel === 'function',
+ // A pane with no `script` is only a closure in this realm and cannot
+ // honestly cross a window boundary; the router falls back to the dock.
+ canHost: (spec) => !!spec.script,
+ mount,
+ unmount,
+ focus: (id) => { desktop.focus(id).catch(() => { /* window already gone */ }); },
+ });
+
+ // The user closed the pane window (or it crashed). Close the pane, so the
+ // dialog its pop-out chip hid comes back — otherwise the user's own UI is
+ // hidden with no way to reach it.
+ desktop.onClosed((paneId) => {
+ if (!open.has(paneId)) return;
+ open.delete(paneId);
+ panes.close(paneId);
+ });
+
+ // The tray asked to toggle a pane it has no window for. Only this realm knows
+ // what opening one means.
+ desktop.onToggle((paneId) => {
+ if (panes.isOpen(paneId)) panes.close(paneId);
+ else panes.detach(paneId);
+ });
+
+ // Keep the tray's menu in step with the registry. Cheap and rare — panes are
+ // registered at load and toggled by hand, never on a playback path.
+ function sync() {
+ desktop.sync(panes.list().map((p) => ({ id: p.id, title: p.title, icon: p.icon, open: p.open })));
+ }
+ bus.on('panes:registered', sync);
+ bus.on('panes:unregistered', sync);
+ bus.on('panes:opened', sync);
+ bus.on('panes:closed', sync);
+ sync();
+})();
diff --git a/static/v3/index.html b/static/v3/index.html
index 33f5c62..bdd6f74 100644
--- a/static/v3/index.html
+++ b/static/v3/index.html
@@ -1327,6 +1327,10 @@
+
+