@@ -67,8 +67,6 @@ import * as Schema from "effect/Schema";
6767
6868/** Enough to explain a failure without letting a chatty page grow unboundedly. */
6969const MAX_CONSOLE_ENTRIES = 200 ;
70- /** Long enough to choose deliberately, short enough not to strand inspect mode. */
71- const PICK_TIMEOUT_MS = 60_000 ;
7270/** Drag-and-drop and text selection depend on held movement between press and release. */
7371const DRAG_MOVE_STEPS = 10 ;
7472
@@ -1096,59 +1094,103 @@ export const make = Effect.sync(function PreviewAutomationMake() {
10961094 expression : buildPickOverlayScript ( colorScheme , mode ) ,
10971095 } ) ;
10981096
1099- const picked = yield * Effect . tryPromise ( {
1100- try : ( ) =>
1101- new Promise < PickResult | null > ( ( resolve ) => {
1102- let done = false ;
1103- const finish = ( value : PickResult | null ) => {
1104- if ( done ) return ;
1105- done = true ;
1106- contents . debugger . off ( "message" , onMessage ) ;
1107- clearTimeout ( timer ) ;
1108- resolve ( value ) ;
1109- } ;
1110- const onMessage = (
1111- _event : unknown ,
1112- method : string ,
1113- params : Record < string , unknown > ,
1114- ) => {
1115- if ( method !== "Runtime.bindingCalled" || params . name !== PICK_OVERLAY_BINDING ) {
1116- return ;
1117- }
1118- try {
1119- const payload = JSON . parse ( String ( params . payload ?? "{}" ) ) as {
1120- count ?: number ;
1121- note ?: string | null ;
1122- styleChanges ?: ReadonlyArray < { property : string ; from : string ; to : string } > ;
1123- cancelled ?: boolean ;
1124- } ;
1125- finish (
1126- payload . cancelled === true || payload . count === undefined || payload . count < 1
1127- ? null
1128- : {
1129- count : payload . count ,
1130- note : payload . note ?? null ,
1131- styleChanges : payload . styleChanges ?? [ ] ,
1132- } ,
1133- ) ;
1134- } catch {
1135- finish ( null ) ;
1136- }
1137- } ;
1138- contents . debugger . on ( "message" , onMessage ) ;
1139- // Reaching for another tool settles this one, rather than leaving
1140- // it waiting on a binding that will never be called.
1141- supersede = ( ) => {
1142- pendingPicks . delete ( webContentsId ) ;
1143- finish ( null ) ;
1144- } ;
1145- // Picking is a deliberate act; if the user wanders off, stop
1146- // waiting rather than leaving the overlay armed forever.
1147- const timer = setTimeout ( ( ) => finish ( null ) , PICK_TIMEOUT_MS ) ;
1148- } ) ,
1149- catch : ( cause ) =>
1150- new PreviewCommandError ( { webContentsId, method : "Runtime.bindingCalled" , cause } ) ,
1151- } ) ;
1097+ // The wait is a loop rather than one promise, because the overlay now
1098+ // speaks twice: once when elements are chosen -- so they can be described
1099+ // while they still exist -- and once when the note is settled. Between
1100+ // the two the page is free to die (a dev server reloading it, most
1101+ // often), and the early description is what lets the annotation survive
1102+ // that. No timeout: writing a note takes as long as it takes, and an
1103+ // armed pick is settled by escape, by another tool, or by the page
1104+ // itself navigating away before anything was chosen.
1105+ type PickEvent =
1106+ | { readonly kind : "chosen" ; readonly count : number }
1107+ | { readonly kind : "final" ; readonly result : PickResult | null } ;
1108+ const eventQueue : PickEvent [ ] = [ ] ;
1109+ let closed = false ;
1110+ let notifyWaiter : ( ( ) => void ) | null = null ;
1111+ const push = ( event : PickEvent ) => {
1112+ if ( closed ) return ;
1113+ eventQueue . push ( event ) ;
1114+ notifyWaiter ?.( ) ;
1115+ } ;
1116+ const onMessage = ( _event : unknown , method : string , params : Record < string , unknown > ) => {
1117+ if ( method === "Page.frameNavigated" ) {
1118+ const frame = params . frame as { parentId ?: string } | undefined ;
1119+ // Main frame only. The document the overlay lived in is gone; any
1120+ // last words it had (the pagehide auto-attach below) arrive on this
1121+ // same ordered stream before this does, so a note in progress is
1122+ // already in the queue and this settles only a truly idle pick.
1123+ if ( frame ?. parentId === undefined ) {
1124+ push ( { kind : "final" , result : null } ) ;
1125+ }
1126+ return ;
1127+ }
1128+ if ( method !== "Runtime.bindingCalled" || params . name !== PICK_OVERLAY_BINDING ) {
1129+ return ;
1130+ }
1131+ try {
1132+ const payload = JSON . parse ( String ( params . payload ?? "{}" ) ) as {
1133+ chosen ?: number ;
1134+ count ?: number ;
1135+ note ?: string | null ;
1136+ styleChanges ?: ReadonlyArray < { property : string ; from : string ; to : string } > ;
1137+ cancelled ?: boolean ;
1138+ } ;
1139+ if ( typeof payload . chosen === "number" ) {
1140+ push ( { kind : "chosen" , count : payload . chosen } ) ;
1141+ return ;
1142+ }
1143+ push ( {
1144+ kind : "final" ,
1145+ result :
1146+ payload . cancelled === true || payload . count === undefined || payload . count < 1
1147+ ? null
1148+ : {
1149+ count : payload . count ,
1150+ note : payload . note ?? null ,
1151+ styleChanges : payload . styleChanges ?? [ ] ,
1152+ } ,
1153+ } ) ;
1154+ } catch {
1155+ push ( { kind : "final" , result : null } ) ;
1156+ }
1157+ } ;
1158+ contents . debugger . on ( "message" , onMessage ) ;
1159+ // Reaching for another tool settles this one, rather than leaving it
1160+ // waiting on a binding that will never be called.
1161+ supersede = ( ) => {
1162+ pendingPicks . delete ( webContentsId ) ;
1163+ push ( { kind : "final" , result : null } ) ;
1164+ } ;
1165+ const nextEvent = ( ) =>
1166+ new Promise < PickEvent > ( ( resolve ) => {
1167+ const take = ( ) => {
1168+ const event = eventQueue . shift ( ) ;
1169+ if ( event === undefined ) {
1170+ notifyWaiter = take ;
1171+ return ;
1172+ }
1173+ notifyWaiter = null ;
1174+ resolve ( event ) ;
1175+ } ;
1176+ take ( ) ;
1177+ } ) ;
1178+
1179+ let earlyDescribed : DesktopPreviewPickedElement [ ] = [ ] ;
1180+ let picked : PickResult | null = null ;
1181+ for ( ; ; ) {
1182+ const event = yield * Effect . promise ( ( ) => nextEvent ( ) ) ;
1183+ if ( event . kind === "chosen" ) {
1184+ earlyDescribed = yield * describeStashed ( contents , PICK_OVERLAY_STASH , event . count ) . pipe (
1185+ Effect . orElseSucceed ( ( ) => [ ] ) ,
1186+ ) ;
1187+ continue ;
1188+ }
1189+ picked = event . result ;
1190+ break ;
1191+ }
1192+ closed = true ;
1193+ contents . debugger . off ( "message" , onMessage ) ;
11521194
11531195 // A superseded pick owns nothing on the page any more: its overlay was
11541196 // replaced, and tearing down would take its replacement with it.
@@ -1161,7 +1203,15 @@ export const make = Effect.sync(function PreviewAutomationMake() {
11611203 // the overlay's handles on the elements it chose.
11621204 const described : DesktopPreviewPickedElement [ ] = [ ] ;
11631205 if ( current && picked !== null ) {
1164- const elements = yield * describeStashed ( contents , PICK_OVERLAY_STASH , picked . count ) ;
1206+ let elements = yield * describeStashed ( contents , PICK_OVERLAY_STASH , picked . count ) . pipe (
1207+ Effect . orElseSucceed ( ( ) : DesktopPreviewPickedElement [ ] => [ ] ) ,
1208+ ) ;
1209+ // Fewer than were chosen means the document died with the stash --
1210+ // the note arrived from pagehide -- and the descriptions taken at
1211+ // choose time are the ones that still know the elements.
1212+ if ( elements . length < picked . count && earlyDescribed . length > elements . length ) {
1213+ elements = earlyDescribed ;
1214+ }
11651215 for ( const element of elements ) {
11661216 described . push ( {
11671217 ...element ,
0 commit comments