Skip to content

Commit 0f61e45

Browse files
authored
Merge pull request #2626 from esokullu/main
Fix context-menu and side panel ownership race conditions
2 parents 14cc71a + 66bcfa2 commit 0f61e45

13 files changed

Lines changed: 2743 additions & 143 deletions

src/chrome/src/background.js

Lines changed: 113 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
buildSelectionPrompt,
3838
createContextMenuStorage,
3939
} from './context-menu-storage.js';
40+
import { createTabChatHandoffCoordinator } from './ui/tab-chat-persistence.js';
4041
import {
4142
prepareRecordingHost,
4243
startTabRecording,
@@ -164,6 +165,21 @@ function getContextMenuPromptStore() {
164165
}
165166

166167
const 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

168184
function 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

19301946
async 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 (/run is already active/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

Comments
 (0)