fix: open browser popup links in managed tabs

Links requesting a new window were silently blocked. Open them in managed browser tabs with the existing session restrictions, clean up script-closed tabs, and follow click-created tabs in subsequent direct and MCP browser tools.

Validation: 232 targeted tests and Windows Electron E2E passed; typecheck and lint passed. The full suite retains six failures outside the popup path.

Release note: Browser links that open a new page now open in a managed tab, and AI browser actions follow the new page after a click.
This commit is contained in:
mesalogo
2026-09-15 23:55:52 +08:00
parent 625182c3dc
commit f1d7197480
11 changed files with 450 additions and 20 deletions
+78
View File
@@ -7,6 +7,7 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
import type { KnowledgeService } from '../src/main/knowledge/knowledge-service'
import { KnowledgeMcpGateway } from '../src/main/agent/knowledge-mcp-gateway'
import { BrowserService } from '../src/main/browser/browser-service'
import { BrowserModelTools } from '../src/main/browser/browser-model-tools'
import type { BrowserLiveState } from '../src/shared/contracts'
const conversationId = 'browser-tabs-electron-e2e'
@@ -31,6 +32,10 @@ async function main(): Promise<void> {
response.end(page('Cookie page', '<p id="cookie"></p><script>document.querySelector("#cookie").textContent = document.cookie</script>'))
return
}
if (request.url === '/popups') {
response.end(page('Popup opener', '<a target="_blank" href="/popup-target">Open target</a><button onclick="window.child=window.open(\'/popup-script\')">Open script</button><button onclick="window.child=window.open();child.location=\'/popup-blank\'">Open blank</button>'))
return
}
response.end(page(request.url === '/mcp' ? 'MCP target' : request.url === '/second' ? 'Second tab' : 'First tab'))
})
await new Promise<void>((resolve, reject) => {
@@ -241,6 +246,79 @@ async function main(): Promise<void> {
await browser.closeTab(conversationId, second.tabId, window.webContents.id)
assert.equal(browser.getTabCount(conversationId), 0)
console.log('Real Electron browser-tab and MCP E2E passed')
await client.close()
client = undefined
const opener = await browser.createTab(conversationId, window.webContents.id)
await browser.navigate(conversationId, `${origin}/popups`, controller.signal, opener.tabId)
browser.setViewport(conversationId, bounds, opener.tabId, 'popup-viewport', window.webContents.id)
window.show()
const openerContents = webContents.getAllWebContents().find((contents) => contents.getURL() === `${origin}/popups`)
assert(openerContents)
openerContents.focus()
await openerContents.executeJavaScript('new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))')
const popupUsage = browser.acquireTabUsage(conversationId, opener.tabId, 'popup-e2e')
token = gateway.grant('popup-e2e', [], controller.signal, 'none', undefined, conversationId, opener.tabId, popupUsage)
assert(token)
client = new Client({ name: 'popup-e2e', version: '1.0.0' })
await client.connect(new StreamableHTTPClientTransport(new URL(gateway.getEndpoint()!), {
requestInit: { headers: { Authorization: `Bearer ${token}` } }
}))
const snapshot = await browser.snapshot(conversationId, controller.signal, opener.tabId)
const link = snapshot.nodes.find((node) => node.name === 'Open target')
assert(link)
const clickResult = await client.callTool({ name: 'browser_click', arguments: { ref: link.ref } })
assert.notEqual(clickResult.isError, true, JSON.stringify(clickResult))
assert.match(JSON.stringify(clickResult), /openedTabId/u)
const popup = browser.listTabs(conversationId).find((tab) => tab.tabId !== opener.tabId)
assert(popup)
assert.equal(popup.workbarInstanceId, popup.tabId)
const popupSnapshot = await client.callTool({ name: 'browser_snapshot', arguments: {} })
assert.notEqual(popupSnapshot.isError, true, JSON.stringify(popupSnapshot))
assert.match(JSON.stringify(popupSnapshot), /popup-target/u)
assert.equal(browser.listTabs(conversationId).find((tab) => tab.tabId === opener.tabId)?.url, `${origin}/popups`)
const popupContents = webContents.getAllWebContents().find((contents) => contents.getURL() === `${origin}/popup-target`)
assert(popupContents)
assert.equal(await popupContents.executeJavaScript('typeof process'), 'undefined')
const popupDestroyed = once(popupContents, 'destroyed', { signal: AbortSignal.timeout(5_000) })
await popupContents.executeJavaScript('window.close()').catch(() => undefined)
await popupDestroyed
assert.equal(browser.getTabCount(conversationId), 1)
assert.equal((await client.callTool({ name: 'browser_snapshot', arguments: {} })).isError, true)
for (const path of ['/popup-script', '/popup-blank']) {
const navigation = new Promise<void>((resolve) => {
const remove = browser.onState((state) => {
if (state.url === `${origin}${path}`) { remove(); resolve() }
})
})
await openerContents.executeJavaScript(path === '/popup-blank'
? `window.child=window.open();child.location='${path}';void 0`
: `window.child=window.open('${path}');void 0`)
await navigation
assert.equal(await openerContents.executeJavaScript('child !== null && !child.closed'), true)
const child = webContents.getAllWebContents().find((contents) => contents.getURL() === `${origin}${path}`)
assert(child)
const destroyedChild = once(child, 'destroyed', { signal: AbortSignal.timeout(5_000) })
await openerContents.executeJavaScript('child.close()')
await destroyedChild
assert.equal(browser.getTabCount(conversationId), 1)
}
assert.equal(await openerContents.executeJavaScript('window.open("file:///unsupported") === null'), true)
assert.equal(browser.getTabCount(conversationId), 1)
const directTools = new BrowserModelTools({ service: browser, conversationId, browserTabId: opener.tabId, recoverClosedTab: true })
const directSnapshot = await browser.snapshot(conversationId, controller.signal, opener.tabId)
const button = directSnapshot.nodes.find((node) => node.name === 'Open script' && node.role === 'button')
assert(button)
assert.match(JSON.stringify(await directTools.callTool('browser_click', { ref: button.ref }, controller.signal)), /openedTabId/u)
assert.match(JSON.stringify(await directTools.callTool('browser_snapshot', {}, controller.signal)), /popup-script/u)
const childContents = webContents.getAllWebContents().find((contents) => contents.getURL() === `${origin}/popup-script`)
assert(childContents)
assert.equal(childContents.session, openerContents.session)
await browser.closeTab(conversationId, opener.tabId)
assert.equal(childContents.isDestroyed(), false)
assert.match(JSON.stringify(await directTools.callTool('browser_snapshot', {}, controller.signal)), /popup-script/u)
await browser.releaseConversation(conversationId)
console.log('Native target=_blank, window.open(url), blank-then-location, script close, opener close, shared partition, URL denial and direct/MCP popup binding passed')
} finally {
if (token) gateway.revoke(token)
await client?.close().catch(() => undefined)