diff --git a/apps/browser-extension/entrypoints/background.ts b/apps/browser-extension/entrypoints/background.ts index 0ee09e3f..3b0a545f 100644 --- a/apps/browser-extension/entrypoints/background.ts +++ b/apps/browser-extension/entrypoints/background.ts @@ -6,7 +6,6 @@ import { } from "../utils/api" import { CONTAINER_TAGS, - CONTEXT_MENU_IDS, MESSAGE_TYPES, POSTHOG_EVENT_KEY, } from "../utils/constants" @@ -26,18 +25,6 @@ export default defineBackground(() => { let twitterImporter: TwitterImporter | null = null browser.runtime.onInstalled.addListener(async (details) => { - browser.contextMenus.create({ - id: CONTEXT_MENU_IDS.SAVE_TO_SUPERMEMORY, - title: "sync to supermemory", - contexts: ["selection", "page", "link"], - }) - - browser.contextMenus.create({ - id: CONTEXT_MENU_IDS.SEARCH_SUPERMEMORY, - title: "search supermemory", - contexts: ["selection"], - }) - if (details.reason === "install") { await trackEvent("extension_installed", { reason: details.reason, @@ -59,38 +46,6 @@ export default defineBackground(() => { ["requestHeaders", "extraHeaders"], ) - // Handle context menu clicks. - browser.contextMenus.onClicked.addListener(async (info, tab) => { - if (info.menuItemId === CONTEXT_MENU_IDS.SAVE_TO_SUPERMEMORY) { - if (tab?.id) { - try { - await browser.tabs.sendMessage(tab.id, { - action: MESSAGE_TYPES.SAVE_MEMORY, - actionSource: "context_menu", - }) - } catch (error) { - console.error("Failed to send message to content script:", error) - } - } - } - - if (info.menuItemId === CONTEXT_MENU_IDS.SEARCH_SUPERMEMORY) { - if (tab?.id && info.selectionText) { - try { - await browser.tabs.sendMessage(tab.id, { - action: MESSAGE_TYPES.OPEN_SEARCH_PANEL, - data: info.selectionText, - }) - } catch (error) { - console.error( - "Failed to send search message to content script:", - error, - ) - } - } - } - }) - // Send message to current active tab. const sendMessageToCurrentTab = async (message: string) => { const tabs = await browser.tabs.query({ @@ -319,30 +274,6 @@ export default defineBackground(() => { return true } - if (message.action === MESSAGE_TYPES.SEARCH_SELECTION) { - ;(async () => { - try { - const query = message.data as string - const responseData = await searchMemories(query) - await trackEvent(POSTHOG_EVENT_KEY.SELECTION_SEARCH_TRIGGERED, { - query_length: query.length, - }) - sendResponse({ success: true, data: responseData }) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error" - const isAuthError = - errorMessage.includes("Authentication") || - errorMessage.includes("token") - sendResponse({ - success: false, - error: errorMessage, - isAuthError, - }) - } - })() - return true - } }, ) }) diff --git a/apps/browser-extension/entrypoints/content/index.ts b/apps/browser-extension/entrypoints/content/index.ts index 34a77b6c..cba33ce2 100644 --- a/apps/browser-extension/entrypoints/content/index.ts +++ b/apps/browser-extension/entrypoints/content/index.ts @@ -2,10 +2,6 @@ import { DOMAINS, MESSAGE_TYPES } from "../../utils/constants" import { DOMUtils } from "../../utils/ui-components" import { initializeChatGPT } from "./chatgpt" import { initializeClaude } from "./claude" -import { - handleOpenSearchPanel, - initializeSelectionSearch, -} from "./selection-search" import { saveMemory, setupGlobalKeyboardShortcut, @@ -28,8 +24,6 @@ export default defineContentScript({ DOMUtils.showToast(message.state) } else if (message.action === MESSAGE_TYPES.SAVE_MEMORY) { await saveMemory() - } else if (message.action === MESSAGE_TYPES.OPEN_SEARCH_PANEL) { - handleOpenSearchPanel(message.data as string) } else if (message.action === MESSAGE_TYPES.TWITTER_IMPORT_OPEN_MODAL) { await openImportModal() } else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) { @@ -74,9 +68,6 @@ export default defineContentScript({ initializeT3() initializeTwitter() - // Initialize universal selection search - initializeSelectionSearch() - // Start observing for dynamic changes if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", observeForDynamicChanges) diff --git a/apps/browser-extension/entrypoints/content/selection-search.ts b/apps/browser-extension/entrypoints/content/selection-search.ts deleted file mode 100644 index 57c87188..00000000 --- a/apps/browser-extension/entrypoints/content/selection-search.ts +++ /dev/null @@ -1,647 +0,0 @@ -import { ELEMENT_IDS, MESSAGE_TYPES, UI_CONFIG } from "../../utils/constants" - -// State -let currentQuery = "" -let fabElement: HTMLElement | null = null -let panelElement: HTMLElement | null = null -const selectedResults: Set = new Set() - -/** - * Get the selection rectangle for positioning the FAB - */ -function getSelectionRect(): DOMRect | null { - const selection = window.getSelection() - if (!selection || selection.rangeCount === 0) return null - - const range = selection.getRangeAt(0) - return range.getBoundingClientRect() -} - -/** - * Check if the selection is inside our extension UI - */ -function isSelectionInsideExtensionUI(): boolean { - const selection = window.getSelection() - if (!selection || selection.rangeCount === 0) return false - - const anchorNode = selection.anchorNode - if (!anchorNode) return false - - const element = - anchorNode.nodeType === Node.ELEMENT_NODE - ? (anchorNode as Element) - : anchorNode.parentElement - - if (!element) return false - - // Check if selection is inside FAB or panel - return ( - !!element.closest(`#${ELEMENT_IDS.SELECTION_SEARCH_FAB}`) || - !!element.closest(`#${ELEMENT_IDS.SELECTION_SEARCH_PANEL}`) - ) -} - -/** - * Create the floating action button (FAB) - */ -function createFAB(): HTMLElement { - const fab = document.createElement("div") - fab.id = ELEMENT_IDS.SELECTION_SEARCH_FAB - - const iconUrl = browser.runtime.getURL("/icon-16.png") - - fab.innerHTML = ` - Search - Search - ` - - fab.style.cssText = ` - position: fixed; - z-index: 2147483646; - display: flex; - align-items: center; - gap: 6px; - padding: 8px 12px; - background: #05070A; - color: #ffffff; - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 8px; - font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - font-size: 13px; - font-weight: 500; - cursor: pointer; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - transition: all 0.15s ease; - user-select: none; - ` - - fab.addEventListener("mouseenter", () => { - fab.style.background = "#0F151F" - fab.style.borderColor = "rgba(255, 255, 255, 0.2)" - }) - - fab.addEventListener("mouseleave", () => { - fab.style.background = "#05070A" - fab.style.borderColor = "rgba(255, 255, 255, 0.1)" - }) - - fab.addEventListener("click", (e) => { - e.preventDefault() - e.stopPropagation() - triggerSearch() - }) - - return fab -} - -/** - * Show the FAB near the selection - */ -function showFAB(rect: DOMRect, text: string) { - hideFAB() - - currentQuery = text - fabElement = createFAB() - - // Position FAB above the selection, centered - const fabWidth = 90 // approximate width - let left = rect.left + rect.width / 2 - fabWidth / 2 - let top = rect.top - 40 - - // Ensure FAB stays within viewport - if (left < 10) left = 10 - if (left + fabWidth > window.innerWidth - 10) { - left = window.innerWidth - fabWidth - 10 - } - if (top < 10) { - // Show below selection if not enough space above - top = rect.bottom + 10 - } - - fabElement.style.left = `${left}px` - fabElement.style.top = `${top}px` - - document.body.appendChild(fabElement) -} - -/** - * Hide the FAB - */ -export function hideFAB() { - if (fabElement) { - fabElement.remove() - fabElement = null - } -} - -/** - * Trigger search with the current query - */ -async function triggerSearch() { - if (!currentQuery) return - - hideFAB() - showPanel(currentQuery, "loading") - - try { - const response = await browser.runtime.sendMessage({ - action: MESSAGE_TYPES.SEARCH_SELECTION, - data: currentQuery, - }) - - if (response.success) { - showPanel(currentQuery, "results", response.data) - } else if (response.isAuthError) { - showPanel(currentQuery, "auth_error") - } else { - showPanel(currentQuery, "error", null, response.error) - } - } catch (error) { - console.error("Search failed:", error) - showPanel( - currentQuery, - "error", - null, - error instanceof Error ? error.message : "Search failed", - ) - } -} - -/** - * Create and show the search results panel - */ -function showPanel( - query: string, - state: "loading" | "results" | "error" | "auth_error", - data?: unknown, - errorMessage?: string, -) { - hidePanel() - selectedResults.clear() - - panelElement = document.createElement("div") - panelElement.id = ELEMENT_IDS.SELECTION_SEARCH_PANEL - - panelElement.style.cssText = ` - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - z-index: 2147483647; - width: 420px; - max-width: 90vw; - max-height: 70vh; - background: #05070A; - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 12px; - font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); - display: flex; - flex-direction: column; - overflow: hidden; - ` - - // Header - const header = document.createElement("div") - header.style.cssText = ` - padding: 16px; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - display: flex; - justify-content: space-between; - align-items: center; - flex-shrink: 0; - ` - - const iconUrl = browser.runtime.getURL("/icon-16.png") - header.innerHTML = ` -
- supermemory - Search Results -
- - ` - - panelElement.appendChild(header) - - // Query display - const queryDisplay = document.createElement("div") - queryDisplay.style.cssText = ` - padding: 12px 16px; - background: rgba(91, 126, 245, 0.04); - border-bottom: 1px solid rgba(255, 255, 255, 0.05); - flex-shrink: 0; - ` - queryDisplay.innerHTML = ` -
Searching for:
-
${escapeHtml(query)}
- ` - panelElement.appendChild(queryDisplay) - - // Content area - const content = document.createElement("div") - content.id = "sm-panel-content" - content.style.cssText = ` - flex: 1; - overflow-y: auto; - padding: 16px; - min-height: 150px; - ` - - if (state === "loading") { - content.innerHTML = ` -
-
- Searching memories... -
- ` - } else if (state === "auth_error") { - content.innerHTML = ` -
-
Sign in required
-

Please sign in to supermemory to search your memories.

- -
- ` - } else if (state === "error") { - content.innerHTML = ` -
-
Search failed
-

${escapeHtml(errorMessage || "An error occurred")}

-
- ` - } else if (state === "results") { - const response = data as { - searchResults?: { results?: Array<{ memory?: string; id?: string }> } - } - const results = response?.searchResults?.results || [] - - if (results.length === 0) { - content.innerHTML = ` -
-
No memories found
-

Try a different search query.

-
- ` - } else { - content.innerHTML = results - .map( - (result, index) => ` -
- -
-

${escapeHtml(result.memory || "")}

-
-
- `, - ) - .join("") - } - } - - panelElement.appendChild(content) - - // Footer with copy button (only for results) - if (state === "results") { - const response = data as { - searchResults?: { results?: Array<{ memory?: string }> } - } - const results = response?.searchResults?.results || [] - - if (results.length > 0) { - const footer = document.createElement("div") - footer.style.cssText = ` - padding: 12px 16px; - border-top: 1px solid rgba(255, 255, 255, 0.1); - display: flex; - justify-content: space-between; - align-items: center; - flex-shrink: 0; - ` - footer.innerHTML = ` - 0 selected - - ` - panelElement.appendChild(footer) - } - } - - // Add animations style - if (!document.getElementById("sm-panel-styles")) { - const style = document.createElement("style") - style.id = "sm-panel-styles" - style.textContent = ` - @keyframes sm-spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } - } - .sm-result-item:hover { - background: rgba(91, 126, 245, 0.08) !important; - } - .sm-result-item.selected { - border-color: rgba(91, 190, 251, 0.3) !important; - background: rgba(91, 126, 245, 0.08) !important; - } - ` - document.head.appendChild(style) - } - - document.body.appendChild(panelElement) - - // Event listeners - setupPanelEventListeners(data) -} - -/** - * Setup event listeners for the panel - */ -function setupPanelEventListeners(data: unknown) { - if (!panelElement) return - - // Close button - const closeBtn = panelElement.querySelector("#sm-panel-close") - closeBtn?.addEventListener("click", hidePanel) - - // Sign in button - const signInBtn = panelElement.querySelector("#sm-sign-in-btn") - signInBtn?.addEventListener("click", () => { - window.open( - import.meta.env.PROD - ? "https://app.supermemory.ai/login" - : "http://localhost:3000/login", - "_blank", - ) - }) - - // Result item checkboxes - const checkboxes = panelElement.querySelectorAll( - '.sm-result-item input[type="checkbox"]', - ) - checkboxes.forEach((checkbox) => { - checkbox.addEventListener("change", (e) => { - const target = e.target as HTMLInputElement - const index = Number.parseInt(target.dataset.index || "0", 10) - const item = target.closest(".sm-result-item") as HTMLElement - - if (target.checked) { - selectedResults.add(index) - item?.classList.add("selected") - } else { - selectedResults.delete(index) - item?.classList.remove("selected") - } - - updateSelectionUI() - }) - }) - - // Result item click (toggle checkbox) - const items = panelElement.querySelectorAll(".sm-result-item") - items.forEach((item) => { - item.addEventListener("click", (e) => { - const target = e.target as HTMLElement - if (target.tagName === "INPUT") return - - const checkbox = item.querySelector( - 'input[type="checkbox"]', - ) as HTMLInputElement - if (checkbox) { - checkbox.checked = !checkbox.checked - checkbox.dispatchEvent(new Event("change")) - } - }) - }) - - // Copy button - const copyBtn = panelElement.querySelector("#sm-copy-btn") - copyBtn?.addEventListener("click", () => { - copySelectedResults(data) - }) - - // Close on escape - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") { - hidePanel() - } - } - document.addEventListener("keydown", handleKeyDown) - - // Close on outside click - const handleOutsideClick = (e: MouseEvent) => { - if (panelElement && !panelElement.contains(e.target as Node)) { - hidePanel() - } - } - setTimeout(() => { - document.addEventListener("click", handleOutsideClick) - }, 100) - - // Cleanup listeners when panel is removed - const observer = new MutationObserver(() => { - if (!document.contains(panelElement)) { - document.removeEventListener("keydown", handleKeyDown) - document.removeEventListener("click", handleOutsideClick) - observer.disconnect() - } - }) - observer.observe(document.body, { childList: true }) -} - -/** - * Update the selection count UI - */ -function updateSelectionUI() { - const countEl = document.getElementById("sm-selected-count") - const copyBtn = document.getElementById("sm-copy-btn") as HTMLButtonElement - - if (countEl) { - countEl.textContent = `${selectedResults.size} selected` - } - - if (copyBtn) { - copyBtn.disabled = selectedResults.size === 0 - copyBtn.style.opacity = selectedResults.size === 0 ? "0.5" : "1" - } -} - -/** - * Copy selected results to clipboard - */ -async function copySelectedResults(data: unknown) { - const response = data as { - searchResults?: { results?: Array<{ memory?: string }> } - } - const results = response?.searchResults?.results || [] - - const selectedMemories = Array.from(selectedResults) - .sort((a, b) => a - b) - .map((index) => results[index]?.memory) - .filter(Boolean) - - if (selectedMemories.length === 0) return - - // Format the copied content - const formattedContent = selectedMemories - .map((memory, i) => `${i + 1}. ${memory}`) - .join("\n\n") - - try { - await navigator.clipboard.writeText(formattedContent) - - // Show copied feedback - const copyBtn = document.getElementById("sm-copy-btn") - if (copyBtn) { - const originalText = copyBtn.textContent - copyBtn.textContent = "Copied!" - setTimeout(() => { - if (copyBtn) copyBtn.textContent = originalText - }, 1500) - } - - // Track event - browser.runtime.sendMessage({ - action: MESSAGE_TYPES.CAPTURE_PROMPT, - data: { - prompt: `Copied ${selectedMemories.length} memories`, - platform: "selection_search", - source: "copy_selected", - }, - }) - } catch (error) { - console.error("Failed to copy to clipboard:", error) - } -} - -/** - * Hide the panel - */ -export function hidePanel() { - if (panelElement) { - panelElement.remove() - panelElement = null - } - selectedResults.clear() -} - -/** - * Escape HTML to prevent XSS - */ -function escapeHtml(text: string): string { - const div = document.createElement("div") - div.textContent = text - return div.innerHTML -} - -/** - * Handle selection change - */ -function handleSelectionChange() { - const selection = window.getSelection() - const text = selection?.toString().trim() || "" - - // Hide FAB if selection is empty or inside extension UI - if ( - !text || - text.length < UI_CONFIG.SELECTION_MIN_LENGTH || - text.length > UI_CONFIG.SELECTION_MAX_LENGTH || - isSelectionInsideExtensionUI() - ) { - hideFAB() - return - } - - const rect = getSelectionRect() - if (rect && rect.width > 0 && rect.height > 0) { - showFAB(rect, text) - } -} - -/** - * Handle message from background to open search panel - */ -export function handleOpenSearchPanel(query: string) { - currentQuery = query - hideFAB() - triggerSearch() -} - -/** - * Initialize selection search functionality - */ -export function initializeSelectionSearch() { - // Listen for mouseup to detect selection - document.addEventListener("mouseup", () => { - // Small delay to ensure selection is complete - setTimeout(handleSelectionChange, 10) - }) - - // Listen for keyup for keyboard selection - document.addEventListener("keyup", (e) => { - if (e.shiftKey) { - setTimeout(handleSelectionChange, 10) - } - }) - - // Hide FAB when clicking elsewhere - document.addEventListener("mousedown", (e) => { - const target = e.target as HTMLElement - if ( - fabElement && - !fabElement.contains(target) && - !panelElement?.contains(target) - ) { - // Don't hide immediately to allow FAB click - setTimeout(() => { - const selection = window.getSelection() - if (!selection || selection.toString().trim().length === 0) { - hideFAB() - } - }, 100) - } - }) -} diff --git a/apps/browser-extension/utils/constants.ts b/apps/browser-extension/utils/constants.ts index 58481706..20977642 100644 --- a/apps/browser-extension/utils/constants.ts +++ b/apps/browser-extension/utils/constants.ts @@ -24,8 +24,6 @@ export const ELEMENT_IDS = { CLAUDE_INPUT_BAR_ELEMENT: "sm-claude-input-bar-element", T3_INPUT_BAR_ELEMENT: "sm-t3-input-bar-element", PROJECT_SELECTION_MODAL: "sm-project-selection-modal", - SELECTION_SEARCH_FAB: "sm-selection-search-fab", - SELECTION_SEARCH_PANEL: "sm-selection-search-panel", } as const /** @@ -51,8 +49,6 @@ export const UI_CONFIG = { OBSERVER_THROTTLE_DELAY: 300, // milliseconds between observer callback executions ROUTE_CHECK_INTERVAL: 2000, // milliseconds between route change checks API_REQUEST_TIMEOUT: 10000, // milliseconds for API request timeout - SELECTION_MIN_LENGTH: 3, // minimum characters to show FAB - SELECTION_MAX_LENGTH: 500, // maximum characters to show FAB } as const /** @@ -86,16 +82,9 @@ export const MESSAGE_TYPES = { GET_RELATED_MEMORIES: "sm-get-related-memories", CAPTURE_PROMPT: "sm-capture-prompt", FETCH_PROJECTS: "sm-fetch-projects", - SEARCH_SELECTION: "sm-search-selection", - OPEN_SEARCH_PANEL: "sm-open-search-panel", TWITTER_IMPORT_OPEN_MODAL: "sm-twitter-import-open-modal", } as const -export const CONTEXT_MENU_IDS = { - SAVE_TO_SUPERMEMORY: "sm-save-to-supermemory", - SEARCH_SUPERMEMORY: "sm-search-supermemory", -} as const - export const POSTHOG_EVENT_KEY = { TWITTER_IMPORT_STARTED: "twitter_import_started", SAVE_MEMORY_ATTEMPTED: "save_memory_attempted", @@ -107,6 +96,4 @@ export const POSTHOG_EVENT_KEY = { CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED: "claude_chat_memories_auto_searched", CHATGPT_CHAT_MEMORIES_SEARCHED: "chatgpt_chat_memories_searched", CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED: "chatgpt_chat_memories_auto_searched", - SELECTION_SEARCH_TRIGGERED: "selection_search_triggered", - SELECTION_SEARCH_COPIED: "selection_search_copied", } as const diff --git a/apps/browser-extension/wxt.config.ts b/apps/browser-extension/wxt.config.ts index 6d689557..9316ccdc 100644 --- a/apps/browser-extension/wxt.config.ts +++ b/apps/browser-extension/wxt.config.ts @@ -12,7 +12,7 @@ export default defineConfig({ name: "supermemory", homepage_url: "https://supermemory.ai", version: "6.1.0", - permissions: ["contextMenus", "storage", "activeTab", "webRequest", "tabs"], + permissions: ["storage", "activeTab", "webRequest", "tabs"], host_permissions: [ "*://x.com/*", "*://twitter.com/*",