fix(stage-tamagotchi): harden overlay isolation and iframe coordinates (#1751)

---------

Co-authored-by-agent: Antigravity <antigravity@gemini.com>
This commit is contained in:
刘梓恒
2026-04-29 22:52:00 +08:00
committed by GitHub
parent 9d0719e6b5
commit 5bd0b19e84
5 changed files with 202 additions and 39 deletions
@@ -29,6 +29,11 @@ import { BrowserWindow, screen } from 'electron'
import { baseUrl, getElectronMainDirname, load, withHashRoute } from '../../libs/electron/location'
import { setupDesktopOverlayElectronInvokes } from './rpc/index.electron'
import {
applyDesktopOverlayInputIsolation,
createDesktopOverlayWindowOptions,
showDesktopOverlayWithoutFocus,
} from './window-contract'
/** Whether the desktop overlay feature is enabled */
export function isDesktopOverlayEnabled(): boolean {
@@ -59,46 +64,17 @@ export async function setupDesktopOverlayWindow(params: {
// Use primary display bounds (not just size) — the origin may be non-zero
// when multiple displays are arranged in macOS Display Preferences.
const primaryDisplay = screen.getPrimaryDisplay()
const { x, y, width, height } = primaryDisplay.bounds
const preloadPath = join(getElectronMainDirname(), '../preload/index.mjs')
overlayWindow = new BrowserWindow({
title: 'AIRI Desktop Overlay',
width,
height,
x,
y,
show: false,
frame: false,
transparent: true,
alwaysOnTop: true,
skipTaskbar: true,
hasShadow: false,
// Round corners off for pixel-accurate overlay
roundedCorners: false,
// Prevent the overlay from stealing focus
focusable: false,
webPreferences: {
preload: join(getElectronMainDirname(), '../preload/index.mjs'),
sandbox: false,
// Disable background throttling so animations stay smooth
backgroundThrottling: false,
},
})
// Make click-through: all mouse events pass through to the desktop
overlayWindow.setIgnoreMouseEvents(true, { forward: true })
// Set to screen level (above all other windows)
overlayWindow.setAlwaysOnTop(true, 'screen-saver')
// Prevent the window from appearing in screenshots/recordings if possible
overlayWindow.setContentProtection(true)
// Hide from Mission Control / Exposé on macOS
overlayWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true })
overlayWindow = new BrowserWindow(createDesktopOverlayWindowOptions({
bounds: primaryDisplay.bounds,
preloadPath,
}))
applyDesktopOverlayInputIsolation(overlayWindow)
overlayWindow.on('ready-to-show', () => {
overlayWindow?.show()
if (overlayWindow)
showDesktopOverlayWithoutFocus(overlayWindow)
})
overlayWindow.on('closed', () => {
@@ -0,0 +1,67 @@
import { describe, expect, it, vi } from 'vitest'
import {
applyDesktopOverlayInputIsolation,
createDesktopOverlayWindowOptions,
showDesktopOverlayWithoutFocus,
} from './window-contract'
describe('createDesktopOverlayWindowOptions', () => {
it('creates non-focusable transparent overlay window options for display bounds', () => {
const options = createDesktopOverlayWindowOptions({
bounds: { x: -222, y: -1080, width: 1920, height: 1080 },
preloadPath: '/tmp/airi-overlay-preload.js',
})
expect(options.title).toBe('AIRI Desktop Overlay')
expect(options.x).toBe(-222)
expect(options.y).toBe(-1080)
expect(options.width).toBe(1920)
expect(options.height).toBe(1080)
expect(options.show).toBe(false)
expect(options.frame).toBe(false)
expect(options.transparent).toBe(true)
expect(options.alwaysOnTop).toBe(true)
expect(options.skipTaskbar).toBe(true)
expect(options.hasShadow).toBe(false)
expect(options.roundedCorners).toBe(false)
expect(options.focusable).toBe(false)
expect(options.webPreferences?.preload).toBe('/tmp/airi-overlay-preload.js')
expect(options.webPreferences?.sandbox).toBe(false)
expect(options.webPreferences?.backgroundThrottling).toBe(false)
})
})
describe('applyDesktopOverlayInputIsolation', () => {
it('applies click-through and non-interactive overlay window flags', () => {
const window = {
setAlwaysOnTop: vi.fn(),
setContentProtection: vi.fn(),
setIgnoreMouseEvents: vi.fn(),
setVisibleOnAllWorkspaces: vi.fn(),
}
applyDesktopOverlayInputIsolation(window)
expect(window.setIgnoreMouseEvents).toHaveBeenCalledWith(true, { forward: true })
expect(window.setAlwaysOnTop).toHaveBeenCalledWith(true, 'screen-saver')
expect(window.setContentProtection).toHaveBeenCalledWith(true)
expect(window.setVisibleOnAllWorkspaces).toHaveBeenCalledWith(true, { visibleOnFullScreen: true })
})
})
describe('showDesktopOverlayWithoutFocus', () => {
it('uses showInactive and never calls active show or focus paths', () => {
const window = {
focus: vi.fn(),
show: vi.fn(),
showInactive: vi.fn(),
}
showDesktopOverlayWithoutFocus(window)
expect(window.showInactive).toHaveBeenCalledTimes(1)
expect(window.show).not.toHaveBeenCalled()
expect(window.focus).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,82 @@
import type { BrowserWindow, BrowserWindowConstructorOptions, Rectangle } from 'electron'
/**
* Build BrowserWindow options for the desktop grounding overlay.
*
* Use when:
* - Creating the transparent desktop overlay BrowserWindow
* - Testing overlay input-isolation without starting Electron
*
* Expects:
* - `bounds` are Electron screen logical coordinates for the display being covered
* - `preloadPath` is an absolute path to the renderer preload script
*
* Returns:
* - BrowserWindow options that keep the overlay visual-only and non-focusable
*/
export function createDesktopOverlayWindowOptions(params: {
bounds: Rectangle
preloadPath: string
}): BrowserWindowConstructorOptions {
return {
title: 'AIRI Desktop Overlay',
width: params.bounds.width,
height: params.bounds.height,
x: params.bounds.x,
y: params.bounds.y,
show: false,
frame: false,
transparent: true,
alwaysOnTop: true,
skipTaskbar: true,
hasShadow: false,
roundedCorners: false,
focusable: false,
webPreferences: {
preload: params.preloadPath,
sandbox: false,
backgroundThrottling: false,
},
}
}
/**
* Apply input-isolation flags to the desktop grounding overlay.
*
* Use when:
* - The overlay window has been created and must become click-through
* - The overlay should render above apps without stealing mouse or focus
*
* Expects:
* - The window is the dedicated desktop overlay window
*
* Returns:
* - Nothing; mutates Electron window flags in place
*/
export function applyDesktopOverlayInputIsolation(
window: Pick<BrowserWindow, 'setAlwaysOnTop' | 'setContentProtection' | 'setIgnoreMouseEvents' | 'setVisibleOnAllWorkspaces'>,
): void {
window.setIgnoreMouseEvents(true, { forward: true })
window.setAlwaysOnTop(true, 'screen-saver')
window.setContentProtection(true)
window.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true })
}
/**
* Show the overlay without activating or focusing it.
*
* Use when:
* - The overlay renderer is ready and should become visible
* - User focus must remain on the controlled application
*
* Expects:
* - The BrowserWindow supports Electron's `showInactive()`
*
* Returns:
* - Nothing; shows the window without stealing focus
*/
export function showDesktopOverlayWithoutFocus(
window: Pick<BrowserWindow, 'showInactive'>,
): void {
window.showInactive()
}
@@ -332,6 +332,36 @@ describe('chromeElementsToTargetCandidates', () => {
expect(candidates[0].bounds.y).toBe(50 + 88 + 140 + 24)
})
it('uses cumulative nested iframe offsets before converting to screen coordinates', () => {
const parentFrameOffset = { x: 320, y: 180 }
const childFrameOffset = { x: 24, y: 48 }
const nestedFrameOffset = {
x: parentFrameOffset.x + childFrameOffset.x,
y: parentFrameOffset.y + childFrameOffset.y,
}
const taggedEl = {
tag: 'button',
text: 'Nested iframe CTA',
rect: { x: 12, y: 24, w: 90, h: 32 },
_frameId: 9,
_frameOffsetX: nestedFrameOffset.x,
_frameOffsetY: nestedFrameOffset.y,
} as any
const candidates = chromeElementsToTargetCandidates(
[taggedEl],
windowBounds,
88,
0,
)
expect(candidates[0].frameId).toBe(9)
expect(candidates[0].bounds.x).toBe(100 + 320 + 24 + 12)
expect(candidates[0].bounds.y).toBe(50 + 88 + 180 + 48 + 24)
expect(candidates[0].bounds.width).toBe(90)
expect(candidates[0].bounds.height).toBe(32)
})
it('falls back to function-level frameId when _frameId is absent', () => {
const el = {
tag: 'button',
@@ -470,12 +470,14 @@ describe('desktop_click_target handler integration', () => {
it('falls back to OS click when the connected extension transport is read-only', async () => {
const sm = new RunStateManager()
const iframeAbsoluteBounds = { x: 456, y: 390, width: 90, height: 32 }
const candidate = makeCandidate({
id: 't_0',
source: 'chrome_dom',
selector: '#login-btn',
frameId: 0,
frameId: 7,
isPageContent: true,
bounds: iframeAbsoluteBounds,
})
sm.updateGroundingSnapshot(freshSnapshot([candidate]))
@@ -494,7 +496,13 @@ describe('desktop_click_target handler integration', () => {
expect(result.executionRoute).toBe('os_input')
expect(result.routeReason).toContain('does not support getClickTarget + clickAt')
expect(bridge.clickSelector).not.toHaveBeenCalled()
expect(executor.click).toHaveBeenCalledOnce()
expect(executor.click).toHaveBeenCalledWith({
x: 501,
y: 406,
button: 'left',
clickCount: 1,
})
expect(result.text).toContain('Point: (501, 406)')
})
// -----------------------------------------------------------------------