Skip to content

Commit d43568e

Browse files
committed
fix(browser): refuse a coordinate hover across a navigation
The coordinate hover captures the navigation epoch before its page probes and rejects a cross-document navigation before moving the pointer, as browser_click_at and browser_drag do. The overlay helper skips the empty registry slots earlier snapshots leave.
1 parent b03c4a3 commit d43568e

3 files changed

Lines changed: 34 additions & 2 deletions

File tree

‎apps/desktop/src/main/browser-agent/driver.test.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3783,6 +3783,36 @@ describe('credential protection', () => {
37833783
expect(cdpCalls(contents, 'Input.dispatchKeyEvent').length).toBeGreaterThan(0)
37843784
})
37853785

3786+
it('aborts a coordinate hover when a cross-document navigation lands mid-flight', async () => {
3787+
const contents = await openPage()
3788+
let navigated = false
3789+
vi.mocked(contents.executeJavaScript).mockImplementation((expression: string) => {
3790+
if (isPageCall(expression, 'describePointTarget')) return Promise.resolve({ found: true })
3791+
if (isPageCall(expression, 'readActiveElementState')) return Promise.resolve({})
3792+
if (isPageCall(expression, 'readPageActionState')) {
3793+
if (!navigated) {
3794+
navigated = true
3795+
emitContentsEvent(contents, 'did-navigate')
3796+
}
3797+
return Promise.resolve({
3798+
url: 'https://example.com/login',
3799+
title: 'Example',
3800+
focus: 'body',
3801+
mutationRevision: 0,
3802+
dialogs: [],
3803+
scroll: [0],
3804+
})
3805+
}
3806+
return Promise.resolve(undefined)
3807+
})
3808+
3809+
const result = await driver.executeTool('chat-test', 'browser_hover', { x: 40, y: 50 })
3810+
3811+
expect(result.ok).toBe(false)
3812+
expect(result.error).toMatch(/active tab or page changed/)
3813+
expect(cdpCalls(contents, 'Input.dispatchMouseEvent')).toHaveLength(0)
3814+
})
3815+
37863816
it('aborts a keypress when a cross-document navigation lands mid-flight', async () => {
37873817
const contents = await openPage()
37883818
let navigated = false

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4509,6 +4509,7 @@ async function executeToolInner(
45094509
case 'browser_hover': {
45104510
const contents = session.requireAutomationTab().view.webContents
45114511
if (params.elementId === undefined) {
4512+
const hoverNavigationEpoch = navigationEpoch(contents)
45124513
const x = requireNum(params, 'x')
45134514
const y = requireNum(params, 'y')
45144515
const path = pointerPath(params)
@@ -4518,7 +4519,7 @@ async function executeToolInner(
45184519
)
45194520
}
45204521
assertCurrentExecution()
4521-
assertActiveContents(contents)
4522+
assertActiveContents(contents, hoverNavigationEpoch)
45224523
const pointTarget = unwrapPageResult(
45234524
await execInPage(contents, describePointTarget, [x, y], false, executionDeadline)
45244525
)
@@ -4528,7 +4529,7 @@ async function executeToolInner(
45284529
const beforePage = await pageActionState(contents, true)
45294530
const beforeElement = await activeElementState(contents)
45304531
assertCurrentExecution()
4531-
assertActiveContents(contents)
4532+
assertActiveContents(contents, hoverNavigationEpoch)
45324533
await cdp.movePointer(contents, path, { x, y }, signal)
45334534
await sleep(150)
45344535
const afterElement = await activeElementState(contents)

‎apps/desktop/src/main/browser-agent/page-functions.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export function installPageHelpers(): void {
125125
const registry = window.__simAgentElements ?? []
126126
for (let id = 0; id < registry.length && controls.length < 4; id++) {
127127
const element = registry[id]
128+
if (!element) continue
128129
let inOverlay = false
129130
for (
130131
let current: Element | null = element;

0 commit comments

Comments
 (0)