Repository navigation
fix(preview): agents see your browser page, and keep working while you're in another thread #18584
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fbf5916
ae8b9ac
00cb6da
f08aab0
a0e1667
8a3f2f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -683,22 +683,26 @@ const make = Effect.gen(function* () { | |
| /** Whether the preview session for a tab still exists. */ | ||
| const sessionOpen = (tab: ServerTab) => !closedSessions.has(tabKey(tab.threadId, tab.tabId)); | ||
|
|
||
| const dropTab = (tab: ServerTab, closeSession: boolean) => { | ||
| /** `handOff` keeps the session for a new page, so viewers reconnect to it. */ | ||
| const dropTab = (tab: ServerTab, closeSession: boolean, handOff = false) => { | ||
| const key = tabKey(tab.threadId, tab.tabId); | ||
| if (tabs.get(key) !== tab) return; | ||
| tabs.delete(key); | ||
| tab.closing = true; | ||
| clearAbortedNavigation(tab); | ||
| // A desktop page outlives the connection unless its session closed with it. | ||
| const end = tab.desktop && !closeSession && sessionOpen(tab) ? "reconnect" : "gone"; | ||
| const end = | ||
| (tab.desktop || handOff) && !closeSession && sessionOpen(tab) ? "reconnect" : "gone"; | ||
| for (const viewer of tab.viewers) viewer.push({ _tag: end }); | ||
| void tab.control.close().catch(constVoid); | ||
| // The desktop owns its page; letting go only ends this connection. | ||
| if (tab.desktop) void tab.desktop.close().catch(constVoid); | ||
| else void tab.page.close().catch(constVoid); | ||
| if (tab.isolatedContext) void tab.page.context().close().catch(constVoid); | ||
| void tab.recording?.encoder.close().catch(constVoid); | ||
| void NodeFSP.rm(downloadDir(tab), { recursive: true, force: true }).catch(constVoid); | ||
| // A handed-off tab keeps its downloads; the folder is keyed by tab, not page. | ||
| if (!handOff) | ||
| void NodeFSP.rm(downloadDir(tab), { recursive: true, force: true }).catch(constVoid); | ||
| reportLiveTabs(); | ||
| if (closeSession) { | ||
| runFork(manager.close({ threadId: tab.threadId, tabId: tab.tabId }).pipe(Effect.ignore)); | ||
|
|
@@ -2233,7 +2237,12 @@ const make = Effect.gen(function* () { | |
| if (next._tag === "frame") runFork(next.ack); | ||
| // State a stalled viewer cannot miss replaces its backlog. It runs | ||
| // synchronously so an older replacement can never land after a newer one. | ||
| else if (next._tag === "gone" || next._tag === "control" || next._tag === "fileChooser") { | ||
| else if ( | ||
| next._tag === "gone" || | ||
| next._tag === "reconnect" || | ||
| next._tag === "control" || | ||
| next._tag === "fileChooser" | ||
| ) { | ||
| const dropped = Effect.runSyncExit(Queue.clear(output)); | ||
| if (dropped._tag === "Failure") return; | ||
| Queue.offerUnsafe(output, next); | ||
|
|
@@ -2408,15 +2417,28 @@ const make = Effect.gen(function* () { | |
| // The page came back, for example after its DevTools closed. Reconnect now | ||
| // rather than on the next viewer or agent, or the tab's URL and title stop | ||
| // reaching every client in the meantime. | ||
| // A tab that went headless before the desktop mounted it (its thread was in | ||
| // the background) moves onto the desktop's page. Otherwise the user and the | ||
| // agent see two different pages under one tab. | ||
| yield* desktopChannel.attached.pipe( | ||
| Stream.runForEach((key) => | ||
| Effect.gen(function* () { | ||
| if (tabs.has(tabKey(key.threadId, key.tabId))) return; | ||
| const pending = pendingTabs.get(tabKey(key.threadId, key.tabId)); | ||
| if (pending) yield* Effect.promise(() => pending.catch(constVoid)); | ||
| const existing = tabs.get(tabKey(key.threadId, key.tabId)); | ||
| if (existing?.desktop) return; | ||
| if (existing) dropTab(existing, false, true); | ||
|
macroscopeapp[bot] marked this conversation as resolved.
Resolved
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| const { sessions } = yield* manager.list({ threadId: ThreadId.make(key.threadId) }); | ||
| const snapshot = sessions.find( | ||
| (session) => session.tabId === key.tabId && session.runtime === "server", | ||
| ); | ||
| if (snapshot) yield* Effect.promise(() => ensureTab(snapshot).catch(constVoid)); | ||
| const tab = snapshot | ||
| ? yield* Effect.promise(() => ensureTab(snapshot).catch(() => null)) | ||
| : null; | ||
| if (!existing) return; | ||
| if (tab) tab.downloads.unshift(...existing.downloads); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Preserve an active recording during handoff. If an agent starts recording on the headless tab before the desktop attaches, 🤖 Prompt for AI Agents
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note 🤖 Claude Opus 5.5 responding on behalf of Theo Declining for this PR; noting it as a follow-up. The case is real, but it needs an agent recording on a headless tab in a background thread while the user opens that thread mid-recording. Neither suggested fix is small:
Before this PR the same tab stayed split forever, so the agent was recording a page the user could not see. That was the bug this PR fixes.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Would you like me to open a follow-up issue for preserving recordings during headless-to-desktop handoff? ✏️ Learnings added
You are interacting with an AI system. |
||
| else | ||
| void NodeFSP.rm(downloadDir(existing), { recursive: true, force: true }).catch(constVoid); | ||
| }).pipe(Effect.ignore), | ||
| ), | ||
| Effect.forkScoped, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.