mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
Make parked browser tabs paint before automation (#4411)
This commit is contained in:
@@ -102,3 +102,17 @@ run with `--json`, including host memory, Orca app process buckets, worktree
|
||||
terminal memory, per-session process roots, and history samples. Text output
|
||||
prints a compact point-in-time summary and the top worktrees by retained
|
||||
terminal memory.
|
||||
|
||||
## Follow-up: Agent-Browser Paintability Guard
|
||||
|
||||
The browser parking fix depends on automation-visible panes staying paintable
|
||||
without activating the user's worktree. The renderer bridge previously waited
|
||||
for two animation frames before creating the automation visibility lease, so the
|
||||
paint wait happened while the parked webview was still hidden. Non-screenshot
|
||||
agent-browser commands could therefore start immediately after the lease was
|
||||
created, before React had made the hidden pane paintable.
|
||||
|
||||
The follow-up changes the order: create the automation visibility lease first,
|
||||
then wait for paint while the pane is actually visible to automation. A
|
||||
renderer-side timeout releases the lease if paint never arrives, so a hung RAF
|
||||
does not pin an inactive browser pane indefinitely.
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('browser automation visibility leases', () => {
|
||||
expect(isBrowserAutomationVisible('page-1')).toBe(false)
|
||||
})
|
||||
|
||||
it('installs a main-process bridge that waits for paint before returning a token', async () => {
|
||||
it('installs a main-process bridge that keeps the page visible while waiting for paint', async () => {
|
||||
const animationFrameCallbacks: FrameRequestCallback[] = []
|
||||
vi.stubGlobal('window', {
|
||||
requestAnimationFrame: (callback: FrameRequestCallback) => {
|
||||
@@ -50,13 +50,13 @@ describe('browser automation visibility leases', () => {
|
||||
const acquirePromise = bridge?.acquire('page-2')
|
||||
await Promise.resolve()
|
||||
|
||||
expect(isBrowserAutomationVisible('page-2')).toBe(false)
|
||||
expect(isBrowserAutomationVisible('page-2')).toBe(true)
|
||||
expect(animationFrameCallbacks).toHaveLength(1)
|
||||
|
||||
animationFrameCallbacks.shift()?.(0)
|
||||
await Promise.resolve()
|
||||
|
||||
expect(isBrowserAutomationVisible('page-2')).toBe(false)
|
||||
expect(isBrowserAutomationVisible('page-2')).toBe(true)
|
||||
expect(animationFrameCallbacks).toHaveLength(1)
|
||||
|
||||
animationFrameCallbacks.shift()?.(16)
|
||||
@@ -68,18 +68,28 @@ describe('browser automation visibility leases', () => {
|
||||
expect(isBrowserAutomationVisible('page-2')).toBe(false)
|
||||
})
|
||||
|
||||
it('does not allocate a main-process bridge lease when the paint wait hangs', async () => {
|
||||
it('releases the main-process bridge lease when the paint wait hangs', async () => {
|
||||
vi.useFakeTimers()
|
||||
vi.stubGlobal('window', {
|
||||
requestAnimationFrame: () => 1
|
||||
})
|
||||
const { isBrowserAutomationVisible } = await import('./browser-automation-visibility')
|
||||
try {
|
||||
const { isBrowserAutomationVisible } = await import('./browser-automation-visibility')
|
||||
|
||||
const bridge = window.__orcaBrowserAutomationVisibility
|
||||
expect(bridge).toBeTruthy()
|
||||
const bridge = window.__orcaBrowserAutomationVisibility
|
||||
expect(bridge).toBeTruthy()
|
||||
|
||||
void bridge?.acquire('page-hung-paint')
|
||||
await Promise.resolve()
|
||||
const acquirePromise = bridge?.acquire('page-hung-paint')
|
||||
await Promise.resolve()
|
||||
|
||||
expect(isBrowserAutomationVisible('page-hung-paint')).toBe(false)
|
||||
expect(isBrowserAutomationVisible('page-hung-paint')).toBe(true)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(2_000)
|
||||
await expect(acquirePromise).resolves.toBeNull()
|
||||
|
||||
expect(isBrowserAutomationVisible('page-hung-paint')).toBe(false)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -18,6 +18,7 @@ const listeners = new Set<() => void>()
|
||||
|
||||
let version = 0
|
||||
let nextLeaseId = 0
|
||||
const AUTOMATION_VISIBILITY_PAINT_TIMEOUT_MS = 2_000
|
||||
|
||||
function emitChange(): void {
|
||||
version += 1
|
||||
@@ -48,6 +49,25 @@ function nextAnimationFrame(): Promise<void> {
|
||||
return new Promise((resolve) => window.requestAnimationFrame(() => resolve()))
|
||||
}
|
||||
|
||||
async function waitForAutomationVisiblePaint(): Promise<boolean> {
|
||||
let timeout: ReturnType<typeof setTimeout> | null = null
|
||||
const paint = (async () => {
|
||||
await nextAnimationFrame()
|
||||
await nextAnimationFrame()
|
||||
return true
|
||||
})()
|
||||
const timedOut = new Promise<false>((resolve) => {
|
||||
timeout = setTimeout(() => resolve(false), AUTOMATION_VISIBILITY_PAINT_TIMEOUT_MS)
|
||||
})
|
||||
try {
|
||||
return await Promise.race([paint, timedOut])
|
||||
} finally {
|
||||
if (timeout !== null) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isBrowserAutomationVisible(browserPageId: string): boolean {
|
||||
return (leaseCountsByPageId.get(browserPageId) ?? 0) > 0
|
||||
}
|
||||
@@ -107,11 +127,15 @@ async function acquireForMainProcess(browserPageId: string): Promise<string | nu
|
||||
if (typeof browserPageId !== 'string' || browserPageId.length === 0) {
|
||||
return null
|
||||
}
|
||||
// Why: only allocate the lease after the paint wait succeeds. If RAF hangs,
|
||||
// main times out without leaving a permanently visible hidden webview.
|
||||
await nextAnimationFrame()
|
||||
await nextAnimationFrame()
|
||||
return acquireBrowserAutomationVisibility(browserPageId)
|
||||
const token = acquireBrowserAutomationVisibility(browserPageId)
|
||||
// Why: the hidden pane only becomes paintable after the visibility lease
|
||||
// exists. Wait after acquiring it so agent-browser commands do not race a
|
||||
// still-hidden parked webview; release locally if paint never arrives.
|
||||
if (await waitForAutomationVisiblePaint()) {
|
||||
return token
|
||||
}
|
||||
releaseBrowserAutomationVisibility(token)
|
||||
return null
|
||||
}
|
||||
|
||||
export function installBrowserAutomationVisibilityBridge(): void {
|
||||
|
||||
Reference in New Issue
Block a user