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": "复制链接地址",