@@ -23,6 +23,7 @@ const CLEANUP_ALARM = "retention-cleanup";
2323const CLEANUP_INTERVAL_MINUTES = 360 ;
2424const RUNTIME_STORAGE_KEY = "runtime_state_v2" ;
2525const FOCUS_MEANINGFUL_THRESHOLD_SEC = 10 ;
26+ const PANEL_HEARTBEAT_TTL_MS = 10_000 ;
2627
2728const state = {
2829 focusedWindowId : chrome . windows . WINDOW_ID_NONE ,
@@ -33,7 +34,8 @@ const state = {
3334 theme : "dark" ,
3435 openPanelOnActionClick : null ,
3536 lastActionClickResult : null ,
36- lastOpenSidePanelResult : null
37+ lastOpenSidePanelResult : null ,
38+ panelHeartbeatByWindow : { }
3739} ;
3840
3941const sessionEngine = createSessionEngine ( {
@@ -55,6 +57,64 @@ function broadcastTheme(theme) {
5557 } ) ;
5658}
5759
60+ function broadcastSidePanelState ( windowId , isOpen ) {
61+ chrome . runtime . sendMessage ( { type : "side-panel-state-changed" , windowId, isOpen } ) . catch ( ( ) => {
62+ // Ignore when no extension page is listening.
63+ } ) ;
64+ }
65+
66+ function prunePanelHeartbeats ( now = Date . now ( ) ) {
67+ for ( const [ windowIdRaw , lastSeenAtRaw ] of Object . entries ( state . panelHeartbeatByWindow ) ) {
68+ const windowId = Number ( windowIdRaw ) ;
69+ const lastSeenAt = Number ( lastSeenAtRaw ) ;
70+ if ( ! Number . isFinite ( windowId ) || ! Number . isFinite ( lastSeenAt ) || now - lastSeenAt > PANEL_HEARTBEAT_TTL_MS ) {
71+ delete state . panelHeartbeatByWindow [ windowIdRaw ] ;
72+ }
73+ }
74+ }
75+
76+ function registerPanelHeartbeat ( windowId , at = Date . now ( ) ) {
77+ if ( typeof windowId !== "number" ) {
78+ return false ;
79+ }
80+
81+ const key = String ( windowId ) ;
82+ const wasOpen = key in state . panelHeartbeatByWindow ;
83+ state . panelHeartbeatByWindow [ key ] = at ;
84+ if ( ! wasOpen ) {
85+ broadcastSidePanelState ( windowId , true ) ;
86+ }
87+ return true ;
88+ }
89+
90+ function markPanelClosed ( windowId ) {
91+ if ( typeof windowId !== "number" ) {
92+ return false ;
93+ }
94+
95+ const key = String ( windowId ) ;
96+ const wasOpen = key in state . panelHeartbeatByWindow ;
97+ delete state . panelHeartbeatByWindow [ key ] ;
98+ if ( wasOpen ) {
99+ broadcastSidePanelState ( windowId , false ) ;
100+ }
101+ return wasOpen ;
102+ }
103+
104+ function isPanelOpenForWindow ( windowId , now = Date . now ( ) ) {
105+ prunePanelHeartbeats ( now ) ;
106+ if ( typeof windowId !== "number" ) {
107+ return false ;
108+ }
109+
110+ const lastSeenAt = Number ( state . panelHeartbeatByWindow [ String ( windowId ) ] ) ;
111+ if ( ! Number . isFinite ( lastSeenAt ) ) {
112+ return false ;
113+ }
114+
115+ return now - lastSeenAt <= PANEL_HEARTBEAT_TTL_MS ;
116+ }
117+
58118function getSenderWindowId ( sender ) {
59119 return typeof sender ?. tab ?. windowId === "number" ? sender . tab . windowId : null ;
60120}
@@ -331,6 +391,7 @@ async function runRetentionCleanup() {
331391 const cutoffTimestampMs = Date . now ( ) - retentionDays * 24 * 60 * 60 * 1000 ;
332392 await pruneSessionsOlderThan ( cutoffTimestampMs ) ;
333393 await pruneTabActivitiesOlderThan ( cutoffTimestampMs ) ;
394+ prunePanelHeartbeats ( ) ;
334395
335396 const snapshots = await listTabSnapshots ( ) ;
336397 await Promise . all (
@@ -627,6 +688,10 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
627688 return { opened, mode, senderWindowId } ;
628689 } ) ( )
629690 . then ( ( result ) => {
691+ if ( result . opened && typeof result . senderWindowId === "number" ) {
692+ registerPanelHeartbeat ( result . senderWindowId ) ;
693+ }
694+
630695 state . lastOpenSidePanelResult = {
631696 ok : result . opened ,
632697 opened : result . opened ,
@@ -645,6 +710,20 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
645710 return true ;
646711 }
647712
713+ if ( message ?. type === "panel-heartbeat" ) {
714+ const windowId = typeof message ?. windowId === "number" ? message . windowId : getSenderWindowId ( _sender ) ;
715+ const ok = registerPanelHeartbeat ( windowId ) ;
716+ sendResponse ( { ok, windowId } ) ;
717+ return false ;
718+ }
719+
720+ if ( message ?. type === "panel-closed" ) {
721+ const windowId = typeof message ?. windowId === "number" ? message . windowId : getSenderWindowId ( _sender ) ;
722+ const closed = markPanelClosed ( windowId ) ;
723+ sendResponse ( { ok : true , windowId, closed } ) ;
724+ return false ;
725+ }
726+
648727 if ( message ?. type === "debug-trigger-action-click" ) {
649728 executeActionClick ( {
650729 getFocusedWindowId : async ( ) => {
@@ -676,6 +755,10 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
676755 }
677756
678757 if ( message ?. type === "get-runtime-status" ) {
758+ const senderWindowId = getSenderWindowId ( _sender ) ;
759+ const requestedWindowId = typeof message ?. windowId === "number" ? message . windowId : senderWindowId ;
760+ const sidePanelOpenForWindow = isPanelOpenForWindow ( requestedWindowId ) ;
761+
679762 const runtimeState = sessionEngine . readState ( ) ;
680763 sendResponse ( {
681764 ok : true ,
@@ -688,7 +771,9 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
688771 sidePanelApiAvailable : Boolean ( chrome . sidePanel ?. open ) ,
689772 openPanelOnActionClick : state . openPanelOnActionClick ,
690773 lastActionClickResult : state . lastActionClickResult ,
691- lastOpenSidePanelResult : state . lastOpenSidePanelResult
774+ lastOpenSidePanelResult : state . lastOpenSidePanelResult ,
775+ sidePanelOpenForWindow,
776+ sidePanelWindowId : requestedWindowId
692777 } ) ;
693778 return false ;
694779 }
0 commit comments