diff --git a/electron/webview-preload.js b/electron/webview-preload.js index 421ec2c8..ab4aa519 100644 --- a/electron/webview-preload.js +++ b/electron/webview-preload.js @@ -177,6 +177,35 @@ try { window.addEventListener('wheel', onWheelCapture, { capture: true, passive: false }); document.addEventListener('wheel', onWheelCapture, { capture: true, passive: false }); + // --------------------------------------------------------------------------- + // Double-click to fit the browser card (parity with agent-chat dblclick). + // + // A is an out-of-process guest, so a dblclick inside it never + // reaches the embedding renderer and the dashboard's card dblclick-to-fit + // never fires over page content. Forward non-interactive dblclicks to the + // host (BrowserCard's ipc-message handler calls onDoubleClick -> fitToCards). + // We never preventDefault: the page keeps its native behavior. We skip + // interactive targets and skip when the dblclick selected a word, so links/ + // buttons/inputs and native word-select win there. canvas is treated as + // interactive on purpose (maps/games/design tools use dblclick meaningfully). + const INTERACTIVE_DBLCLICK_SELECTOR = [ + 'a[href]', 'button', 'input', 'textarea', 'select', 'option', 'label', + 'summary', 'details', 'video', 'audio', 'iframe', 'embed', 'object', 'canvas', + '[contenteditable=""]', '[contenteditable="true"]', + '[role="button"]', '[role="link"]', '[role="textbox"]', '[role="menuitem"]', + '[role="tab"]', '[role="checkbox"]', '[role="radio"]', '[role="switch"]', '[role="slider"]', + ].join(','); + const onDblClickCapture = (e) => { + try { + const t = e.target; + if (t && t.closest && t.closest(INTERACTIVE_DBLCLICK_SELECTOR)) return; + const sel = window.getSelection && window.getSelection(); + if (sel && String(sel).trim().length > 0) return; + ipcRenderer.sendToHost('browser-dblclick'); + } catch (_) {} + }; + document.addEventListener('dblclick', onDblClickCapture, { capture: true }); + // --------------------------------------------------------------------------- // [FRONTEND] console capture for the App Builder Terminal pane. // diff --git a/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx b/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx index f21d1ce4..31862f43 100644 --- a/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx +++ b/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx @@ -192,6 +192,10 @@ const BrowserCard: React.FC = ({ }) => { const c = useClaudeTokens(); const dispatch = useAppDispatch(); + // Read via ref inside the webview-attach effect so a new onDoubleClick identity + // doesn't re-run that effect (which would re-register the webview). + const onDoubleClickRef = useRef(onDoubleClick); + onDoubleClickRef.current = onDoubleClick; const scrollOverlayRef = useOverlayScrollPassthrough(isSelected); const browserHomepage = useAppSelector((state) => state.settings.data.browser_homepage); const elementSelectionCtx = useElementSelection(); @@ -308,6 +312,8 @@ const BrowserCard: React.FC = ({ // No unconditional log; forwarded webview-console messages were causing 100s of host warns/sec and main-thread stalls. if (e?.channel === 'passkey-detected') { setPasskeyDialogOpen(true); + } else if (e?.channel === 'browser-dblclick') { + onDoubleClickRef.current?.(browserId, 'browser'); } else if (e?.channel === 'canvas-wheel-zoom') { // Convert guest coords to doc coords and dispatch a CustomEvent; synthetic WheelEvent bubble was unreliable through GuestView. const payload = e.args?.[0] || {};