Repository navigation
fix(preview): agents can close tabs their desktop browser tab opens #26258
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
base: main
Are you sure you want to change the base?
Changes from all commits
df26b16
62018f8
197c8b3
9b2b409
3497ee3
4e7113a
1a7ba41
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 |
|---|---|---|
|
|
@@ -39,8 +39,10 @@ import { | |
| type PreviewAutomationUploadInput, | ||
| type PreviewAutomationWaitForInput, | ||
| PreviewClearProfileError, | ||
| type PreviewError, | ||
| type PreviewEvent, | ||
| type PreviewNavStatus, | ||
| type PreviewOpenInput, | ||
| type PreviewSessionSnapshot, | ||
| type PreviewViewportSetting, | ||
| ThreadId, | ||
|
|
@@ -283,6 +285,9 @@ export class ServerBrowser extends Context.Service< | |
| readonly downloadId: string; | ||
| }) => Effect.Effect<Option.Option<{ readonly path: string; readonly fileName: string }>>; | ||
|
|
||
| /** A tab opened from an agent's tab joins that agent while it has room for another. */ | ||
| readonly open: (input: PreviewOpenInput) => Effect.Effect<PreviewSessionSnapshot, PreviewError>; | ||
|
|
||
| readonly clearProfile: (profileId: string) => Effect.Effect<void, PreviewClearProfileError>; | ||
|
|
||
| readonly profileReporter: Effect.Effect<ProfileReporter, never, Scope.Scope>; | ||
|
|
@@ -535,6 +540,7 @@ const make = Effect.gen(function* () { | |
|
|
||
| const closedPendingTabs = new Set<string>(); | ||
| const adoptedPages = new Map<string, { readonly page: Page; readonly openerTabId: string }>(); | ||
| const desktopOpeners = new Map<string, string>(); | ||
| const closedSessions = new Set<string>(); | ||
| let hostConnectionId: string | null = null; | ||
| let viewerResizeOrder = 0; | ||
|
|
@@ -895,6 +901,8 @@ const make = Effect.gen(function* () { | |
| ): Promise<ServerTab> => { | ||
| const adopted = adoptedPages.get(tabKey(snapshot.threadId, snapshot.tabId)); | ||
| adoptedPages.delete(tabKey(snapshot.threadId, snapshot.tabId)); | ||
| const desktopOpener = desktopOpeners.get(tabKey(snapshot.threadId, snapshot.tabId)); | ||
| desktopOpeners.delete(tabKey(snapshot.threadId, snapshot.tabId)); | ||
| const desktop = | ||
| adopted === undefined && (await desktopRenders(snapshot)) | ||
| ? await connectDesktop(snapshot) | ||
|
|
@@ -965,7 +973,7 @@ const make = Effect.gen(function* () { | |
| isolatedContext, | ||
| desktop: desktop === null ? null : { close: desktop.close }, | ||
| profileId: snapshot.profileId, | ||
| openerTabId: adopted?.openerTabId, | ||
| openerTabId: adopted?.openerTabId ?? desktopOpener, | ||
| downloads: [], | ||
| fileChooser: null, | ||
| dialog: null, | ||
|
|
@@ -3101,8 +3109,34 @@ const make = Effect.gen(function* () { | |
| catch: (cause) => new PreviewClearProfileError({ profileId, cause }), | ||
| }); | ||
|
|
||
| const open = Effect.fn("ServerBrowser.open")(function* (input: PreviewOpenInput) { | ||
| const { openerTabId, ...openInput } = input; | ||
| const owner = | ||
| openerTabId === undefined || input.runtime !== "server" | ||
| ? undefined | ||
| : (yield* manager.list({ threadId: input.threadId })).sessions.find( | ||
| (session) => session.tabId === openerTabId, | ||
| )?.automationOwner; | ||
| if (owner === undefined || openerTabId === undefined || atTabLimit(owner)) | ||
| return yield* manager.open(openInput); | ||
| let opened: PreviewSessionSnapshot | undefined; | ||
| return yield* openTab({ | ||
| ...openInput, | ||
| automationOwner: owner, | ||
| beforePublish: (snapshot) => { | ||
| opened = snapshot; | ||
| desktopOpeners.set(tabKey(snapshot.threadId, snapshot.tabId), openerTabId); | ||
| }, | ||
| }).pipe( | ||
| Effect.flatMap(() => (opened ? Effect.succeed(opened) : Effect.fail(undefined))), | ||
| // Losing a capacity race or the launch still opens the tab the user clicked, unclaimed. | ||
| Effect.catch(() => manager.open(openInput)), | ||
| ); | ||
| }); | ||
|
Comment on lines
+3112
to
+3135
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.
|
||
|
|
||
| return ServerBrowser.of({ | ||
| attachViewer, | ||
| open, | ||
| clearProfile, | ||
| profileReporter, | ||
| openDownload, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Preserve the opener ID across desktop reconnects.
This deletion makes
openerTabIdavailable only during the first setup. When the desktop detaches,dropTab(tab, false)removes the tab but keeps its session. On attachment,ensureTab(snapshot)recreates the tab without an opener ID because neitherdesktopOpenersnorPreviewSessionSnapshotretains it. The status response then loses the popup relationship for the same open session.Keep the opener metadata for the session lifetime. Remove it when the session closes, not when setup reads it. Extend an existing reconnect test to check that status retains
openerTabId.