@@ -278,11 +343,16 @@ async function saveMemoriesToSupermemory() {
action: MESSAGE_TYPES.SAVE_MEMORY,
data: {
html: combinedContent,
+ sourcePlatform: "chatgpt",
+ sourceSurface: "memories_dialog",
+ url: window.location.href,
},
actionSource: "chatgpt_memories_dialog",
})
- console.log({ response })
+ debugChatGPT("memory dialog saved", {
+ success: response.success,
+ })
if (response.success) {
DOMUtils.showToast("success")
@@ -300,272 +370,242 @@ function updateChatGPTIconFeedback(
iconElement: HTMLElement,
resetAfter = 0,
) {
- if (!iconElement.dataset.originalHtml) {
- iconElement.dataset.originalHtml = iconElement.innerHTML
+ const memories = iconElement.dataset.memoriesData
+ const fallbackReset =
+ resetAfter || (message === "Included Memories" ? 0 : 2200)
+
+ if (message === "Included Memories" || message === "Memories found") {
+ setMemoryMarkerStatus(iconElement, "found")
+ showMarkerPopover(iconElement, "Included Memories", memories)
+ return
}
- 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 = `
-
✓
-
${message}
- `
-
- 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 = `
-
Included Memories
- `
-
- 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 = `
`
- 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 = `\n\nSupermemories of user (only for the reference): ${updatedMemories}`
- }
-
- 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)
+ if (message.toLowerCase().includes("searching")) {
+ setMemoryMarkerStatus(iconElement, "searching")
+ showMarkerPopover(iconElement, message)
+ return
}
- iconElement.innerHTML = ""
- iconElement.appendChild(feedbackDiv)
-
- if (resetAfter > 0) {
- setTimeout(() => {
- iconElement.innerHTML = iconElement.dataset.originalHtml || ""
- delete iconElement.dataset.originalHtml
- }, resetAfter)
- }
+ setMemoryMarkerStatus(
+ iconElement,
+ message.toLowerCase().includes("error") ? "error" : "none",
+ )
+ showMarkerPopover(iconElement, message, undefined, fallbackReset)
}
function addSaveChatGPTElementBeforeComposerBtn() {
- const composerButtons = document.querySelectorAll("button.composer-btn")
+ const promptInput = getChatGPTPromptInput()
+ if (!promptInput) {
+ debugChatGPT("prompt input not found", getChatGPTDomSnapshot())
+ return
+ }
- composerButtons.forEach((button) => {
- if (button.hasAttribute("data-supermemory-icon-added-before")) {
- return
+ const composer = findChatGPTComposerRoot(promptInput)
+ if (!composer?.querySelector) {
+ debugChatGPT("composer root not found", describeElement(promptInput))
+ return
+ }
+
+ const existingMarkers = Array.from(
+ document.querySelectorAll(
+ `[id*="${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer"]`,
+ ),
+ )
+ if (existingMarkers.length > 1) {
+ debugChatGPT("removed duplicate markers", existingMarkers.length)
+ for (const marker of existingMarkers) {
+ marker.remove()
}
+ } else if (existingMarkers.length === 1) {
+ debugChatGPT("marker already exists")
+ 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()
+ const buttons = findChatGPTComposerButtons(promptInput, composer)
+ debugChatGPT("candidate ChatGPT buttons", {
+ input: describeElement(promptInput),
+ composer: describeElement(composer),
+ buttons: buttons.map((button) => ({
+ label: buttonLabel(button),
+ element: describeElement(button),
+ })),
})
+
+ const micButton = buttons.find((button) => isChatGPTMicButton(button))
+ const voiceButton = buttons.find((button) => isChatGPTVoiceButton(button))
+ const sendButton = buttons.find((button) => isChatGPTSendButton(button))
+ const anchorButton =
+ micButton || voiceButton || sendButton || buttons[buttons.length - 1]
+ const anchorSlot = findChatGPTButtonSlot(anchorButton, composer)
+ const speechContainer = composer.querySelector(
+ '[data-testid="composer-speech-button-container"]',
+ ) as HTMLElement | null
+ const targetContainer =
+ anchorSlot?.parentElement ||
+ speechContainer?.parentElement ||
+ promptInput.parentElement
+
+ if (!targetContainer) {
+ debugChatGPT("could not find insertion target", {
+ anchor: anchorButton ? describeElement(anchorButton) : null,
+ input: describeElement(promptInput),
+ })
+ 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)}`
+
+ if (anchorSlot?.parentElement === targetContainer) {
+ targetContainer.insertBefore(saveChatGPTElement, anchorSlot)
+ debugChatGPT("inserted marker before anchor button", {
+ anchorLabel: anchorButton ? buttonLabel(anchorButton) : null,
+ anchorSlot: describeElement(anchorSlot),
+ target: describeElement(targetContainer),
+ })
+ } else {
+ targetContainer.appendChild(saveChatGPTElement)
+ debugChatGPT("inserted marker into fallback target", {
+ target: describeElement(targetContainer),
+ })
+ }
+
+ setupChatGPTAutoFetch()
+}
+
+function getChatGPTPromptInput(): HTMLElement | null {
+ return document.querySelector(
+ '#prompt-textarea, [data-testid="prompt-textarea"], div[contenteditable="true"]',
+ ) as HTMLElement | null
+}
+
+function findChatGPTComposerRoot(input: HTMLElement): HTMLElement {
+ const form = input.closest("form") as HTMLElement | null
+ if (form) return form
+
+ let current: HTMLElement | null = input
+ for (let depth = 0; current && depth < 8; depth += 1) {
+ if (current.querySelectorAll("button").length >= 2) {
+ return current
+ }
+ current = current.parentElement
+ }
+
+ return input.parentElement || document.body
+}
+
+function findChatGPTComposerButtons(
+ input: HTMLElement,
+ composer: HTMLElement,
+): HTMLButtonElement[] {
+ const composerButtons = Array.from(composer.querySelectorAll("button"))
+ if (composerButtons.length > 0) {
+ return composerButtons
+ }
+
+ const inputRect = input.getBoundingClientRect()
+ const allButtons = Array.from(document.querySelectorAll("button"))
+
+ return allButtons.filter((button) => {
+ const rect = button.getBoundingClientRect()
+ const verticallyNear =
+ Math.abs(
+ rect.top + rect.height / 2 - (inputRect.top + inputRect.height / 2),
+ ) < 120
+ const horizontallyNear =
+ rect.left > inputRect.left - 80 && rect.left < inputRect.right + 260
+
+ return verticallyNear && horizontallyNear
+ })
+}
+
+function buttonLabel(button: HTMLButtonElement): string {
+ return [
+ button.id,
+ button.getAttribute("aria-label"),
+ button.getAttribute("title"),
+ button.getAttribute("data-testid"),
+ button.getAttribute("data-test-id"),
+ button.textContent,
+ ]
+ .filter(Boolean)
+ .join(" ")
+}
+
+function isChatGPTMicButton(button: HTMLButtonElement): boolean {
+ return /mic|microphone|dictate/i.test(buttonLabel(button))
+}
+
+function isChatGPTVoiceButton(button: HTMLButtonElement): boolean {
+ return /voice|audio|speech/i.test(buttonLabel(button))
+}
+
+function isChatGPTSendButton(button: HTMLButtonElement): boolean {
+ const label = buttonLabel(button)
+ return /composer-submit-button|send|submit/i.test(label)
+}
+
+function findChatGPTButtonSlot(
+ button: HTMLButtonElement | undefined,
+ composer: HTMLElement,
+): HTMLElement | null {
+ if (!button) return null
+
+ let current: HTMLElement | null = button
+ while (current?.parentElement && current.parentElement !== composer) {
+ const parent: HTMLElement = current.parentElement
+ const parentStyle = window.getComputedStyle(parent)
+ const hasSiblingControls = parent.children.length > 1
+ const isRow =
+ parentStyle.display.includes("flex") &&
+ parentStyle.flexDirection !== "column"
+
+ if (hasSiblingControls && isRow) {
+ return current
+ }
+
+ current = parent
+ }
+
+ return current || button
+}
+
+function describeElement(element: Element | null): string | null {
+ if (!element) return null
+
+ const parts = [element.tagName.toLowerCase()]
+ if (element.id) parts.push(`#${element.id}`)
+ if (element.className && typeof element.className === "string") {
+ parts.push(
+ `.${element.className.trim().split(/\s+/).slice(0, 4).join(".")}`,
+ )
+ }
+
+ for (const attr of ["aria-label", "data-testid", "data-test-id", "role"]) {
+ const value = element.getAttribute(attr)
+ if (value) parts.push(`[${attr}="${value}"]`)
+ }
+
+ return parts.join("")
+}
+
+function getChatGPTDomSnapshot() {
+ return {
+ promptTextareas: document.querySelectorAll("#prompt-textarea").length,
+ contenteditables: document.querySelectorAll('[contenteditable="true"]')
+ .length,
+ textareas: document.querySelectorAll("textarea").length,
+ buttons: document.querySelectorAll("button").length,
+ composerButtons: document.querySelectorAll("button.composer-btn").length,
+ speechContainers: document.querySelectorAll(
+ '[data-testid="composer-speech-button-container"]',
+ ).length,
+ }
}
async function setupChatGPTAutoFetch() {
@@ -586,12 +626,29 @@ async function setupChatGPTAutoFetch() {
promptTextarea.setAttribute("data-supermemory-auto-fetch", "true")
const handleInput = () => {
+ const content = promptTextarea.textContent?.trim() || ""
+ syncAcceptedSupermemoryState(promptTextarea)
+
+ if (content.length === 0) {
+ clearMemorySuggestion("chatgpt", promptTextarea)
+ document
+ .querySelectorAll(
+ '[id*="sm-chatgpt-input-bar-element-before-composer"]',
+ )
+ .forEach((icon) => {
+ setMemoryMarkerStatus(icon as HTMLElement, "neutral")
+ })
+ }
+
if (chatGPTDebounceTimeout) {
clearTimeout(chatGPTDebounceTimeout)
}
chatGPTDebounceTimeout = setTimeout(async () => {
- const content = promptTextarea.textContent?.trim() || ""
+ if (hasAcceptedSupermemoryContext(promptTextarea)) {
+ clearMemorySuggestion("chatgpt", promptTextarea)
+ return
+ }
if (content.length > 2) {
await getRelatedMemoriesForChatGPT(
@@ -604,6 +661,7 @@ async function setupChatGPTAutoFetch() {
icons.forEach((icon) => {
const iconElement = icon as HTMLElement
+ setMemoryMarkerStatus(iconElement, "neutral")
if (iconElement.dataset.originalHtml) {
iconElement.innerHTML = iconElement.dataset.originalHtml
delete iconElement.dataset.originalHtml
@@ -612,7 +670,7 @@ async function setupChatGPTAutoFetch() {
})
if (promptTextarea.dataset.supermemories) {
- delete promptTextarea.dataset.supermemories
+ clearMemorySuggestion("chatgpt", promptTextarea)
}
}
}, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)
@@ -631,7 +689,7 @@ function setupChatGPTPromptCapture() {
const autoCapture = (await autoCapturePromptsEnabled.getValue()) ?? false
if (!autoCapture) {
- console.log("Auto capture prompts is disabled, skipping prompt capture")
+ debugChatGPT("auto prompt capture disabled")
return
}
const promptTextarea = document.getElementById("prompt-textarea")
@@ -641,26 +699,18 @@ function setupChatGPTPromptCapture() {
promptContent = promptTextarea.textContent || ""
}
- const storedMemories = promptTextarea?.dataset.supermemories
- if (
- storedMemories &&
- promptTextarea &&
- !promptContent.includes("Supermemories of user")
- ) {
- promptTextarea.appendChild(document.createTextNode(storedMemories))
- promptContent = promptTextarea.textContent || ""
- }
-
if (promptTextarea && promptContent.trim()) {
- console.log(`ChatGPT prompt submitted via ${source}:`, promptContent)
-
+ debugChatGPT("prompt submitted", {
+ source,
+ promptLength: promptContent.length,
+ })
try {
await browser.runtime.sendMessage({
action: MESSAGE_TYPES.CAPTURE_PROMPT,
data: {
prompt: promptContent,
platform: "chatgpt",
- source: source,
+ source: window.location.href,
},
})
} catch (error) {
@@ -682,7 +732,7 @@ function setupChatGPTPromptCapture() {
})
if (promptTextarea?.dataset.supermemories) {
- delete promptTextarea.dataset.supermemories
+ clearMemorySuggestion("chatgpt", promptTextarea)
}
}
@@ -705,6 +755,18 @@ function setupChatGPTPromptCapture() {
async (event) => {
const target = event.target as HTMLElement
+ if (
+ (target.id === "prompt-textarea" ||
+ target.closest("#prompt-textarea")) &&
+ acceptMemorySuggestion(
+ event,
+ "chatgpt",
+ document.getElementById("prompt-textarea"),
+ )
+ ) {
+ return
+ }
+
if (
target.id === "prompt-textarea" &&
event.key === "Enter" &&
diff --git a/apps/browser-extension/entrypoints/content/claude.ts b/apps/browser-extension/entrypoints/content/claude.ts
index 4fe41cbd..7bff4dfc 100644
--- a/apps/browser-extension/entrypoints/content/claude.ts
+++ b/apps/browser-extension/entrypoints/content/claude.ts
@@ -13,18 +13,37 @@ import {
createClaudeInputBarElement,
DOMUtils,
} from "../../utils/ui-components"
+import {
+ acceptMemorySuggestion,
+ clearMemorySuggestion,
+ hasAcceptedSupermemoryContext,
+ setMemoryMarkerStatus,
+ showLoadingSuggestion,
+ showMarkerPopover,
+ showMemorySuggestion,
+ syncAcceptedSupermemoryState,
+} from "./memory-suggestion"
let claudeDebounceTimeout: NodeJS.Timeout | null = null
let claudeRouteObserver: MutationObserver | null = null
let claudeUrlCheckInterval: NodeJS.Timeout | null = null
let claudeObserverThrottle: NodeJS.Timeout | null = null
+const CLAUDE_DEBUG = false
+const CLAUDE_LOG_PREFIX = "[supermemory:claude]"
export function initializeClaude() {
+ debugClaude("initializeClaude called", {
+ host: window.location.hostname,
+ href: window.location.href,
+ })
+
if (!DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
+ debugClaude("not on Claude domain, skipping")
return
}
if (document.body.hasAttribute("data-claude-initialized")) {
+ debugClaude("already initialized")
return
}
@@ -39,6 +58,18 @@ export function initializeClaude() {
setupClaudeRouteChangeDetection()
document.body.setAttribute("data-claude-initialized", "true")
+ debugClaude("initialized listeners")
+}
+
+function debugClaude(message: string, data?: unknown) {
+ if (!CLAUDE_DEBUG) return
+
+ if (data === undefined) {
+ console.log(CLAUDE_LOG_PREFIX, message)
+ return
+ }
+
+ console.log(CLAUDE_LOG_PREFIX, message, data)
}
function setupClaudeRouteChangeDetection() {
@@ -58,7 +89,7 @@ function setupClaudeRouteChangeDetection() {
const checkForRouteChange = () => {
if (window.location.href !== currentUrl) {
currentUrl = window.location.href
- console.log("Claude route changed, re-adding supermemory icon")
+ debugClaude("route changed, re-adding supermemory icon", currentUrl)
setTimeout(() => {
addSupermemoryButtonToClaudeMemoryDialog()
addSupermemoryIconToClaudeInput()
@@ -84,9 +115,11 @@ function setupClaudeRouteChangeDetection() {
element.querySelector?.('[role="dialog"]') ||
element.querySelector?.('div[contenteditable="true"]') ||
element.querySelector?.("textarea") ||
+ element.querySelector?.("button") ||
element.matches?.('[role="dialog"]') ||
element.matches?.('div[contenteditable="true"]') ||
element.matches?.("textarea") ||
+ element.matches?.("button") ||
element.textContent?.includes("Manage memory")
) {
shouldRecheck = true
@@ -100,6 +133,7 @@ function setupClaudeRouteChangeDetection() {
claudeObserverThrottle = setTimeout(() => {
try {
claudeObserverThrottle = null
+ debugClaude("DOM changed near composer, rechecking UI")
addSupermemoryButtonToClaudeMemoryDialog()
addSupermemoryIconToClaudeInput()
setupClaudeAutoFetch()
@@ -125,39 +159,207 @@ function setupClaudeRouteChangeDetection() {
}
function addSupermemoryIconToClaudeInput() {
- const targetContainers = document.querySelectorAll(
- ".relative.flex-1.flex.items-center.gap-2.shrink.min-w-0",
+ const input = getClaudePromptInput()
+ if (!input) {
+ debugClaude("prompt input not found", getClaudeDomSnapshot())
+ return
+ }
+
+ const composer = findComposerRoot(input)
+ if (!composer?.querySelector) {
+ debugClaude("composer root not found", describeElement(input))
+ return
+ }
+
+ const existingMarkers = Array.from(
+ document.querySelectorAll(
+ `[id*="${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}"]`,
+ ),
)
-
- targetContainers.forEach((container) => {
- if (container.hasAttribute("data-supermemory-icon-added")) {
- return
+ if (existingMarkers.length > 1) {
+ debugClaude("removed duplicate markers", existingMarkers.length)
+ for (const marker of existingMarkers) {
+ marker.remove()
}
+ } else if (existingMarkers.length === 1) {
+ debugClaude("marker already exists")
+ 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)
+ const buttons = findClaudeComposerButtons(input, composer)
+ debugClaude("candidate Claude buttons", {
+ input: describeElement(input),
+ composer: describeElement(composer),
+ buttons: buttons.map((button) => ({
+ label: buttonLabel(button),
+ element: describeElement(button),
+ })),
})
+
+ const micButton = buttons.find((button) => isClaudeMicButton(button))
+ const voiceButton = buttons.find((button) => isClaudeVoiceButton(button))
+ const sendButton = buttons.find((button) => isClaudeSendButton(button))
+ const anchorButton =
+ micButton || voiceButton || sendButton || buttons[buttons.length - 1]
+ const anchorSlot = findClaudeButtonSlot(anchorButton, composer)
+ const targetContainer = anchorSlot?.parentElement || input.parentElement
+
+ if (!targetContainer) {
+ debugClaude("could not find insertion target", {
+ anchor: anchorButton ? describeElement(anchorButton) : null,
+ input: describeElement(input),
+ })
+ 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)}`
+
+ if (anchorSlot?.parentElement === targetContainer) {
+ targetContainer.insertBefore(supermemoryIcon, anchorSlot)
+ debugClaude("inserted marker before anchor button", {
+ anchorLabel: anchorButton ? buttonLabel(anchorButton) : null,
+ anchorSlot: describeElement(anchorSlot),
+ target: describeElement(targetContainer),
+ })
+ return
+ }
+
+ targetContainer.appendChild(supermemoryIcon)
+ debugClaude("inserted marker into fallback target", {
+ target: describeElement(targetContainer),
+ })
+}
+
+function getClaudePromptInput(): HTMLElement | null {
+ return document.querySelector(
+ '.ProseMirror[contenteditable="true"], div[contenteditable="true"], textarea',
+ ) as HTMLElement | null
+}
+
+function findComposerRoot(input: HTMLElement): HTMLElement {
+ return (
+ (input.closest("form") as HTMLElement | null) ||
+ (input.closest('[data-testid*="composer"]') as HTMLElement | null) ||
+ (input.closest('[class*="composer"]') as HTMLElement | null) ||
+ (input.closest(".relative") as HTMLElement | null) ||
+ input.parentElement ||
+ document.body
+ )
+}
+
+function buttonLabel(button: HTMLButtonElement): string {
+ return [
+ button.getAttribute("aria-label"),
+ button.getAttribute("title"),
+ button.getAttribute("data-testid"),
+ button.getAttribute("data-test-id"),
+ button.textContent,
+ ]
+ .filter(Boolean)
+ .join(" ")
+}
+
+function findClaudeComposerButtons(
+ input: HTMLElement,
+ composer: HTMLElement,
+): HTMLButtonElement[] {
+ const composerButtons = Array.from(composer.querySelectorAll("button"))
+ if (composerButtons.length > 0) {
+ return composerButtons
+ }
+
+ const inputRect = input.getBoundingClientRect()
+ const allButtons = Array.from(document.querySelectorAll("button"))
+
+ return allButtons.filter((button) => {
+ const rect = button.getBoundingClientRect()
+ const verticallyNear =
+ Math.abs(
+ rect.top + rect.height / 2 - (inputRect.top + inputRect.height / 2),
+ ) < 120
+ const horizontallyNear =
+ rect.left > inputRect.left - 80 && rect.left < inputRect.right + 260
+
+ return verticallyNear && horizontallyNear
+ })
+}
+
+function isClaudeMicButton(button: HTMLButtonElement): boolean {
+ return /mic|microphone|dictate/i.test(buttonLabel(button))
+}
+
+function isClaudeVoiceButton(button: HTMLButtonElement): boolean {
+ return /voice|audio|speech/i.test(buttonLabel(button))
+}
+
+function isClaudeSendButton(button: HTMLButtonElement): boolean {
+ return /send|submit/i.test(buttonLabel(button))
+}
+
+function findClaudeButtonSlot(
+ button: HTMLButtonElement | undefined,
+ composer: HTMLElement,
+): HTMLElement | null {
+ if (!button) return null
+
+ let current: HTMLElement | null = button
+ while (current?.parentElement && current.parentElement !== composer) {
+ const parent: HTMLElement = current.parentElement
+ const parentStyle = window.getComputedStyle(parent)
+ const hasSiblingControls = parent.children.length > 1
+ const isRow =
+ parentStyle.display.includes("flex") &&
+ parentStyle.flexDirection !== "column"
+
+ if (hasSiblingControls && isRow) {
+ return current
+ }
+
+ current = parent
+ }
+
+ return current || button
+}
+
+function describeElement(element: Element | null): string | null {
+ if (!element) return null
+
+ const parts = [element.tagName.toLowerCase()]
+ if (element.id) parts.push(`#${element.id}`)
+ if (element.className && typeof element.className === "string") {
+ parts.push(
+ `.${element.className.trim().split(/\s+/).slice(0, 4).join(".")}`,
+ )
+ }
+
+ for (const attr of ["aria-label", "data-testid", "data-test-id", "role"]) {
+ const value = element.getAttribute(attr)
+ if (value) parts.push(`[${attr}="${value}"]`)
+ }
+
+ return parts.join("")
+}
+
+function getClaudeDomSnapshot() {
+ return {
+ proseMirrors: document.querySelectorAll(".ProseMirror").length,
+ contenteditables: document.querySelectorAll('[contenteditable="true"]')
+ .length,
+ textareas: document.querySelectorAll("textarea").length,
+ buttons: document.querySelectorAll("button").length,
+ }
}
async function getRelatedMemoriesForClaude(actionSource: string) {
try {
+ const isAutoSearch =
+ actionSource === POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED
let userQuery = ""
const supermemoryContainer = document.querySelector(
@@ -194,10 +396,12 @@ async function getRelatedMemoriesForClaude(actionSource: string) {
}
}
- console.log("Claude query extracted:", userQuery)
+ debugClaude("query extracted", {
+ queryLength: userQuery.length,
+ })
if (!userQuery.trim()) {
- console.log("No query text found for Claude")
+ debugClaude("memory search skipped because query is empty")
return
}
@@ -210,7 +414,15 @@ async function getRelatedMemoriesForClaude(actionSource: string) {
return
}
- updateClaudeIconFeedback("Searching memories...", iconElement)
+ if (isAutoSearch) {
+ const input = getClaudePromptInput()
+ if (input) {
+ showLoadingSuggestion("claude", input)
+ }
+ setMemoryMarkerStatus(iconElement, "searching")
+ } else {
+ updateClaudeIconFeedback("Searching memories...", iconElement)
+ }
const timeoutPromise = new Promise((_, reject) =>
setTimeout(
@@ -228,7 +440,9 @@ async function getRelatedMemoriesForClaude(actionSource: string) {
timeoutPromise,
])
- console.log("Claude memories response:", response)
+ debugClaude("memory search response", {
+ success: response?.success,
+ })
if (response?.success && response?.data) {
const textareaElement = document.querySelector(
@@ -236,24 +450,39 @@ async function getRelatedMemoriesForClaude(actionSource: string) {
) as HTMLElement
if (textareaElement) {
- textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${response.data}`
- console.log(
- "Text element dataset:",
- textareaElement.dataset.supermemories,
+ const memoryText = showMemorySuggestion(
+ "claude",
+ textareaElement,
+ response.data,
)
+ debugClaude("memory suggestion rendered", {
+ memoryLength: memoryText.length,
+ })
- iconElement.dataset.memoriesData = response.data
+ iconElement.dataset.memoriesData = String(response.data)
- updateClaudeIconFeedback("Included Memories", iconElement)
+ if (isAutoSearch) {
+ setMemoryMarkerStatus(iconElement, "found")
+ } else {
+ updateClaudeIconFeedback("Included Memories", iconElement)
+ }
} else {
console.warn(
"Claude input area not found after successful memory fetch",
)
- updateClaudeIconFeedback("Memories found", iconElement)
+ if (isAutoSearch) {
+ setMemoryMarkerStatus(iconElement, "found")
+ } else {
+ updateClaudeIconFeedback("Memories found", iconElement)
+ }
}
} else {
console.warn("No memories found or API response invalid for Claude")
- updateClaudeIconFeedback("No memories found", iconElement)
+ if (isAutoSearch) {
+ setMemoryMarkerStatus(iconElement, "none")
+ } else {
+ updateClaudeIconFeedback("No memories found", iconElement)
+ }
}
} catch (error) {
console.error("Error getting related memories for Claude:", error)
@@ -262,7 +491,13 @@ async function getRelatedMemoriesForClaude(actionSource: string) {
'[id*="sm-claude-input-bar-element"]',
) as HTMLElement
if (icon) {
- updateClaudeIconFeedback("Error fetching memories", icon)
+ if (
+ actionSource === POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED
+ ) {
+ setMemoryMarkerStatus(icon, "error")
+ } else {
+ updateClaudeIconFeedback("Error fetching memories", icon)
+ }
}
} catch (feedbackError) {
console.error("Failed to update Claude error feedback:", feedbackError)
@@ -441,7 +676,9 @@ async function saveClaudeMemoriesToSupermemory(memoryDialog: HTMLElement) {
actionSource: "claude_memories_dialog",
})
- console.log({ response })
+ debugClaude("memory dialog saved", {
+ success: response.success,
+ })
if (response.success) {
DOMUtils.showToast("success")
@@ -459,220 +696,27 @@ function updateClaudeIconFeedback(
iconElement: HTMLElement,
resetAfter = 0,
) {
- if (!iconElement.dataset.originalHtml) {
- iconElement.dataset.originalHtml = iconElement.innerHTML
+ const memories = iconElement.dataset.memoriesData
+ const fallbackReset =
+ resetAfter || (message === "Included Memories" ? 0 : 2200)
+
+ if (message === "Included Memories" || message === "Memories found") {
+ setMemoryMarkerStatus(iconElement, "found")
+ showMarkerPopover(iconElement, "Included Memories", memories)
+ return
}
- 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 = `
-
✓
-
${message}
- `
-
- 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 = `
-
Included Memories
- `
-
- 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 = `
`
- 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 = `\n\nSupermemories of user (only for the reference): ${updatedMemories}`
- }
-
- 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)
+ if (message.toLowerCase().includes("searching")) {
+ setMemoryMarkerStatus(iconElement, "searching")
+ showMarkerPopover(iconElement, message)
+ return
}
- iconElement.innerHTML = ""
- iconElement.appendChild(feedbackDiv)
-
- if (resetAfter > 0) {
- setTimeout(() => {
- iconElement.innerHTML = iconElement.dataset.originalHtml || ""
- delete iconElement.dataset.originalHtml
- }, resetAfter)
- }
+ setMemoryMarkerStatus(
+ iconElement,
+ message.toLowerCase().includes("error") ? "error" : "none",
+ )
+ showMarkerPopover(iconElement, message, undefined, fallbackReset)
}
function setupClaudePromptCapture() {
@@ -684,7 +728,7 @@ function setupClaudePromptCapture() {
const autoCapture = (await autoCapturePromptsEnabled.getValue()) ?? false
if (!autoCapture) {
- console.log("Auto capture prompts is disabled, skipping prompt capture")
+ debugClaude("auto prompt capture disabled")
return
}
let promptContent = ""
@@ -704,19 +748,11 @@ function setupClaudePromptCapture() {
}
}
- const storedMemories = contentEditableDiv?.dataset.supermemories
- if (
- storedMemories &&
- contentEditableDiv &&
- !promptContent.includes("Supermemories of user")
- ) {
- contentEditableDiv.appendChild(document.createTextNode(storedMemories))
- promptContent =
- contentEditableDiv.textContent || contentEditableDiv.innerText || ""
- }
-
if (promptContent.trim()) {
- console.log(`Claude prompt submitted via ${source}:`, promptContent)
+ debugClaude("prompt submitted", {
+ source,
+ promptLength: promptContent.length,
+ })
try {
await browser.runtime.sendMessage({
@@ -724,7 +760,7 @@ function setupClaudePromptCapture() {
data: {
prompt: promptContent,
platform: "claude",
- source: source,
+ source: window.location.href,
},
})
} catch (error) {
@@ -746,7 +782,7 @@ function setupClaudePromptCapture() {
})
if (contentEditableDiv?.dataset.supermemories) {
- delete contentEditableDiv.dataset.supermemories
+ clearMemorySuggestion("claude", contentEditableDiv)
}
}
@@ -754,14 +790,16 @@ function setupClaudePromptCapture() {
"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 (target.closest('[data-supermemory-connected-indicator="true"]')) {
+ return
+ }
- if (sendButton) {
+ const sendButton = target.closest("button")
+
+ if (
+ sendButton &&
+ buttonLabel(sendButton as HTMLButtonElement).match(/send|submit/i)
+ ) {
await captureClaudePromptContent("button click")
}
},
@@ -773,10 +811,18 @@ function setupClaudePromptCapture() {
async (event) => {
const target = event.target as HTMLElement
+ const activeInput =
+ (target.closest('div[contenteditable="true"]') as HTMLElement | null) ||
+ (target.matches("textarea") ? (target as HTMLTextAreaElement) : null)
+ if (acceptMemorySuggestion(event, "claude", activeInput)) {
+ return
+ }
+
if (
(target.matches('div[contenteditable="true"]') ||
target.matches(".ProseMirror") ||
target.matches("textarea") ||
+ target.closest('div[contenteditable="true"]') ||
target.closest(".ProseMirror")) &&
event.key === "Enter" &&
!event.shiftKey
@@ -808,12 +854,27 @@ async function setupClaudeAutoFetch() {
textareaElement.setAttribute("data-supermemory-auto-fetch", "true")
const handleInput = () => {
+ const content = textareaElement.textContent?.trim() || ""
+ syncAcceptedSupermemoryState(textareaElement)
+
+ if (content.length === 0) {
+ clearMemorySuggestion("claude", textareaElement)
+ document
+ .querySelectorAll('[id*="sm-claude-input-bar-element"]')
+ .forEach((icon) => {
+ setMemoryMarkerStatus(icon as HTMLElement, "neutral")
+ })
+ }
+
if (claudeDebounceTimeout) {
clearTimeout(claudeDebounceTimeout)
}
claudeDebounceTimeout = setTimeout(async () => {
- const content = textareaElement.textContent?.trim() || ""
+ if (hasAcceptedSupermemoryContext(textareaElement)) {
+ clearMemorySuggestion("claude", textareaElement)
+ return
+ }
if (content.length > 2) {
await getRelatedMemoriesForClaude(
@@ -826,6 +887,7 @@ async function setupClaudeAutoFetch() {
icons.forEach((icon) => {
const iconElement = icon as HTMLElement
+ setMemoryMarkerStatus(iconElement, "neutral")
if (iconElement.dataset.originalHtml) {
iconElement.innerHTML = iconElement.dataset.originalHtml
delete iconElement.dataset.originalHtml
@@ -834,7 +896,7 @@ async function setupClaudeAutoFetch() {
})
if (textareaElement.dataset.supermemories) {
- delete textareaElement.dataset.supermemories
+ clearMemorySuggestion("claude", textareaElement)
}
}
}, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)
diff --git a/apps/browser-extension/entrypoints/content/gemini.ts b/apps/browser-extension/entrypoints/content/gemini.ts
new file mode 100644
index 00000000..6ece78df
--- /dev/null
+++ b/apps/browser-extension/entrypoints/content/gemini.ts
@@ -0,0 +1,661 @@
+import {
+ DOMAINS,
+ ELEMENT_IDS,
+ MESSAGE_TYPES,
+ POSTHOG_EVENT_KEY,
+ UI_CONFIG,
+} from "../../utils/constants"
+import {
+ autoCapturePromptsEnabled,
+ autoSearchEnabled,
+} from "../../utils/storage"
+import {
+ createGeminiInputBarElement,
+ DOMUtils,
+} from "../../utils/ui-components"
+import {
+ acceptMemorySuggestion,
+ clearMemorySuggestion,
+ hasAcceptedSupermemoryContext,
+ setMemoryMarkerStatus,
+ showLoadingSuggestion,
+ showMarkerPopover,
+ showMemorySuggestion,
+ syncAcceptedSupermemoryState,
+} from "./memory-suggestion"
+
+let geminiDebounceTimeout: NodeJS.Timeout | null = null
+let geminiRouteObserver: MutationObserver | null = null
+let geminiUrlCheckInterval: NodeJS.Timeout | null = null
+let geminiObserverThrottle: NodeJS.Timeout | null = null
+const GEMINI_DEBUG = false
+const GEMINI_LOG_PREFIX = "[supermemory:gemini]"
+
+type GeminiInput = HTMLElement | HTMLTextAreaElement
+
+export function initializeGemini() {
+ debugGemini("initializeGemini called", {
+ host: window.location.hostname,
+ href: window.location.href,
+ })
+
+ if (!DOMUtils.isOnDomain(DOMAINS.GEMINI)) {
+ debugGemini("not on Gemini domain, skipping")
+ return
+ }
+
+ if (document.body.hasAttribute("data-gemini-initialized")) {
+ debugGemini("already initialized")
+ return
+ }
+
+ setTimeout(() => {
+ addSupermemoryIconToGeminiInput()
+ setupGeminiAutoFetch()
+ }, 2000)
+
+ setupGeminiPromptCapture()
+ setupGeminiRouteChangeDetection()
+
+ document.body.setAttribute("data-gemini-initialized", "true")
+ debugGemini("initialized listeners")
+}
+
+function debugGemini(message: string, data?: unknown) {
+ if (!GEMINI_DEBUG) return
+
+ if (data === undefined) {
+ console.log(GEMINI_LOG_PREFIX, message)
+ return
+ }
+
+ console.log(GEMINI_LOG_PREFIX, message, data)
+}
+
+function setupGeminiRouteChangeDetection() {
+ if (geminiRouteObserver) {
+ geminiRouteObserver.disconnect()
+ }
+ if (geminiUrlCheckInterval) {
+ clearInterval(geminiUrlCheckInterval)
+ }
+ if (geminiObserverThrottle) {
+ clearTimeout(geminiObserverThrottle)
+ geminiObserverThrottle = null
+ }
+
+ let currentUrl = window.location.href
+
+ const recheckGeminiUI = () => {
+ addSupermemoryIconToGeminiInput()
+ setupGeminiAutoFetch()
+ }
+
+ const checkForRouteChange = () => {
+ if (window.location.href !== currentUrl) {
+ currentUrl = window.location.href
+ debugGemini("route changed, rechecking UI", currentUrl)
+ setTimeout(recheckGeminiUI, 1000)
+ }
+ }
+
+ geminiUrlCheckInterval = setInterval(checkForRouteChange, 2000)
+
+ geminiRouteObserver = new MutationObserver((mutations) => {
+ if (geminiObserverThrottle) {
+ return
+ }
+
+ const shouldRecheck = mutations.some((mutation) =>
+ Array.from(mutation.addedNodes).some((node) => {
+ if (node.nodeType !== Node.ELEMENT_NODE) {
+ return false
+ }
+
+ const element = node as Element
+ return (
+ element.matches?.("rich-textarea, textarea, button") ||
+ element.matches?.('[contenteditable="true"]') ||
+ !!element.querySelector?.(
+ 'rich-textarea, textarea, button, [contenteditable="true"]',
+ )
+ )
+ }),
+ )
+
+ if (shouldRecheck) {
+ geminiObserverThrottle = setTimeout(() => {
+ geminiObserverThrottle = null
+ debugGemini("DOM changed near Gemini composer, rechecking UI")
+ recheckGeminiUI()
+ }, 300)
+ }
+ })
+
+ try {
+ geminiRouteObserver.observe(document.body, {
+ childList: true,
+ subtree: true,
+ })
+ } catch (error) {
+ console.error("Failed to set up Gemini route observer:", error)
+ if (geminiUrlCheckInterval) {
+ clearInterval(geminiUrlCheckInterval)
+ }
+ geminiUrlCheckInterval = setInterval(checkForRouteChange, 1000)
+ }
+}
+
+function addSupermemoryIconToGeminiInput() {
+ const input = getGeminiPromptInput()
+ if (!input) {
+ debugGemini("prompt input not found", getGeminiDomSnapshot())
+ return
+ }
+
+ const composer = findGeminiComposerRoot(input)
+ if (!composer?.querySelector) {
+ debugGemini("composer root not found", describeElement(input))
+ return
+ }
+
+ const existingMarkers = Array.from(
+ document.querySelectorAll(
+ `[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`,
+ ),
+ )
+ if (existingMarkers.length > 1) {
+ debugGemini("removed duplicate markers", existingMarkers.length)
+ for (const marker of existingMarkers) {
+ marker.remove()
+ }
+ } else if (existingMarkers.length === 1) {
+ debugGemini("marker already exists")
+ return
+ }
+
+ const buttons = findGeminiComposerButtons(input, composer)
+ debugGemini("candidate Gemini buttons", {
+ input: describeElement(input),
+ composer: describeElement(composer),
+ buttons: buttons.map((button) => ({
+ label: buttonLabel(button),
+ element: describeElement(button),
+ })),
+ })
+
+ const micButton = buttons.find((button) => isGeminiMicButton(button))
+ const sendButton = buttons.find((button) => isGeminiSendButton(button))
+ const anchorButton = micButton || sendButton || buttons[buttons.length - 1]
+ const anchorSlot = findGeminiButtonSlot(anchorButton, composer)
+ const targetContainer =
+ anchorSlot?.parentElement ||
+ (input.closest("rich-textarea") as HTMLElement | null)?.parentElement ||
+ input.parentElement
+
+ if (!targetContainer) {
+ debugGemini("could not find insertion target", {
+ anchor: anchorButton ? describeElement(anchorButton) : null,
+ input: describeElement(input),
+ })
+ return
+ }
+
+ const supermemoryIcon = createGeminiInputBarElement(async () => {
+ await getRelatedMemoriesForGemini(
+ POSTHOG_EVENT_KEY.GEMINI_CHAT_MEMORIES_SEARCHED,
+ )
+ })
+
+ supermemoryIcon.id = `${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
+
+ if (anchorSlot?.parentElement === targetContainer) {
+ targetContainer.insertBefore(supermemoryIcon, anchorSlot)
+ debugGemini("inserted marker before anchor button", {
+ anchorLabel: anchorButton ? buttonLabel(anchorButton) : null,
+ anchorSlot: describeElement(anchorSlot),
+ target: describeElement(targetContainer),
+ })
+ return
+ }
+
+ targetContainer.appendChild(supermemoryIcon)
+ debugGemini("inserted marker into fallback target", {
+ target: describeElement(targetContainer),
+ })
+}
+
+function getGeminiPromptInput(): GeminiInput | null {
+ return document.querySelector(
+ 'rich-textarea .ql-editor[contenteditable="true"], rich-textarea [contenteditable="true"], .ql-editor[contenteditable="true"], div[contenteditable="true"], textarea',
+ ) as GeminiInput | null
+}
+
+function findGeminiComposerRoot(input: GeminiInput): HTMLElement {
+ const form = input.closest("form") as HTMLElement | null
+ if (form) return form
+
+ let current: HTMLElement | null = input
+ for (let depth = 0; current && depth < 8; depth += 1) {
+ if (current.querySelectorAll("button").length >= 2) {
+ return current
+ }
+ current = current.parentElement
+ }
+
+ return input.parentElement || document.body
+}
+
+function findGeminiComposerButtons(
+ input: GeminiInput,
+ composer: HTMLElement,
+): HTMLButtonElement[] {
+ const composerButtons = Array.from(composer.querySelectorAll("button"))
+ if (composerButtons.length > 0) {
+ return composerButtons
+ }
+
+ const inputRect = input.getBoundingClientRect()
+ const allButtons = Array.from(document.querySelectorAll("button"))
+
+ return allButtons.filter((button) => {
+ const rect = button.getBoundingClientRect()
+ const verticallyNear =
+ Math.abs(
+ rect.top + rect.height / 2 - (inputRect.top + inputRect.height / 2),
+ ) < 120
+ const horizontallyNear =
+ rect.left > inputRect.left - 80 && rect.left < inputRect.right + 240
+
+ return verticallyNear && horizontallyNear
+ })
+}
+
+function buttonLabel(button: HTMLButtonElement): string {
+ return [
+ button.getAttribute("aria-label"),
+ button.getAttribute("title"),
+ button.getAttribute("data-testid"),
+ button.getAttribute("data-test-id"),
+ button.getAttribute("jsname"),
+ button.textContent,
+ ]
+ .filter(Boolean)
+ .join(" ")
+}
+
+function isGeminiMicButton(button: HTMLButtonElement): boolean {
+ return /mic|microphone|voice|dictate|audio/i.test(buttonLabel(button))
+}
+
+function isGeminiSendButton(button: HTMLButtonElement): boolean {
+ const label = buttonLabel(button)
+ if (/send|submit/i.test(label)) {
+ return true
+ }
+
+ return !!button.querySelector(
+ 'mat-icon[fonticon="send"], mat-icon[data-mat-icon-name="send"], [data-icon-name="send"]',
+ )
+}
+
+function findGeminiButtonSlot(
+ button: HTMLButtonElement | undefined,
+ composer: HTMLElement,
+): HTMLElement | null {
+ if (!button) return null
+
+ let current: HTMLElement | null = button
+ while (current?.parentElement && current.parentElement !== composer) {
+ const parent: HTMLElement = current.parentElement
+ const parentStyle = window.getComputedStyle(parent)
+ const hasSiblingControls = parent.children.length > 1
+ const isRow =
+ parentStyle.display.includes("flex") &&
+ parentStyle.flexDirection !== "column"
+
+ if (hasSiblingControls && isRow) {
+ return current
+ }
+
+ current = parent
+ }
+
+ return current || button
+}
+
+function describeElement(element: Element | null): string | null {
+ if (!element) return null
+
+ const parts = [element.tagName.toLowerCase()]
+ if (element.id) parts.push(`#${element.id}`)
+ if (element.className && typeof element.className === "string") {
+ parts.push(
+ `.${element.className.trim().split(/\s+/).slice(0, 4).join(".")}`,
+ )
+ }
+
+ for (const attr of ["aria-label", "data-testid", "data-test-id", "role"]) {
+ const value = element.getAttribute(attr)
+ if (value) parts.push(`[${attr}="${value}"]`)
+ }
+
+ return parts.join("")
+}
+
+function getGeminiDomSnapshot() {
+ return {
+ richTextareas: document.querySelectorAll("rich-textarea").length,
+ qlEditors: document.querySelectorAll(".ql-editor").length,
+ contenteditables: document.querySelectorAll('[contenteditable="true"]')
+ .length,
+ textareas: document.querySelectorAll("textarea").length,
+ buttons: document.querySelectorAll("button").length,
+ }
+}
+
+function getInputText(input: GeminiInput | null): string {
+ if (!input) return ""
+ if (input instanceof HTMLTextAreaElement) {
+ return input.value || ""
+ }
+
+ return input.innerText || input.textContent || ""
+}
+
+async function getRelatedMemoriesForGemini(actionSource: string) {
+ try {
+ const isAutoSearch =
+ actionSource === POSTHOG_EVENT_KEY.GEMINI_CHAT_MEMORIES_AUTO_SEARCHED
+ const input = getGeminiPromptInput()
+ const userQuery = getInputText(input).trim()
+ debugGemini("manual/auto memory search requested", {
+ actionSource,
+ hasInput: !!input,
+ queryLength: userQuery.length,
+ })
+
+ if (!userQuery) {
+ debugGemini("memory search skipped because query is empty")
+ return
+ }
+
+ const iconElement = document.querySelector(
+ `[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`,
+ ) as HTMLElement | null
+
+ if (!iconElement) {
+ console.warn("Gemini icon element not found, cannot update feedback")
+ return
+ }
+
+ if (input && isAutoSearch) {
+ showLoadingSuggestion("gemini", input)
+ }
+ setMemoryMarkerStatus(iconElement, "searching")
+ if (!isAutoSearch) {
+ updateGeminiIconFeedback("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,
+ }),
+ timeoutPromise,
+ ])) as { success?: boolean; data?: string }
+
+ debugGemini("memory search response", response)
+
+ if (response?.success && response?.data && input) {
+ const memoryText = showMemorySuggestion("gemini", input, response.data)
+ iconElement.dataset.memoriesData = String(response.data)
+ iconElement.dataset.supermemories = memoryText
+ if (isAutoSearch) {
+ setMemoryMarkerStatus(iconElement, "found")
+ } else {
+ updateGeminiIconFeedback("Included Memories", iconElement)
+ }
+ return
+ }
+
+ if (isAutoSearch) {
+ setMemoryMarkerStatus(iconElement, "none")
+ } else {
+ updateGeminiIconFeedback("No memories found", iconElement, 1800)
+ }
+ } catch (error) {
+ console.error("Error getting related memories for Gemini:", error)
+ const iconElement = document.querySelector(
+ `[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`,
+ ) as HTMLElement | null
+ if (iconElement) {
+ if (
+ actionSource === POSTHOG_EVENT_KEY.GEMINI_CHAT_MEMORIES_AUTO_SEARCHED
+ ) {
+ setMemoryMarkerStatus(iconElement, "error")
+ } else {
+ updateGeminiIconFeedback("Error fetching memories", iconElement, 1800)
+ }
+ }
+ }
+}
+
+function updateGeminiIconFeedback(
+ message: string,
+ iconElement: HTMLElement,
+ resetAfter = 0,
+) {
+ const memories = iconElement.dataset.memoriesData
+ const fallbackReset =
+ resetAfter || (message === "Included Memories" ? 0 : 2200)
+
+ if (message === "Included Memories" || message === "Memories found") {
+ setMemoryMarkerStatus(iconElement, "found")
+ showMarkerPopover(iconElement, "Included Memories", memories)
+ return
+ }
+
+ if (message.toLowerCase().includes("searching")) {
+ setMemoryMarkerStatus(iconElement, "searching")
+ showMarkerPopover(iconElement, message)
+ return
+ }
+
+ setMemoryMarkerStatus(
+ iconElement,
+ message.toLowerCase().includes("error") ? "error" : "none",
+ )
+ showMarkerPopover(iconElement, message, undefined, fallbackReset)
+}
+
+function setupGeminiPromptCapture() {
+ if (document.body.hasAttribute("data-gemini-prompt-capture-setup")) {
+ return
+ }
+
+ document.body.setAttribute("data-gemini-prompt-capture-setup", "true")
+
+ const captureGeminiPromptContent = async (source: string) => {
+ const autoCapture = (await autoCapturePromptsEnabled.getValue()) ?? false
+ debugGemini("capture requested", { source, autoCapture })
+
+ if (!autoCapture) {
+ debugGemini("auto prompt capture disabled")
+ return
+ }
+
+ const input = getGeminiPromptInput()
+ const promptContent = getInputText(input)
+ debugGemini("capture input state", {
+ hasInput: !!input,
+ promptLength: promptContent.length,
+ hasStoredMemories: !!input?.dataset.supermemories,
+ })
+
+ if (promptContent.trim()) {
+ try {
+ const response = await browser.runtime.sendMessage({
+ action: MESSAGE_TYPES.CAPTURE_PROMPT,
+ data: {
+ prompt: promptContent,
+ platform: "gemini",
+ source: window.location.href,
+ },
+ })
+ debugGemini("capture response", response)
+ } catch (error) {
+ console.error("Error sending Gemini prompt to background:", error)
+ }
+ } else {
+ debugGemini("capture skipped because prompt is empty")
+ }
+
+ const icons = document.querySelectorAll(
+ `[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`,
+ )
+
+ icons.forEach((icon) => {
+ const iconElement = icon as HTMLElement
+ iconElement.querySelector("[data-supermemory-status-badge]")?.remove()
+ delete iconElement.dataset.supermemoryStatus
+ delete iconElement.dataset.memoriesData
+ if (iconElement.dataset.originalHtml) {
+ iconElement.innerHTML = iconElement.dataset.originalHtml
+ delete iconElement.dataset.originalHtml
+ }
+ })
+
+ if (input?.dataset.supermemories) {
+ clearMemorySuggestion("gemini", input)
+ }
+ }
+
+ document.addEventListener(
+ "click",
+ async (event) => {
+ const target = event.target as HTMLElement
+ if (target.closest('[data-supermemory-connected-indicator="true"]')) {
+ return
+ }
+
+ const sendButton = target.closest("button")
+ if (sendButton && isGeminiSendButton(sendButton as HTMLButtonElement)) {
+ debugGemini("send button click detected", {
+ label: buttonLabel(sendButton as HTMLButtonElement),
+ element: describeElement(sendButton),
+ })
+ await captureGeminiPromptContent("button click")
+ }
+ },
+ true,
+ )
+
+ document.addEventListener(
+ "keydown",
+ async (event) => {
+ const target = event.target as HTMLElement
+
+ const activeInput =
+ (target.closest('[contenteditable="true"]') as GeminiInput | null) ||
+ (target.matches("textarea") ? (target as HTMLTextAreaElement) : null)
+ if (acceptMemorySuggestion(event, "gemini", activeInput)) {
+ return
+ }
+
+ if (
+ (target.matches("textarea") ||
+ target.matches('[contenteditable="true"]') ||
+ target.closest('[contenteditable="true"]')) &&
+ event.key === "Enter" &&
+ !event.shiftKey
+ ) {
+ debugGemini("Enter submit detected", {
+ target: describeElement(target),
+ })
+ await captureGeminiPromptContent("Enter key")
+ }
+ },
+ true,
+ )
+}
+
+async function setupGeminiAutoFetch() {
+ const autoSearch = (await autoSearchEnabled.getValue()) ?? false
+ debugGemini("setup auto fetch", { autoSearch })
+ if (!autoSearch) {
+ return
+ }
+
+ const input = getGeminiPromptInput()
+ if (!input || input.hasAttribute("data-supermemory-auto-fetch")) {
+ debugGemini("auto fetch skipped", {
+ hasInput: !!input,
+ alreadyAttached: input?.hasAttribute("data-supermemory-auto-fetch"),
+ })
+ return
+ }
+
+ input.setAttribute("data-supermemory-auto-fetch", "true")
+ debugGemini("auto fetch attached", describeElement(input))
+
+ const handleInput = () => {
+ const content = getInputText(input).trim()
+ syncAcceptedSupermemoryState(input)
+
+ if (content.length === 0) {
+ clearMemorySuggestion("gemini", input)
+ document
+ .querySelectorAll(`[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`)
+ .forEach((icon) => {
+ setMemoryMarkerStatus(icon as HTMLElement, "neutral")
+ })
+ }
+
+ if (geminiDebounceTimeout) {
+ clearTimeout(geminiDebounceTimeout)
+ }
+
+ geminiDebounceTimeout = setTimeout(async () => {
+ if (hasAcceptedSupermemoryContext(input)) {
+ clearMemorySuggestion("gemini", input)
+ return
+ }
+
+ if (content.length > 2) {
+ await getRelatedMemoriesForGemini(
+ POSTHOG_EVENT_KEY.GEMINI_CHAT_MEMORIES_AUTO_SEARCHED,
+ )
+ } else if (content.length === 0) {
+ const icons = document.querySelectorAll(
+ `[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`,
+ )
+
+ icons.forEach((icon) => {
+ const iconElement = icon as HTMLElement
+ iconElement.querySelector("[data-supermemory-status-badge]")?.remove()
+ delete iconElement.dataset.supermemoryStatus
+ delete iconElement.dataset.memoriesData
+ if (iconElement.dataset.originalHtml) {
+ iconElement.innerHTML = iconElement.dataset.originalHtml
+ delete iconElement.dataset.originalHtml
+ }
+ })
+
+ if (input.dataset.supermemories) {
+ clearMemorySuggestion("gemini", input)
+ }
+ }
+ }, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)
+ }
+
+ input.addEventListener("input", handleInput)
+}
diff --git a/apps/browser-extension/entrypoints/content/index.ts b/apps/browser-extension/entrypoints/content/index.ts
index 1c863530..776d9e9f 100644
--- a/apps/browser-extension/entrypoints/content/index.ts
+++ b/apps/browser-extension/entrypoints/content/index.ts
@@ -3,6 +3,7 @@ import { DOMUtils } from "../../utils/ui-components"
import { initializeChatGPT } from "./chatgpt"
import { initializeClaude } from "./claude"
import { initializeGrok } from "./grok"
+import { initializeGemini } from "./gemini"
import {
saveMemory,
setupGlobalKeyboardShortcut,
@@ -20,13 +21,13 @@ export default defineContentScript({
matches: ["
"],
main() {
// Setup global event listeners
- browser.runtime.onMessage.addListener(async (message) => {
+ browser.runtime.onMessage.addListener((message) => {
if (message.action === MESSAGE_TYPES.SHOW_TOAST) {
DOMUtils.showToast(message.state)
} else if (message.action === MESSAGE_TYPES.SAVE_MEMORY) {
- await saveMemory()
+ return saveMemory(message.actionSource || "content_script")
} else if (message.action === MESSAGE_TYPES.TWITTER_IMPORT_OPEN_MODAL) {
- await openImportModal()
+ return openImportModal()
} else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) {
updateTwitterImportUI(message)
} else if (message.type === MESSAGE_TYPES.IMPORT_DONE) {
@@ -52,6 +53,9 @@ export default defineContentScript({
if (DOMUtils.isOnDomain(DOMAINS.GROK)) {
initializeGrok()
}
+ if (DOMUtils.isOnDomain(DOMAINS.GEMINI)) {
+ initializeGemini()
+ }
if (DOMUtils.isOnDomain(DOMAINS.T3)) {
initializeT3()
}
@@ -70,6 +74,7 @@ export default defineContentScript({
initializeChatGPT()
initializeClaude()
initializeGrok()
+ initializeGemini()
initializeT3()
initializeTwitter()
diff --git a/apps/browser-extension/entrypoints/content/memory-suggestion.ts b/apps/browser-extension/entrypoints/content/memory-suggestion.ts
new file mode 100644
index 00000000..1722e71e
--- /dev/null
+++ b/apps/browser-extension/entrypoints/content/memory-suggestion.ts
@@ -0,0 +1,409 @@
+type SuggestionInput = HTMLElement | HTMLTextAreaElement
+
+const SUGGESTION_ATTR = "data-supermemory-memory-suggestion"
+const SUPERMEMORY_PREFIX = "Supermemories of user (only for the reference):"
+const SUPERMEMORY_BLUE = "#1A88FF"
+
+export function buildSupermemoryText(memories: unknown): string {
+ const memoryText = Array.isArray(memories)
+ ? memories.join("").trim()
+ : String(memories || "").trim()
+
+ return `\n\n${SUPERMEMORY_PREFIX} ${memoryText}`
+}
+
+export function showMemorySuggestion(
+ platform: string,
+ input: SuggestionInput,
+ memories: unknown,
+): string {
+ const suggestionText = buildSupermemoryText(memories)
+ input.dataset.supermemories = suggestionText
+ delete input.dataset.supermemoriesInjected
+
+ removeMemorySuggestion(platform)
+
+ const anchor = getSuggestionAnchor(input)
+ if (!anchor) return suggestionText
+
+ const previousPosition = window.getComputedStyle(anchor).position
+ if (previousPosition === "static") {
+ anchor.dataset.supermemoryPreviousPosition = "static"
+ anchor.style.position = "relative"
+ }
+
+ const suggestion = createSuggestionContainer(platform, input, anchor)
+ suggestion.dataset.supermemorySuggestionState = "ready"
+ suggestion.style.gap = "8px"
+ suggestion.style.alignItems = "center"
+
+ const text = document.createElement("span")
+ text.style.cssText = `
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ `
+ text.textContent = suggestionText.trim()
+
+ const tabKey = document.createElement("span")
+ tabKey.style.cssText = `
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ height: 20px;
+ padding: 0 8px;
+ border-radius: 999px;
+ background: ${SUPERMEMORY_BLUE};
+ color: #FFFFFF;
+ font-size: 11px;
+ font-weight: 700;
+ line-height: 1;
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.16) inset, 0 6px 18px rgba(26, 136, 255, 0.24);
+ flex-shrink: 0;
+ `
+ tabKey.textContent = "Tab"
+
+ suggestion.appendChild(text)
+ suggestion.appendChild(tabKey)
+ anchor.appendChild(suggestion)
+
+ return suggestionText
+}
+
+export function showLoadingSuggestion(
+ platform: string,
+ input: SuggestionInput,
+) {
+ removeMemorySuggestion(platform)
+
+ const anchor = getSuggestionAnchor(input)
+ if (!anchor) return
+
+ const previousPosition = window.getComputedStyle(anchor).position
+ if (previousPosition === "static") {
+ anchor.dataset.supermemoryPreviousPosition = "static"
+ anchor.style.position = "relative"
+ }
+
+ ensureSuggestionAnimationStyle()
+
+ const suggestion = createSuggestionContainer(platform, input, anchor)
+ suggestion.dataset.supermemorySuggestionState = "loading"
+ suggestion.style.gap = "4px"
+ suggestion.setAttribute("aria-label", "supermemory searching memories")
+
+ for (let index = 0; index < 3; index += 1) {
+ const dot = document.createElement("span")
+ dot.style.cssText = `
+ width: 5px;
+ height: 5px;
+ border-radius: 999px;
+ background: ${SUPERMEMORY_BLUE};
+ animation: supermemorySuggestionDot 1s ease-in-out infinite;
+ animation-delay: ${index * 0.14}s;
+ `
+ suggestion.appendChild(dot)
+ }
+
+ anchor.appendChild(suggestion)
+}
+
+function createSuggestionContainer(
+ platform: string,
+ input: SuggestionInput,
+ anchor: HTMLElement,
+): HTMLDivElement {
+ const suggestion = document.createElement("div")
+ suggestion.setAttribute(SUGGESTION_ATTR, platform)
+ const position = getCaretPosition(input, anchor)
+ const verticalOffset = platform === "gemini" ? -10 : 0
+ suggestion.style.cssText = `
+ position: absolute;
+ left: ${position.left + 6}px;
+ top: ${position.top + verticalOffset}px;
+ max-width: min(540px, calc(100% - ${position.left + 220}px));
+ display: inline-flex;
+ align-items: center;
+ height: 22px;
+ color: rgba(255, 255, 255, 0.34);
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ font-size: 14px;
+ line-height: 1.35;
+ pointer-events: none;
+ z-index: 2147483646;
+ `
+ return suggestion
+}
+
+export function removeMemorySuggestion(platform: string) {
+ const elements = document.querySelectorAll(
+ `[${SUGGESTION_ATTR}="${platform}"]`,
+ )
+ for (const element of elements) {
+ element.remove()
+ }
+}
+
+export function acceptMemorySuggestion(
+ event: KeyboardEvent,
+ platform: string,
+ input: SuggestionInput | null,
+): boolean {
+ if (event.key !== "Tab" || !input?.dataset.supermemories) {
+ return false
+ }
+
+ event.preventDefault()
+ event.stopPropagation()
+
+ const text = input.dataset.supermemories
+ appendTextToInput(input, text)
+ delete input.dataset.supermemories
+ input.dataset.supermemoriesInjected = "true"
+ removeMemorySuggestion(platform)
+
+ return true
+}
+
+export function hasAcceptedSupermemoryContext(
+ input: SuggestionInput | null,
+): boolean {
+ if (!input) return false
+ const text =
+ input instanceof HTMLTextAreaElement
+ ? input.value
+ : input.innerText || input.textContent || ""
+
+ return text.includes(SUPERMEMORY_PREFIX)
+}
+
+export function syncAcceptedSupermemoryState(input: SuggestionInput | null) {
+ if (!input?.dataset.supermemoriesInjected) return
+
+ if (!hasAcceptedSupermemoryContext(input)) {
+ delete input.dataset.supermemoriesInjected
+ }
+}
+
+export function clearMemorySuggestion(
+ platform: string,
+ input: SuggestionInput | null,
+) {
+ removeMemorySuggestion(platform)
+ if (input?.dataset.supermemories) {
+ delete input.dataset.supermemories
+ }
+ if (input?.dataset.supermemoriesInjected) {
+ delete input.dataset.supermemoriesInjected
+ }
+}
+
+export function setMemoryMarkerStatus(
+ iconElement: HTMLElement | null,
+ status: "neutral" | "searching" | "found" | "none" | "error",
+) {
+ if (!iconElement) return
+
+ iconElement.querySelector("[data-supermemory-status-badge]")?.remove()
+
+ if (status === "neutral" || status === "none") {
+ delete iconElement.dataset.supermemoryStatus
+ return
+ }
+
+ iconElement.dataset.supermemoryStatus = status
+ const badge = document.createElement("span")
+ badge.dataset.supermemoryStatusBadge = "true"
+ badge.style.cssText = `
+ position: absolute;
+ top: 3px;
+ right: 3px;
+ width: ${status === "searching" ? "7px" : "8px"};
+ height: ${status === "searching" ? "7px" : "8px"};
+ border-radius: 999px;
+ background: ${status === "found" ? "#36F3D7" : status === "searching" ? SUPERMEMORY_BLUE : status === "error" ? "#EF4444" : "rgba(255, 255, 255, 0.55)"};
+ border: 1px solid rgba(5, 7, 10, 0.9);
+ box-shadow: ${status === "found" ? "0 0 0 2px rgba(54, 243, 215, 0.18)" : "none"};
+ pointer-events: none;
+ `
+ iconElement.appendChild(badge)
+}
+
+export function showMarkerPopover(
+ iconElement: HTMLElement,
+ message: string,
+ memories?: string,
+ resetAfter = 0,
+) {
+ iconElement.querySelector("[data-supermemory-marker-popover]")?.remove()
+ ensureSuggestionAnimationStyle()
+
+ const popover = document.createElement("div")
+ popover.dataset.supermemoryMarkerPopover = "true"
+ popover.style.cssText = `
+ position: absolute;
+ right: 0;
+ bottom: calc(100% + 10px);
+ min-width: 168px;
+ max-width: 280px;
+ padding: 10px;
+ border-radius: 12px;
+ background: rgba(10, 14, 20, 0.96);
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ color: #FAFAFA;
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.32);
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ font-size: 12px;
+ line-height: 1.35;
+ text-align: left;
+ z-index: 2147483647;
+ pointer-events: auto;
+ `
+
+ const title = document.createElement("div")
+ title.style.cssText = `
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-weight: 700;
+ margin-bottom: ${memories ? "8px" : "0"};
+ `
+
+ if (message.toLowerCase().includes("searching")) {
+ const dots = document.createElement("span")
+ dots.style.cssText = "display: inline-flex; gap: 3px; align-items: center;"
+ for (let index = 0; index < 3; index += 1) {
+ const dot = document.createElement("span")
+ dot.style.cssText = `
+ width: 4px;
+ height: 4px;
+ border-radius: 999px;
+ background: ${SUPERMEMORY_BLUE};
+ animation: supermemorySuggestionDot 1s ease-in-out infinite;
+ animation-delay: ${index * 0.14}s;
+ `
+ dots.appendChild(dot)
+ }
+ title.appendChild(dots)
+ }
+
+ const titleText = document.createElement("span")
+ titleText.textContent =
+ message === "Included Memories" ? "Included memories" : message
+ title.appendChild(titleText)
+ popover.appendChild(title)
+
+ if (memories) {
+ const list = document.createElement("div")
+ list.style.cssText = `
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ max-height: 160px;
+ overflow-y: auto;
+ color: rgba(255, 255, 255, 0.76);
+ `
+
+ memories
+ .split(/[,\n]/)
+ .map((memory) => memory.trim())
+ .filter((memory) => memory.length > 0 && memory !== ",")
+ .slice(0, 5)
+ .forEach((memory) => {
+ const item = document.createElement("div")
+ item.textContent = memory
+ list.appendChild(item)
+ })
+
+ popover.appendChild(list)
+ }
+
+ iconElement.appendChild(popover)
+
+ if (resetAfter > 0) {
+ setTimeout(() => {
+ popover.remove()
+ }, resetAfter)
+ }
+}
+
+function ensureSuggestionAnimationStyle() {
+ if (document.getElementById("supermemory-suggestion-animation-style")) {
+ return
+ }
+
+ const style = document.createElement("style")
+ style.id = "supermemory-suggestion-animation-style"
+ style.textContent = `
+ @keyframes supermemorySuggestionDot {
+ 0%, 80%, 100% { opacity: 0.3; transform: translateY(0); }
+ 40% { opacity: 1; transform: translateY(-1px); }
+ }
+ `
+ document.head.appendChild(style)
+}
+
+function getSuggestionAnchor(input: SuggestionInput): HTMLElement | null {
+ return (
+ (input.closest("form") as HTMLElement | null) ||
+ (input.closest('[role="textbox"]') as HTMLElement | null)?.parentElement ||
+ input.parentElement
+ )
+}
+
+function getCaretPosition(input: SuggestionInput, anchor: HTMLElement) {
+ const anchorRect = anchor.getBoundingClientRect()
+
+ if (!(input instanceof HTMLTextAreaElement)) {
+ const selection = window.getSelection()
+ if (selection?.rangeCount) {
+ const range = selection.getRangeAt(0).cloneRange()
+ if (input.contains(range.startContainer)) {
+ range.collapse(true)
+ let rect = range.getBoundingClientRect()
+ if (rect.width === 0 && rect.height === 0) {
+ const marker = document.createElement("span")
+ marker.textContent = "\u200b"
+ range.insertNode(marker)
+ rect = marker.getBoundingClientRect()
+ marker.remove()
+ }
+
+ if (rect.width || rect.height) {
+ return {
+ left: Math.max(18, rect.right - anchorRect.left + 4),
+ top: Math.max(10, rect.top - anchorRect.top),
+ }
+ }
+ }
+ }
+ }
+
+ const inputRect = input.getBoundingClientRect()
+ return {
+ left: Math.max(18, inputRect.left - anchorRect.left + 18),
+ top: Math.max(10, inputRect.top - anchorRect.top + 8),
+ }
+}
+
+function appendTextToInput(input: SuggestionInput, text: string) {
+ if (input instanceof HTMLTextAreaElement) {
+ input.value = `${input.value}${text}`
+ input.dispatchEvent(new Event("input", { bubbles: true }))
+ return
+ }
+
+ input.focus()
+ const selection = window.getSelection()
+ const range = document.createRange()
+ range.selectNodeContents(input)
+ range.collapse(false)
+ range.insertNode(document.createTextNode(text))
+ range.collapse(false)
+ selection?.removeAllRanges()
+ selection?.addRange(range)
+ input.dispatchEvent(
+ new InputEvent("input", { bubbles: true, inputType: "insertText" }),
+ )
+}
diff --git a/apps/browser-extension/entrypoints/content/shared.ts b/apps/browser-extension/entrypoints/content/shared.ts
index 68d117a1..12647908 100644
--- a/apps/browser-extension/entrypoints/content/shared.ts
+++ b/apps/browser-extension/entrypoints/content/shared.ts
@@ -1,9 +1,12 @@
import { MESSAGE_TYPES } from "../../utils/constants"
import { bearerToken, userData } from "../../utils/storage"
+import type { APIResponse } from "../../utils/types"
import { DOMUtils } from "../../utils/ui-components"
import { default as TurndownService } from "turndown"
-export async function saveMemory() {
+export async function saveMemory(
+ actionSource = "content_script",
+): Promise {
try {
DOMUtils.showToast("loading")
@@ -64,21 +67,28 @@ export async function saveMemory() {
data.markdown = markdown
}
- const response = await browser.runtime.sendMessage({
+ const response = (await browser.runtime.sendMessage({
action: MESSAGE_TYPES.SAVE_MEMORY,
data,
- actionSource: "context_menu",
- })
+ actionSource,
+ })) as APIResponse
- console.log("Response from enxtension:", response)
- if (response.success) {
+ if (response?.success) {
DOMUtils.showToast("success")
- } else {
- DOMUtils.showToast("error")
+ return response
+ }
+ DOMUtils.showToast("error")
+ return {
+ success: false,
+ error: response?.error || "Failed to save memory",
}
} catch (error) {
console.error("Error saving memory:", error)
DOMUtils.showToast("error")
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "Unknown error",
+ }
}
}
@@ -90,7 +100,7 @@ export function setupGlobalKeyboardShortcut() {
event.key === "m"
) {
event.preventDefault()
- await saveMemory()
+ await saveMemory("keyboard_shortcut")
}
})
}
@@ -110,9 +120,6 @@ export function setupStorageListener() {
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
}
diff --git a/apps/browser-extension/entrypoints/content/t3.ts b/apps/browser-extension/entrypoints/content/t3.ts
index c7bdb09a..66a11235 100644
--- a/apps/browser-extension/entrypoints/content/t3.ts
+++ b/apps/browser-extension/entrypoints/content/t3.ts
@@ -26,7 +26,6 @@ export function initializeT3() {
}
setTimeout(() => {
- console.log("Adding supermemory icon to T3 input")
addSupermemoryIconToT3Input()
setupT3AutoFetch()
}, 2000)
@@ -55,7 +54,6 @@ function setupT3RouteChangeDetection() {
const checkForRouteChange = () => {
if (window.location.href !== currentUrl) {
currentUrl = window.location.href
- console.log("T3 route changed, re-adding supermemory icon")
setTimeout(() => {
addSupermemoryIconToT3Input()
setupT3AutoFetch()
@@ -183,10 +181,7 @@ async function getRelatedMemoriesForT3(actionSource: string) {
}
}
- console.log("T3 query extracted:", userQuery)
-
if (!userQuery.trim()) {
- console.log("No query text found for T3")
return
}
@@ -217,8 +212,6 @@ async function getRelatedMemoriesForT3(actionSource: string) {
timeoutPromise,
])
- console.log("T3 memories response:", response)
-
if (response?.success && response?.data) {
let textareaElement = null
const supermemoryContainer = document.querySelector(
@@ -337,12 +330,10 @@ function updateT3IconFeedback(
`
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")
@@ -493,11 +484,10 @@ function setupT3PromptCapture() {
}
document.body.setAttribute("data-t3-prompt-capture-setup", "true")
- const captureT3PromptContent = async (source: string) => {
+ const captureT3PromptContent = async (_source: string) => {
const autoCapture = (await autoCapturePromptsEnabled.getValue()) ?? false
if (!autoCapture) {
- console.log("Auto capture prompts is disabled, skipping prompt capture")
return
}
let promptContent = ""
@@ -538,15 +528,13 @@ function setupT3PromptCapture() {
}
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,
+ source: window.location.href,
},
})
} catch (error) {
diff --git a/apps/browser-extension/entrypoints/content/twitter.ts b/apps/browser-extension/entrypoints/content/twitter.ts
index ffa138af..875f4bab 100644
--- a/apps/browser-extension/entrypoints/content/twitter.ts
+++ b/apps/browser-extension/entrypoints/content/twitter.ts
@@ -253,7 +253,7 @@ async function showOnboardingToast() {
header.style.cssText =
"display: flex; align-items: flex-start; gap: 12px; position: relative;"
- const iconUrl = browser.runtime.getURL("/icon-16.png")
+ const iconUrl = browser.runtime.getURL("/new_logo.png")
const icon = document.createElement("img")
icon.src = iconUrl
icon.alt = "Supermemory"
@@ -512,7 +512,7 @@ function showOrUpdateImportProgressToast(message: string, isComplete = false) {
animation: smSlideInUp 0.3s ease-out;
`
- const iconUrl = browser.runtime.getURL("/icon-16.png")
+ const iconUrl = browser.runtime.getURL("/new_logo.png")
const icon = document.createElement("img")
icon.src = iconUrl
icon.alt = "Supermemory"
diff --git a/apps/browser-extension/entrypoints/popup/App.tsx b/apps/browser-extension/entrypoints/popup/App.tsx
index bcc2a910..ace10a03 100644
--- a/apps/browser-extension/entrypoints/popup/App.tsx
+++ b/apps/browser-extension/entrypoints/popup/App.tsx
@@ -2,7 +2,12 @@ import { useQueryClient } from "@tanstack/react-query"
import { useEffect, useState } from "react"
import "./App.css"
import { validateAuthToken } from "../../utils/api"
-import { MESSAGE_TYPES, STORAGE_KEYS, UI_CONFIG } from "../../utils/constants"
+import {
+ getSupermemoryLoginUrl,
+ MESSAGE_TYPES,
+ STORAGE_KEYS,
+ UI_CONFIG,
+} from "../../utils/constants"
import {
useDefaultProject,
useProjects,
@@ -253,6 +258,7 @@ function App() {
const [autoCapturePromptsEnabled, setAutoCapturePromptsEnabled] =
useState(false)
const [authInvalidated, setAuthInvalidated] = useState(false)
+ const [saveError, setSaveError] = useState(null)
const queryClient = useQueryClient()
const { data: projects = [], isLoading: loadingProjects } = useProjects({
@@ -375,29 +381,70 @@ function App() {
const handleSaveCurrentPage = async () => {
setSaving(true)
+ setSaveError(null)
try {
const tabs = await chrome.tabs.query({
active: true,
currentWindow: true,
})
- if (tabs.length > 0 && tabs[0].id) {
- const response = await chrome.tabs.sendMessage(tabs[0].id, {
- action: MESSAGE_TYPES.SAVE_MEMORY,
- actionSource: "popup",
- })
+ const tab = tabs[0]
+ let response: { success?: boolean; error?: string } | undefined
- if (response?.success) {
- await chrome.tabs.sendMessage(tabs[0].id, {
- action: MESSAGE_TYPES.SHOW_TOAST,
- state: "success",
+ if (tab?.id) {
+ try {
+ response = await chrome.tabs.sendMessage(tab.id, {
+ action: MESSAGE_TYPES.SAVE_MEMORY,
+ actionSource: "popup",
})
+ } catch (contentScriptError) {
+ console.warn("Content script save failed:", contentScriptError)
+ }
+ }
+
+ if (response && !response.success) {
+ throw new Error(response.error || "Failed to save current page")
+ }
+
+ if (!response) {
+ const fallbackUrl = tab?.url || currentUrl
+ const fallbackTitle = tab?.title || currentTitle || "Current Page"
+
+ if (!fallbackUrl) {
+ throw new Error("No active page URL found")
+ }
+
+ response = await chrome.runtime.sendMessage({
+ action: MESSAGE_TYPES.SAVE_MEMORY,
+ actionSource: "popup_fallback",
+ data: {
+ url: fallbackUrl,
+ title: fallbackTitle,
+ content: `${fallbackTitle}\n\n${fallbackUrl}`,
+ },
+ })
+ }
+
+ if (response?.success) {
+ if (tab?.id) {
+ await chrome.tabs
+ .sendMessage(tab.id, {
+ action: MESSAGE_TYPES.SHOW_TOAST,
+ state: "success",
+ })
+ .catch(() => undefined)
}
window.close()
+ return
}
+
+ throw new Error(response?.error || "Failed to save current page")
} catch (error) {
console.error("Failed to save current page:", error)
+ setSaveError(
+ error instanceof Error ? error.message : "Could not save page",
+ )
try {
const tabs = await chrome.tabs.query({
@@ -413,8 +460,6 @@ function App() {
} catch (toastError) {
console.error("Failed to show error toast:", toastError)
}
-
- window.close()
} finally {
setSaving(false)
}
@@ -592,7 +637,7 @@ function App() {
>
@@ -600,11 +645,9 @@ function App() {