# fix\(desktop\): agent preview tabs keep working while T3 is hidden · PR \#22743 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/22743)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: open (draft)

Source repository: pingdotgg/t3code

Source branch: t3/preview-background-rendering-lease

Target branch: main

Head commit: 6a3a2cd473c404b17eaaa97a472080fad23b58dc

[Discussion and reviews](https://git.cafe/pingdotgg/t3code/pull/22743)

[Commits](https://git.cafe/pingdotgg/t3code/pull/22743/commits?format=markdown)

[Files changed](https://git.cafe/pingdotgg/t3code/pull/22743/diffs?format=markdown)

## Description

---

Agent `preview_snapshot` calls time out whenever the T3 window isn't on screen, while `preview_evaluate` and clicks keep working. This has been fixed and has come back several times.

## Why it keeps coming back

Every preview tab is a `<webview>` inside the main window. To save energy, inactive tabs are parked off-screen ([#8567](https://github.com/pingdotgg/t3code/pull/8567), [#16961](https://github.com/pingdotgg/t3code/pull/16961)). A screenshot needs a freshly drawn frame, and Chromium stops drawing a page that sits outside its window. Earlier fixes added opt-in "keep drawing" leases, and each refactor that moved capture to another layer lost them. [#15328](https://github.com/pingdotgg/t3code/pull/15328) dropped the automation lease when agent automation moved to the server.

I tested this in real Electron 44.9.3 on macOS, with one fresh launch per condition, screenshots taken over CDP, and the page changed before each capture to tell fresh frames from stale ones:

| Where the agent's page lives | window visible | hidden | minimized |
|---|---|---|---|
| `<webview>` parked off-screen (today) | times out | times out | times out |
| parked, then moved back inside the window to capture | fresh | **never** | about half the time |
| kept inside the window, created while the window was visible | fresh | fresh | fresh |
| kept inside the window, **created while the window was hidden** | fresh | **never** | **never** |
| any of the above with throttling off or Chromium's no-backgrounding switches | no help | no help | no help |
| **its own never-shown `BrowserWindow`** | fresh | fresh | fresh |

Agents open tabs whenever they like, including while you're away, so no lease or placement trick on a `<webview>` can work.

## Fix

Agent-opened tabs (sessions with `automationOwner`) no longer get a `<webview>`. On desktop:

- **Desktop:** the desktop app hosts each one in a never-shown `BrowserWindow` with the same partition (your logins), preload, sandbox and passkey bridge as a panel guest (`preview/BackgroundTabWindow.ts`). The manager registers it through the same path a `<webview>` uses, so listeners, zoom, mute, the debugger and the relay attach are all unchanged. It's destroyed when the tab closes. Off macOS it also closes with the main window, so the app can still quit.
- **Web:** the panel shows these tabs through the server stream view, as web and mobile already do for server tabs.
- **Server:** the relay's `attached` event now carries `background`. The server sizes those tabs itself (viewport, zoom, viewer resize), handles their file choosers, and closes them when idle, the same as headless tabs.
- **Errors:** a timeout that comes from the browser host no longer claims the broker's "timed out after 15000ms".

Tabs you open yourself are unchanged.

Agent tabs stay on desktop rather than moving to the server's headless browser because agent tabs default to your **default profile**, and the headless browser has separate profile storage. Moving them would log agents out of sites you're signed into.

**What changes for agent tabs:** they look like server tabs do on web. That means no native DevTools, element picker or desktop recording for them in the panel. Agent recording is server-side and still works.

## Regression guard

- **New CI job, Desktop Background Preview (macOS):** runs `apps/desktop/scripts/verify-background-preview.mjs`. It launches real Electron, hides and then minimizes the app window, and creates a tab window **after** that, using the exact options the manager ships. It fails unless every screenshot returns fresh pixels. It runs when preview code, this check, the Electron pin in `apps/desktop/package.json`, or the CI files change. It fails when painting is disabled.
- **Focused tests, each confirmed to fail without the change:**
  - the manager creates and registers the hidden window and destroys it on close
  - the browser host mounts no `<webview>` and asks for a background window
  - the server sizes background desktop tabs and leaves panel tabs alone

## Verification

- Every preview test file in the desktop package passes, plus the preview IPC tests, `ServerBrowser.test.ts`, and the browser host browser test. Typecheck is clean for contracts, server, web and desktop. Changed files lint clean.
- The real-Electron check passes locally.
- **Not verified end to end in the full app.** I tried a real agent turn against `vp run dev:desktop`. To let the web UI pair, I had to switch the dev desktop to network-accessible mode, and in that mode the dev desktop didn't host *any* tab, including a human `<webview>` tab, which my change doesn't touch. The server fell back to headless and every snapshot succeeded, but that says nothing about this change. Before merging, someone should run a desktop build, minimize T3, have an agent open a tab, and take snapshots.

Opus 5.5 via Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
