aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDhravya Shah <[email protected]>2025-09-10 19:13:40 -0700
committerDhravya Shah <[email protected]>2025-09-10 19:13:40 -0700
commita17655460f77f533bfbd4b15fa4a4ff9fe443008 (patch)
treee0f5f02d885a16509b32814e95849208611ce597
parentmake docs public (diff)
parentfeat (extension) : Auto Search Toggle for Chat Applications (#418) (diff)
downloadsupermemory-a17655460f77f533bfbd4b15fa4a4ff9fe443008.tar.xz
supermemory-a17655460f77f533bfbd4b15fa4a4ff9fe443008.zip
Merge branch 'main' of https://github.com/supermemoryai/supermemory
-rw-r--r--apps/browser-extension/entrypoints/background.ts41
-rw-r--r--apps/browser-extension/entrypoints/content.ts674
-rw-r--r--apps/browser-extension/entrypoints/content/chatgpt.ts724
-rw-r--r--apps/browser-extension/entrypoints/content/claude.ts648
-rw-r--r--apps/browser-extension/entrypoints/content/index.ts67
-rw-r--r--apps/browser-extension/entrypoints/content/shared.ts77
-rw-r--r--apps/browser-extension/entrypoints/content/t3.ts692
-rw-r--r--apps/browser-extension/entrypoints/content/twitter.ts102
-rw-r--r--apps/browser-extension/entrypoints/popup/App.tsx77
-rw-r--r--apps/browser-extension/utils/constants.ts9
-rw-r--r--apps/browser-extension/utils/memory-popup.ts93
-rw-r--r--apps/browser-extension/utils/route-detection.ts117
-rw-r--r--apps/browser-extension/utils/types.ts3
-rw-r--r--apps/browser-extension/utils/ui-components.ts9
-rw-r--r--apps/browser-extension/wxt.config.ts4
15 files changed, 2641 insertions, 696 deletions
diff --git a/apps/browser-extension/entrypoints/background.ts b/apps/browser-extension/entrypoints/background.ts
index 05fef55d..9d46a5e9 100644
--- a/apps/browser-extension/entrypoints/background.ts
+++ b/apps/browser-extension/entrypoints/background.ts
@@ -23,7 +23,7 @@ export default defineBackground(() => {
browser.runtime.onInstalled.addListener(async (details) => {
browser.contextMenus.create({
id: CONTEXT_MENU_IDS.SAVE_TO_SUPERMEMORY,
- title: "Save to supermemory",
+ title: "sync to supermemory",
contexts: ["selection", "page", "link"],
})
@@ -114,7 +114,9 @@ export default defineBackground(() => {
const payload: MemoryPayload = {
containerTags: [containerTag],
- content: `${data.highlightedText}\n\n${data.html}\n\n${data?.url}`,
+ content:
+ data.content ||
+ `${data.highlightedText}\n\n${data.html}\n\n${data?.url}`,
metadata: { sm_source: "consumer" },
}
@@ -144,9 +146,9 @@ export default defineBackground(() => {
const response = responseData as {
results?: Array<{ memory?: string }>
}
- let memories = ""
+ const memories: string[] = []
response.results?.forEach((result, index) => {
- memories += `[${index + 1}] ${result.memory} `
+ memories.push(`${index + 1}. ${result.memory} \n`)
})
console.log("Memories:", memories)
await trackEvent(eventSource)
@@ -216,6 +218,37 @@ export default defineBackground(() => {
})()
return true
}
+
+ if (message.action === MESSAGE_TYPES.CAPTURE_PROMPT) {
+ ;(async () => {
+ try {
+ const messageData = message.data as {
+ prompt: string
+ platform: string
+ source: string
+ }
+ console.log("=== PROMPT CAPTURED ===")
+ console.log(messageData)
+ console.log("========================")
+
+ const memoryData: MemoryData = {
+ content: messageData.prompt,
+ }
+
+ const result = await saveMemoryToSupermemory(
+ memoryData,
+ `prompt_capture_${messageData.platform}`,
+ )
+ sendResponse(result)
+ } catch (error) {
+ sendResponse({
+ success: false,
+ error: error instanceof Error ? error.message : "Unknown error",
+ })
+ }
+ })()
+ return true
+ }
},
)
})
diff --git a/apps/browser-extension/entrypoints/content.ts b/apps/browser-extension/entrypoints/content.ts
deleted file mode 100644
index e755efa6..00000000
--- a/apps/browser-extension/entrypoints/content.ts
+++ /dev/null
@@ -1,674 +0,0 @@
-import {
- DOMAINS,
- ELEMENT_IDS,
- MESSAGE_TYPES,
- POSTHOG_EVENT_KEY,
- STORAGE_KEYS,
-} from "../utils/constants"
-import { trackEvent } from "../utils/posthog"
-import {
- createChatGPTInputBarElement,
- createClaudeInputBarElement,
- createT3InputBarElement,
- createTwitterImportButton,
- DOMUtils,
-} from "../utils/ui-components"
-
-export default defineContentScript({
- matches: ["<all_urls>"],
- main() {
-
- browser.runtime.onMessage.addListener(async (message) => {
- if (message.action === MESSAGE_TYPES.SHOW_TOAST) {
- DOMUtils.showToast(message.state)
- } else if (message.action === MESSAGE_TYPES.SAVE_MEMORY) {
- await saveMemory()
- } else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) {
- updateTwitterImportUI(message)
- } else if (message.type === MESSAGE_TYPES.IMPORT_DONE) {
- updateTwitterImportUI(message)
- }
- })
-
- const observeForMemoriesDialog = () => {
- const observer = new MutationObserver(() => {
- if (DOMUtils.isOnDomain(DOMAINS.CHATGPT)) {
- addSupermemoryButtonToMemoriesDialog()
- addSaveChatGPTElementBeforeComposerBtn()
- }
- if (DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
- addSupermemoryIconToClaudeInput()
- }
- if (DOMUtils.isOnDomain(DOMAINS.T3)) {
- addSupermemoryIconToT3Input()
- }
- if (
- DOMUtils.isOnDomain(DOMAINS.TWITTER) &&
- window.location.pathname === "/i/bookmarks"
- ) {
- addTwitterImportButton()
- } else if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
- if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
- DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)
- }
- }
- })
-
- observer.observe(document.body, {
- childList: true,
- subtree: true,
- })
- }
-
- if (
- DOMUtils.isOnDomain(DOMAINS.TWITTER) &&
- window.location.pathname === "/i/bookmarks"
- ) {
- setTimeout(() => {
- addTwitterImportButton() // Wait 2 seconds for page to load
- //addSaveTweetElement();
- }, 2000)
- } else if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
- if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
- DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)
- }
- }
-
- if (DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
- setTimeout(() => {
- addSupermemoryIconToClaudeInput() // Wait 2 seconds for Claude page to load
- }, 2000)
- }
-
- if (DOMUtils.isOnDomain(DOMAINS.T3)) {
- setTimeout(() => {
- addSupermemoryIconToT3Input() // Wait 2 seconds for T3 page to load
- }, 2000)
- }
-
- if (document.readyState === "loading") {
- document.addEventListener("DOMContentLoaded", observeForMemoriesDialog)
- } else {
- observeForMemoriesDialog()
- }
-
- async function saveMemory() {
- try {
- DOMUtils.showToast("loading")
-
- const highlightedText = window.getSelection()?.toString() || ""
-
- const url = window.location.href
-
- const html = document.documentElement.outerHTML
-
- const response = await browser.runtime.sendMessage({
- action: MESSAGE_TYPES.SAVE_MEMORY,
- data: {
- html,
- highlightedText,
- url,
- },
- actionSource: "context_menu",
- })
-
- console.log("Response from enxtension:", response)
- if (response.success) {
- DOMUtils.showToast("success")
- } else {
- DOMUtils.showToast("error")
- }
- } catch (error) {
- console.error("Error saving memory:", error)
- DOMUtils.showToast("error")
- }
- }
-
- async function getRelatedMemories(actionSource: string) {
- try {
- const userQuery =
- document.getElementById("prompt-textarea")?.textContent || ""
-
- const response = await browser.runtime.sendMessage({
- action: MESSAGE_TYPES.GET_RELATED_MEMORIES,
- data: userQuery,
- actionSource: actionSource,
- })
-
- if (response.success && response.data) {
- const promptElement = document.getElementById("prompt-textarea")
- if (promptElement) {
- const currentContent = promptElement.innerHTML
- promptElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}`
- }
- }
- } catch (error) {
- console.error("Error getting related memories:", error)
- }
- }
-
- function addSupermemoryButtonToMemoriesDialog() {
- const dialogs = document.querySelectorAll('[role="dialog"]')
- let memoriesDialog: HTMLElement | null = null
-
- for (const dialog of dialogs) {
- const headerText = dialog.querySelector("h2")
- if (headerText?.textContent?.includes("Saved memories")) {
- memoriesDialog = dialog as HTMLElement
- break
- }
- }
-
- if (!memoriesDialog) return
-
- if (memoriesDialog.querySelector("#supermemory-save-button")) return
-
- const deleteAllContainer = memoriesDialog.querySelector(
- ".mt-5.flex.justify-end",
- )
- if (!deleteAllContainer) return
-
- const supermemoryButton = document.createElement("button")
- supermemoryButton.id = "supermemory-save-button"
- supermemoryButton.className = "btn relative btn-primary-outline mr-2"
-
- const iconUrl = browser.runtime.getURL("/icon-16.png")
-
- supermemoryButton.innerHTML = `
- <div class="flex items-center justify-center gap-2">
- <img src="${iconUrl}" alt="supermemory" style="width: 16px; height: 16px; flex-shrink: 0; border-radius: 2px;" />
- Save to supermemory
- </div>
- `
-
- supermemoryButton.style.cssText = `
- background: #1C2026 !important;
- color: white !important;
- border: 1px solid #1C2026 !important;
- border-radius: 9999px !important;
- padding: 10px 16px !important;
- font-weight: 500 !important;
- font-size: 14px !important;
- margin-right: 8px !important;
- cursor: pointer !important;
- `
-
- supermemoryButton.addEventListener("mouseenter", () => {
- supermemoryButton.style.backgroundColor = "#2B2E33"
- })
-
- supermemoryButton.addEventListener("mouseleave", () => {
- supermemoryButton.style.backgroundColor = "#1C2026"
- })
-
- supermemoryButton.addEventListener("click", async () => {
- await saveMemoriesToSupermemory()
- })
-
- deleteAllContainer.insertBefore(
- supermemoryButton,
- deleteAllContainer.firstChild,
- )
- }
-
- async function saveMemoriesToSupermemory() {
- try {
- DOMUtils.showToast("loading")
-
- const memoriesTable = document.querySelector(
- '[role="dialog"] table tbody',
- )
- if (!memoriesTable) {
- DOMUtils.showToast("error")
- return
- }
-
- const memoryRows = memoriesTable.querySelectorAll("tr")
- const memories: string[] = []
-
- memoryRows.forEach((row) => {
- const memoryCell = row.querySelector("td .py-2.whitespace-pre-wrap")
- if (memoryCell?.textContent) {
- memories.push(memoryCell.textContent.trim())
- }
- })
-
- console.log("Memories:", memories)
-
- if (memories.length === 0) {
- DOMUtils.showToast("error")
- return
- }
-
- const combinedContent = `ChatGPT Saved Memories:\n\n${memories.map((memory, index) => `${index + 1}. ${memory}`).join("\n\n")}`
-
- const response = await browser.runtime.sendMessage({
- action: MESSAGE_TYPES.SAVE_MEMORY,
- data: {
- html: combinedContent,
- },
- actionSource: "chatgpt_memories_dialog",
- })
-
- console.log({ response })
-
- if (response.success) {
- DOMUtils.showToast("success")
- } else {
- DOMUtils.showToast("error")
- }
- } catch (error) {
- console.error("Error saving memories to supermemory:", error)
- DOMUtils.showToast("error")
- }
- }
-
- function addTwitterImportButton() {
- if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
- return
- }
-
- // Only show the import button on the bookmarks page
- if (window.location.pathname !== "/i/bookmarks") {
- return
- }
-
- if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
- return
- }
-
- const button = createTwitterImportButton(async () => {
- try {
- await browser.runtime.sendMessage({
- type: MESSAGE_TYPES.BATCH_IMPORT_ALL,
- })
- await trackEvent(POSTHOG_EVENT_KEY.TWITTER_IMPORT_STARTED, {
- source: `${POSTHOG_EVENT_KEY.SOURCE}_content_script`,
- })
- } catch (error) {
- console.error("Error starting import:", error)
- }
- })
-
- document.body.appendChild(button)
- }
-
-
- function updateTwitterImportUI(message: {
- type: string
- importedMessage?: string
- totalImported?: number
- }) {
- const importButton = document.getElementById(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)
- if (!importButton) return
-
- const iconUrl = browser.runtime.getURL("/icon-16.png")
-
- if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) {
- importButton.innerHTML = `
- <img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
- <span style="font-weight: 500; font-size: 14px;">${message.importedMessage}</span>
- `
- importButton.style.cursor = "default"
- }
-
- if (message.type === MESSAGE_TYPES.IMPORT_DONE) {
- importButton.innerHTML = `
- <img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
- <span style="font-weight: 500; font-size: 14px; color: #059669;">✓ Imported ${message.totalImported} tweets!</span>
- `
-
- setTimeout(() => {
- importButton.innerHTML = `
- <img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
- <span style="font-weight: 500; font-size: 14px;">Import Bookmarks</span>
- `
- importButton.style.cursor = "pointer"
- }, 3000)
- }
- }
-
- function addSaveChatGPTElementBeforeComposerBtn() {
- if (!DOMUtils.isOnDomain(DOMAINS.CHATGPT)) {
- return
- }
-
- const composerButtons = document.querySelectorAll("button.composer-btn")
-
- composerButtons.forEach((button) => {
- if (button.hasAttribute("data-supermemory-icon-added-before")) {
- return
- }
-
- const parent = button.parentElement
- if (!parent) return
-
- const parentSiblings = parent.parentElement?.children
- if (!parentSiblings) return
-
- let hasSpeechButtonSibling = false
- for (const sibling of parentSiblings) {
- if (
- sibling.getAttribute("data-testid") ===
- "composer-speech-button-container"
- ) {
- hasSpeechButtonSibling = true
- break
- }
- }
-
- if (!hasSpeechButtonSibling) return
-
- const grandParent = parent.parentElement
- if (!grandParent) return
-
- const existingIcon = grandParent.querySelector(
- `#${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer`,
- )
- if (existingIcon) {
- button.setAttribute("data-supermemory-icon-added-before", "true")
- return
- }
-
- const saveChatGPTElement = createChatGPTInputBarElement(async () => {
- await getRelatedMemories(
- POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_SEARCHED,
- )
- })
-
- saveChatGPTElement.id = `${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
-
- button.setAttribute("data-supermemory-icon-added-before", "true")
-
- grandParent.insertBefore(saveChatGPTElement, parent)
- })
- }
-
- function addSupermemoryIconToClaudeInput() {
- if (!DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
- return
- }
-
- const targetContainers = document.querySelectorAll(
- ".relative.flex-1.flex.items-center.gap-2.shrink.min-w-0",
- )
-
- targetContainers.forEach((container) => {
- if (container.hasAttribute("data-supermemory-icon-added")) {
- return
- }
-
- const existingIcon = container.querySelector(
- `#${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}`,
- )
- if (existingIcon) {
- container.setAttribute("data-supermemory-icon-added", "true")
- return
- }
-
- const supermemoryIcon = createClaudeInputBarElement(async () => {
- await getRelatedMemoriesForClaude()
- })
-
- supermemoryIcon.id = `${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
-
- container.setAttribute("data-supermemory-icon-added", "true")
-
- container.insertBefore(supermemoryIcon, container.firstChild)
- })
- }
-
- async function getRelatedMemoriesForClaude() {
- try {
- let userQuery = ""
-
- const supermemoryContainer = document.querySelector(
- '[data-supermemory-icon-added="true"]',
- )
- if (supermemoryContainer?.parentElement?.previousElementSibling) {
- const pTag =
- supermemoryContainer.parentElement.previousElementSibling.querySelector(
- "p",
- )
- userQuery = pTag?.innerText || pTag?.textContent || ""
- }
-
- if (!userQuery.trim()) {
- const textareaElement = document.querySelector(
- 'div[contenteditable="true"]',
- ) as HTMLElement
- userQuery =
- textareaElement?.innerText || textareaElement?.textContent || ""
- }
-
- if (!userQuery.trim()) {
- const inputElements = document.querySelectorAll(
- 'div[contenteditable="true"], textarea, input[type="text"]',
- )
- for (const element of inputElements) {
- const text =
- (element as HTMLElement).innerText ||
- (element as HTMLInputElement).value
- if (text?.trim()) {
- userQuery = text.trim()
- break
- }
- }
- }
-
- console.log("Claude query extracted:", userQuery)
-
- if (!userQuery.trim()) {
- console.log("No query text found")
- DOMUtils.showToast("error")
- return
- }
-
- const response = await browser.runtime.sendMessage({
- action: MESSAGE_TYPES.GET_RELATED_MEMORIES,
- data: userQuery,
- actionSource: POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_SEARCHED,
- })
-
- console.log("Claude memories response:", response)
-
- if (response.success && response.data) {
- const textareaElement = document.querySelector(
- 'div[contenteditable="true"]',
- ) as HTMLElement
-
- if (textareaElement) {
- const currentContent = textareaElement.innerHTML
- textareaElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}`
-
- textareaElement.dispatchEvent(new Event("input", { bubbles: true }))
- } else {
- console.log("Could not find Claude input area")
- }
- } else {
- console.log(
- "Failed to get memories:",
- response.error || "Unknown error",
- )
- }
- } catch (error) {
- console.error("Error getting related memories for Claude:", error)
- }
- }
-
- function addSupermemoryIconToT3Input() {
- if (!DOMUtils.isOnDomain(DOMAINS.T3)) {
- return
- }
-
- const targetContainers = document.querySelectorAll(
- ".flex.min-w-0.items-center.gap-2.overflow-hidden",
- )
-
- targetContainers.forEach((container) => {
- if (container.hasAttribute("data-supermemory-icon-added")) {
- return
- }
-
- const existingIcon = container.querySelector(
- `#${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}`,
- )
- if (existingIcon) {
- container.setAttribute("data-supermemory-icon-added", "true")
- return
- }
-
- const supermemoryIcon = createT3InputBarElement(async () => {
- await getRelatedMemoriesForT3()
- })
-
- supermemoryIcon.id = `${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
-
- container.setAttribute("data-supermemory-icon-added", "true")
-
- container.insertBefore(supermemoryIcon, container.firstChild)
- })
- }
-
- async function getRelatedMemoriesForT3() {
- try {
- let userQuery = ""
-
- const supermemoryContainer = document.querySelector(
- '[data-supermemory-icon-added="true"]',
- )
- if (
- supermemoryContainer?.parentElement?.parentElement
- ?.previousElementSibling
- ) {
- const textareaElement =
- supermemoryContainer.parentElement.parentElement.previousElementSibling.querySelector(
- "textarea",
- )
- userQuery = textareaElement?.value || ""
- }
-
- if (!userQuery.trim()) {
- const textareaElement = document.querySelector(
- 'div[contenteditable="true"]',
- ) as HTMLElement
- userQuery =
- textareaElement?.innerText || textareaElement?.textContent || ""
- }
-
- if (!userQuery.trim()) {
- const textareas = document.querySelectorAll("textarea")
- for (const textarea of textareas) {
- const text = (textarea as HTMLTextAreaElement).value
- if (text?.trim()) {
- userQuery = text.trim()
- break
- }
- }
- }
-
- console.log("T3 query extracted:", userQuery)
-
- if (!userQuery.trim()) {
- console.log("No query text found")
- return
- }
-
- const response = await browser.runtime.sendMessage({
- action: MESSAGE_TYPES.GET_RELATED_MEMORIES,
- data: userQuery,
- actionSource: POSTHOG_EVENT_KEY.T3_CHAT_MEMORIES_SEARCHED,
- })
-
- console.log("T3 memories response:", response)
-
- if (response.success && response.data) {
- let textareaElement = null
- const supermemoryContainer = document.querySelector(
- '[data-supermemory-icon-added="true"]',
- )
- if (
- supermemoryContainer?.parentElement?.parentElement
- ?.previousElementSibling
- ) {
- textareaElement =
- supermemoryContainer.parentElement.parentElement.previousElementSibling.querySelector(
- "textarea",
- )
- }
-
- if (!textareaElement) {
- textareaElement = document.querySelector(
- 'div[contenteditable="true"]',
- ) as HTMLElement
- }
-
- if (textareaElement) {
- if (textareaElement.tagName === "TEXTAREA") {
- const currentContent = (textareaElement as HTMLTextAreaElement)
- .value
- ;(textareaElement as HTMLTextAreaElement).value =
- `${currentContent}\n\nSupermemories: ${response.data}`
- } else {
- const currentContent = textareaElement.innerHTML
- textareaElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}`
- }
-
- textareaElement.dispatchEvent(new Event("input", { bubbles: true }))
- } else {
- console.log("Could not find T3 input area")
- }
- } else {
- console.log(
- "Failed to get memories:",
- response.error || "Unknown error",
- )
- }
- } catch (error) {
- console.error("Error getting related memories for T3:", error)
- }
- }
-
-
- document.addEventListener("keydown", async (event) => {
- if (
- (event.ctrlKey || event.metaKey) &&
- event.shiftKey &&
- event.key === "m"
- ) {
- event.preventDefault()
- await saveMemory()
- }
- })
-
- window.addEventListener("message", (event) => {
- if (event.source !== window) {
- return
- }
- const bearerToken = event.data.token
- const userData = event.data.userData
- if (bearerToken && userData) {
- if (
- !(
- window.location.hostname === "localhost" ||
- window.location.hostname === "supermemory.ai" ||
- window.location.hostname === "app.supermemory.ai"
- )
- ) {
- console.log(
- "Bearer token and user data is only allowed to be used on localhost or supermemory.ai",
- )
- return
- }
-
- chrome.storage.local.set(
- {
- [STORAGE_KEYS.BEARER_TOKEN]: bearerToken,
- [STORAGE_KEYS.USER_DATA]: userData,
- },
- () => {},
- )
- }
- })
- },
-})
diff --git a/apps/browser-extension/entrypoints/content/chatgpt.ts b/apps/browser-extension/entrypoints/content/chatgpt.ts
new file mode 100644
index 00000000..73e0354f
--- /dev/null
+++ b/apps/browser-extension/entrypoints/content/chatgpt.ts
@@ -0,0 +1,724 @@
+import {
+ DOMAINS,
+ ELEMENT_IDS,
+ MESSAGE_TYPES,
+ POSTHOG_EVENT_KEY,
+ STORAGE_KEYS,
+ UI_CONFIG,
+} from "../../utils/constants"
+import {
+ createChatGPTInputBarElement,
+ DOMUtils,
+} from "../../utils/ui-components"
+
+let chatGPTDebounceTimeout: NodeJS.Timeout | null = null
+let chatGPTRouteObserver: MutationObserver | null = null
+let chatGPTUrlCheckInterval: NodeJS.Timeout | null = null
+let chatGPTObserverThrottle: NodeJS.Timeout | null = null
+
+export function initializeChatGPT() {
+ if (!DOMUtils.isOnDomain(DOMAINS.CHATGPT)) {
+ return
+ }
+
+ if (document.body.hasAttribute("data-chatgpt-initialized")) {
+ return
+ }
+
+ setTimeout(() => {
+ addSupermemoryButtonToMemoriesDialog()
+ addSaveChatGPTElementBeforeComposerBtn()
+ setupChatGPTAutoFetch()
+ }, 2000)
+
+ setupChatGPTPromptCapture()
+
+ setupChatGPTRouteChangeDetection()
+
+ document.body.setAttribute("data-chatgpt-initialized", "true")
+}
+
+function setupChatGPTRouteChangeDetection() {
+ if (chatGPTRouteObserver) {
+ chatGPTRouteObserver.disconnect()
+ }
+ if (chatGPTUrlCheckInterval) {
+ clearInterval(chatGPTUrlCheckInterval)
+ }
+ if (chatGPTObserverThrottle) {
+ clearTimeout(chatGPTObserverThrottle)
+ chatGPTObserverThrottle = null
+ }
+
+ let currentUrl = window.location.href
+
+ const checkForRouteChange = () => {
+ if (window.location.href !== currentUrl) {
+ currentUrl = window.location.href
+ console.log("ChatGPT route changed, re-adding supermemory elements")
+ setTimeout(() => {
+ addSupermemoryButtonToMemoriesDialog()
+ addSaveChatGPTElementBeforeComposerBtn()
+ setupChatGPTAutoFetch()
+ }, 1000)
+ }
+ }
+
+ chatGPTUrlCheckInterval = setInterval(checkForRouteChange, 2000)
+
+ chatGPTRouteObserver = new MutationObserver((mutations) => {
+ if (chatGPTObserverThrottle) {
+ return
+ }
+
+ let shouldRecheck = false
+ mutations.forEach((mutation) => {
+ if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
+ mutation.addedNodes.forEach((node) => {
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ const element = node as Element
+ if (
+ element.querySelector?.("#prompt-textarea") ||
+ element.querySelector?.("button.composer-btn") ||
+ element.querySelector?.('[role="dialog"]') ||
+ element.matches?.("#prompt-textarea") ||
+ element.id === "prompt-textarea"
+ ) {
+ shouldRecheck = true
+ }
+ }
+ })
+ }
+ })
+
+ if (shouldRecheck) {
+ chatGPTObserverThrottle = setTimeout(() => {
+ try {
+ chatGPTObserverThrottle = null
+ addSupermemoryButtonToMemoriesDialog()
+ addSaveChatGPTElementBeforeComposerBtn()
+ setupChatGPTAutoFetch()
+ } catch (error) {
+ console.error("Error in ChatGPT observer callback:", error)
+ }
+ }, 300)
+ }
+ })
+
+ try {
+ chatGPTRouteObserver.observe(document.body, {
+ childList: true,
+ subtree: true,
+ })
+ } catch (error) {
+ console.error("Failed to set up ChatGPT route observer:", error)
+ if (chatGPTUrlCheckInterval) {
+ clearInterval(chatGPTUrlCheckInterval)
+ }
+ chatGPTUrlCheckInterval = setInterval(checkForRouteChange, 1000)
+ }
+}
+
+async function getRelatedMemoriesForChatGPT(actionSource: string) {
+ try {
+ const userQuery =
+ document.getElementById("prompt-textarea")?.textContent || ""
+
+ const icon = document.querySelectorAll(
+ '[id*="sm-chatgpt-input-bar-element-before-composer"]',
+ )[0]
+
+ const iconElement = icon as HTMLElement
+
+ if (!iconElement) {
+ console.warn("ChatGPT icon element not found, cannot update feedback")
+ return
+ }
+
+ updateChatGPTIconFeedback("Searching memories...", iconElement)
+
+ const timeoutPromise = new Promise((_, reject) =>
+ setTimeout(
+ () => reject(new Error("Memory search timeout")),
+ UI_CONFIG.API_REQUEST_TIMEOUT,
+ ),
+ )
+
+ const response = await Promise.race([
+ browser.runtime.sendMessage({
+ action: MESSAGE_TYPES.GET_RELATED_MEMORIES,
+ data: userQuery,
+ actionSource: actionSource,
+ }),
+ timeoutPromise,
+ ])
+
+ if (response?.success && response?.data) {
+ const promptElement = document.getElementById("prompt-textarea")
+ if (promptElement) {
+ promptElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${response.data}</div>`
+ console.log(
+ "Prompt element dataset:",
+ promptElement.dataset.supermemories,
+ )
+
+ iconElement.dataset.memoriesData = response.data
+
+ updateChatGPTIconFeedback("Included Memories", iconElement)
+ } else {
+ console.warn(
+ "ChatGPT prompt element not found after successful memory fetch",
+ )
+ updateChatGPTIconFeedback("Memories found", iconElement)
+ }
+ } else {
+ console.warn("No memories found or API response invalid")
+ updateChatGPTIconFeedback("No memories found", iconElement)
+ }
+ } catch (error) {
+ console.error("Error getting related memories:", error)
+ try {
+ const icon = document.querySelectorAll(
+ '[id*="sm-chatgpt-input-bar-element-before-composer"]',
+ )[0] as HTMLElement
+ if (icon) {
+ updateChatGPTIconFeedback("Error fetching memories", icon)
+ }
+ } catch (feedbackError) {
+ console.error("Failed to update error feedback:", feedbackError)
+ }
+ }
+}
+
+function addSupermemoryButtonToMemoriesDialog() {
+ const dialogs = document.querySelectorAll('[role="dialog"]')
+ let memoriesDialog: HTMLElement | null = null
+
+ for (const dialog of dialogs) {
+ const headerText = dialog.querySelector("h2")
+ if (headerText?.textContent?.includes("Saved memories")) {
+ memoriesDialog = dialog as HTMLElement
+ break
+ }
+ }
+
+ if (!memoriesDialog) return
+
+ if (memoriesDialog.querySelector("#supermemory-save-button")) return
+
+ const deleteAllContainer = memoriesDialog.querySelector(
+ ".mt-5.flex.justify-end",
+ )
+ if (!deleteAllContainer) return
+
+ const supermemoryButton = document.createElement("button")
+ supermemoryButton.id = "supermemory-save-button"
+ supermemoryButton.className = "btn relative btn-primary-outline mr-2"
+
+ const iconUrl = browser.runtime.getURL("/icon-16.png")
+
+ supermemoryButton.innerHTML = `
+ <div class="flex items-center justify-center gap-2">
+ <img src="${iconUrl}" alt="supermemory" style="width: 16px; height: 16px; flex-shrink: 0; border-radius: 2px;" />
+ Save to supermemory
+ </div>
+ `
+
+ supermemoryButton.style.cssText = `
+ background: #1C2026 !important;
+ color: white !important;
+ border: 1px solid #1C2026 !important;
+ border-radius: 9999px !important;
+ padding: 10px 16px !important;
+ font-weight: 500 !important;
+ font-size: 14px !important;
+ margin-right: 8px !important;
+ cursor: pointer !important;
+ `
+
+ supermemoryButton.addEventListener("mouseenter", () => {
+ supermemoryButton.style.backgroundColor = "#2B2E33"
+ })
+
+ supermemoryButton.addEventListener("mouseleave", () => {
+ supermemoryButton.style.backgroundColor = "#1C2026"
+ })
+
+ supermemoryButton.addEventListener("click", async () => {
+ await saveMemoriesToSupermemory()
+ })
+
+ deleteAllContainer.insertBefore(
+ supermemoryButton,
+ deleteAllContainer.firstChild,
+ )
+}
+
+async function saveMemoriesToSupermemory() {
+ try {
+ DOMUtils.showToast("loading")
+
+ const memoriesTable = document.querySelector('[role="dialog"] table tbody')
+ if (!memoriesTable) {
+ DOMUtils.showToast("error")
+ return
+ }
+
+ const memoryRows = memoriesTable.querySelectorAll("tr")
+ const memories: string[] = []
+
+ memoryRows.forEach((row) => {
+ const memoryCell = row.querySelector("td .py-2.whitespace-pre-wrap")
+ if (memoryCell?.textContent) {
+ memories.push(memoryCell.textContent.trim())
+ }
+ })
+
+ console.log("Memories:", memories)
+
+ if (memories.length === 0) {
+ DOMUtils.showToast("error")
+ return
+ }
+
+ const combinedContent = `ChatGPT Saved Memories:\n\n${memories.map((memory, index) => `${index + 1}. ${memory}`).join("\n\n")}`
+
+ const response = await browser.runtime.sendMessage({
+ action: MESSAGE_TYPES.SAVE_MEMORY,
+ data: {
+ html: combinedContent,
+ },
+ actionSource: "chatgpt_memories_dialog",
+ })
+
+ console.log({ response })
+
+ if (response.success) {
+ DOMUtils.showToast("success")
+ } else {
+ DOMUtils.showToast("error")
+ }
+ } catch (error) {
+ console.error("Error saving memories to supermemory:", error)
+ DOMUtils.showToast("error")
+ }
+}
+
+function updateChatGPTIconFeedback(
+ message: string,
+ iconElement: HTMLElement,
+ resetAfter = 0,
+) {
+ if (!iconElement.dataset.originalHtml) {
+ iconElement.dataset.originalHtml = iconElement.innerHTML
+ }
+
+ const feedbackDiv = document.createElement("div")
+ feedbackDiv.style.cssText = `
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 8px;
+ background: #513EA9;
+ border-radius: 12px;
+ color: white;
+ font-size: 12px;
+ font-weight: 500;
+ cursor: ${message === "Included Memories" ? "pointer" : "default"};
+ position: relative;
+ `
+
+ feedbackDiv.innerHTML = `
+ <span>✓</span>
+ <span>${message}</span>
+ `
+
+ if (message === "Included Memories" && iconElement.dataset.memoriesData) {
+ const popup = document.createElement("div")
+ popup.style.cssText = `
+ position: fixed;
+ bottom: 80px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: #1a1a1a;
+ color: white;
+ padding: 0;
+ border-radius: 12px;
+ font-size: 13px;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
+ max-width: 500px;
+ max-height: 400px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
+ z-index: 999999;
+ display: none;
+ border: 1px solid #333;
+ `
+
+ const header = document.createElement("div")
+ header.style.cssText = `
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px;
+ border-bottom: 1px solid #333;
+ opacity: 0.8;
+ `
+ header.innerHTML = `
+ <span style="font-weight: 600; color: #fff;">Included Memories</span>
+ `
+
+ const content = document.createElement("div")
+ content.style.cssText = `
+ padding: 0;
+ max-height: 300px;
+ overflow-y: auto;
+ `
+
+ const memoriesText = iconElement.dataset.memoriesData || ""
+ console.log("Memories text:", memoriesText)
+ const individualMemories = memoriesText
+ .split(/[,\n]/)
+ .map((memory) => memory.trim())
+ .filter((memory) => memory.length > 0 && memory !== ",")
+ console.log("Individual memories:", individualMemories)
+
+ individualMemories.forEach((memory, index) => {
+ const memoryItem = document.createElement("div")
+ memoryItem.style.cssText = `
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 10px;
+ font-size: 13px;
+ line-height: 1.4;
+ `
+
+ const memoryText = document.createElement("div")
+ memoryText.style.cssText = `
+ flex: 1;
+ color: #e5e5e5;
+ `
+ memoryText.textContent = memory.trim()
+
+ const removeBtn = document.createElement("button")
+ removeBtn.style.cssText = `
+ background: transparent;
+ color: #9ca3af;
+ border: none;
+ padding: 4px;
+ border-radius: 4px;
+ cursor: pointer;
+ flex-shrink: 0;
+ height: fit-content;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ `
+ removeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`
+ removeBtn.dataset.memoryIndex = index.toString()
+
+ removeBtn.addEventListener("mouseenter", () => {
+ removeBtn.style.color = "#ef4444"
+ })
+ removeBtn.addEventListener("mouseleave", () => {
+ removeBtn.style.color = "#9ca3af"
+ })
+
+ memoryItem.appendChild(memoryText)
+ memoryItem.appendChild(removeBtn)
+ content.appendChild(memoryItem)
+ })
+
+ popup.appendChild(header)
+ popup.appendChild(content)
+ document.body.appendChild(popup)
+
+ feedbackDiv.addEventListener("mouseenter", () => {
+ const textSpan = feedbackDiv.querySelector("span:last-child")
+ if (textSpan) {
+ textSpan.textContent = "Click to see memories"
+ }
+ })
+
+ feedbackDiv.addEventListener("mouseleave", () => {
+ const textSpan = feedbackDiv.querySelector("span:last-child")
+ if (textSpan) {
+ textSpan.textContent = "Included Memories"
+ }
+ })
+
+ feedbackDiv.addEventListener("click", (e) => {
+ e.stopPropagation()
+ popup.style.display = "block"
+ })
+
+ document.addEventListener("click", (e) => {
+ if (!popup.contains(e.target as Node)) {
+ popup.style.display = "none"
+ }
+ })
+
+ content.querySelectorAll("button[data-memory-index]").forEach((button) => {
+ const htmlButton = button as HTMLButtonElement
+ htmlButton.addEventListener("click", () => {
+ const index = Number.parseInt(htmlButton.dataset.memoryIndex || "0", 10)
+ const memoryItem = htmlButton.parentElement
+
+ if (memoryItem) {
+ content.removeChild(memoryItem)
+ }
+
+ const currentMemories = (iconElement.dataset.memoriesData || "")
+ .split(/[,\n]/)
+ .map((memory) => memory.trim())
+ .filter((memory) => memory.length > 0 && memory !== ",")
+ currentMemories.splice(index, 1)
+
+ const updatedMemories = currentMemories.join(" ,")
+
+ iconElement.dataset.memoriesData = updatedMemories
+
+ const promptElement = document.getElementById("prompt-textarea")
+ if (promptElement) {
+ promptElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${updatedMemories}</div>`
+ }
+
+ content
+ .querySelectorAll("button[data-memory-index]")
+ .forEach((btn, newIndex) => {
+ const htmlBtn = btn as HTMLButtonElement
+ htmlBtn.dataset.memoryIndex = newIndex.toString()
+ })
+
+ if (currentMemories.length <= 1) {
+ if (promptElement?.dataset.supermemories) {
+ delete promptElement.dataset.supermemories
+ delete iconElement.dataset.memoriesData
+ iconElement.innerHTML = iconElement.dataset.originalHtml || ""
+ delete iconElement.dataset.originalHtml
+ }
+ popup.style.display = "none"
+ if (document.body.contains(popup)) {
+ document.body.removeChild(popup)
+ }
+ }
+ })
+ })
+
+ setTimeout(() => {
+ if (document.body.contains(popup)) {
+ document.body.removeChild(popup)
+ }
+ }, 300000)
+ }
+
+ iconElement.innerHTML = ""
+ iconElement.appendChild(feedbackDiv)
+
+ if (resetAfter > 0) {
+ setTimeout(() => {
+ iconElement.innerHTML = iconElement.dataset.originalHtml || ""
+ delete iconElement.dataset.originalHtml
+ }, resetAfter)
+ }
+}
+
+function addSaveChatGPTElementBeforeComposerBtn() {
+ const composerButtons = document.querySelectorAll("button.composer-btn")
+
+ composerButtons.forEach((button) => {
+ if (button.hasAttribute("data-supermemory-icon-added-before")) {
+ return
+ }
+
+ const parent = button.parentElement
+ if (!parent) return
+
+ const parentSiblings = parent.parentElement?.children
+ if (!parentSiblings) return
+
+ let hasSpeechButtonSibling = false
+ for (const sibling of parentSiblings) {
+ if (
+ sibling.getAttribute("data-testid") ===
+ "composer-speech-button-container"
+ ) {
+ hasSpeechButtonSibling = true
+ break
+ }
+ }
+
+ if (!hasSpeechButtonSibling) return
+
+ const grandParent = parent.parentElement
+ if (!grandParent) return
+
+ const existingIcon = grandParent.querySelector(
+ `#${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer`,
+ )
+ if (existingIcon) {
+ button.setAttribute("data-supermemory-icon-added-before", "true")
+ return
+ }
+
+ const saveChatGPTElement = createChatGPTInputBarElement(async () => {
+ await getRelatedMemoriesForChatGPT(
+ POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_SEARCHED,
+ )
+ })
+
+ saveChatGPTElement.id = `${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
+
+ button.setAttribute("data-supermemory-icon-added-before", "true")
+
+ grandParent.insertBefore(saveChatGPTElement, parent)
+
+ setupChatGPTAutoFetch()
+ })
+}
+
+async function setupChatGPTAutoFetch() {
+ const result = await chrome.storage.local.get([
+ STORAGE_KEYS.AUTO_SEARCH_ENABLED,
+ ])
+ const autoSearchEnabled = result[STORAGE_KEYS.AUTO_SEARCH_ENABLED] ?? false
+
+ if (!autoSearchEnabled) {
+ return
+ }
+
+ const promptTextarea = document.getElementById("prompt-textarea")
+ if (
+ !promptTextarea ||
+ promptTextarea.hasAttribute("data-supermemory-auto-fetch")
+ ) {
+ return
+ }
+
+ promptTextarea.setAttribute("data-supermemory-auto-fetch", "true")
+
+ const handleInput = () => {
+ if (chatGPTDebounceTimeout) {
+ clearTimeout(chatGPTDebounceTimeout)
+ }
+
+ chatGPTDebounceTimeout = setTimeout(async () => {
+ const content = promptTextarea.textContent?.trim() || ""
+
+ if (content.length > 2) {
+ await getRelatedMemoriesForChatGPT(
+ POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED,
+ )
+ } else if (content.length === 0) {
+ const icons = document.querySelectorAll(
+ '[id*="sm-chatgpt-input-bar-element-before-composer"]',
+ )
+
+ icons.forEach((icon) => {
+ const iconElement = icon as HTMLElement
+ if (iconElement.dataset.originalHtml) {
+ iconElement.innerHTML = iconElement.dataset.originalHtml
+ delete iconElement.dataset.originalHtml
+ delete iconElement.dataset.memoriesData
+ }
+ })
+
+ if (promptTextarea.dataset.supermemories) {
+ delete promptTextarea.dataset.supermemories
+ }
+ }
+ }, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)
+ }
+
+ promptTextarea.addEventListener("input", handleInput)
+}
+
+function setupChatGPTPromptCapture() {
+ if (document.body.hasAttribute("data-chatgpt-prompt-capture-setup")) {
+ return
+ }
+ document.body.setAttribute("data-chatgpt-prompt-capture-setup", "true")
+
+ const capturePromptContent = async (source: string) => {
+ const promptTextarea = document.getElementById("prompt-textarea")
+
+ let promptContent = ""
+ if (promptTextarea) {
+ promptContent = promptTextarea.textContent || ""
+ }
+
+ const storedMemories = promptTextarea?.dataset.supermemories
+ if (
+ storedMemories &&
+ promptTextarea &&
+ !promptContent.includes("Supermemories of user")
+ ) {
+ promptTextarea.innerHTML = `${promptTextarea.innerHTML} ${storedMemories}`
+ promptContent = promptTextarea.textContent || ""
+ }
+
+ if (promptTextarea && promptContent.trim()) {
+ console.log(`ChatGPT prompt submitted via ${source}:`, promptContent)
+
+ try {
+ await browser.runtime.sendMessage({
+ action: MESSAGE_TYPES.CAPTURE_PROMPT,
+ data: {
+ prompt: promptContent,
+ platform: "chatgpt",
+ source: source,
+ },
+ })
+ } catch (error) {
+ console.error("Error sending ChatGPT prompt to background:", error)
+ }
+ }
+
+ const icons = document.querySelectorAll(
+ '[id*="sm-chatgpt-input-bar-element-before-composer"]',
+ )
+
+ icons.forEach((icon) => {
+ const iconElement = icon as HTMLElement
+ if (iconElement.dataset.originalHtml) {
+ iconElement.innerHTML = iconElement.dataset.originalHtml
+ delete iconElement.dataset.originalHtml
+ delete iconElement.dataset.memoriesData
+ }
+ })
+
+ if (promptTextarea?.dataset.supermemories) {
+ delete promptTextarea.dataset.supermemories
+ }
+ }
+
+ document.addEventListener(
+ "click",
+ async (event) => {
+ const target = event.target as HTMLElement
+ if (
+ target.id === "composer-submit-button" ||
+ target.closest("#composer-submit-button")
+ ) {
+ await capturePromptContent("button click")
+ }
+ },
+ true,
+ )
+
+ document.addEventListener(
+ "keydown",
+ async (event) => {
+ const target = event.target as HTMLElement
+
+ if (
+ target.id === "prompt-textarea" &&
+ event.key === "Enter" &&
+ !event.shiftKey
+ ) {
+ await capturePromptContent("Enter key")
+ }
+ },
+ true,
+ )
+}
diff --git a/apps/browser-extension/entrypoints/content/claude.ts b/apps/browser-extension/entrypoints/content/claude.ts
new file mode 100644
index 00000000..e0853d41
--- /dev/null
+++ b/apps/browser-extension/entrypoints/content/claude.ts
@@ -0,0 +1,648 @@
+import {
+ DOMAINS,
+ ELEMENT_IDS,
+ MESSAGE_TYPES,
+ POSTHOG_EVENT_KEY,
+ STORAGE_KEYS,
+ UI_CONFIG,
+} from "../../utils/constants"
+import {
+ createClaudeInputBarElement,
+ DOMUtils,
+} from "../../utils/ui-components"
+
+let claudeDebounceTimeout: NodeJS.Timeout | null = null
+let claudeRouteObserver: MutationObserver | null = null
+let claudeUrlCheckInterval: NodeJS.Timeout | null = null
+let claudeObserverThrottle: NodeJS.Timeout | null = null
+
+export function initializeClaude() {
+ if (!DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
+ return
+ }
+
+ if (document.body.hasAttribute("data-claude-initialized")) {
+ return
+ }
+
+ setTimeout(() => {
+ addSupermemoryIconToClaudeInput()
+ setupClaudeAutoFetch()
+ }, 2000)
+
+ setupClaudePromptCapture()
+
+ setupClaudeRouteChangeDetection()
+
+ document.body.setAttribute("data-claude-initialized", "true")
+}
+
+function setupClaudeRouteChangeDetection() {
+ if (claudeRouteObserver) {
+ claudeRouteObserver.disconnect()
+ }
+ if (claudeUrlCheckInterval) {
+ clearInterval(claudeUrlCheckInterval)
+ }
+ if (claudeObserverThrottle) {
+ clearTimeout(claudeObserverThrottle)
+ claudeObserverThrottle = null
+ }
+
+ let currentUrl = window.location.href
+
+ const checkForRouteChange = () => {
+ if (window.location.href !== currentUrl) {
+ currentUrl = window.location.href
+ console.log("Claude route changed, re-adding supermemory icon")
+ setTimeout(() => {
+ addSupermemoryIconToClaudeInput()
+ setupClaudeAutoFetch()
+ }, 1000)
+ }
+ }
+
+ claudeUrlCheckInterval = setInterval(checkForRouteChange, 2000)
+
+ claudeRouteObserver = new MutationObserver((mutations) => {
+ if (claudeObserverThrottle) {
+ return
+ }
+
+ let shouldRecheck = false
+ mutations.forEach((mutation) => {
+ if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
+ mutation.addedNodes.forEach((node) => {
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ const element = node as Element
+ if (
+ element.querySelector?.('div[contenteditable="true"]') ||
+ element.querySelector?.("textarea") ||
+ element.matches?.('div[contenteditable="true"]') ||
+ element.matches?.("textarea")
+ ) {
+ shouldRecheck = true
+ }
+ }
+ })
+ }
+ })
+
+ if (shouldRecheck) {
+ claudeObserverThrottle = setTimeout(() => {
+ try {
+ claudeObserverThrottle = null
+ addSupermemoryIconToClaudeInput()
+ setupClaudeAutoFetch()
+ } catch (error) {
+ console.error("Error in Claude observer callback:", error)
+ }
+ }, 300)
+ }
+ })
+
+ try {
+ claudeRouteObserver.observe(document.body, {
+ childList: true,
+ subtree: true,
+ })
+ } catch (error) {
+ console.error("Failed to set up Claude route observer:", error)
+ if (claudeUrlCheckInterval) {
+ clearInterval(claudeUrlCheckInterval)
+ }
+ claudeUrlCheckInterval = setInterval(checkForRouteChange, 1000)
+ }
+}
+
+function addSupermemoryIconToClaudeInput() {
+ const targetContainers = document.querySelectorAll(
+ ".relative.flex-1.flex.items-center.gap-2.shrink.min-w-0",
+ )
+
+ targetContainers.forEach((container) => {
+ if (container.hasAttribute("data-supermemory-icon-added")) {
+ return
+ }
+
+ const existingIcon = container.querySelector(
+ `#${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}`,
+ )
+ if (existingIcon) {
+ container.setAttribute("data-supermemory-icon-added", "true")
+ return
+ }
+
+ const supermemoryIcon = createClaudeInputBarElement(async () => {
+ await getRelatedMemoriesForClaude(
+ POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_SEARCHED,
+ )
+ })
+
+ supermemoryIcon.id = `${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
+
+ container.setAttribute("data-supermemory-icon-added", "true")
+
+ container.insertBefore(supermemoryIcon, container.firstChild)
+ })
+}
+
+async function getRelatedMemoriesForClaude(actionSource: string) {
+ try {
+ let userQuery = ""
+
+ const supermemoryContainer = document.querySelector(
+ '[data-supermemory-icon-added="true"]',
+ )
+ if (supermemoryContainer?.parentElement?.previousElementSibling) {
+ const pTag =
+ supermemoryContainer.parentElement.previousElementSibling.querySelector(
+ "p",
+ )
+ userQuery = pTag?.innerText || pTag?.textContent || ""
+ }
+
+ if (!userQuery.trim()) {
+ const textareaElement = document.querySelector(
+ 'div[contenteditable="true"]',
+ ) as HTMLElement
+ userQuery =
+ textareaElement?.innerText || textareaElement?.textContent || ""
+ }
+
+ if (!userQuery.trim()) {
+ const inputElements = document.querySelectorAll(
+ 'div[contenteditable="true"], textarea, input[type="text"]',
+ )
+ for (const element of inputElements) {
+ const text =
+ (element as HTMLElement).innerText ||
+ (element as HTMLInputElement).value
+ if (text?.trim()) {
+ userQuery = text.trim()
+ break
+ }
+ }
+ }
+
+ console.log("Claude query extracted:", userQuery)
+
+ if (!userQuery.trim()) {
+ console.log("No query text found for Claude")
+ return
+ }
+
+ const icon = document.querySelector('[id*="sm-claude-input-bar-element"]')
+
+ const iconElement = icon as HTMLElement
+
+ if (!iconElement) {
+ console.warn("Claude icon element not found, cannot update feedback")
+ return
+ }
+
+ updateClaudeIconFeedback("Searching memories...", iconElement)
+
+ const timeoutPromise = new Promise((_, reject) =>
+ setTimeout(
+ () => reject(new Error("Memory search timeout")),
+ UI_CONFIG.API_REQUEST_TIMEOUT,
+ ),
+ )
+
+ const response = await Promise.race([
+ browser.runtime.sendMessage({
+ action: MESSAGE_TYPES.GET_RELATED_MEMORIES,
+ data: userQuery,
+ actionSource: actionSource,
+ }),
+ timeoutPromise,
+ ])
+
+ console.log("Claude memories response:", response)
+
+ if (response?.success && response?.data) {
+ const textareaElement = document.querySelector(
+ 'div[contenteditable="true"]',
+ ) as HTMLElement
+
+ if (textareaElement) {
+ textareaElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${response.data}</div>`
+ console.log(
+ "Text element dataset:",
+ textareaElement.dataset.supermemories,
+ )
+
+ iconElement.dataset.memoriesData = response.data
+
+ updateClaudeIconFeedback("Included Memories", iconElement)
+ } else {
+ console.warn(
+ "Claude input area not found after successful memory fetch",
+ )
+ updateClaudeIconFeedback("Memories found", iconElement)
+ }
+ } else {
+ console.warn("No memories found or API response invalid for Claude")
+ updateClaudeIconFeedback("No memories found", iconElement)
+ }
+ } catch (error) {
+ console.error("Error getting related memories for Claude:", error)
+ try {
+ const icon = document.querySelector(
+ '[id*="sm-claude-input-bar-element"]',
+ ) as HTMLElement
+ if (icon) {
+ updateClaudeIconFeedback("Error fetching memories", icon)
+ }
+ } catch (feedbackError) {
+ console.error("Failed to update Claude error feedback:", feedbackError)
+ }
+ }
+}
+
+function updateClaudeIconFeedback(
+ message: string,
+ iconElement: HTMLElement,
+ resetAfter = 0,
+) {
+ if (!iconElement.dataset.originalHtml) {
+ iconElement.dataset.originalHtml = iconElement.innerHTML
+ }
+
+ const feedbackDiv = document.createElement("div")
+ feedbackDiv.style.cssText = `
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 8px;
+ background: #513EA9;
+ border-radius: 6px;
+ color: white;
+ font-size: 12px;
+ font-weight: 500;
+ cursor: ${message === "Included Memories" ? "pointer" : "default"};
+ position: relative;
+ `
+
+ feedbackDiv.innerHTML = `
+ <span>✓</span>
+ <span>${message}</span>
+ `
+
+ if (message === "Included Memories" && iconElement.dataset.memoriesData) {
+ const popup = document.createElement("div")
+ popup.style.cssText = `
+ position: fixed;
+ bottom: 80px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: #1a1a1a;
+ color: white;
+ padding: 0;
+ border-radius: 12px;
+ font-size: 13px;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
+ max-width: 500px;
+ max-height: 400px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
+ z-index: 999999;
+ display: none;
+ border: 1px solid #333;
+ `
+
+ const header = document.createElement("div")
+ header.style.cssText = `
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px;
+ border-bottom: 1px solid #333;
+ opacity: 0.8;
+ `
+ header.innerHTML = `
+ <span style="font-weight: 600; color: #fff;">Included Memories</span>
+ `
+
+ const content = document.createElement("div")
+ content.style.cssText = `
+ padding: 0;
+ max-height: 300px;
+ overflow-y: auto;
+ `
+
+ const memoriesText = iconElement.dataset.memoriesData || ""
+ console.log("Memories text:", memoriesText)
+ const individualMemories = memoriesText
+ .split(/[,\n]/)
+ .map((memory) => memory.trim())
+ .filter((memory) => memory.length > 0 && memory !== ",")
+ console.log("Individual memories:", individualMemories)
+
+ individualMemories.forEach((memory, index) => {
+ const memoryItem = document.createElement("div")
+ memoryItem.style.cssText = `
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 10px;
+ font-size: 13px;
+ line-height: 1.4;
+ `
+
+ const memoryText = document.createElement("div")
+ memoryText.style.cssText = `
+ flex: 1;
+ color: #e5e5e5;
+ `
+ memoryText.textContent = memory.trim()
+
+ const removeBtn = document.createElement("button")
+ removeBtn.style.cssText = `
+ background: transparent;
+ color: #9ca3af;
+ border: none;
+ padding: 4px;
+ border-radius: 4px;
+ cursor: pointer;
+ flex-shrink: 0;
+ height: fit-content;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ `
+ removeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`
+ removeBtn.dataset.memoryIndex = index.toString()
+
+ removeBtn.addEventListener("mouseenter", () => {
+ removeBtn.style.color = "#ef4444"
+ })
+ removeBtn.addEventListener("mouseleave", () => {
+ removeBtn.style.color = "#9ca3af"
+ })
+
+ memoryItem.appendChild(memoryText)
+ memoryItem.appendChild(removeBtn)
+ content.appendChild(memoryItem)
+ })
+
+ popup.appendChild(header)
+ popup.appendChild(content)
+ document.body.appendChild(popup)
+
+ feedbackDiv.addEventListener("mouseenter", () => {
+ const textSpan = feedbackDiv.querySelector("span:last-child")
+ if (textSpan) {
+ textSpan.textContent = "Click to see memories"
+ }
+ })
+
+ feedbackDiv.addEventListener("mouseleave", () => {
+ const textSpan = feedbackDiv.querySelector("span:last-child")
+ if (textSpan) {
+ textSpan.textContent = "Included Memories"
+ }
+ })
+
+ feedbackDiv.addEventListener("click", (e) => {
+ e.stopPropagation()
+ popup.style.display = "block"
+ })
+
+ document.addEventListener("click", (e) => {
+ if (!popup.contains(e.target as Node)) {
+ popup.style.display = "none"
+ }
+ })
+
+ content.querySelectorAll("button[data-memory-index]").forEach((button) => {
+ const htmlButton = button as HTMLButtonElement
+ htmlButton.addEventListener("click", () => {
+ const index = Number.parseInt(htmlButton.dataset.memoryIndex || "0", 10)
+ const memoryItem = htmlButton.parentElement
+
+ if (memoryItem) {
+ content.removeChild(memoryItem)
+ }
+
+ const currentMemories = (iconElement.dataset.memoriesData || "")
+ .split(/[,\n]/)
+ .map((memory) => memory.trim())
+ .filter((memory) => memory.length > 0 && memory !== ",")
+ currentMemories.splice(index, 1)
+
+ const updatedMemories = currentMemories.join(" ,")
+
+ iconElement.dataset.memoriesData = updatedMemories
+
+ const textareaElement = document.querySelector(
+ 'div[contenteditable="true"]',
+ ) as HTMLElement
+ if (textareaElement) {
+ textareaElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${updatedMemories}</div>`
+ }
+
+ content
+ .querySelectorAll("button[data-memory-index]")
+ .forEach((btn, newIndex) => {
+ const htmlBtn = btn as HTMLButtonElement
+ htmlBtn.dataset.memoryIndex = newIndex.toString()
+ })
+
+ if (currentMemories.length <= 1) {
+ if (textareaElement?.dataset.supermemories) {
+ delete textareaElement.dataset.supermemories
+ delete iconElement.dataset.memoriesData
+ iconElement.innerHTML = iconElement.dataset.originalHtml || ""
+ delete iconElement.dataset.originalHtml
+ }
+ popup.style.display = "none"
+ if (document.body.contains(popup)) {
+ document.body.removeChild(popup)
+ }
+ }
+ })
+ })
+
+ setTimeout(() => {
+ if (document.body.contains(popup)) {
+ document.body.removeChild(popup)
+ }
+ }, 300000)
+ }
+
+ iconElement.innerHTML = ""
+ iconElement.appendChild(feedbackDiv)
+
+ if (resetAfter > 0) {
+ setTimeout(() => {
+ iconElement.innerHTML = iconElement.dataset.originalHtml || ""
+ delete iconElement.dataset.originalHtml
+ }, resetAfter)
+ }
+}
+
+function setupClaudePromptCapture() {
+ if (document.body.hasAttribute("data-claude-prompt-capture-setup")) {
+ return
+ }
+ document.body.setAttribute("data-claude-prompt-capture-setup", "true")
+ const captureClaudePromptContent = async (source: string) => {
+ let promptContent = ""
+
+ const contentEditableDiv = document.querySelector(
+ 'div[contenteditable="true"]',
+ ) as HTMLElement
+ if (contentEditableDiv) {
+ promptContent =
+ contentEditableDiv.textContent || contentEditableDiv.innerText || ""
+ }
+
+ if (!promptContent) {
+ const textarea = document.querySelector("textarea") as HTMLTextAreaElement
+ if (textarea) {
+ promptContent = textarea.value || ""
+ }
+ }
+
+ const storedMemories = contentEditableDiv?.dataset.supermemories
+ if (
+ storedMemories &&
+ contentEditableDiv &&
+ !promptContent.includes("Supermemories of user")
+ ) {
+ contentEditableDiv.innerHTML = `${contentEditableDiv.innerHTML} ${storedMemories}`
+ promptContent =
+ contentEditableDiv.textContent || contentEditableDiv.innerText || ""
+ }
+
+ if (promptContent.trim()) {
+ console.log(`Claude prompt submitted via ${source}:`, promptContent)
+
+ try {
+ await browser.runtime.sendMessage({
+ action: MESSAGE_TYPES.CAPTURE_PROMPT,
+ data: {
+ prompt: promptContent,
+ platform: "claude",
+ source: source,
+ },
+ })
+ } catch (error) {
+ console.error("Error sending Claude prompt to background:", error)
+ }
+ }
+
+ const icons = document.querySelectorAll(
+ '[id*="sm-claude-input-bar-element"]',
+ )
+
+ icons.forEach((icon) => {
+ const iconElement = icon as HTMLElement
+ if (iconElement.dataset.originalHtml) {
+ iconElement.innerHTML = iconElement.dataset.originalHtml
+ delete iconElement.dataset.originalHtml
+ delete iconElement.dataset.memoriesData
+ }
+ })
+
+ if (contentEditableDiv?.dataset.supermemories) {
+ delete contentEditableDiv.dataset.supermemories
+ }
+ }
+
+ document.addEventListener(
+ "click",
+ async (event) => {
+ const target = event.target as HTMLElement
+ const sendButton =
+ target.closest(
+ "button.inline-flex.items-center.justify-center.relative.shrink-0.can-focus.select-none",
+ ) ||
+ target.closest('button[class*="bg-accent-main-000"]') ||
+ target.closest('button[class*="rounded-lg"]')
+
+ if (sendButton) {
+ await captureClaudePromptContent("button click")
+ }
+ },
+ true,
+ )
+
+ document.addEventListener(
+ "keydown",
+ async (event) => {
+ const target = event.target as HTMLElement
+
+ if (
+ (target.matches('div[contenteditable="true"]') ||
+ target.matches(".ProseMirror") ||
+ target.matches("textarea") ||
+ target.closest(".ProseMirror")) &&
+ event.key === "Enter" &&
+ !event.shiftKey
+ ) {
+ await captureClaudePromptContent("Enter key")
+ }
+ },
+ true,
+ )
+}
+
+async function setupClaudeAutoFetch() {
+ const result = await chrome.storage.local.get([
+ STORAGE_KEYS.AUTO_SEARCH_ENABLED,
+ ])
+ const autoSearchEnabled = result[STORAGE_KEYS.AUTO_SEARCH_ENABLED] ?? false
+ if (!autoSearchEnabled) {
+ return
+ }
+
+ const textareaElement = document.querySelector(
+ 'div[contenteditable="true"]',
+ ) as HTMLElement
+
+ if (
+ !textareaElement ||
+ textareaElement.hasAttribute("data-supermemory-auto-fetch")
+ ) {
+ return
+ }
+
+ textareaElement.setAttribute("data-supermemory-auto-fetch", "true")
+
+ const handleInput = () => {
+ if (claudeDebounceTimeout) {
+ clearTimeout(claudeDebounceTimeout)
+ }
+
+ claudeDebounceTimeout = setTimeout(async () => {
+ const content = textareaElement.textContent?.trim() || ""
+
+ if (content.length > 2) {
+ await getRelatedMemoriesForClaude(
+ POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED,
+ )
+ } else if (content.length === 0) {
+ const icons = document.querySelectorAll(
+ '[id*="sm-claude-input-bar-element"]',
+ )
+
+ icons.forEach((icon) => {
+ const iconElement = icon as HTMLElement
+ if (iconElement.dataset.originalHtml) {
+ iconElement.innerHTML = iconElement.dataset.originalHtml
+ delete iconElement.dataset.originalHtml
+ delete iconElement.dataset.memoriesData
+ }
+ })
+
+ if (textareaElement.dataset.supermemories) {
+ delete textareaElement.dataset.supermemories
+ }
+ }
+ }, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)
+ }
+
+ textareaElement.addEventListener("input", handleInput)
+}
diff --git a/apps/browser-extension/entrypoints/content/index.ts b/apps/browser-extension/entrypoints/content/index.ts
new file mode 100644
index 00000000..a79f50fb
--- /dev/null
+++ b/apps/browser-extension/entrypoints/content/index.ts
@@ -0,0 +1,67 @@
+import { DOMAINS, MESSAGE_TYPES } from "../../utils/constants"
+import { DOMUtils } from "../../utils/ui-components"
+import { initializeChatGPT } from "./chatgpt"
+import { initializeClaude } from "./claude"
+import { saveMemory, setupGlobalKeyboardShortcut, setupStorageListener } from "./shared"
+import { initializeT3 } from "./t3"
+import { handleTwitterNavigation, initializeTwitter, updateTwitterImportUI } from "./twitter"
+
+export default defineContentScript({
+ matches: ["<all_urls>"],
+ main() {
+ // Setup global event listeners
+ browser.runtime.onMessage.addListener(async (message) => {
+ if (message.action === MESSAGE_TYPES.SHOW_TOAST) {
+ DOMUtils.showToast(message.state)
+ } else if (message.action === MESSAGE_TYPES.SAVE_MEMORY) {
+ await saveMemory()
+ } else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) {
+ updateTwitterImportUI(message)
+ } else if (message.type === MESSAGE_TYPES.IMPORT_DONE) {
+ updateTwitterImportUI(message)
+ }
+ })
+
+ // Setup global keyboard shortcuts
+ setupGlobalKeyboardShortcut()
+
+ // Setup storage listener
+ setupStorageListener()
+
+ // Observer for dynamic content changes
+ const observeForDynamicChanges = () => {
+ const observer = new MutationObserver(() => {
+ if (DOMUtils.isOnDomain(DOMAINS.CHATGPT)) {
+ initializeChatGPT()
+ }
+ if (DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
+ initializeClaude()
+ }
+ if (DOMUtils.isOnDomain(DOMAINS.T3)) {
+ initializeT3()
+ }
+ if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
+ handleTwitterNavigation()
+ }
+ })
+
+ observer.observe(document.body, {
+ childList: true,
+ subtree: true,
+ })
+ }
+
+ // Initialize platform-specific functionality
+ initializeChatGPT()
+ initializeClaude()
+ initializeT3()
+ initializeTwitter()
+
+ // Start observing for dynamic changes
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", observeForDynamicChanges)
+ } else {
+ observeForDynamicChanges()
+ }
+ },
+}) \ No newline at end of file
diff --git a/apps/browser-extension/entrypoints/content/shared.ts b/apps/browser-extension/entrypoints/content/shared.ts
new file mode 100644
index 00000000..d8b665c5
--- /dev/null
+++ b/apps/browser-extension/entrypoints/content/shared.ts
@@ -0,0 +1,77 @@
+import { MESSAGE_TYPES, STORAGE_KEYS } from "../../utils/constants"
+import { DOMUtils } from "../../utils/ui-components"
+
+export async function saveMemory() {
+ try {
+ DOMUtils.showToast("loading")
+
+ const highlightedText = window.getSelection()?.toString() || ""
+ const url = window.location.href
+ const html = document.documentElement.outerHTML
+
+ const response = await browser.runtime.sendMessage({
+ action: MESSAGE_TYPES.SAVE_MEMORY,
+ data: {
+ html,
+ highlightedText,
+ url,
+ },
+ actionSource: "context_menu",
+ })
+
+ console.log("Response from enxtension:", response)
+ if (response.success) {
+ DOMUtils.showToast("success")
+ } else {
+ DOMUtils.showToast("error")
+ }
+ } catch (error) {
+ console.error("Error saving memory:", error)
+ DOMUtils.showToast("error")
+ }
+}
+
+export function setupGlobalKeyboardShortcut() {
+ document.addEventListener("keydown", async (event) => {
+ if (
+ (event.ctrlKey || event.metaKey) &&
+ event.shiftKey &&
+ event.key === "m"
+ ) {
+ event.preventDefault()
+ await saveMemory()
+ }
+ })
+}
+
+export function setupStorageListener() {
+ window.addEventListener("message", (event) => {
+ if (event.source !== window) {
+ return
+ }
+ const bearerToken = event.data.token
+ const userData = event.data.userData
+ if (bearerToken && userData) {
+ if (
+ !(
+ window.location.hostname === "localhost" ||
+ window.location.hostname === "supermemory.ai" ||
+ window.location.hostname === "app.supermemory.ai"
+ )
+ ) {
+ console.log(
+ "Bearer token and user data is only allowed to be used on localhost or supermemory.ai",
+ )
+ return
+ }
+
+ chrome.storage.local.set(
+ {
+ [STORAGE_KEYS.BEARER_TOKEN]: bearerToken,
+ [STORAGE_KEYS.USER_DATA]: userData,
+ },
+ () => {},
+ )
+ }
+ })
+} \ No newline at end of file
diff --git a/apps/browser-extension/entrypoints/content/t3.ts b/apps/browser-extension/entrypoints/content/t3.ts
new file mode 100644
index 00000000..4332076e
--- /dev/null
+++ b/apps/browser-extension/entrypoints/content/t3.ts
@@ -0,0 +1,692 @@
+import {
+ DOMAINS,
+ ELEMENT_IDS,
+ MESSAGE_TYPES,
+ POSTHOG_EVENT_KEY,
+ STORAGE_KEYS,
+ UI_CONFIG,
+} from "../../utils/constants"
+import { createT3InputBarElement, DOMUtils } from "../../utils/ui-components"
+
+let t3DebounceTimeout: NodeJS.Timeout | null = null
+let t3RouteObserver: MutationObserver | null = null
+let t3UrlCheckInterval: NodeJS.Timeout | null = null
+let t3ObserverThrottle: NodeJS.Timeout | null = null
+
+export function initializeT3() {
+ if (!DOMUtils.isOnDomain(DOMAINS.T3)) {
+ return
+ }
+
+ if (document.body.hasAttribute("data-t3-initialized")) {
+ return
+ }
+
+ setTimeout(() => {
+ console.log("Adding supermemory icon to T3 input")
+ addSupermemoryIconToT3Input()
+ setupT3AutoFetch()
+ }, 2000)
+
+ setupT3PromptCapture()
+
+ setupT3RouteChangeDetection()
+
+ document.body.setAttribute("data-t3-initialized", "true")
+}
+
+function setupT3RouteChangeDetection() {
+ if (t3RouteObserver) {
+ t3RouteObserver.disconnect()
+ }
+ if (t3UrlCheckInterval) {
+ clearInterval(t3UrlCheckInterval)
+ }
+ if (t3ObserverThrottle) {
+ clearTimeout(t3ObserverThrottle)
+ t3ObserverThrottle = null
+ }
+
+ let currentUrl = window.location.href
+
+ const checkForRouteChange = () => {
+ if (window.location.href !== currentUrl) {
+ currentUrl = window.location.href
+ console.log("T3 route changed, re-adding supermemory icon")
+ setTimeout(() => {
+ addSupermemoryIconToT3Input()
+ setupT3AutoFetch()
+ }, 1000)
+ }
+ }
+
+ t3UrlCheckInterval = setInterval(checkForRouteChange, 2000)
+
+ t3RouteObserver = new MutationObserver((mutations) => {
+ if (t3ObserverThrottle) {
+ return
+ }
+
+ let shouldRecheck = false
+ mutations.forEach((mutation) => {
+ if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
+ mutation.addedNodes.forEach((node) => {
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ const element = node as Element
+ if (
+ element.querySelector?.("textarea") ||
+ element.querySelector?.('div[contenteditable="true"]') ||
+ element.matches?.("textarea") ||
+ element.matches?.('div[contenteditable="true"]')
+ ) {
+ shouldRecheck = true
+ }
+ }
+ })
+ }
+ })
+
+ if (shouldRecheck) {
+ t3ObserverThrottle = setTimeout(() => {
+ try {
+ t3ObserverThrottle = null
+ addSupermemoryIconToT3Input()
+ setupT3AutoFetch()
+ } catch (error) {
+ console.error("Error in T3 observer callback:", error)
+ }
+ }, 300)
+ }
+ })
+
+ try {
+ t3RouteObserver.observe(document.body, {
+ childList: true,
+ subtree: true,
+ })
+ } catch (error) {
+ console.error("Failed to set up T3 route observer:", error)
+ if (t3UrlCheckInterval) {
+ clearInterval(t3UrlCheckInterval)
+ }
+ t3UrlCheckInterval = setInterval(checkForRouteChange, 1000)
+ }
+}
+
+function addSupermemoryIconToT3Input() {
+ const targetContainers = document.querySelectorAll(
+ ".flex.min-w-0.items-center.gap-2.overflow-hidden",
+ )
+
+ targetContainers.forEach((container) => {
+ if (container.hasAttribute("data-supermemory-icon-added")) {
+ return
+ }
+
+ const existingIcon = container.querySelector(
+ `#${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}`,
+ )
+ if (existingIcon) {
+ container.setAttribute("data-supermemory-icon-added", "true")
+ return
+ }
+
+ const supermemoryIcon = createT3InputBarElement(async () => {
+ await getRelatedMemoriesForT3(POSTHOG_EVENT_KEY.T3_CHAT_MEMORIES_SEARCHED)
+ })
+
+ supermemoryIcon.id = `${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
+
+ container.setAttribute("data-supermemory-icon-added", "true")
+
+ container.insertBefore(supermemoryIcon, container.firstChild)
+ })
+}
+
+async function getRelatedMemoriesForT3(actionSource: string) {
+ try {
+ let userQuery = ""
+
+ const supermemoryContainer = document.querySelector(
+ '[data-supermemory-icon-added="true"]',
+ )
+ if (
+ supermemoryContainer?.parentElement?.parentElement?.previousElementSibling
+ ) {
+ const textareaElement =
+ supermemoryContainer.parentElement.parentElement.previousElementSibling.querySelector(
+ "textarea",
+ )
+ userQuery = textareaElement?.value || ""
+ }
+
+ if (!userQuery.trim()) {
+ const textareaElement = document.querySelector(
+ 'div[contenteditable="true"]',
+ ) as HTMLElement
+ userQuery =
+ textareaElement?.innerText || textareaElement?.textContent || ""
+ }
+
+ if (!userQuery.trim()) {
+ const textareas = document.querySelectorAll("textarea")
+ for (const textarea of textareas) {
+ const text = (textarea as HTMLTextAreaElement).value
+ if (text?.trim()) {
+ userQuery = text.trim()
+ break
+ }
+ }
+ }
+
+ console.log("T3 query extracted:", userQuery)
+
+ if (!userQuery.trim()) {
+ console.log("No query text found for T3")
+ return
+ }
+
+ const icon = document.querySelector('[id*="sm-t3-input-bar-element"]')
+
+ const iconElement = icon as HTMLElement
+
+ if (!iconElement) {
+ console.warn("T3 icon element not found, cannot update feedback")
+ return
+ }
+
+ updateT3IconFeedback("Searching memories...", iconElement)
+
+ const timeoutPromise = new Promise((_, reject) =>
+ setTimeout(
+ () => reject(new Error("Memory search timeout")),
+ UI_CONFIG.API_REQUEST_TIMEOUT,
+ ),
+ )
+
+ const response = await Promise.race([
+ browser.runtime.sendMessage({
+ action: MESSAGE_TYPES.GET_RELATED_MEMORIES,
+ data: userQuery,
+ actionSource: actionSource,
+ }),
+ timeoutPromise,
+ ])
+
+ console.log("T3 memories response:", response)
+
+ if (response?.success && response?.data) {
+ let textareaElement = null
+ const supermemoryContainer = document.querySelector(
+ '[data-supermemory-icon-added="true"]',
+ )
+ if (
+ supermemoryContainer?.parentElement?.parentElement
+ ?.previousElementSibling
+ ) {
+ textareaElement =
+ supermemoryContainer.parentElement.parentElement.previousElementSibling.querySelector(
+ "textarea",
+ )
+ }
+
+ if (!textareaElement) {
+ textareaElement = document.querySelector(
+ 'div[contenteditable="true"]',
+ ) as HTMLElement
+ }
+
+ if (textareaElement) {
+ if (textareaElement.tagName === "TEXTAREA") {
+ ;(textareaElement as HTMLTextAreaElement).dataset.supermemories =
+ `<br>Supermemories of user (only for the reference): ${response.data}</br>`
+ } else {
+ ;(textareaElement as HTMLElement).dataset.supermemories =
+ `<br>Supermemories of user (only for the reference): ${response.data}</br>`
+ }
+
+ iconElement.dataset.memoriesData = response.data
+
+ updateT3IconFeedback("Included Memories", iconElement)
+ } else {
+ console.warn("T3 input area not found after successful memory fetch")
+ updateT3IconFeedback("Memories found", iconElement)
+ }
+ } else {
+ console.warn("No memories found or API response invalid for T3")
+ updateT3IconFeedback("No memories found", iconElement)
+ }
+ } catch (error) {
+ console.error("Error getting related memories for T3:", error)
+ try {
+ const icon = document.querySelector(
+ '[id*="sm-t3-input-bar-element"]',
+ ) as HTMLElement
+ if (icon) {
+ updateT3IconFeedback("Error fetching memories", icon)
+ }
+ } catch (feedbackError) {
+ console.error("Failed to update T3 error feedback:", feedbackError)
+ }
+ }
+}
+
+function updateT3IconFeedback(
+ message: string,
+ iconElement: HTMLElement,
+ resetAfter = 0,
+) {
+ if (!iconElement.dataset.originalHtml) {
+ iconElement.dataset.originalHtml = iconElement.innerHTML
+ }
+
+ const feedbackDiv = document.createElement("div")
+ feedbackDiv.style.cssText = `
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 8px;
+ background: #513EA9;
+ border-radius: 6px;
+ color: white;
+ font-size: 12px;
+ font-weight: 500;
+ cursor: ${message === "Included Memories" ? "pointer" : "default"};
+ position: relative;
+ `
+
+ feedbackDiv.innerHTML = `
+ <span>✓</span>
+ <span>${message}</span>
+ `
+
+ if (message === "Included Memories" && iconElement.dataset.memoriesData) {
+ const popup = document.createElement("div")
+ popup.style.cssText = `
+ position: fixed;
+ bottom: 80px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: #1a1a1a;
+ color: white;
+ padding: 0;
+ border-radius: 12px;
+ font-size: 13px;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
+ max-width: 500px;
+ max-height: 400px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
+ z-index: 999999;
+ display: none;
+ border: 1px solid #333;
+ `
+
+ const header = document.createElement("div")
+ header.style.cssText = `
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px;
+ border-bottom: 1px solid #333;
+ opacity: 0.8;
+ `
+ header.innerHTML = `
+ <span style="font-weight: 600; color: #fff;">Included Memories</span>
+ `
+
+ const content = document.createElement("div")
+ content.style.cssText = `
+ padding: 0;
+ max-height: 300px;
+ overflow-y: auto;
+ `
+
+ const memoriesText = iconElement.dataset.memoriesData || ""
+ console.log("Memories text:", memoriesText)
+ const individualMemories = memoriesText
+ .split(/[,\n]/)
+ .map((memory) => memory.trim())
+ .filter((memory) => memory.length > 0 && memory !== ",")
+ console.log("Individual memories:", individualMemories)
+
+ individualMemories.forEach((memory, index) => {
+ const memoryItem = document.createElement("div")
+ memoryItem.style.cssText = `
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 10px;
+ font-size: 13px;
+ line-height: 1.4;
+ `
+
+ const memoryText = document.createElement("div")
+ memoryText.style.cssText = `
+ flex: 1;
+ color: #e5e5e5;
+ `
+ memoryText.textContent = memory.trim()
+
+ const removeBtn = document.createElement("button")
+ removeBtn.style.cssText = `
+ background: transparent;
+ color: #9ca3af;
+ border: none;
+ padding: 4px;
+ border-radius: 4px;
+ cursor: pointer;
+ flex-shrink: 0;
+ height: fit-content;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ `
+ removeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`
+ removeBtn.dataset.memoryIndex = index.toString()
+
+ removeBtn.addEventListener("mouseenter", () => {
+ removeBtn.style.color = "#ef4444"
+ })
+ removeBtn.addEventListener("mouseleave", () => {
+ removeBtn.style.color = "#9ca3af"
+ })
+
+ memoryItem.appendChild(memoryText)
+ memoryItem.appendChild(removeBtn)
+ content.appendChild(memoryItem)
+ })
+
+ popup.appendChild(header)
+ popup.appendChild(content)
+ document.body.appendChild(popup)
+
+ feedbackDiv.addEventListener("mouseenter", () => {
+ const textSpan = feedbackDiv.querySelector("span:last-child")
+ if (textSpan) {
+ textSpan.textContent = "Click to see memories"
+ }
+ })
+
+ feedbackDiv.addEventListener("mouseleave", () => {
+ const textSpan = feedbackDiv.querySelector("span:last-child")
+ if (textSpan) {
+ textSpan.textContent = "Included Memories"
+ }
+ })
+
+ feedbackDiv.addEventListener("click", (e) => {
+ e.stopPropagation()
+ popup.style.display = "block"
+ })
+
+ document.addEventListener("click", (e) => {
+ if (!popup.contains(e.target as Node)) {
+ popup.style.display = "none"
+ }
+ })
+
+ content.querySelectorAll("button[data-memory-index]").forEach((button) => {
+ const htmlButton = button as HTMLButtonElement
+ htmlButton.addEventListener("click", () => {
+ const index = Number.parseInt(htmlButton.dataset.memoryIndex || "0", 10)
+ const memoryItem = htmlButton.parentElement
+
+ if (memoryItem) {
+ content.removeChild(memoryItem)
+ }
+
+ const currentMemories = (iconElement.dataset.memoriesData || "")
+ .split(/[,\n]/)
+ .map((memory) => memory.trim())
+ .filter((memory) => memory.length > 0 && memory !== ",")
+ currentMemories.splice(index, 1)
+
+ const updatedMemories = currentMemories.join(" ,")
+
+ iconElement.dataset.memoriesData = updatedMemories
+
+ const textareaElement =
+ (document.querySelector("textarea") as HTMLTextAreaElement) ||
+ (document.querySelector('div[contenteditable="true"]') as HTMLElement)
+ if (textareaElement) {
+ textareaElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${updatedMemories}</div>`
+ }
+
+ content
+ .querySelectorAll("button[data-memory-index]")
+ .forEach((btn, newIndex) => {
+ const htmlBtn = btn as HTMLButtonElement
+ htmlBtn.dataset.memoryIndex = newIndex.toString()
+ })
+
+ if (currentMemories.length <= 1) {
+ if (textareaElement?.dataset.supermemories) {
+ delete textareaElement.dataset.supermemories
+ delete iconElement.dataset.memoriesData
+ iconElement.innerHTML = iconElement.dataset.originalHtml || ""
+ delete iconElement.dataset.originalHtml
+ }
+ popup.style.display = "none"
+ if (document.body.contains(popup)) {
+ document.body.removeChild(popup)
+ }
+ }
+ })
+ })
+
+ setTimeout(() => {
+ if (document.body.contains(popup)) {
+ document.body.removeChild(popup)
+ }
+ }, 300000)
+ }
+
+ iconElement.innerHTML = ""
+ iconElement.appendChild(feedbackDiv)
+
+ if (resetAfter > 0) {
+ setTimeout(() => {
+ iconElement.innerHTML = iconElement.dataset.originalHtml || ""
+ delete iconElement.dataset.originalHtml
+ }, resetAfter)
+ }
+}
+
+function setupT3PromptCapture() {
+ if (document.body.hasAttribute("data-t3-prompt-capture-setup")) {
+ return
+ }
+ document.body.setAttribute("data-t3-prompt-capture-setup", "true")
+
+ const captureT3PromptContent = async (source: string) => {
+ let promptContent = ""
+
+ const textarea = document.querySelector("textarea") as HTMLTextAreaElement
+ if (textarea) {
+ promptContent = textarea.value || ""
+ }
+
+ if (!promptContent) {
+ const contentEditableDiv = document.querySelector(
+ 'div[contenteditable="true"]',
+ ) as HTMLElement
+ if (contentEditableDiv) {
+ promptContent =
+ contentEditableDiv.textContent || contentEditableDiv.innerText || ""
+ }
+ }
+
+ const textareaElement =
+ textarea ||
+ (document.querySelector('div[contenteditable="true"]') as HTMLElement)
+ const storedMemories = textareaElement?.dataset.supermemories
+ if (
+ storedMemories &&
+ textareaElement &&
+ !promptContent.includes("Supermemories of user")
+ ) {
+ if (textareaElement.tagName === "TEXTAREA") {
+ ;(textareaElement as HTMLTextAreaElement).value =
+ `${promptContent} ${storedMemories}`
+ promptContent = (textareaElement as HTMLTextAreaElement).value
+ } else {
+ textareaElement.innerHTML = `${textareaElement.innerHTML} ${storedMemories}`
+ promptContent =
+ textareaElement.textContent || textareaElement.innerText || ""
+ }
+ }
+
+ if (promptContent.trim()) {
+ console.log(`T3 prompt submitted via ${source}:`, promptContent)
+
+ try {
+ await browser.runtime.sendMessage({
+ action: MESSAGE_TYPES.CAPTURE_PROMPT,
+ data: {
+ prompt: promptContent,
+ platform: "t3",
+ source: source,
+ },
+ })
+ } catch (error) {
+ console.error("Error sending T3 prompt to background:", error)
+ }
+ }
+
+ const icons = document.querySelectorAll('[id*="sm-t3-input-bar-element"]')
+
+ icons.forEach((icon) => {
+ const iconElement = icon as HTMLElement
+ if (iconElement.dataset.originalHtml) {
+ iconElement.innerHTML = iconElement.dataset.originalHtml
+ delete iconElement.dataset.originalHtml
+ delete iconElement.dataset.memoriesData
+ }
+ })
+
+ if (textareaElement?.dataset.supermemories) {
+ delete textareaElement.dataset.supermemories
+ }
+ }
+
+ document.addEventListener(
+ "click",
+ async (event) => {
+ const target = event.target as HTMLElement
+ const sendButton =
+ target.closest("button.focus-visible\\:ring-ring") ||
+ target.closest('button[class*="bg-[rgb(162,59,103)]"]') ||
+ target.closest('button[class*="rounded-lg"]')
+
+ if (sendButton) {
+ await captureT3PromptContent("button click")
+ }
+ },
+ true,
+ )
+
+ document.addEventListener(
+ "keydown",
+ async (event) => {
+ const target = event.target as HTMLElement
+
+ if (
+ (target.matches("textarea") ||
+ target.matches('div[contenteditable="true"]')) &&
+ event.key === "Enter" &&
+ !event.shiftKey
+ ) {
+ if (target.matches("textarea")) {
+ const promptContent = (target as HTMLTextAreaElement).value || ""
+ if (promptContent.trim()) {
+ console.log("T3 prompt submitted via Enter key:", promptContent)
+
+ try {
+ await browser.runtime.sendMessage({
+ action: MESSAGE_TYPES.CAPTURE_PROMPT,
+ data: {
+ prompt: promptContent,
+ platform: "t3",
+ source: "Enter key",
+ },
+ actionSource: "t3",
+ })
+ } catch (error) {
+ console.error(
+ "Error sending T3 textarea prompt to background:",
+ error,
+ )
+ }
+ }
+ } else {
+ await captureT3PromptContent("Enter key")
+ }
+ }
+ },
+ true,
+ )
+}
+
+async function setupT3AutoFetch() {
+ const result = await chrome.storage.local.get([
+ STORAGE_KEYS.AUTO_SEARCH_ENABLED,
+ ])
+ const autoSearchEnabled = result[STORAGE_KEYS.AUTO_SEARCH_ENABLED] ?? false
+
+ if (!autoSearchEnabled) {
+ return
+ }
+
+ const textareaElement =
+ (document.querySelector("textarea") as HTMLTextAreaElement) ||
+ (document.querySelector('div[contenteditable="true"]') as HTMLElement)
+
+ if (
+ !textareaElement ||
+ textareaElement.hasAttribute("data-supermemory-auto-fetch")
+ ) {
+ return
+ }
+
+ textareaElement.setAttribute("data-supermemory-auto-fetch", "true")
+
+ const handleInput = () => {
+ if (t3DebounceTimeout) {
+ clearTimeout(t3DebounceTimeout)
+ }
+
+ t3DebounceTimeout = setTimeout(async () => {
+ let content = ""
+ if (textareaElement.tagName === "TEXTAREA") {
+ content = (textareaElement as HTMLTextAreaElement).value?.trim() || ""
+ } else {
+ content = textareaElement.textContent?.trim() || ""
+ }
+
+ if (content.length > 2) {
+ await getRelatedMemoriesForT3(
+ POSTHOG_EVENT_KEY.T3_CHAT_MEMORIES_AUTO_SEARCHED,
+ )
+ } else if (content.length === 0) {
+ const icons = document.querySelectorAll(
+ '[id*="sm-t3-input-bar-element"]',
+ )
+
+ icons.forEach((icon) => {
+ const iconElement = icon as HTMLElement
+ if (iconElement.dataset.originalHtml) {
+ iconElement.innerHTML = iconElement.dataset.originalHtml
+ delete iconElement.dataset.originalHtml
+ delete iconElement.dataset.memoriesData
+ }
+ })
+
+ if (textareaElement.dataset.supermemories) {
+ delete textareaElement.dataset.supermemories
+ }
+ }
+ }, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)
+ }
+
+ textareaElement.addEventListener("input", handleInput)
+}
diff --git a/apps/browser-extension/entrypoints/content/twitter.ts b/apps/browser-extension/entrypoints/content/twitter.ts
new file mode 100644
index 00000000..15c6ec50
--- /dev/null
+++ b/apps/browser-extension/entrypoints/content/twitter.ts
@@ -0,0 +1,102 @@
+import {
+ DOMAINS,
+ ELEMENT_IDS,
+ MESSAGE_TYPES,
+ POSTHOG_EVENT_KEY,
+} from "../../utils/constants"
+import { trackEvent } from "../../utils/posthog"
+import { createTwitterImportButton, DOMUtils } from "../../utils/ui-components"
+
+export function initializeTwitter() {
+ if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
+ return
+ }
+
+ // Initial setup
+ if (window.location.pathname === "/i/bookmarks") {
+ setTimeout(() => {
+ addTwitterImportButton()
+ }, 2000)
+ } else {
+ // Remove button if not on bookmarks page
+ if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
+ DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)
+ }
+ }
+}
+
+function addTwitterImportButton() {
+ // Only show the import button on the bookmarks page
+ if (window.location.pathname !== "/i/bookmarks") {
+ return
+ }
+
+ if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
+ return
+ }
+
+ const button = createTwitterImportButton(async () => {
+ try {
+ await browser.runtime.sendMessage({
+ type: MESSAGE_TYPES.BATCH_IMPORT_ALL,
+ })
+ await trackEvent(POSTHOG_EVENT_KEY.TWITTER_IMPORT_STARTED, {
+ source: `${POSTHOG_EVENT_KEY.SOURCE}_content_script`,
+ })
+ } catch (error) {
+ console.error("Error starting import:", error)
+ }
+ })
+
+ document.body.appendChild(button)
+}
+
+export function updateTwitterImportUI(message: {
+ type: string
+ importedMessage?: string
+ totalImported?: number
+}) {
+ const importButton = document.getElementById(
+ ELEMENT_IDS.TWITTER_IMPORT_BUTTON,
+ )
+ if (!importButton) return
+
+ const iconUrl = browser.runtime.getURL("/icon-16.png")
+
+ if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) {
+ importButton.innerHTML = `
+ <img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
+ <span style="font-weight: 500; font-size: 14px;">${message.importedMessage}</span>
+ `
+ importButton.style.cursor = "default"
+ }
+
+ if (message.type === MESSAGE_TYPES.IMPORT_DONE) {
+ importButton.innerHTML = `
+ <img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
+ <span style="font-weight: 500; font-size: 14px; color: #059669;">✓ Imported ${message.totalImported} tweets!</span>
+ `
+
+ setTimeout(() => {
+ importButton.innerHTML = `
+ <img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
+ <span style="font-weight: 500; font-size: 14px;">Import Bookmarks</span>
+ `
+ importButton.style.cursor = "pointer"
+ }, 3000)
+ }
+}
+
+export function handleTwitterNavigation() {
+ if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
+ return
+ }
+
+ if (window.location.pathname === "/i/bookmarks") {
+ addTwitterImportButton()
+ } else {
+ if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
+ DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)
+ }
+ }
+} \ No newline at end of file
diff --git a/apps/browser-extension/entrypoints/popup/App.tsx b/apps/browser-extension/entrypoints/popup/App.tsx
index ddb498c6..3e4f15e2 100644
--- a/apps/browser-extension/entrypoints/popup/App.tsx
+++ b/apps/browser-extension/entrypoints/popup/App.tsx
@@ -16,7 +16,10 @@ function App() {
const [currentUrl, setCurrentUrl] = useState<string>("")
const [currentTitle, setCurrentTitle] = useState<string>("")
const [saving, setSaving] = useState<boolean>(false)
- const [activeTab, setActiveTab] = useState<"save" | "imports">("save")
+ const [activeTab, setActiveTab] = useState<"save" | "imports" | "settings">(
+ "save",
+ )
+ const [autoSearchEnabled, setAutoSearchEnabled] = useState<boolean>(false)
const queryClient = useQueryClient()
const { data: projects = [], isLoading: loadingProjects } = useProjects({
@@ -32,9 +35,14 @@ function App() {
try {
const result = await chrome.storage.local.get([
STORAGE_KEYS.BEARER_TOKEN,
+ STORAGE_KEYS.AUTO_SEARCH_ENABLED,
])
const isSignedIn = !!result[STORAGE_KEYS.BEARER_TOKEN]
setUserSignedIn(isSignedIn)
+
+ const autoSearchSetting =
+ result[STORAGE_KEYS.AUTO_SEARCH_ENABLED] ?? false
+ setAutoSearchEnabled(autoSearchSetting)
} catch (error) {
console.error("Error checking auth status:", error)
setUserSignedIn(false)
@@ -103,11 +111,6 @@ function App() {
action: MESSAGE_TYPES.SHOW_TOAST,
state: "success",
})
- } else {
- await chrome.tabs.sendMessage(tabs[0].id, {
- action: MESSAGE_TYPES.SHOW_TOAST,
- state: "error",
- })
}
window.close()
@@ -136,6 +139,17 @@ function App() {
}
}
+ const handleAutoSearchToggle = async (enabled: boolean) => {
+ try {
+ await chrome.storage.local.set({
+ [STORAGE_KEYS.AUTO_SEARCH_ENABLED]: enabled,
+ })
+ setAutoSearchEnabled(enabled)
+ } catch (error) {
+ console.error("Error updating auto search setting:", error)
+ }
+ }
+
const handleSignOut = async () => {
try {
await chrome.storage.local.remove([STORAGE_KEYS.BEARER_TOKEN])
@@ -208,7 +222,7 @@ function App() {
{/* Tab Navigation */}
<div className="flex bg-gray-100 rounded-lg p-1 mb-4">
<button
- className={`flex-1 py-2 px-4 bg-transparent border-none rounded-md text-sm font-medium cursor-pointer transition-all duration-200 outline-none appearance-none ${
+ className={`flex-1 py-2 px-3 bg-transparent border-none rounded-md text-sm font-medium cursor-pointer transition-all duration-200 outline-none appearance-none ${
activeTab === "save"
? "bg-white text-black shadow-sm"
: "text-gray-500 hover:text-gray-700"
@@ -219,7 +233,7 @@ function App() {
Save
</button>
<button
- className={`flex-1 py-2 px-4 bg-transparent border-none rounded-md text-sm font-medium cursor-pointer transition-all duration-200 outline-none appearance-none ${
+ className={`flex-1 py-2 px-3 bg-transparent border-none rounded-md text-sm font-medium cursor-pointer transition-all duration-200 outline-none appearance-none ${
activeTab === "imports"
? "bg-white text-black shadow-sm"
: "text-gray-500 hover:text-gray-700"
@@ -229,6 +243,17 @@ function App() {
>
Imports
</button>
+ <button
+ className={`flex-1 py-2 px-3 bg-transparent border-none rounded-md text-sm font-medium cursor-pointer transition-all duration-200 outline-none appearance-none ${
+ activeTab === "settings"
+ ? "bg-white text-black shadow-sm"
+ : "text-gray-500 hover:text-gray-700"
+ }`}
+ onClick={() => setActiveTab("settings")}
+ type="button"
+ >
+ Settings
+ </button>
</div>
{/* Tab Content */}
@@ -295,7 +320,7 @@ function App() {
</button>
</div>
</div>
- ) : (
+ ) : activeTab === "imports" ? (
<div className="flex flex-col gap-4 min-h-[200px]">
{/* Import Actions */}
<div className="flex flex-col gap-4">
@@ -370,6 +395,40 @@ function App() {
</div>
</div>
</div>
+ ) : (
+ <div className="flex flex-col gap-4 min-h-[200px]">
+ <div className="mb-4">
+ <h3 className="text-base font-semibold text-black mb-3">
+ Chat Integration
+ </h3>
+ <div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-200">
+ <div className="flex flex-col">
+ <span className="text-sm font-medium text-black">
+ Auto Search Memories
+ </span>
+ <span className="text-xs text-gray-500">
+ Automatically search your memories while typing in chat
+ apps
+ </span>
+ </div>
+ <label className="relative inline-flex items-center cursor-pointer">
+ <input
+ checked={autoSearchEnabled}
+ className="sr-only peer"
+ onChange={(e) =>
+ handleAutoSearchToggle(e.target.checked)
+ }
+ type="checkbox"
+ />
+ <div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-gray-700" />
+ </label>
+ </div>
+ <p className="text-xs text-gray-500 mt-2">
+ When enabled, supermemory will search your memories as you
+ type in ChatGPT, Claude, and T3.chat
+ </p>
+ </div>
+ </div>
)}
{showProjectSelector && (
diff --git a/apps/browser-extension/utils/constants.ts b/apps/browser-extension/utils/constants.ts
index 5ebd76d1..ac286717 100644
--- a/apps/browser-extension/utils/constants.ts
+++ b/apps/browser-extension/utils/constants.ts
@@ -21,6 +21,7 @@ export const STORAGE_KEYS = {
TWITTER_CSRF: "twitter-csrf",
TWITTER_AUTH_TOKEN: "twitter-auth-token",
DEFAULT_PROJECT: "sm-default-project",
+ AUTO_SEARCH_ENABLED: "sm-auto-search-enabled",
} as const
/**
@@ -44,6 +45,10 @@ export const UI_CONFIG = {
TOAST_DURATION: 3000, // milliseconds
RATE_LIMIT_BASE_WAIT: 60000, // 1 minute
PAGINATION_DELAY: 1000, // 1 second between requests
+ AUTO_SEARCH_DEBOUNCE_DELAY: 1500, // milliseconds to wait after user stops typing
+ 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
} as const
/**
@@ -75,6 +80,7 @@ export const MESSAGE_TYPES = {
IMPORT_UPDATE: "sm-import-update",
IMPORT_DONE: "sm-import-done",
GET_RELATED_MEMORIES: "sm-get-related-memories",
+ CAPTURE_PROMPT: "sm-capture-prompt",
} as const
export const CONTEXT_MENU_IDS = {
@@ -87,6 +93,9 @@ export const POSTHOG_EVENT_KEY = {
SAVE_MEMORY_ATTEMPT_FAILED: "save_memory_attempt_failed",
SOURCE: "extension",
T3_CHAT_MEMORIES_SEARCHED: "t3_chat_memories_searched",
+ T3_CHAT_MEMORIES_AUTO_SEARCHED: "t3_chat_memories_auto_searched",
CLAUDE_CHAT_MEMORIES_SEARCHED: "claude_chat_memories_searched",
+ 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",
} as const
diff --git a/apps/browser-extension/utils/memory-popup.ts b/apps/browser-extension/utils/memory-popup.ts
new file mode 100644
index 00000000..ba2d2a1c
--- /dev/null
+++ b/apps/browser-extension/utils/memory-popup.ts
@@ -0,0 +1,93 @@
+/**
+ * Memory Popup Utilities
+ * Standardized popup positioning and styling for memory display across platforms
+ */
+
+export interface MemoryPopupConfig {
+ memoriesData: string
+ onClose: () => void
+ onRemove?: () => void
+}
+
+export function createMemoryPopup(config: MemoryPopupConfig): HTMLElement {
+ const popup = document.createElement("div")
+ popup.style.cssText = `
+ position: fixed;
+ bottom: 80px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: #1a1a1a;
+ color: white;
+ padding: 0;
+ border-radius: 12px;
+ font-size: 13px;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
+ max-width: 500px;
+ max-height: 400px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
+ z-index: 999999;
+ display: none;
+ overflow: hidden;
+ `
+
+ const header = document.createElement("div")
+ header.style.cssText = `
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px;
+ border-bottom: 1px solid #333;
+ opacity: 0.8;
+ `
+ header.innerHTML = `
+ <span style="font-size: 11px; font-weight: 600; letter-spacing: 0.5px;">INCLUDED MEMORIES</span>
+ <div style="display: flex; gap: 4px;">
+ ${config.onRemove ? '<button id="remove-memories-btn" style="background: none; border: none; color: #ff4444; cursor: pointer; font-size: 14px; padding: 2px; border-radius: 2px;" title="Remove memories">✕</button>' : ""}
+ <button id="close-popup-btn" style="background: none; border: none; color: white; cursor: pointer; font-size: 14px; padding: 2px; border-radius: 2px;">✕</button>
+ </div>
+ `
+
+ const content = document.createElement("div")
+ content.style.cssText = `
+ padding: 8px;
+ max-height: 300px;
+ overflow-y: auto;
+ line-height: 1.4;
+ `
+ content.textContent = config.memoriesData
+
+ const closeBtn = header.querySelector("#close-popup-btn")
+ closeBtn?.addEventListener("click", config.onClose)
+
+ const removeBtn = header.querySelector("#remove-memories-btn")
+ if (removeBtn && config.onRemove) {
+ removeBtn.addEventListener("click", config.onRemove)
+ }
+
+ popup.appendChild(header)
+ popup.appendChild(content)
+
+ return popup
+}
+
+export function showMemoryPopup(popup: HTMLElement): void {
+ popup.style.display = "block"
+
+ setTimeout(() => {
+ if (popup.style.display === "block") {
+ hideMemoryPopup(popup)
+ }
+ }, 10000)
+}
+
+export function hideMemoryPopup(popup: HTMLElement): void {
+ popup.style.display = "none"
+}
+
+export function toggleMemoryPopup(popup: HTMLElement): void {
+ if (popup.style.display === "none" || popup.style.display === "") {
+ showMemoryPopup(popup)
+ } else {
+ hideMemoryPopup(popup)
+ }
+}
diff --git a/apps/browser-extension/utils/route-detection.ts b/apps/browser-extension/utils/route-detection.ts
new file mode 100644
index 00000000..a8a4714f
--- /dev/null
+++ b/apps/browser-extension/utils/route-detection.ts
@@ -0,0 +1,117 @@
+/**
+ * Route Detection Utilities
+ * Shared logic for detecting route changes across different AI chat platforms
+ */
+
+import { UI_CONFIG } from "./constants"
+
+export interface RouteDetectionConfig {
+ platform: string
+ selectors: string[]
+ reinitCallback: () => void
+ checkInterval?: number
+ observerThrottleDelay?: number
+}
+
+export interface RouteDetectionCleanup {
+ observer: MutationObserver | null
+ urlCheckInterval: NodeJS.Timeout | null
+ observerThrottle: NodeJS.Timeout | null
+}
+
+export function createRouteDetection(
+ config: RouteDetectionConfig,
+ cleanup: RouteDetectionCleanup,
+): void {
+ if (cleanup.observer) {
+ cleanup.observer.disconnect()
+ }
+ if (cleanup.urlCheckInterval) {
+ clearInterval(cleanup.urlCheckInterval)
+ }
+ if (cleanup.observerThrottle) {
+ clearTimeout(cleanup.observerThrottle)
+ cleanup.observerThrottle = null
+ }
+
+ let currentUrl = window.location.href
+
+ const checkForRouteChange = () => {
+ if (window.location.href !== currentUrl) {
+ currentUrl = window.location.href
+ console.log(`${config.platform} route changed, re-initializing`)
+ setTimeout(config.reinitCallback, 1000)
+ }
+ }
+
+ cleanup.urlCheckInterval = setInterval(
+ checkForRouteChange,
+ config.checkInterval || UI_CONFIG.ROUTE_CHECK_INTERVAL,
+ )
+
+ cleanup.observer = new MutationObserver((mutations) => {
+ if (cleanup.observerThrottle) {
+ return
+ }
+
+ let shouldRecheck = false
+ mutations.forEach((mutation) => {
+ if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
+ mutation.addedNodes.forEach((node) => {
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ const element = node as Element
+
+ for (const selector of config.selectors) {
+ if (
+ element.querySelector?.(selector) ||
+ element.matches?.(selector)
+ ) {
+ shouldRecheck = true
+ break
+ }
+ }
+ }
+ })
+ }
+ })
+
+ if (shouldRecheck) {
+ cleanup.observerThrottle = setTimeout(() => {
+ try {
+ cleanup.observerThrottle = null
+ config.reinitCallback()
+ } catch (error) {
+ console.error(`Error in ${config.platform} observer callback:`, error)
+ }
+ }, config.observerThrottleDelay || UI_CONFIG.OBSERVER_THROTTLE_DELAY)
+ }
+ })
+
+ try {
+ cleanup.observer.observe(document.body, {
+ childList: true,
+ subtree: true,
+ })
+ } catch (error) {
+ console.error(`Failed to set up ${config.platform} route observer:`, error)
+ if (cleanup.urlCheckInterval) {
+ clearInterval(cleanup.urlCheckInterval)
+ }
+ cleanup.urlCheckInterval = setInterval(checkForRouteChange, 1000)
+ }
+}
+
+export function cleanupRouteDetection(cleanup: RouteDetectionCleanup): void {
+ if (cleanup.observer) {
+ cleanup.observer.disconnect()
+ cleanup.observer = null
+ }
+ if (cleanup.urlCheckInterval) {
+ clearInterval(cleanup.urlCheckInterval)
+ cleanup.urlCheckInterval = null
+ }
+ if (cleanup.observerThrottle) {
+ clearTimeout(cleanup.observerThrottle)
+ cleanup.observerThrottle = null
+ }
+}
diff --git a/apps/browser-extension/utils/types.ts b/apps/browser-extension/utils/types.ts
index d20f899e..06a4ae72 100644
--- a/apps/browser-extension/utils/types.ts
+++ b/apps/browser-extension/utils/types.ts
@@ -24,7 +24,8 @@ export interface ExtensionMessage {
* Memory data structure for saving content
*/
export interface MemoryData {
- html: string
+ html?: string
+ content?: string
highlightedText?: string
url?: string
}
diff --git a/apps/browser-extension/utils/ui-components.ts b/apps/browser-extension/utils/ui-components.ts
index 29388656..8a56ea5a 100644
--- a/apps/browser-extension/utils/ui-components.ts
+++ b/apps/browser-extension/utils/ui-components.ts
@@ -242,7 +242,7 @@ export function createChatGPTInputBarElement(onClick: () => void): HTMLElement {
display: inline-flex;
align-items: center;
justify-content: center;
- width: 24px;
+ width: auto;
height: 24px;
cursor: pointer;
transition: opacity 0.2s ease;
@@ -284,13 +284,12 @@ export function createClaudeInputBarElement(onClick: () => void): HTMLElement {
display: inline-flex;
align-items: center;
justify-content: center;
- width: 32px;
+ width: auto;
height: 32px;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 6px;
background: transparent;
- border: 1px solid rgba(0, 0, 0, 0.1);
`
const iconFileName = "/icon-16.png"
@@ -329,13 +328,12 @@ export function createT3InputBarElement(onClick: () => void): HTMLElement {
display: inline-flex;
align-items: center;
justify-content: center;
- width: 32px;
+ width: auto;
height: 32px;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 6px;
background: transparent;
- border: 1px solid rgba(0, 0, 0, 0.1);
`
const iconFileName = "/icon-16.png"
@@ -421,7 +419,6 @@ export const DOMUtils = {
const text = existingToast.querySelector("span")
if (icon && text) {
- // Update based on new state
if (state === "success") {
const iconUrl = browser.runtime.getURL("/icon-16.png")
icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />`
diff --git a/apps/browser-extension/wxt.config.ts b/apps/browser-extension/wxt.config.ts
index 61b83517..1bd7910d 100644
--- a/apps/browser-extension/wxt.config.ts
+++ b/apps/browser-extension/wxt.config.ts
@@ -11,7 +11,7 @@ export default defineConfig({
manifest: {
name: "supermemory",
homepage_url: "https://supermemory.ai",
- version: "6.0.001",
+ version: "6.0.002",
permissions: ["contextMenus", "storage", "activeTab", "webRequest", "tabs"],
host_permissions: [
"*://x.com/*",
@@ -30,6 +30,6 @@ export default defineConfig({
],
},
webExt: {
- disabled: true,
+ chromiumArgs: ["--user-data-dir=./.wxt/chrome-data"],
},
})