Skip to content

[Bug]: Agent and browser panel show two different browser pages for the same browser tab #16901

Description

@slkiser

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. In the desktop app, start a Claude Code thread (A) on any local project.
  2. Ask the agent: "Run sleep 45, then call preview_open with {"url": "https://example.com", "open": false, "reuseExistingTab": false}, tell me the tabId, and stop."
  3. While it sleeps, switch to another thread (B) and stay there for about a minute.
  4. Go back to thread A. Ask the agent to call preview_open with that tabId and "open": true, so the browser tab shows in the browser panel.
  5. Ask the agent to make the browser page red with preview_evaluate (document.body.style.background = "red"), then read the value back.

Expected behavior

The agent and I use the same browser page. When the agent makes it red, the browser page in my panel turns red. When I type or sign in on the browser page in my panel, the agent sees it.

Actual behavior

The agent's browser page is red (it reads back rgb(255, 0, 0) and its screenshot is red), but the browser page in my panel stays white. One browser tab, two different browser pages.

The agent can't tell this happened. preview_open returned visible: false both when it broke and when it worked. The only difference was that a viewport of 1280×800 came back when it broke. The desktop trace shows the browser page in my panel being created about 40 seconds after the agent opened the browser tab, when I came back to thread A.

If I stay on thread A while the agent opens the browser tab (same steps, no sleep, no switching), it works, and we share one browser page. The browser page in my panel turns red, and text I typed into a box on that browser page was read back by the agent.

I first ran into this when an agent needed me to sign in to a website. I signed in on the browser page in my panel, but the agent's browser page stayed on the login form. Opening a new browser tab didn't help.

Likely cause: when the agent opens a browser tab, ServerBrowser.ts waits up to 10 seconds (DESKTOP_ATTACH_TIMEOUT) for the desktop app to provide the browser page (a webview). If none arrives, the server makes its own hidden, headless browser page with fresh storage, because the tab has an automationOwner. The desktop only loads browser tabs for the selected thread, so its browser page can arrive later than that. When it does, nothing swaps the browser pages or tells the client. My panel shows the desktop's browser page while the agent keeps using the hidden one. Showing the hidden browser page in the panel as a stream, which the server already supports, would avoid the split. So would telling the client which browser page the agent is using.

Related: #16701 covers the default browser profile and handing your own browser tabs to an agent. This one is about one browser tab ending up with two browser pages.

Impact

Major degradation or frequent failure

Version or commit

0.0.46-nightly.20261007.2774

Environment

macOS 15 (arm64), T3 Code Nightly desktop app, local environment, Claude Code (Full access)

Logs or stack traces

# Split run (UTC)
17:39:08  agent opens the browser tab (preview_open, open:false) while I'm on thread B
          -> viewport 1280x800, visible:false
17:40:02  desktop trace: the browser page in my panel is created
          (PreviewManager.createTab + registerWebview) as I return to thread A
17:41:19  agent makes its browser page red, reads back rgb(255, 0, 0);
          the browser page in my panel stays white
# Control run (I stay on thread A)
17:43     agent opens a new browser tab -> no viewport field, visible:false
17:43:49  agent makes the browser page red -> the browser page in my panel turns red
17:46:12  agent reads back "asdasd", which I typed on the browser page in my panel

Screenshots, recordings, or supporting files

1-split-run-two-pages.png
2-control-run-shared-page.png

Workaround

Stay on the thread while the agent opens a new browser tab. A browser tab that has already split stays split. Have the agent open a new browser tab while you're on the thread.

