Skip to content

Commit fd1493c

Browse files
committed
fix(desktop-browser): ask only about the user's own on-screen page, never the agent's
1 parent da7decb commit fd1493c

2 files changed

Lines changed: 62 additions & 6 deletions

File tree

‎apps/desktop/e2e/browser-page-dialogs.spec.ts‎

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const FORM_FIXTURE = `<!doctype html><title>form</title>
2727
type Bridge = typeof globalThis & {
2828
simDesktop: SimDesktopApi
2929
pageDialog?: BrowserPageDialog | null
30+
boundsTimer?: number
3031
}
3132

3233
/**
@@ -79,7 +80,7 @@ test('page dialogs wait for the user on their page and stay automatic for the ag
7980
response.end(
8081
!isSite
8182
? SHELL_FIXTURE
82-
: path === '/form'
83+
: path === '/form' || path === '/agent'
8384
? FORM_FIXTURE
8485
: '<!doctype html><title>next</title>'
8586
)
@@ -117,7 +118,7 @@ test('page dialogs wait for the user on their page and stay automatic for the ag
117118
scope
118119
)
119120
updateBounds()
120-
window.setInterval(updateBounds, 200)
121+
bridge.boundsTimer = window.setInterval(updateBounds, 200)
121122
bridge.pageDialog = null
122123
api.onPageState((state) => {
123124
bridge.pageDialog = state.dialog ?? null
@@ -260,6 +261,53 @@ test('page dialogs wait for the user on their page and stay automatic for the ag
260261
await expect.poll(pageUrl).toBe(`${site}/next`)
261262
expect(await pageDialog()).toBeNull()
262263
})
264+
/** Opens a page-initiated confirm (no user gesture) on the tab at `path`. */
265+
const confirmFromPage = (path: string) =>
266+
shellApp.evaluate(({ webContents }, url) => {
267+
const contents = webContents.getAllWebContents().find((c) => c.getURL() === url)
268+
if (!contents) throw new Error(`No page at ${url}`)
269+
void contents.executeJavaScript(
270+
"setTimeout(() => { document.title = 'confirm:' + confirm('Continue?') })"
271+
)
272+
}, `${site}${path}`)
273+
const titleAt = (path: string) =>
274+
shellApp.evaluate(
275+
({ webContents }, url) =>
276+
webContents
277+
.getAllWebContents()
278+
.find((c) => c.getURL() === url)
279+
?.getTitle() ?? null,
280+
`${site}${path}`
281+
)
282+
283+
await check(
284+
"a dialog on the agent's page stays automatic until the user takes it",
285+
async () => {
286+
await execute('browser_open_tab', { url: `${site}/agent` })
287+
const tabs = await shell.evaluate(
288+
async (scope) =>
289+
(await (globalThis as Bridge).simDesktop.browserAgent.activateScope(scope)).tabs,
290+
SCOPE
291+
)
292+
const agentTabId = tabs.find((tab) => tab.url === `${site}/agent`)?.tabId
293+
// The resource strip mirrors the agent's tab on screen without the user claiming it.
294+
await panelAction({ action: 'switch-tab', tabId: agentTabId, claim: false })
295+
await confirmFromPage('/agent')
296+
await expect.poll(() => titleAt('/agent')).toBe('confirm:false')
297+
expect(await pageDialog()).toBeNull()
298+
}
299+
)
300+
301+
await check('a dialog while the browser is off screen stays automatic', async () => {
302+
await shell.evaluate((scope) => {
303+
const bridge = globalThis as Bridge
304+
window.clearInterval(bridge.boundsTimer)
305+
bridge.simDesktop.browserAgent.setPanelBounds(null, null, scope)
306+
}, SCOPE)
307+
await confirmFromPage('/agent')
308+
await expect.poll(() => titleAt('/agent')).toBe('confirm:false')
309+
expect(await pageDialog()).toBeNull()
310+
})
263311
passed = true
264312
} finally {
265313
mkdirSync(dirname(reportPath), { recursive: true })

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1881,13 +1881,18 @@ export function enablePageDialogs(): void {
18811881

18821882
/**
18831883
* Whether a dialog on this tab is the user's to answer: the renderer can show
1884-
* it, the page is the one on screen, and no agent action is driving it.
1884+
* it, the page is on screen, and it is the user's page rather than the agent's.
1885+
* Everything else keeps the shell answering, so the user is never asked about
1886+
* work they did not start and a hidden page is never left blocked.
18851887
*/
18861888
function userOwnsPageDialogs(tab: AgentTab): boolean {
18871889
const contents = tab.view.webContents
18881890
if (!currentScope.pageDialogsEnabled || contents.isDestroyed()) return false
1889-
if (tab.id !== currentScope.activeTabId || isDispatchingAgentInput(contents)) return false
1890-
return !(currentScope.automationActive && automationTab()?.id === tab.id)
1891+
if (tab.id !== currentScope.activeTabId || !isPanelVisible()) return false
1892+
if (getBrowserScopeId() !== getActiveBrowserScopeId()) return false
1893+
if (isDispatchingAgentInput(contents)) return false
1894+
if (automationTab()?.id !== tab.id) return true
1895+
return automationTabClaimedByUser() && !currentScope.automationActive
18911896
}
18921897

18931898
function holdPageDialogForUser(
@@ -3450,8 +3455,11 @@ export function switchTab(tabId: string, { claim = true }: { claim?: boolean } =
34503455
revokeTabMediaPermissions(previousActiveTab, false)
34513456
previousActiveTab.pendingUserFocus = false
34523457
}
3453-
currentScope.activeTabId = tab.id
3458+
// The claim describes the page on screen, so a mirrored switch to another
3459+
// page leaves that page unclaimed rather than inheriting the last one's.
34543460
if (claim) currentScope.visibleTabUserSelected = true
3461+
else if (currentScope.activeTabId !== tab.id) currentScope.visibleTabUserSelected = false
3462+
currentScope.activeTabId = tab.id
34553463
promotePendingTabRestore(tab)
34563464
// Visible selection does not move the automation exemption; the user may
34573465
// inspect another page while a tool continues in its background tab.

0 commit comments

Comments
 (0)