refactor(browser): split the headless page book out of the offscreen backend

The backend crossed the 300-line cap, and a max-lines suppression is not an
option here. The split follows the distinction the fix is built on: a headless
page and the renderer behind it have different lifetimes. OffscreenBrowserOpenPages
is the book of open pages; the backend owns renderers. "Parked" is exactly a
record in the book with no window attached, so the queries the runtime needs —
which pages are parked, which is most recently used — belong with the records
rather than threaded through renderer bookkeeping.

Also fixes an existing spec my first commit broke: the implicit-target activity
touch calls getActivePageId, which that test's partial bridge stub did not
have. Caught here because a background full-suite run launched mid-edit had
raced the files it was reading — the pass it reported was not of this tree.
This commit is contained in:
Jinwoo-H
2026-08-19 19:07:11 -04:00
parent 2bba8e17f5
commit 9f77906067
3 changed files with 117 additions and 66 deletions
+22 -66
View File
@@ -1,5 +1,4 @@
import { randomUUID } from 'node:crypto'
import type { BrowserWindow } from 'electron'
import { ORCA_BROWSER_PARTITION } from '../../shared/constants'
import type { AgentBrowserBridge } from './agent-browser-bridge'
import type { BrowserBackend, BrowserBackendCreateTab, ParkedBrowserPage } from './browser-backend'
@@ -11,6 +10,10 @@ import {
selectOffscreenBrowserPagesToPark,
type OffscreenBrowserReclaimPolicy
} from './offscreen-browser-page-reclaim'
import {
OffscreenBrowserOpenPages,
type OffscreenBrowserPage
} from './offscreen-browser-open-pages'
import { createOffscreenBrowserWindow, loadOffscreenBrowserUrl } from './offscreen-browser-window'
// Why (STA-4341): this backend is the lifecycle owner for headless browser
@@ -19,22 +22,6 @@ import { createOffscreenBrowserWindow, loadOffscreenBrowserUrl } from './offscre
// process behind it as a reclaimable resource: an idle page is parked (renderer
// destroyed, record kept) and woken on the next command that targets it.
type OffscreenPage = {
browserPageId: string
worktreeId?: string
profileId?: string
partition: string
url: string
title: string
/** null while parked — the page exists, its renderer does not. */
window: BrowserWindow | null
/** Whether the page was its worktree's active tab when it parked. */
activeWhenParked: boolean
/** True while the initial or post-wake navigation is still in flight. */
loading: boolean
lastActivityAt: number
}
export type OffscreenBrowserBackendOptions = {
getAgentBrowserBridge?: () => AgentBrowserBridge | null
/** Pages a client is streaming or that have a command in flight. */
@@ -43,7 +30,7 @@ export type OffscreenBrowserBackendOptions = {
}
export class OffscreenBrowserBackend implements BrowserBackend {
private readonly pages = new Map<string, OffscreenPage>()
private readonly pages = new OffscreenBrowserOpenPages()
/** Renderer teardowns this backend initiated, keyed by page. Park and close
* both destroy the window on purpose, so the crash handler stands down for
* them — and a wake waits on one rather than racing it. */
@@ -71,7 +58,7 @@ export class OffscreenBrowserBackend implements BrowserBackend {
? browserSessionRegistry.getProfile(params.profileId)
: browserSessionRegistry.getDefaultProfile()
const url = params.url || 'about:blank'
const page: OffscreenPage = {
const page: OffscreenBrowserPage = {
browserPageId,
worktreeId: params.worktreeId,
profileId: profile?.id ?? undefined,
@@ -83,7 +70,7 @@ export class OffscreenBrowserBackend implements BrowserBackend {
loading: false,
lastActivityAt: this.now()
}
this.pages.set(browserPageId, page)
this.pages.add(page)
// Why: register the guest and return immediately so the new tab appears
// without waiting for the page to finish loading. A failed load leaves the
// (usable) tab open, matching how a normal browser tab survives one.
@@ -99,10 +86,9 @@ export class OffscreenBrowserBackend implements BrowserBackend {
}
async closeTab(browserPageId: string): Promise<void> {
const page = this.pages.get(browserPageId)
this.pages.delete(browserPageId)
const page = this.pages.delete(browserPageId) ?? null
this.waking.delete(browserPageId)
await this.releaseRenderer(page ?? null, browserPageId)
await this.releaseRenderer(page, browserPageId)
if (this.pages.size === 0) {
this.stopSweep()
}
@@ -162,47 +148,18 @@ export class OffscreenBrowserBackend implements BrowserBackend {
}
listParkedPages(worktreeId?: string): ParkedBrowserPage[] {
const parked: ParkedBrowserPage[] = []
for (const page of this.pages.values()) {
if (page.window && !page.window.isDestroyed()) {
continue
}
if (worktreeId && page.worktreeId !== worktreeId) {
continue
}
parked.push({
browserPageId: page.browserPageId,
worktreeId: page.worktreeId,
profileId: page.profileId,
url: page.url,
title: page.title,
active: page.activeWhenParked
})
}
return parked
return this.pages.listParked(worktreeId)
}
/** Most-recently-used parked page in a worktree, for implicit targeting. */
getMostRecentlyUsedParkedPageId(worktreeId?: string): string | null {
let best: OffscreenPage | null = null
for (const page of this.pages.values()) {
if (page.window && !page.window.isDestroyed()) {
continue
}
if (worktreeId && page.worktreeId !== worktreeId) {
continue
}
if (!best || page.lastActivityAt > best.lastActivityAt) {
best = page
}
}
return best?.browserPageId ?? null
return this.pages.mostRecentlyUsedParkedId(worktreeId)
}
destroyAll(): void {
this.stopSweep()
for (const [pageId, page] of this.pages) {
this.browserManager.unregisterGuest(pageId)
for (const page of this.pages.all()) {
this.browserManager.unregisterGuest(page.browserPageId)
if (page.window && !page.window.isDestroyed()) {
page.window.destroy()
}
@@ -214,9 +171,7 @@ export class OffscreenBrowserBackend implements BrowserBackend {
/** Park every page the policy no longer wants resident. Exposed for tests. */
async reclaimIdlePages(): Promise<string[]> {
const now = this.now()
const resident = [...this.pages.values()].filter(
(page) => page.window && !page.window.isDestroyed() && !this.releasing.has(page.browserPageId)
)
const resident = this.pages.resident().filter((page) => !this.releasing.has(page.browserPageId))
const doomed = selectOffscreenBrowserPagesToPark(
resident.map((page) => ({
browserPageId: page.browserPageId,
@@ -239,9 +194,7 @@ export class OffscreenBrowserBackend implements BrowserBackend {
// agent that opens pages forever and closes none would still grow the backend
// without limit, so the oldest parked pages are eventually closed outright.
private async closeOverRetainedPages(): Promise<void> {
const parked = [...this.pages.values()].filter(
(page) => !page.window || page.window.isDestroyed()
)
const parked = this.pages.parked()
const doomed = selectOffscreenBrowserPagesToClose(
parked.map((page) => ({
browserPageId: page.browserPageId,
@@ -286,7 +239,10 @@ export class OffscreenBrowserBackend implements BrowserBackend {
// Why: teardown order matters — the bridge must destroy the helper session and
// detach its debugger while the WebContents is still alive and still mapped,
// or the session, its CDP proxy and its listening port outlive the page.
private async releaseRenderer(page: OffscreenPage | null, browserPageId: string): Promise<void> {
private async releaseRenderer(
page: OffscreenBrowserPage | null,
browserPageId: string
): Promise<void> {
const pending = this.releasing.get(browserPageId)
if (pending) {
await pending
@@ -304,7 +260,7 @@ export class OffscreenBrowserBackend implements BrowserBackend {
}
private async runReleaseRenderer(
page: OffscreenPage | null,
page: OffscreenBrowserPage | null,
browserPageId: string
): Promise<void> {
const bridge = this.options.getAgentBrowserBridge?.() ?? null
@@ -318,7 +274,7 @@ export class OffscreenBrowserBackend implements BrowserBackend {
}
}
private materialize(page: OffscreenPage): void {
private materialize(page: OffscreenBrowserPage): void {
const win = createOffscreenBrowserWindow(page.partition)
page.window = win
// Why: reading win.webContents once the contents are destroyed throws, and
@@ -355,7 +311,7 @@ export class OffscreenBrowserBackend implements BrowserBackend {
})
}
private async loadPage(page: OffscreenPage, url: string): Promise<void> {
private async loadPage(page: OffscreenBrowserPage, url: string): Promise<void> {
const win = page.window
if (!win || win.isDestroyed()) {
return
@@ -0,0 +1,94 @@
import type { BrowserWindow } from 'electron'
import type { ParkedBrowserPage } from './browser-backend'
// Why (STA-4341): a headless browser page and the renderer behind it have
// different lifetimes — the page is open until it is closed, the renderer only
// while something needs it. This is the book of open pages; the backend owns
// the renderers. Keeping them apart is what makes "parked" expressible: a
// record here with no window attached.
export type OffscreenBrowserPage = {
browserPageId: string
worktreeId?: string
profileId?: string
partition: string
url: string
title: string
/** null while parked — the page exists, its renderer does not. */
window: BrowserWindow | null
/** Whether the page was its worktree's active tab when it parked. */
activeWhenParked: boolean
/** True while the initial or post-wake navigation is still in flight. */
loading: boolean
lastActivityAt: number
}
function isResident(page: OffscreenBrowserPage): boolean {
return Boolean(page.window && !page.window.isDestroyed())
}
export class OffscreenBrowserOpenPages {
private readonly pages = new Map<string, OffscreenBrowserPage>()
get size(): number {
return this.pages.size
}
has(browserPageId: string): boolean {
return this.pages.has(browserPageId)
}
get(browserPageId: string): OffscreenBrowserPage | undefined {
return this.pages.get(browserPageId)
}
add(page: OffscreenBrowserPage): void {
this.pages.set(page.browserPageId, page)
}
delete(browserPageId: string): OffscreenBrowserPage | undefined {
const page = this.pages.get(browserPageId)
this.pages.delete(browserPageId)
return page
}
clear(): void {
this.pages.clear()
}
all(): OffscreenBrowserPage[] {
return [...this.pages.values()]
}
resident(): OffscreenBrowserPage[] {
return this.all().filter(isResident)
}
parked(worktreeId?: string): OffscreenBrowserPage[] {
return this.all().filter(
(page) => !isResident(page) && (!worktreeId || page.worktreeId === worktreeId)
)
}
listParked(worktreeId?: string): ParkedBrowserPage[] {
return this.parked(worktreeId).map((page) => ({
browserPageId: page.browserPageId,
worktreeId: page.worktreeId,
profileId: page.profileId,
url: page.url,
title: page.title,
active: page.activeWhenParked
}))
}
/** Most-recently-used parked page, for commands that name a worktree. */
mostRecentlyUsedParkedId(worktreeId?: string): string | null {
let best: OffscreenBrowserPage | null = null
for (const page of this.parked(worktreeId)) {
if (!best || page.lastActivityAt > best.lastActivityAt) {
best = page
}
}
return best?.browserPageId ?? null
}
}
@@ -858,6 +858,7 @@ describe('RuntimeBrowserCommands headless offscreen routing', () => {
const keyboardInsertText = vi.fn().mockResolvedValue({ inserted: true })
const bridge = {
getRegisteredTabs: vi.fn(() => new Map([['page-1', 100]])),
getActivePageId: vi.fn(() => 'page-1'),
keyboardInsertText
} as unknown as AgentBrowserBridge
const commands = new RuntimeBrowserCommands(