@@ -49,6 +49,7 @@ import * as Path from "effect/Path";
4949import * as Ref from "effect/Ref" ;
5050import * as Schema from "effect/Schema" ;
5151import * as Semaphore from "effect/Semaphore" ;
52+ import * as Stream from "effect/Stream" ;
5253import * as Schedule from "effect/Schedule" ;
5354import * as Scope from "effect/Scope" ;
5455import * as SynchronizedRef from "effect/SynchronizedRef" ;
@@ -67,6 +68,7 @@ import {
6768 RECORDING_CURSOR_CHANNEL ,
6869 RECORDING_INPUT_CHANNEL ,
6970 RECORDING_CONTROLLER_CHANNEL ,
71+ RECORDING_POINTER_CHANNEL ,
7072 START_PICK_CHANNEL ,
7173} from "./GuestProtocol.ts" ;
7274import { isPreviewAnnotationPayload } from "./PickedElementPayload.ts" ;
@@ -2287,6 +2289,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
22872289 return ;
22882290 }
22892291 if ( afterAttach . serverTab ) {
2292+ yield * listenForAgentPointers ;
22902293 browserHost . attach ( afterAttach . serverTab , { webContents : wc , debugger : control . debugger } ) ;
22912294 }
22922295 if ( afterAttach . colorScheme !== "system" ) {
@@ -3173,6 +3176,58 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
31733176 ) ;
31743177 } ) ;
31753178
3179+ let pointerSequence = 0 ;
3180+ /**
3181+ * The server reports where an agent action is about to land on a tab it
3182+ * drives here. The live cursor and desktop recordings draw it like any agent.
3183+ */
3184+ const emitAgentPointer = Effect . fn ( "PreviewManager.emitAgentPointer" ) ( function * ( pointer : {
3185+ readonly key : { readonly threadId : string ; readonly tabId : string } ;
3186+ readonly phase : "move" | "click" ;
3187+ readonly x : number ;
3188+ readonly y : number ;
3189+ } ) {
3190+ const tab = [ ...( yield * SynchronizedRef . get ( tabsRef ) ) . values ( ) ] . find (
3191+ ( candidate ) =>
3192+ candidate . serverTab ?. threadId === pointer . key . threadId &&
3193+ candidate . serverTab . tabId === pointer . key . tabId ,
3194+ ) ;
3195+ if ( ! tab ) return ;
3196+ const event : DesktopPreviewPointerEvent = {
3197+ tabId : tab . tabId ,
3198+ phase : pointer . phase ,
3199+ x : pointer . x ,
3200+ y : pointer . y ,
3201+ sequence : ++ pointerSequence ,
3202+ createdAt : yield * currentIso ,
3203+ } ;
3204+ const recording = ( yield * SynchronizedRef . get ( frameCaptureSessionsRef ) ) . get ( tab . tabId ) ;
3205+ const webContentsId = tab . webContentsId ;
3206+ if ( recording ?. consumers . has ( "recording" ) && webContentsId != null ) {
3207+ yield * attempt ( { operation : "recording.pointer" , tabId : tab . tabId } , ( ) => {
3208+ const contents = webContents . fromId ( webContentsId ) ;
3209+ if ( contents && ! contents . isDestroyed ( ) ) contents . send ( RECORDING_POINTER_CHANNEL , event ) ;
3210+ } ) . pipe ( Effect . ignore ) ;
3211+ }
3212+ const listeners = yield * Ref . get ( pointerEventListenersRef ) ;
3213+ yield * Effect . forEach (
3214+ listeners ,
3215+ ( listener ) => deliverEvent ( "pointer-event" , tab . tabId , ( ) => listener ( event ) ) ,
3216+ { discard : true } ,
3217+ ) ;
3218+ } ) ;
3219+ // Listening starts with the first tab of the desktop's own server.
3220+ let pointersStarted = false ;
3221+ const listenForAgentPointers = Effect . suspend ( ( ) => {
3222+ if ( pointersStarted ) return Effect . void ;
3223+ pointersStarted = true ;
3224+ return browserHost . pointers . pipe (
3225+ Stream . runForEach ( emitAgentPointer ) ,
3226+ Effect . forkIn ( parentScope ) ,
3227+ Effect . asVoid ,
3228+ ) ;
3229+ } ) ;
3230+
31763231 const subscribe = < A > (
31773232 ref : Ref . Ref < ReadonlySet < A > > ,
31783233 listener : A ,
0 commit comments