You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug]: Remote preview stays on "Connecting to browser…" when the viewer's first still capture never answers #24852
A remote viewer (iPhone app, via T3 Connect relay) opened an agent-owned tab and stayed on "Connecting to browser…" for four minutes, until the agent closed the tab. Host is the Windows desktop app 0.1.2 with the headless server browser (no desktop-hosted webview involved), so this is not the off-window Electron guest in #22461 / #23015 / #24514.
Why
ServerBrowserStream calls attachViewerbefore upgrading the WebSocket, and attachViewer awaits an initial still (Page.captureScreenshot) with no deadline. If the page's renderer does not answer, the handshake never completes: the phone's socket never opens, and the UI has no timeout for that state. The pending still also holds tab.captureLock, so every later agent snapshot on the tab queues behind it (#23022 describes that half).
Host trace (server.trace.ndjson, UTC 2026-10-25):
09:02:27 preview_open https://www.costco.co.uk/ ok, 4.3 s
09:02:28 GET /api/preview-stream/ws → ServerBrowser.attachViewer 244,877 ms
09:02:29… preview_snapshot / evaluate / navigate on that tab all time out (15 s)
09:04:22 second viewer attach (full screen) 131,158 ms
09:06:33 t3_preview_close → both attachViewer spans end, GETs 499
A second tab on the same site worked normally. Why the first renderer stopped answering is not reproduced; the T3 fault is that one unanswered capture blocks the viewer handshake and the tab.
Expected
The viewer's socket opens as soon as the screencast is attached; the still is best-effort. A client that gets no frame after a reasonable time shows an error with a reconnect, not a permanent spinner.
Reproduction
Unit level: stub Page.captureScreenshot to never resolve on the viewer's CDP session and call attachViewer; it never returns. Test and fix on robrab2000/t3code@investigate/mobile-preview-connecting-hang, PR to follow.
Still reproducing on 0.1.8-nightly.20261105.4218 (macOS arm64, desktop app host, viewer via T3 Connect relay). Today's source still has the unbounded pushStill before the upgrade (ServerBrowser.ts:2683, captureViewport in ServerBrowserPage.ts:469 has no deadline).
Host server.trace.ndjson, 2026-11-05 UTC:
15:20–15:30 tab_4 ServerBrowser.attachViewer 35–53 ms ok
15:31:19 tab_5 ServerBrowser.attachViewer 55,474 ms GET /api/preview-stream/ws interrupted
15:31:31 tab_5 ServerBrowser.attachViewer 43,539 ms interrupted
15:31:40 tab_5 ServerBrowser.attachViewer 34,270 ms interrupted
The relay connector logs Incoming request ended abruptly: context canceled for each preview-stream request at the same moments (15:31:31, 15:31:34, 15:37:51, 15:38:42), so the viewer gives up while the server is still waiting on the still. A freshly opened tab works until its renderer stops answering. Open fixes: #27663, #25381, #25862.
I reproduced the trigger on the headless server browser. A viewer's still capture that is pending when the tab commits a navigation to a different site never gets an answer. pushStill then holds tab.captureLock until the tab closes, and the tab wedges as this issue describes. This answers "Why the first renderer stopped answering is not reproduced" above. The renderer did not stop. Chromium dropped the request when it swapped renderer processes.
Standalone reproduction
The script below does not use T3. It uses playwright-core 1.60.0 and the chrome-headless-shell 156.0.8078.70 that T3 downloads. A second CDP session plays the viewer. It starts Page.captureScreenshot while page script is busy, and then the page navigates. 127.0.0.1 and localhost are different sites.
Navigation while the capture is pending
Pending capture
New capture on the new page
Same site
answered
answered in 35 ms
Different site
no answer after 30 s (180 s in a longer run)
answered in 12 ms
Different site, then CDPSession.detach()
rejected at the detach
answered in 8 ms
On the same busy page, Page.stopScreencast and Page.startScreencast answered in under 5 ms, so the viewer's pause and resume are not the hang. Page.captureScreenshot, Runtime.evaluate and key presses waited until the page script finished.
// node lost-capture-repro.cjs <playwright-core dir> <chrome-headless-shell path>// A viewer-style CDP session starts Page.captureScreenshot while the page is busy, then the// page navigates. 127.0.0.1 and localhost are different sites, so Chromium swaps processes.consthttp=require("node:http");const{ chromium }=require(process.argv[2]);constbusy=(ms)=>`<script>setTimeout(() => { const e = Date.now() + ${ms}; while (Date.now() < e); }, 200)</script>`;constserver=http.createServer((req,res)=>{res.setHeader("content-type","text/html");res.end(req.url==="/busy" ? `<h1>busy</h1>${busy(5000)}` : "<h1>idle</h1>");});constsettle=(p,ms)=>{constt0=Date.now();returnPromise.race([p.then(()=>`answered after ${Date.now()-t0} ms`,(e)=>`rejected after ${Date.now()-t0} ms: ${e.message.split("\n")[0]}`),newPromise((r)=>setTimeout(()=>r(`no answer after ${ms} ms`),ms)),]);};(async()=>{awaitnewPromise((r)=>server.listen(0,"127.0.0.1",r));constport=server.address().port;constbrowser=awaitchromium.launch({executablePath: process.argv[3]});constcontext=awaitbrowser.newContext();for(const[label,target,detach]of[["same site",`http://127.0.0.1:${port}/idle`,false],["other site",`http://localhost:${port}/idle`,false],["other site, then detach",`http://localhost:${port}/idle`,true],]){constpage=awaitcontext.newPage();awaitpage.goto(`http://127.0.0.1:${port}/busy`);constviewer=awaitcontext.newCDPSession(page);awaitnewPromise((r)=>setTimeout(r,600));conststill=viewer.send("Page.captureScreenshot",{format: "jpeg"});awaitnewPromise((r)=>setTimeout(r,500));awaitpage.goto(target);if(detach)setTimeout(()=>viewer.detach().catch(()=>{}),8000);console.log(`${label.padEnd(24)} pending capture: ${awaitsettle(still,30000)}`);constfresh=awaitcontext.newCDPSession(page);console.log(`${"".padEnd(24)} new capture on new page: ${awaitsettle(fresh.send("Page.captureScreenshot"),5000)}`);awaitpage.close();}awaitbrowser.close();server.close();})();
pushStill (ServerBrowser.ts:2625) awaits captureViewport (ServerBrowserPage.ts:469) inside withCaptureLock. The capture has no deadline.
The capture waits while page script is busy. On a heavy page, this window is several seconds wide.
A navigation to a different site in that window loses the capture. The agent's preview_navigate, a click on an off-site link, or a redirect can cause it.
The next preview_snapshot waits for the lock inside SessionControl.agent, before its 10 s bound starts. It never answers.
Every later action on the tab queues behind that snapshot. The deadline check runs only when an entry starts, so these entries never start. Each one costs its full timeout plus 1 s, and a host eviction. Only preview_status, preview_dialog and t3_preview_close still work.
pushStill runs when a viewer attaches before its first frame. It also runs 200 ms after screencast motion or a dropped frame. So the trigger needs a viewer, a busy page and a navigation to another site at the same time. This explains why the failure is intermittent, and why it hits one thread's tab while other tabs keep working.
Field evidence
Host: desktop app 0.1.6-nightly.20261031.3977 with the headless server browser, macOS 27.0 arm64, Codex agent. Provider log, UTC start times:
2026-11-09, tab_11, one thread
13:32:47 preview_navigate 967 ms ok, visible:false
13:33:04 preview_snapshot 8,026 ms "The page was too busy to capture a snapshot in time" (bot-check page)
13:33:18 preview_evaluate 23 ms ok
13:33:28 preview_navigate 832 ms ok, to a different site, visible:true (a viewer attached since the last navigate)
13:33:34 preview_snapshot 16,003 ms timed out, host evicted
13:33:54 preview_evaluate 16,009 ms timed out, host evicted
13:34:57 preview_navigate 21,010 ms timed out, host evicted
tab_12 in another thread worked normally on the same host the whole time
Two more wedges on 2026-11-06 follow the same pattern: a snapshot with no answer, then evaluate and navigate with no answer, preview_status instant, and a new tab that works at once. One of them had visible: true. The logs for those two do not show the navigation.
For the open fixes
The lost request never settles by itself. A bound on captureViewport, as in #23515 and #25862, covers this trigger. Without a bound, only a session detach or a tab close frees the lock. The viewer's finalizer detaches its session, so a viewer that disconnects also frees a wedged tab. I did not test that path in the app.
Posted by Claude Code (Fable 5.2) on behalf of the account owner.
Still reproduces on desktop Nightly 0.1.9-nightly.20261110.4478 (macOS 26.6.2, desktop app host, iOS viewer over T3 Connect), with a trigger that links this to #23139.
An agent opened a tab while the host window was off screen, and a phone attached as a viewer. The phone stayed on "Connecting to browser…".
The tab stayed stuck for over two hours. That included time when the host window was on screen and the tab was showing in the desktop browser panel.
During that time, Page.captureScreenshot sent straight to the same guest over the DevTools port answered in 26 ms. So the renderer was healthy and only the tab's viewer path was wedged.
A second tab opened in the same thread streamed to the phone normally.
So one capture that hangs because of #23139 leaves the tab unusable for viewers until it is closed, even after the window is visible again.
Activity
xIGBClutchIx commentedon Oct 25, 2026
Glad to hear I am not the only one having issues. Just started using T3 Code. It also really wanted to open the dev url as a file.
ryanskybluechem commentedon Nov 5, 2026
Still reproducing on 0.1.8-nightly.20261105.4218 (macOS arm64, desktop app host, viewer via T3 Connect relay). Today's source still has the unbounded
pushStillbefore the upgrade (ServerBrowser.ts:2683,captureViewportinServerBrowserPage.ts:469has no deadline).Host
server.trace.ndjson, 2026-11-05 UTC:The relay connector logs
Incoming request ended abruptly: context canceledfor each preview-stream request at the same moments (15:31:31, 15:31:34, 15:37:51, 15:38:42), so the viewer gives up while the server is still waiting on the still. A freshly opened tab works until its renderer stops answering. Open fixes: #27663, #25381, #25862.Triaged with Claude Opus 5.5 in T3 Code.
alexispurslane commentedon Nov 9, 2026
I reproduced the trigger on the headless server browser. A viewer's still capture that is pending when the tab commits a navigation to a different site never gets an answer.
pushStillthen holdstab.captureLockuntil the tab closes, and the tab wedges as this issue describes. This answers "Why the first renderer stopped answering is not reproduced" above. The renderer did not stop. Chromium dropped the request when it swapped renderer processes.Standalone reproduction
The script below does not use T3. It uses
playwright-core1.60.0 and thechrome-headless-shell156.0.8078.70 that T3 downloads. A second CDP session plays the viewer. It startsPage.captureScreenshotwhile page script is busy, and then the page navigates.127.0.0.1andlocalhostare different sites.CDPSession.detach()On the same busy page,
Page.stopScreencastandPage.startScreencastanswered in under 5 ms, so the viewer's pause and resume are not the hang.Page.captureScreenshot,Runtime.evaluateand key presses waited until the page script finished.node lost-capture-repro.cjs <playwright-core dir> <chrome-headless-shell path>How it wedges the tab (main @ 4f0b74e)
pushStill(ServerBrowser.ts:2625) awaitscaptureViewport(ServerBrowserPage.ts:469) insidewithCaptureLock. The capture has no deadline.preview_navigate, a click on an off-site link, or a redirect can cause it.preview_snapshotwaits for the lock insideSessionControl.agent, before its 10 s bound starts. It never answers.preview_status,preview_dialogandt3_preview_closestill work.pushStillruns when a viewer attaches before its first frame. It also runs 200 ms after screencast motion or a dropped frame. So the trigger needs a viewer, a busy page and a navigation to another site at the same time. This explains why the failure is intermittent, and why it hits one thread's tab while other tabs keep working.Field evidence
Host: desktop app 0.1.6-nightly.20261031.3977 with the headless server browser, macOS 27.0 arm64, Codex agent. Provider log, UTC start times:
Two more wedges on 2026-11-06 follow the same pattern: a snapshot with no answer, then evaluate and navigate with no answer,
preview_statusinstant, and a new tab that works at once. One of them hadvisible: true. The logs for those two do not show the navigation.For the open fixes
The lost request never settles by itself. A bound on
captureViewport, as in #23515 and #25862, covers this trigger. Without a bound, only a session detach or a tab close frees the lock. The viewer's finalizer detaches its session, so a viewer that disconnects also frees a wedged tab. I did not test that path in the app.Investigated with Claude Opus 5.5 in T3 Code.
hunterbohm commentedon Nov 10, 2026
Note
Posted by Claude Code (Fable 5.2) on behalf of the account owner.
Still reproduces on desktop Nightly 0.1.9-nightly.20261110.4478 (macOS 26.6.2, desktop app host, iOS viewer over T3 Connect), with a trigger that links this to #23139.
Page.captureScreenshotsent straight to the same guest over the DevTools port answered in 26 ms. So the renderer was healthy and only the tab's viewer path was wedged.So one capture that hangs because of #23139 leaves the tab unusable for viewers until it is closed, even after the window is visible again.