revamp chrome extension

This commit is contained in:
Ishaan Gupta 2026-05-19 16:04:50 +05:30
parent ef0026a23c
commit 3e9391b451
12 changed files with 1995 additions and 289 deletions

View file

@ -13,18 +13,37 @@ import {
createChatGPTInputBarElement,
DOMUtils,
} from "../../utils/ui-components"
import {
acceptMemorySuggestion,
clearMemorySuggestion,
hasAcceptedSupermemoryContext,
setMemoryMarkerStatus,
showLoadingSuggestion,
showMarkerPopover,
showMemorySuggestion,
syncAcceptedSupermemoryState,
} from "./memory-suggestion"
let chatGPTDebounceTimeout: NodeJS.Timeout | null = null
let chatGPTRouteObserver: MutationObserver | null = null
let chatGPTUrlCheckInterval: NodeJS.Timeout | null = null
let chatGPTObserverThrottle: NodeJS.Timeout | null = null
const CHATGPT_DEBUG = true
const CHATGPT_LOG_PREFIX = "[supermemory:chatgpt]"
export function initializeChatGPT() {
debugChatGPT("initializeChatGPT called", {
host: window.location.hostname,
href: window.location.href,
})
if (!DOMUtils.isOnDomain(DOMAINS.CHATGPT)) {
debugChatGPT("not on ChatGPT domain, skipping")
return
}
if (document.body.hasAttribute("data-chatgpt-initialized")) {
debugChatGPT("already initialized")
return
}
@ -39,6 +58,18 @@ export function initializeChatGPT() {
setupChatGPTRouteChangeDetection()
document.body.setAttribute("data-chatgpt-initialized", "true")
debugChatGPT("initialized listeners")
}
function debugChatGPT(message: string, data?: unknown) {
if (!CHATGPT_DEBUG) return
if (data === undefined) {
console.log(CHATGPT_LOG_PREFIX, message)
return
}
console.log(CHATGPT_LOG_PREFIX, message, data)
}
function setupChatGPTRouteChangeDetection() {
@ -58,7 +89,7 @@ function setupChatGPTRouteChangeDetection() {
const checkForRouteChange = () => {
if (window.location.href !== currentUrl) {
currentUrl = window.location.href
console.log("ChatGPT route changed, re-adding supermemory elements")
debugChatGPT("route changed, re-adding supermemory elements", currentUrl)
setTimeout(() => {
addSupermemoryButtonToMemoriesDialog()
addSaveChatGPTElementBeforeComposerBtn()
@ -83,8 +114,10 @@ function setupChatGPTRouteChangeDetection() {
if (
element.querySelector?.("#prompt-textarea") ||
element.querySelector?.("button.composer-btn") ||
element.querySelector?.("button") ||
element.querySelector?.('[role="dialog"]') ||
element.matches?.("#prompt-textarea") ||
element.matches?.("button") ||
element.id === "prompt-textarea"
) {
shouldRecheck = true
@ -98,6 +131,7 @@ function setupChatGPTRouteChangeDetection() {
chatGPTObserverThrottle = setTimeout(() => {
try {
chatGPTObserverThrottle = null
debugChatGPT("DOM changed near composer, rechecking UI")
addSupermemoryButtonToMemoriesDialog()
addSaveChatGPTElementBeforeComposerBtn()
setupChatGPTAutoFetch()
@ -124,6 +158,8 @@ function setupChatGPTRouteChangeDetection() {
async function getRelatedMemoriesForChatGPT(actionSource: string) {
try {
const isAutoSearch =
actionSource === POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED
const userQuery =
document.getElementById("prompt-textarea")?.textContent || ""
@ -138,7 +174,15 @@ async function getRelatedMemoriesForChatGPT(actionSource: string) {
return
}
updateChatGPTIconFeedback("Searching memories...", iconElement)
if (isAutoSearch) {
const promptElement = document.getElementById("prompt-textarea")
if (promptElement) {
showLoadingSuggestion("chatgpt", promptElement)
}
setMemoryMarkerStatus(iconElement, "searching")
} else {
updateChatGPTIconFeedback("Searching memories...", iconElement)
}
const timeoutPromise = new Promise((_, reject) =>
setTimeout(
@ -159,24 +203,40 @@ async function getRelatedMemoriesForChatGPT(actionSource: string) {
if (response?.success && response?.data) {
const promptElement = document.getElementById("prompt-textarea")
if (promptElement) {
promptElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${response.data}`
const memoryText = showMemorySuggestion(
"chatgpt",
promptElement,
response.data,
)
console.log(
"Prompt element dataset:",
promptElement.dataset.supermemories,
memoryText,
)
iconElement.dataset.memoriesData = response.data
iconElement.dataset.memoriesData = String(response.data)
updateChatGPTIconFeedback("Included Memories", iconElement)
if (isAutoSearch) {
setMemoryMarkerStatus(iconElement, "found")
} else {
updateChatGPTIconFeedback("Included Memories", iconElement)
}
} else {
console.warn(
"ChatGPT prompt element not found after successful memory fetch",
)
updateChatGPTIconFeedback("Memories found", iconElement)
if (isAutoSearch) {
setMemoryMarkerStatus(iconElement, "found")
} else {
updateChatGPTIconFeedback("Memories found", iconElement)
}
}
} else {
console.warn("No memories found or API response invalid")
updateChatGPTIconFeedback("No memories found", iconElement)
if (isAutoSearch) {
setMemoryMarkerStatus(iconElement, "none")
} else {
updateChatGPTIconFeedback("No memories found", iconElement)
}
}
} catch (error) {
console.error("Error getting related memories:", error)
@ -185,7 +245,13 @@ async function getRelatedMemoriesForChatGPT(actionSource: string) {
'[id*="sm-chatgpt-input-bar-element-before-composer"]',
)[0] as HTMLElement
if (icon) {
updateChatGPTIconFeedback("Error fetching memories", icon)
if (
actionSource === POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED
) {
setMemoryMarkerStatus(icon, "error")
} else {
updateChatGPTIconFeedback("Error fetching memories", icon)
}
}
} catch (feedbackError) {
console.error("Failed to update error feedback:", feedbackError)
@ -300,6 +366,29 @@ function updateChatGPTIconFeedback(
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)
return
if (!iconElement.dataset.originalHtml) {
iconElement.dataset.originalHtml = iconElement.innerHTML
}
@ -515,57 +604,218 @@ function updateChatGPTIconFeedback(
}
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 parent = button.parentElement
if (!parent) 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)
existingMarkers.forEach((marker) => marker.remove())
} else if (existingMarkers.length === 1) {
debugChatGPT("marker already exists")
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 +836,27 @@ 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 +869,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 +878,7 @@ async function setupChatGPTAutoFetch() {
})
if (promptTextarea.dataset.supermemories) {
delete promptTextarea.dataset.supermemories
clearMemorySuggestion("chatgpt", promptTextarea)
}
}
}, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)
@ -641,16 +907,6 @@ 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)
@ -682,7 +938,7 @@ function setupChatGPTPromptCapture() {
})
if (promptTextarea?.dataset.supermemories) {
delete promptTextarea.dataset.supermemories
clearMemorySuggestion("chatgpt", promptTextarea)
}
}
@ -705,6 +961,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" &&

View file

@ -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}"]`),
)
if (existingMarkers.length > 1) {
debugClaude("removed duplicate markers", existingMarkers.length)
existingMarkers.forEach((marker) => marker.remove())
} else if (existingMarkers.length === 1) {
debugClaude("marker already exists")
return
}
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)
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(
@ -210,7 +412,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(
@ -236,24 +446,40 @@ async function getRelatedMemoriesForClaude(actionSource: string) {
) as HTMLElement
if (textareaElement) {
textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${response.data}`
const memoryText = showMemorySuggestion(
"claude",
textareaElement,
response.data,
)
console.log(
"Text element dataset:",
textareaElement.dataset.supermemories,
memoryText,
)
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 +488,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)
@ -459,6 +691,29 @@ function updateClaudeIconFeedback(
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)
return
if (!iconElement.dataset.originalHtml) {
iconElement.dataset.originalHtml = iconElement.innerHTML
}
@ -704,17 +959,6 @@ 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)
@ -746,7 +990,7 @@ function setupClaudePromptCapture() {
})
if (contentEditableDiv?.dataset.supermemories) {
delete contentEditableDiv.dataset.supermemories
clearMemorySuggestion("claude", contentEditableDiv)
}
}
@ -754,14 +998,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 +1019,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 +1062,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 +1095,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 +1104,7 @@ async function setupClaudeAutoFetch() {
})
if (textareaElement.dataset.supermemories) {
delete textareaElement.dataset.supermemories
clearMemorySuggestion("claude", textareaElement)
}
}
}, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)

View file

@ -0,0 +1,681 @@
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 = true
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)
existingMarkers.forEach((marker) => 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 || ""
}
function appendStoredMemories(input: GeminiInput, storedMemories: string) {
if (input instanceof HTMLTextAreaElement) {
const promptContent = input.value || ""
input.value = `${promptContent}${storedMemories}`
input.dispatchEvent(new Event("input", { bubbles: true }))
return input.value
}
input.appendChild(document.createTextNode(storedMemories))
input.dispatchEvent(
new InputEvent("input", { bubbles: true, inputType: "insertText" }),
)
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) {
console.log("Auto capture prompts is disabled, skipping prompt capture")
return
}
const input = getGeminiPromptInput()
let 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,
},
})
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)
}

View file

@ -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: ["<all_urls>"],
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()

View file

@ -0,0 +1,402 @@
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) {
document
.querySelectorAll(`[${SUGGESTION_ATTR}="${platform}"]`)
.forEach((element) => 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" }),
)
}

View file

@ -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<APIResponse> {
try {
DOMUtils.showToast("loading")
@ -64,21 +67,30 @@ 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) {
console.log("Response from extension:", response)
if (response?.success) {
DOMUtils.showToast("success")
return response
} else {
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 +102,7 @@ export function setupGlobalKeyboardShortcut() {
event.key === "m"
) {
event.preventDefault()
await saveMemory()
await saveMemory("keyboard_shortcut")
}
})
}

View file

@ -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<boolean>(false)
const [authInvalidated, setAuthInvalidated] = useState<boolean>(false)
const [saveError, setSaveError] = useState<string | null>(null)
const queryClient = useQueryClient()
const { data: projects = [], isLoading: loadingProjects } = useProjects({
@ -375,29 +381,68 @@ 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 +458,6 @@ function App() {
} catch (toastError) {
console.error("Failed to show error toast:", toastError)
}
window.close()
} finally {
setSaving(false)
}
@ -931,6 +974,11 @@ function App() {
{saving ? "Saving..." : "Add to supermemory"}
</button>
{saveError && (
<p className="mt-2 text-xs leading-snug text-red-300">
{saveError}
</p>
)}
</div>
</div>
) : activeTab === "imports" ? (
@ -1269,13 +1317,13 @@ function App() {
</h2>
<ul className="list-none p-0 m-0 text-left">
<li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-[''] before:absolute before:left-0 before:text-neutral-500 before:font-bold">
<li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-['-'] before:absolute before:left-0 before:text-neutral-500 before:font-bold">
Save any page to your supermemory
</li>
<li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-[''] before:absolute before:left-0 before:text-neutral-500 before:font-bold">
<li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-['-'] before:absolute before:left-0 before:text-neutral-500 before:font-bold">
Import all your Twitter / X Bookmarks
</li>
<li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-[''] before:absolute before:left-0 before:text-neutral-500 before:font-bold">
<li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-['-'] before:absolute before:left-0 before:text-neutral-500 before:font-bold">
Import your ChatGPT Memories
</li>
</ul>
@ -1300,9 +1348,7 @@ function App() {
className="w-full py-3 px-6 bg-[#2d3f5c] text-white border-none rounded-3xl text-base font-medium cursor-pointer transition-colors duration-200 hover:bg-[#3d5270] disabled:bg-neutral-600 disabled:cursor-not-allowed"
onClick={() => {
chrome.tabs.create({
url: import.meta.env.PROD
? "https://app.supermemory.ai/login"
: "http://localhost:3000/login",
url: getSupermemoryLoginUrl(),
})
}}
type="button"

View file

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Default Popup Title</title>
<title>supermemory</title>
<meta name="manifest.type" content="browser_action" />
</head>
<body>

View file

@ -1,105 +1,99 @@
import { getSupermemoryLoginUrl } from "../../utils/constants"
function Welcome() {
return (
<div className="min-h-screen font-[Space_Grotesk,-apple-system,BlinkMacSystemFont,Segoe_UI,Roboto,sans-serif] flex items-center justify-center p-8 bg-gradient-to-br from-gray-50 to-white">
<div className="max-w-4xl w-full text-center">
{/* Header */}
<div className="mb-12">
<img
alt="supermemory"
className="h-16 mb-6 mx-auto"
src="https://assets.supermemory.ai/brand/wordmark/dark-transparent.svg"
/>
<p className="text-gray-600 text-lg font-normal max-w-2xl mx-auto">
Your AI second brain for saving and organizing everything that
matters. Supermemory learns and remembers everything you save, your
preferences, and understands you.
</p>
</div>
<div className="relative min-h-screen overflow-hidden bg-[#05080D] text-white font-[Space_Grotesk,-apple-system,BlinkMacSystemFont,Segoe_UI,Roboto,sans-serif]">
<div
className="pointer-events-none absolute inset-0"
style={{
background:
"linear-gradient(180deg, #05080D 0%, #05070A 48%, #060A18 100%)",
}}
/>
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(105,167,240,0.20)_1px,transparent_1px)] bg-size-[32px_32px] opacity-70 mask-[linear-gradient(to_bottom,transparent_0%,black_12%,black_100%)]" />
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-[55%] bg-[radial-gradient(ellipse_at_bottom,rgba(20,65,255,0.42),transparent_68%)]" />
{/* Features Section */}
<div className="mb-12">
<h2 className="text-2xl font-semibold text-black mb-8">
What can you do with supermemory ?
</h2>
<main className="relative mx-auto flex min-h-screen w-full max-w-6xl flex-col px-6 py-6 sm:px-10">
<header className="flex items-center justify-between border-b border-white/10 pb-5">
<div className="flex items-center gap-2">
<img
alt=""
className="size-8 rounded-[4px]"
src="./icon-48.png"
/>
<img
alt="supermemory"
className="h-6 w-auto"
src="./logo-fullmark.svg"
/>
</div>
<span className="rounded-full border border-[#369BFD]/25 bg-[#369BFD]/10 px-3 py-1 text-xs font-medium text-[#9FCBFF]">
Chrome extension
</span>
</header>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div className="bg-white border border-gray-200 rounded-xl p-6 text-center transition-all duration-200 shadow-sm hover:-translate-y-0.5 hover:shadow-md hover:border-gray-300">
<div className="text-3xl mb-4 block">💾</div>
<h3 className="text-lg font-semibold text-black mb-3">
Save Any Page
</h3>
<p className="text-sm text-gray-600 leading-snug">
Instantly save web pages, articles, and content to your personal
knowledge base
</p>
<section className="flex flex-1 items-center py-10">
<div className="max-w-3xl">
<div className="mb-5 inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.035] px-3 py-1 text-sm font-medium text-[#A7B7CB]">
<span className="h-1.5 w-1.5 rounded-full bg-[#36FDFD]" />
Browser memory connected
</div>
<div className="bg-white border border-gray-200 rounded-xl p-6 text-center transition-all duration-200 shadow-sm hover:-translate-y-0.5 hover:shadow-md hover:border-gray-300">
<div className="text-3xl mb-4 block">🐦</div>
<h3 className="text-lg font-semibold text-black mb-3">
Import Twitter/X Bookmarks
</h3>
<p className="text-sm text-gray-600 leading-snug">
Bring all your saved tweets and bookmarks into one organized
place
</p>
</div>
<h1 className="max-w-3xl text-4xl font-semibold leading-[1.05] tracking-normal text-white sm:text-6xl">
Your browser now has{" "}
<span
className="text-transparent bg-clip-text"
style={{
backgroundImage:
"linear-gradient(94deg, #369BFD 4.8%, #36FDFD 77.04%, #36FDB5 143.99%)",
}}
>
supermemory.
</span>
</h1>
<div className="bg-white border border-gray-200 rounded-xl p-6 text-center transition-all duration-200 shadow-sm hover:-translate-y-0.5 hover:shadow-md hover:border-gray-300">
<div className="text-3xl mb-4 block">🤖</div>
<h3 className="text-lg font-semibold text-black mb-3">
Import ChatGPT Memories
</h3>
<p className="text-sm text-gray-600 leading-snug">
Keep your important AI conversations and insights accessible
</p>
</div>
<p className="mt-5 max-w-xl text-base leading-7 text-[#A1A1AA] sm:text-lg">
Sign in once, then use the extension to save pages, import
bookmarks, and bring your memory into supported chat apps.
</p>
<div className="bg-white border border-gray-200 rounded-xl p-6 text-center transition-all duration-200 shadow-sm hover:-translate-y-0.5 hover:shadow-md hover:border-gray-300">
<div className="text-3xl mb-4 block">🔍</div>
<h3 className="text-lg font-semibold text-black mb-3">
Your context, everywhere.
</h3>
<p className="text-sm text-gray-600 leading-snug">
You can connect chatbots with MCP, chat with your personal
assistant, and more.
</p>
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
<button
className="h-12 rounded-xl px-6 text-sm font-semibold text-white transition hover:brightness-110 focus:outline-none focus:ring-2 focus:ring-[#36fdfd]/70"
style={{
background:
"linear-gradient(182.37deg, #0ff0d2 -91.53%, #5bd3fb -67.8%, #1e0ff0 95.17%)",
boxShadow:
"1px 1px 2px 0px #1A88FF inset, 0 2px 18px 0 rgba(54, 155, 253, 0.24)",
}}
onClick={() => {
chrome.tabs.create({
url: getSupermemoryLoginUrl(),
})
}}
type="button"
>
Sign in to connect
</button>
<button
className="h-12 rounded-xl border border-[#369BFD]/25 bg-[#080B0F]/80 px-6 text-sm font-semibold text-[#C7D7F2] transition hover:border-[#369BFD]/50 hover:bg-[#0D121A] focus:outline-none focus:ring-2 focus:ring-[#369BFD]/30"
onClick={() => {
chrome.tabs.create({
url: "https://supermemory.ai",
})
}}
type="button"
>
Open supermemory.ai
</button>
</div>
</div>
</div>
</section>
{/* Actions */}
<div className="mb-8">
<button
className="min-w-[200px] px-8 py-4 bg-gray-700 text-white border-none rounded-3xl text-base font-semibold cursor-pointer transition-colors duration-200 mb-4 outline-none hover:bg-gray-800 disabled:bg-gray-400 disabled:cursor-not-allowed"
onClick={() => {
chrome.tabs.create({
url: import.meta.env.PROD
? "https://app.supermemory.ai/login"
: "http://localhost:3000/login",
})
}}
type="button"
>
Login to Get started
</button>
</div>
{/* Footer */}
<div className="border-t border-gray-200 pt-6 mt-8">
<p className="text-sm text-gray-600">
Learn more at{" "}
<a
className="text-blue-500 no-underline hover:underline hover:text-blue-700"
href="https://supermemory.ai"
rel="noopener noreferrer"
target="_blank"
>
supermemory.ai
</a>
</p>
</div>
</div>
<footer className="border-t border-white/10 py-5 text-xs text-[#737373]">
supermemory stores your extension session locally in Chrome.
</footer>
</main>
</div>
)
}

View file

@ -10,6 +10,17 @@ export const API_ENDPOINTS = {
: "http://localhost:3000",
} as const
export function getSupermemoryLoginUrl(): string {
const baseUrl = API_ENDPOINTS.SUPERMEMORY_WEB
const loginUrl = new URL("/login", baseUrl)
const redirectUrl = new URL("/", baseUrl)
redirectUrl.searchParams.set("extension-auth-success", "true")
loginUrl.searchParams.set("redirect", redirectUrl.toString())
return loginUrl.toString()
}
/**
* DOM Element IDs
*/
@ -22,6 +33,7 @@ export const ELEMENT_IDS = {
SAVE_TWEET_ELEMENT: "sm-save-tweet-element",
CHATGPT_INPUT_BAR_ELEMENT: "sm-chatgpt-input-bar-element",
CLAUDE_INPUT_BAR_ELEMENT: "sm-claude-input-bar-element",
GEMINI_INPUT_BAR_ELEMENT: "sm-gemini-input-bar-element",
T3_INPUT_BAR_ELEMENT: "sm-t3-input-bar-element",
PROJECT_SELECTION_MODAL: "sm-project-selection-modal",
} as const
@ -59,6 +71,7 @@ export const DOMAINS = {
CHATGPT: ["chatgpt.com", "chat.openai.com"],
CLAUDE: ["claude.ai"],
GROK: ["grok.com", "x.ai"],
GEMINI: ["gemini.google.com"],
T3: ["t3.chat"],
SUPERMEMORY: ["localhost", "supermemory.ai", "app.supermemory.ai"],
} as const
@ -95,6 +108,8 @@ export const POSTHOG_EVENT_KEY = {
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",
GEMINI_CHAT_MEMORIES_SEARCHED: "gemini_chat_memories_searched",
GEMINI_CHAT_MEMORIES_AUTO_SEARCHED: "gemini_chat_memories_auto_searched",
CHATGPT_CHAT_MEMORIES_SEARCHED: "chatgpt_chat_memories_searched",
CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED: "chatgpt_chat_memories_auto_searched",
} as const

View file

@ -261,31 +261,72 @@ export function createSaveTweetElement(onClick: () => void): HTMLElement {
* @returns HTMLElement - The save button element
*/
export function createChatGPTInputBarElement(onClick: () => void): HTMLElement {
const iconButton = document.createElement("div")
return createConnectedIndicator(onClick)
}
export function createConnectedIndicator(onClick: () => void): HTMLElement {
const iconButton = document.createElement("button")
iconButton.type = "button"
iconButton.setAttribute("aria-label", "supermemory connected")
iconButton.dataset.supermemoryConnectedIndicator = "true"
iconButton.style.cssText = `
display: inline-flex;
align-items: center;
justify-content: center;
width: auto;
height: 24px;
width: 32px;
height: 32px;
min-width: 32px;
cursor: pointer;
transition: opacity 0.2s ease;
transition: opacity 0.2s ease, background-color 0.2s ease, transform 0.2s ease;
border-radius: 50%;
border: none;
background: transparent;
padding: 0;
position: relative;
flex-shrink: 0;
`
// Use appropriate icon based on theme
const iconFileName = "/icon-16.png"
const iconUrl = browser.runtime.getURL(iconFileName)
iconButton.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 50%;" />
<img src="${iconUrl}" width="20" height="20" alt="" style="border-radius: 5px; display: block;" />
`
const tooltip = document.createElement("div")
tooltip.textContent = "supermemory connected"
tooltip.style.cssText = `
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%) translateY(2px);
background: #0A0E14;
color: #FAFAFA;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 8px;
padding: 6px 8px;
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 12px;
font-weight: 500;
line-height: 1;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.16s ease, transform 0.16s ease;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
z-index: 2147483647;
`
iconButton.appendChild(tooltip)
iconButton.addEventListener("mouseenter", () => {
iconButton.style.opacity = "0.8"
iconButton.style.backgroundColor = "rgba(255, 255, 255, 0.08)"
tooltip.style.opacity = "1"
tooltip.style.transform = "translateX(-50%) translateY(0)"
})
iconButton.addEventListener("mouseleave", () => {
iconButton.style.opacity = "1"
iconButton.style.backgroundColor = "transparent"
tooltip.style.opacity = "0"
tooltip.style.transform = "translateX(-50%) translateY(2px)"
})
iconButton.addEventListener("click", (event) => {
@ -303,42 +344,11 @@ export function createChatGPTInputBarElement(onClick: () => void): HTMLElement {
* @returns HTMLElement - The save button element
*/
export function createClaudeInputBarElement(onClick: () => void): HTMLElement {
const iconButton = document.createElement("div")
iconButton.style.cssText = `
display: inline-flex;
align-items: center;
justify-content: center;
width: auto;
height: 32px;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 6px;
background: transparent;
`
return createConnectedIndicator(onClick)
}
const iconFileName = "/icon-16.png"
const iconUrl = browser.runtime.getURL(iconFileName)
iconButton.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Get Related Memories from supermemory" style="border-radius: 4px;" />
`
iconButton.addEventListener("mouseenter", () => {
iconButton.style.backgroundColor = "rgba(0, 0, 0, 0.05)"
iconButton.style.borderColor = "rgba(0, 0, 0, 0.2)"
})
iconButton.addEventListener("mouseleave", () => {
iconButton.style.backgroundColor = "transparent"
iconButton.style.borderColor = "rgba(0, 0, 0, 0.1)"
})
iconButton.addEventListener("click", (event) => {
event.stopPropagation()
event.preventDefault()
onClick()
})
return iconButton
export function createGeminiInputBarElement(onClick: () => void): HTMLElement {
return createConnectedIndicator(onClick)
}
/**

View file

@ -29,7 +29,7 @@ export default defineConfig({
manifest: {
name: "supermemory",
homepage_url: "https://supermemory.ai",
version: "6.1.4",
version: "6.1.3",
permissions: ["storage", "activeTab", "webRequest", "tabs"],
host_permissions: [
"*://x.com/*",
@ -42,6 +42,9 @@ export default defineConfig({
"*://*.grok.com/*",
"*://x.ai/*",
"*://*.x.ai/*",
"*://claude.ai/*",
"*://gemini.google.com/*",
"*://t3.chat/*",
"https://*.posthog.com/*",
],
web_accessible_resources: [