From 9d225263e1ecef65331c74df4c32c215134b5731 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 29 Sep 2026 17:52:57 -0700 Subject: [PATCH 1/3] fix(desktop-browser): keep error-page focus recovery inside the browser and route hard reload through recovery --- .../desktop/src/main/browser-agent/session.ts | 20 ++++++++--- .../browser-session/browser-find-bar.tsx | 2 +- .../browser-session/browser-page-issue.tsx | 36 ++++++++++--------- .../browser-session/browser-session.tsx | 3 +- 4 files changed, 39 insertions(+), 22 deletions(-) diff --git a/apps/desktop/src/main/browser-agent/session.ts b/apps/desktop/src/main/browser-agent/session.ts index 6f494b979f8..1fd6837f0e9 100644 --- a/apps/desktop/src/main/browser-agent/session.ts +++ b/apps/desktop/src/main/browser-agent/session.ts @@ -2012,6 +2012,20 @@ export function reloadPage(contents: WebContents): void { contents.reload() } +/** + * Reloads past the HTTP cache, the browser's Shift-reload. A page showing an + * issue takes the same recovery as a plain reload instead: a hung renderer + * must be restarted, since reloading it in place waits on the hung page. + */ +function hardReloadPage(contents: WebContents): void { + if (tabForContents(contents)?.pageIssue) { + reloadPage(contents) + return + } + prepareExplicitNavigation(contents) + contents.reloadIgnoringCache() +} + /** Hands one page selection to the exact app window and chat hosting its tab. */ function addPageSelectionToChat(contents: WebContents, text: string): void { if (!text.trim() || getBrowserScopeId() !== getActiveBrowserScopeId()) return @@ -2494,8 +2508,7 @@ function initializeTabView( return } if (shortcut === 'hard-reload') { - prepareExplicitNavigation(contents) - contents.reloadIgnoringCache() + hardReloadPage(contents) return } @@ -3512,8 +3525,7 @@ export function handleFocusedShortcut( reloadPage(shortcutTab.view.webContents) return true case 'hard-reload': - prepareExplicitNavigation(shortcutTab.view.webContents) - shortcutTab.view.webContents.reloadIgnoringCache() + hardReloadPage(shortcutTab.view.webContents) return true case 'back': goBack(shortcutTab.view.webContents) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-find-bar.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-find-bar.tsx index 5827b5d2013..8a1f2bc6539 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-find-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-find-bar.tsx @@ -142,7 +142,7 @@ export function BrowserFindBar({ inputRef, onClose, scopeId }: BrowserFindBarPro // The panel and the global command layer both listen for these. event.stopPropagation() // Keys during an IME composition edit the composed text. - if (event.nativeEvent.isComposing) return + if (event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) return if (event.key === 'Escape') { event.preventDefault() dismiss() diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-page-issue.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-page-issue.tsx index e1a5bc9d603..49d7e91fcf6 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-page-issue.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-page-issue.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react' +import { type RefObject, useEffect, useRef } from 'react' import type { BrowserPageIssue } from '@sim/browser-protocol' import { Chip } from '@sim/emcn' import { CircleAlert, Globe, RefreshCw } from '@sim/emcn/icons' @@ -7,6 +7,8 @@ interface BrowserPageIssueProps { issue: BrowserPageIssue onReload: () => void focusRecovery: boolean + /** The browser panel; focus anywhere else in Sim is never taken for recovery. */ + panelRef: RefObject } interface BrowserPageIssueCopy { @@ -125,29 +127,31 @@ export function browserPageIssueCopy(issue: BrowserPageIssue): BrowserPageIssueC } } -function isEditingOutside(heading: HTMLElement | null): boolean { +/** + * Whether the user's focus is somewhere in Sim other than the browser. + * `document.activeElement` survives a window blur, so a caret left in chat + * still counts as elsewhere. + */ +function isFocusElsewhere(panel: HTMLElement | null): boolean { const active = document.activeElement - // activeElement survives a window blur, so a caret left in chat still counts. - if (!(active instanceof HTMLElement)) return false - const section = heading?.closest('section') - if (section?.contains(active)) return false - return ( - active.isContentEditable || - active instanceof HTMLInputElement || - active instanceof HTMLTextAreaElement - ) + if (!(active instanceof HTMLElement) || active === document.body) return false + return !panel?.contains(active) } /** Replaces a hidden native page and optionally claims renderer focus for keyboard recovery. */ -export function BrowserPageIssueView({ issue, onReload, focusRecovery }: BrowserPageIssueProps) { +export function BrowserPageIssueView({ + issue, + onReload, + focusRecovery, + panelRef, +}: BrowserPageIssueProps) { const headingRef = useRef(null) const copy = browserPageIssueCopy(issue) useEffect(() => { - // Keyboard recovery for someone who was in the page; a caret in chat or - // any other Sim field stays where it is. - if (focusRecovery && !isEditingOutside(headingRef.current)) headingRef.current?.focus() - }, [focusRecovery, issue]) + // Keyboard recovery for someone who was in the browser. + if (focusRecovery && !isFocusElsewhere(panelRef.current)) headingRef.current?.focus() + }, [focusRecovery, issue, panelRef]) const Icon = issue.kind === 'load-error' ? Globe : CircleAlert diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-session.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-session.tsx index 94ce6a6d3a3..760d541112e 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-session.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-session.tsx @@ -1157,7 +1157,7 @@ export function BrowserSession({ onKeyDown={(event) => { event.stopPropagation() // Keys during an IME composition edit the composed text, not the URL. - if (event.nativeEvent.isComposing) return + if (event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) return if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { // Never move a highlight through a list that is not on screen. if (!suggestionsOpen) return @@ -1359,6 +1359,7 @@ export function BrowserSession({ sendBrowserPanelAction('reload', {}, scopeId)} /> )} From a067e41814a94c837fd30103e7c4b9a26ddffdcc Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 29 Sep 2026 18:03:58 -0700 Subject: [PATCH 2/3] fix(desktop-browser): recover issue-page focus only when nothing holds focus, and keep cache bypass except for hung pages --- .../desktop/src/main/browser-agent/session.ts | 8 +++--- .../browser-session/browser-page-issue.tsx | 27 +++++++------------ .../browser-session/browser-session.tsx | 1 - 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/apps/desktop/src/main/browser-agent/session.ts b/apps/desktop/src/main/browser-agent/session.ts index 1fd6837f0e9..2449d4f28f9 100644 --- a/apps/desktop/src/main/browser-agent/session.ts +++ b/apps/desktop/src/main/browser-agent/session.ts @@ -2013,12 +2013,12 @@ export function reloadPage(contents: WebContents): void { } /** - * Reloads past the HTTP cache, the browser's Shift-reload. A page showing an - * issue takes the same recovery as a plain reload instead: a hung renderer - * must be restarted, since reloading it in place waits on the hung page. + * Reloads past the HTTP cache, the browser's Shift-reload. A hung page takes + * the plain reload's recovery instead: its renderer must be restarted, since + * reloading it in place waits on the hung page. */ function hardReloadPage(contents: WebContents): void { - if (tabForContents(contents)?.pageIssue) { + if (tabForContents(contents)?.pageIssue?.kind === 'unresponsive') { reloadPage(contents) return } diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-page-issue.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-page-issue.tsx index 49d7e91fcf6..d59a858afd0 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-page-issue.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-page-issue.tsx @@ -1,4 +1,4 @@ -import { type RefObject, useEffect, useRef } from 'react' +import { useEffect, useRef } from 'react' import type { BrowserPageIssue } from '@sim/browser-protocol' import { Chip } from '@sim/emcn' import { CircleAlert, Globe, RefreshCw } from '@sim/emcn/icons' @@ -7,8 +7,6 @@ interface BrowserPageIssueProps { issue: BrowserPageIssue onReload: () => void focusRecovery: boolean - /** The browser panel; focus anywhere else in Sim is never taken for recovery. */ - panelRef: RefObject } interface BrowserPageIssueCopy { @@ -128,30 +126,25 @@ export function browserPageIssueCopy(issue: BrowserPageIssue): BrowserPageIssueC } /** - * Whether the user's focus is somewhere in Sim other than the browser. - * `document.activeElement` survives a window blur, so a caret left in chat - * still counts as elsewhere. + * Whether nothing in Sim holds focus, as when it was in the native page that + * just failed. Any focused element, from the chat composer to the omnibox, + * keeps it. `document.activeElement` survives a window blur, so a caret left + * in chat still counts. */ -function isFocusElsewhere(panel: HTMLElement | null): boolean { +function isFocusFree(): boolean { const active = document.activeElement - if (!(active instanceof HTMLElement) || active === document.body) return false - return !panel?.contains(active) + return !active || active === document.body } /** Replaces a hidden native page and optionally claims renderer focus for keyboard recovery. */ -export function BrowserPageIssueView({ - issue, - onReload, - focusRecovery, - panelRef, -}: BrowserPageIssueProps) { +export function BrowserPageIssueView({ issue, onReload, focusRecovery }: BrowserPageIssueProps) { const headingRef = useRef(null) const copy = browserPageIssueCopy(issue) useEffect(() => { // Keyboard recovery for someone who was in the browser. - if (focusRecovery && !isFocusElsewhere(panelRef.current)) headingRef.current?.focus() - }, [focusRecovery, issue, panelRef]) + if (focusRecovery && isFocusFree()) headingRef.current?.focus() + }, [focusRecovery, issue]) const Icon = issue.kind === 'load-error' ? Globe : CircleAlert diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-session.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-session.tsx index 760d541112e..fa9fc4b5bd2 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-session.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-session.tsx @@ -1359,7 +1359,6 @@ export function BrowserSession({ sendBrowserPanelAction('reload', {}, scopeId)} /> )} From 8d079918a96f3976da0ad46caef4ea6e677e25e2 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 29 Sep 2026 18:11:11 -0700 Subject: [PATCH 3/3] fix(desktop-browser): retry the failed URL when hard reloading a load error --- apps/desktop/src/main/browser-agent/session.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/main/browser-agent/session.ts b/apps/desktop/src/main/browser-agent/session.ts index 2449d4f28f9..abbefea8537 100644 --- a/apps/desktop/src/main/browser-agent/session.ts +++ b/apps/desktop/src/main/browser-agent/session.ts @@ -2013,12 +2013,14 @@ export function reloadPage(contents: WebContents): void { } /** - * Reloads past the HTTP cache, the browser's Shift-reload. A hung page takes - * the plain reload's recovery instead: its renderer must be restarted, since - * reloading it in place waits on the hung page. + * Reloads past the HTTP cache, the browser's Shift-reload. A failed or hung + * page takes the plain reload's recovery instead: a load error retries the + * URL that failed, which need not be the committed page, and a hung renderer + * must be restarted, since reloading it in place waits on the hung page. */ function hardReloadPage(contents: WebContents): void { - if (tabForContents(contents)?.pageIssue?.kind === 'unresponsive') { + const issue = tabForContents(contents)?.pageIssue + if (issue?.kind === 'load-error' || issue?.kind === 'unresponsive') { reloadPage(contents) return }