Activity

  1. added
    bugSomething is broken or behaving incorrectly.
    needs-triageIssue needs maintainer review and initial categorization.
    on Oct 7, 2026
  2. juliusmarminge commented on Oct 7, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Thanks for the precise repro and timeline. Confirmed in code on main (f570bd2).

    What happens

    1. Fallback after 10s. On a desktop-launched server, a new server tab waits for the desktop's <webview> to attach (DESKTOP_ATTACH_TIMEOUT = "10 seconds", desktopRenders). If nothing attaches in time, createTab opens its own headless page. Because the agent's preview_open sets automationOwner, that page gets an isolated per-tab context with fresh storage. That's why your sign-in in the panel never reached the agent. Only this headless path sets a fixed viewport (L761-L763, UNATTACHED_FILL_VIEWPORT = 1280×800), so that's the viewport field you saw.
    2. Why the desktop doesn't attach for a background thread. ElectronBrowserHost mounts webviews only for threads that have sessions in the client preview store (activePreviewSessionsAtom). On main, that store is filled by usePreviewSession, which runs only for the active thread (PreviewSessionSync in ChatView) and in its preview panel. A tab the agent opens on a thread you aren't viewing gets no webview until you come back, which is long after the 10s wait.
    3. Nothing reconciles a late attach. The desktop channel records and publishes attached events (DesktopBrowserChannel.ts L123-L125). But ServerBrowser only uses awaitAttached once, while creating the tab, and otherwise subscribes only to detached. After the tab exists, ensureTab keeps returning the headless tab. Meanwhile the client decides "native webview vs. stream" only by environment and runtime (rendersServerTabNatively). It never learns that the server fell back to headless, so it mounts its own webview, which loads the URL independently, and the agent can't see it. preview_status also doesn't say which page backs the tab: visible only counts viewers.

    Fix direction

    • Open PR fix(web): host local server browser tabs outside chat views #16620 removes the main trigger. A persistent desktop-side subscription hydrates ElectronBrowserHost from preview.list/events, so background threads' tabs get a webview while the server is still waiting.
    • fix(web): host local server browser tabs outside chat views #16620 doesn't change ServerBrowser at all, though. Any attach that lands after the 10s timeout, such as from a renderer that isn't ready yet, still produces the same split, with no swap and no signal. To close that gap, ServerBrowser needs to handle late attached events for tabs that are running headless. It could switch the client to the stream view for those tabs, or move the tab onto the desktop page. Either way, the snapshot or preview_status should say which page backs the tab.

    Workaround

    Stay on the thread while the agent opens a new browser tab. A tab that has already split stays split for its lifetime, so have the agent open a fresh tab while you're on the thread, and use that one.

    Related but distinct: #16701 (default profile, handing human tabs to agents), #16596 (Cloudflare on the headless page), #14171 (agent pinned to another desktop's copy of a tab).

  3. added
    via-triageFiled through npx t3 triage
    and removed
    needs-triageIssue needs maintainer review and initial categorization.
    on Oct 7, 2026
  4. pfbaer commented on Oct 9, 2026

    @pfbaer

    I ran into this while using Codex to configure an OAuth client in Google Cloud through T3 Code’s Browser panel. The visible page was already signed in, but Codex’s browser tools kept seeing Google’s sign-in form.

    It still happens after updating and fully restarting T3 Code Desktop on 0.0.46-nightly.20261009.2861 (macOS 27.0.1, Apple Silicon). For the post-restart attempt, Codex opened a fresh tab with open: true, show: true, reuseExistingTab: false, and profileId: "default".

    The corresponding trace window on Oct 9, UTC:

    08:26:47.051  preview_open invocation begins (PreviewToolkit.invoke)
    08:26:59.307  Tool response: available:true, visible:false, 1280×800; sign-in page
    08:27:23.201  Desktop PreviewManager.createTab
    08:27:24.438  Desktop PreviewManager.registerWebview
    

    The desktop webview was registered about 37 seconds after the request began. After that, repeated preview_snapshot calls still showed the sign-in form while the visible page showed the signed-in Cloud console. Navigating the visible page through native input did not change the page returned by the tool.

    The T3 address bar also retained the sign-in URL while its visible page content showed the signed-in console.

  5. marius-ciocoiu commented on Oct 9, 2026

    @marius-ciocoiu

    Same on macOS 26.7.1 arm64, T3 Code Nightly 0.0.46-nightly.20261008.2833, Claude Code thread, local environment. Our timings fit the 10 s attach fallback (UTC, 2026-10-09):

    • 08:51:14: preview_open returned after 11.6 s. The desktop <webview> for that tab was only created at 08:55:34.
    • 09:23:19: preview_open returned after 11.0 s. The desktop createTab for it ran at 09:23:58, 39 s later.
    • In both cases the agent opened the login page in its tab, and I signed in from the Browser panel. preview_evaluate on the agent's tab then found 0 ID tokens in localStorage for the same origin, while my panel tab was signed in. This repeated three times (09:24, 09:41, 09:54).

    Evidence: server.trace.ndjson (McpServer tools/call durations) and renderer-history.ndjson (webview created events).

  6. Luzivog commented on Oct 11, 2026

    @Luzivog

    Still present on 0.0.46-nightly.20261009.2873 (Linux, AppImage) and in the code on main @ 75c439f. Three additions: a scripted repro against the shipped server that needs no UI, two more symptoms, and a case for the stream fix direction over moving the tab onto the webview.

    Scripted repro (no UI)

    This test runs the shipped apps/server/dist/bin.mjs (under the AppImage's Electron with ELECTRON_RUN_AS_NODE=1) in a throwaway T3 home. The test process plays the desktop on the desktop browser channel: it writes the DesktopBackendBootstrap envelope with desktopBrowserFd: 6, desktopBrowserControlFd: 7 to fd 3, writes attached events to fd 6 and reads commands from fd 7. It opens a tab with preview.open {runtime: "server"} over /ws, signing in with the bootstrap token.

    1. Control: attached sent right after preview.open. The server sends cdp frames for the tab and starts no headless page. Passes.
    2. attached sent after the tab went headless (the server's 10 s awaitAttached timed out; the headless page exists). The test waits 5 s and expects the server to adopt the webview (cdp), release it, or close its headless page. Fails: no command is sent for the tab, and the headless page stays.

    The root cause is the early return in the attached handler (ServerBrowser.ts L2411-L2420): if (tabs.has(tabKey(...))) return;. createTab decides the host once (L745-L752). Meanwhile rendersServerTabNatively (previewRuntime.ts L61-L72) is true for every primary-environment server tab in Electron. So the panel never streams a headless tab: it always mounts a webview for it, and that webview is never the tab.

    The harness and test are about 200 lines of plain Node, and I can attach them. A port into the server test suite (fake desktop channel; attached after awaitAttached timed out; expect a cdp or release command, or the headless page closed) would make a regression test for whichever fix lands.

    Two more symptoms

    • The panel shows a frozen page under the live URL and title. The late webview loads navStatus.url once, at mount (initialUrl, src in ElectronBrowserHost). The panel's URL bar and title come from the server snapshot, which keeps following the headless page. Example: an agent signed in to one site, then called preview_navigate to a second, unrelated site. The panel kept drawing the first site's login page under the second site's address and title. The webview also never reports back (serverDriven webviews skip reportStatus), so nothing corrects it.
    • The late webview loads the URL a second time, on its own, with the desktop partition's cookies. For one-time links (magic sign-in, email confirmation, OAuth callbacks), this second GET can use up the link or start a second session.

    Fix direction: stream the headless tab

    #16620 removes the main trigger by mounting webviews for background threads, and the closed #17336 moved a late tab onto the webview. Both make the Electron webview the only way to see an agent's tab. Some users run the server browser on purpose. Sites treat it as a normal Chrome (Google sign-in refuses Electron webviews). It also keeps its own persistent profile, which agents use. For them, the right result of a late attach is to keep the headless page and stream it in the panel. The server and client already support streaming for remote environments.

    Concretely:

    1. Add the backing host to the session snapshot, e.g. renderer: "desktop" | "server", set in createTab.
    2. Make rendersServerTabNatively require renderer === "desktop", so a headless tab gets the screencast viewer instead of a webview.
    3. On a late attached for a headless tab, send the desktop a release (or a new unmount) command so it doesn't keep a second page.
    4. Optional: a setting such as "Show agent tabs from the server browser", which makes desktopRenders return false. It also removes the 10 s wait on every preview_open.

    This also fixes preview_status. visible would then be true when the user watches the stream; today it reports false while the user is looking at the stale copy.

  7. miguelSoldado1 commented on Oct 11, 2026

    @miguelSoldado1

    Reproduced on macOS (Darwin 25.6.0, arm64), desktop app nightly 0.0.46-nightly.20261010.2935, Codex provider, local environment. No commits on main since v0.0.46-nightly.20261011.2980 touch this.

    Timeline (UTC, 2026-10-11)

    • 15:00:02: thread created. 15:00:14: Codex calls preview_open {url: "https://us.posthog.com/…", profileId: "default"}. No desktop.ipc.preview.createTab or registerWebview appears in the desktop trace at that time; the thread wasn't focused with the panel open.
    • 15:12:45: the user opens the thread's Browser panel. The desktop creates the webview and the user signs in to PostHog there.
    • 15:13:22–15:13:28: the agent's preview_navigate / preview_open {show: true} on the same tabId still lands on /login?next=…, and preview_status reports visible: false.

    Extra detail. With an explicit profileId: "default", the fallback isn't an isolated context. It's the persistent <stateDir>/server-browser/profiles/default launched by launchPersistentContext, which has its own cookie jar separate from the Electron partition persist:t3code-preview-<hash>. Comparing cookie names (not values):

    • Electron partition: PostHog sessionid and ph_authenticated_us
    • server-browser/profiles/default: only anonymous ph_*_posthog and posthog_csrftoken

    So "sign into the default profile" can never fix it from the UI once the tab has fallen back. Also, the desktopChannel.attached handler in ServerBrowser.ts returns early when the tab already exists (if (tabs.has(...)) return;), which is why the late attach never takes over the headless page.

    This is a common path for scheduled or skill-driven runs, where the user usually isn't watching the thread when the agent opens its first tab.

    Filed via t3 triage by Claude Code (Opus 5.5).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions