@@ -321,6 +321,9 @@ const desktopRenders = (tabId: string) => {
321321 }
322322 return desktopTabs . has ( tabId ) ;
323323} ;
324+ /** When set, a desktop that has not attached a tab waits for it the way the real channel does. */
325+ let desktopAttachesLate = false ;
326+ let desktopAttachWaiting = Promise . withResolvers < void > ( ) ;
324327const shownDesktopTabs = new Set < string > ( ) ;
325328const releasedDesktopTabs : Array < string > = [ ] ;
326329const desktopTurns = new NodeEvents . EventEmitter ( ) ;
@@ -355,7 +358,30 @@ const dependencies = Layer.mergeAll(
355358 } ) ,
356359 Layer . succeed ( DesktopChannel . DesktopBrowserChannel , {
357360 available : true ,
358- awaitAttached : ( key ) => Effect . sync ( ( ) => desktopRenders ( key . tabId ) ) ,
361+ awaitAttached : ( key , timeout ) =>
362+ ! desktopAttachesLate || desktopRenders ( key . tabId )
363+ ? Effect . sync ( ( ) => desktopRenders ( key . tabId ) )
364+ : Effect . raceFirst (
365+ Stream . callback < string > ( ( queue ) =>
366+ Effect . acquireRelease (
367+ Effect . sync ( ( ) => {
368+ const onAttach = ( attached : { tabId : string } ) =>
369+ Queue . offerUnsafe ( queue , attached . tabId ) ;
370+ desktopDetaches . on ( "attach" , onAttach ) ;
371+ return onAttach ;
372+ } ) ,
373+ ( onAttach ) => Effect . sync ( ( ) => desktopDetaches . off ( "attach" , onAttach ) ) ,
374+ ) ,
375+ ) . pipe (
376+ Stream . filter ( ( tabId ) => tabId === key . tabId ) ,
377+ Stream . runHead ,
378+ Effect . as ( true ) ,
379+ ) ,
380+ Effect . sync ( ( ) => desktopAttachWaiting . resolve ( ) ) . pipe (
381+ Effect . andThen ( Effect . sleep ( timeout ) ) ,
382+ Effect . as ( false ) ,
383+ ) ,
384+ ) ,
359385 detached : Stream . callback < { threadId : string ; tabId : string } > ( ( queue ) =>
360386 Effect . acquireRelease (
361387 Effect . sync ( ( ) => {
@@ -460,6 +486,8 @@ beforeEach(() => {
460486 desktopTabs . clear ( ) ;
461487 shownDesktopTabs . clear ( ) ;
462488 desktopRendersNext = false ;
489+ desktopAttachesLate = false ;
490+ desktopAttachWaiting = Promise . withResolvers < void > ( ) ;
463491 desktopPageUrl = "about:blank" ;
464492 desktopSizeAnswer = null ;
465493 desktopDetachReady = Promise . withResolvers < void > ( ) ;
@@ -627,6 +655,63 @@ it.effect("a tab the desktop does not draw is marked headless, so the desktop st
627655 ) . pipe ( Effect . provide ( layer ) ) ,
628656) ;
629657
658+ it . effect ( "an agent's tab the reloaded desktop dropped goes back to the desktop" , ( ) =>
659+ Effect . scoped (
660+ Effect . gen ( function * ( ) {
661+ const { broker } = yield * ready ;
662+ const manager = yield * Manager . PreviewManager ;
663+ const threadId = scope . thread . threadId ;
664+ desktopRendersNext = true ;
665+ const opened = yield * broker . invoke < PreviewAutomationStatus > ( {
666+ scope,
667+ operation : "open" ,
668+ input : { url : "https://example.com/?t=1" , reuseExistingTab : false , open : false } ,
669+ } ) ;
670+ const tabId = PreviewTabId . make ( opened . tabId ! ) ;
671+ // The renderer reloads on another thread: its webview goes, and it only mounts this
672+ // thread's tabs again once a preview event for the thread reaches it.
673+ yield * Effect . promise ( ( ) => desktopDetachReady . promise ) ;
674+ desktopAttachesLate = true ;
675+ desktopTabs . delete ( tabId ) ;
676+ desktopDetaches . emit ( "detach" , { threadId, tabId } ) ;
677+ yield * Effect . promise ( ( ) => desktopReleased . promise ) ;
678+ // It is still reloading when the agent's call arrives, so it misses the first event.
679+ yield * Effect . gen ( function * ( ) {
680+ yield * Effect . promise ( ( ) => desktopAttachWaiting . promise ) ;
681+ const events = yield * manager . subscribeEvents ;
682+ const nextForThread = Effect . gen ( function * ( ) {
683+ let event = yield * PubSub . take ( events ) ;
684+ while ( event . threadId !== threadId ) event = yield * PubSub . take ( events ) ;
685+ } ) ;
686+ yield * nextForThread ;
687+ yield * nextForThread ;
688+ desktopTabs . add ( tabId ) ;
689+ desktopDetaches . emit ( "attach" , { threadId, tabId } ) ;
690+ } ) . pipe ( Effect . forkScoped ) ;
691+
692+ const evaluating = yield * broker
693+ . invoke ( {
694+ scope,
695+ tabId,
696+ operation : "evaluate" ,
697+ input : { expression : "navigator.userAgent" } ,
698+ } )
699+ . pipe ( Effect . forkScoped ) ;
700+ yield * Effect . raceFirst (
701+ Fiber . join ( evaluating ) ,
702+ Effect . promise ( ( ) => desktopAttachWaiting . promise ) . pipe (
703+ Effect . andThen ( TestClock . adjust ( "10 seconds" ) ) ,
704+ Effect . andThen ( Fiber . join ( evaluating ) ) ,
705+ ) ,
706+ ) ;
707+ const { sessions } = yield * manager . list ( { threadId } ) ;
708+
709+ expect ( sessions . find ( ( session ) => session . tabId === tabId ) ?. headless ) . toBeUndefined ( ) ;
710+ expect ( desktopConnections ) . toHaveLength ( 2 ) ;
711+ } ) ,
712+ ) . pipe ( Effect . provide ( layer ) ) ,
713+ ) ;
714+
630715it . effect ( "a desktop page that loaded before the server attached reports its loaded title" , ( ) =>
631716 Effect . scoped (
632717 Effect . gen ( function * ( ) {
0 commit comments