From ccd0bc39714b7ac468229a891c4ada6ae07ef201 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Thu, 25 Jun 2026 20:45:18 -0700 Subject: [PATCH] [eric] browser: Ctrl/Cmd+R reloads the last-interacted browser, else the app --- electron/main.js | 12 +++++++++ electron/preload.js | 7 ++++++ .../src/app/components/Layout/AppShell.tsx | 25 +++++++++++++++++++ .../app/pages/Dashboard/cards/BrowserCard.tsx | 4 +++ frontend/src/shared/browserFocus.ts | 17 +++++++++++++ frontend/src/types/electron.d.ts | 1 + 6 files changed, 66 insertions(+) create mode 100644 frontend/src/shared/browserFocus.ts diff --git a/electron/main.js b/electron/main.js index 4e75dbd7..fb3e3d00 100644 --- a/electron/main.js +++ b/electron/main.js @@ -1959,6 +1959,17 @@ function swallowCloseWindowShortcut(event, input) { } } +// Cmd/Ctrl+R: the default menu's Reload accelerator reloads the WHOLE app even when a browser webview is focused (the "Ctrl+R reloads OpenSwarm, not the browser" complaint). preventDefault kills that accelerator (same electron#19279 path as Cmd+W, dispatched against whichever webContents is focused, hence both main window AND guests); the renderer then reloads the last-interacted browser, or the app if none. Shift+R (force reload) is left alone. +function routeReloadShortcut(event, input) { + if (input.type !== 'keyDown') return; + if (!(input.meta || input.control) || input.shift || input.alt) return; + if ((input.key || '').toLowerCase() !== 'r') return; + event.preventDefault(); + try { + if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('openswarm:reload-shortcut'); + } catch (_) {} +} + app.on('web-contents-created', (_event, contents) => { // Block Cmd+W from closing the main window, whether the window chrome or one of // its embedded webviews has focus. OAuth popups (their own 'window' contents, @@ -1966,6 +1977,7 @@ app.on('web-contents-created', (_event, contents) => { // still Cmd+W them shut. if (isCreatingMainWindow || contents.getType() === 'webview') { contents.on('before-input-event', swallowCloseWindowShortcut); + contents.on('before-input-event', routeReloadShortcut); } // Override the user-agent on popup BrowserWindows (i.e. anything created diff --git a/electron/preload.js b/electron/preload.js index 99647e74..ae70b4bd 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -108,6 +108,13 @@ contextBridge.exposeInMainWorld('openswarm', { return () => ipcRenderer.removeListener('webview-new-window', listener); }, + // Cmd/Ctrl+R, intercepted in main (kills the default-menu reload), so the renderer can reload the focused browser instead of the whole app. + onReloadShortcut: (cb) => { + const listener = () => cb(); + ipcRenderer.on('openswarm:reload-shortcut', listener); + return () => ipcRenderer.removeListener('openswarm:reload-shortcut', listener); + }, + // Deep-link callback: fires when the OS opens the app with an // openswarm://auth?token=... URL (after Stripe-hosted checkout). onAuthUrl: (cb) => { diff --git a/frontend/src/app/components/Layout/AppShell.tsx b/frontend/src/app/components/Layout/AppShell.tsx index 8e02cf0b..b72a807d 100644 --- a/frontend/src/app/components/Layout/AppShell.tsx +++ b/frontend/src/app/components/Layout/AppShell.tsx @@ -1,6 +1,8 @@ import React, { useState, useEffect, useRef, useCallback, startTransition, useMemo } from 'react'; import { NavLink, Outlet, useNavigate, useLocation } from 'react-router-dom'; import { openSettingsModal } from '@/shared/state/settingsSlice'; +import { getLastInteractedBrowser, setLastInteractedBrowser, clearLastInteractedBrowser } from '@/shared/browserFocus'; +import { getWebview } from '@/shared/browserRegistry'; import Box from '@mui/material/Box'; import ListItemButton from '@mui/material/ListItemButton'; import ListItemIcon from '@mui/material/ListItemIcon'; @@ -323,6 +325,29 @@ const AppShell: React.FC = () => { }); }, [openUrlInBrowser]); + // Track the browser card the user last touched. Chrome clicks land on this document; a webview PAGE click can't reach it, so BrowserCard reports those via the app-clicked IPC. Clearing on any non-browser-card click is what makes Ctrl+R fall back to reloading the app. + useEffect(() => { + const onPointerDown = (e: PointerEvent) => { + const card = (e.target as HTMLElement | null)?.closest?.('[data-select-type="browser-card"]') as HTMLElement | null; + if (card) setLastInteractedBrowser(card.getAttribute('data-select-id') || ''); + else clearLastInteractedBrowser(); + }; + document.addEventListener('pointerdown', onPointerDown, true); + return () => document.removeEventListener('pointerdown', onPointerDown, true); + }, []); + + // Cmd/Ctrl+R: main neutralizes the default-menu reload (which would always reload the whole app) and hands us the decision. Reload the browser you last interacted with; if that wasn't a live browser, reload the app, exactly as before. + useEffect(() => { + const w = window as any; + if (!w.openswarm?.onReloadShortcut) return; + return w.openswarm.onReloadShortcut(() => { + const id = getLastInteractedBrowser(); + const wv = id ? getWebview(id) : undefined; + if (wv) { try { wv.reload(); return; } catch (_e) { /* torn-down webview; fall through to app reload */ } } + window.location.reload(); + }); + }, []); + useEffect(() => { try { localStorage.setItem(SIDEBAR_WIDTH_KEY, String(sidebarWidth)); } catch {} }, [sidebarWidth]); diff --git a/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx b/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx index 1556a5f3..14effe16 100644 --- a/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx +++ b/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx @@ -46,6 +46,7 @@ import { setActiveTab as setRegistryActiveTab, type BrowserWebview, } from '@/shared/browserRegistry'; +import { setLastInteractedBrowser } from '@/shared/browserFocus'; import { useBrowserActivity } from '@/shared/useBrowserActivity'; import { getActionLabel } from '@/shared/browserCommandHandler'; import { resolveInput, isGoogleSearch } from '@/shared/resolveUrl'; @@ -325,6 +326,9 @@ const BrowserCard: React.FC = ({ }, }), ); + } else if (e?.channel === 'app-clicked') { + // First in-guest mousedown: a page click never reaches the host document, so this IPC is how a webview-content click marks this browser as last-interacted (drives Ctrl+R/zoom/tab targeting). + setLastInteractedBrowser(browserId); } }; diff --git a/frontend/src/shared/browserFocus.ts b/frontend/src/shared/browserFocus.ts new file mode 100644 index 00000000..4cd06a67 --- /dev/null +++ b/frontend/src/shared/browserFocus.ts @@ -0,0 +1,17 @@ +// Tracks which browser card the user last interacted with (clicked into its page or its chrome), +// so global shortcuts (Ctrl+R reload, Ctrl +/- zoom, Ctrl+Tab) target THAT browser instead of a +// guess. Module-level and imperative on purpose: shortcut handlers read it on keydown, so no React +// re-render is needed. Cleared the moment the user clicks anything that isn't a browser card. +let lastInteractedBrowserId: string | null = null; + +export function setLastInteractedBrowser(browserId: string): void { + lastInteractedBrowserId = browserId; +} + +export function clearLastInteractedBrowser(): void { + lastInteractedBrowserId = null; +} + +export function getLastInteractedBrowser(): string | null { + return lastInteractedBrowserId; +} diff --git a/frontend/src/types/electron.d.ts b/frontend/src/types/electron.d.ts index e129804e..8d8ba8cb 100644 --- a/frontend/src/types/electron.d.ts +++ b/frontend/src/types/electron.d.ts @@ -47,6 +47,7 @@ declare global { onUpdateDownloaded: (cb: (info: OpenSwarmUpdateInfo) => void) => () => void; onUpdateError: (cb: (message: string) => void) => () => void; onWebviewNewWindow: (cb: (url: string, webContentsId: number) => void) => () => void; + onReloadShortcut?: (cb: () => void) => () => void; openExternal: (url: string) => Promise; hardReset?: () => Promise; clearBrowserData?: () => Promise<{ ok: boolean }>;