From 17918fdeafdf302d4eb4c2d58affc22b37b3370f Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 20 Oct 2026 15:49:46 -0700 Subject: [PATCH] =?UTF-8?q?fix(desktop):=20=E2=8C=98R=20reloads=20the=20fo?= =?UTF-8?q?cused=20browser=20tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../browser-pane-restore.spec.ts | 36 +++++++++++++++++++ .../gui-extensions/src/browser/chromium.ts | 4 +-- .../gui-extensions/src/browser/keys.test.ts | 30 +++++++++++++++- packages/gui-extensions/src/browser/keys.ts | 14 +++++++- .../src/browser/panel.fixture.tsx | 6 ++++ .../gui-extensions/src/browser/renderer.tsx | 20 +++++++++++ 6 files changed, 106 insertions(+), 4 deletions(-) diff --git a/packages/app/component-tests/browser-pane-restore.spec.ts b/packages/app/component-tests/browser-pane-restore.spec.ts index 26f16aa0d8d9..100c235841df 100644 --- a/packages/app/component-tests/browser-pane-restore.spec.ts +++ b/packages/app/component-tests/browser-pane-restore.spec.ts @@ -344,6 +344,42 @@ story("opens a browser tab with ⌘T inside the pane and a session everywhere el await expect(sessions).toHaveText("1") }) +story("reloads the browser tab with ⌘R inside the pane and leaves it to the window elsewhere", async ({ page }) => { + const root = page.getByTestId("browser-region-fixture") + const mac = await page.evaluate(() => /Mac|iPhone|iPad|iPod/.test(navigator.platform)) + const outside = root.getByRole("button", { name: "Beta", exact: true }) + const address = root.getByRole("combobox", { name: "Browser address", exact: true }) + const reloads = root.getByTestId("browser-reloads") + await expect(root.getByText("Registrations: 1", { exact: true })).toBeVisible() + await outside.click() + await expect(root.getByText("Registrations: 2", { exact: true })).toBeVisible() + await root.getByRole("button", { name: "First inventory", exact: true }).click() + await expect(root.getByRole("tab", { name: "Preview", exact: true })).toHaveAttribute("aria-selected", "true") + + // Returns whether the chord stays unhandled, so it reaches the window's Reload menu item. + const press = (target: typeof outside) => + target.evaluate( + (element, meta) => + element.dispatchEvent( + new KeyboardEvent("keydown", { + key: "r", + code: "KeyR", + metaKey: meta, + ctrlKey: !meta, + bubbles: true, + cancelable: true, + }), + ), + mac, + ) + + expect(await press(outside)).toBe(true) + await expect(reloads).toHaveText("0") + + expect(await press(address)).toBe(false) + await expect(reloads).toHaveText("1") +}) + story("reopens a closed browser tab with ⌘⇧T after the side panel and a session tab elsewhere", async ({ page }) => { const root = page.getByTestId("browser-region-fixture") const mac = await page.evaluate(() => /Mac|iPhone|iPad|iPod/.test(navigator.platform)) diff --git a/packages/gui-extensions/src/browser/chromium.ts b/packages/gui-extensions/src/browser/chromium.ts index bec250879978..f35b315ed7f1 100644 --- a/packages/gui-extensions/src/browser/chromium.ts +++ b/packages/gui-extensions/src/browser/chromium.ts @@ -7,7 +7,7 @@ import { createCdp, abortError, waitFor } from "./cdp" import { createBrowserFiles } from "./files" import { createDiagnostics } from "./diagnostics" import { loadIcon } from "./icon" -import { pageLetter, pageShortcut, type PageShortcut } from "./keys" +import { pageLetter, pageReload, pageShortcut, type PageShortcut } from "./keys" import { createProfiling, type Recording } from "./profiling" import type { BrowserNetwork } from "./network" import { @@ -178,7 +178,7 @@ export function createBrowserPage( contents.on("before-input-event", (event, input) => { if (input.type !== "keyDown") return - if (input.key === "F5" && !input.meta && !input.control && !input.alt && !input.shift) { + if (pageReload(input, process.platform === "darwin")) { event.preventDefault() contents.reload() diff --git a/packages/gui-extensions/src/browser/keys.test.ts b/packages/gui-extensions/src/browser/keys.test.ts index 35338ddb2351..bf4474466c99 100644 --- a/packages/gui-extensions/src/browser/keys.test.ts +++ b/packages/gui-extensions/src/browser/keys.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test" -import { pageLetter, pageShortcut, type PageKey } from "./keys" +import { pageLetter, pageReload, pageShortcut, type PageKey } from "./keys" /** A key press with `mod` as the platform's modifier: Command on macOS, Control elsewhere. */ const press = (modifiers: string, key: string, mac: boolean, code = ""): PageKey => { @@ -121,3 +121,31 @@ test("forwards browser and shell shortcuts while leaving page editing, find, rel expect(results).toEqual(cases.map(([modifiers, key, mac, forwarded]) => [modifiers, key, mac, forwarded])) }) + +test("reloads the focused page on F5 and the platform's reload chord, leaving other R chords to the window", () => { + const cases: [modifiers: string, key: string, mac: boolean, reloads: boolean, code?: string][] = [ + ["", "F5", true, true], + ["", "F5", false, true], + ["mod", "r", true, true], + ["mod", "r", false, true], + // Dvorak: R is on the physical O key; the character decides. + ["mod", "r", true, true, "KeyO"], + ["mod", "\u043a", true, true, "KeyR"], + // ⇧⌘R toggles review, and macOS Control+R is the page's own. + ["mod+shift", "R", true, false], + ["mod+shift", "R", false, false], + ["mod+alt", "r", true, false], + ["ctrl", "r", true, false], + ["mod", "F5", true, false], + ["", "r", true, false], + ] + + const results = cases.map(([modifiers, key, mac, , code]) => [ + modifiers, + key, + mac, + pageReload(press(modifiers, key, mac, code), mac), + ]) + + expect(results).toEqual(cases.map(([modifiers, key, mac, reloads]) => [modifiers, key, mac, reloads])) +}) diff --git a/packages/gui-extensions/src/browser/keys.ts b/packages/gui-extensions/src/browser/keys.ts index 572c8be9473d..ad0c7c4ffd9f 100644 --- a/packages/gui-extensions/src/browser/keys.ts +++ b/packages/gui-extensions/src/browser/keys.ts @@ -37,10 +37,22 @@ export function pageLetter(input: Pick) { return input.code.slice(3).toLowerCase() } +/** + * Whether a page key press is the system browser's reload: F5, or the platform's modifier with R. The focused page + * reloads itself; left to the window, the Reload menu item would reload the whole app. + */ +export function pageReload(input: PageKey, mac: boolean) { + if (input.alt || input.shift) return false + + if (input.key === "F5") return !input.meta && !input.control + + return (mac ? input.meta && !input.control : input.control && !input.meta) && pageLetter(input) === "r" +} + /** * Returns the shortcut to forward to the app window when a focused page receives a key press that belongs to the * browser pane or app shell, or `undefined` when the key belongs to the page (typing, editing, clipboard, find, - * reload, history, or in-page shortcuts like ⌘K and ⌘B). + * history, or in-page shortcuts like ⌘K and ⌘B). */ export function pageShortcut(input: PageKey, mac: boolean): PageShortcut | undefined { const key = pageLetter(input) diff --git a/packages/gui-extensions/src/browser/panel.fixture.tsx b/packages/gui-extensions/src/browser/panel.fixture.tsx index ddba7d505d33..b89b9ad50f84 100644 --- a/packages/gui-extensions/src/browser/panel.fixture.tsx +++ b/packages/gui-extensions/src/browser/panel.fixture.tsx @@ -641,6 +641,8 @@ type RegionFixtureState = { sessions: number /** How many browser tabs the pane asked main to open. */ opens: number + /** How many page reloads the pane asked main for. */ + reloads: number reopens: number switches: number /** The page main reports, blank or loaded. */ @@ -678,6 +680,7 @@ export function mountBrowserRegion(input: RegionHost) { opened: true, sessions: 0, opens: 0, + reloads: 0, reopens: 0, switches: 0, blank: false, @@ -849,6 +852,8 @@ export function mountBrowserRegion(input: RegionHost) { command: async (value) => { if (value.command.type === "tabs.open") setStore("opens", (count) => count + 1) + if (value.command.type === "reload") setStore("reloads", (count) => count + 1) + if (value.command.type !== "tabs.close") return const tabID = value.command.tabID @@ -1099,6 +1104,7 @@ export function mountBrowserRegion(input: RegionHost) {