diff --git a/packages/browseros-agent/apps/agent/entrypoints/background/index.ts b/packages/browseros-agent/apps/agent/entrypoints/background/index.ts index 3cdfb825c..d5390f6d2 100644 --- a/packages/browseros-agent/apps/agent/entrypoints/background/index.ts +++ b/packages/browseros-agent/apps/agent/entrypoints/background/index.ts @@ -18,6 +18,7 @@ import { syncScheduledJobs, } from '@/lib/schedules/scheduleStorage' import { searchActionsStorage } from '@/lib/search-actions/searchActionsStorage' +import { selectedTextStorage } from '@/lib/selected-text/selectedTextStorage' import { stopAgentStorage } from '@/lib/stop-agent/stop-agent-storage' import { scheduledJobRuns } from './scheduledJobRuns' @@ -66,7 +67,12 @@ export default defineBackground(() => { } }) - chrome.runtime.onMessage.addListener((message, sender) => { + chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message?.type === 'get-tab-id') { + sendResponse({ tabId: sender.tab?.id }) + return true + } + if (message?.type === 'AUTH_SUCCESS' && sender.tab?.id) { const tabId = sender.tab.id authRedirectPathStorage @@ -93,6 +99,17 @@ export default defineBackground(() => { } }) + // Clean up selected text storage when a tab is closed + chrome.tabs.onRemoved.addListener((tabId) => { + const key = String(tabId) + selectedTextStorage.getValue().then((map) => { + if (map[key]) { + const { [key]: _, ...rest } = map + selectedTextStorage.setValue(rest) + } + }) + }) + sessionStorage.watch(async (newSession) => { if (newSession?.user?.id) { try { diff --git a/packages/browseros-agent/apps/agent/entrypoints/selection.content.ts b/packages/browseros-agent/apps/agent/entrypoints/selection.content.ts new file mode 100644 index 000000000..8084d02b1 --- /dev/null +++ b/packages/browseros-agent/apps/agent/entrypoints/selection.content.ts @@ -0,0 +1,42 @@ +import { selectedTextStorage } from '@/lib/selected-text/selectedTextStorage' + +const MAX_SELECTED_TEXT_LENGTH = 5000 + +export default defineContentScript({ + matches: ['*://*/*'], + runAt: 'document_idle', + async main() { + const response = await chrome.runtime.sendMessage({ type: 'get-tab-id' }) + const tabId: number | undefined = response?.tabId + if (!tabId) return + + const key = String(tabId) + + document.addEventListener('mouseup', () => { + const text = window.getSelection()?.toString().trim() + + if (text && text.length > 0) { + selectedTextStorage.getValue().then((map) => { + selectedTextStorage.setValue({ + ...map, + [key]: { + text: text.slice(0, MAX_SELECTED_TEXT_LENGTH), + pageUrl: window.location.href, + pageTitle: document.title, + tabId, + timestamp: Date.now(), + }, + }) + }) + } else { + // User clicked without selecting — clear this tab's entry only + selectedTextStorage.getValue().then((map) => { + if (map[key]) { + const { [key]: _, ...rest } = map + selectedTextStorage.setValue(rest) + } + }) + } + }) + }, +}) diff --git a/packages/browseros-agent/apps/agent/entrypoints/sidepanel/index/ChatFooter.tsx b/packages/browseros-agent/apps/agent/entrypoints/sidepanel/index/ChatFooter.tsx index ea6826364..b85aec285 100644 --- a/packages/browseros-agent/apps/agent/entrypoints/sidepanel/index/ChatFooter.tsx +++ b/packages/browseros-agent/apps/agent/entrypoints/sidepanel/index/ChatFooter.tsx @@ -8,12 +8,17 @@ import { useGetUserMCPIntegrations } from '@/entrypoints/app/connect-mcp/useGetU import { Feature } from '@/lib/browseros/capabilities' import { useCapabilities } from '@/lib/browseros/useCapabilities' import { useMcpServers } from '@/lib/mcp/mcpServerStorage' +import { + type SelectedTextData, + selectedTextStorage, +} from '@/lib/selected-text/selectedTextStorage' import { cn } from '@/lib/utils' import type { VoiceInputState } from '@/lib/voice/useVoiceInput' import { useWorkspace } from '@/lib/workspace/use-workspace' import { ChatAttachedTabs } from './ChatAttachedTabs' import { ChatInput, type ChatInputHandle } from './ChatInput' import { ChatModeToggle } from './ChatModeToggle' +import { ChatSelectedText } from './ChatSelectedText' import type { ChatMode } from './chatTypes' interface ChatFooterProps { @@ -48,6 +53,33 @@ export const ChatFooter: FC = ({ const { servers: mcpServers } = useMcpServers() const { data: userMCPIntegrations } = useGetUserMCPIntegrations() const chatInputRef = useRef(null) + const [selectionMap, setSelectionMap] = useState< + Record + >({}) + const [activeTabId, setActiveTabId] = useState() + + // Track active tab for tab-scoped selection display + useEffect(() => { + chrome.tabs + .query({ active: true, currentWindow: true }) + .then((tabs) => setActiveTabId(tabs[0]?.id)) + const listener = (activeInfo: { tabId: number }) => { + setActiveTabId(activeInfo.tabId) + } + chrome.tabs.onActivated.addListener(listener) + return () => chrome.tabs.onActivated.removeListener(listener) + }, []) + + // Watch selected text storage (per-tab map) + useEffect(() => { + selectedTextStorage.getValue().then(setSelectionMap) + const unwatch = selectedTextStorage.watch(setSelectionMap) + return () => unwatch() + }, []) + + const visibleSelectedText = activeTabId + ? (selectionMap[String(activeTabId)] ?? null) + : null const [isTabMentionOpen, setIsTabMentionOpen] = useState(false) useEffect(() => { @@ -81,6 +113,19 @@ export const ChatFooter: FC = ({ return (