Showing preview only (751K chars total). Download the full file or copy to clipboard to get everything.
Repository: deshraj/claude-memory
Branch: main
Commit: 54a882ab6f25
Files: 43
Total size: 726.5 KB
Directory structure:
gitextract_b1wvur0_/
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── docs/
│ └── types.md
├── manifest.json
├── package.json
├── privacy-policy.md
├── src/
│ ├── background.ts
│ ├── chatgpt/
│ │ └── content.ts
│ ├── claude/
│ │ └── content.ts
│ ├── context-menu-memory.ts
│ ├── deepseek/
│ │ └── content.ts
│ ├── direct-url-tracker.ts
│ ├── gemini/
│ │ └── content.ts
│ ├── grok/
│ │ └── content.ts
│ ├── mem0/
│ │ └── content.ts
│ ├── perplexity/
│ │ └── content.ts
│ ├── popup.html
│ ├── popup.ts
│ ├── replit/
│ │ └── content.ts
│ ├── search_tracker.ts
│ ├── selection_context.ts
│ ├── sidebar.ts
│ ├── types/
│ │ ├── api.ts
│ │ ├── browser.ts
│ │ ├── dom.ts
│ │ ├── memory.ts
│ │ ├── messages.ts
│ │ ├── organizations.ts
│ │ ├── providers.ts
│ │ ├── settings.ts
│ │ └── storage.ts
│ └── utils/
│ ├── background_search.ts
│ ├── llm_prompts.ts
│ ├── site_config.ts
│ ├── util_functions.ts
│ └── util_positioning.ts
├── tsconfig.json
└── vite.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintignore
================================================
dist/
node_modules/
*.min.js
icons/
package-lock.json
*.log
.env*
================================================
FILE: .eslintrc.json
================================================
{
"root": true,
"env": {
"browser": true,
"es2022": true,
"webextensions": true
},
"extends": [
"eslint:recommended",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["@typescript-eslint", "prettier", "import"],
"rules": {
"prettier/prettier": "error",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-empty-function": "warn",
"@typescript-eslint/no-inferrable-types": "off",
"no-var": "error",
"no-console": "off",
"no-debugger": "error",
"no-duplicate-imports": "error",
"no-unused-expressions": "error",
"eqeqeq": ["error", "always"],
"curly": ["error", "all"],
"import/order": [
"error",
{
"groups": [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index"
],
"newlines-between": "always",
"alphabetize": {
"order": "asc",
"caseInsensitive": true
}
}
]
},
"ignorePatterns": [
"dist/",
"node_modules/",
"*.min.js",
"icons/"
]
}
================================================
FILE: .gitignore
================================================
# Node modules
node_modules/
# Build output
dist/
build/
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# OS generated files
.DS_Store
Thumbs.db
# Environment files
.env
.env.local
.env.*.local
# Chrome extension specific
*.crx
*.pem
# IDE files
.vscode/
.idea/
*.sublime-workspace
*.sublime-project
================================================
FILE: .prettierignore
================================================
dist/
node_modules/
*.min.js
icons/
package-lock.json
*.log
.env*
================================================
FILE: .prettierrc
================================================
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf",
"quoteProps": "as-needed",
"bracketSameLine": false,
"proseWrap": "preserve",
"overrides": [
{
"files": "*.html",
"options": {
"parser": "html"
}
}
]
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2024 Deshraj Yadav
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
> [!CAUTION]
> ## This project has been archived
>
> **mem0-chrome-extension** is no longer actively maintained and this repository is now a public archive.
>
> Thank you to the 600+ stargazers, 97 forkers, and every contributor who helped shape this project. Your support and feedback meant the world to us.
>
> **Looking for memory across your AI assistants?** Check out [Mem0](https://www.mem0.ai) for the latest.
>
> You're welcome to fork this repo and build on it — the MIT license still applies.
# Mem0 Chrome Extension — Cross-LLM Memory
Mem0 brings ChatGPT-style memory to all your favorite AI assistants. Share context seamlessly across ChatGPT, Claude, Perplexity, and more, making your AI interactions more personalized and efficient.
<a href="https://chromewebstore.google.com/detail/claude-memory/onihkkbipkfeijkadecaafbgagkhglop?hl=en-GB&utm_source=ext_sidebar" style="display: inline-block; padding: 8px 12px; background-color: white; color: #3c4043; text-decoration: none; font-family: 'Roboto', Arial, sans-serif; font-size: 14px; font-weight: 500; border-radius: 4px; border: 1px solid #dadce0; box-shadow: 0 1px 2px rgba(60,64,67,0.3), 0 1px 3px 1px rgba(60,64,67,0.15);">
<img src="https://www.google.com/chrome/static/images/chrome-logo.svg" alt="Chrome logo" style="height: 24px; vertical-align: middle; margin-right: 8px;">
Add to Chrome, It's Free
</a>
<br>
<br>
Built using [Mem0](https://www.mem0.ai) ❤️
## Demo
Watch the Mem0 Chrome Extension in action (full-resolution video available [here](https://www.youtube.com/watch?v=cByzXztn-YY)):
https://github.com/user-attachments/assets/a069a178-631e-4b35-a182-9f4fef7735c4
## Features
- **Universal Memory Layer:** Share context across ChatGPT, Claude, Perplexity, and more
- **Smart Context Detection:** Automatically captures relevant information from your conversations
- **Intelligent Memory Retrieval:** Surfaces relevant memories at the right time
- **One-click sync** with existing ChatGPT memories
- **Memory dashboard** to manage all memories
## Installation
> **Note:** Make sure you have [Node.js](https://nodejs.org/) installed before proceeding.
1. Clone this repository.
2. Navigate to the directory where you cloned the repository.
3. Run `npm install` to install dependencies.
4. Run `npm run build` to build the extension.
5. The built files will be in the `dist` directory.
6. Open Google Chrome and navigate to `chrome://extensions`.
7. Enable "Developer mode" in the top right corner.
8. Click "Load unpacked" and select the `dist` directory containing the extension files.
9. The Mem0 Chrome Extension should now appear in your Chrome toolbar.
## Usage
1. After installation, look for the Mem0 icon in your Chrome toolbar
2. Sign in with Google
3. Start chatting with any supported AI assistant
4. For ChatGPT and Perplexity, just press enter while chatting as you would normally
5. On Claude, click the Mem0 button or use shortcut ^ + M
## ❤️ Free to Use
Mem0 is completely free with:
- No usage limits
- No ads
- All features included
## Configuration
- API Key: Required for connecting to the Mem0 API. Obtain this from your Mem0 Dashboard.
- User ID: Your unique identifier in the Mem0 system. If not provided, it defaults to 'chrome-extension-user'.
## Troubleshooting
If you encounter any issues:
- Check your internet connection
- Verify you're signed in correctly
- Clear your browser cache if needed
- Contact support if issues persist
## Privacy and Data Security
Your messages are sent to the Mem0 API for extracting and retrieving memories.
## Contributing
Contributions to improve Mem0 Chrome Extension are welcome. Please feel free to submit pull requests or open issues for bugs and feature requests.
## License
MIT License
================================================
FILE: docs/types.md
================================================
# Types Documentation
This section contains TypeScript type definitions for the Mem0 Chrome Extension. The types are organized into several files based on their functionality.
## Overview
### `types/api.ts`
Defines types for API interactions:
- `MessageRole` - Enum for message roles (User, Assistant)
- `ApiMessage` - Message structure with role and content
- `ApiMemoryRequest` - Request payload for memory API calls
- `MemorySearchResponse` - Array of memory search results
- `LoginData` - User authentication data structure
- `DEFAULT_USER_ID` - Default user ID constant
- `SOURCE` - Extension source identifier
### `types/browser.ts`
Browser-specific type definitions:
- `OnCommittedDetails` - Web navigation event details
- `JsonPrimitive` - JSON primitive value types
- `JsonValue` - Recursive JSON value type
- `JsonObject` - JSON object structure
- `HistoryStateData` - Browser history state data
- `HistoryUrl` - Browser history URL type
### `types/dom.ts`
DOM-related type extensions and global declarations:
- `ExtendedHTMLElement` - Extended HTML element with cleanup methods
- `ExtendedDocument` - Extended document with mem0-specific properties
- `ExtendedElement` - Extended element with additional properties
- `ModalDimensions` - Modal size and pagination settings
- `ModalPosition` - Modal positioning coordinates
- `MutableMutationObserver` - Extended mutation observer with timers
- Global interface extensions for `Element`, `CSSStyleDeclaration`, and `Window`
### `types/memory.ts`
Core memory-related types:
- `MemoryItem` - Individual memory structure with id, text, and categories
- `Memory` - Simplified memory structure
- `MemorySearchItem` - Search result item from API
- `MemoriesResponse` - API response wrapper for memories
- `OpenMemoryPrompts` - Prompt templates and regex patterns
- `OptionalApiParams` - Optional parameters for API calls (org_id, project_id)
### `types/messages.ts`
Message passing between extension components:
- `ToastVariant` - Enum for toast notification types (SUCCESS, ERROR)
- `MessageType` - Enum for different message types
- `SidebarAction` - Enum for sidebar actions (TOGGLE_SIDEBAR, OPEN_POPUP, etc.)
- `SelectionContextPayload` - Payload for selection context messages
- `SelectionContextResponse` - Response structure for selection context
- `GetSelectionContextMessage` - Message type for getting selection context
- `ToastMessage` - Toast notification message structure
- `SelectionContextMessage` - Union type for selection context messages
- `SendResponse` - Response callback type
- Various sidebar action message types
### `types/organizations.ts`
Organization and project management:
- `Organization` - Organization structure with org_id and name
- `Project` - Project structure with project_id and name
### `types/providers.ts`
AI provider definitions:
- `Provider` - Enum for supported AI providers
- `Category` - Enum for memory categories (BOOKMARK, NAVIGATION, SEARCH)
### `types/settings.ts`
User settings and preferences:
- `UserSettings` - User preference structure with API keys, memory settings, and thresholds
- `SidebarSettings` - Sidebar-specific settings with organization and project info
- `Settings` - Legacy settings structure for compatibility
### `types/storage.ts`
Chrome storage key definitions:
- `StorageKey` - Enum for all storage keys used in the extension
- `StorageItems` - Type mapping for storage values (required fields)
- `StorageData` - Type mapping for storage values (optional fields)
================================================
FILE: manifest.json
================================================
{
"manifest_version": 3,
"name": "OpenMemory",
"version": "1.3.17",
"description": "🧠 OpenMemory keeps your conversations in sync. 🔄 No more repeating yourself—just seamless AI collaboration! ✨",
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"permissions": ["storage", "activeTab", "contextMenus", "scripting", "webNavigation"],
"host_permissions": [
"https://api.mem0.ai/*",
"https://app.mem0.ai/*",
"https://claude.ai/*"
],
"action": {
"default_popup": "src/popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"background": {
"service_worker": "src/background.ts",
"type": "module"
},
"content_scripts": [
{
"matches": ["https://claude.ai/*"],
"js": ["src/claude/content.ts"]
},
{
"matches": ["https://chat.openai.com/*", "https://chatgpt.com/*"],
"js": ["src/chatgpt/content.ts"]
},
{
"matches": ["https://www.perplexity.ai/*"],
"js": ["src/perplexity/content.ts"]
},
{
"matches": ["https://app.mem0.ai/*"],
"js": ["src/mem0/content.ts"]
},
{
"matches": ["https://grok.com/*", "https://x.com/i/grok*"],
"js": ["src/grok/content.ts"]
},
{
"matches": ["https://chat.deepseek.com/*"],
"js": ["src/deepseek/content.ts"]
},
{
"matches": ["https://gemini.google.com/*"],
"js": ["src/gemini/content.ts"]
},
{
"matches": ["https://replit.com/*"],
"js": ["src/replit/content.ts"]
},
{
"matches": ["<all_urls>"],
"js": ["src/sidebar.ts"],
"run_at": "document_end"
},
{
"matches": ["<all_urls>"],
"js": ["src/selection_context.ts"],
"run_at": "document_idle",
"all_frames": true
}
,
{
"matches": ["<all_urls>"],
"js": ["src/search_tracker.ts"],
"run_at": "document_idle"
}
],
"web_accessible_resources": [
{
"resources": ["icons/*"],
"matches": ["<all_urls>"]
}
]
}
================================================
FILE: package.json
================================================
{
"name": "mem0-extension",
"description": "🧠 OpenMemory keeps your conversations in sync. 🔄 No more repeating yourself—just seamless AI collaboration! ✨",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"lint": "eslint src --ext .ts,.tsx --fix",
"lint:check": "eslint src --ext .ts,.tsx",
"format": "prettier --write src/**/*.{ts,html}",
"format:check": "prettier --check src/**/*.{ts,html}",
"type-check": "tsc --noEmit"
},
"devDependencies": {
"@crxjs/vite-plugin": "^2.2.0",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"chrome-types": "^0.1.375",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.1.3",
"prettier": "^3.3.3",
"typescript": "^5.9.2",
"vite": "^7.1.4"
}
}
================================================
FILE: privacy-policy.md
================================================
# Privacy Policy for Claude Memory
Last updated: 09-06-2024
## Introduction
Welcome to Claude Memory ("we", "our", or "us"). We are committed to protecting your personal information and your right to privacy. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you use our Chrome extension.
## Information We Collect
We may collect the following types of information:
Personal Information: Full name and email address through which you sign up on the Mem0 platform.
Usage Data: Every time you use the extension, we collect the message that you are sending to Claude.
## How We Use Your Information
We use the collected information for various purposes, including:
- To provide and maintain our extension
- To notify you about changes to our extension
- To provide customer support
- To gather analysis or valuable information so that we can improve our extension
- To monitor the usage of our extension
- To detect, prevent and address technical issues
## Data Retention
We will retain your information for as long as necessary to fulfill the purposes outlined in this Privacy Policy, unless a longer retention period is required or permitted by law.
## Data Security
We use commercially acceptable means to protect your personal information. However, no method of transmission over the Internet or method of electronic storage is 100% secure.
## Your Data Protection Rights
Depending on your location, you may have certain rights regarding your personal information, such as the right to access, correct, or delete your data. Please contact us to exercise these rights.
## Changes to This Privacy Policy
We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page and updating the "Last updated" date.
## Contact Us
If you have any questions about this Privacy Policy, please contact us at: deshraj@mem0.ai
================================================
FILE: src/background.ts
================================================
import { initContextMenuMemory } from './context-menu-memory';
import { initDirectUrlTracking } from './direct-url-tracker';
import { type OpenDashboardMessage, SidebarAction } from './types/messages';
chrome.runtime.onInstalled.addListener(() => {
chrome.storage.sync.set({ memory_enabled: true }, () => {
console.log('Memory enabled set to true on install/update');
});
});
chrome.runtime.onMessage.addListener((request: OpenDashboardMessage) => {
if (request.action === SidebarAction.OPEN_DASHBOARD && request.url) {
chrome.tabs.create({ url: request.url });
}
return undefined;
});
chrome.runtime.onMessage.addListener(
(request: { action?: string }, sender: chrome.runtime.MessageSender) => {
if (request.action === SidebarAction.SIDEBAR_SETTINGS) {
const tabId = sender.tab?.id;
if (tabId !== null && tabId !== undefined) {
chrome.tabs.sendMessage(tabId, { action: SidebarAction.SIDEBAR_SETTINGS });
}
}
return undefined;
}
);
initContextMenuMemory();
initDirectUrlTracking();
================================================
FILE: src/chatgpt/content.ts
================================================
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { DEFAULT_USER_ID, type LoginData, MessageRole } from '../types/api';
import type { HistoryStateData } from '../types/browser';
import type { MemoryItem, MemorySearchItem, OptionalApiParams } from '../types/memory';
import { SidebarAction } from '../types/messages';
import { type StorageData, StorageKey } from '../types/storage';
import { createOrchestrator, type SearchStorage } from '../utils/background_search';
import { OPENMEMORY_PROMPTS } from '../utils/llm_prompts';
import { SITE_CONFIG } from '../utils/site_config';
import { getBrowser, sendExtensionEvent } from '../utils/util_functions';
import { OPENMEMORY_UI } from '../utils/util_positioning';
export {};
let isProcessingMem0: boolean = false;
// Initialize the MutationObserver variable
let observer: MutationObserver;
let memoryModalShown: boolean = false;
// Global variable to store all memories
let allMemories: string[] = [];
// Track added memories by ID
const allMemoriesById: Set<string> = new Set<string>();
// Reference to the modal overlay for updates
let currentModalOverlay: HTMLDivElement | null = null;
// Store dragged position
let draggedPosition: { top: number; left: number } | null = null;
let inputValueCopy: string = '';
let currentModalSourceButtonId: string | null = null;
const chatgptSearch = createOrchestrator({
fetch: async function (query: string, opts: { signal?: AbortSignal }) {
const data = await new Promise<SearchStorage>(resolve => {
chrome.storage.sync.get(
[
StorageKey.API_KEY,
StorageKey.USER_ID_CAMEL,
StorageKey.ACCESS_TOKEN,
StorageKey.SELECTED_ORG,
StorageKey.SELECTED_PROJECT,
StorageKey.USER_ID,
StorageKey.SIMILARITY_THRESHOLD,
StorageKey.TOP_K,
],
function (items) {
resolve(items as SearchStorage);
}
);
});
const apiKey = data[StorageKey.API_KEY];
const accessToken = data[StorageKey.ACCESS_TOKEN];
if (!apiKey && !accessToken) {
return [];
}
const authHeader = accessToken ? `Bearer ${accessToken}` : `Token ${apiKey}`;
const userId =
data[StorageKey.USER_ID_CAMEL] || data[StorageKey.USER_ID] || 'chrome-extension-user';
const threshold =
data[StorageKey.SIMILARITY_THRESHOLD] !== undefined
? data[StorageKey.SIMILARITY_THRESHOLD]
: 0.1;
const topK = data[StorageKey.TOP_K] !== undefined ? data[StorageKey.TOP_K] : 10;
const optionalParams: OptionalApiParams = {};
if (data[StorageKey.SELECTED_ORG]) {
optionalParams.org_id = data[StorageKey.SELECTED_ORG];
}
if (data[StorageKey.SELECTED_PROJECT]) {
optionalParams.project_id = data[StorageKey.SELECTED_PROJECT];
}
const payload = {
query,
filters: { user_id: userId },
rerank: true,
threshold: threshold,
top_k: topK,
filter_memories: false,
source: 'OPENMEMORY_CHROME_EXTENSION',
...optionalParams,
};
const res = await fetch('https://api.mem0.ai/v2/memories/search/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: authHeader,
},
body: JSON.stringify(payload),
signal: opts && opts.signal,
});
if (!res.ok) {
throw new Error(`API request failed with status ${res.status}`);
}
return await res.json();
},
// Don't render on prefetch. When modal is open, update it.
onSuccess: function (normQuery: string, responseData: MemorySearchItem[]) {
if (!memoryModalShown) {
return;
}
const memoryItems = ((responseData as MemorySearchItem[]) || []).map(
(item: MemorySearchItem) => ({
id: String(item.id),
text: item.memory,
categories: item.categories || [],
})
);
createMemoryModal(memoryItems, false, currentModalSourceButtonId);
},
onError: function () {
if (memoryModalShown) {
createMemoryModal([], false, currentModalSourceButtonId);
}
},
minLength: 3,
debounceMs: 75,
cacheTTL: 60000,
});
function createMemoryModal(
memoryItems: MemoryItem[],
isLoading: boolean = false,
sourceButtonId: string | null = null
): void {
// Close existing modal if it exists
if (memoryModalShown && currentModalOverlay) {
document.body.removeChild(currentModalOverlay);
}
memoryModalShown = true;
let currentMemoryIndex = 0;
// Calculate modal dimensions (estimated)
const modalWidth = 447;
let modalHeight = 400; // Default height
let memoriesPerPage = 3; // Default number of memories per page
let topPosition: number = 0;
let leftPosition: number = 0;
// Use dragged position if available, otherwise calculate based on button
if (draggedPosition) {
topPosition = draggedPosition.top;
leftPosition = draggedPosition.left;
} else if (sourceButtonId === 'mem0-icon-button') {
// Position relative to the mem0-icon-button (in the input area)
const iconButton = document.querySelector('#mem0-icon-button');
if (iconButton) {
const buttonRect = iconButton.getBoundingClientRect();
// Determine if there's enough space above the button
const spaceAbove = buttonRect.top;
const viewportHeight = window.innerHeight;
// Calculate position - for icon button, prefer to show ABOVE
leftPosition = buttonRect.left - modalWidth + buttonRect.width;
// Make sure modal doesn't go off-screen to the left
leftPosition = Math.max(leftPosition, 10);
// For icon button, show above if enough space, otherwise below
if (spaceAbove >= modalHeight + 10) {
// Place above
topPosition = buttonRect.top - modalHeight - 10;
} else {
// Not enough space above, place below
topPosition = buttonRect.bottom + 10;
// Check if it's in the lower half of the screen
if (buttonRect.bottom > viewportHeight / 2) {
modalHeight = 300; // Reduced height
memoriesPerPage = 2; // Show only 2 memories
}
}
} else {
// Fallback to input-based positioning
positionRelativeToInput();
}
} else if (sourceButtonId === 'sync-button') {
// Position relative to the sync button
const syncButton = document.querySelector('#sync-button');
if (syncButton) {
const buttonRect = syncButton.getBoundingClientRect();
const viewportHeight = window.innerHeight;
// Position below the sync button by default
leftPosition = buttonRect.left;
topPosition = buttonRect.bottom + 10;
// Check if it's in the lower half of the screen
if (buttonRect.bottom > viewportHeight / 2) {
modalHeight = 300; // Reduced height
memoriesPerPage = 2; // Show only 2 memories
}
// Make sure modal doesn't go off-screen to the right
leftPosition = Math.min(leftPosition, window.innerWidth - modalWidth - 10);
} else {
// Fallback to input-based positioning
positionRelativeToInput();
}
} else {
// Default positioning relative to the input field
positionRelativeToInput();
}
// Helper function to position modal relative to input field
function positionRelativeToInput() {
const inputElement =
document.querySelector('#prompt-textarea') ||
document.querySelector('div[contenteditable="true"]') ||
document.querySelector('textarea');
if (!inputElement) {
console.error('Input element not found');
return;
}
// Get the position and dimensions of the input field
const inputRect = inputElement.getBoundingClientRect();
// Determine if there's enough space below the input field
const viewportHeight = window.innerHeight;
const spaceBelow = viewportHeight - inputRect.bottom;
// Position the modal aligned to the right of the input
leftPosition = Math.max(inputRect.right - 20 - modalWidth, 10); // 20px offset from right edge
// Decide whether to place modal above or below based on available space
if (spaceBelow >= modalHeight) {
// Place below the input
topPosition = inputRect.bottom + 10;
// Check if it's in the lower half of the screen
if (inputRect.bottom > viewportHeight / 2) {
modalHeight = 300; // Reduced height
memoriesPerPage = 2; // Show only 2 memories
}
} else {
// Place above the input if not enough space below
topPosition = inputRect.top - modalHeight - 10;
}
}
// Create modal overlay
const modalOverlay = document.createElement('div');
modalOverlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: transparent;
display: flex;
z-index: 10000;
pointer-events: auto;
`;
// Save reference to current modal overlay
currentModalOverlay = modalOverlay;
// Add event listener to close modal when clicking outside
modalOverlay.addEventListener('click', event => {
// Only close if clicking directly on the overlay, not its children
if (event.target === modalOverlay) {
closeModal();
}
});
// Create modal container with positioning
const modalContainer = document.createElement('div');
modalContainer.style.cssText = `
background-color: #1C1C1E;
border-radius: 12px;
width: ${modalWidth}px;
height: ${modalHeight}px;
display: flex;
flex-direction: column;
color: white;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
position: absolute;
top: ${topPosition}px;
left: ${leftPosition}px;
pointer-events: auto;
border: 1px solid #27272A;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
overflow: hidden;
`;
// Create modal header
const modalHeader = document.createElement('div');
modalHeader.style.cssText = `
display: flex;
align-items: center;
padding: 10px 16px;
justify-content: space-between;
background-color: #232325;
flex-shrink: 0;
cursor: move;
user-select: none;
`;
// Create header left section with just the logo
const headerLeft = document.createElement('div');
headerLeft.style.cssText = `
display: flex;
flex-direction: row;
align-items: center;
`;
// Add Mem0 logo (updated to SVG)
const logoImg = document.createElement('img');
logoImg.src = chrome.runtime.getURL('icons/mem0-claude-icon.png');
logoImg.style.cssText = `
width: 26px;
height: 26px;
border-radius: 50%;
margin-right: 8px;
`;
// Add "OpenMemory" title
const title = document.createElement('div');
title.textContent = 'OpenMemory';
title.style.cssText = `
font-size: 16px;
font-weight: 600;
color: white;
`;
// Create header right section
const headerRight = document.createElement('div');
headerRight.style.cssText = `
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
`;
// Create Add to Prompt button with arrow
const addToPromptBtn = document.createElement('button');
addToPromptBtn.style.cssText = `
display: flex;
flex-direction: row;
align-items: center;
padding: 5px 16px;
gap: 8px;
background-color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
color: black;
`;
addToPromptBtn.textContent = 'Add to Prompt';
// Add arrow icon to button
const arrowIcon = document.createElement('span');
arrowIcon.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M5 12h14M12 5l7 7-7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`;
addToPromptBtn.appendChild(arrowIcon);
// (Removed) LLM button – auto-rerank is now handled on modal open
// Create settings button
const settingsBtn = document.createElement('button');
settingsBtn.style.cssText = `
background: none;
border: none;
cursor: pointer;
padding: 8px;
opacity: 0.6;
transition: opacity 0.2s;
`;
settingsBtn.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" xmlns="http://www.w3.org/2000/svg">
<path d="M12 15a3 3 0 100-6 3 3 0 000 6z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`;
// Add click event to open app.mem0.ai in a new tab
settingsBtn.addEventListener('click', () => {
if (currentModalOverlay && document.body.contains(currentModalOverlay)) {
document.body.removeChild(currentModalOverlay);
memoryModalShown = false;
currentModalOverlay = null;
}
chrome.runtime.sendMessage({ action: SidebarAction.SIDEBAR_SETTINGS });
});
// Add hover effect for the settings button
settingsBtn.addEventListener('mouseenter', () => {
settingsBtn.style.opacity = '1';
});
settingsBtn.addEventListener('mouseleave', () => {
settingsBtn.style.opacity = '0.6';
});
// Add drag functionality to modal header
let isDragging = false;
const dragOffset = { x: 0, y: 0 };
modalHeader.addEventListener('mousedown', (e: MouseEvent) => {
// Don't start dragging if clicking on buttons
const target = e.target as HTMLElement;
if (target?.closest('button')) {
return;
}
isDragging = true;
modalHeader.style.cursor = 'grabbing';
const modalRect = modalContainer.getBoundingClientRect();
dragOffset.x = e.clientX - modalRect.left;
dragOffset.y = e.clientY - modalRect.top;
e.preventDefault();
});
document.addEventListener('mousemove', e => {
if (!isDragging) {
return;
}
const newLeft = e.clientX - dragOffset.x;
const newTop = e.clientY - dragOffset.y;
// Constrain to viewport
const maxLeft = window.innerWidth - modalWidth;
const maxTop = window.innerHeight - modalHeight;
const constrainedLeft = Math.max(0, Math.min(newLeft, maxLeft));
const constrainedTop = Math.max(0, Math.min(newTop, maxTop));
modalContainer.style.left = `${constrainedLeft}px`;
modalContainer.style.top = `${constrainedTop}px`;
// Store the dragged position
draggedPosition = {
left: constrainedLeft,
top: constrainedTop,
};
e.preventDefault();
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
modalHeader.style.cursor = 'move';
}
});
// Content section
const contentSection = document.createElement('div');
const contentSectionHeight = modalHeight - 130; // Account for header and navigation
contentSection.style.cssText = `
display: flex;
flex-direction: column;
padding: 0 16px;
gap: 12px;
overflow: hidden;
flex: 1;
height: ${contentSectionHeight}px;
`;
// Create memories counter
const memoriesCounter = document.createElement('div');
memoriesCounter.style.cssText = `
font-size: 16px;
font-weight: 600;
color: #FFFFFF;
margin-top: 16px;
flex-shrink: 0;
`;
// Update counter text based on loading state and number of memories
if (isLoading) {
memoriesCounter.textContent = `Loading Relevant Memories...`;
} else {
memoriesCounter.textContent = `${memoryItems.length} Relevant Memories`;
}
// Calculate max height for memories content based on modal height
const memoriesContentMaxHeight = contentSectionHeight - 40; // Account for memories counter
// Create memories content container with adjusted height
const memoriesContent = document.createElement('div');
memoriesContent.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
overflow-y: auto;
flex: 1;
max-height: ${memoriesContentMaxHeight}px;
padding-right: 8px;
margin-right: -8px;
scrollbar-width: none;
-ms-overflow-style: none;
`;
memoriesContent.style.cssText += '::-webkit-scrollbar { display: none; }';
// Track currently expanded memory
let currentlyExpandedMemory: HTMLElement | null = null;
// Function to create skeleton loading items (adjusted for different heights)
function createSkeletonItems() {
memoriesContent.innerHTML = '';
for (let i = 0; i < memoriesPerPage; i++) {
const skeletonItem = document.createElement('div');
skeletonItem.style.cssText = `
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
padding: 12px;
background-color: #27272A;
border-radius: 8px;
height: 72px;
flex-shrink: 0;
animation: pulse 1.5s infinite ease-in-out;
`;
const skeletonText = document.createElement('div');
skeletonText.style.cssText = `
background-color: #383838;
border-radius: 4px;
height: 14px;
width: 85%;
margin-bottom: 8px;
`;
const skeletonText2 = document.createElement('div');
skeletonText2.style.cssText = `
background-color: #383838;
border-radius: 4px;
height: 14px;
width: 65%;
`;
const skeletonActions = document.createElement('div');
skeletonActions.style.cssText = `
display: flex;
gap: 4px;
margin-left: 10px;
`;
const skeletonButton1 = document.createElement('div');
skeletonButton1.style.cssText = `
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #383838;
`;
const skeletonButton2 = document.createElement('div');
skeletonButton2.style.cssText = `
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #383838;
`;
skeletonActions.appendChild(skeletonButton1);
skeletonActions.appendChild(skeletonButton2);
const textContainer = document.createElement('div');
textContainer.style.cssText = `
display: flex;
flex-direction: column;
flex-grow: 1;
`;
textContainer.appendChild(skeletonText);
textContainer.appendChild(skeletonText2);
skeletonItem.appendChild(textContainer);
skeletonItem.appendChild(skeletonActions);
memoriesContent.appendChild(skeletonItem);
}
// Add keyframe animation to document if not exists
if (!document.getElementById('skeleton-animation')) {
const style = document.createElement('style');
style.id = 'skeleton-animation';
style.innerHTML = `
@keyframes pulse {
0% { opacity: 0.6; }
50% { opacity: 0.8; }
100% { opacity: 0.6; }
}
`;
document.head.appendChild(style);
}
}
// Function to expand memory
function expandMemory(
memoryContainer: HTMLDivElement,
memoryText: HTMLDivElement,
contentWrapper: HTMLDivElement,
removeButton: HTMLButtonElement,
isExpanded: { value: boolean }
) {
if (currentlyExpandedMemory && currentlyExpandedMemory !== memoryContainer) {
currentlyExpandedMemory.dispatchEvent(new Event('collapse'));
}
isExpanded.value = true;
memoryText.style.webkitLineClamp = 'unset';
memoryText.style.height = 'auto';
contentWrapper.style.overflowY = 'auto';
contentWrapper.style.maxHeight = '240px'; // Limit height to prevent overflow
contentWrapper.style.scrollbarWidth = 'none';
// contentWrapper.style.msOverflowStyle is non-standard; omit to satisfy TS
contentWrapper.style.cssText += '::-webkit-scrollbar { display: none; }';
memoryContainer.style.backgroundColor = '#1C1C1E';
memoryContainer.style.maxHeight = '300px'; // Allow expansion but within container
memoryContainer.style.overflow = 'hidden';
removeButton.style.display = 'flex';
currentlyExpandedMemory = memoryContainer;
// Scroll to make expanded memory visible if needed
memoriesContent.scrollTop = memoryContainer.offsetTop - memoriesContent.offsetTop;
}
// Function to collapse memory
function collapseMemory(
memoryContainer: HTMLDivElement,
memoryText: HTMLDivElement,
contentWrapper: HTMLDivElement,
removeButton: HTMLButtonElement,
isExpanded: { value: boolean }
) {
isExpanded.value = false;
memoryText.style.webkitLineClamp = '2';
memoryText.style.height = '42px';
contentWrapper.style.overflowY = 'visible';
memoryContainer.style.backgroundColor = '#27272A';
memoryContainer.style.maxHeight = '72px';
memoryContainer.style.overflow = 'hidden';
removeButton.style.display = 'none';
currentlyExpandedMemory = null;
}
// Function to show memories with adjusted count based on modal position
function showMemories() {
memoriesContent.innerHTML = '';
if (isLoading) {
createSkeletonItems();
return;
}
if (memoryItems.length === 0) {
showEmptyState();
// Disable navigation buttons when there are no memories
updateNavigationState(0, 0);
return;
}
// Use the dynamically set memoriesPerPage value
const memoriesToShow = Math.min(memoriesPerPage, memoryItems.length);
// Calculate total pages and current page
const totalPages = Math.ceil(memoryItems.length / memoriesToShow);
const currentPage = Math.floor(currentMemoryIndex / memoriesToShow) + 1;
// Update navigation buttons state
updateNavigationState(currentPage, totalPages);
for (let i = 0; i < memoriesToShow; i++) {
const memoryIndex = currentMemoryIndex + i;
if (memoryIndex >= memoryItems.length) {
break;
} // Stop if we've reached the end
const memory = memoryItems[memoryIndex]!;
// Skip memories that have been added already
if (allMemoriesById.has(String(memory.id))) {
continue;
}
// Ensure memory has an ID
if (!memory.id) {
memory.id = `memory-${Date.now()}-${memoryIndex}`;
}
const memoryContainer = document.createElement('div');
memoryContainer.style.cssText = `
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
padding: 12px;
background-color: #27272A;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
min-height: 72px;
max-height: 72px;
overflow: hidden;
flex-shrink: 0;
`;
const memoryText = document.createElement('div');
memoryText.style.cssText = `
font-size: 14px;
line-height: 1.5;
color: #D4D4D8;
flex-grow: 1;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
transition: all 0.2s ease;
height: 42px; /* Height for 2 lines of text */
`;
memoryText.textContent = memory.text || '';
const actionsContainer = document.createElement('div');
actionsContainer.style.cssText = `
display: flex;
gap: 4px;
margin-left: 10px;
flex-shrink: 0;
`;
// Add button
const addButton = document.createElement('button');
addButton.style.cssText = `
border: none;
cursor: pointer;
padding: 4px;
background:rgb(66, 66, 69);
color:rgb(199, 199, 201);
border-radius: 100%;
transition: all 0.2s ease;
`;
addButton.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`;
// Add click handler for add button
addButton.addEventListener('click', (e: MouseEvent) => {
e.stopPropagation();
sendExtensionEvent('memory_injection', {
provider: 'chatgpt',
source: 'OPENMEMORY_CHROME_EXTENSION',
browser: getBrowser(),
injected_all: false,
memory_id: memory.id,
});
// Add this memory
allMemoriesById.add(String(memory.id));
allMemories.push(String(memory.text || ''));
updateInputWithMemories();
// Remove this memory from the list
const index = memoryItems.findIndex((m: MemoryItem) => m.id === memory.id);
if (index !== -1) {
memoryItems.splice(index, 1);
// Recalculate pagination after removing an item
// If we're on a page that's now empty, go to previous page
if (currentMemoryIndex > 0 && currentMemoryIndex >= memoryItems.length) {
currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage);
}
memoriesCounter.textContent = `${memoryItems.length} Relevant Memories`;
showMemories();
}
});
// Menu button
const menuButton = document.createElement('button');
menuButton.style.cssText = `
background: none;
border: none;
cursor: pointer;
padding: 4px;
color: #A1A1AA;
`;
menuButton.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="2"/>
<circle cx="12" cy="5" r="2"/>
<circle cx="12" cy="19" r="2"/>
</svg>`;
// Track expanded state using object to maintain reference
const isExpanded = { value: false };
// Create remove button (hidden by default)
const removeButton = document.createElement('button');
removeButton.style.cssText = `
display: none;
align-items: center;
gap: 6px;
background:rgb(66, 66, 69);
color:rgb(199, 199, 201);
border-radius: 8px;
padding: 2px 4px;
border: none;
cursor: pointer;
font-size: 13px;
margin-top: 12px;
width: fit-content;
`;
removeButton.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Remove
`;
// Create content wrapper for text and remove button
const contentWrapper = document.createElement('div');
contentWrapper.style.cssText = `
display: flex;
flex-direction: column;
flex-grow: 1;
`;
contentWrapper.appendChild(memoryText);
contentWrapper.appendChild(removeButton);
memoryContainer.addEventListener('collapse', () => {
collapseMemory(memoryContainer, memoryText, contentWrapper, removeButton, isExpanded);
});
menuButton.addEventListener('click', (e: MouseEvent) => {
e.stopPropagation();
if (isExpanded.value) {
collapseMemory(memoryContainer, memoryText, contentWrapper, removeButton, isExpanded);
} else {
expandMemory(memoryContainer, memoryText, contentWrapper, removeButton, isExpanded);
}
});
// Add click handler for remove button
removeButton.addEventListener('click', (e: MouseEvent) => {
e.stopPropagation();
// Remove from memoryItems
const index = memoryItems.findIndex((m: MemoryItem) => m.id === memory.id);
if (index !== -1) {
memoryItems.splice(index, 1);
// Recalculate pagination after removing an item
// If we're on the last page and it's now empty, go to previous page
if (currentMemoryIndex > 0 && currentMemoryIndex >= memoryItems.length) {
currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage);
}
memoriesCounter.textContent = `${memoryItems.length} Relevant Memories`;
showMemories();
}
});
actionsContainer.appendChild(addButton);
actionsContainer.appendChild(menuButton);
memoryContainer.appendChild(contentWrapper);
memoryContainer.appendChild(actionsContainer);
memoriesContent.appendChild(memoryContainer);
// Add hover effect
memoryContainer.addEventListener('mouseenter', () => {
memoryContainer.style.backgroundColor = isExpanded.value ? '#18181B' : '#323232';
});
memoryContainer.addEventListener('mouseleave', () => {
memoryContainer.style.backgroundColor = isExpanded.value ? '#1C1C1E' : '#27272A';
});
}
// If after filtering for already added memories, there are no items to show,
// check if we need to go to previous page
if (memoriesContent.children.length === 0 && memoryItems.length > 0) {
if (currentMemoryIndex > 0) {
currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage);
showMemories();
} else {
showEmptyState();
}
}
}
// Function to show empty state
function showEmptyState(): void {
memoriesContent.innerHTML = '';
const emptyContainer = document.createElement('div');
emptyContainer.style.cssText = `
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32px 16px;
text-align: center;
flex: 1;
min-height: 200px;
`;
const emptyIcon = document.createElement('div');
emptyIcon.innerHTML = `<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#71717A" xmlns="http://www.w3.org/2000/svg">
<path d="M9 3H5a2 2 0 00-2 2v4m6-6h10a2 2 0 012 2v10a2 2 0 01-2 2h-4M3 21h4a2 2 0 002-2v-4m-6 6V9m18 12a9 9 0 11-18 0 9 9 0 0118 0z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`;
emptyIcon.style.marginBottom = '16px';
const emptyText = document.createElement('div');
emptyText.textContent = 'No relevant memories found';
emptyText.style.cssText = `
color: #71717A;
font-size: 14px;
font-weight: 500;
`;
emptyContainer.appendChild(emptyIcon);
emptyContainer.appendChild(emptyText);
memoriesContent.appendChild(emptyContainer);
}
// Update navigation button states
function updateNavigationState(currentPage: number, totalPages: number): void {
// If there are no memories or total pages is 0, disable both buttons
if (memoryItems.length === 0 || totalPages === 0) {
prevButton.disabled = true;
prevButton.style.opacity = '0.5';
prevButton.style.cursor = 'not-allowed';
nextButton.disabled = true;
nextButton.style.opacity = '0.5';
nextButton.style.cursor = 'not-allowed';
return;
}
if (currentPage <= 1) {
prevButton.disabled = true;
prevButton.style.opacity = '0.5';
prevButton.style.cursor = 'not-allowed';
} else {
prevButton.disabled = false;
prevButton.style.opacity = '1';
prevButton.style.cursor = 'pointer';
}
if (currentPage >= totalPages) {
nextButton.disabled = true;
nextButton.style.opacity = '0.5';
nextButton.style.cursor = 'not-allowed';
} else {
nextButton.disabled = false;
nextButton.style.opacity = '1';
nextButton.style.cursor = 'pointer';
}
}
// Navigation section at bottom
const navigationSection = document.createElement('div');
navigationSection.style.cssText = `
display: flex;
justify-content: center;
gap: 12px;
padding: 10px;
border-top: none;
flex-shrink: 0;
`;
// Navigation buttons
const prevButton = document.createElement('button');
prevButton.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 19l-7-7 7-7" stroke="#A1A1AA" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`;
prevButton.style.cssText = `
background: #27272A;
border: none;
border-radius: 50%;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background-color 0.2s;
`;
const nextButton = document.createElement('button');
nextButton.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 5l7 7-7 7" stroke="#A1A1AA" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`;
nextButton.style.cssText = prevButton.style.cssText;
// Add navigation button handlers
prevButton.addEventListener('click', () => {
if (currentMemoryIndex >= memoriesPerPage) {
currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage);
showMemories();
}
});
nextButton.addEventListener('click', () => {
if (currentMemoryIndex + memoriesPerPage < memoryItems.length) {
currentMemoryIndex = currentMemoryIndex + memoriesPerPage;
showMemories();
}
});
// Add hover effects
[prevButton, nextButton].forEach(button => {
button.addEventListener('mouseenter', () => {
if (!button.disabled) {
button.style.backgroundColor = '#323232';
}
});
button.addEventListener('mouseleave', () => {
if (!button.disabled) {
button.style.backgroundColor = '#27272A';
}
});
});
// Assemble modal
headerLeft.appendChild(logoImg);
headerLeft.appendChild(title);
headerRight.appendChild(addToPromptBtn);
headerRight.appendChild(settingsBtn);
// No LLM button; auto-rerank happens below if enabled
modalHeader.appendChild(headerLeft);
modalHeader.appendChild(headerRight);
contentSection.appendChild(memoriesCounter);
contentSection.appendChild(memoriesContent);
navigationSection.appendChild(prevButton);
navigationSection.appendChild(nextButton);
modalContainer.appendChild(modalHeader);
modalContainer.appendChild(contentSection);
modalContainer.appendChild(navigationSection);
modalOverlay.appendChild(modalContainer);
// Append to body
document.body.appendChild(modalOverlay);
// Show initial memories
showMemories();
// Function to close the modal
function closeModal() {
if (currentModalOverlay && document.body.contains(currentModalOverlay)) {
document.body.removeChild(currentModalOverlay);
}
currentModalOverlay = null;
memoryModalShown = false;
// Reset dragged position when modal is explicitly closed
draggedPosition = null;
}
// Update Add to Prompt button click handler
addToPromptBtn.addEventListener('click', () => {
// Only add memories that are not already added
const newMemories = memoryItems
.filter(memory => !allMemoriesById.has(String(memory.id)) && !memory.removed)
.map(memory => {
allMemoriesById.add(String(memory.id));
return String(memory.text || '');
});
sendExtensionEvent('memory_injection', {
provider: 'chatgpt',
source: 'OPENMEMORY_CHROME_EXTENSION',
browser: getBrowser(),
injected_all: true,
memory_count: newMemories.length,
});
// Add all new memories to allMemories
allMemories.push(...newMemories);
// Update the input with all memories
if (allMemories.length > 0) {
updateInputWithMemories();
closeModal();
} else {
// If no new memories were added but we have existing ones, just close
if (allMemoriesById.size > 0) {
closeModal();
}
}
// Remove all added memories from the memoryItems list
for (let i = memoryItems.length - 1; i >= 0; i--) {
if (allMemoriesById.has(String(memoryItems[i]?.id))) {
memoryItems.splice(i, 1);
}
}
});
}
// Shared function to update the input field with all collected memories
function updateInputWithMemories(): void {
const inputElement =
document.querySelector('#prompt-textarea') ||
document.querySelector('div[contenteditable="true"]') ||
document.querySelector('textarea');
if (inputElement && allMemories.length > 0) {
// Get the content without any existing memory wrappers
const baseContent = getContentWithoutMemories();
// Create the memory wrapper with all collected memories
let memoriesContent =
'<div id="mem0-wrapper" contenteditable="false" style="background-color: rgb(220, 252, 231); padding: 8px; border-radius: 4px; margin-top: 8px; margin-bottom: 8px;">';
memoriesContent += OPENMEMORY_PROMPTS.memory_header_html_strong;
// Add all memories to the content
allMemories.forEach((mem, idx) => {
const safe = (mem || '').toString();
memoriesContent += `<div data-mem0-idx="${idx}" style="user-select: text;">- ${safe}</div>`;
});
memoriesContent += '</div>';
// Add the final content to the input
if (inputElement.tagName.toLowerCase() === 'div') {
inputElement.innerHTML = `${baseContent}<div><br></div>${memoriesContent}`;
} else {
(inputElement as HTMLTextAreaElement).value = `${baseContent}\n${memoriesContent}`;
}
// Make only the wrapper non-editable; allow user to select/copy text inside
try {
const wrapper = document.getElementById('mem0-wrapper');
if (wrapper) {
wrapper.setAttribute('contenteditable', 'false');
wrapper.style.userSelect = 'text';
}
} catch {
// Ignore errors when setting contenteditable
}
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
}
}
// Function to get the content without any memory wrappers
function getContentWithoutMemories(message?: string): string {
if (typeof message === 'string') {
return message;
}
const inputElement =
(document.querySelector('#prompt-textarea') as HTMLTextAreaElement | HTMLDivElement) ||
(document.querySelector('div[contenteditable="true"]') as HTMLDivElement) ||
(document.querySelector('textarea') as HTMLTextAreaElement);
if (!inputElement) {
return '';
}
let content =
(inputElement as HTMLTextAreaElement)?.value ||
inputElement.textContent ||
(inputElement as HTMLDivElement).innerHTML;
if (
message &&
(!content ||
content.trim() ===
'<p data-placeholder="Ask anything" class="placeholder"><br class="ProseMirror-trailingBreak"></p>')
) {
content = message;
}
// Remove any memory wrappers
content = content.replace(/<div id="mem0-wrapper"[\s\S]*?<\/div>/g, '');
// Remove any memory headers using shared prompts (HTML and plain variants)
try {
const MEM0_PLAIN = OPENMEMORY_PROMPTS.memory_header_plain_regex;
const MEM0_HTML = OPENMEMORY_PROMPTS.memory_header_html_regex;
content = content.replace(MEM0_HTML, '');
content = content.replace(MEM0_PLAIN, '');
} catch {
// Ignore errors during re-initialization
}
// Clean up any leftover paragraph markers
content = content.replace(/<p><br class="ProseMirror-trailingBreak"><\/p><p>$/g, '');
// Replace <p> with nothing
content = content.replace(/<p>[\s\S]*?<\/p>/g, '');
return content.trim();
}
// Add an event listener for the send button to clear memories after sending
function addSendButtonListener(): void {
const sendButton = document.querySelector('#composer-submit-button') as HTMLButtonElement;
if (sendButton && !sendButton.dataset.mem0Listener) {
sendButton.dataset.mem0Listener = 'true';
sendButton.addEventListener('click', function () {
// Capture and save memory asynchronously
captureAndStoreMemory();
// Clear all memories after sending
setTimeout(() => {
allMemories = [];
allMemoriesById.clear();
}, 100);
});
// Also handle Enter key press
const inputElement =
(document.querySelector('#prompt-textarea') as HTMLTextAreaElement | HTMLDivElement) ||
(document.querySelector('div[contenteditable="true"]') as HTMLDivElement) ||
(document.querySelector('textarea') as HTMLTextAreaElement);
if (inputElement && !inputElement.dataset.mem0KeyListener) {
inputElement.dataset.mem0KeyListener = 'true';
(inputElement as HTMLElement).addEventListener('keydown', function (event: KeyboardEvent) {
// Check if Enter was pressed without Shift (standard send behavior)
inputValueCopy =
(inputElement as HTMLTextAreaElement)?.value ||
inputElement.textContent ||
inputValueCopy;
if (event.key === 'Enter' && !event.shiftKey) {
// Capture and save memory asynchronously
captureAndStoreMemory();
// Clear all memories after sending
setTimeout(() => {
allMemories = [];
allMemoriesById.clear();
}, 100);
}
});
}
}
}
// Function to capture and store memory asynchronously
function captureAndStoreMemory(): void {
// Get the message content
// id is prompt-textarea
const inputElement =
(document.querySelector('#prompt-textarea') as HTMLTextAreaElement | HTMLDivElement) ||
(document.querySelector('div[contenteditable="true"]') as HTMLDivElement) ||
(document.querySelector('textarea') as HTMLTextAreaElement) ||
(document.querySelector('textarea[data-virtualkeyboard="true"]') as HTMLTextAreaElement);
if (!inputElement) {
return;
}
// Get raw content from the input element
let message = inputElement.textContent || (inputElement as HTMLTextAreaElement)?.value;
if (!message || message.trim() === '') {
message = inputValueCopy;
}
if (!message || message.trim() === '') {
return;
}
// Clean the message of any memory wrapper content
message = getContentWithoutMemories(message);
// Skip if message is empty after cleaning
if (!message || message.trim() === '') {
return;
}
// Asynchronously store the memory
chrome.storage.sync.get(
[
StorageKey.API_KEY,
StorageKey.USER_ID_CAMEL,
StorageKey.ACCESS_TOKEN,
StorageKey.MEMORY_ENABLED,
StorageKey.SELECTED_ORG,
StorageKey.SELECTED_PROJECT,
StorageKey.USER_ID,
],
function (items) {
// Skip if memory is disabled or no credentials
if (
items[StorageKey.MEMORY_ENABLED] === false ||
(!items[StorageKey.API_KEY] && !items[StorageKey.ACCESS_TOKEN])
) {
return;
}
const authHeader = items[StorageKey.ACCESS_TOKEN]
? `Bearer ${items[StorageKey.ACCESS_TOKEN]}`
: `Token ${items[StorageKey.API_KEY]}`;
const userId =
items[StorageKey.USER_ID_CAMEL] || items[StorageKey.USER_ID] || DEFAULT_USER_ID;
// Get recent messages for context (if available)
const messages = getLastMessages(2);
messages.push({ role: MessageRole.User, content: message });
const optionalParams: OptionalApiParams = {};
if (items[StorageKey.SELECTED_ORG]) {
optionalParams.org_id = items[StorageKey.SELECTED_ORG];
}
if (items[StorageKey.SELECTED_PROJECT]) {
optionalParams.project_id = items[StorageKey.SELECTED_PROJECT];
}
// Send memory to mem0 API asynchronously without waiting for response
const storagePayload = {
messages: messages,
user_id: userId,
infer: true,
metadata: {
provider: 'ChatGPT',
},
source: 'OPENMEMORY_CHROME_EXTENSION',
...optionalParams,
};
fetch('https://api.mem0.ai/v1/memories/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: authHeader,
},
body: JSON.stringify(storagePayload),
}).catch(error => {
console.error('Error saving memory:', error);
});
}
);
}
async function updateNotificationDot(): Promise<void> {
const memoryEnabled = await getMemoryEnabledState();
if (!memoryEnabled) {
return;
}
const input =
document.querySelector('#prompt-textarea') ||
document.querySelector('div[contenteditable="true"]') ||
document.querySelector('textarea');
const host = document.getElementById('mem0-icon-button'); // shadow host
if (!input || !host) {
setTimeout(updateNotificationDot, 1000);
return;
}
const set = () => {
const txt = input.textContent || input.value || '';
host.setAttribute('data-has-text', txt.trim() ? '1' : '0');
};
const mo = new MutationObserver(set);
mo.observe(input, { childList: true, characterData: true, subtree: true });
input.addEventListener('input', set);
input.addEventListener('keyup', set);
input.addEventListener('focus', set);
set();
}
// Modified function to handle Mem0 modal instead of direct injection
async function handleMem0Modal(sourceButtonId: string | null = null): Promise<void> {
const memoryEnabled = await getMemoryEnabledState();
if (!memoryEnabled) {
return;
}
// Check if user is logged in
const loginData = await new Promise<LoginData>(resolve => {
chrome.storage.sync.get(
[StorageKey.API_KEY, StorageKey.USER_ID_CAMEL, StorageKey.ACCESS_TOKEN],
function (items) {
resolve(items);
}
);
});
// If no API key and no access token, show login popup
if (!loginData[StorageKey.API_KEY] && !loginData[StorageKey.ACCESS_TOKEN]) {
showLoginPopup();
return;
}
const mem0Button = document.querySelector('#mem0-icon-button') as HTMLElement;
let message = getInputValue();
// If no message, show a popup and return
if (!message || message.trim() === '') {
if (mem0Button) {
showButtonPopup(mem0Button as HTMLElement, 'Please enter some text first');
}
return;
}
try {
const MEM0_PLAIN = OPENMEMORY_PROMPTS.memory_header_plain_regex;
message = message.replace(MEM0_PLAIN, '').trim();
} catch {
// Ignore errors during re-initialization
}
const endIndex = message.indexOf('</p>');
if (endIndex !== -1) {
message = message.slice(0, endIndex + 4);
}
if (isProcessingMem0) {
return;
}
isProcessingMem0 = true;
// Show the loading modal immediately with the source button ID
createMemoryModal([], true, sourceButtonId);
try {
const data = await new Promise<StorageData>(resolve => {
chrome.storage.sync.get(
[
StorageKey.API_KEY,
StorageKey.USER_ID_CAMEL,
StorageKey.ACCESS_TOKEN,
StorageKey.SELECTED_ORG,
StorageKey.SELECTED_PROJECT,
StorageKey.USER_ID,
StorageKey.SIMILARITY_THRESHOLD,
StorageKey.TOP_K,
],
function (items) {
resolve(items);
}
);
});
const apiKey = data[StorageKey.API_KEY];
const accessToken = data[StorageKey.ACCESS_TOKEN];
if (!apiKey && !accessToken) {
isProcessingMem0 = false;
return;
}
sendExtensionEvent('modal_clicked', {
provider: 'chatgpt',
source: 'OPENMEMORY_CHROME_EXTENSION',
browser: getBrowser(),
});
const messages = getLastMessages(2);
messages.push({ role: MessageRole.User, content: message });
const optionalParams: OptionalApiParams = {};
if (data[StorageKey.SELECTED_ORG]) {
optionalParams.org_id = data[StorageKey.SELECTED_ORG];
}
if (data[StorageKey.SELECTED_PROJECT]) {
optionalParams.project_id = data[StorageKey.SELECTED_PROJECT];
}
currentModalSourceButtonId = sourceButtonId;
chatgptSearch.runImmediate(message);
} catch (error) {
console.error('Error:', error);
// Still show the modal but with empty state if there was an error
createMemoryModal([], false, sourceButtonId);
throw error;
} finally {
isProcessingMem0 = false;
}
}
// Function to show a small popup message near the button
function showButtonPopup(button: HTMLElement, message: string): void {
let host = button || document.getElementById('mem0-icon-button');
if (!host) {
return;
}
let root = host.shadowRoot || host;
// Remove any existing popups
const existingPopup = root.querySelector('.mem0-button-popup');
if (existingPopup) {
existingPopup.remove();
}
// Also hide any hover popover that might be showing
const hoverPopover = document.querySelector('.mem0-button-popover') as HTMLElement;
if (hoverPopover) {
hoverPopover.style.opacity = '0';
hoverPopover.style.display = 'none';
}
const popup = document.createElement('div');
popup.className = 'mem0-button-popup';
popup.style.cssText = `
position: absolute;
top: -40px;
left: 50%;
transform: translateX(-50%);
background-color: #1C1C1E;
border: 1px solid #27272A;
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
white-space: nowrap;
z-index: 10001;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
`;
popup.textContent = message;
// Create arrow
const arrow = document.createElement('div');
arrow.style.cssText = `
position: absolute;
bottom: -5px;
left: 50%;
transform: translateX(-50%) rotate(45deg);
width: 10px;
height: 10px;
background-color: #1C1C1E;
border-right: 1px solid #27272A;
border-bottom: 1px solid #27272A;
`;
popup.appendChild(arrow);
root.appendChild(popup);
setTimeout(function () {
if (popup.isConnected) {
popup.remove();
}
}, 3000);
// Position relative to button
// button.style.position = 'relative';
// button.appendChild(popup);
// // Auto-remove after 3 seconds
// setTimeout(() => {
// if (document.body.contains(popup)) {
// popup.remove();
// }
// }, 3000);
}
// Safe no-op to prevent ReferenceError if auto-inject prefetch isn't defined elsewhere
function setupAutoInjectPrefetch() {
try {
// Intentionally left blank; legacy callers expect this to exist.
// Inline hint handles lightweight suggestion awareness.
} catch {
// Ignore errors during re-initialization
}
}
(function () {
if (!OPENMEMORY_UI || !OPENMEMORY_UI.mountOnEditorFocus) {
return;
}
// 1) Try to mount immediately from cached anchor on page load (before focus)
try {
// Skip if already mounted (e.g., hot reload / rapid SPA replace)
if (!document.getElementById('mem0-icon-button')) {
OPENMEMORY_UI.resolveCachedAnchor(
{ learnKey: location.host + ':' + location.pathname },
null,
24 * 60 * 60 * 1000
)
.then(function (hit) {
if (!hit || !hit.el) {
return;
}
// Reuse the same render and placement as the focus-driven path
let hs = OPENMEMORY_UI.createShadowRootHost('mem0-root');
let host = hs.host,
shadow = hs.shadow;
host.id = 'mem0-icon-button';
let unplace = OPENMEMORY_UI.applyPlacement({
container: host,
anchor: hit.el,
placement: hit.placement || {
strategy: 'inline',
where: 'beforeend',
inlineAlign: 'end',
},
});
let style = document.createElement('style');
style.textContent = `
:host { position: relative; }
.mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center;
justify-content:center; width:32px; height:32px; border-radius:50%; }
.mem0-btn img { width:18px; height:18px; border-radius:50%; }
.dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px;
background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; }
:host([data-has-text="1"]) .dot { display:block; }
`;
let btn = document.createElement('button');
btn.className = 'mem0-btn';
let img = document.createElement('img');
img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png');
let dot = document.createElement('div');
dot.className = 'dot';
btn.appendChild(img);
shadow.append(style, btn, dot);
// Nudge to the left of mic if present in same anchor
try {
let mic =
hit.el &&
(hit.el.querySelector('button[aria-label="Dictate button"]') ||
hit.el.querySelector('button[aria-label*="mic" i]') ||
hit.el.querySelector('button[aria-label*="voice" i]'));
if (mic && hit.el) {
let child: Element | null = mic;
while (child && child.parentElement !== hit.el) {
child = child.parentElement;
}
if (child && child.parentElement === hit.el) {
hit.el.insertBefore(host, child);
}
}
} catch {
// Ignore errors during re-initialization
}
btn.addEventListener('click', function () {
handleMem0Modal('mem0-icon-button');
});
if (typeof updateNotificationDot === 'function') {
setTimeout(updateNotificationDot, 0);
}
// If the anchor disappears, allow normal focus flow to re-mount
const removal = new MutationObserver(function () {
if (!document.contains(hit.el) || !document.contains(host)) {
try {
unplace();
} catch {
// Ignore errors during re-initialization
}
try {
removal.disconnect();
} catch {
// Ignore errors during re-initialization
}
}
});
removal.observe(document.documentElement, { childList: true, subtree: true });
})
.catch(function () {
// Ignore errors during re-initialization
});
}
} catch {
// Ignore errors during re-initialization
}
// 2) Standard focus-driven mount
OPENMEMORY_UI.mountOnEditorFocus({
existingHostSelector: '#mem0-icon-button',
editorSelector:
typeof SITE_CONFIG !== 'undefined' &&
SITE_CONFIG.chatgpt &&
SITE_CONFIG.chatgpt.editorSelector
? SITE_CONFIG.chatgpt.editorSelector
: 'textarea, [contenteditable="true"], input[type="text"]',
deriveAnchor:
typeof SITE_CONFIG !== 'undefined' &&
SITE_CONFIG.chatgpt &&
typeof SITE_CONFIG.chatgpt.deriveAnchor === 'function'
? SITE_CONFIG.chatgpt.deriveAnchor
: function (editor) {
return editor.closest('form') || editor.parentElement;
},
placement:
typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.chatgpt && SITE_CONFIG.chatgpt.placement
? SITE_CONFIG.chatgpt.placement
: { strategy: 'inline', where: 'beforeend', inlineAlign: 'end' },
render: function (shadow: ShadowRoot, host: HTMLElement, anchor: Element | null) {
host.id = 'mem0-icon-button'; // existing code relies on this
let style = document.createElement('style');
style.textContent = `
:host { position: relative; }
.mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center;
justify-content:center; width:32px; height:32px; border-radius:50%; }
.mem0-btn img { width:18px; height:18px; border-radius:50%; }
.dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px;
background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; }
:host([data-has-text="1"]) .dot { display:block; }
`;
let btn = document.createElement('button');
btn.className = 'mem0-btn';
let img = document.createElement('img');
img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png');
let dot = document.createElement('div');
dot.className = 'dot';
btn.appendChild(img);
shadow.append(style, btn, dot);
try {
let cfg =
typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.chatgpt ? SITE_CONFIG.chatgpt : null;
let mic = null;
if (cfg && Array.isArray(cfg.adjacentTargets)) {
for (let i = 0; i < cfg.adjacentTargets.length; i++) {
let sel = cfg.adjacentTargets[i];
if (sel) {
mic = anchor && anchor.querySelector(sel);
if (mic) {
break;
}
}
}
}
if (mic && anchor) {
let child: Element | null = mic;
while (child && child.parentElement !== anchor) {
child = child.parentElement;
}
if (child && child.parentElement === anchor) {
anchor.insertBefore(host, child);
}
host.style.marginRight = ''; // rely on container gap
host.style.marginLeft = '';
} else {
host.style.marginLeft = '4px'; // mild fallback spacing
}
} catch (_e) {
// Ignore errors during re-initialization
}
btn.addEventListener('click', function () {
handleMem0Modal('mem0-icon-button');
});
if (typeof updateNotificationDot === 'function') {
setTimeout(updateNotificationDot, 0);
}
},
// Optional safety net if deriveAnchor fails
fallback: function () {
let cfg =
typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.chatgpt ? SITE_CONFIG.chatgpt : null;
OPENMEMORY_UI.mountResilient({
anchors: [
{
find: function () {
let sel =
(cfg && cfg.editorSelector) ||
'textarea, [contenteditable="true"], input[type="text"]';
let ed = document.querySelector(sel);
if (!ed) {
return null;
}
try {
return cfg && typeof cfg.deriveAnchor === 'function'
? cfg.deriveAnchor(ed)
: ed.closest('form') || ed.parentElement;
} catch (_) {
return ed.closest('form') || ed.parentElement;
}
},
},
],
placement: (cfg && cfg.placement) || {
strategy: 'inline',
where: 'beforeend',
inlineAlign: 'end',
},
enableFloatingFallback: true,
render: function (shadow: ShadowRoot, host: HTMLElement, anchor: Element | null) {
host.id = 'mem0-icon-button'; // host is the shadow root container
let style = document.createElement('style');
style.textContent = `
:host { position: relative; }
.mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center;
justify-content:center; width:32px; height:32px; border-radius:50%; }
.mem0-btn img { width:18px; height:18px; border-radius:50%; }
.dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px;
background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; }
:host([data-has-text="1"]) .dot { display:block; }
`;
let btn = document.createElement('button');
btn.className = 'mem0-btn';
let img = document.createElement('img');
img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png');
let dot = document.createElement('div');
dot.className = 'dot';
btn.appendChild(img);
shadow.append(style, btn, dot);
btn.addEventListener('click', function () {
handleMem0Modal('mem0-icon-button');
});
// Move host to the left of the mic inside the same toolbar container
try {
let cfg =
typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.chatgpt
? SITE_CONFIG.chatgpt
: null;
let mic = null;
if (cfg && Array.isArray(cfg.adjacentTargets)) {
for (let i = 0; i < cfg.adjacentTargets.length; i++) {
let sel = cfg.adjacentTargets[i];
if (sel) {
mic = anchor && anchor.querySelector(sel);
if (mic) {
break;
}
}
}
} else {
mic =
anchor &&
(anchor.querySelector('button[aria-label="Dictate button"]') ||
anchor.querySelector('button[aria-label*="mic" i]') ||
anchor.querySelector('button[aria-label*="voice" i]'));
}
if (mic && anchor) {
let child: Element | null = mic;
while (child && child.parentElement !== anchor) {
child = child.parentElement;
}
if (child && child.parentElement === anchor) {
anchor.insertBefore(host, child);
}
host.style.marginRight = ''; // rely on container gap
host.style.marginLeft = '';
} else {
host.style.marginLeft = '4px'; // mild fallback spacing
}
} catch (_e) {
// Ignore errors during re-initialization
}
if (typeof updateNotificationDot === 'function') {
setTimeout(updateNotificationDot, 0);
}
},
});
},
persistCache: true,
cacheTtlMs: 24 * 60 * 60 * 1000,
});
})();
function getLastMessages(count: number): Array<{ role: MessageRole; content: string }> {
const messageContainer = document.querySelector('.flex.flex-col.text-sm.md\\:pb-9');
if (!messageContainer) {
return [];
}
const messageElements = Array.from(messageContainer.children).reverse();
const messages: Array<{ role: MessageRole; content: string }> = [];
for (const element of messageElements) {
if (messages.length >= count) {
break;
}
const userElement = element.querySelector('[data-message-author-role="user"]');
const assistantElement = element.querySelector('[data-message-author-role="assistant"]');
if (userElement) {
const content = userElement.querySelector('.whitespace-pre-wrap')?.textContent?.trim() || '';
messages.unshift({ role: MessageRole.User, content });
} else if (assistantElement) {
const content = assistantElement.querySelector('.markdown')?.textContent?.trim() || '';
messages.unshift({ role: MessageRole.Assistant, content });
}
}
return messages;
}
function getInputValue(): string {
const inputElement =
(document.querySelector('#prompt-textarea') as HTMLTextAreaElement | HTMLDivElement) ||
(document.querySelector('div[contenteditable="true"]') as HTMLDivElement) ||
(document.querySelector('textarea') as HTMLTextAreaElement);
return inputElement
? inputElement.textContent || (inputElement as HTMLTextAreaElement)?.value || ''
: '';
}
let chatgptBackgroundSearchHandler: ((this: Element, ev: Event) => void) | null = null;
function hookBackgroundSearchTyping() {
const inputElement =
document.querySelector('#prompt-textarea') ||
document.querySelector('div[contenteditable="true"]') ||
document.querySelector('textarea');
if (!inputElement) {
return;
}
if (inputElement.dataset.mem0BackgroundHooked) {
return;
}
inputElement.dataset.mem0BackgroundHooked = 'true';
if (!chatgptBackgroundSearchHandler) {
chatgptBackgroundSearchHandler = function () {
const text = getInputValue() || '';
console.log("Background search for:", text);
chatgptSearch.setText(text);
};
}
inputElement.addEventListener(
'input',
chatgptBackgroundSearchHandler as (this: Element, ev: Event) => void
);
inputElement.addEventListener(
'keyup',
chatgptBackgroundSearchHandler as (this: Element, ev: Event) => void
);
}
function addSyncButton(): void {
const buttonContainer = document.querySelector('div.mt-5.flex.justify-end');
if (buttonContainer) {
let syncButton = document.querySelector('#sync-button') as HTMLButtonElement;
// If the syncButton does not exist, create it
if (!syncButton) {
syncButton = document.createElement('button');
syncButton.id = 'sync-button';
syncButton.className = 'btn relative btn-neutral mr-2';
syncButton.style.color = 'rgb(213, 213, 213)';
syncButton.style.backgroundColor = 'transparent';
syncButton.innerHTML =
'<div id="sync-button-content" class="flex items-center justify-center font-semibold">Sync Memory</div>';
syncButton.style.border = '1px solid rgb(213, 213, 213)';
syncButton.style.fontSize = '12px';
syncButton.style.fontWeight = '500';
// add margin right to syncButton
syncButton.style.marginRight = '8px';
const syncIcon = document.createElement('img');
syncIcon.src = chrome.runtime.getURL('icons/mem0-claude-icon.png');
syncIcon.style.width = '16px';
syncIcon.style.height = '16px';
syncIcon.style.marginRight = '8px';
syncButton.prepend(syncIcon);
syncButton.addEventListener('click', handleSyncClick);
syncButton.addEventListener('mouseenter', () => {
if (!syncButton!.disabled) {
syncButton!.style.filter = 'opacity(0.7)';
}
});
syncButton.addEventListener('mouseleave', () => {
if (!syncButton!.disabled) {
syncButton!.style.filter = 'opacity(1)';
}
});
}
if (!buttonContainer.contains(syncButton)) {
buttonContainer.insertBefore(syncButton, buttonContainer.firstChild);
}
// Update sync button state
const updateSyncButtonState = (): void => {
// Define when the sync button should be enabled or disabled
(syncButton as HTMLButtonElement).disabled = false; // For example, always enabled
// Update opacity or pointer events if needed
if ((syncButton as HTMLButtonElement).disabled) {
(syncButton as HTMLButtonElement).style.opacity = '0.5';
(syncButton as HTMLButtonElement).style.pointerEvents = 'none';
} else {
(syncButton as HTMLButtonElement).style.opacity = '1';
(syncButton as HTMLButtonElement).style.pointerEvents = 'auto';
}
};
updateSyncButtonState();
} else {
// If resetMemoriesButton or specificTable is not found, remove syncButton from DOM
const existingSyncButton = document.querySelector('#sync-button');
if (existingSyncButton && existingSyncButton.parentNode) {
existingSyncButton.parentNode.removeChild(existingSyncButton);
}
}
}
function handleSyncClick(): void {
getMemoryEnabledState().then(memoryEnabled => {
if (!memoryEnabled) {
const btn = document.querySelector('#sync-button') as HTMLElement;
if (btn) {
showSyncPopup(btn, 'Memory is disabled');
}
return;
}
const table = document.querySelector('table.w-full.border-separate.border-spacing-0');
const syncButton = document.querySelector('#sync-button') as HTMLButtonElement;
if (table && syncButton) {
const rows = table.querySelectorAll('tbody tr');
const memories: Array<{ role: string; content: string }> = [];
// Change sync button state to loading
setSyncButtonLoadingState(true);
let syncedCount = 0;
const totalCount = rows.length;
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 1 && cells[0]) {
const content =
cells[0].querySelector('div.whitespace-pre-wrap')?.textContent?.trim() || '';
const memory = {
role: MessageRole.User,
content: `Remember this about me: ${content}`,
};
memories.push(memory);
sendMemoryToMem0(memory, false)
.then(() => {
syncedCount++;
if (syncedCount === totalCount) {
showSyncPopup(syncButton, `${syncedCount} memories synced`);
setSyncButtonLoadingState(false);
// Open the modal with memories after syncing
// handleMem0Modal('sync-button');
}
})
.catch(() => {
if (syncedCount === totalCount) {
showSyncPopup(syncButton, `${syncedCount}/${totalCount} memories synced`);
setSyncButtonLoadingState(false);
// Open the modal with memories after syncing
// handleMem0Modal('sync-button');
}
});
}
});
sendMemoriesToMem0(memories)
.then(() => {
if (syncButton) {
showSyncPopup(syncButton, `${memories.length} memories synced`);
}
setSyncButtonLoadingState(false);
// Open the modal with memories after syncing
handleMem0Modal('sync-button');
})
.catch(error => {
console.error('Error syncing memories:', error);
if (syncButton) {
showSyncPopup(syncButton, 'Error syncing memories');
}
setSyncButtonLoadingState(false);
// Open the modal even if there was an error
handleMem0Modal('sync-button');
});
} else {
console.error('Table or Sync button not found');
}
});
}
// New function to send memories in batch
function sendMemoriesToMem0(memories: Array<{ role: string; content: string }>): Promise<void> {
return new Promise<void>((resolve, reject) => {
chrome.storage.sync.get(
[
StorageKey.API_KEY,
StorageKey.USER_ID_CAMEL,
StorageKey.ACCESS_TOKEN,
StorageKey.SELECTED_ORG,
StorageKey.SELECTED_PROJECT,
StorageKey.USER_ID,
],
function (items) {
if (items[StorageKey.API_KEY] || items[StorageKey.ACCESS_TOKEN]) {
const authHeader = items[StorageKey.ACCESS_TOKEN]
? `Bearer ${items[StorageKey.ACCESS_TOKEN]}`
: `Token ${items[StorageKey.API_KEY]}`;
const userId =
items[StorageKey.USER_ID_CAMEL] || items[StorageKey.USER_ID] || DEFAULT_USER_ID;
const optionalParams: OptionalApiParams = {};
if (items[StorageKey.SELECTED_ORG]) {
optionalParams.org_id = items[StorageKey.SELECTED_ORG];
}
if (items[StorageKey.SELECTED_PROJECT]) {
optionalParams.project_id = items[StorageKey.SELECTED_PROJECT];
}
fetch('https://api.mem0.ai/v1/memories/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: authHeader,
},
body: JSON.stringify({
messages: memories,
user_id: userId,
infer: true,
metadata: {
provider: 'ChatGPT',
},
source: 'OPENMEMORY_CHROME_EXTENSION',
...optionalParams,
}),
})
.then(response => {
if (!response.ok) {
reject(`Failed to add memories: ${response.status}`);
} else {
resolve();
}
})
.catch(error => reject(`Error sending memories to Mem0: ${error}`));
} else {
reject('API Key/Access Token not set');
}
}
);
});
}
function setSyncButtonLoadingState(isLoading: boolean): void {
const syncButton = document.querySelector('#sync-button') as HTMLButtonElement;
const syncButtonContent = document.querySelector('#sync-button-content') as HTMLElement;
if (syncButton) {
if (isLoading) {
syncButton.disabled = true;
syncButton.style.cursor = 'wait';
document.body.style.cursor = 'wait';
syncButton.style.opacity = '0.7';
if (syncButtonContent) {
syncButtonContent.textContent = 'Syncing...';
}
} else {
syncButton.disabled = false;
syncButton.style.cursor = 'pointer';
syncButton.style.opacity = '1';
document.body.style.cursor = 'default';
if (syncButtonContent) {
syncButtonContent.textContent = 'Sync Memory';
}
}
}
}
function showSyncPopup(button: HTMLElement, message: string): void {
const popup = document.createElement('div');
// Create and add the (i) icon
const infoIcon = document.createElement('span');
infoIcon.textContent = 'ⓘ ';
infoIcon.style.marginRight = '3px';
popup.appendChild(infoIcon);
popup.appendChild(document.createTextNode(message));
popup.style.cssText = `
position: absolute;
top: 50%;
left: -160px;
transform: translateY(-50%);
background-color: #171717;
color: white;
padding: 6px 8px;
border-radius: 6px;
font-size: 12px;
white-space: nowrap;
z-index: 1000;
`;
button.style.position = 'relative';
button.appendChild(popup);
setTimeout(() => {
popup.remove();
}, 3000);
}
function sendMemoryToMem0(
memory: { role: string; content: string },
infer: boolean = true
): Promise<void> {
return new Promise<void>((resolve, reject) => {
chrome.storage.sync.get(
[
StorageKey.API_KEY,
StorageKey.USER_ID_CAMEL,
StorageKey.ACCESS_TOKEN,
StorageKey.SELECTED_ORG,
StorageKey.SELECTED_PROJECT,
StorageKey.USER_ID,
],
function (items) {
if (items[StorageKey.API_KEY] || items[StorageKey.ACCESS_TOKEN]) {
const authHeader = items[StorageKey.ACCESS_TOKEN]
? `Bearer ${items[StorageKey.ACCESS_TOKEN]}`
: `Token ${items[StorageKey.API_KEY]}`;
const userId =
items[StorageKey.USER_ID_CAMEL] || items[StorageKey.USER_ID] || DEFAULT_USER_ID;
const optionalParams: OptionalApiParams = {};
if (items[StorageKey.SELECTED_ORG]) {
optionalParams.org_id = items[StorageKey.SELECTED_ORG];
}
if (items[StorageKey.SELECTED_PROJECT]) {
optionalParams.project_id = items[StorageKey.SELECTED_PROJECT];
}
fetch('https://api.mem0.ai/v1/memories/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: authHeader,
},
body: JSON.stringify({
messages: [{ content: memory.content, role: MessageRole.User }],
user_id: userId,
infer: infer,
metadata: {
provider: 'ChatGPT',
},
source: 'OPENMEMORY_CHROME_EXTENSION',
...optionalParams,
}),
})
.then(response => {
if (!response.ok) {
reject(`Failed to add memory: ${response.status}`);
} else {
resolve();
}
})
.catch(error => reject(`Error sending memory to Mem0: ${error}`));
} else {
reject('API Key/Access Token not set');
}
}
);
});
}
// Add this new function to get the memory_enabled state
function getMemoryEnabledState(): Promise<boolean> {
return new Promise<boolean>(resolve => {
chrome.storage.sync.get([StorageKey.MEMORY_ENABLED], function (result) {
resolve(result.memory_enabled !== false); // Default to true if not set
});
});
}
// Returns whether auto-inject is enabled (default: false if not present)
// (auto-inject helpers removed)
// Update the initialization function to add the Mem0 icon button but not intercept Enter key
function initializeMem0Integration(): void {
document.addEventListener('DOMContentLoaded', () => {
addSyncButton();
// (async () => await addMem0IconButton())();
addSendButtonListener();
// (async () => await updateNotificationDot())();
hookBackgroundSearchTyping();
setupAutoInjectPrefetch();
});
document.addEventListener('keydown', function (event) {
if (event.ctrlKey && event.key === 'm') {
event.preventDefault();
(async () => {
await handleMem0Modal('mem0-icon-button');
})();
}
});
// Remove global Enter interception previously added for auto-inject
observer = new MutationObserver(() => {
addSyncButton();
// (async () => await addMem0IconButton())();
addSendButtonListener();
// (async () => await updateNotificationDot())();
hookBackgroundSearchTyping();
setupAutoInjectPrefetch();
});
observer.observe(document.body, { childList: true, subtree: true });
// Add a MutationObserver to watch for changes in the DOM but don't intercept Enter key
const observerForUI = new MutationObserver(() => {
// (async () => await addMem0IconButton())();
addSendButtonListener();
// (async () => await updateNotificationDot())();
hookBackgroundSearchTyping();
setupAutoInjectPrefetch();
});
observerForUI.observe(document.body, {
childList: true,
subtree: true,
});
}
// (global auto-inject interceptors removed)
// Function to show login popup
function showLoginPopup() {
// First remove any existing popups
const existingPopup = document.querySelector('#mem0-login-popup');
if (existingPopup) {
existingPopup.remove();
}
// Create popup container
const popupOverlay = document.createElement('div');
popupOverlay.id = 'mem0-login-popup';
popupOverlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 10001;
`;
const popupContainer = document.createElement('div');
popupContainer.style.cssText = `
background-color: #1C1C1E;
border-radius: 12px;
width: 320px;
padding: 24px;
color: white;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
// Close button
const closeButton = document.createElement('button');
closeButton.style.cssText = `
position: absolute;
top: 16px;
right: 16px;
background: none;
border: none;
color: #A1A1AA;
font-size: 16px;
cursor: pointer;
`;
closeButton.innerHTML = '×';
closeButton.addEventListener('click', () => {
document.body.removeChild(popupOverlay);
});
// Logo and heading
const logoContainer = document.createElement('div');
logoContainer.style.cssText = `
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
`;
const logo = document.createElement('img');
logo.src = chrome.runtime.getURL('icons/mem0-claude-icon.png');
logo.style.cssText = `
width: 24px;
height: 24px;
border-radius: 50%;
margin-right: 12px;
`;
const logoDark = document.createElement('img');
logoDark.src = chrome.runtime.getURL('icons/mem0-icon-black.png');
logoDark.style.cssText = `
width: 24px;
height: 24px;
border-radius: 50%;
margin-right: 12px;
`;
const heading = document.createElement('h2');
heading.textContent = 'Sign in to OpenMemory';
heading.style.cssText = `
margin: 0;
font-size: 18px;
font-weight: 600;
`;
logoContainer.appendChild(heading);
// Message
const message = document.createElement('p');
message.textContent =
'Please sign in to access your memories and personalize your conversations!';
message.style.cssText = `
margin-bottom: 24px;
color: #D4D4D8;
font-size: 14px;
line-height: 1.5;
text-align: center;
`;
// Sign in button
const signInButton = document.createElement('button');
signInButton.style.cssText = `
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 10px;
background-color: white;
color: black;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
`;
// Add text in span for better centering
const signInText = document.createElement('span');
signInText.textContent = 'Sign in with Mem0';
signInButton.appendChild(logoDark);
signInButton.appendChild(signInText);
signInButton.addEventListener('mouseenter', () => {
signInButton.style.backgroundColor = '#f5f5f5';
});
signInButton.addEventListener('mouseleave', () => {
signInButton.style.backgroundColor = 'white';
});
// Open sign-in page when clicked
signInButton.addEventListener('click', () => {
window.open('https://app.mem0.ai/login', '_blank');
document.body.removeChild(popupOverlay);
});
// Assemble popup
popupContainer.appendChild(logoContainer);
popupContainer.appendChild(message);
popupContainer.appendChild(signInButton);
popupOverlay.appendChild(popupContainer);
popupOverlay.appendChild(closeButton);
// Add click event to close when clicking outside
popupOverlay.addEventListener('click', e => {
if (e.target === popupOverlay) {
document.body.removeChild(popupOverlay);
}
});
// Add to body
document.body.appendChild(popupOverlay);
}
initializeMem0Integration();
// --- SPA navigation handling and extension context guard (mirrors Claude) ---
let chatgptExtensionContextValid = true;
let chatgptCurrentUrl = window.location.href;
function chatgptCheckExtensionContext() {
try {
// chrome.runtime may throw if context invalidated
// Using optional chaining to avoid ReferenceError
// lastError exists only after an API call; treat presence of runtime as validity
const isValid = !!(chrome && chrome.runtime);
if (chatgptExtensionContextValid && !isValid) {
chatgptExtensionContextValid = false;
}
return isValid;
} catch {
chatgptExtensionContextValid = false;
return false;
}
}
function chatgptDetectNavigation() {
const newUrl = window.location.href;
if (newUrl !== chatgptCurrentUrl) {
chatgptCurrentUrl = newUrl;
// Re-initialize UI after small delay for DOM to settle
setTimeout(() => {
try {
addSyncButton();
// (async () => await addMem0IconButton())();
addSendButtonListener();
// (async () => await updateNotificationDot())();
} catch {
// Ignore errors when setting contenteditable
}
}, 300);
}
}
// Poll for SPA navigations and context validity
setInterval(() => {
chatgptCheckExtensionContext();
chatgptDetectNavigation();
}, 1000);
// Hook browser history navigation
window.addEventListener('popstate', () => setTimeout(chatgptDetectNavigation, 100));
const chatgptOriginalPushState = history.pushState;
history.pushState = function (data: HistoryStateData, unused: string, url?: string | URL | null) {
chatgptOriginalPushState.call(history, data, unused, url);
setTimeout(chatgptDetectNavigation, 100);
};
const chatgptOriginalReplaceState = history.replaceState;
history.replaceState = function (
data: HistoryStateData,
unused: string,
url?: string | URL | null
) {
chatgptOriginalReplaceState.call(history, data, unused, url);
setTimeout(chatgptDetectNavigation, 100);
};
================================================
FILE: src/claude/content.ts
================================================
import { MessageRole } from '../types/api';
import type { HistoryStateData } from '../types/browser';
import type { ExtendedDocument, ExtendedElement } from '../types/dom';
import type { MemoryItem, MemorySearchItem, OptionalApiParams } from '../types/memory';
import { SidebarAction } from '../types/messages';
import { type StorageData, StorageKey } from '../types/storage';
import { createOrchestrator, type SearchStorage } from '../utils/background_search';
import { OPENMEMORY_PROMPTS } from '../utils/llm_prompts';
import { SITE_CONFIG } from '../utils/site_config';
import { getBrowser, sendExtensionEvent } from '../utils/util_functions';
import { OPENMEMORY_UI, type Placement } from '../utils/util_positioning';
// Chrome runtime types
interface ChromeRuntimeLastError {
message?: string;
}
interface ChromeRuntimeWithLastError {
lastError?: ChromeRuntimeLastError;
}
interface ChromeRuntime extends ChromeRuntimeWithLastError {
// Add other chrome.runtime properties as needed
}
export {};
// Global variables to store all memories
let allMemories: string[] = [];
let memoryModalShown: boolean = false;
let isProcessingMem0: boolean = false;
let memoryEnabled: boolean = true;
// Cache of the latest typed text to avoid race when the editor is cleared
let lastTyped = '';
// Timestamp of when a send was initiated (to prevent duplicate fallback posts)
let lastSendInitiatedAt = 0;
let currentModalSourceButtonId: string | null = null;
const claudeSearch = createOrchestrator({
fetch: async function (query: string, opts: { signal?: AbortSignal }) {
const data = await new Promise<SearchStorage>(resolve => {
chrome.storage.sync.get(
[
StorageKey.API_KEY,
StorageKey.USER_ID_CAMEL,
StorageKey.ACCESS_TOKEN,
StorageKey.SELECTED_ORG,
StorageKey.SELECTED_PROJECT,
StorageKey.USER_ID,
StorageKey.SIMILARITY_THRESHOLD,
StorageKey.TOP_K,
],
function (items) {
resolve(items as SearchStorage);
}
);
});
const apiKey = data[StorageKey.API_KEY];
const accessToken = data[StorageKey.ACCESS_TOKEN];
if (!apiKey && !accessToken) {
return [];
}
const authHeader = accessToken ? `Bearer ${accessToken}` : `Token ${apiKey}`;
const userId =
data[StorageKey.USER_ID_CAMEL] || data[StorageKey.USER_ID] || 'chrome-extension-user';
const threshold =
data[StorageKey.SIMILARITY_THRESHOLD] !== undefined
? data[StorageKey.SIMILARITY_THRESHOLD]
: 0.1;
const topK = data[StorageKey.TOP_K] !== undefined ? data[StorageKey.TOP_K] : 10;
const optionalParams: OptionalApiParams = {};
if (data[StorageKey.SELECTED_ORG]) {
optionalParams.org_id = data[StorageKey.SELECTED_ORG];
}
if (data[StorageKey.SELECTED_PROJECT]) {
optionalParams.project_id = data[StorageKey.SELECTED_PROJECT];
}
// Clean query by stripping any appended memory header/content (debounced path)
const cleanQuery = (function () {
try {
const MEM0_PLAIN = OPENMEMORY_PROMPTS.memory_header_plain_regex;
return String(query).replace(MEM0_PLAIN, '').trim();
} catch (_e) {
return query;
}
})();
const payload = {
query: cleanQuery,
filters: { user_id: userId },
rerank: true,
threshold,
top_k: topK,
filter_memories: false,
source: 'OPENMEMORY_CHROME_EXTENSION',
...optionalParams,
};
const res = await fetch('https://api.mem0.ai/v2/memories/search/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: authHeader,
},
body: JSON.stringify(payload),
signal: opts && opts.signal,
});
if (!res.ok) {
throw new Error(`API request failed with status ${res.status}`);
}
return await res.json();
},
onSuccess: function (normQuery: string, responseData: MemorySearchItem[]) {
if (!memoryModalShown) {
return;
}
const memoryItems = (responseData || []).map((item: MemorySearchItem, index: number) => ({
id: String(item.id || `memory-${Date.now()}-${index}`),
text: item.memory,
categories: item.categories || [],
}));
createMemoryModal(memoryItems, false, currentModalSourceButtonId);
},
onError: function () {
if (memoryModalShown) {
createMemoryModal([], false, currentModalSourceButtonId);
}
},
minLength: 3,
debounceMs: 75,
cacheTTL: 60000,
});
// Sliding window for conversation context
let conversationHistory: Array<{ role: MessageRole; content: string; timestamp: number }> = [];
const MAX_CONVERSATION_HISTORY = 12; // Keep last 12 messages (6 pairs of user/assistant)
// Function to add message to conversation history with sliding window
function addToConversationHistory(role: MessageRole, content: string) {
if (!content || !content.trim()) {
return;
}
const trimmedContent = content.trim();
// Check for duplicate - don't add if the last message is identical
if (conversationHistory.length > 0) {
const lastMessage = conversationHistory[conversationHistory.length - 1];
if (lastMessage && lastMessage.role === role && lastMessage.content === trimmedContent) {
return;
}
}
const message = {
role: role,
content: trimmedContent,
timestamp: Date.now(),
};
// Add to history
conversationHistory.push(message);
// Maintain sliding window - remove oldest messages if we exceed limit
if (conversationHistory.length > MAX_CONVERSATION_HISTORY) {
conversationHistory.splice(0, conversationHistory.length - MAX_CONVERSATION_HISTORY);
}
}
// Function to get conversation context for memory creation
function getConversationContext(includeCurrent: boolean = true) {
if (conversationHistory.length === 0) {
return [];
}
// Get the last 6 messages for context (excluding current if requested)
const contextSize = 6;
let contextMessages = [...conversationHistory];
if (!includeCurrent && contextMessages.length > 0) {
// Remove the last message if it's the current user message
contextMessages = contextMessages.slice(0, -1);
}
// Get last N messages
const context = contextMessages.slice(-contextSize).map(msg => ({
role: msg.role,
content: msg.content,
}));
return context;
}
// Function to initialize conversation history from existing messages on page
function initializeConversationHistoryFromDOM() {
const messageContainer = document.querySelector(
'.flex-1.flex.flex-col.gap-3.px-4.max-w-3xl.mx-auto.w-full'
);
if (!messageContainer) {
return;
}
const messageElements = Array.from(messageContainer.children);
// Process existing messages in chronological order
messageElements.forEach(element => {
const userElement = element.querySelector('.font-user-message');
const assistantElement = element.querySelector('.font-claude-message');
if (userElement) {
const content = (userElement.textContent || '').trim();
if (content) {
addToConversationHistory(MessageRole.User, content);
}
} else if (assistantElement) {
const content = (assistantElement.textContent || '').trim();
if (content) {
addToConversationHistory(MessageRole.Assistant, content);
}
}
});
}
// Initialize the MutationObserver variable
// let observer: MutationObserver;
// let inputObserver: MutationObserver;
// let debounceTimer: number | undefined;
// Track added memories by ID
const allMemoriesById: Set<string> = new Set<string>();
// Reference to the modal overlay for updates
let currentModalOverlay:
| HTMLDivElement
| (HTMLDivElement & { _cleanupDragEvents?: () => void })
| null = null;
// Track the current modal container and placement cleanup
let currentModalContainer: HTMLDivElement | null = null;
let currentModalUnplace: (() => void) | null = null;
// Function to get memory enabled state from storage
async function getMemoryEnabledState() {
return new Promise(resolve => {
// Check if extension context is valid
if (!chrome || !chrome.storage || !chrome.storage.sync) {
console.log('⚠️ Chrome extension context invalid, defaulting to enabled');
resolve(true); // Default to enabled if we can't check
return;
}
try {
chrome.storage.sync.get(StorageKey.MEMORY_ENABLED, function (data) {
try {
// @ts-ignore
if (chrome.runtime && chrome.runtime.lastError) {
console.log(
'⚠️ Chrome storage error, defaulting to enabled:',
(chrome.runtime as ChromeRuntime).lastError
);
resolve(true); // Default to enabled if error
return;
}
console.log('🧠 Memory enabled from storage:', data.memory_enabled);
resolve(data.memory_enabled);
} catch {
// Ignore errors when checking chrome.runtime.lastError
}
});
} catch (error) {
console.log('⚠️ Exception getting memory state, defaulting to enabled:', error);
resolve(true); // Default to enabled if exception
}
});
}
// Function to remove mem0 button if it exists
function removeMemButton(): void {
const mem0Button = document.querySelector('#mem0-button');
if (mem0Button) {
const buttonContainer = mem0Button.closest('div');
if (buttonContainer) {
buttonContainer.remove();
} else {
mem0Button.remove();
}
}
// Also remove tooltip if it exists
const tooltip = document.querySelector('#mem0-tooltip');
if (tooltip) {
tooltip.remove();
}
}
// function addMem0Button(): void {
// // Legacy addMem0Button removed; OPENMEMORY_UI handles icon mounting
// }
function createPopup(container: HTMLElement, position: string = 'top'): HTMLElement {
const popup = document.createElement('div');
popup.className = 'mem0-popup';
let positionStyles = '';
if (position === 'top') {
positionStyles = `
bottom: 100%;
left: 50%;
transform: translateX(-40%);
margin-bottom: 11px;
`;
} else if (position === 'right') {
positionStyles = `
top: 50%;
left: 100%;
transform: translateY(-50%);
margin-left: 11px;
`;
}
popup.style.cssText = `
display: none;
position: absolute;
background-color: #21201C;
color: white;
padding: 6px 8px;
border-radius: 6px;
font-size: 12px;
z-index: 10000;
white-space: nowrap;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
${positionStyles}
`;
container.appendChild(popup);
return popup;
}
const sendButton = document.querySelector('button[aria-label="Send Message"]');
const sendUpButton = document.querySelector('button[aria-label="Send message"]');
const screenshotButton = document.querySelector('button[aria-label="Capture screenshot"]');
const inputToolsMenuButton = document.querySelector('#input-tools-menu-trigger');
// Check for any existing mem0 buttons before creating legacy button
const hasAnyMem0Button =
document.querySelector('#mem0-button') ||
document.getElementById('mem0-icon-button') ||
document.querySelector('[id*="mem0"], .mem0-btn');
if (inputToolsMenuButton && !hasAnyMem0Button) {
const buttonContainer = document.createElement('div');
buttonContainer.style.position = 'relative';
buttonContainer.style.display = 'inline-block';
const mem0Button = document.createElement('button');
mem0Button.id = 'mem0-button';
mem0Button.className = inputToolsMenuButton.className;
mem0Button.style.marginLeft = '0px';
mem0Button.setAttribute('aria-label', 'Add memories to your prompt');
const mem0Icon = document.createElement('img');
mem0Icon.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png');
mem0Icon.style.width = '16px';
mem0Icon.style.height = '16px';
mem0Icon.style.borderRadius = '50%';
const popup = createPopup(buttonContainer, 'top');
mem0Button.appendChild(mem0Icon);
mem0Button.addEventListener('click', () => {
if (memoryEnabled) {
// Hide the tooltip if it's showing
const tooltip = document.querySelector('#mem0-tooltip');
if (tooltip) {
tooltip.style.display = 'none';
}
handleMem0Modal(popup);
}
});
// Create notification dot
const notificationDot = document.createElement('div');
notificationDot.id = 'mem0-notification-dot';
notificationDot.style.cssText = `
position: absolute;
top: -3px;
right: -3px;
width: 10px;
height: 10px;
background-color: rgb(128, 221, 162);
border-radius: 50%;
border: 2px solid #1C1C1E;
display: none;
z-index: 1001;
pointer-events: none;
`;
mem0Button.appendChild(notificationDot);
// Add keyframe animation for the dot
if (!document.getElementById('notification-dot-animation')) {
const style = document.createElement('style');
style.id = 'notification-dot-animation';
style.innerHTML = `
@keyframes popIn {
0% { transform: scale(0); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
#mem0-notification-dot.active {
display: block !important;
animation: popIn 0.3s ease-out forwards;
}
`;
document.head.appendChild(style);
}
buttonContainer.appendChild(mem0Button);
const tooltip = document.createElement('div');
tooltip.id = 'mem0-tooltip';
tooltip.textContent = 'Add memories to your prompt';
tooltip.style.cssText = `
display: none;
position: fixed;
background-color: black;
color: white;
padding: 3px 7px;
border-radius: 6px;
font-size: 12px;
z-index: 10000;
pointer-events: none;
white-space: nowrap;
transform: translateX(-50%);
`;
document.body.appendChild(tooltip);
mem0Button.addEventListener('mouseenter', () => {
// Hide any existing popup first
const existingMem0Popup = document.querySelector('.mem0-popup[style*="display: block"]');
if (existingMem0Popup && existingMem0Popup !== popup) {
existingMem0Popup.style.display = 'none';
}
const rect = mem0Button.getBoundingClientRect();
const buttonCenterX = rect.left + rect.width / 2;
// Set initial tooltip properties
tooltip.style.display = 'block';
// Once displayed, we can get its height and set proper positioning
const tooltipHeight = (tooltip as HTMLElement).offsetHeight || 24; // Default height if not yet rendered
tooltip.style.left = `${buttonCenterX}px`;
tooltip.style.top = `${rect.top - tooltipHeight - 10}px`; // Position 10px above button
});
mem0Button.addEventListener('mouseleave', () => {
tooltip.style.display = 'none';
});
// Find the parent container to place the button at the same level as input-tools-menu
const parentContainer =
inputToolsMenuButton.closest('.relative.flex-1.flex.items-center.gap-2') ||
inputToolsMenuButton.closest('.relative.flex-1') ||
((inputToolsMenuButton.parentNode as HTMLElement)?.parentNode?.parentNode?.parentNode
?.parentNode as HTMLElement);
if (parentContainer) {
// Find the third position in the container - after the first two divs
// Looking for the flex-row div to insert before it
const flexRowDiv = parentContainer.querySelector('.flex.flex-row.items-center.gap-2.min-w-0');
// Find the tools div that we want to position after
const toolsDiv = inputToolsMenuButton.closest('div > div > div > div')?.parentNode?.parentNode;
// Make sure our button is the third div in the container
if (flexRowDiv && toolsDiv) {
// Insert right after the tools div and before the flex-row div
parentContainer.insertBefore(buttonContainer, flexRowDiv);
} else {
// Fallback to just append to the parent
parentContainer.appendChild(buttonContainer);
}
} else {
// Fallback to original behavior if parent not found
inputToolsMenuButton.parentNode?.insertBefore(
buttonContainer,
inputToolsMenuButton.nextSibling
);
}
// Update notification dot
updateNotificationDot();
} else if (
window.location.href.includes('claude.ai/new') &&
screenshotButton &&
!hasAnyMem0Button
) {
const buttonContainer = document.createElement('div');
buttonContainer.style.position = 'relative';
buttonContainer.style.display = 'inline-block';
const mem0Button = document.createElement('button');
mem0Button.id = 'mem0-button';
mem0Button.className = screenshotButton.className;
mem0Button.style.marginLeft = '0px';
mem0Button.setAttribute('aria-label', 'Add memories to your prompt');
const mem0Icon = document.createElement('img');
mem0Icon.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png');
mem0Icon.style.width = '16px';
mem0Icon.style.height = '16px';
mem0Icon.style.borderRadius = '50%';
const popup = createPopup(buttonContainer, 'right');
mem0Button.appendChild(mem0Icon);
mem0Button.addEventListener('click', () => {
if (memoryEnabled) {
// Hide the tooltip if it's showing
const tooltip = document.querySelector('#mem0-tooltip');
if (tooltip) {
tooltip.style.display = 'none';
}
handleMem0Modal(popup);
}
});
// Create notification dot
const notificationDot = document.createElement('div');
notificationDot.id = 'mem0-notification-dot';
notificationDot.style.cssText = `
position: absolute;
top: -3px;
right: -3px;
width: 10px;
height: 10px;
background-color: rgb(128, 221, 162);
border-radius: 50%;
border: 2px solid #1C1C1E;
display: none;
z-index: 1001;
pointer-events: none;
`;
mem0Button.appendChild(notificationDot);
buttonContainer.appendChild(mem0Button);
const tooltip = document.createElement('div');
tooltip.id = 'mem0-tooltip';
tooltip.textContent = 'Add memories to your prompt';
tooltip.style.cssText = `
display: none;
position: fixed;
background-color: black;
color: white;
padding: 3px 7px;
border-radius: 6px;
font-size: 12px;
z-index: 10000;
pointer-events: none;
white-space: nowrap;
transform: translateX(-50%);
`;
document.body.appendChild(tooltip);
mem0Button.addEventListener('mouseenter', () => {
// Hide any existing popup first
const existingMem0Popup = document.querySelector('.mem0-popup[style*="display: block"]');
if (existingMem0Popup && existingMem0Popup !== popup) {
existingMem0Popup.style.display = 'none';
}
const rect = mem0Button.getBoundingClientRect();
const buttonCenterX = rect.left + rect.width / 2;
// Set initial tooltip properties
tooltip.style.display = 'block';
// Once displayed, we can get its height and set proper positioning
const tooltipHeight = tooltip.offsetHeight || 24; // Default height if not yet rendered
tooltip.style.left = `${buttonCenterX}px`;
tooltip.style.top = `${rect.top - tooltipHeight - 10}px`; // Position 10px above button
});
mem0Button.addEventListener('mouseleave', () => {
tooltip.style.display = 'none';
});
screenshotButton.parentNode?.insertBefore(buttonContainer, screenshotButton.nextSibling);
// Update notification dot
updateNotificationDot();
} else if ((sendButton || sendUpButton) && !hasAnyMem0Button) {
const targetButton = sendButton || sendUpButton;
if (targetButton) {
// Find the parent container of the send button
const buttonParent = targetButton.parentNode;
if (buttonParent) {
const buttonContainer = document.createElement('div');
buttonContainer.style.position = 'relative';
buttonContainer.style.display = 'inline-block';
buttonContainer.style.marginRight = '12px';
const mem0Button = document.createElement('button');
mem0Button.id = 'mem0-button';
mem0Button.style.cssText = `
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
background: transparent;
border: none;
cursor: pointer;
border-radius: 8px;
position: relative;
transition: background-color 0.3s ease;
`;
mem0Button.setAttribute('aria-label', 'Add memories to your prompt');
const mem0Icon = document.createElement('img');
mem0Icon.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png');
mem0Icon.style.width = '20px';
mem0Icon.style.height = '20px';
mem0Icon.style.borderRadius = '50%';
// Create notification dot
const notificationDot = document.createElement('div');
notificationDot.id = 'mem0-notification-dot';
notificationDot.style.cssText = `
position: absolute;
top: 0px;
right: 0px;
width: 10px;
height: 10px;
background-color: rgb(128, 221, 162);
border-radius: 50%;
border: 2px solid #1C1C1E;
display: none;
z-index: 1001;
pointer-events: none;
`;
const popup = createPopup(buttonContainer, 'top');
mem0Button.appendChild(mem0Icon);
mem0Button.appendChild(notificationDot);
mem0Button.addEventListener('click', () => {
if (memoryEnabled) {
// Hide the tooltip if it's showing
const tooltip = document.querySelector('#mem0-tooltip');
if (tooltip) {
tooltip.style.display = 'none';
}
handleMem0Modal(popup);
}
});
const tooltip = document.createElement('div');
tooltip.id = 'mem0-tooltip';
tooltip.textContent = 'Add memories to your prompt';
tooltip.style.cssText = `
display: none;
position: fixed;
background-color: black;
color: white;
padding: 3px 7px;
border-radius: 6px;
font-size: 12px;
z-index: 10000;
pointer-events: none;
white-space: nowrap;
transform: translateX(-50%);
`;
document.body.appendChild(tooltip);
mem0Button.addEventListener('mouseenter', () => {
// Hide any existing popup first
const existingMem0Popup = document.querySelector('.mem0-popup[style*="display: block"]');
if (existingMem0Popup && existingMem0Popup !== popup) {
existingMem0Popup.style.display = 'none';
}
const rect = mem0Button.getBoundingClientRect();
const buttonCenterX = rect.left + rect.width / 2;
// Set initial tooltip properties
tooltip.style.display = 'block';
// Once displayed, we can get its height and set proper positioning
const tooltipHeight = tooltip.offsetHeight || 24; // Default height if not yet rendered
tooltip.style.left = `${buttonCenterX}px`;
tooltip.style.top = `${rect.top - tooltipHeight - 10}px`; // Position 10px above button
});
mem0Button.addEventListener('mouseleave', () => {
mem0Button.style.backgroundColor = 'transparent';
popup.style.display = 'none';
});
// Set popover text
popup.textContent = 'Add memories to your prompt';
buttonContainer.appendChild(mem0Button);
// Insert the button before the send button
if (buttonParent.querySelector('button[aria-label="Send message"]')) {
buttonParent.insertBefore(
buttonContainer,
buttonParent.querySelector('button[aria-label="Send message"]')
);
} else {
buttonParent.insertBefore(buttonContainer, targetButton);
}
// Update notification dot
updateNotificationDot();
}
}
}
// Send button listeners are now handled in initializeMem0Integration for better reliability
// Also handle Enter key press for sending messages
const inputElement =
document.querySelector('div[contenteditable="true"]') ||
document.querySelector('textarea') ||
document.querySelector('p[data-placeholder="How can I help you today?"]') ||
document.querySelector('p[data-placeholder="Reply to Claude..."]');
if (inputElement && !inputElement.dataset.mem0KeyListener) {
inputElement.dataset.mem0KeyListener = 'true';
inputElement.addEventListener('keydown', function (event: Event) {
const keyboardEvent = event as KeyboardEvent;
// Check if Enter was pressed without Shift (standard send behavior)
if (
keyboardEvent.key === 'Enter' &&
!keyboardEvent.shiftKey &&
!keyboardEvent.ctrlKey &&
!keyboardEvent.metaKey
) {
// Don't process for textarea which may want newlines
if (inputElement.tagName.toLowerCase() !== 'textarea') {
// Snapshot before send
const current = getInputValue();
if (current && current.trim() !== '') {
lastTyped = current;
}
lastSendInitiatedAt = Date.now();
// Capture and save memory asynchronously
captureAndStoreMemory(lastTyped);
// Clear all memories after sending
setTimeout(() => {
allMemories = [];
allMemoriesById.clear();
}, 100);
}
}
});
// Keep a live cache during typing to improve reliability
if (!inputElement.dataset.mem0CacheListener) {
inputElement.dataset.mem0CacheListener = 'true';
const updateCache = () => {
const val = getInputValue();
if (val && val.trim() !== '') {
lastTyped = val;
}
};
inputElement.addEventListener('input', updateCache, true);
inputElement.addEventListener('compositionend', updateCache, true);
}
}
// Update notification dot state
updateNotificationDot();
// Using MemoryItem from src/types/content-scripts.ts
function createMemoryModal(
memoryItems: MemoryItem[],
isLoading: boolean = false,
sourceButtonId: string | null = null
): void {
console.log('🎯 createMemoryModal called', {
memoryItems,
isLoading,
sourceButtonId,
memoryModalShown,
});
// Close existing modal if it exists
if (memoryModalShown) {
console.log('🗂️ Closing existing modal');
try {
if (typeof currentModalUnplace === 'function') {
currentModalUnplace();
}
} catch {
// Ignore error
}
if (currentModalContainer && currentModalContainer.isConnected) {
try {
currentModalContainer.remove();
} catch {
// Ignore error
}
}
if (currentModalOverlay && document.body.contains(currentModalOverlay)) {
document.body.removeChild(currentModalOverlay);
}
}
memoryModalShown = true;
console.log('✅ Modal state set to shown');
let currentMemoryIndex = 0;
// Calculate modal dimensions (estimated)
const modalWidth = 447;
let modalHeight = 400; // Default height
let memoriesPerPage = 3; // Default number of memories per page
// Resolve anchor element
function resolveAnchor() {
if (sourceButtonId) {
const byId = document.getElementById(sourceButtonId);
if (byId) {
return byId;
}
}
const iconBtn = document.querySelector('#mem0-icon-button');
if (iconBtn) {
return iconBtn;
}
return (
document.querySelector('div[contenteditable="true"]') ||
document.querySelector('textarea') ||
document.querySelector('p[data-placeholder="How can I help you today?"]') ||
document.querySelector('p[data-placeholder="Reply to Claude..."]')
);
}
const anchorEl = resolveAnchor();
// Choose placement side and adjust height/page density
let placementSide = 'bottom';
// let placementAlign = 'end';
if (anchorEl) {
const r = anchorEl.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (r.top >= modalHeight + 10) {
placementSide = 'top';
memoriesPerPage = 3;
} else {
placementSide = 'bottom';
if (r.bottom > viewportHeight / 2) {
modalHeight = 300;
memoriesPerPage = 2;
}
}
}
// Create modal overlay
const modalOverlay = document.createElement('div');
modalOverlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: transparent;
display: flex;
z-index: 10000;
pointer-events: auto;
`;
// Save reference to current modal overlay
currentModalOverlay = modalOverlay;
// Add event listener to close modal when clicking outside
modalOverlay.addEventListener('click', event => {
// Only close if clicking directly on the overlay, not its children
if (event.target === modalOverlay) {
closeModal();
}
});
// Create modal container (placement handled by OPENMEMORY_UI)
const modalContainer = document.createElement('div');
modalContainer.style.cssText = `
background-color: #1C1C1E;
border-radius: 12px;
width: ${modalWidth}px;
height: ${modalHeight}px;
display: flex;
flex-direction: column;
color: white;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
pointer-events: auto;
border: 1px solid #27272A;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
overflow: hidden;
`;
// Place near the anchor using the new positioning util
let unplace = () => {
// Ignore error
};
if (OPENMEMORY_UI && anchorEl) {
// Prefer SITE_CONFIG for Claude to dock within the composer box
let cfg = typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.claude ? SITE_CONFIG.claude : null;
let placementCfg = (cfg && cfg.placement) || {
strategy: 'dock' as const,
container: 'form',
side: (placementSide === 'top' ? 'top' : 'bottom') as 'top' | 'bottom',
align: 'start' as const,
gap: 8,
};
unplace = OPENMEMORY_UI.applyPlacement({
container: modalContainer,
anchor: anchorEl,
placement: placementCfg,
});
} else {
// Fallback placement
document.body.appendChild(modalContainer);
Object.assign(modalContainer.style, {
position: 'fixed',
left: '50%',
top: '20%',
transform: 'translateX(-50%)',
zIndex: '2147483647',
});
}
// Track current container/unplace for cleanup on next open
currentModalContainer = modalContainer;
currentModalUnplace = unplace;
// Create modal header
const modalHeader = document.createElement('div');
modalHeader.style.cssText = `
display: flex;
align-items: center;
padding: 10px 16px;
justify-content: space-between;
background-color: #232325;
flex-shrink: 0;
cursor: move;
user-select: none;
`;
// Add drag functionality
let isDragging = false;
let dragOffset = { x: 0, y: 0 };
modalHeader.addEventListener('mousedown', e => {
// Only start drag if clicking on the header itself, not buttons
if (
e.target === modalHeader ||
e.target === title ||
e.target === logoImg ||
e.target === headerLeft
) {
isDragging = true;
const rect = modalContainer.getBoundingClientRect();
dragOffset.x = e.clientX - rect.left;
dragOffset.y = e.clientY - rect.top;
// Switch to fixed positioning for dragging
modalContainer.style.position = 'fixed';
modalContainer.style.left = rect.left + 'px';
modalContainer.style.top = rect.top + 'px';
modalContainer.style.transform = 'none';
modalContainer.style.zIndex = '2147483647';
e.preventDefault();
}
});
document.addEventListener('mousemove', e => {
if (isDragging && modalContainer) {
const newX = e.clientX - dragOffset.x;
const newY = e.clientY - dragOffset.y;
// Keep modal within viewport bounds
const maxX = window.innerWidth - modalContainer.offsetWidth;
const maxY = window.innerHeight - modalContainer.offsetHeight;
modalContainer.style.left = Math.max(0, Math.min(newX, maxX)) + 'px';
modalContainer.style.top = Math.max(0, Math.min(newY, maxY)) + 'px';
}
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
// Create header left section with just the logo
const headerLeft = document.createElement('div');
headerLeft.style.cssText = `
display: flex;
flex-direction: row;
align-items: center;
`;
// Add Mem0 logo
const logoImg = document.createElement('img');
logoImg.src = chrome.runtime.getURL('icons/mem0-claude-icon.png');
logoImg.style.cssText = `
width: 26px;
height: 26px;
border-radius: 50%;
margin-right: 10px;
`;
// Add "OpenMemory" title
const title = document.createElement('div');
title.textContent = 'OpenMemory';
title.style.cssText = `
font-size: 16px;
font-weight: 600;
color: white;
`;
// Create header right section
const headerRight = document.createElement('div');
headerRight.style.cssText = `
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
`;
// Create Add to Prompt button with arrow
const addToPromptBtn = document.createElement('button');
addToPromptBtn.style.cssText = `
display: flex;
flex-direction: row;
align-items: center;
padding: 5px 16px;
gap: 8px;
background-color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
color: black;
`;
addToPromptBtn.textContent = 'Add to Prompt';
// Add arrow icon to button
const arrowIcon = document.createElement('span');
arrowIcon.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M5 12h14M12 5l7 7-7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`;
addToPromptBtn.appendChild(arrowIcon);
// Create settings button
const settingsBtn = document.createElement('button');
settingsBtn.style.cssText = `
background: none;
border: none;
cursor: pointer;
padding: 8px;
opacity: 0.6;
transition: opacity 0.2s;
`;
settingsBtn.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" xmlns="http://www.w3.org/2000/svg">
<path d="M12 15a3 3 0 100-6 3 3 0 000 6z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`;
// Add click event to open app.mem0.ai in a new tab
settingsBtn.addEventListener('click', () => {
if (currentModalOverlay && document.body.contains(currentModalOverlay)) {
document.body.removeChild(currentModalOverlay);
memoryModalShown = false;
currentModalOverlay = null;
}
chrome.runtime.sendMessage({ action: SidebarAction.SIDEBAR_SETTINGS });
});
// Add hover effect for the settings button
settingsBtn.addEventListener('mouseenter', () => {
settingsBtn.style.opacity = '1';
});
settingsBtn.addEventListener('mouseleave', () => {
settingsBtn.style.opacity = '0.6';
});
// Content section
const contentSection = document.createElement('div');
const contentSectionHeight = modalHeight - 130; // Account for header and navigation
contentSection.style.cssText = `
display: flex;
flex-direction: column;
padding: 0 16px;
gap: 12px;
overflow: hidden;
flex: 1;
height: ${contentSectionHeight}px;
`;
// Create memories counter
const memoriesCounter = document.createElement('div');
memoriesCounter.style.cssText = `
font-size: 16px;
font-weight: 600;
color: #FFFFFF;
margin-top: 16px;
flex-shrink: 0;
`;
// Update counter text based on loading state and number of memories
if (isLoading) {
memoriesCounter.textContent = `Loading Relevant Memories...`;
} else if (memoryItems.length === 0) {
memoriesCounter.textContent = `No Relevant Memories`;
} else {
memoriesCounter.textContent = `${memoryItems.length} Relevant Memories`;
}
// Calculate max height for memories content based on modal height
const memoriesContentMaxHeight = contentSectionHeight - 40; // Account for memories counter
// Create memories content container with adjusted height
const memoriesContent = document.createElement('div');
memoriesContent.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
overflow-y: auto;
flex: 1;
max-height: ${memoriesContentMaxHeight}px;
padding-right: 8px;
margin-right: -8px;
scrollbar-width: none;
-ms-overflow-style: none;
`;
memoriesContent.style.cssText += '::-webkit-scrollbar { display: none; }';
// Track currently expanded memory
let currentlyExpandedMemory: HTMLElement | null = null;
// Function to create skeleton loading items
function createSkeletonItems() {
memoriesContent.innerHTML = '';
for (let i = 0; i < memoriesPerPage; i++) {
const skeletonItem = document.createElement('div');
skeletonItem.style.cssText = `
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
padding: 12px;
background-color: #27272A;
border-radius: 8px;
height: 72px;
flex-shrink: 0;
animation: pulse 1.5s infinite ease-in-out;
`;
const skeletonText = document.createElement('div');
skeletonText.style.cssText = `
background-color: #383838;
border-radius: 4px;
height: 14px;
width: 85%;
margin-bottom: 8px;
`;
const skeletonText2 = document.createElement('div');
skeletonText2.style.cssText = `
background-color: #383838;
border-radius: 4px;
height: 14px;
width: 65%;
`;
const skeletonActions = document.createElement('div');
skeletonActions.style.cssText = `
display: flex;
gap: 4px;
margin-left: 10px;
`;
const skeletonButton1 = document.createElement('div');
skeletonButton1.style.cssText = `
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #383838;
`;
const skeletonButton2 = document.createElement('div');
skeletonButton2.style.cssText = `
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #383838;
`;
skeletonActions.appendChild(skeletonButton1);
skeletonActions.appendChild(skeletonButton2);
const textContainer = document.createElement('div');
textContainer.style.cssText = `
display: flex;
flex-direction: column;
flex-grow: 1;
`;
textContainer.appendChild(skeletonText);
textContainer.appendChild(skeletonText2);
skeletonItem.appendChild(textContainer);
skeletonItem.appendChild(skeletonActions);
memoriesContent.appendChild(skeletonItem);
}
// Add keyframe animation to document if not exists
if (!document.getElementById('skeleton-animation')) {
const style = document.createElement('style');
style.id = 'skeleton-animation';
style.innerHTML = `
@keyframes pulse {
0% { opacity: 0.6; }
50% { opacity: 0.8; }
100% { opacity: 0.6; }
}
`;
document.head.appendChild(style);
}
}
// Function to expand memory
function expandMemory(
memoryContainer: HTMLDivElement,
memoryText: HTMLDivElement,
contentWrapper: HTMLDivElement,
removeButton: HTMLButtonElement,
isExpanded: { value: boolean }
) {
if (currentlyExpandedMemory && currentlyExpandedMemory !== memoryContainer) {
currentlyExpandedMemory.dispatchEvent(new Event('collapse'));
}
isExpanded.value = true;
memoryText.style.webkitLineClamp = 'unset';
memoryText.style.height = 'auto';
contentWrapper.style.overflowY = 'auto';
contentWrapper.style.maxHeight = '240px'; // Limit height to prevent overflow
contentWrapper.style.scrollbarWidth = 'none';
contentWrapper.style.msOverflowStyle = 'none';
contentWrapper.style.cssText += '::-webkit-scrollbar { display: none; }';
memoryContainer.style.backgroundColor = '#1C1C1E';
memoryContainer.style.maxHeight = '300px'; // Allow expansion but within container
memoryContainer.style.overflow = 'hidden';
removeButton.style.display = 'flex';
currentlyExpandedMemory = memoryContainer;
// Scroll to make expanded memory visible if needed
memoriesContent.scrollTop = memoryContainer.offsetTop - memoriesContent.offsetTop;
}
// Function to collapse memory
function collapseMemory(
memoryContainer: HTMLDivElement,
memoryText: HTMLDivElement,
contentWrapper: HTMLDivElement,
removeButton: HTMLButtonElement,
isExpanded: { value: boolean }
) {
isExpanded.value = false;
memoryText.style.webkitLineClamp = '2';
memoryText.style.height = '42px';
contentWrapper.style.overflowY = 'visible';
memoryContainer.style.backgroundColor = '#27272A';
memoryContainer.style.maxHeight = '72px';
memoryContainer.style.overflow = 'hidden';
removeButton.style.display = 'none';
currentlyExpandedMemory = null;
}
// Function to show memories with adjusted count based on modal position
function showMemories() {
memoriesContent.innerHTML = '';
if (isLoading) {
createSkeletonItems();
return;
}
if (memoryItems.length === 0) {
showEmptyState();
updateNavigationState(0, 0);
return;
}
// Reset Add to Prompt button state
if (addToPromptBtn) {
addToPromptBtn.disabled = false;
addToPromptBtn.style.opacity = '1';
addToPromptBtn.style.cursor = 'pointer';
}
// Use the dynamically set memoriesPerPage value
const memoriesToShow = Math.min(memoriesPerPage, memoryItems.length);
// Calculate total pages and current page
const totalPages = Math.ceil(memoryItems.length / memoriesToShow);
const currentPage = Math.floor(currentMemoryIndex / memoriesToShow) + 1;
// Update navigation buttons state
updateNavigationState(currentPage, totalPages);
for (let i = 0; i < memoriesToShow; i++) {
const memoryIndex = currentMemoryIndex + i;
if (memoryIndex >= memoryItems.length) {
break;
} // Stop if we've reached the end
const memory = memoryItems[memoryIndex];
if (!memory) {
continue;
}
// Skip memories that have been added already
if (allMemoriesById.has(String(memory.id))) {
continue;
}
// Ensure memory has an ID
if (!memory.id) {
memory.id = `memory-${Date.now()}-${memoryIndex}`;
}
const memoryContainer = document.createElement('div');
memoryContainer.style.cssText = `
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
padding: 12px;
background-color: #27272A;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
min-height: 72px;
max-height: 72px;
overflow: hidden;
flex-shrink: 0;
`;
const memoryText = document.createElement('div');
memoryText.style.cssText = `
font-size: 14px;
line-height: 1.5;
color: #D4D4D8;
flex-grow: 1;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
transition: all 0.2s ease;
height: 42px; /* Height for 2 lines of text */
`;
memoryText.textContent = memory.text || '';
const actionsContainer = document.createElement('div');
actionsContainer.style.cssText = `
display: flex;
gap: 4px;
margin-left: 10px;
flex-shrink: 0;
`;
// Add button
const addButton = document.createElement('button');
addButton.style.cssText = `
border: none;
cursor: pointer;
padding: 4px;
background:rgb(66, 66, 69);
color:rgb(199, 199, 201);
border-radius: 100%;
transition: all 0.2s ease;
`;
addButton.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`;
// Add click handler for add button
addButton.addEventListener('click', (e: MouseEvent) => {
e.stopPropagation();
sendExtensionEvent('memory_injection', {
provider: 'claude',
source: 'OPENMEMORY_CHROME_EXTENSION',
browser: getBrowser(),
injected_all: false,
memory_id: memory.id,
});
// Mark this memory as added
allMemoriesById.add(String(memory.id));
// Add this memory to existing ones instead of replacing
allMemories.push(String(memory.text || ''));
// Update the input with all memories
updateInputWithMemories();
// Remove this memory from the list
const index = memoryItems.findIndex((m: MemoryItem) => m.id === memory.id);
if (index !== -1) {
memoryItems.splice(index, 1);
// Recalculate pagination after removing an item
// If we're on a page that's now empty, go to previous page
if (currentMemoryIndex > 0 && currentMemoryIndex >= memoryItems.length) {
currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage);
}
memoriesCounter.textContent = `${memoryItems.length} Relevant Memories`;
showMemories();
}
// Don't close the modal, allow adding more memories
});
// Menu button
const menuButton = document.createElement('button');
menuButton.style.cssText = `
background: none;
border: none;
cursor: pointer;
padding: 4px;
color: #A1A1AA;
`;
menuButton.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="2"/>
<circle cx="12" cy="5" r="2"/>
<circle cx="12" cy="19" r="2"/>
</svg>`;
// Track expanded state using object to maintain reference
const isExpanded = { value: false };
// Create remove button (hidden by default)
const removeButton = document.createElement('button');
removeButton.style.cssText = `
display: none;
align-items: center;
gap: 6px;
background:rgb(66, 66, 69);
color:rgb(199, 199, 201);
border-radius: 8px;
padding: 2px 4px;
border: none;
cursor: pointer;
font-size: 13px;
margin-top: 12px;
width: fit-content;
`;
removeButton.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Remove
`;
// Create content wrapper for text and remove button
const contentWrapper = document.createElement('div');
contentWrapper.style.cssText = `
display: flex;
flex-direction: column;
flex-grow: 1;
`;
contentWrapper.appendChild(memoryText);
contentWrapper.appendChild(removeButton);
memoryContainer.addEventListener('collapse', () => {
collapseMemory(memoryContainer, memoryText, contentWrapper, removeButton, isExpanded);
});
menuButton.addEventListener('click', (e: MouseEvent) => {
e.stopPropagation();
if (isExpanded.value) {
collapseMemory(memoryContainer, memoryText, contentWrapper, removeButton, isExpanded);
} else {
expandMemory(memoryContainer, memoryText, contentWrapper, removeButton, isExpanded);
}
});
// Add click handler for remove button
removeButton.addEventListener('click', (e: MouseEvent) => {
e.stopPropagation();
// Remove from memoryItems
const index = memoryItems.findIndex((m: MemoryItem) => m.id === memory.id);
if (index !== -1) {
memoryItems.splice(index, 1);
// Recalculate pagination after removing an item
// If we're on the last page and it's now empty, go to previous page
if (currentMemoryIndex > 0 && currentMemoryIndex >= memoryItems.length) {
currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage);
}
memoriesCounter.textContent = `${memoryItems.length} Relevant Memories`;
showMemories();
}
});
actionsContainer.appendChild(addButton);
actionsContainer.appendChild(menuButton);
memoryContainer.appendChild(contentWrapper);
memoryContainer.appendChild(actionsContainer);
memoriesContent.appendChild(memoryContainer);
// Add hover effect
memoryContainer.addEventListener('mouseenter', () => {
memoryContainer.style.backgroundColor = isExpanded.value ? '#18181B' : '#323232';
});
memoryContainer.addEventListener('mouseleave', () => {
memoryContainer.style.backgroundColor = isExpanded.value ? '#1C1C1E' : '#27272A';
});
}
// If after filtering for already added memories, there are no items to show,
// check if we need to go to previous page
if (memoriesContent.children.length === 0 && memoryItems.length > 0) {
if (currentMemoryIndex > 0) {
currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage);
showMemories();
} else {
updateNavigationState(0, 0);
showEmptyState();
}
}
}
// Function to show empty state
function showEmptyState() {
memoriesContent.innerHTML = '';
memoriesCounter.textContent = 'No Relevant Memories';
const emptyContainer = document.createElement('div');
emptyContainer.style.cssText = `
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32px 16px;
text-align: center;
flex: 1;
min-height: 200px;
`;
const emptyIcon = document.createElement('div');
emptyIcon.innerHTML = `<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#71717A" xmlns="http://www.w3.org/2000/svg">
<path d="M9 3H5a2 2 0 00-2 2v4m6-6h10a2 2 0 012 2v10a2 2 0 01-2 2h-4M3 21h4a2 2 0 002-2v-4m-6 6V9m18 12a9 9 0 11-18 0 9 9 0 0118 0z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`;
emptyIcon.style.marginBottom = '16px';
const emptyText = document.createElement('div');
emptyText.textContent = 'No relevant memories found';
emptyText.style.cssText = `
color: #71717A;
font-size: 14px;
font-weight: 500;
`;
emptyContainer.appendChild(emptyIcon);
emptyContainer.appendChild(emptyText);
memoriesContent.appendChild(emptyContainer);
// Disable the Add to Prompt button when there are no memories
if (addToPromptBtn) {
addToPromptBtn.disabled = true;
addToPromptBtn.style.opacity = '0.5';
addToPromptBtn.style.cursor = 'not-allowed';
}
}
// Update navigation button states
function updateNavigationState(currentPage: number, totalPages: number): void {
if (memoryItems.length === 0 || totalPages === 0) {
prevButton.disabled = true;
prevButton.style.opacity = '0.5';
prevButton.style.cursor = 'not-allowed';
nextButton.disabled = true;
nextButton.style.opacity = '0.5';
nextButton.style.cursor = 'not-allowed';
return;
}
if (currentPage <= 1) {
prevButton.disabled = true;
prevButton.style.opacity = '0.5';
prevButton.style.cursor = 'not-allowed';
} else {
prevButton.disabled = false;
prevButton.style.opacity = '1';
prevButton.style.cursor = 'pointer';
}
if (currentPage >= totalPages) {
nextButton.disabled = true;
nextButton.style.opacity = '0.5';
nextButton.style.cursor = 'not-allowed';
} else {
nextButton.disabled = false;
nextButton.style.opacity = '1';
nextButton.style.cursor = 'pointer';
}
}
// Navigation section at bottom
const navigationSection = document.createElement('div');
navigationSection.style.cssText = `
display: flex;
justify-content: center;
gap: 12px;
padding: 10px;
border-top: none;
flex-shrink: 0;
`;
// Navigation buttons
const prevButton = document.createElement('button');
prevButton.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 19l-7-7 7-7" stroke="#A1A1AA" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`;
prevButton.style.cssText = `
background: #27272A;
border: none;
border-radius: 50%;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background-color 0.2s;
`;
const nextButton = document.createElement('button');
nextButton.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 5l7 7-7 7" stroke="#A1A1AA" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`;
nextButton.style.cssText = prevButton.style.cssText;
// Add navigation button handlers
prevButton.addEventListener('click', () => {
if (currentMemoryIndex >= memoriesPerPage) {
currentMemoryIndex = Math.max(0, currentMemoryIndex - memoriesPerPage);
showMemories();
}
});
nextButton.addEventListener('click', () => {
if (currentMemoryIndex + memoriesPerPage < memoryItems.length) {
currentMemoryIndex = currentMemoryIndex + memoriesPerPage;
showMemories();
}
});
// Add hover effects
[prevButton, nextButton].forEach(button => {
button.addEventListener('mouseenter', () => {
if (!button.disabled) {
button.style.backgroundColor = '#323232';
}
});
button.addEventListener('mouseleave', () => {
if (!button.disabled) {
button.style.backgroundColor = '#27272A';
}
});
});
// Assemble modal
headerLeft.appendChild(logoImg);
headerLeft.appendChild(title);
headerRight.appendChild(addToPromptBtn);
headerRight.appendChild(settingsBtn);
modalHeader.appendChild(headerLeft);
modalHeader.appendChild(headerRight);
// Drag behavior removed in favor of utility-based placement
contentSection.appendChild(memoriesCounter);
contentSection.appendChild(memoriesContent);
navigationSection.appendChild(prevButton);
navigationSection.appendChild(nextButton);
modalContainer.appendChild(modalHeader);
modalContainer.appendChild(contentSection);
modalContainer.appendChild(navigationSection);
// When using floating placement, modalContainer is already in the DOM.
// Always append overlay last so outside-click works and layering is correct.
document.body.appendChild(modalOverlay);
// Show initial memories or loading state
if (isLoading) {
createSkeletonItems();
} else if (memoryItems.length === 0) {
showEmptyState();
} else {
showMemories();
}
// Function to close the modal
function closeModal(): void {
console.log('🚪 Closing modal');
try {
if (typeof unplace === 'function') {
unplace();
}
} catch {
// Ignore error
}
try {
if (typeof currentModalUnplace === 'function') {
currentModalUnplace();
}
} catch {
// Ignore error
}
if (currentModalContainer && currentModalContainer.isConnected) {
try {
currentModalContainer.remove();
} catch {
// Ignore error
}
}
if (currentModalOverlay && document.body.contains(currentModalOverlay)) {
// Clean up drag event listeners
document.body.removeChild(currentModalOverlay);
}
if (modalOverlay && document.body.contains(modalOverlay)) {
document.body.removeChild(modalOverlay);
}
currentModalOverlay = null;
currentModalContainer = null;
currentModalUnplace = null;
memoryModalShown = false;
isProcessingMem0 = false;
}
// Update Add to Prompt button click handler
addToPromptBtn.addEventListener('click', () => {
// Only add memories that are not already added
const newMemories = memoryItems
.filter(memory => !allMemoriesById.has(String(memory.id)) && !memory.removed)
.map(memory => {
allMemoriesById.add(String(memory.id));
return String(memory.text || '');
});
sendExtensionEvent('memory_injection', {
provider: 'claude',
source: 'OPENMEMORY_CHROME_EXTENSION',
browser: getBrowser(),
injected_all: true,
memory_count: newMemories.length,
});
// Add new memories to allMemories (don't replace existing ones)
if (newMemories.length > 0) {
// Add new memories to the existing array
allMemories = [...allMemories, ...newMemories];
// Update the input with all memories
updateInputWithMemories();
}
// Close the modal
closeModal();
// Remove all added memories from the memoryItems list
for (let i = memoryItems.length - 1; i >= 0; i--) {
if (allMemoriesById.has(String(memoryItems[i]?.id))) {
memoryItems.splice(i, 1);
}
}
});
}
// Shared function to update the input field with all collected memories
function updateInputWithMemories() {
// Find the input element (prioritizing the ProseMirror div with contenteditable="true")
let inputElement = document.querySelector('div[contenteditable="true"].ProseMirror');
// If ProseMirror not found, try other input elements
if (!inputElement) {
inputElement =
document.querySelector('div[contenteditable="true"]') ||
document.querySelector('textarea') ||
document.querySelector('p[data-placeholder="How can I help you today?"]') ||
document.querySelector('p[data-placeholder="Reply to Claude..."]');
}
if (inputElement && allMemories.length > 0) {
// Define the header text
const headerText = OPENMEMORY_PROMPTS.memory_header_text;
// Check if ProseMirror editor
if (inputElement.classList.contains('ProseMirror')) {
// First check if the header already exists
const headerExists = Array.from(inputElement.querySelectorAll('p strong')).some(el =>
(el.textContent || '').includes('Here is some of my memories')
);
if (headerExists) {
// Get all existing memory paragraphs
const paragraphs = Array.from(inputElement.querySelectorAll('p')) as HTMLElement[];
let headerIndex = -1;
const existingMemories = [];
// Find the index of the header paragraph
for (let i = 0; i < paragraphs.length; i++) {
const strongEl = paragraphs[i]?.querySelector('strong');
if (strongEl && (strongEl.textContent || '').includes('Here is some of my memories')) {
headerIndex = i;
break;
}
}
// Collect all existing memories after the header
if (headerIndex >= 0) {
for (let i = headerIndex + 1; i < paragraphs.length; i++) {
const para = paragraphs[i];
if (!para) {
continue;
}
const text = (para.textContent || '').trim();
if (text.startsWith('-')) {
existingMemories.push(text.substring(1).trim());
}
}
// Keep everything up to and including the header paragraph
const newHTML = Array.from(paragraphs)
.slice(0, headerIndex + 1)
.map(p => p.outerHTML)
.join('');
// Combine existing and new memories, avoiding duplicates
const combinedMemories = [...existingMemories];
// Add new memories if they don't already exist
allMemories.forEach(mem => {
if (!combinedMemories.includes(mem)) {
combinedMemories.push(mem);
}
});
// Add the memories after the header
const memoriesHTML = combinedMemories.map(mem => `<p>- ${mem}</p>`).join('');
// Set the new HTML content
inputElement.innerHTML = newHTML + memoriesHTML;
}
} else {
// Header doesn't exist, get the content without any existing memory wrappers
const baseContent = getContentWithoutMemories(undefined);
// Create the memory section
let memoriesContent = `<p><strong>${headerText}</strong></p>`;
// Add all memories to the content with proper paragraph tags
memoriesContent += allMemories.map(mem => `<p>- ${mem}</p>`).join('');
// If empty, replace the entire content
if (
!baseContent ||
baseContent.trim() === '' ||
(inputElement.querySelectorAll('p').length === 1 &&
inputElement.querySelector('p.is-empty') !== null)
) {
inputElement.innerHTML = memoriesContent;
} else {
// Otherwise append after a line break
inputElement.innerHTML = `${baseContent}<p><br></p>${memoriesContent}`;
}
}
// Dispatch proper events for ProseMirror
const inputEvent = new InputEvent('input', {
bubbles: true,
cancelable: true,
inputType: 'insertText',
});
inputElement.dispatchEvent(inputEvent);
// Also dispatch a change event
const changeEvent = new Event('change', { bubbles: true });
inputElement.dispatchEvent(changeEvent);
} else if (inputElement.tagName.toLowerCase() === 'div') {
// For normal contenteditable divs
// Check if the header already exists
if (inputElement.innerHTML.includes(headerText)) {
// Find the header position and extract existing memories
const htmlParts = inputElement.innerHTML.split(headerText);
if (htmlParts.length > 1) {
const beforeHeader = htmlParts[0];
const afterHeader = htmlParts[1];
// Extract existing memories from the content after the header
const tempDiv = document.createElement('div');
tempDiv.innerHTML = afterHeader || '';
const existingMemories: string[] = [];
// Find all paragraphs that start with a dash
Array.from(tempDiv.querySelectorAll('p')).forEach(p => {
const text = (p.textContent || '').trim();
if (text.startsWith('-')) {
existingMemories.push(text.substring(1).trim());
}
});
// Combine existing and new memories, avoiding duplicates
const combinedMemories = [...existingMemories];
// Add new memories if they don't already exist
allMemories.forEach(mem => {
if (!combinedMemories.includes(mem)) {
combinedMemories.push(mem);
}
});
// Create HTML with header and all memories
let newHTML = beforeHeader + `<p><strong>${headerText}</strong></p>`;
combinedMemories.forEach(mem => {
newHTML += `<p>- ${mem}</p>`;
});
inputElement.innerHTML = newHTML;
}
} else {
// Header doesn't exist
const baseContent = getContentWithoutMemories(undefined);
let memoriesContent = `<p><strong>${headerText}</strong></p>`;
allMemories.forEach(mem => {
memoriesContent += `<p>- ${mem}</p>`;
});
inputElement.innerHTML = `${baseContent}${baseContent ? '<p><br></p>' : ''}${memoriesContent}`;
}
// Dispatch input event
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
} else if (
inputElement.tagName.toLowerCase() === 'p' &&
(inputElement.getAttribute('data-placeholder') === 'How can I help you today?' ||
inputElement.getAttribute('data-placeholder') === 'Reply to Claude...')
) {
// For p element placeholders
// Check if the header already exists
if ((inputElement.textContent || '').includes(headerText)) {
// Find the header position and extract existing memories
const textParts = (inputElement.textContent || '').split(headerText);
if (textParts.length > 1) {
const beforeHeader = textParts[0];
const afterHeader = textParts[1];
// Extract existing memories
const existingMemories: string[] = [];
const memoryLines = (afterHeader || '').split('\n');
memoryLines.forEach(line => {
const trimmed = line.trim();
if (trimmed.startsWith('-')) {
existingMemories.push(trimmed.substring(1).trim());
}
});
// Combine existing and new memories, avoiding duplicates
const combinedMemories = [...existingMemories];
// Add new memories if they don't already exist
allMemories.forEach(mem => {
if (!combinedMemories.includes(mem)) {
combinedMemories.push(mem);
}
});
// Create text with header and all memories
const newText =
beforeHeader + headerText + '\n\n' + combinedMemories.map(mem => `- ${mem}`).join('\n');
inputElement.textContent = newText;
}
} else {
// Header doesn't exist
const baseContent = getContentWithoutMemories(undefined);
inputElement.textContent = `${baseContent}${baseContent ? '\n\n' : ''}${headerText}\n\n${allMemories.map(mem => `- ${mem}`).join('\n')}`;
}
// Dispatch various events
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
inputElement.dispatchEvent(new Event('focus', { bubbles: true }));
inputElement.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true }));
inputElement.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true }));
inputElement.dispatchEvent(new Event('change', { bubbles: true }));
} else {
// For textarea
// Check if the header already exists
if (((inputElement as HTMLTextAreaElement).value || '').includes(headerText)) {
// Find the header position and extract existing memories
const valueParts = ((inputElement as HTMLTextAreaElement).value || '').split(headerText);
if (valueParts.length > 1) {
const beforeHeader = valueParts[0];
const afterHeader = valueParts[1];
// Extract existing memories
const existingMemories: string[] = [];
const memoryLines = (afterHeader || '').split('\n');
memoryLines.forEach(line => {
const trimmed = line.trim();
if (trimmed.startsWith('-')) {
existingMemories.push(trimmed.substring(1).trim());
}
});
// Combine existing and new memories, avoiding duplicates
const combinedMemories = [...existingMemories];
// Add new memories if they don't already exist
allMemories.forEach(mem => {
if (!combinedMemories.includes(mem)) {
combinedMemories.push(mem);
}
});
// Create text with header and all memories
const newValue =
beforeHeader + headerText + '\n\n' + combinedMemories.map(mem => `- ${mem}`).join('\n');
inputElement.value = newValue;
}
} else {
// Header doesn't exist
const baseContent = getContentWithoutMemories(undefined);
(inputElement as HTMLTextAreaElement).value =
`${baseContent}${baseContent ? '\n\n' : ''}${headerText}\n\n${allMemories.map(mem => `- ${mem}`).join('\n')}`;
}
// Dispatch input event
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
}
// Focus the input element to ensure the user can continue typing
(inputElement as HTMLElement).focus();
}
}
// Function to get the content without any memory wrappers
function getContentWithoutMemories(providedMessage: string | undefined) {
// Find the input element (prioritizing the ProseMirror div with contenteditable="true")
let inputElement = document.querySelector('div[contenteditable="true"].ProseMirror');
// If ProseMirror not found, try other input elements
if (!inputElement) {
inputElement =
document.querySelector('div[contenteditable="true"]') ||
document.querySelector('textarea') ||
document.querySelector('p[data-placeholder="How can I help you today?"]') ||
document.querySelector('p[data-placeholder="Reply to Claude..."]');
}
// If a message is provided, operate on it; otherwise read from DOM
let content = '';
if (typeof providedMessage === 'string') {
content = providedMessage;
} else {
if (!inputElement) {
return '';
}
if (inputElement.classList.contains('ProseMirror')) {
// For ProseMirror, get the innerHTML for proper structure handling
content = inputElement.innerHTML;
} else if (inputElement.tagName.toLowerCase() === 'div') {
// For normal contenteditable divs
content = inputElement.innerHTML;
} else if (
inputElement.tagName.toLowerCase() === 'p' &&
(inputElement.getAttribute('data-placeholder') === 'How can I help you today?' ||
inputElement.getAttribute('data-placeholder') === 'Reply to Claude...')
) {
// For p element placeholders
content = inputElement.innerHTML || inputElement.textContent || '';
} else {
// For textarea
content = (inputElement as HTMLTextAreaElement).value || '';
}
}
// Remove any memory headers and content
// Match both HTML and plain text variants
// HTML variant
try {
const MEM0_HTML = OPENMEMORY_PROMPTS.memory_header_html_regex;
const MEM0_PLAIN = OPENMEMORY_PROMPTS.memory_header_plain_regex;
content = content.replace(MEM0_HTML, '');
content = content.replace(MEM0_PLAIN, '');
} catch {
// Ignore errors when processing memory content
}
// Also clean up any empty paragraphs at the end
content = content.replace(/<p><br><\/p>$/g, '');
content = content.replace(
/<p class="is-empty"><br class="ProseMirror-trailingBreak"><\/p>$/g,
''
);
return content.trim();
}
// New function to handle the memory modal
async function handleMem0Modal(
popup: HTMLElement | null,
clickSendButton: boolean = false,
sourceButtonId: string | null = null
): Promise<void> {
console.log('🚀 handleMem0Modal called', {
popup,
clickSendButton,
sourceButtonId,
isProcessingMem0,
memoryModalShown,
});
if (isProcessingMem0) {
console.log('⏸️ Already processing, returning early');
return;
}
if (memoryModalShown) {
console.log('📱 Modal already shown, returning early');
return;
}
// First check if memory is enabled
const enabled = await getMemoryEnabledState();
console.log('🧠 Memory enabled state:', enabled);
if (enabled === false) {
console.log('❌ Memory disabled, not showing modal');
return; // Don't show modal or login popup if memory is disabled
}
isProcessingMem0 = true;
// Set loading state for button
setButtonLoadingState();
// Hide any tooltip that might be showing
const tooltip = document.querySelector('#mem0-tooltip');
if (tooltip) {
tooltip.style.display = 'none';
}
try {
const data = await new Promise<StorageData>(resolve => {
chrome.storage.sync.get(
[
StorageKey.API_KEY,
StorageKey.USER_ID_CAMEL,
StorageKey.ACCESS_TOKEN,
StorageKey.SELECTED_ORG,
StorageKey.SELECTED_PROJECT,
StorageKey.USER_ID,
StorageKey.SIMILARITY_THRESHOLD,
StorageKey.TOP_K,
],
function (items) {
resolve(items);
}
);
});
const apiKey = data.apiKey;
const userId = data.userId || data.user_id || 'chrome-extension-user';
const accessToken = data.access_token;
if (!apiKey && !accessToken) {
// Show login popup instead of error message
isProcessingMem0 = false;
setButtonLoadingState();
showLoginPopup();
return;
}
let message = getInputValue();
console.log('📝 Input message:', message);
if (!message || message.trim() === '') {
console.log('❌ No input message, showing popup');
if (popup) {
// Hide any existing tooltip first
const tooltip = document.querySelector('#mem0-tooltip');
if (tooltip) {
tooltip.style.display = 'none';
}
showPopup(popup, 'Please enter some text first');
}
isProcessingMem0 = false;
setButtonLoadingState();
return;
}
console.log('✅ All checks passed, creating memory modal');
// Now we can show the loading modal since we have text input
createMemoryModal([], true, sourceButtonId);
// Clean the message by removing any existing memory wrappers
message = getContentWithoutMemories(undefined);
// Strip HTML tags to ensure clean text for search (fix for <p> tag issue)
const tempDiv = document.createElement('div');
tempDiv.innerHTML = message;
message = tempDiv.textContent || tempDiv.innerText || message;
message = message.trim();
sendExtensionEvent('modal_clicked', {
provider: 'claude',
source: 'OPENMEMORY_CHROME_EXTENSION',
browser: getBrowser(),
});
const authHeader = accessToken ? `Bearer ${accessToken}` : `Token ${apiKey}`;
const messages = getConversationContext(false); // Use sliding window context
messages.push({ role: MessageRole.User, content: message });
// If clickSendButton is true, click the send button
if (clickSendButton) {
const sendButton =
(document.querySelector('button[aria-label="Send Message"]') as HTMLElement) ||
(document.querySelector('button[aria-label="Send message"]') as HTMLElement);
if (sendButton) {
setTimeout(() => {
(sendButton as HTMLElement).click();
}, 100);
}
}
const optionalParams: OptionalApiParams = {};
if (data.selected_org) {
optionalParams.org_id = data.selected_org;
}
if (data.selected_project) {
optionalParams.project_id = data.selected_project;
}
// Use raw input for search key to match typing cache; cleaning happens in fetch
currentModalSourceButtonId = sourceButtonId;
try {
const rawInput = (function () {
const el =
document.querySelector('div[contenteditable="true"]') ||
document.querySelector('textarea') ||
document.querySelector('p[data-placeholder="How can I help you today?"]') ||
document.querySelector('p[data-placeholder="Reply to Claude..."]');
if (!el) {
return message;
}
const val = (el.textContent || el.value || '').trim();
return val || message;
})();
console.log("Claude modal search for:", rawInput);
console.log("Cache state:", claudeSearch.getState());
claudeSearch.runImmediate(rawInput);
} catch (_) {
claudeSearch.runImmediate(message);
}
// New add memory API call (non-blocking)
fetch('https://api.mem0.ai/v1/memories/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: authHeader,
},
body: JSON.stringify({
messages: messages,
user_id: userId,
infer: true,
metadata: {
provider: 'Claude',
},
source: 'OPENMEMORY_CHROME_EXTENSION',
...optionalParams,
}),
})
.then(response => {
if (!response.ok) {
// Silent failure for background memory addition
}
})
.catch(() => {
// Silent failure for background memory addition
});
} catch {
if (popup) {
showPopup(popup, 'Failed to send message to Mem0');
}
} finally {
isProcessingMem0 = false;
setButtonLoadingState();
}
}
function setButtonLoadingState(): void {
// Legacy mem0-button state removed; no-op
}
function showPopup(popup: HTMLElement, message: string): void {
// Legacy tooltip suppressed; keep popup container-only usage
if (!popup) {
return;
}
Array.from(document.querySelectorAll('#mem0-tooltip')).forEach(p => {
try {
p.remove();
} catch {
// Ignore error
}
});
popup.textContent = message;
popup.style.display = 'block';
setTimeout(() => {
popup.style.display = 'none';
}, 2000);
}
function getInputValue(): string | null {
const inputElement =
document.querySelector('div[contenteditable="true"]') ||
document.querySelector('textarea') ||
document.querySelector('p[data-placeholder="How can I help you today?"]') ||
document.querySelector('p[data-placeholder="Reply to Claude..."]');
if (!inputElement) {
return null;
}
// For the p element placeholders specifically
if (
inputElement.tagName.toLowerCase() === 'p' &&
(inputElement.getAttribute('data-placeholder') === 'How can I help you today?' ||
inputElement.getAttribute('data-placeholder') === 'Reply to Claude...')
) {
return inputElement.textContent || '';
}
return inputElement.textContent || (inputElement as HTMLTextAreaElement)?.value || null;
}
let claudeBackgroundSearchHandler: (() => void) | null = null;
function hookClaudeBackgroundSearchTyping() {
const inputElement =
document.querySelector('div[contenteditable="true"]') ||
document.querySelector('textarea') ||
document.querySelector('p[data-placeholder="How can I help you today?"]') ||
document.querySelector('p[data-placeholder="Reply to Claude..."]');
if (!inputElement) {
return;
}
if (inputElement.dataset.claudeBackgroundHooked) {
return;
}
inputElement.dataset.claudeBackgroundHooked = 'true';
if (!claudeBackgroundSearchHandler) {
claudeBackgroundSearchHandler = function () {
let text = getInputValue() || '';
try {
const MEM0_PLAIN = OPENMEMORY_PROMPTS.memory_header_plain_regex;
text = text.replace(MEM0_PLAIN, '').trim();
} catch {}
console.log("Claude background search triggered:", text);
claudeSearch.setText(text);
};
}
inputElement.addEventListener('input', claudeBackgroundSearchHandler);
inputElement.addEventListener('keyup', claudeBackgroundSearchHandler);
}
// Auto-inject support: simple debounce and config
async function updateMemoryEnabled() {
memoryEnabled = Boolean(await getMemoryEnabledState());
// If memory is disabled, remove the button completely
if (memoryEnabled === false) {
removeMemButton();
} else {
// If memory is enabled, ensure the button is added
// addMem0Button(); // Removed: OPENMEMORY_UI handles icon mounting
}
}
function initializeMem0Integration(): void {
console.log('🚀 initializeMem0Integration started');
updateMemoryEnabled();
hookClaudeBackgroundSearchTyping();
if (!(OPENMEMORY_UI && OPENMEMORY_UI.mountOnEditorFocus)) {
// addMem0Button(); // Removed: OPENMEMORY_UI handles icon mounting
}
// Prime the cache so the very first send is captured
const _initVal = getInputValue();
if (_initVal && _initVal.trim()) {
lastTyped = _initVal;
}
// Ensure send button listeners are attached early and repeatedly
const ensureSendButtonListeners = () => {
const allSendButtons = [
document.querySelector('button[aria-label="Send Message"]'),
document.querySelector('button[aria-label="Send message"]'),
].filter(Boolean);
allSendButtons.forEach(sendBtn => {
if (sendBtn && !sendBtn.dataset.mem0Listener) {
sendBtn.dataset.mem0Listener = 'true';
// Snapshot current input as early as possible (before Claude clears it)
sendBtn.addEventListener(
'pointerdown',
function () {
const current = getInputValue();
if (current && current.trim() !== '') {
lastTyped = current;
}
lastSendInitiatedAt = Date.now();
},
true
);
// Use capture-phase click so we run before Claude's handler
sendBtn.addEventListener(
'click',
function () {
// Capture and save memory with snapshot fallback
captureAndStoreMemory(lastTyped);
// Clear all memories after sending
setTimeout(() => {
allMemories = [];
allMemoriesById.clear();
}, 100);
},
true
);
}
});
};
// Attach listeners immediately
ensureSendButtonListeners();
// Also attach them repeatedly during early page load
const earlyAttachInterval = setInterval(() => {
ensureSendButtonListeners();
}, 100);
// Stop the aggressive checking after page is more stable
setTimeout(() => {
clearInterval(earlyAttachInterval);
}, 5000);
// Refresh cache whenever the editor gains focus
if (!(document as ExtendedDocument).__mem0FocusPrimed) {
(document as ExtendedDocument).__mem0FocusPrimed = true;
document.addEventListener(
'focusin',
e => {
const target = e.target as Element | null;
const el =
target &&
(target as ExtendedElement).closest &&
(target as ExtendedElement).closest(
'div[contenteditable="true"], textarea, p[data-placeholder="How can I help you today?"], p[data-placeholder="Reply to Claude..."]'
);
if (el) {
const v = getInputValue();
if (v && v.trim()) {
lastTyped = v;
}
}
},
true
);
}
document.addEventListener('keydown', function (event) {
if (event.ctrlKey && event.key === 'm') {
event.preventDefault();
console.log('⌨️ Ctrl+M pressed', { memoryEnabled, memoryModalShown, isProcessingMem0 });
// If modal is already shown, close it instead of creating duplicate
if (memoryModalShown) {
console.log('🔄 Modal already shown, closing it');
if (currentModalOverlay && document.body.contains(currentModalOverlay)) {
document.body.removeChild(currentModalOverlay);
}
memoryModalShown = false;
currentModalOverlay = null;
return;
}
if (memoryEnabled && !isProcessingMem0) {
const popup = document.querySelector('.mem0-popup');
if (popup) {
(async () => {
await handleMem0Modal(popup as HTMLElement, false);
})();
} else {
// If no popup is available, use the mem0-icon-button as source
(async () => {
await handleMem0Modal(null, false, 'mem0-icon-button');
})();
}
}
}
});
// Cache-first mount for Claude (left/bottom placement relative to textarea)
console.log('🎯 Starting cache-first mount for Claude');
function renderMem0Icon(shadow: ShadowRoot, host: HTMLElement) {
console.log('🎨 renderMem0Icon called', { shadow, host });
let style = document.createElement('style');
style.textContent = `
:host { position: relative; }
.mem0-btn { all: initial; cursor: pointer; display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:50%; }
.mem0-btn img { width:18px; height:18px; border-radius:50%; }
.dot { position:absolute; top:-2px; right:-2px; width:8px; height:8px; background:#80DDA2; border-radius:50%; border:2px solid #1C1C1E; display:none; }
:host([data-has-text="1"]) .dot { display:block; }
`;
let btn = document.createElement('button');
btn.className = 'mem0-btn';
let img = document.createElement('img');
img.src = chrome.runtime.getURL('icons/mem0-claude-icon-p.png');
let dot = document.createElement('div');
dot.className = 'dot';
btn.appendChild(img);
shadow.append(style, btn, dot);
btn.addEventListener('click', function () {
console.log('🖱️ Mem0 button clicked!');
handleMem0Modal(null, false, 'mem0-icon-button');
});
if (typeof updateNotificationDot === 'function') {
setTimeout(updateNotificationDot, 0);
}
}
try {
console.log('🔍 Checking for existing button and OPENMEMORY_UI', {
existingButton: !!document.getElementById('mem0-icon-button'),
hasOpenMemoryUI: !!OPENMEMORY_UI,
});
// Only create button if none exists (including shadow DOM buttons)
const existingButton =
document.getElementById('mem0-icon-button') ||
document.querySelector('[id*="mem0"]') ||
document.querySelector('.mem0-btn');
if (!existingButton && OPENMEMORY_UI) {
console.log('🚀 Starting OPENMEMORY_UI.resolveCachedAnchor');
OPENMEMORY_UI.resolveCachedAnchor(
{ learnKey: location.host + ':' + location.pathname },
null,
24 * 60 * 60 * 1000
)
.then(function (hit: { el: Element; placement: Placement | null } | null) {
console.log('🎯 resolveCachedAnchor result:', hit);
if (!hit || !hit.el) {
console.log('❌ No anchor found, cannot create button');
return;
}
console.log('✅ Anchor found, creating button');
let hs = OPENMEMORY_UI.createShadowRootHost('mem0-root');
let host = hs.host,
shadow = hs.shadow;
host.id = 'mem0-icon-button';
let cfg =
typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.claude ? SITE_CONFIG.claude : null;
let placement = hit.placement ||
(cfg && cfg.placement) || {
strategy: 'dock',
container: 'form',
side: 'bottom',
align: 'start',
gap: 8,
};
OPENMEMORY_UI.applyPlacement({ container: host, anchor: hit.el, placement: placement });
renderMem0Icon(shadow, host);
})
.catch(function (error: Error) {
console.error('❌ Error in resolveCachedAnchor:', error);
});
} else {
console.log('❌ Cannot create button:', {
buttonExists: !!document.getElementById('mem0-icon-button'),
hasOpenMemoryUI: !!OPENMEMORY_UI,
});
}
} catch (error) {
// Ignore errors during re-initialization
console.error('❌ Error in cache-first mount:', error);
}
// Focus-driven mount for Claude
console.log('🎯 Checking focus-driven mount');
if (
OPENMEMORY_UI &&
OPENMEMORY_UI.mountOnEditorFocus &&
!document.querySelector('[id*="mem0"], .mem0-btn')
) {
console.log('🚀 Starting focus-driven mount');
OPENMEMORY_UI.mountOnEditorFocus({
existingHostSelector: '#mem0-icon-button, [id*="mem0"], .mem0-btn',
editorSelector:
typeof SITE_CONFIG !== 'undefined' &&
SITE_CONFIG.claude &&
SITE_CONFIG.claude.editorSelector
? SITE_CONFIG.claude.editorSelector
: 'div[contenteditable="true"], textarea, p[data-placeholder], [contenteditable="true"]',
deriveAnchor:
typeof SITE_CONFIG !== 'undefined' &&
SITE_CONFIG.claude &&
typeof SITE_CONFIG.claude.deriveAnchor === 'function'
? SITE_CONFIG.claude.deriveAnchor
: function (editor: Element) {
return editor.closest('form') || editor.parentElement;
},
placement:
typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.claude && SITE_CONFIG.claude.placement
? SITE_CONFIG.claude.placement
: { strategy: 'dock', container: 'form', side: 'bottom', align: 'start', gap: 8 },
render: function (shadow: ShadowRoot, host: HTMLElement) {
host.id = 'mem0-icon-button';
return renderMem0Icon(shadow, host);
},
fallback: function () {
try {
let cfg =
typeof SITE_CONFIG !== 'undefined' && SITE_CONFIG.claude ? SITE_CONFIG.claude : null;
if (!cfg || !OPENMEMORY_UI || !OPENMEMORY_UI.mountResilient) {
return;
}
OPENMEMORY_UI.mountResilient({
anchors: cfg.fallbackAnchors || [],
placement: cfg.placement,
enableFloatingFallback: true,
render: function (shadow: ShadowRoot, host: HTMLElement) {
host.id = 'mem0-icon-button';
return renderMem0Icon(shadow, host);
},
});
} catch {
// Ignore error
}
},
});
}
// Global early keydown capture for Enter to snapshot the very first send
if (!(document as ExtendedDocument).__mem0EnterCapture) {
(document as ExtendedDocument).__mem0EnterCapture = true;
document.addEventListener(
'keydown',
e => {
if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
const v = getInputValue();
if (v && v.trim()) {
lastTyped = v;
lastSendInitiatedAt = Date.now();
}
}
},
true
);
}
// Global submit capture to catch earliest send even if buttons/forms change
if (!(document as ExtendedDocument).__mem0SubmitCapture) {
(document as ExtendedDocument).__mem0SubmitCapture = true;
document.addEventListener(
'submit',
() => {
const v = getInputValue();
if (v && v.trim()) {
lastTyped = v;
}
lastSendInitiatedAt = Date.now();
captureAndStoreMemory(lastTyped);
},
true
);
}
// Find the input element and observe it
// function observeInput() {
// const inputElement =
// document.querySelector('div[contenteditable="true"]') ||
// document.querySelector('textarea') ||
// document.querySelector('p[data-placeholder="How can I help you today?"]') ||
// document.querySelector('p[data-placeholder="Reply to Claude..."]');
// if (inputElement) {
// inputObserver = new MutationObserver(() => {
// // Observer callback - can be empty for now
// });
// inputObserver.observe(inputElement, {
// childList: true,
// characterData: true,
// subtree: true,
// });
// } else {
// // If no input element found, try again later
// setTimeout(observeInput, 1000);
// }
// }
chrome.storage.onChanged.addListener((changes, namespace) => {
if (namespace === 'sync' && changes.memory_enabled) {
updateMemoryEnabled();
}
});
// Fallback: observe chat thread for newly added user bubbles and post if we missed send
const ensureThreadObserver = () => {
const thread = document.querySelector(
'.flex-1.flex.flex-col.gap-3.px-4.max-w-3xl.mx-auto.w-full'
);
if (!thread) {
setTimeout(ensureThreadObserver, 1000);
return;
}
if ((thread as ExtendedElement).__mem0Observed) {
return;
}
(thread as ExtendedElement).__mem0Observed = true;
// Track processed messages to avoid duplicates
const processedMessages = new Set();
const observer = new MutationObserver(mutations => {
for (let i = 0; i < mutations.length; i++) {
const m = mutations[i];
if (!m) {
continue;
}
const added = m.addedNodes;
for (let j = 0; j < added.length; j++) {
const n = added[j];
if (!n) {
continue;
}
const node = (n as ExtendedElement).nodeType === 1 ? (n as Element) : null;
if (!node) {
continue;
}
const userEl = (node as ExtendedElement).matches?.('.font-user-message')
? node
: (node as ExtendedElement).querySelector?.('.font-user-message');
if (userEl) {
const text = (userEl.textContent || '').trim();
if (text) {
// Create a simple hash of the message to avoid duplicates
const messageHash = text.length + '_' + text.substring(0, 50);
// Skip if we've already processed this exact message recently
if (processedMessages.has(messageHash)) {
return;
}
// Add to processed set and clean up old entries periodically
processedMessages.add(messageHash);
if (processedMessages.size > 10) {
const entries = Array.from(processedMessages);
processedMessages.clear();
// Keep the last 5 entries
entries.slice(-5).forEach(entry => processedMessages.add(entry));
}
// If we just initiated a send, give primary handlers a brief head start
const justInitiated = Date.now() - lastSendInitiatedAt < 500; // Reduced from 1200ms to 500ms
if (justInitiated) {
// For very recent sends, delay slightly to let primary handlers run first
setTimeout(() => {
// Double-check if we still need to process this message
if (!processedMessages.has(messageHash + '_processed')) {
processedMessages.add(messageHash + '_processed');
captureAndStoreMemory(text);
}
}, 200);
} else {
// For older messages or when no recent send detected, process immediately
processedMessages.add(messageHash + '_processed');
captureAndStoreMemory(text);
}
// Update lastSendInitiatedAt to help coordinate with other handlers
lastSendInitiatedAt = Date.now();
return;
}
}
}
}
});
observer.observe(thread, { childList: true, subtree: true });
};
ensureThreadObserver();
}
// Function to show login popup
function showLoginPopup() {
// First remove any existing popups
const existingPopup = document.querySelector('#mem0-login-popup');
if (existingPopup) {
existingPopup.remove();
}
// Create popup container
const popupOverlay = document.createElement('div');
popupOverlay.id = 'mem0-login-popup';
popupOverlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 10001;
`;
const popupContainer = document.createElement('div');
popupContainer.style.cssText = `
background-color: #1C1C1E;
border-radius: 12px;
width: 320px;
padding: 24px;
color: white;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
// Close button
const closeButton = document.createElement('button');
closeButton.style.cssText = `
position: absolute;
top: 16px;
right: 16px;
background: none;
border: none;
color: #A1A1AA;
font-size: 16px;
cursor: pointer;
`;
closeButton.innerHTML = '×';
closeButton.addEventListener('click', () => {
document.body.removeChild(popupOverlay);
});
// Logo and heading
const logoContainer = document.createElement('div');
logoContainer.style.cssText = `
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
`;
const logo = document.createElement('img');
logo.src = chrome.runtime.getURL('icons/mem0-claude-icon.png');
logo.style.cssText = `
width: 24px;
height: 24px;
border-radius: 50%;
margin-right: 12px;
`;
const logoDark = document.createElement('img');
logoDark.src = chrome.runtime.getURL('icons/mem0-icon-black.png');
logoDark.style.cssText = `
width: 24px;
height: 24px;
border-radius: 50%;
margin-right: 12px;
`;
const heading = document.createElement('h2');
heading.textContent = 'Sign in to OpenMemory';
heading.style.cssText = `
margin: 0;
font-size: 18px;
font-weight: 600;
`;
logoContainer.appendChild(heading);
// Message
const message = document.createElement('p');
message.textContent =
'Please sign in to access your memories and personalize your conversations!';
message.style.cssText = `
margin-bottom: 24px;
color: #D4D4D8;
gitextract_b1wvur0_/ ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── docs/ │ └── types.md ├── manifest.json ├── package.json ├── privacy-policy.md ├── src/ │ ├── background.ts │ ├── chatgpt/ │ │ └── content.ts │ ├── claude/ │ │ └── content.ts │ ├── context-menu-memory.ts │ ├── deepseek/ │ │ └── content.ts │ ├── direct-url-tracker.ts │ ├── gemini/ │ │ └── content.ts │ ├── grok/ │ │ └── content.ts │ ├── mem0/ │ │ └── content.ts │ ├── perplexity/ │ │ └── content.ts │ ├── popup.html │ ├── popup.ts │ ├── replit/ │ │ └── content.ts │ ├── search_tracker.ts │ ├── selection_context.ts │ ├── sidebar.ts │ ├── types/ │ │ ├── api.ts │ │ ├── browser.ts │ │ ├── dom.ts │ │ ├── memory.ts │ │ ├── messages.ts │ │ ├── organizations.ts │ │ ├── providers.ts │ │ ├── settings.ts │ │ └── storage.ts │ └── utils/ │ ├── background_search.ts │ ├── llm_prompts.ts │ ├── site_config.ts │ ├── util_functions.ts │ └── util_positioning.ts ├── tsconfig.json └── vite.config.ts
SYMBOL INDEX (292 symbols across 27 files)
FILE: src/chatgpt/content.ts
function createMemoryModal (line 133) | function createMemoryModal(
function updateInputWithMemories (line 1118) | function updateInputWithMemories(): void {
function getContentWithoutMemories (line 1163) | function getContentWithoutMemories(message?: string): string {
function addSendButtonListener (line 1214) | function addSendButtonListener(): void {
function captureAndStoreMemory (line 1262) | function captureAndStoreMemory(): void {
function updateNotificationDot (line 1359) | async function updateNotificationDot(): Promise<void> {
function handleMem0Modal (line 1389) | async function handleMem0Modal(sourceButtonId: string | null = null): Pr...
function showButtonPopup (line 1499) | function showButtonPopup(button: HTMLElement, message: string): void {
function setupAutoInjectPrefetch (line 1575) | function setupAutoInjectPrefetch() {
function getLastMessages (line 1877) | function getLastMessages(count: number): Array<{ role: MessageRole; cont...
function getInputValue (line 1906) | function getInputValue(): string {
function hookBackgroundSearchTyping (line 1919) | function hookBackgroundSearchTyping() {
function addSyncButton (line 1951) | function addSyncButton(): void {
function handleSyncClick (line 2021) | function handleSyncClick(): void {
function sendMemoriesToMem0 (line 2103) | function sendMemoriesToMem0(memories: Array<{ role: string; content: str...
function setSyncButtonLoadingState (line 2163) | function setSyncButtonLoadingState(isLoading: boolean): void {
function showSyncPopup (line 2187) | function showSyncPopup(button: HTMLElement, message: string): void {
function sendMemoryToMem0 (line 2220) | function sendMemoryToMem0(
function getMemoryEnabledState (line 2284) | function getMemoryEnabledState(): Promise<boolean> {
function initializeMem0Integration (line 2296) | function initializeMem0Integration(): void {
function showLoginPopup (line 2346) | function showLoginPopup() {
function chatgptCheckExtensionContext (line 2509) | function chatgptCheckExtensionContext() {
function chatgptDetectNavigation (line 2525) | function chatgptDetectNavigation() {
FILE: src/claude/content.ts
type ChromeRuntimeLastError (line 14) | interface ChromeRuntimeLastError {
type ChromeRuntimeWithLastError (line 18) | interface ChromeRuntimeWithLastError {
type ChromeRuntime (line 22) | interface ChromeRuntime extends ChromeRuntimeWithLastError {
constant MAX_CONVERSATION_HISTORY (line 146) | const MAX_CONVERSATION_HISTORY = 12;
function addToConversationHistory (line 149) | function addToConversationHistory(role: MessageRole, content: string) {
function getConversationContext (line 180) | function getConversationContext(includeCurrent: boolean = true) {
function initializeConversationHistoryFromDOM (line 204) | function initializeConversationHistoryFromDOM() {
function getMemoryEnabledState (line 253) | async function getMemoryEnabledState() {
function removeMemButton (line 288) | function removeMemButton(): void {
function createPopup (line 310) | function createPopup(container: HTMLElement, position: string = 'top'): ...
function createMemoryModal (line 792) | function createMemoryModal(
function updateInputWithMemories (line 1814) | function updateInputWithMemories() {
function getContentWithoutMemories (line 2087) | function getContentWithoutMemories(providedMessage: string | undefined) {
function handleMem0Modal (line 2152) | async function handleMem0Modal(
function setButtonLoadingState (line 2346) | function setButtonLoadingState(): void {
function showPopup (line 2350) | function showPopup(popup: HTMLElement, message: string): void {
function getInputValue (line 2370) | function getInputValue(): string | null {
function hookClaudeBackgroundSearchTyping (line 2395) | function hookClaudeBackgroundSearchTyping() {
function updateMemoryEnabled (line 2429) | async function updateMemoryEnabled() {
function initializeMem0Integration (line 2441) | function initializeMem0Integration(): void {
function showLoginPopup (line 2862) | function showLoginPopup() {
function captureAndStoreMemory (line 3021) | async function captureAndStoreMemory(snapshot: string) {
function updateNotificationDot (line 3169) | function updateNotificationDot() {
function setupEnhancedDOMMonitoring (line 3256) | function setupEnhancedDOMMonitoring() {
function checkExtensionContext (line 3428) | function checkExtensionContext() {
function detectNavigation (line 3438) | function detectNavigation() {
FILE: src/context-menu-memory.ts
function initContextMenuMemory (line 12) | function initContextMenuMemory(): void {
function toast (line 82) | function toast(tabId: number, message: string, variant: ToastVariant = T...
function normalize (line 91) | function normalize(text: string): string {
function clamp (line 95) | function clamp(text: string, max: number): string {
function composeBasic (line 105) | function composeBasic({ selection }: { selection: string; title: string;...
function requestSelectionContext (line 111) | function requestSelectionContext(tabId: number): Promise<SelectionContex...
function tryInjectSelectionScript (line 128) | async function tryInjectSelectionScript(tabId: number): Promise<boolean> {
function getSettings (line 143) | function getSettings(): Promise<Settings> {
function addMemory (line 169) | async function addMemory(content: string, settings: Settings): Promise<b...
FILE: src/deepseek/content.ts
constant INPUT_SELECTOR (line 14) | const INPUT_SELECTOR = "#chat-input, textarea, [contenteditable='true']";
function isIgnoredNode (line 17) | function isIgnoredNode(node: Element, ignoredSelectors: string[]): boole...
function expandMemory (line 46) | function expandMemory(
function collapseMemory (line 76) | function collapseMemory(
function removeMem0IconButton (line 104) | function removeMem0IconButton() {
function getInputElement (line 129) | function getInputElement() {
function getSendButtonElement (line 177) | function getSendButtonElement(): HTMLElement | null {
function handleEnterKey (line 294) | async function handleEnterKey(event: KeyboardEvent) {
function initializeMem0Integration (line 336) | function initializeMem0Integration(): void {
function getMemoryEnabledState (line 573) | async function getMemoryEnabledState(): Promise<boolean> {
function getInputElementValue (line 589) | function getInputElementValue(): string | null {
function getAuthDetails (line 600) | function getAuthDetails(): Promise<{ apiKey: string; accessToken: string...
constant MEM0_API_BASE_URL (line 615) | const MEM0_API_BASE_URL = 'https://api.mem0.ai';
function hookDeepseekBackgroundSearchTyping (line 716) | function hookDeepseekBackgroundSearchTyping() {
function addMemory (line 816) | function addMemory(memoryText: string) {
function triggerSendAction (line 903) | async function triggerSendAction(): Promise<void> {
function handleMem0Processing (line 1003) | async function handleMem0Processing(): Promise<void> {
function createMemoryModal (line 1047) | function createMemoryModal(
function showEmptyState (line 1890) | function showEmptyState(container: HTMLElement) {
function updateNavigationState (line 1929) | function updateNavigationState(
function updateInputWithMemories (line 1971) | function updateInputWithMemories(): void {
function getContentWithoutMemories (line 1993) | function getContentWithoutMemories(): string {
function handleMem0Modal (line 2011) | async function handleMem0Modal(sourceButtonId: string | null = null): Pr...
function showGuidancePopover (line 2070) | function showGuidancePopover(): void {
function showLoginModal (line 2162) | function showLoginModal(): void {
function addMem0IconButton (line 2330) | function addMem0IconButton() {
function updateNotificationDot (line 2878) | function updateNotificationDot() {
function addSendButtonListener (line 2917) | function addSendButtonListener(): void {
FILE: src/direct-url-tracker.ts
function getSettings (line 7) | function getSettings(): Promise<Settings> {
function addMemory (line 33) | async function addMemory(content: string, settings: Settings, pageUrl: s...
function shouldTrackTyped (line 75) | function shouldTrackTyped(details: OnCommittedDetails): boolean {
function initDirectUrlTracking (line 94) | function initDirectUrlTracking(): void {
function isSearchResultsUrl (line 146) | function isSearchResultsUrl(urlString: string): boolean {
function formatTimestamp (line 184) | function formatTimestamp(): { date: string; time: string } {
FILE: src/gemini/content.ts
constant MAX_SETUP_RETRIES (line 35) | const MAX_SETUP_RETRIES: number = 10;
function hookGeminiBackgroundSearchTyping (line 139) | function hookGeminiBackgroundSearchTyping() {
function getTextarea (line 160) | function getTextarea(): HTMLElement | null {
function getSendButton (line 197) | function getSendButton(): HTMLButtonElement | null {
function startElementDetection (line 226) | function startElementDetection(): void {
function setupInputObserver (line 260) | function setupInputObserver(): void {
function setInputValue (line 279) | function setInputValue(inputElement: HTMLElement, value: string): void {
function getContentWithoutMemories (line 322) | function getContentWithoutMemories(): string {
function getMemoryEnabledState (line 350) | function getMemoryEnabledState(): Promise<boolean> {
function addSendButtonListener (line 363) | function addSendButtonListener(): void {
function injectMem0Button (line 491) | function injectMem0Button(): void {
function updateNotificationDot (line 860) | function updateNotificationDot(): void {
function updateInputWithMemories (line 924) | function updateInputWithMemories(): void {
function showButtonPopup (line 947) | function showButtonPopup(button: HTMLElement, message: string): void {
function showLoginPopup (line 1005) | function showLoginPopup(): void {
function createMemoryModal (line 1154) | function createMemoryModal(
function handleMem0Modal (line 2018) | async function handleMem0Modal(): Promise<void> {
function initializeMem0Integration (line 2163) | function initializeMem0Integration(): void {
function cleanup (line 2341) | function cleanup(): void {
FILE: src/grok/content.ts
function getTextarea (line 136) | function getTextarea(): HTMLTextAreaElement | null {
function hookGrokBackgroundSearchTyping (line 157) | function hookGrokBackgroundSearchTyping() {
function setupInputObserver (line 179) | function setupInputObserver(): void {
function setInputValue (line 189) | function setInputValue(inputElement: HTMLTextAreaElement | null, value: ...
function initializeMem0Integration (line 319) | function initializeMem0Integration(): void {
function updateNotificationDot (line 728) | function updateNotificationDot(): void {
function createMemoryModal (line 757) | function createMemoryModal(memoryItems: MemoryItem[], isLoading: boolean...
function updateInputWithMemories (line 1753) | function updateInputWithMemories() {
function getContentWithoutMemories (line 1776) | function getContentWithoutMemories(): string {
function getMemoryEnabledState (line 1804) | function getMemoryEnabledState(): Promise<boolean> {
function handleMem0Modal (line 1813) | async function handleMem0Modal() {
function showButtonPopup (line 1947) | function showButtonPopup(button: HTMLElement, message: string): void {
function showLoginPopup (line 2004) | function showLoginPopup(): void {
FILE: src/mem0/content.ts
function fetchAndSaveSession (line 3) | function fetchAndSaveSession() {
FILE: src/perplexity/content.ts
function hookPerplexityBackgroundSearchTyping (line 126) | function hookPerplexityBackgroundSearchTyping() {
function getTextarea (line 147) | function getTextarea(): HTMLElement | null {
function getInputText (line 158) | function getInputText(inputElement: HTMLElement | null): string {
function setInputText (line 197) | function setInputText(inputElement: HTMLElement | null, text: string): v...
function simulateTyping (line 284) | function simulateTyping(inputElement: HTMLElement, text: string): void {
function addMem0Button (line 430) | async function addMem0Button() {
function updateNotificationDot (line 1040) | function updateNotificationDot() {
function createMemoryModal (line 1077) | function createMemoryModal(
function updateInputWithMemories (line 2045) | function updateInputWithMemories() {
function getMemoryEnabledState (line 2076) | function getMemoryEnabledState(): Promise<boolean> {
function captureAndStoreMemory (line 2085) | function captureAndStoreMemory() {
function setupSubmitButtonListener (line 2167) | function setupSubmitButtonListener() {
function setupConversationObserver (line 2221) | function setupConversationObserver() {
function setupInputObserver (line 2263) | function setupInputObserver() {
function handleMem0Processing (line 2272) | async function handleMem0Processing(
function handleMem0Modal (line 2419) | function handleMem0Modal(sourceButtonId: string | null = null) {
function setInputValue (line 2429) | function setInputValue(inputElement: HTMLElement | null, value: string) {
function clickSendButtonWithDelay (line 2435) | function clickSendButtonWithDelay() {
function initializeMem0Integration (line 2453) | function initializeMem0Integration() {
function showLoginPopup (line 2513) | function showLoginPopup() {
function closeModal (line 2672) | function closeModal() {
FILE: src/replit/content.ts
type MutableMutationObserver (line 15) | type MutableMutationObserver = MutationObserver & {
function hookReplitBackgroundSearchTyping (line 145) | function hookReplitBackgroundSearchTyping() {
function getTextarea (line 166) | function getTextarea(): HTMLElement | null {
function setupInputObserver (line 198) | function setupInputObserver(): void {
function setInputValue (line 254) | function setInputValue(
function getContentWithoutMemories (line 308) | function getContentWithoutMemories(message: string | null = null): string {
function getMemoryEnabledState (line 342) | function getMemoryEnabledState(): Promise<boolean> {
function addSendButtonListener (line 355) | function addSendButtonListener(): void {
function handleMem0Modal (line 539) | async function handleMem0Modal() {
function initializeMem0Integration (line 680) | function initializeMem0Integration() {
function updateInputWithMemories (line 958) | function updateInputWithMemories() {
function showButtonPopup (line 982) | function showButtonPopup(button: HTMLElement, message: string): void {
function showLoginPopup (line 1040) | function showLoginPopup() {
function createMemoryModal (line 1189) | function createMemoryModal(memoryItems: MemoryItem[], isLoading: boolean...
function updateNotificationDot (line 2136) | function updateNotificationDot() {
FILE: src/search_tracker.ts
function normalize (line 8) | function normalize(text: string): string {
function getSettings (line 12) | function getSettings(): Promise<Settings> {
function maybeSend (line 38) | function maybeSend(engine: string, query: string): void {
function urlCapture (line 65) | function urlCapture(): void {
function installSpaUrlWatcher (line 107) | function installSpaUrlWatcher(): void {
FILE: src/selection_context.ts
function getSelectedText (line 33) | function getSelectedText(): string {
function showToast (line 43) | function showToast(message: string, variant: ToastVariant = ToastVariant...
FILE: src/sidebar.ts
function initializeMem0Sidebar (line 12) | function initializeMem0Sidebar(): void {
function toggleSidebar (line 44) | function toggleSidebar(): void {
function handleEscapeKey (line 77) | function handleEscapeKey(event: KeyboardEvent): void {
function handleOutsideClick (line 89) | function handleOutsideClick(event: MouseEvent): void {
function createSidebar (line 100) | function createSidebar(): void {
function saveSettings (line 445) | function saveSettings(
function setupEventListeners (line 553) | function setupEventListeners(
function fetchOrganizations (line 679) | function fetchOrganizations(): void {
function fetchProjects (line 739) | function fetchProjects(orgId: string, projectSelect: HTMLSelectElement):...
function fetchMemoriesAndCount (line 784) | function fetchMemoriesAndCount(): void {
function updateMemoryCount (line 835) | function updateMemoryCount(count: number | string): void {
function getHeaders (line 844) | function getHeaders(apiKey?: string, accessToken?: string): Record<strin...
function closeSearchInput (line 856) | function closeSearchInput(): void {
function filterMemories (line 869) | function filterMemories(searchTerm: string): void {
function addStyles (line 888) | function addStyles() {
function logout (line 1558) | function logout() {
function openDashboard (line 1582) | function openDashboard() {
function displayMemories (line 1592) | function displayMemories(memories: Memory[]): void {
function displayErrorMessage (line 1689) | function displayErrorMessage(message = 'Error loading memories') {
FILE: src/types/api.ts
type MessageRole (line 2) | enum MessageRole {
type ApiMessage (line 8) | type ApiMessage = {
type ApiMemoryRequest (line 14) | type ApiMemoryRequest = {
type MemorySearchResponse (line 29) | type MemorySearchResponse = Array<{
type LoginData (line 39) | type LoginData = Partial<{
constant DEFAULT_USER_ID (line 47) | const DEFAULT_USER_ID = 'chrome-extension-user';
constant SOURCE (line 50) | const SOURCE = 'OPENMEMORY_CHROME_EXTENSION';
FILE: src/types/browser.ts
type OnCommittedDetails (line 2) | type OnCommittedDetails = {
type JsonPrimitive (line 18) | type JsonPrimitive = string | number | boolean | null;
type JsonValue (line 20) | type JsonValue = JsonPrimitive | JsonValue[] | { [k: string]: JsonValue };
type JsonObject (line 22) | type JsonObject = { [k: string]: JsonValue };
type HistoryStateData (line 25) | type HistoryStateData = JsonObject | null;
type HistoryUrl (line 27) | type HistoryUrl = string | URL | null;
FILE: src/types/dom.ts
type Element (line 3) | interface Element {
type CSSStyleDeclaration (line 9) | interface CSSStyleDeclaration {
type Window (line 12) | interface Window {
type ExtendedHTMLElement (line 20) | type ExtendedHTMLElement = HTMLElement & {
type ExtendedDocument (line 25) | type ExtendedDocument = Document & {
type ExtendedElement (line 32) | type ExtendedElement = Element & {
type ModalDimensions (line 41) | type ModalDimensions = {
type ModalPosition (line 48) | type ModalPosition = {
type MutableMutationObserver (line 54) | type MutableMutationObserver = MutationObserver & {
FILE: src/types/memory.ts
type MemoryItem (line 2) | type MemoryItem = {
type Memory (line 13) | type Memory = Partial<{
type MemorySearchItem (line 20) | type MemorySearchItem = { id: string | number; memory: string; categorie...
type MemoriesResponse (line 23) | type MemoriesResponse = Partial<{
type OpenMemoryPrompts (line 29) | type OpenMemoryPrompts = {
type OptionalApiParams (line 36) | type OptionalApiParams = Partial<{
FILE: src/types/messages.ts
type ToastVariant (line 2) | enum ToastVariant {
type MessageType (line 8) | enum MessageType {
type SidebarAction (line 15) | enum SidebarAction {
type SelectionContextPayload (line 26) | type SelectionContextPayload = Partial<{
type SelectionContextResponse (line 33) | type SelectionContextResponse = Partial<{
type GetSelectionContextMessage (line 40) | type GetSelectionContextMessage = {
type ToastMessage (line 45) | type ToastMessage = {
type SelectionContextMessage (line 54) | type SelectionContextMessage = GetSelectionContextMessage | ToastMessage;
type SendResponse (line 57) | type SendResponse = (response: SelectionContextResponse) => void;
type ToggleSidebarMessage (line 59) | type ToggleSidebarMessage = {
type OpenPopupMessage (line 63) | type OpenPopupMessage = {
type ToggleMem0Message (line 67) | type ToggleMem0Message = {
type OpenDashboardMessage (line 72) | type OpenDashboardMessage = {
type SidebarActionMessage (line 77) | type SidebarActionMessage =
FILE: src/types/organizations.ts
type Organization (line 2) | type Organization = {
type Project (line 8) | type Project = {
FILE: src/types/providers.ts
type Provider (line 2) | enum Provider {
type Category (line 16) | enum Category {
FILE: src/types/settings.ts
type UserSettings (line 2) | type UserSettings = Partial<{
type SidebarSettings (line 14) | type SidebarSettings = {
type Settings (line 28) | type Settings = {
FILE: src/types/storage.ts
type StorageKey (line 2) | enum StorageKey {
type StorageItems (line 18) | type StorageItems = {
type StorageData (line 31) | type StorageData = Partial<{
FILE: src/utils/background_search.ts
type SearchStorage (line 4) | type SearchStorage = Partial<{
type FetchFn (line 15) | type FetchFn<T> = (query: string, opts: { signal?: AbortSignal }) => Pro...
type OrchestratorOptions (line 17) | interface OrchestratorOptions {
type OrchestratorState (line 34) | interface OrchestratorState {
type Orchestrator (line 43) | interface Orchestrator {
function normalizeQuery (line 59) | function normalizeQuery(s?: string | number | boolean): string {
function createOrchestrator (line 66) | function createOrchestrator(options: OrchestratorOptions): Orchestrator {
FILE: src/utils/llm_prompts.ts
constant OPENMEMORY_PROMPTS (line 1) | const OPENMEMORY_PROMPTS = {
method memory_header_html_strong (line 25) | get memory_header_html_strong() {
FILE: src/utils/site_config.ts
type InlinePlacement (line 2) | type InlinePlacement = {
type DockPlacement (line 9) | type DockPlacement = {
type FloatPlacement (line 17) | type FloatPlacement = {
type Placement (line 35) | type Placement = InlinePlacement | DockPlacement | FloatPlacement;
type SiteConfig (line 37) | interface SiteConfig {
type SiteConfigs (line 49) | interface SiteConfigs {
constant SITE_CONFIG (line 60) | const SITE_CONFIG = {
FILE: src/utils/util_functions.ts
type EventType (line 3) | type EventType = string;
type AdditionalData (line 4) | type AdditionalData = Record<string, unknown>;
type CallbackFunction (line 5) | type CallbackFunction = (success: boolean) => void;
type ExtensionEventPayload (line 7) | type ExtensionEventPayload = {
type BrowserType (line 18) | type BrowserType = 'Edge' | 'Opera' | 'Chrome' | 'Firefox' | 'Safari' | ...
FILE: src/utils/util_positioning.ts
type AnchorCandidate (line 2) | type AnchorCandidate =
type InlinePlacement (line 8) | type InlinePlacement = {
type DockPlacement (line 15) | type DockPlacement = {
type FloatPlacement (line 23) | type FloatPlacement = {
type Placement (line 41) | type Placement = InlinePlacement | DockPlacement | FloatPlacement;
type ApplyPlacementOptions (line 43) | type ApplyPlacementOptions = {
type FindAnchorOptions (line 49) | type FindAnchorOptions = {
type MountResilientOptions (line 55) | type MountResilientOptions = {
type MountOnEditorFocusOptions (line 64) | type MountOnEditorFocusOptions = {
type Stopper (line 76) | type Stopper = () => void;
type ShadowHost (line 78) | type ShadowHost = {
type CachedAnchorHint (line 83) | type CachedAnchorHint = {
function watchForRemoval (line 110) | function watchForRemoval(node: Node, onGone: () => void): MutationObserv...
function watchSpaNavigation (line 123) | function watchSpaNavigation(callback: () => void, intervalMs: number = 5...
function createShadowRootHost (line 163) | function createShadowRootHost(className: string = 'mem0-root'): ShadowHo...
function findAnchor (line 170) | async function findAnchor(
function applyPlacement (line 195) | function applyPlacement(opts: ApplyPlacementOptions): Stopper {
function validateCachedAnchor (line 351) | function validateCachedAnchor(sel: string | undefined, editor: Element |...
function selectorFor (line 366) | function selectorFor(el: Element | null): string {
function keyFor (line 386) | function keyFor(opts: { learnKey?: string }): string {
function now (line 391) | function now(): number {
function ver (line 395) | function ver(): string {
function getSession (line 404) | async function getSession<T = unknown>(k: string): Promise<T | null> {
function setSession (line 416) | async function setSession<T = unknown>(k: string, v: T): Promise<void> {
function getLocal (line 428) | async function getLocal<T = unknown>(k: string): Promise<T | null> {
function setLocal (line 440) | async function setLocal<T = unknown>(k: string, v: T): Promise<void> {
function delLocal (line 452) | async function delLocal(k: string | string[]): Promise<void> {
function resolveCachedAnchor (line 463) | async function resolveCachedAnchor(
function saveAnchorHint (line 493) | async function saveAnchorHint(
function mountResilient (line 513) | function mountResilient(opts: MountResilientOptions): Stopper {
function mountOnEditorFocus (line 608) | function mountOnEditorFocus(opts: MountOnEditorFocusOptions): Stopper {
constant OPENMEMORY_UI (line 699) | const OPENMEMORY_UI = {
Condensed preview — 43 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (772K chars).
[
{
"path": ".eslintignore",
"chars": 66,
"preview": "dist/\nnode_modules/\n*.min.js\nicons/\npackage-lock.json\n*.log\n.env*\n"
},
{
"path": ".eslintrc.json",
"chars": 1473,
"preview": "{\n \"root\": true,\n \"env\": {\n \"browser\": true,\n \"es2022\": true,\n \"webextensions\": true\n },\n \"extends\": [\n "
},
{
"path": ".gitignore",
"chars": 313,
"preview": "# Node modules\nnode_modules/\n\n# Build output\ndist/\nbuild/\n\n# Logs\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# OS g"
},
{
"path": ".prettierignore",
"chars": 66,
"preview": "dist/\nnode_modules/\n*.min.js\nicons/\npackage-lock.json\n*.log\n.env*\n"
},
{
"path": ".prettierrc",
"chars": 394,
"preview": "{\n \"semi\": true,\n \"trailingComma\": \"es5\",\n \"singleQuote\": true,\n \"printWidth\": 100,\n \"tabWidth\": 2,\n \"useTabs\": fa"
},
{
"path": "LICENSE",
"chars": 1070,
"preview": "MIT License\n\nCopyright (c) 2024 Deshraj Yadav\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
},
{
"path": "README.md",
"chars": 3769,
"preview": "> [!CAUTION]\n> ## This project has been archived\n>\n> **mem0-chrome-extension** is no longer actively maintained and this"
},
{
"path": "docs/types.md",
"chars": 3505,
"preview": "# Types Documentation\n\nThis section contains TypeScript type definitions for the Mem0 Chrome Extension. The types are or"
},
{
"path": "manifest.json",
"chars": 2135,
"preview": "{\n \"manifest_version\": 3,\n \"name\": \"OpenMemory\",\n \"version\": \"1.3.17\",\n \"description\": \"🧠 OpenMemory keeps your conv"
},
{
"path": "package.json",
"chars": 896,
"preview": "{\n \"name\": \"mem0-extension\",\n \"description\": \"🧠 OpenMemory keeps your conversations in sync. 🔄 No more repeating yours"
},
{
"path": "privacy-policy.md",
"chars": 1944,
"preview": "# Privacy Policy for Claude Memory\n\nLast updated: 09-06-2024\n\n## Introduction\n\nWelcome to Claude Memory (\"we\", \"our\", or"
},
{
"path": "src/background.ts",
"chars": 1047,
"preview": "import { initContextMenuMemory } from './context-menu-memory';\nimport { initDirectUrlTracking } from './direct-url-track"
},
{
"path": "src/chatgpt/content.ts",
"chars": 83886,
"preview": "/* eslint-disable @typescript-eslint/no-non-null-assertion */\nimport { DEFAULT_USER_ID, type LoginData, MessageRole } fr"
},
{
"path": "src/claude/content.ts",
"chars": 115444,
"preview": "import { MessageRole } from '../types/api';\nimport type { HistoryStateData } from '../types/browser';\nimport type { Exte"
},
{
"path": "src/context-menu-memory.ts",
"chars": 5734,
"preview": "import { type ApiMemoryRequest, DEFAULT_USER_ID, MessageRole, SOURCE } from './types/api';\nimport {\n MessageType,\n typ"
},
{
"path": "src/deepseek/content.ts",
"chars": 93458,
"preview": "import { MessageRole } from '../types/api';\nimport type { ExtendedHTMLElement } from '../types/dom';\nimport type { Memor"
},
{
"path": "src/direct-url-tracker.ts",
"chars": 5650,
"preview": "import { type ApiMemoryRequest, DEFAULT_USER_ID, MessageRole, SOURCE } from './types/api';\nimport type { OnCommittedDeta"
},
{
"path": "src/gemini/content.ts",
"chars": 76382,
"preview": "import { MessageRole } from '../types/api';\nimport type { ExtendedElement, MutableMutationObserver } from '../types/dom'"
},
{
"path": "src/grok/content.ts",
"chars": 71740,
"preview": "/* eslint-disable @typescript-eslint/no-non-null-assertion */\nimport { MessageRole } from '../types/api';\nimport type { "
},
{
"path": "src/mem0/content.ts",
"chars": 869,
"preview": "import { getBrowser, sendExtensionEvent } from '../utils/util_functions';\n\nfunction fetchAndSaveSession() {\n fetch('htt"
},
{
"path": "src/perplexity/content.ts",
"chars": 89937,
"preview": "/* eslint-disable @typescript-eslint/no-non-null-assertion */\nimport { MessageRole } from '../types/api';\nimport type { "
},
{
"path": "src/popup.html",
"chars": 2146,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-w"
},
{
"path": "src/popup.ts",
"chars": 1352,
"preview": "import { DEFAULT_USER_ID } from './types/api';\nimport { SidebarAction } from './types/messages';\nimport { StorageKey } f"
},
{
"path": "src/replit/content.ts",
"chars": 73095,
"preview": "/* eslint-disable @typescript-eslint/no-non-null-assertion */\n/* eslint-disable no-inner-declarations */\nimport { Messag"
},
{
"path": "src/search_tracker.ts",
"chars": 3661,
"preview": "import { DEFAULT_USER_ID } from './types/api';\nimport type { HistoryStateData, HistoryUrl } from './types/browser';\nimpo"
},
{
"path": "src/selection_context.ts",
"chars": 2113,
"preview": "import {\n MessageType,\n type SelectionContextMessage,\n type SelectionContextPayload,\n type SendResponse,\n ToastVari"
},
{
"path": "src/sidebar.ts",
"chars": 56393,
"preview": "import { DEFAULT_USER_ID } from './types/api';\nimport type { MemoriesResponse, Memory } from './types/memory';\nimport { "
},
{
"path": "src/types/api.ts",
"chars": 1058,
"preview": "/** message roles (User, Assistant) */\nexport enum MessageRole {\n User = 'user',\n Assistant = 'assistant',\n}\n\n/** Mess"
},
{
"path": "src/types/browser.ts",
"chars": 931,
"preview": "/** Web navigation event details */\nexport type OnCommittedDetails = {\n tabId: number;\n url: string;\n processId: numb"
},
{
"path": "src/types/dom.ts",
"chars": 1419,
"preview": "// Global interfaces for DOM extensions\ndeclare global {\n interface Element {\n value?: string;\n disabled?: boolea"
},
{
"path": "src/types/memory.ts",
"chars": 957,
"preview": "/** Individual memory structure with id, text, and categories */\nexport type MemoryItem = {\n id?: string;\n text: strin"
},
{
"path": "src/types/messages.ts",
"chars": 2013,
"preview": "/** Enum for toast notification types (SUCCESS, ERROR) */\nexport enum ToastVariant {\n SUCCESS = 'success',\n ERROR = 'e"
},
{
"path": "src/types/organizations.ts",
"chars": 233,
"preview": "/** Organization structure with org_id and name */\nexport type Organization = {\n org_id: string;\n name: string;\n};\n\n/*"
},
{
"path": "src/types/providers.ts",
"chars": 480,
"preview": "/** Enum for supported AI providers */\nexport enum Provider {\n ContextMenu = 'ContextMenu',\n DirectURL = 'DirectURL',\n"
},
{
"path": "src/types/settings.ts",
"chars": 932,
"preview": "/** User preference structure with API keys, memory settings, and thresholds */\nexport type UserSettings = Partial<{\n a"
},
{
"path": "src/types/storage.ts",
"chars": 1088,
"preview": "/** Enum for all storage keys used in the extension */\nexport enum StorageKey {\n API_KEY = 'apiKey',\n ACCESS_TOKEN = '"
},
{
"path": "src/utils/background_search.ts",
"chars": 7029,
"preview": "import type { MemorySearchItem } from '../types/memory';\nimport type { StorageKey } from '../types/storage';\n\nexport typ"
},
{
"path": "src/utils/llm_prompts.ts",
"chars": 1895,
"preview": "export const OPENMEMORY_PROMPTS = {\n rerank_system_prompt: `\nYou are OpenMemory Filterer.\n\nYour tasks:\n1) From the prov"
},
{
"path": "src/utils/site_config.ts",
"chars": 4727,
"preview": "// --- Types ---\ntype InlinePlacement = {\n strategy: 'inline';\n where?: 'beforebegin' | 'afterbegin' | 'beforeend' | '"
},
{
"path": "src/utils/util_functions.ts",
"chars": 3118,
"preview": "import { StorageKey, type StorageData } from '../types/storage';\n\ntype EventType = string;\ntype AdditionalData = Record<"
},
{
"path": "src/utils/util_positioning.ts",
"chars": 18683,
"preview": "/* eslint-disable no-empty */\ntype AnchorCandidate =\n | string\n | {\n find: () => Element | null;\n };\n\ntype Inl"
},
{
"path": "tsconfig.json",
"chars": 588,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"lib\": [\"ES2022\", \"DOM\"],\n \"module\": \"ES2022\",\n \"moduleResolu"
},
{
"path": "vite.config.ts",
"chars": 233,
"preview": "import { defineConfig } from 'vite';\nimport { crx } from '@crxjs/vite-plugin';\nimport manifest from './manifest.json';\n\n"
}
]
About this extraction
This page contains the full source code of the deshraj/claude-memory GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 43 files (726.5 KB), approximately 179.1k tokens, and a symbol index with 292 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.