@@ -37,6 +37,7 @@ import {
3737 buildSelectionPrompt ,
3838 createContextMenuStorage ,
3939} from './context-menu-storage.js' ;
40+ import { createTabChatHandoffCoordinator } from './ui/tab-chat-persistence.js' ;
4041import {
4142 prepareRecordingHost ,
4243 startTabRecording ,
@@ -164,6 +165,21 @@ function getContextMenuPromptStore() {
164165}
165166
166167const contextMenuStorage = createContextMenuStorage ( getContextMenuPromptStore ) ;
168+ const tabChatHandoff = createTabChatHandoffCoordinator ( chrome . storage . session , {
169+ requestHandoff : async ( tabId , { ownerId, generation } ) => {
170+ try {
171+ return await chrome . runtime . sendMessage ( {
172+ target : 'sidepanel' ,
173+ action : 'tab_chat_handoff_request' ,
174+ tabId,
175+ ownerId,
176+ generation,
177+ } ) ;
178+ } catch {
179+ return null ;
180+ }
181+ } ,
182+ } ) ;
167183
168184function createContextMenus ( ) {
169185 if ( ! chrome . contextMenus ?. create ) return ;
@@ -1689,8 +1705,8 @@ chrome.tabs.onRemoved.addListener((tabId) => {
16891705 clearTimeout ( pendingContextMenuNotifications . get ( tabId ) ) ;
16901706 pendingContextMenuNotifications . delete ( tabId ) ;
16911707 contextMenuStorage . cleanup ( tabId ) ;
1708+ tabChatHandoff . clear ( tabId ) . catch ( ( ) => { } ) ;
16921709 savePanelTabs ( ) ;
1693- chrome . storage . session ?. remove ( `tabChat:${ tabId } ` ) . catch ( ( ) => { } ) ;
16941710 scheduler . cancelForTab ( tabId ) . catch ( ( ) => { } ) ;
16951711 agent . clearDevCssPatchesForTab ( tabId ) . catch ( ( ) => { } ) ;
16961712 try { agent . _cleanupTab ( tabId ) ; } catch { /* ignore */ }
@@ -1928,7 +1944,13 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
19281944} ) ;
19291945
19301946async function handleMessage ( msg , sender ) {
1931- const lightweightAction = msg . action === 'get_recording_state' ;
1947+ const lightweightAction = [
1948+ 'get_recording_state' ,
1949+ 'persist_tab_chat' ,
1950+ 'load_tab_chat' ,
1951+ 'clear_tab_chat' ,
1952+ 'release_context_menu_prompt_claim' ,
1953+ ] . includes ( msg . action ) ;
19321954 if ( ! lightweightAction ) {
19331955 // Ensure providers are loaded
19341956 if ( providerManager . providers . size === 0 ) {
@@ -2151,8 +2173,41 @@ async function handleMessage(msg, sender) {
21512173 } ;
21522174 }
21532175
2154- case 'chat_start' :
2155- return launchDetachedRun ( 'chat' , msg , sender ) ;
2176+ case 'chat_start' : {
2177+ const claim = msg . contextMenuClaim ;
2178+ if ( ! claim ?. promptId || ! claim ?. claimantId ) {
2179+ return launchDetachedRun ( 'chat' , msg , sender ) ;
2180+ }
2181+ const tabId = msg . tabId || sender . tab ?. id ;
2182+ try {
2183+ const reservation = await contextMenuStorage . reserve (
2184+ tabId ,
2185+ claim . promptId ,
2186+ claim . claimantId ,
2187+ ( ) => launchDetachedRun ( 'chat' , msg , sender ) ,
2188+ ) ;
2189+ if ( reservation ?. reserved ) return reservation ;
2190+ return {
2191+ ok : false ,
2192+ accepted : false ,
2193+ code : 'context-menu-reservation-rejected' ,
2194+ reason : reservation ?. reason || 'claim-lost' ,
2195+ leaseExpiresAt : reservation ?. leaseExpiresAt ,
2196+ retryAfterMs : reservation ?. reason === 'run-active' ? 1_000 : undefined ,
2197+ } ;
2198+ } catch ( error ) {
2199+ if ( / r u n i s a l r e a d y a c t i v e / i. test ( String ( error ?. message || '' ) ) ) {
2200+ return {
2201+ ok : false ,
2202+ accepted : false ,
2203+ code : 'context-menu-reservation-rejected' ,
2204+ reason : 'run-active' ,
2205+ retryAfterMs : 1_000 ,
2206+ } ;
2207+ }
2208+ throw error ;
2209+ }
2210+ }
21562211
21572212 case 'continue_start' :
21582213 return launchDetachedRun ( 'continue' , msg , sender ) ;
@@ -2586,11 +2641,65 @@ async function handleMessage(msg, sender) {
25862641 return await contextMenuStorage . consume ( tabId ) ;
25872642 }
25882643
2644+ case 'claim_context_menu_prompt' : {
2645+ const tabId = msg . tabId || sender . tab ?. id ;
2646+ if ( ! tabId ) return { ok : false , claimed : false , error : 'No tab ID' } ;
2647+ return await contextMenuStorage . claim (
2648+ tabId ,
2649+ msg . promptId ,
2650+ msg . claimantId ,
2651+ ( ) => agent . activeRunState ( tabId ) ?. running || detachedRunStarts . has ( tabId ) ,
2652+ ) ;
2653+ }
2654+
2655+ case 'release_context_menu_prompt_claim' : {
2656+ const tabId = msg . tabId || sender . tab ?. id ;
2657+ const result = await contextMenuStorage . release (
2658+ tabId ,
2659+ msg . promptId ,
2660+ msg . claimantId ,
2661+ ) ;
2662+ if ( result ?. released && result . prompt ?. text ) {
2663+ notifySidePanelOfContextMenuPrompt ( result . prompt ) ;
2664+ }
2665+ return result ;
2666+ }
2667+
25892668 case 'clear_context_menu_prompt' : {
25902669 const tabId = msg . tabId || sender . tab ?. id ;
25912670 return await contextMenuStorage . clear ( tabId , msg . promptId ) ;
25922671 }
25932672
2673+ case 'persist_tab_chat' :
2674+ return await tabChatHandoff . save ( msg . tabId || sender . tab ?. id , msg . html , {
2675+ ownerId : msg . handoffOwnerId ,
2676+ handoffGeneration : msg . handoffGeneration ,
2677+ } ) ;
2678+
2679+ case 'load_tab_chat' :
2680+ return await tabChatHandoff . load ( msg . tabId || sender . tab ?. id , {
2681+ waitForHandoff : msg . waitForHandoff === true ,
2682+ claimantId : msg . handoffOwnerId ,
2683+ } ) ;
2684+
2685+ case 'clear_tab_chat' : {
2686+ const tabId = msg . tabId || sender . tab ?. id ;
2687+ const result = await tabChatHandoff . clear ( tabId , {
2688+ ownerId : msg . handoffOwnerId ,
2689+ handoffGeneration : msg . handoffGeneration ,
2690+ } ) ;
2691+ if ( result ?. ok && ! result . skipped ) {
2692+ chrome . runtime . sendMessage ( {
2693+ target : 'sidepanel' ,
2694+ action : 'tab_chat_cleared' ,
2695+ tabId,
2696+ handoffOwnerId : result . handoffOwnerId ,
2697+ handoffGeneration : result . handoffGeneration ,
2698+ } ) . catch ( ( ) => { } ) ;
2699+ }
2700+ return result ;
2701+ }
2702+
25942703 case 'list_scheduled_jobs' : {
25952704 const tabId = msg . tabId || sender . tab ?. id || null ;
25962705 return { ok : true , jobs : await scheduler . listJobs ( { tabId : msg . all ? null : tabId } ) } ;
0 commit comments