From 1fcca3b32cfda3fefc0686d5b057ee45eebb39d3 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 2 May 2026 19:20:33 +0700 Subject: [PATCH] fix(app): browser pane keyboard tweaks - Refresh icon: use single-arrow RotateCw instead of two-arrow RefreshCw - URL bar selects all text on focus - Cmd+R now reloads the active browser pane (tracked via IPC) when the URL bar has focus, instead of reloading the Electron window - Forward Paseo shortcut keys (b, e, w, t, k, /, \, comma, digits, enter, arrows) from the embedded webview to the main window so sidebar/tab/etc. shortcuts still fire when focus is inside the loaded page --- .../src/components/browser-pane.electron.tsx | 24 +++++++-- packages/app/src/desktop/host.ts | 5 ++ .../app/src/hooks/use-keyboard-shortcuts.ts | 26 +++++++++ .../desktop/src/features/browser-webviews.ts | 24 ++++++++- packages/desktop/src/features/menu.ts | 11 +++- packages/desktop/src/main.ts | 54 +++++++++++++++++++ packages/desktop/src/preload.ts | 4 ++ 7 files changed, 141 insertions(+), 7 deletions(-) diff --git a/packages/app/src/components/browser-pane.electron.tsx b/packages/app/src/components/browser-pane.electron.tsx index 90dc1db5d..a6cf4f3ee 100644 --- a/packages/app/src/components/browser-pane.electron.tsx +++ b/packages/app/src/components/browser-pane.electron.tsx @@ -8,7 +8,7 @@ import { createElement, } from "react"; import { Pressable, Text, TextInput, View } from "react-native"; -import { ArrowLeft, ArrowRight, MousePointer2, RefreshCw } from "lucide-react-native"; +import { ArrowLeft, ArrowRight, MousePointer2, RotateCw } from "lucide-react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { buildWorkspaceAttachmentScopeKey, @@ -336,13 +336,27 @@ export function BrowserPane({ const updateBrowserRef = useRef(updateBrowser); updateBrowserRef.current = updateBrowser; - const focusUrlBar = useCallback(() => { - urlInputRef.current?.focus(); + const selectUrlBar = useCallback(() => { window.setTimeout(() => { getTextInputNativeElement(urlInputRef.current)?.select(); }, 0); }, []); + const markActivePane = useCallback(() => { + if (!isElectronRuntime()) return; + void getDesktopHost()?.browser?.setActivePane?.(browserIdRef.current); + }, []); + + const handleUrlBarFocus = useCallback(() => { + markActivePane(); + selectUrlBar(); + }, [markActivePane, selectUrlBar]); + + const focusUrlBar = useCallback(() => { + urlInputRef.current?.focus(); + selectUrlBar(); + }, [selectUrlBar]); + const syncNavigationState = useCallback((input?: { syncUrl?: boolean }) => { const webview = webviewRef.current; if (!webview || !domReadyRef.current) { @@ -466,6 +480,7 @@ export function BrowserPane({ }; const handleWebviewFocus = () => { onFocusPane?.(); + markActivePane(); }; webview.addEventListener("did-start-loading", handleStartLoading); @@ -937,7 +952,7 @@ export function BrowserPane({ onPress={handleRefresh} style={baseIconButtonStyle} > - + @@ -946,6 +961,7 @@ export function BrowserPane({ autoCapitalize="none" autoCorrect={false} onChangeText={setDraftUrl} + onFocus={handleUrlBarFocus} onSubmitEditing={handleNavigateDraftUrl} placeholder="Enter URL" placeholderTextColor={theme.colors.foregroundMuted} diff --git a/packages/app/src/desktop/host.ts b/packages/app/src/desktop/host.ts index c3b9fceaa..4ec360a23 100644 --- a/packages/app/src/desktop/host.ts +++ b/packages/app/src/desktop/host.ts @@ -74,6 +74,10 @@ export interface DesktopBrowserShortcutEvent { action: "focus-url"; } +export interface DesktopBrowserBridge { + setActivePane?: (browserId: string | null) => Promise; +} + export interface DesktopInvokeBridge { invoke?: (command: string, args?: Record) => Promise; } @@ -88,6 +92,7 @@ export interface DesktopHostBridge { notification?: DesktopNotificationBridge; opener?: DesktopOpenerBridge; menu?: DesktopMenuBridge; + browser?: DesktopBrowserBridge; } declare global { diff --git a/packages/app/src/hooks/use-keyboard-shortcuts.ts b/packages/app/src/hooks/use-keyboard-shortcuts.ts index 857006152..7518050b9 100644 --- a/packages/app/src/hooks/use-keyboard-shortcuts.ts +++ b/packages/app/src/hooks/use-keyboard-shortcuts.ts @@ -24,6 +24,7 @@ import { getShortcutOs } from "@/utils/shortcut-platform"; import { useOpenProjectPicker } from "@/hooks/use-open-project-picker"; import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-overrides"; import { isNative } from "@/constants/platform"; +import { getDesktopHost, isElectronRuntime } from "@/desktop/host"; import { isImeComposingKeyboardEvent } from "@/utils/keyboard-ime"; import { getRelativeSidebarShortcutTarget } from "@/utils/sidebar-shortcuts"; import { useActiveServerId } from "@/hooks/use-active-server-id"; @@ -418,6 +419,26 @@ export function useKeyboardShortcuts({ window.addEventListener("keyup", handleKeyUp, true); window.addEventListener("blur", handleBlurOrHide); document.addEventListener("visibilitychange", handleBlurOrHide); + + const forwardedKeySubscription = isElectronRuntime() + ? getDesktopHost()?.events?.on?.("browser-forwarded-key", (payload) => { + if (!payload || typeof payload !== "object") return; + const p = payload as Record; + if (typeof p.key !== "string") return; + window.dispatchEvent( + new KeyboardEvent("keydown", { + key: p.key, + code: typeof p.code === "string" ? p.code : "", + metaKey: p.meta === true, + ctrlKey: p.control === true, + shiftKey: p.shift === true, + altKey: p.alt === true, + bubbles: true, + }), + ); + }) + : null; + return () => { if (chordStateRef.current.timeoutId !== null) { clearTimeout(chordStateRef.current.timeoutId); @@ -431,6 +452,11 @@ export function useKeyboardShortcuts({ window.removeEventListener("keyup", handleKeyUp, true); window.removeEventListener("blur", handleBlurOrHide); document.removeEventListener("visibilitychange", handleBlurOrHide); + if (typeof forwardedKeySubscription === "function") { + forwardedKeySubscription(); + } else { + void forwardedKeySubscription?.then((dispose) => dispose()); + } }; }, [ bindings, diff --git a/packages/desktop/src/features/browser-webviews.ts b/packages/desktop/src/features/browser-webviews.ts index 3cce3480b..9e1d5bff5 100644 --- a/packages/desktop/src/features/browser-webviews.ts +++ b/packages/desktop/src/features/browser-webviews.ts @@ -1,11 +1,15 @@ -import type { WebContents } from "electron"; +import { webContents as allWebContents, type WebContents } from "electron"; const browserIdsByWebContentsId = new Map(); +let activeBrowserPaneId: string | null = null; export function registerPaseoBrowserWebContents(contents: WebContents, browserId: string): void { browserIdsByWebContentsId.set(contents.id, browserId); contents.once("destroyed", () => { browserIdsByWebContentsId.delete(contents.id); + if (activeBrowserPaneId === browserId) { + activeBrowserPaneId = null; + } }); } @@ -15,3 +19,21 @@ export function getPaseoBrowserIdForWebContents(contents: WebContents | null): s } return browserIdsByWebContentsId.get(contents.id) ?? null; } + +export function setActivePaseoBrowserPaneId(browserId: string | null): void { + activeBrowserPaneId = browserId; +} + +export function getActivePaseoBrowserWebContents(): WebContents | null { + if (!activeBrowserPaneId) { + return null; + } + for (const [contentsId, browserId] of browserIdsByWebContentsId) { + if (browserId !== activeBrowserPaneId) continue; + const contents = allWebContents.fromId(contentsId); + if (contents && !contents.isDestroyed()) { + return contents; + } + } + return null; +} diff --git a/packages/desktop/src/features/menu.ts b/packages/desktop/src/features/menu.ts index e036c3d71..cd66fe722 100644 --- a/packages/desktop/src/features/menu.ts +++ b/packages/desktop/src/features/menu.ts @@ -1,5 +1,8 @@ import { app, Menu, BrowserWindow, ipcMain, webContents } from "electron"; -import { getPaseoBrowserIdForWebContents } from "./browser-webviews.js"; +import { + getActivePaseoBrowserWebContents, + getPaseoBrowserIdForWebContents, +} from "./browser-webviews.js"; interface ShowContextMenuInput { kind?: "terminal"; @@ -20,8 +23,12 @@ function getFocusedPaseoBrowserWebContents(): Electron.WebContents | null { return getPaseoBrowserIdForWebContents(focusedContents) ? focusedContents : null; } +function getReloadTargetBrowserWebContents(): Electron.WebContents | null { + return getFocusedPaseoBrowserWebContents() ?? getActivePaseoBrowserWebContents(); +} + function reloadFocusedContentsOrWindow(win: BrowserWindow, options?: { ignoreCache?: boolean }) { - const browserContents = getFocusedPaseoBrowserWebContents(); + const browserContents = getReloadTargetBrowserWebContents(); if (browserContents) { if (options?.ignoreCache) { browserContents.reloadIgnoringCache(); diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts index 731e73408..5ddef207a 100644 --- a/packages/desktop/src/main.ts +++ b/packages/desktop/src/main.ts @@ -36,6 +36,7 @@ import { setupApplicationMenu } from "./features/menu.js"; import { getPaseoBrowserIdForWebContents, registerPaseoBrowserWebContents, + setActivePaseoBrowserPaneId, } from "./features/browser-webviews.js"; import { parseOpenProjectPathFromArgv } from "./open-project-routing.js"; import { getDesktopSettingsStore } from "./settings/desktop-settings-electron.js"; @@ -76,6 +77,33 @@ function preventUnsafeBrowserWebviewNavigation( } const OPEN_PROJECT_EVENT = "paseo:event:open-project"; const BROWSER_SHORTCUT_EVENT = "paseo:event:browser-shortcut"; +const BROWSER_FORWARDED_KEY_EVENT = "paseo:event:browser-forwarded-key"; + +const FORWARDED_PASEO_SHORTCUT_KEYS = new Set([ + "b", + "e", + "w", + "t", + "k", + "/", + "\\", + ",", + ".", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "enter", + "arrowleft", + "arrowright", + "arrowup", + "arrowdown", +]); const DESKTOP_SMOKE_ENV = "PASEO_DESKTOP_SMOKE"; const DESKTOP_SMOKE_STOP_REQUEST = "paseo-smoke-stop"; app.setName("Paseo"); @@ -105,6 +133,16 @@ function isBrowserLocationInput(input: Electron.Input): boolean { return (input.meta || input.control) && input.key.toLowerCase() === "l"; } +function isForwardablePaseoShortcutInput(input: Electron.Input): boolean { + if (input.type !== "keyDown") { + return false; + } + if (!input.meta && !input.control) { + return false; + } + return FORWARDED_PASEO_SHORTCUT_KEYS.has(input.key.toLowerCase()); +} + // In dev mode, detect git worktrees and isolate each instance so multiple // Electron windows can run side-by-side (separate userData = separate lock). let devWorktreeName: string | null = null; @@ -177,6 +215,10 @@ ipcMain.handle("paseo:get-pending-open-project", () => { return result; }); +ipcMain.handle("paseo:browser:set-active-pane", (_event, browserId: unknown) => { + setActivePaseoBrowserPaneId(typeof browserId === "string" ? browserId : null); +}); + protocol.registerSchemesAsPrivileged([ { scheme: APP_SCHEME, privileges: { standard: true, secure: true, supportFetchAPI: true } }, ]); @@ -317,6 +359,18 @@ async function createMainWindow(): Promise { action: "focus-url", ...(focusedBrowserId ? { browserId: focusedBrowserId } : {}), }); + return; + } + if (isForwardablePaseoShortcutInput(input)) { + event.preventDefault(); + mainWindow.webContents.send(BROWSER_FORWARDED_KEY_EVENT, { + key: input.key, + code: input.code, + meta: input.meta, + control: input.control, + shift: input.shift, + alt: input.alt, + }); } }); contents.setWindowOpenHandler(({ url }) => { diff --git a/packages/desktop/src/preload.ts b/packages/desktop/src/preload.ts index 8b7b02b5f..3159e7641 100644 --- a/packages/desktop/src/preload.ts +++ b/packages/desktop/src/preload.ts @@ -57,4 +57,8 @@ contextBridge.exposeInMainWorld("paseoDesktop", { showContextMenu: (input?: Record) => ipcRenderer.invoke("paseo:menu:showContextMenu", input), }, + browser: { + setActivePane: (browserId: string | null) => + ipcRenderer.invoke("paseo:browser:set-active-pane", browserId), + }, });