From b1b291db08b8f142a14ed3019946a72a5d89a682 Mon Sep 17 00:00:00 2001 From: Maxon Phong <55618346+Aealen@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:25:21 +0800 Subject: [PATCH] feat(browser): add hard reload option and shortcut hints to reload button (#12483) * feat(browser): add hard reload option and shortcut hints to reload button Add a tooltip to the browser reload button showing the reload shortcut. Add a right-click context menu with Reload and Hard Reload options. Add localized labels for Hard Reload across EN, ZH, JA, KO, ES. * fix(browser): add aria-labels to reload buttons * feat(browser): make reload button contextual and extract action logic - Button label now reflects actual action: Stop when loading, Retry on failure, Reload when idle - Extract reload intent resolution into reusable browser-reload-action module with tests - Add keyboard support (Enter/Space) for the reload button - Simplify remote page reload to tooltip-only (no ignore-cache RPC for remote pages) - Add "Stop" translations for all supported languages * fix(browser): exhaust reload intent switch for type-aware lint Replace the default branch with an explicit reload case so oxlint switch-exhaustiveness-check accepts BrowserReloadIntent. --------- Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> --- .../components/browser-pane/BrowserPane.tsx | 171 +++++++++++++----- .../browser-reload-action.test.ts | 53 ++++++ .../browser-pane/browser-reload-action.ts | 47 +++++ src/renderer/src/i18n/locales/en.json | 2 + src/renderer/src/i18n/locales/es.json | 2 + src/renderer/src/i18n/locales/ja.json | 2 + src/renderer/src/i18n/locales/ko.json | 2 + src/renderer/src/i18n/locales/zh.json | 2 + 8 files changed, 240 insertions(+), 41 deletions(-) create mode 100644 src/renderer/src/components/browser-pane/browser-reload-action.test.ts create mode 100644 src/renderer/src/components/browser-pane/browser-reload-action.ts diff --git a/src/renderer/src/components/browser-pane/BrowserPane.tsx b/src/renderer/src/components/browser-pane/BrowserPane.tsx index 33319c24d..a452757ec 100644 --- a/src/renderer/src/components/browser-pane/BrowserPane.tsx +++ b/src/renderer/src/components/browser-pane/BrowserPane.tsx @@ -195,6 +195,11 @@ import { createBrowserPageGuestRecovery } from './browser-page-guest-recovery' import { subscribeBrowserSystemResume } from './browser-system-resume' +import { + type BrowserReloadTrigger, + resolveBrowserReloadButtonLabelKind, + resolveBrowserReloadIntent +} from './browser-reload-action' type BrowserTabPageState = Partial< Pick< @@ -2613,18 +2618,31 @@ function RemoteBrowserPagePane({ > - + {/* Why: no ignore-cache RPC exists for remote pages, and this pane binds no reload chord, so there is + nothing truthful to put in a menu or a shortcut hint here — tooltip only. */} + + + + + + {translate('auto.components.browser.pane.BrowserPane.0e080d820e', 'Reload')} + + (browserTab.faviconUrl) const initialBrowserUrlRef = useRef(browserTab.url) const browserTabUrlRef = useRef(browserTab.url) @@ -2916,6 +2937,47 @@ function BrowserPagePane({ const [findOpen, setFindOpen] = useState(false) const grab = useGrabMode(browserTab.id) + const reloadState = useMemo( + () => ({ loading: browserTab.loading, loadErrorCode: browserTab.loadError?.code ?? null }), + [browserTab.loading, browserTab.loadError] + ) + const runReloadTrigger = useCallback( + (trigger: BrowserReloadTrigger) => { + const webview = webviewRef.current + if (!webview) { + return + } + switch (resolveBrowserReloadIntent(trigger, reloadState)) { + case 'stop': + webview.stop() + break + case 'retry-guest-recovery': + onUpdatePageStateRef.current(browserTab.id, { loading: true }) + retryGuestRecoveryRef.current() + break + case 'retry-load': + retryBrowserTabLoad(webview, browserTab, onUpdatePageStateRef.current) + break + case 'hard-reload': + webview.reloadIgnoringCache() + break + case 'reload': + webview.reload() + break + } + }, + [browserTab, reloadState] + ) + + // Keep the accessible name honest: the same button is Stop mid-load and Retry after a failure. + const reloadButtonLabelKind = resolveBrowserReloadButtonLabelKind(reloadState) + const reloadButtonLabel = + reloadButtonLabelKind === 'stop' + ? translate('auto.components.browser.pane.BrowserPane.b7e4d9c1a2', 'Stop') + : reloadButtonLabelKind === 'retry' + ? translate('auto.components.browser.pane.BrowserPane.781d6459ad', 'Retry') + : translate('auto.components.browser.pane.BrowserPane.0e080d820e', 'Reload') + const markup = useMarkupMode({ getCaptureContext: useCallback((): MarkupCaptureContext | null => { const webview = webviewRef.current @@ -5175,35 +5237,62 @@ function BrowserPagePane({ > - + + {/* Why: suppress the tooltip while the menu is open — both anchor below the button and would overlap. */} + + + + + + + + {reloadButtonLabel} + {/* Why: the chord maps to plain reload(), which is not what Stop or Retry do — only hint when they match. */} + {reloadShortcut && reloadButtonLabelKind === 'reload' ? ` · ${reloadShortcut}` : ''} + + + + runReloadTrigger('reload')}> + {translate('auto.components.browser.pane.BrowserPane.0e080d820e', 'Reload')} + {reloadShortcut} + + runReloadTrigger('hard-reload')}> + {translate('auto.components.browser.pane.BrowserPane.a1f3c2e4b5', 'Hard Reload')} + {hardReloadShortcut} + + + { + it('reloads an idle page from every trigger', () => { + expect(resolveBrowserReloadIntent('button', idle)).toBe('reload') + expect(resolveBrowserReloadIntent('reload', idle)).toBe('reload') + expect(resolveBrowserReloadIntent('hard-reload', idle)).toBe('hard-reload') + }) + + it('stops an in-flight load only from the toolbar button', () => { + expect(resolveBrowserReloadIntent('button', loading)).toBe('stop') + expect(resolveBrowserReloadIntent('reload', loading)).toBe('reload') + expect(resolveBrowserReloadIntent('hard-reload', loading)).toBe('hard-reload') + }) + + // Why: reload() on chrome-error:// only refreshes the error page — every entry point must retry the load. + it('routes a failed load to the retry path from the menu too', () => { + expect(resolveBrowserReloadIntent('button', failed)).toBe('retry-load') + expect(resolveBrowserReloadIntent('reload', failed)).toBe('retry-load') + expect(resolveBrowserReloadIntent('hard-reload', failed)).toBe('retry-load') + }) + + it('routes a guest-recovery failure to guest recovery from the menu too', () => { + expect(resolveBrowserReloadIntent('button', guestFailed)).toBe('retry-guest-recovery') + expect(resolveBrowserReloadIntent('reload', guestFailed)).toBe('retry-guest-recovery') + expect(resolveBrowserReloadIntent('hard-reload', guestFailed)).toBe('retry-guest-recovery') + }) + + it('prefers stop over retry when a failed page is already reloading', () => { + expect(resolveBrowserReloadIntent('button', { loading: true, loadErrorCode: -105 })).toBe( + 'stop' + ) + }) +}) + +describe('resolveBrowserReloadButtonLabelKind', () => { + it('names the button for what it actually does', () => { + expect(resolveBrowserReloadButtonLabelKind(idle)).toBe('reload') + expect(resolveBrowserReloadButtonLabelKind(loading)).toBe('stop') + expect(resolveBrowserReloadButtonLabelKind(failed)).toBe('retry') + expect(resolveBrowserReloadButtonLabelKind(guestFailed)).toBe('retry') + }) +}) diff --git a/src/renderer/src/components/browser-pane/browser-reload-action.ts b/src/renderer/src/components/browser-pane/browser-reload-action.ts new file mode 100644 index 000000000..2bebeaa9c --- /dev/null +++ b/src/renderer/src/components/browser-pane/browser-reload-action.ts @@ -0,0 +1,47 @@ +import { BROWSER_GUEST_RECOVERY_ERROR_CODE } from './browser-page-guest-recovery' + +/** Where the reload request came from: the toolbar button, or an explicit menu entry. */ +export type BrowserReloadTrigger = 'button' | 'reload' | 'hard-reload' + +export type BrowserReloadIntent = + | 'stop' + | 'retry-guest-recovery' + | 'retry-load' + | 'reload' + | 'hard-reload' + +export type BrowserReloadState = { + loading: boolean + loadErrorCode: number | null +} + +/** + * Why: webview.reload() only refreshes a chrome-error:// page, so a failed load has to go through the + * recovery paths no matter which entry point asked for it. Only the toolbar button doubles as Stop. + */ +export function resolveBrowserReloadIntent( + trigger: BrowserReloadTrigger, + state: BrowserReloadState +): BrowserReloadIntent { + if (trigger === 'button' && state.loading) { + return 'stop' + } + if (state.loadErrorCode !== null) { + return state.loadErrorCode === BROWSER_GUEST_RECOVERY_ERROR_CODE + ? 'retry-guest-recovery' + : 'retry-load' + } + return trigger === 'hard-reload' ? 'hard-reload' : 'reload' +} + +/** Accessible name for the toolbar button, which is Stop mid-load and Retry after a failure. */ +export type BrowserReloadButtonLabelKind = 'stop' | 'retry' | 'reload' + +export function resolveBrowserReloadButtonLabelKind( + state: BrowserReloadState +): BrowserReloadButtonLabelKind { + if (state.loading) { + return 'stop' + } + return state.loadErrorCode !== null ? 'retry' : 'reload' +} diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index de66d5412..c4913683d 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -13736,6 +13736,8 @@ "1b179ab561": "Copy Page URL", "f7ab83f7ed": "Open Page In Default Browser", "0e080d820e": "Reload", + "a1f3c2e4b5": "Hard Reload", + "b7e4d9c1a2": "Stop", "250a9b3e42": "Forward", "40edfa75cb": "Back", "efb0e8f7f3": "Copy Link Address", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index a490d3c25..68a9bcb8f 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -13455,6 +13455,8 @@ "1b179ab561": "Copiar URL de página", "f7ab83f7ed": "Abrir página en el navegador predeterminado", "0e080d820e": "Recargar", + "a1f3c2e4b5": "Recarga forzada", + "b7e4d9c1a2": "Detener", "250a9b3e42": "Adelante", "40edfa75cb": "Atrás", "efb0e8f7f3": "Copiar dirección de enlace", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 9d45db9dd..9ef5a69cb 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -13455,6 +13455,8 @@ "1b179ab561": "ページの URL をコピー", "f7ab83f7ed": "デフォルトのブラウザでページを開く", "0e080d820e": "リロード", + "a1f3c2e4b5": "ハードリロード", + "b7e4d9c1a2": "停止", "250a9b3e42": "進む", "40edfa75cb": "戻る", "efb0e8f7f3": "リンクアドレスをコピーする", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index b68da1a54..d6c3e34fc 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -13466,6 +13466,8 @@ "1b179ab561": "페이지 URL 복사", "f7ab83f7ed": "기본 브라우저에서 페이지 열기", "0e080d820e": "새로고침", + "a1f3c2e4b5": "강제 새로고침", + "b7e4d9c1a2": "중지", "250a9b3e42": "앞으로", "40edfa75cb": "뒤로", "efb0e8f7f3": "링크 주소 복사", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index b34fb4322..c5f255fe4 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -13475,6 +13475,8 @@ "1b179ab561": "复制页面网址", "f7ab83f7ed": "在默认浏览器中打开页面", "0e080d820e": "重新加载", + "a1f3c2e4b5": "硬刷新", + "b7e4d9c1a2": "停止", "250a9b3e42": "向前", "40edfa75cb": "返回", "efb0e8f7f3": "复制链接地址",