diff --git a/apps/agent/entrypoints/background/index.ts b/apps/agent/entrypoints/background/index.ts index 3cdfb825..1ca4d940 100644 --- a/apps/agent/entrypoints/background/index.ts +++ b/apps/agent/entrypoints/background/index.ts @@ -31,6 +31,17 @@ export default defineBackground(() => { scheduledJobRuns() + // Track which tabs belong to each agent conversation for side panel management + const agentTabSets = new Map>() + + chrome.tabs.onRemoved.addListener((tabId) => { + for (const [conversationId, tabSet] of agentTabSets) { + tabSet.delete(tabId) + // Prune empty entries to prevent unbounded growth + if (tabSet.size === 0) agentTabSets.delete(conversationId) + } + }) + chrome.action.onClicked.addListener(async (tab) => { if (tab.id) { await toggleSidePanel(tab.id) @@ -91,6 +102,29 @@ export default defineBackground(() => { timestamp: Date.now(), }) } + + // Open side panel on tabs the agent interacts with + if ( + message?.type === 'open-sidepanel-on-tab' && + message?.tabId && + message?.conversationId + ) { + const { tabId, conversationId } = message + let tabSet = agentTabSets.get(conversationId) + if (!tabSet) { + tabSet = new Set() + agentTabSets.set(conversationId, tabSet) + } + if (!tabSet.has(tabId)) { + tabSet.add(tabId) + openSidePanel(tabId).catch(() => {}) + } + } + + // Clean up tab tracking when conversation resets + if (message?.type === 'clear-agent-tabs' && message?.conversationId) { + agentTabSets.delete(message.conversationId) + } }) sessionStorage.watch(async (newSession) => { diff --git a/apps/agent/entrypoints/sidepanel/index/useChatSession.ts b/apps/agent/entrypoints/sidepanel/index/useChatSession.ts index c9d13e7a..b5a21529 100644 --- a/apps/agent/entrypoints/sidepanel/index/useChatSession.ts +++ b/apps/agent/entrypoints/sidepanel/index/useChatSession.ts @@ -468,6 +468,13 @@ export const useChatSession = (options?: ChatSessionOptions) => { const resetConversation = () => { track(CONVERSATION_RESET_EVENT, { message_count: messages.length }) stop() + // Clear agent tab tracking in background before generating new conversationId + chrome.runtime + .sendMessage({ + type: 'clear-agent-tabs', + conversationId: conversationIdRef.current, + }) + .catch(() => {}) setConversationId(crypto.randomUUID()) setMessages([]) setTextToAction(new Map()) diff --git a/apps/agent/entrypoints/sidepanel/index/useNotifyActiveTab.tsx b/apps/agent/entrypoints/sidepanel/index/useNotifyActiveTab.tsx index 14483e2a..3a4dc427 100644 --- a/apps/agent/entrypoints/sidepanel/index/useNotifyActiveTab.tsx +++ b/apps/agent/entrypoints/sidepanel/index/useNotifyActiveTab.tsx @@ -29,6 +29,7 @@ export const useNotifyActiveTab = ({ conversationId: string }) => { const lastTabIdRef = useRef(null) + const knownTabsRef = useRef>(new Set()) const lastMessage = messages?.[messages.length - 1] @@ -39,6 +40,11 @@ export const useNotifyActiveTab = ({ const hasToolCalls = !!latestTool const toolTabId = extractTabId(latestTool as ToolUIPart | null) + // biome-ignore lint/correctness/useExhaustiveDependencies: intentionally reset when conversationId changes + useEffect(() => { + knownTabsRef.current = new Set() + }, [conversationId]) + useEffect(() => { const isStreaming = status === 'streaming' const previousTabId = lastTabIdRef.current @@ -82,6 +88,18 @@ export const useNotifyActiveTab = ({ if (cancelled || !targetTabId) return + // Open side panel on tabs the agent hasn't seen yet + if (!knownTabsRef.current.has(targetTabId)) { + knownTabsRef.current.add(targetTabId) + chrome.runtime + .sendMessage({ + type: 'open-sidepanel-on-tab', + tabId: targetTabId, + conversationId, + }) + .catch(() => {}) + } + if (previousTabId && previousTabId !== targetTabId) { const deactivateMessage: GlowMessage = { conversationId,