Bridge extension never closes its owned container windows, leaving orphaned about:blank windows
- Dominant language
- JavaScript
- Stars
- 29.5k
- Forks
- 2.9k
- Avg merge
- 15h 36m
- Merged PRs (30d)
- 70
Description
## Environment
- `@jackwener/opencli` 1.8.6
- Browser Bridge extension 1.0.22
- macOS, Chromium-based browser (ego lite)
## Summary
Every OpenCLI session leaves behind an empty `about:blank` browser window that is never reclaimed. Users see a permanently growing set of blank windows in Mission Control / the window list, and there is no supported way to close them from the CLI.
The extension creates "owned container" windows but **never removes them**: `grep -c 'chrome.windows.remove' dist/background.js` → `0`.
## Root cause
`releaseLease()` — when no other owned lease remains on the container window, the last tab is *not* closed. It is navigated back to `about:blank` and kept as a reusable placeholder:
```js
// else branch of releaseLease()
const tab = await chrome.tabs.update(tabId, { url: BLANK_PAGE, active: true });
const group = await ensureOwnedContainerGroup(getOwnedWindowRole(leaseKey), session.windowId, [tab.id ?? tabId]);
```
Because the window always has that placeholder tab, Chromium never auto-closes it, and nothing else ever calls `chrome.windows.remove`.
## Orphan accumulation (the worse half)
`ownedContainers` tracks only the two *currently canonical* windows (`interactive`, `automation`) — it is not a bound on how many owned windows can exist:
- `readRegistry()` and `writeRegistry()` swallow errors and carry no revision/CAS, so registry state can be silently lost.
- `reconcileTargetLeaseRegistry()` only checks `chrome.windows.get(windowId)` ("does this id still exist"), never that the window is still an OpenCLI container. On failure it nulls the in-memory reference **without searching for and removing the old window**.
- Recovery is asymmetric: `collectOwnedGroupCandidates("automation")` returns `[]` immediately, so an `automation` container that loses its registry reference **can never be rediscovered**. The next adapter command creates a fresh one and the old window becomes a permanent orphan. (`interactive` at least self-heals by querying for the `"OpenCLI Browser"` tab group title.)
- `convergeOwnedGroupDuplicates()` only merges tab *groups* — it moves tabs into the canonical window but never removes the duplicate window.
## `opencli doctor` makes this worse
`doctor.js` `checkConnectivity()` hardcodes `surface: 'browser'`:
```js
const page = await bridge.connect({ timeout: timeoutSeconds, session: DOCTOR_SESSION, surface: 'browser' });
```
Via `getSurfaceFromKey` → `getOwnedWindowRole` → `getWindowMode`, `interactive` defaults to `foreground`. So a plain health check **steals focus with a popup window** and permanently leaves an `about:blank` window tagged with the orange `OpenCLI Browser` group. `opencli doctor --help` exposes only `-v/--verbose`, so there is no way to ask for background.
(`OPENCLI_WINDOW=background opencli doctor` does suppress the focus steal via the `sendCommandRaw` env fallback, but the window is still created and still kept.)
## Workarounds that do not work
| Attempt | Result |
|---|---|
| `opencli browser close` | `close-window` → `handleCloseWindow` → `releaseLease` only. Window stays. |
| `opencli browser tab close ` | Tab closes, window stays (the placeholder tab is still there). |
| `opencli browser tab list` | Never lists the placeholder tab, so it cannot be targeted. |
| CDP `Target.closeTarget` / `Page.close` / `Browser.close` | All rejected by `CDP_ALLOWLIST`. (`Browser.close` would kill the whole browser anyway.) |
| `exec` + `window.close()` | Container is created by `chrome.windows.create`, not `window.open`, so it is not reliably script-closable after the placeholder has been navigated. |
## Suggested fix
Preferred: **add a first-class windowless `probe`/`health` action** so `doctor` can verify the daemon → extension round trip without resolving a tab or creating a lease. This is demonstrably possible today — the existing `cookies` action already does a full round trip with no window:
```bash
curl -sS -H 'X-OpenCLI: 1' -H 'Content-Type: application/json' \
--data '{"id":"probe-1","action":"cookies","session":"health-probe","surface":"browser","domain":"opencli-probe.invalid","timeout":10,"deadlineAt":}' \
http://127.0.0.1:19825/command
# → {"id":"probe-1","ok":true,"data":[]} and the window count does not change
```
Smaller, independent improvements:
1. Make `doctor` default to `windowMode: "background"` (one line, no new flag) so a health check never steals focus.
2. Give `close-window` an explicit opt-in such as `destroyContainerIfEmpty`, so callers that know they are done can have the container torn down — without changing the default warm-container reuse for everyone.
3. Give `automation` a discovery mechanism symmetric to `interactive` (e.g. a marker so orphaned automation windows can be found and reclaimed after registry loss), and/or have `reconcileTargetLeaseRegistry()` verify ownership rather than mere window-id existence.
Happy to send a PR for (1) if that direction is acceptable.
Contributor guide
Research direction
Start with releaseLease(), owned-container reconciliation and discovery in dist/background.js, then inspect checkConnectivity() in doctor.js. Reproduce with opencli doctor and the provided cookies probe while checking browser window counts. Done should mean the agreed fix prevents unintended orphaned windows without breaking the stated container behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- cli, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100