Repository navigation
WebUI workspace browser tab model — specification (the panel as a browser) #199
Description
Parent specification for the workspace browser tab model programme — not an executable work item.
The executable work items are the tickets listed under Ticket board at the bottom of
this issue. Each ticket cites this issue as its parent.
Decision authority: ADR 0022
in the repository (packages/webui/docs/adr/0022-…md), and
ADR 0019 amendment A8
for the change to the panel's role. ADR 0022 is the decision record for this feature; this issue
and the tickets must not restate or reopen its decisions.
Development plan: .plan/workspace-browser-tab/workspace-browser-tab-plan.md in the working
checkout. The plan is workspace material and is deliberately not part of the published source
projection.
Problem Statement
A person asks the Agent to open a browser and visit a page, and every action fails with
browser_target_unknown — including the one action that should have started it.
The Agent's browser tools refuse every action when the run has selected nothing, and the only party
that could create that selection was the browser panel, so the person had to open the panel and
click a target before the Agent could do anything at all. The refusal itself is right and stays: a
run that has selected nothing is refused rather than handed a page nobody chose. What is missing is
the other half of the rule ADR 0019 already stated — both the user and the Agent can select a
target.
Underneath that sits a second problem. A page has no identity in the interface. The right-hand
column is a tabbed panel whose tabs are a fixed enumeration of kinds, and the browser is one of
those kinds: a tab that shows a surface's target list, a live image of one page, and forwards
input to it. A person changes the Agent's target by clicking a chip inside that tab; the Agent
cannot change it at all. The selection is the one piece of browser state with no name, no place in
the layout, and no way to be operated by either party.
That also makes the panel a poor browser. It cannot hold a second page, it cannot show a page the
person found themselves, and it cannot be driven by a person and the Agent at the same time: the
person's browsing and the Agent's work compete for one selection.
Solution
The right-hand column becomes a browser.
Its strip holds tabs, and a tab is a stable slot whose content can be replaced in place. A
newly opened tab shows a new tab page — a launcher offering the tool views (changes, terminal,
canvas, files) and a URL box — and choosing either replaces that tab's content. A web tab is a
tab whose content is a browser page: the isolated Chromium holds one page per web tab. There is no
persistent address bar; a web tab carries a toolbar instead (back, forward, reload, and the URL
shown read-only).
Two selections exist, and they are different things. The run's selection is the page the Agent
acts on. The tab the person is looking at is the page the person sees and can type into. The
Agent opens its own tabs: open_tab creates a page, makes it the run's selection and tells the
client, so the tab appears in the strip and becomes the active one. A person browsing does not move
the Agent off its page, and the image stream is per target rather than per run, so each tab's
picture is its own. The one exception is a bootstrap: a person who opens a web tab while the run
has no selection gives the run that page, because a run with no selection cannot act at all.
The browser tool tab is deleted rather than kept alongside. The embedded panel is the isolated
browser's view, permanently: the external browsers are driven in the user's own Edge and Chrome
windows, where the person sees them, so the panel is not where a surface is chosen and the surface
switcher goes with it.
User Stories
- As a WebUI user, I want the Agent to open a browser by itself, so that "open a browser and go to
this page" works without me setting anything up first. - As a WebUI user, I want the Agent's new tab to appear in my panel, so that I can see what it is
doing without hunting for it. - As a WebUI user, I want the panel to be a browser with a tab strip, so that I can open a second
page without losing the first. - As a WebUI user, I want a new tab to offer me the workspace's tools and a place to type a
web address, so that one place does both. - As a WebUI user, I want the address box to live inside the new tab page, so that a page shows its
address and a tool does not pretend to have one. - As a WebUI user, I want to browse in my own tab while the Agent works in another, so that my
looking around does not move the Agent's work. - As a WebUI user, I want to type into the page I am looking at, so that my typing goes where I
am pointing, not where the Agent happens to be. - As a WebUI user, I want the Agent's actions to stay on the Agent's page, so that the Agent never
clicks in a page I opened. - As a WebUI user, I want to open a page myself and then ask the Agent about it, so that the Agent
reads the page I am looking at. - As a WebUI user, I want a web tab to show the page's title, so that I can tell my tabs apart.
- As a WebUI user, I want to go back and forward in a web tab, so that I can undo my own
navigation without losing the page. - As a WebUI user, I want a closed tab to close its page, so that the browser does not accumulate
pages nobody is watching. - As a WebUI user, I want the Agent's browser to be authorized by the same switch as mine, so
that turning the browser off turns it off for both of us. - As a WebUI user, I want a run that cannot act to be refused clearly, so that I know the Agent is
blocked rather than idle. - As a WebUI user, I want Edge and Chrome to keep working in their own windows, so that losing
the panel's surface switcher does not take my logged-in browser away. - As a maintainer, I want the page's generation to advance on every tab, so that a stale
reference is refused on the second page as well as the first. - As a maintainer, I want the frame stream keyed by target, so that a tab's picture is its own and
two tabs cannot show each other's pages. - As a maintainer, I want a session's pages released when the session ends, so that no page is
left running for a session that is gone. - As a maintainer, I want the tab's identity to be the client's, so that two web tabs in one
session are two tabs and a page that navigates is still the same tab. - As a maintainer, I want the browser business to stay separate from the workspace business, so
that a browser with no workspace still has somewhere to live. - As a maintainer, I want the Agent's browser path exercised against a real browser, so that the
claim that the panel and the Agent share a page is an observation and not a design.
Implementation Decisions
These are the decisions the implementation must honour. ADR 0022 holds the rationale; this list is
the operational summary, not a second decision record.
- A tab is a stable id plus a mutable content. The tab shape changes from a discriminated
union keyed by kind to an id and a content descriptor. The kinds become the content descriptors. - The client assigns the tab id. The runtime's target identity is a binding the tab holds, not
the tab's identity: a page that navigates keeps its target id and gains a generation, and the tab
that holds it is still the same tab. - The new tab page is a launcher with four tiles and a URL box. Choosing a tile replaces the
current tab's content; typing a URL opens a page through the browser's port. Thebrowserkind
is deleted. - The URL box exists only inside the new tab page. A web tab carries a toolbar — back, forward,
reload, and the URL shown read-only — instead. - Every web tab goes through the same surface switch, the person's launcher path and the
Agent's path alike. - A web tab is titled by the page and glyphed by default. The title is the one the provider
already reports; fetching a site's icon is out of scope. - The isolated provider holds one page per web tab, with per-target sessions, per-target
screencasts and per-target generations. - The Agent opens tabs.
open_tabis the action that gives a run its first selection and the
action that moves it to another one. Every other action of a run with no selection is still
refused withbrowser_target_unknown. - The run's selection and the tab the person is looking at are separate cells. The person's
input follows their tab; the Agent's actions follow the run's selection. The one bootstrap is a
person opening a web tab while the run has no selection. - The frame stream is per target. This is a change to the existing frame operation's request
shape, not a second operation beside it, and the fence is unchanged: a frame reaches only the tab
that asked for it, and the generation check still decides whether the page behind it is the one on
screen. - The runtime publishes the web tabs as a snapshot, not as deltas: the list is small, and a
snapshot is idempotent, so a reconnect reconciles instead of drifting. - The strip is per session. A second session has its own strip.
- The strip is the workspace business's and the web tab is the browser business's. The only
direction between them is the one the workspace already uses: it mounts a published browser
component and calls the browser's port. It never reaches into the browser's state. - Neither business is aggregated into the other. The browser business is a capability the Agent
uses and has to work without a workspace. - The browser tool tab and its surface switcher are deleted. The embedded panel is the isolated
browser's view; the external browsers are driven in the user's own windows. - A closed web tab closes its page, and a session's pages are released when the session ends —
deleted, archived, or transferred away.
Testing Decisions
- Test observable behaviour, not implementation detail. A test asserts what a person or the
Agent can observe, never that a testid string exists. - Use the highest seam available. Contract and validator behaviour is covered by unit tests;
assembled runtime and server behaviour by the application-level suites; the built client by the
built-client browser suite. - No DOM test framework. Interaction coverage extracts the transitions into pure functions and
asserts render branches through static markup rendering. - Every new test file is registered. A new Vitest file must be listed in the repository's
declared suite groups or it never runs, and a new source path must be recorded in the frozen path
metadata fixture. - A fake provider is not acceptance. The isolated Chromium path must be exercised against the
real provider. - The Agent's path is part of the definition of done. A real browser, the Agent's tool opening
a real tab, the strip gaining it, the picture updating, and the person taking over. The gap this
programme closes is that the Agent's browser tools had never been driven against a real browser. - The generation fence is adversarial coverage. A navigation on a page that is not the launch
page must advance that page's generation and refuse the stale reference. A fence that only works
on the first page is a fence that does not work.
Out of Scope
- The external browsers inside the embedded panel. Edge and Chrome are driven in the user's own
windows; the panel is the isolated browser's view. - The Agent choosing which of the user's already-open Edge or Chrome tabs to drive. The extension
drives the browser through its debugger and cannot open a page in it, so a web tab there would
bind a tab the extension reports rather than create one — a different rule, and not this
programme's. - A persistent address bar, a browser-wide history or a recommendation area in the new tab page.
- Favicons.
- An Agent action that closes a tab. The Agent opens tabs; the person closes them.
- The strip's affordances for a person browsing alongside the Agent. That a person may browse while
the Agent works, on separate targets, is settled; how the strip shows it is not. - Any change to the browser authorization switches, which already exist and are already surfaced
in Settings.
Further Notes
- The development plan that produced this board is workspace material, deliberately outside the
published source projection. The board and the plan must not drift apart: an amendment belongs in
the plan first, and the affected tickets are updated to match. - Research notes captured before implementation, for the implementers:
#197 runtime and provider sites,
#198 the client's tab model and the sites a mutable content breaks. - Ticket 01 is the technical validation work package. It blocks every other ticket, because the
per-page generation accounting, the per-target frame stream, the operation registration and the
tab-model sites are what the later tickets are written against. - If a technical finding requires a product change, ADR 0022 is updated first, then the affected
tickets are re-scoped.
Ticket board
| Ticket | Title | Blocked by |
|---|---|---|
| 200 | 01 — Validate the tab model's technical contracts and pin the capability matrix | — |
| 201 | 02 — The isolated provider holds one page per web tab | 200 |
| 202 | 03 — open_tab creates a page that can be addressed |
201 |
| 203 | 04 — The runtime owns the session's web tabs and publishes them | 200, 201, 202 |
| 204 | 05 — The image stream becomes per target | 203 |
| 205 | 06 — A session's pages are released when the session ends | 203 |
| 206 | 07 — The Agent opens tabs and gives a run its first selection | 202, 203 |
| 207 | 08 — The workspace tab shape becomes a stable id and a mutable content | 200 |
| 208 | 09 — The strip renders web tabs, and the browser tool tab is deleted | 207, 203 |
| 209 | 10 — The new tab page: the tool tiles and the URL box | 208, 206 |
| 210 | 11 — The web tab's toolbar: back, forward, reload and the URL | 208, 204 |
| 211 | 12 — The run's selection and the tab the person is looking at | 204, 206, 210 |
| 212 | 13 — Real-browser acceptance, and the documentation caught up | 211 |
Activity
Fectivnfy112357 commentedon Oct 21, 2026
Delivered. The specification is implemented on
feat/workspace-browser-tab(16 commits, one per issue plus the review's fixes). ADR 0022 now reads Accepted and implemented, and every rule it pins isimplementedwith the file and the line that carries it inpackages/webui/docs/browser-use/05-target-identity.md. The WebUI gates pass: typecheck, dependency, build, boundary, installed package, 3229 unit tests, 146 real-browser tests.Fectivnfy112357 commentedon Oct 21, 2026
Final state — the tip is
5d154673, and the gate is green there.The commit hashes named in the individual closing comments were renumbered when the launcher's fix was folded back into its own issue, so the list is here once, and it is the authoritative one:
d2b25743f0519e7aopen_tabcreates an addressable pagebf4b12a68c50962b(runtime),8de2b142(the strip)12977ab9525bb8a5a59ad38629945e498de2b1422486d5a8f7f2aa9ce3c158bf6027cf2b456f814c0cc664a0341dde475d154673The gate. Every gate the plan's §7 names, run at
5d154673:pnpm typecheck:webuipnpm test:webuipnpm check:webui-dependencypnpm build:webuithenpnpm check:webui-boundarynode scripts/source-inventory.mjs --writethenpnpm check:sourcepnpm test:webui-browserpnpm test:webui-packagealso passes: the installed package starts a real runtime and the packaged bridge admits and shuts down.On the earlier claim. A previous comment on this issue said the gates pass, and at the moment it was written they did — for the steps that had been run. What it did not cover was
check:sourceafter the review's record was added: the new file was not in the published inventory, so that gate was red at the tip that comment described. The inventory is regenerated (5d154673) and the gate is green there now. The review's own record is in the repository rather than only in a commit message:.plan/workspace-browser-tab/complete/codex-review.md.What is not covered. The cross-surface bootstrap — a person opening a web tab while the run is selected on Edge — is implemented run-wide, but it has no dedicated test; the two same-surface cases are covered by the acceptance suite. The macOS
verifyjob and the CI run oftest:webui-browserare CI's business and were not observed here.Fectivnfy112357 commentedon Oct 21, 2026
The gate, re-run at the tip —
4d247f43.The previous comment said the gates pass, and they did for the steps that had been run. Two things were wrong with that, and both are fixed rather than re-asserted.
The gate was red at the tip that comment described. The review's record was a new file in the published projection and the inventory did not name it, so
check:source— one of the plan's own §7 gates — failed. The inventory is regenerated (5d154673).Two further runs found two real defects, and they are fixed.
782c137b).ENOTEMPTYand failed the suite's file with every test in it passing. The removal is retried now (782c137b).The gate at
4d247f43, every §7 gate:pnpm typecheck:webuipnpm test:webuipnpm check:webui-dependencypnpm build:webuithenpnpm check:webui-boundarynode scripts/source-inventory.mjs --writethenpnpm check:sourcepnpm test:webui-browserpnpm test:webui-packageThe unit suite was run four more times on that tree: 208 files and 3230 tests, every time, with no unhandled errors.
The evidence is in the repository, not in a commit message.
.plan/workspace-browser-tab/complete/gate-record.mdsays what was run, on what, and how often;.plan/workspace-browser-tab/complete/codex-review.mdrecords the external review, its five findings, the sixth the gate found, and what closed each. The gate record also says plainly that it ran five times, each after a whole block of changes, and that the first two runs each found a defect.Still not observed: the macOS
verifyjob and CI's own run oftest:webui-browser; and the cross-surface bootstrap — a person opening a tab while the run is selected on Edge — is implemented run-wide but has no dedicated test.Fectivnfy112357 commentedon Oct 21, 2026
What the fork's CI says, and what it does not.
The CI runs the repository's whole
verifyprofile, which is wider than the plan's §7 gates. Its history on this branch and on the base branch:83d8052a— the first pushtest:webuion ubuntu: the suite's file failing with every test in it passing. That was theENOTEMPTYcleanup race — the provider removed its profile directory while the browser was still writing it. Fixed in782c137b: the removal is retried.5d154673test:capabilitieson macos-latest, inpackages/local-runtime-v2/test/unit/agent/storage/agent.repository.test.tsa63a6c99— the base branch's own tip, before this programmeverify (macos-latest)andverify (ubuntu-latest)So the
test:capabilitiesfailure is pre-existing on the base branch, in a package this slice does not touch. The plan's §7 says the repository-level gates cover ground this change does not move, and the history is what says that.The webui gates — the ones §7 names and §9 requires — are green at the tip that carries the record: typecheck, 3230 unit tests across 208 files, dependency direction, build, build boundary, the source projection (5407 published paths) and 146 real-browser tests against real Chrome 152, plus the installed-package check. The full §7 gate was run at
4d247f43and again on the tree that carries this record; the record is.plan/workspace-browser-tab/complete/gate-record.md.The CI run for the current tip is in flight. Its verdict on the webui gates is the same run this comment describes locally; the
test:capabilitiesfailure inlocal-runtime-v2is the base branch's, and it is not something this programme can fix from here without touching a package the plan does not cover.Fectivnfy112357 commentedon Oct 21, 2026
The CI's verdict on the delivered commit, and what it says.
Source verificationat the tip that carries the record:changespass,verify (windows-latest)pass,verify (macos-latest)pass,verify (ubuntu-latest)failure, stopped attest:webui-browser.Inside the ubuntu job:
test:webuitest:webui-packagetest:webui-browserThe six are all in
packages/webui/test/browser/browser-bridge-real-external.spec.ts— the previous programme's external-bridge cases, which launch a real Edge or Chrome with the extension installed. They fail as timeouts: the runner has a Chrome binary, so the spec's "is the browser installed" guard does not fire, but the runner cannot drive that browser with the extension.The same six cases pass on this tree in 5 seconds. The extension build, the bridge worker's lifecycle and the extension's pairing are not touched by this slice; the bridge provider only gained an optional
openTabmember, which the extension bridge does not implement and which the service refuses withbrowser_capability_denied.The base branch's own CI is not green either: its
Source verificationrun failedverify (macos-latest)andverify (ubuntu-latest)before this programme started. Here the macOS job passes, and the ubuntu job fails on those external-bridge cases.So: everything this slice is responsible for is green in CI —
test:webuiandtest:webui-package— and the browser suite is green locally at 146 cases. The remaining CI redness is the previous programme's external-bridge cases in an environment that cannot run them, and the record says so rather than leaving it to be inferred.Fectivnfy112357 commentedon Oct 21, 2026
A correction, with the evidence for it.
Two things this issue's earlier comments said were wrong, and one thing they did not say. The record in the repository now carries the corrected version (
.plan/workspace-browser-tab/complete/gate-record.md).The wrong claim about the CI. The earlier comment said the six external-bridge failures happened "because the runner has a Chrome binary … but cannot drive it with the extension". That was wrong. In the same ubuntu job, the sibling spec
browser-bridge-real-browser.spec.tspasses in both channels — the extension completes the handshake in 785 ms inchromiumand in 22.6 s inmsedge. The runner does launch a real browser with the built extension, and the extension does pair with the host. What is true is narrower: the external spec's operations against the live tab never come back on that runner.The wrong verdict for the macOS job. At the delivered tip the matrix cancelled
verify (macos-latest)when the ubuntu job failed, so that job has no verdict — it did not pass. It passed atacbf1b40, on the same code (the commits between are documentation only).What was not said: where the redness comes from. The same six cases failed the same way on
a18c916b— the browser-use programme's own head commit, merged as PR #195 and an ancestor of this branch's base:This slice changes neither the extension, nor the bridge server, nor that spec:
So the ubuntu job is red on six cases that were already red on the base branch's own head, and the base branch's one CI run (
a63a6c99) cannot speak about them — the spec did not exist at that commit; it was introduced by the browser-use programme at91a1a1c4, and every CI run of that programme from there on has failed the same six.What is green. The plan's §7 gates are green at the tip that carries the record: typecheck, 208 files and 3230 unit tests, dependency direction, build, build boundary, the source projection (5407 published paths), and 146 real-browser tests against real Chrome 152. In CI,
test:webuiandtest:webui-packagepass;test:webui-browserfails on those six.The gate ran five times, not once. §7 says the gates run once at the end. They were never run per work package, but the gate as a whole ran five times, each after a whole block of changes, and each of the first four found a real defect. The record now lists each run, the tree it ran at, and what it found, and separates those runs from the targeted re-runs of a single suite.
What is being done about the inherited failure. The harness's waits on the extension had no deadlines of their own, so a case that hung there reported only "the test timed out" — with nothing about which step stopped. Every such wait is now bounded and names the step it was waiting for (
9d945e54). That does not make a failing case pass and does not skip one; a bound that expires fails the case with a diagnosis. It is there so that the next run on the runner says where these cases stop.Fectivnfy112357 commentedon Oct 21, 2026
The gate is green in CI now, and the six cases that were red since the previous programme are fixed.
The correction I posted earlier stands — the six external-bridge cases were inherited, not introduced, and the macOS job had been cancelled rather than passed. What has changed since then is what was done about them.
They were diagnosed. The cases hung with no diagnosis, because the test harness had waits with no deadline of their own: a service worker Chrome has reclaimed does not answer, and a message nobody replies to does not settle. Every such wait is now bounded, and each bound names the step it was waiting for (
9d945e54). That is not a way of making a failing case pass — a bound that expires fails the case with a diagnosis, and nothing is skipped — it is a way of making the failure say where it stopped.It said where it stopped:
The cases' work was finished — the browser started, the extension paired, the handshake completed, the DevTools command ran against the live tab. What hung was the cleanup: the browser was still running while the harness closed its server, so the browser may still have been holding a connection, and the extension reconnects to a host that is going away. A server that waits for a client to go away first cannot close while that client is still running.
It is fixed (
a4f8f7c6): the browser stops before the server, and the harness cuts the connections it still holds instead of waiting for them. Test-side only, and no assertion changed.The CI run at
54db73a3hasverify (ubuntu-latest)andverify (windows-latest)passing —test:webui208 files,test:webui-browser146 passed in 2.8m, the source projection 5407 paths, the dependency and build-boundary checks, the installed package, and the capability suite. That is the first green ubuntu run since the browser-use programme landed those cases.The whole of §7 was also run again locally at the tip that carries the record: types, 208 files and 3230 unit tests, dependency direction, build, build boundary, the source projection (5407 paths) and 146 browser cases in 2.0m.
The record is
.plan/workspace-browser-tab/complete/gate-record.md, and it now says which CI verdicts are verdicts and which are the runner queue.Fectivnfy112357 commentedon Oct 21, 2026
The final state of the CI, stated precisely.
3d3b78469ac67622ENOTEMPTYfixb495a4dcverify (windows-latest)pass,verify (ubuntu-latest)pass, andverify (macos-latest)cancelled while it was still queued — it never started, twice, and a re-run is queued behind the same runner queueThe head differs from the two green runs by three generated record files only — the acceptance records the suites write and the gate record itself:
No source file, no test and no runtime behaviour differs from the commit whose macOS job passed. The macOS job at this head is waiting for a macOS runner, and it has been cancelled twice without ever starting; that is the runner queue, not a verdict, and the record says so in the same words.
Fectivnfy112357 commentedon Oct 21, 2026
The CI is green at this pull request's head.
The
Source verificationrun atb495a4dc— the head this pull request carries — now has every job passing:changesverify (windows-latest, 24)verify (macos-latest, 24)verify (ubuntu-latest, 24)test:webui208 files,test:webui-browser146 passed, the source projection 5407 paths, the dependency and build-boundary checks, the installed package, the capability suiteverification— "Require every planned check"Release auditandPerformancepass at the same head. This pull request's checks are clean, and it is mergeable.The six external-bridge cases that had been red on that runner since the browser-use programme introduced them are green now: the harness names the step a failure stops at (
9d945e54), and the cleanup stops the browser before the server it was holding open (a4f8f7c6).