feat(panel,host,desktop,web): watch and take over an agent's cloud Browser tab
PR F of the "Browser agent tab automation setup" rollout: the panel side of the cloud browser. A task's agent Browser tabs now show in the side panel, on the desktop and on the web. Each one plays live on a canvas, and a member can take it over. It sits behind Settings › Experimental › Browser tab.
What's in it
A cloud kind of Browser tab (CloudBrowserStripTab in @luke/ui/tab-strip-model)
- The panel reads the task's tabs from the service (
agentBrowserTabs.list, through a newtasks.browserTabsact, gateway method and host read). Every reader of the task shares that one read. - The task record's Neon Realtime read doesn't carry the tabs, so the list is read again every 5 s while the task is open.
- Each tab is named by its page title, with the address on hover. Its detail says whose browser it's in: the agent's name, or Luke.
- Closing a tab hides it in this window only. A chosen tab stays chosen while the list is being read again.
- It's offered behind the Browser tab experiment on every platform, the web included, because it uses none of the Mac tab's acts.
The live view (packages/panel/src/tasks/cloud-browser-live.ts, cloud-browser-pane.tsx)
- Frames are drawn on a canvas that keeps the page's shape inside the pane. The canvas size is the
viewportevery point is sent in. viewportis sent when the socket opens, on every resize, and once a minute after that (Schedule.spaced).- After a drop, the view reconnects with an exponential backoff from 1 s up to 30 s (
Schedule). The backoff starts over once a socket has been heard on. not-openedis asked again every 5 s. A socket closed asidleorgonestays closed until the pane is shown again; an idle one also offers Watch again.- Until the first frame, it says "Waiting for the page".
- The pane is
ph-no-capture, and session replay already doesn't record canvases.session-replay.tsandPRIVACY.mdnow name the live view. Frames, address and title are drawn and dropped; none of them goes into app state, a trace or a count.
Takeover in the tab's bar
- Take over turns into Hand back while you drive. The bar says "You're driving", " is driving", or " is using this tab" when the agent acted in the last 15 s.
- While you drive, mouse, wheel, keys and paste go over the wire in viewport pixels.
- Command chords the window answers stay with the window (
chordIsWindows), as on the Mac Browser tab. ⌘A/C/X/Z go to the page as Control, because the page runs on Linux. ⌘V arrives as a paste.
The socket's two doors (AppPlatform.openBrowserLive)
- Desktop: the host opens the relay socket under the account's bearer, using the Terminal tab's socket opener and its bearer-renew step (now shared as
relay-bearer.ts). Main (browser-live-acts.ts) posts aMessagePortto the asking panel only, and the preload hands it into the page as it does a terminal's port. Main refuses the attach from anything but a panel, or while the experiment is off. - Web:
web-browser-live.tsopens a plain same-originWebSocketfrom the page, carrying the session cookie. - The reconnect and viewport logic lives in
packages/panel, the same for both doors.
Screenshots
These are fixture runs with a staging patch that wasn't committed: a fake socket sends a synthetic sign-in page, and the list read returns two fixture tabs. Desktop is the Electron app under Xvfb; web is the web app's own page.
Desktop: Luke is using the tab · after Take over

Web: Luke is using the tab · after Take over

Tests
cloud-browser-live.test.ts(TestClock): a frame is drawn, and the viewport is sent on open and again after a minute; take over and hand back are sent, and input goes only while the member drives; a socket that drops reconnects after the backoff, and the backoff doubles when a socket never opens; an idle close stays closed.cloud-browser-tab.test.ts(jsdom): the task's tabs are read only while the experiment is on; a listed tab stands in the strip, stays chosen while the list is being read, closes in this window, and gives way once it isn't listed; the pane waits, draws once a frame arrives, and takes over and hands back.browser-live-acts.test.ts(desktop main): the port goes to the asking panel only; the service's frames reach that port only, and a close closes it; detach and the page going away let the view go; attach is refused with the experiment off or from a window that isn't a panel.compose-terminals.test.ts(host): the socket opens under the bearer at the tab's path; sends made while it opens arrive first; the service's frames reach the panel by connection, with the close last; a refused socket closes as 1006; a view that was let go says nothing more.tasks-procedures.test.ts: the tabs act answers a panel, and an unanswered read is refused.web-browser-live.test.ts: the web door opens on the page's own origin, holds sends until the socket is open, and passes on the service's frames and the close.
Screenshots
These were taken with a throwaway staging patch that isn't committed: two fixture tabs, a fake socket sending a synthetic sign-in page, and the experiment on. Desktop is the fixture run (--fixture smoke --profile tasks-transcript) under Xvfb; web is the web app on Vite with the same fixture profile.
| Watching (Luke acted lately) | Taken over | |
|---|---|---|
| Desktop | ![]() | ![]() |
| Web | ![]() | ![]() |
Not in it
- D's
connectRestoringswitch in the relay: #4318 hasn't merged yet. - The list isn't refreshed when a
browsertool call lands in the transcript; the 5 s re-read covers it. - Locally, the sandbox ran out of memory on
pnpm check:affected's service typecheck and discarded-effect steps (neither touched here). I typechecked host, panel, features, ui, desktop and web on their own, and lint passed. CI runs the rest.
Made with Luke



