From f5f788652e552eff1e1052566a71ed45ba8e3cc7 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Fri, 22 May 2026 15:43:30 +0100 Subject: [PATCH] Fix active-pane detection via activeElement polling After exhausting event-based approaches that all failed in WebView2: - per-leaf onpointerdown: xterm.js stopPropagation - document-capture pointerdown: only first event ever delivered - document-capture mousedown/click: never delivered at all - document-capture focusin: silently fails - term.onFocus: no such xterm.js API The bulletproof fallback: poll document.activeElement every 250ms and call orch.setActive on its closest [data-leaf-id] ancestor. No DOM events involved. Verified working with automation: clicking pane 2 turns its border blue, clicking pane 1 moves the border to pane 1, etc. XtermPane gained an onFocus prop (still wired through LeafPane) as a secondary signal that might fire in some configurations, but the polling is the actual fix. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/App.svelte | 35 +++++++++++++++++++-------------- src/components/XtermPane.svelte | 17 ++++++++++++++++ src/lib/layout/LeafPane.svelte | 1 + 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/App.svelte b/src/App.svelte index c33edb8..1e90889 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -184,24 +184,29 @@ return () => window.removeEventListener("keydown", onKey, true); }); - // ---- Document-level active-pane detector -------------------------------- - // xterm.js calls `stopPropagation` on pointerdown inside terminals, so a - // per-leaf `onpointerdown` never fires for body clicks. A document-level - // CAPTURE-phase listener fires before xterm.js can intercept, then finds - // the nearest `data-leaf-id` ancestor to know which pane was clicked. - // Toolbar buttons also pass through (they're outside the xterm container, - // their own onclick still fires in the bubble phase afterwards). + // ---- Active-pane detector via active-element polling -------------------- + // Tried (and failed in WebView2): per-leaf onpointerdown (xterm blocks + // propagation), document-capture pointerdown (Webview2 only delivers the + // first one then nothing), document-capture focusin (also silently fails), + // xterm.js term.onFocus (no such API), textarea focus listener (race). + // + // Polling document.activeElement is the only thing left that's bulletproof + // — no events involved at all. 250ms is fast enough to feel instant when + // clicking and cheap enough to not show up on a CPU profile. $effect(() => { - function onAnyPointerDown(e: PointerEvent) { - const t = e.target as Element | null; - if (!t) return; - const leafEl = t.closest("[data-leaf-id]"); + let lastLeafId: string | null = null; + const interval = window.setInterval(() => { + const el = document.activeElement; + if (!el) return; + const leafEl = el.closest("[data-leaf-id]"); if (!leafEl) return; const id = leafEl.getAttribute("data-leaf-id"); - if (id) orch.setActive(id); - } - document.addEventListener("pointerdown", onAnyPointerDown, true); - return () => document.removeEventListener("pointerdown", onAnyPointerDown, true); + if (id && id !== lastLeafId) { + lastLeafId = id; + orch.setActive(id); + } + }, 250); + return () => clearInterval(interval); }); // ---- preset layouts ------------------------------------------------------ diff --git a/src/components/XtermPane.svelte b/src/components/XtermPane.svelte index ccccd09..b50ee6a 100644 --- a/src/components/XtermPane.svelte +++ b/src/components/XtermPane.svelte @@ -20,6 +20,7 @@ onSpawn = undefined, onInput = undefined, onDataReceived = undefined, + onFocus = undefined, focusTrigger = 0, }: { distro?: string; @@ -31,6 +32,8 @@ onInput?: (dataB64: string) => void; /** Fired whenever output arrives from the PTY. Used for idle detection. */ onDataReceived?: () => void; + /** Fired when xterm's textarea gains focus (i.e., user clicked here). */ + onFocus?: () => void; /** Increment to refocus the terminal programmatically (palette etc.). */ focusTrigger?: number; } = $props(); @@ -112,6 +115,20 @@ onInput?.(b64); }); + // xterm.js's own focus event — fires when the hidden textarea gets focus + // (i.e., user clicked anywhere in the terminal). Most reliable signal + // for "user wants this pane active" — no DOM event traversal involved. + term.onSelectionChange(() => {}); // ensure the addon system is initialized; noop + if (typeof (term as unknown as { onFocus?: unknown }).onFocus === "function") { + (term as unknown as { onFocus: (cb: () => void) => void }).onFocus(() => { + onFocus?.(); + }); + } else { + // Fallback: listen on the textarea element directly. + const ta = containerEl.querySelector(".xterm-helper-textarea"); + if (ta) ta.addEventListener("focus", () => onFocus?.(), true); + } + // Re-fit on container resize; forward new size to the PTY. ro = new ResizeObserver(() => { try { diff --git a/src/lib/layout/LeafPane.svelte b/src/lib/layout/LeafPane.svelte index d190444..645a095 100644 --- a/src/lib/layout/LeafPane.svelte +++ b/src/lib/layout/LeafPane.svelte @@ -214,6 +214,7 @@ }} onSpawn={onPaneSpawned} onInput={onTerminalInput} + onFocus={() => orch.setActive(leaf.id)} {onDataReceived} {focusTrigger} />