Skip to content

Commit cfed4d0

Browse files
committed
fix(desktop-browser): return popup focus to the user's page and wait for a shown view
1 parent 75fcb2c commit cfed4d0

4 files changed

Lines changed: 44 additions & 8 deletions

File tree

‎apps/desktop/e2e/browser-focus.spec.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,18 @@ test('browser focus and shortcuts stay with the surface the user is using', asyn
251251
await expect.poll(focusedPageUrls).toEqual([`${site}/four`])
252252
})
253253

254+
await check('a popup the agent opens hands focus back to the page the user is in', async () => {
255+
const before = await tabCount()
256+
const snapshot = await execute('browser_snapshot', {})
257+
const ref = /"Open popup" \[ref=(\d+)\]/.exec(
258+
String((snapshot as { outline?: string }).outline)
259+
)?.[1]
260+
expect(ref, 'snapshot lists the popup link').toBeTruthy()
261+
await execute('browser_click', { elementId: Number(ref) })
262+
await expect.poll(tabCount).toBe(before + 1)
263+
await expect.poll(focusedPageUrls).toEqual([`${site}/four`])
264+
})
265+
254266
await check('reload keys typed in the page reload only that page', async () => {
255267
for (const [keyCode, modifiers] of [
256268
['R', [PRIMARY]],

‎apps/desktop/src/main/browser-agent/session.ts‎

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1857,7 +1857,9 @@ export function focusPageForUser(contents: WebContents): void {
18571857
function applyPendingUserFocus(view: WebContentsView): void {
18581858
const tab = activeTab()
18591859
if (!tab?.pendingUserFocus || tab.view !== view || view.webContents.isDestroyed()) return
1860-
if (!isPanelVisible() || getBrowserScopeId() !== getActiveBrowserScopeId()) return
1860+
// A renderer modal can hide the view while the panel keeps its bounds.
1861+
if (!view.getVisible() || !isPanelVisible()) return
1862+
if (getBrowserScopeId() !== getActiveBrowserScopeId()) return
18611863
tab.pendingUserFocus = false
18621864
view.webContents.focus()
18631865
}
@@ -1880,8 +1882,9 @@ function publishPageIssue(tab: AgentTab, focusRecovery = false): void {
18801882
// Recovery moves focus to the renderer's issue page only when the failed page
18811883
// held it; the user typing in chat keeps their caret.
18821884
const pageHadFocus =
1883-
currentScope.focusedBrowserTabId === tab.id ||
1884-
(!tab.view.webContents.isDestroyed() && tab.view.webContents.isFocused())
1885+
!currentScope.browserChromeFocused &&
1886+
(currentScope.focusedBrowserTabId === tab.id ||
1887+
(!tab.view.webContents.isDestroyed() && tab.view.webContents.isFocused()))
18851888
if (
18861889
focusRecovery &&
18871890
pageHadFocus &&
@@ -2307,15 +2310,31 @@ function initializeTabView(
23072310
const tab = tabs.find((entry) => entry.view.webContents === contents)
23082311
// A tab the user cannot see never keeps keyboard focus. Chromium focuses
23092312
// a page opened without an opener (a target=_blank link) while creating
2310-
// it, before the tab is even listed. Hand focus back once that focus
2311-
// call has returned, or Chromium finishes it over the top.
2313+
// it, before the tab is even listed. Hand focus back to the visible page
2314+
// the user was in, or else to Sim, once that focus call has returned, or
2315+
// Chromium finishes it over the top.
23122316
if (!tab || tab.id !== currentScope.activeTabId) {
2317+
const active = activeTab()
2318+
const returnTo =
2319+
active &&
2320+
!currentScope.browserChromeFocused &&
2321+
currentScope.focusedBrowserTabId === active.id
2322+
? active
2323+
: null
23132324
setImmediate(
23142325
bindToBrowserScope(scopeId, () => {
23152326
const current = tabs.find((entry) => entry.view.webContents === contents)
23162327
const win = panelWindow()
23172328
if (current?.id === currentScope.activeTabId || !win || win.isDestroyed()) return
2318-
if (!contents.isDestroyed() && contents.isFocused()) win.webContents.focus()
2329+
if (contents.isDestroyed() || !contents.isFocused()) return
2330+
if (
2331+
returnTo?.id === currentScope.activeTabId &&
2332+
!returnTo.view.webContents.isDestroyed()
2333+
) {
2334+
returnTo.view.webContents.focus()
2335+
} else {
2336+
win.webContents.focus()
2337+
}
23192338
})
23202339
)
23212340
return

‎apps/desktop/src/test/electron-mock.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,11 @@ function createWebContentsMock() {
250250
export class WebContentsView {
251251
webContents = createWebContentsMock()
252252
setBackgroundColor = vi.fn()
253-
setVisible = vi.fn()
253+
private visible = true
254+
setVisible = vi.fn((visible: boolean) => {
255+
this.visible = visible
256+
})
257+
getVisible = vi.fn(() => this.visible)
254258
private bounds = { x: 0, y: 0, width: 0, height: 0 }
255259
setBounds = vi.fn((bounds: { x: number; y: number; width: number; height: number }) => {
256260
this.bounds = { ...bounds }

‎apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-page-issue.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@ export function browserPageIssueCopy(issue: BrowserPageIssue): BrowserPageIssueC
127127

128128
function isEditingOutside(heading: HTMLElement | null): boolean {
129129
const active = document.activeElement
130-
if (!(active instanceof HTMLElement) || !document.hasFocus()) return false
130+
// activeElement survives a window blur, so a caret left in chat still counts.
131+
if (!(active instanceof HTMLElement)) return false
131132
const section = heading?.closest('section')
132133
if (section?.contains(active)) return false
133134
return (

0 commit comments

Comments
 (0)