@@ -117,6 +117,8 @@ type AutomationSession = {
117117 windowId : number ;
118118 idleTimer : ReturnType < typeof setTimeout > | null ;
119119 idleDeadlineAt : number ;
120+ owned : boolean ;
121+ preferredTabId : number | null ;
120122} ;
121123
122124const automationSessions = new Map < string , AutomationSession > ( ) ;
@@ -134,6 +136,11 @@ function resetWindowIdleTimer(workspace: string): void {
134136 session . idleTimer = setTimeout ( async ( ) => {
135137 const current = automationSessions . get ( workspace ) ;
136138 if ( ! current ) return ;
139+ if ( ! current . owned ) {
140+ console . log ( `[opencli] Borrowed workspace ${ workspace } detached from window ${ current . windowId } (idle timeout)` ) ;
141+ automationSessions . delete ( workspace ) ;
142+ return ;
143+ }
137144 try {
138145 await chrome . windows . remove ( current . windowId ) ;
139146 console . log ( `[opencli] Automation window ${ current . windowId } (${ workspace } ) closed (idle timeout)` ) ;
@@ -177,6 +184,8 @@ async function getAutomationWindow(workspace: string, initialUrl?: string): Prom
177184 windowId : win . id ! ,
178185 idleTimer : null ,
179186 idleDeadlineAt : Date . now ( ) + WINDOW_IDLE_TIMEOUT ,
187+ owned : true ,
188+ preferredTabId : null ,
180189 } ;
181190 automationSessions . set ( workspace , session ) ;
182191 console . log ( `[opencli] Created automation window ${ session . windowId } (${ workspace } , start=${ startUrl } )` ) ;
@@ -279,6 +288,14 @@ async function handleCommand(cmd: Command): Promise<Result> {
279288 return await handleSessions ( cmd ) ;
280289 case 'set-file-input' :
281290 return await handleSetFileInput ( cmd , workspace ) ;
291+ case 'insert-text' :
292+ return await handleInsertText ( cmd , workspace ) ;
293+ case 'bind-current' :
294+ return await handleBindCurrent ( cmd , workspace ) ;
295+ case 'network-capture-start' :
296+ return await handleNetworkCaptureStart ( cmd , workspace ) ;
297+ case 'network-capture-read' :
298+ return await handleNetworkCaptureRead ( cmd , workspace ) ;
282299 default :
283300 return { id : cmd . id , ok : false , error : `Unknown action: ${ cmd . action } ` } ;
284301 }
@@ -326,7 +343,31 @@ function isTargetUrl(currentUrl: string | undefined, targetUrl: string): boolean
326343 return normalizeUrlForComparison ( currentUrl ) === normalizeUrlForComparison ( targetUrl ) ;
327344}
328345
329- function setWorkspaceSession ( workspace : string , session : Pick < AutomationSession , 'windowId' > ) : void {
346+ function matchesDomain ( url : string | undefined , domain : string ) : boolean {
347+ if ( ! url ) return false ;
348+ try {
349+ const parsed = new URL ( url ) ;
350+ return parsed . hostname === domain || parsed . hostname . endsWith ( `.${ domain } ` ) ;
351+ } catch {
352+ return false ;
353+ }
354+ }
355+
356+ function matchesBindCriteria ( tab : chrome . tabs . Tab , cmd : Command ) : boolean {
357+ if ( ! tab . id || ! isDebuggableUrl ( tab . url ) ) return false ;
358+ if ( cmd . matchDomain && ! matchesDomain ( tab . url , cmd . matchDomain ) ) return false ;
359+ if ( cmd . matchPathPrefix ) {
360+ try {
361+ const parsed = new URL ( tab . url ! ) ;
362+ if ( ! parsed . pathname . startsWith ( cmd . matchPathPrefix ) ) return false ;
363+ } catch {
364+ return false ;
365+ }
366+ }
367+ return true ;
368+ }
369+
370+ function setWorkspaceSession ( workspace : string , session : Omit < AutomationSession , 'idleTimer' | 'idleDeadlineAt' > ) : void {
330371 const existing = automationSessions . get ( workspace ) ;
331372 if ( existing ?. idleTimer ) clearTimeout ( existing . idleTimer ) ;
332373 automationSessions . set ( workspace , {
@@ -348,9 +389,11 @@ async function resolveTab(tabId: number | undefined, workspace: string, initialU
348389 try {
349390 const tab = await chrome . tabs . get ( tabId ) ;
350391 const session = automationSessions . get ( workspace ) ;
351- const matchesSession = session ? tab . windowId === session . windowId : false ;
392+ const matchesSession = session
393+ ? ( session . preferredTabId !== null ? session . preferredTabId === tabId : tab . windowId === session . windowId )
394+ : false ;
352395 if ( isDebuggableUrl ( tab . url ) && matchesSession ) return { tabId, tab } ;
353- if ( session && ! matchesSession && isDebuggableUrl ( tab . url ) ) {
396+ if ( session && ! matchesSession && session . preferredTabId === null && isDebuggableUrl ( tab . url ) ) {
354397 // Tab drifted to another window but content is still valid.
355398 // Try to move it back instead of abandoning it.
356399 console . warn ( `[opencli] Tab ${ tabId } drifted to window ${ tab . windowId } , moving back to ${ session . windowId } ` ) ;
@@ -371,6 +414,16 @@ async function resolveTab(tabId: number | undefined, workspace: string, initialU
371414 }
372415 }
373416
417+ const existingSession = automationSessions . get ( workspace ) ;
418+ if ( existingSession ?. preferredTabId !== null ) {
419+ try {
420+ const preferredTab = await chrome . tabs . get ( existingSession . preferredTabId ) ;
421+ if ( isDebuggableUrl ( preferredTab . url ) ) return { tabId : preferredTab . id ! , tab : preferredTab } ;
422+ } catch {
423+ automationSessions . delete ( workspace ) ;
424+ }
425+ }
426+
374427 // Get (or create) the automation window
375428 const windowId = await getAutomationWindow ( workspace , initialUrl ) ;
376429
@@ -408,6 +461,14 @@ async function resolveTabId(tabId: number | undefined, workspace: string, initia
408461async function listAutomationTabs ( workspace : string ) : Promise < chrome . tabs . Tab [ ] > {
409462 const session = automationSessions . get ( workspace ) ;
410463 if ( ! session ) return [ ] ;
464+ if ( session . preferredTabId !== null ) {
465+ try {
466+ return [ await chrome . tabs . get ( session . preferredTabId ) ] ;
467+ } catch {
468+ automationSessions . delete ( workspace ) ;
469+ return [ ] ;
470+ }
471+ }
411472 try {
412473 return await chrome . tabs . query ( { windowId : session . windowId } ) ;
413474 } catch {
@@ -681,10 +742,12 @@ async function handleCdp(cmd: Command, workspace: string): Promise<Result> {
681742async function handleCloseWindow ( cmd : Command , workspace : string ) : Promise < Result > {
682743 const session = automationSessions . get ( workspace ) ;
683744 if ( session ) {
684- try {
685- await chrome . windows . remove ( session . windowId ) ;
686- } catch {
687- // Window may already be closed
745+ if ( session . owned ) {
746+ try {
747+ await chrome . windows . remove ( session . windowId ) ;
748+ } catch {
749+ // Window may already be closed
750+ }
688751 }
689752 if ( session . idleTimer ) clearTimeout ( session . idleTimer ) ;
690753 automationSessions . delete ( workspace ) ;
@@ -705,6 +768,39 @@ async function handleSetFileInput(cmd: Command, workspace: string): Promise<Resu
705768 }
706769}
707770
771+ async function handleInsertText ( cmd : Command , workspace : string ) : Promise < Result > {
772+ if ( typeof cmd . text !== 'string' ) {
773+ return { id : cmd . id , ok : false , error : 'Missing text payload' } ;
774+ }
775+ const tabId = await resolveTabId ( cmd . tabId , workspace ) ;
776+ try {
777+ await executor . insertText ( tabId , cmd . text ) ;
778+ return { id : cmd . id , ok : true , data : { inserted : true } } ;
779+ } catch ( err ) {
780+ return { id : cmd . id , ok : false , error : err instanceof Error ? err . message : String ( err ) } ;
781+ }
782+ }
783+
784+ async function handleNetworkCaptureStart ( cmd : Command , workspace : string ) : Promise < Result > {
785+ const tabId = await resolveTabId ( cmd . tabId , workspace ) ;
786+ try {
787+ await executor . startNetworkCapture ( tabId , cmd . pattern ) ;
788+ return { id : cmd . id , ok : true , data : { started : true } } ;
789+ } catch ( err ) {
790+ return { id : cmd . id , ok : false , error : err instanceof Error ? err . message : String ( err ) } ;
791+ }
792+ }
793+
794+ async function handleNetworkCaptureRead ( cmd : Command , workspace : string ) : Promise < Result > {
795+ const tabId = await resolveTabId ( cmd . tabId , workspace ) ;
796+ try {
797+ const data = await executor . readNetworkCapture ( tabId ) ;
798+ return { id : cmd . id , ok : true , data } ;
799+ } catch ( err ) {
800+ return { id : cmd . id , ok : false , error : err instanceof Error ? err . message : String ( err ) } ;
801+ }
802+ }
803+
708804async function handleSessions ( cmd : Command ) : Promise < Result > {
709805 const now = Date . now ( ) ;
710806 const data = await Promise . all ( [ ...automationSessions . entries ( ) ] . map ( async ( [ workspace , session ] ) => ( {
@@ -716,11 +812,49 @@ async function handleSessions(cmd: Command): Promise<Result> {
716812 return { id : cmd . id , ok : true , data } ;
717813}
718814
815+ async function handleBindCurrent ( cmd : Command , workspace : string ) : Promise < Result > {
816+ const activeTabs = await chrome . tabs . query ( { active : true , lastFocusedWindow : true } ) ;
817+ const fallbackTabs = await chrome . tabs . query ( { lastFocusedWindow : true } ) ;
818+ const allTabs = await chrome . tabs . query ( { } ) ;
819+ const boundTab = activeTabs . find ( ( tab ) => matchesBindCriteria ( tab , cmd ) )
820+ ?? fallbackTabs . find ( ( tab ) => matchesBindCriteria ( tab , cmd ) )
821+ ?? allTabs . find ( ( tab ) => matchesBindCriteria ( tab , cmd ) ) ;
822+ if ( ! boundTab ?. id ) {
823+ return {
824+ id : cmd . id ,
825+ ok : false ,
826+ error : cmd . matchDomain || cmd . matchPathPrefix
827+ ? `No visible tab matching ${ cmd . matchDomain ?? 'domain' } ${ cmd . matchPathPrefix ? ` ${ cmd . matchPathPrefix } ` : '' } `
828+ : 'No active debuggable tab found' ,
829+ } ;
830+ }
831+
832+ setWorkspaceSession ( workspace , {
833+ windowId : boundTab . windowId ,
834+ owned : false ,
835+ preferredTabId : boundTab . id ,
836+ } ) ;
837+ resetWindowIdleTimer ( workspace ) ;
838+ console . log ( `[opencli] Workspace ${ workspace } explicitly bound to tab ${ boundTab . id } (${ boundTab . url } )` ) ;
839+ return {
840+ id : cmd . id ,
841+ ok : true ,
842+ data : {
843+ tabId : boundTab . id ,
844+ windowId : boundTab . windowId ,
845+ url : boundTab . url ,
846+ title : boundTab . title ,
847+ workspace,
848+ } ,
849+ } ;
850+ }
851+
719852export const __test__ = {
720853 handleNavigate,
721854 isTargetUrl,
722855 handleTabs,
723856 handleSessions,
857+ handleBindCurrent,
724858 resolveTabId,
725859 resetWindowIdleTimer,
726860 getSession : ( workspace : string = 'default' ) => automationSessions . get ( workspace ) ?? null ,
@@ -734,9 +868,11 @@ export const __test__ = {
734868 }
735869 setWorkspaceSession ( workspace , {
736870 windowId,
871+ owned : true ,
872+ preferredTabId : null ,
737873 } ) ;
738874 } ,
739- setSession : ( workspace : string , session : { windowId : number } ) => {
875+ setSession : ( workspace : string , session : { windowId : number ; owned : boolean ; preferredTabId : number | null } ) => {
740876 setWorkspaceSession ( workspace , session ) ;
741877 } ,
742878} ;
0 commit comments