feat(browser-extension): folder level x bookmarks import with project selection (#495)

Feature : Import folder level x bookmarks

[Screen Recording 2025-10-17 at 1.37.52 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/user-attachments/thumbnails/15cd60ff-856e-4f29-8897-74ae3c869c87.mov" />](https://app.graphite.dev/user-attachments/video/15cd60ff-856e-4f29-8897-74ae3c869c87.mov)
This commit is contained in:
MaheshtheDev 2025-10-19 00:38:47 +00:00
parent 5a001c639a
commit 809eaa0d49
8 changed files with 573 additions and 18 deletions

View file

@ -1,4 +1,9 @@
import { getDefaultProject, saveMemory, searchMemories } from "../utils/api"
import {
getDefaultProject,
saveMemory,
searchMemories,
fetchProjects,
} from "../utils/api"
import {
CONTAINER_TAGS,
CONTEXT_MENU_IDS,
@ -169,6 +174,9 @@ export default defineBackground(() => {
// Handle Twitter import request
if (message.type === MESSAGE_TYPES.BATCH_IMPORT_ALL) {
const importConfig: TwitterImportConfig = {
isFolderImport: message.isFolderImport,
bookmarkCollectionId: message.bookmarkCollectionId,
selectedProject: message.selectedProject,
onProgress: sendMessageToCurrentTab,
onComplete: sendImportDoneMessage,
onError: async (error: Error) => {
@ -249,6 +257,21 @@ export default defineBackground(() => {
})()
return true
}
if (message.action === MESSAGE_TYPES.FETCH_PROJECTS) {
;(async () => {
try {
const projects = await fetchProjects()
sendResponse({ success: true, data: projects })
} catch (error) {
sendResponse({
success: false,
error: error instanceof Error ? error.message : "Unknown error",
})
}
})()
return true
}
},
)
})

View file

@ -5,7 +5,26 @@ import {
POSTHOG_EVENT_KEY,
} from "../../utils/constants"
import { trackEvent } from "../../utils/posthog"
import { createTwitterImportButton, DOMUtils } from "../../utils/ui-components"
import {
createTwitterImportButton,
createProjectSelectionModal,
DOMUtils,
} from "../../utils/ui-components"
async function loadSpaceGroteskFonts(): Promise<void> {
if (document.getElementById("supermemory-modal-styles")) {
return Promise.resolve()
}
const style = document.createElement("style")
style.id = "supermemory-modal-styles"
style.textContent = `
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap');
`
document.head.appendChild(style)
await document.fonts.ready
}
export function initializeTwitter() {
if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
@ -16,6 +35,7 @@ export function initializeTwitter() {
if (window.location.pathname === "/i/bookmarks") {
setTimeout(() => {
addTwitterImportButton()
addTwitterImportButtonForFolders()
}, 2000)
} else {
// Remove button if not on bookmarks page
@ -26,7 +46,6 @@ export function initializeTwitter() {
}
function addTwitterImportButton() {
// Only show the import button on the bookmarks page
if (window.location.pathname !== "/i/bookmarks") {
return
}
@ -51,6 +70,46 @@ function addTwitterImportButton() {
document.body.appendChild(button)
}
function addTwitterImportButtonForFolders() {
if (window.location.pathname !== "/i/bookmarks") {
return
}
const targetElements = document.querySelectorAll(
".css-175oi2r.r-1wtj0ep.r-16x9es5.r-1mmae3n.r-o7ynqc.r-6416eg.r-1ny4l3l.r-1loqt21",
)
targetElements.forEach((element) => {
addButtonToElement(element as HTMLElement)
})
}
function addButtonToElement(element: HTMLElement) {
if (element.querySelector("[data-supermemory-button]")) {
return
}
loadSpaceGroteskFonts()
const button = createSaveTweetElement(async () => {
const url = element.getAttribute("href")
const bookmarkCollectionId = url?.split("/").pop()
console.log("Bookmark collection ID:", bookmarkCollectionId)
if (bookmarkCollectionId) {
await showProjectSelectionModal(bookmarkCollectionId)
}
})
button.setAttribute("data-supermemory-button", "true")
element.appendChild(button)
element.style.flexDirection = "row"
element.style.alignItems = "center"
element.style.justifyContent = "center"
element.style.gap = "10px"
element.style.padding = "10px"
}
export function updateTwitterImportUI(message: {
type: string
importedMessage?: string
@ -94,9 +153,120 @@ export function handleTwitterNavigation() {
if (window.location.pathname === "/i/bookmarks") {
addTwitterImportButton()
addTwitterImportButtonForFolders()
} else {
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)
}
document.querySelectorAll("[data-supermemory-button]").forEach((button) => {
button.remove()
})
}
}
}
/**
* Shows the project selection modal for folder imports
* @param bookmarkCollectionId - The ID of the bookmark collection to import
*/
async function showProjectSelectionModal(bookmarkCollectionId: string) {
try {
const modal = createProjectSelectionModal(
[],
async (selectedProject) => {
modal.remove()
try {
await browser.runtime.sendMessage({
type: MESSAGE_TYPES.BATCH_IMPORT_ALL,
isFolderImport: true,
bookmarkCollectionId: bookmarkCollectionId,
selectedProject: selectedProject,
})
} catch (error) {
console.error("Error importing bookmarks:", error)
}
},
() => {
modal.remove()
},
)
document.body.appendChild(modal)
try {
const response = await browser.runtime.sendMessage({
action: MESSAGE_TYPES.FETCH_PROJECTS,
})
if (response.success && response.data) {
const projects = response.data
if (projects.length === 0) {
console.warn("No projects available for import")
updateModalWithProjects(modal, [])
} else {
updateModalWithProjects(modal, projects)
}
} else {
console.error("Failed to fetch projects:", response.error)
updateModalWithProjects(modal, [])
}
} catch (error) {
console.error("Error fetching projects:", error)
updateModalWithProjects(modal, [])
}
} catch (error) {
console.error("Error showing project selection modal:", error)
}
}
/**
* Updates the modal with fetched projects
* @param modal - The modal element
* @param projects - Array of projects to populate the dropdown
*/
function updateModalWithProjects(
modal: HTMLElement,
projects: Array<{ id: string; name: string; containerTag: string }>,
) {
const select = modal.querySelector("#project-select") as HTMLSelectElement
if (!select) return
while (select.children.length > 1) {
select.removeChild(select.children[1])
}
if (projects.length === 0) {
const noProjectsOption = document.createElement("option")
noProjectsOption.value = ""
noProjectsOption.textContent = "No projects available"
noProjectsOption.disabled = true
select.appendChild(noProjectsOption)
const importButton = modal.querySelector(
"button:last-child",
) as HTMLButtonElement
if (importButton) {
importButton.disabled = true
importButton.style.cssText = `
padding: 10px 16px;
border: none;
border-radius: 8px;
background: #d1d5db;
color: #9ca3af;
font-size: 14px;
font-weight: 500;
cursor: not-allowed;
transition: all 0.2s ease;
`
}
} else {
projects.forEach((project) => {
const option = document.createElement("option")
option.value = project.id
option.textContent = project.name
option.dataset.containerTag = project.containerTag
select.appendChild(option)
})
}
}

View file

@ -35,6 +35,7 @@ export const ELEMENT_IDS = {
CHATGPT_INPUT_BAR_ELEMENT: "sm-chatgpt-input-bar-element",
CLAUDE_INPUT_BAR_ELEMENT: "sm-claude-input-bar-element",
T3_INPUT_BAR_ELEMENT: "sm-t3-input-bar-element",
PROJECT_SELECTION_MODAL: "sm-project-selection-modal",
} as const
/**
@ -81,6 +82,7 @@ export const MESSAGE_TYPES = {
IMPORT_DONE: "sm-import-done",
GET_RELATED_MEMORIES: "sm-get-related-memories",
CAPTURE_PROMPT: "sm-capture-prompt",
FETCH_PROJECTS: "sm-fetch-projects",
} as const
export const CONTEXT_MENU_IDS = {

View file

@ -2,12 +2,14 @@
* Twitter Bookmarks Import Module
* Handles the import process for Twitter bookmarks
*/
import { saveAllTweets } from "./api"
import type { MemoryPayload } from "./types"
import { createTwitterAPIHeaders, getTwitterTokens } from "./twitter-auth"
import {
BOOKMARKS_URL,
BOOKMARK_COLLECTION_URL,
buildRequestVariables,
buildBookmarkCollectionVariables,
extractNextCursor,
getAllTweets,
type TwitterAPIResponse,
@ -18,6 +20,13 @@ export type ImportProgressCallback = (message: string) => Promise<void>
export type ImportCompleteCallback = (totalImported: number) => Promise<void>
export interface TwitterImportConfig {
isFolderImport?: boolean
bookmarkCollectionId?: string
selectedProject?: {
id: string
name: string
containerTag: string
}
onProgress: ImportProgressCallback
onComplete: ImportCompleteCallback
onError: (error: Error) => Promise<void>
@ -81,7 +90,11 @@ export class TwitterImporter {
* @param cursor - Pagination cursor for Twitter API
* @param totalImported - Number of tweets imported so far
*/
private async batchImportAll(cursor = "", totalImported = 0, uniqueGroupId = "twitter_bookmarks"): Promise<void> {
private async batchImportAll(
cursor = "",
totalImported = 0,
uniqueGroupId = "twitter_bookmarks",
): Promise<void> {
try {
// Use a local variable to track imported count
let importedCount = totalImported
@ -99,10 +112,19 @@ export class TwitterImporter {
const headers = createTwitterAPIHeaders(tokens)
// Build API request with pagination
const variables = buildRequestVariables(cursor)
const variables =
this.config.isFolderImport && this.config.bookmarkCollectionId
? buildBookmarkCollectionVariables(this.config.bookmarkCollectionId)
: buildRequestVariables(cursor)
const urlWithCursor = cursor
? `${BOOKMARKS_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}`
: BOOKMARKS_URL
? `${
this.config.isFolderImport && this.config.bookmarkCollectionId
? `${BOOKMARK_COLLECTION_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}`
: BOOKMARKS_URL
}&variables=${encodeURIComponent(JSON.stringify(variables))}`
: this.config.isFolderImport && this.config.bookmarkCollectionId
? `${BOOKMARK_COLLECTION_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}`
: `${BOOKMARKS_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}`
const response = await fetch(urlWithCursor, {
method: "GET",
@ -140,14 +162,20 @@ export class TwitterImporter {
retweets: tweet.retweet_count || 0,
sm_internal_group_id: uniqueGroupId,
}
const containerTag =
this.config.selectedProject?.containerTag ||
"sm_project_twitter_bookmarks"
documents.push({
containerTags: ["sm_project_twitter_bookmarks"],
containerTags: [containerTag],
content: `https://x.com/${tweet.user.screen_name}/status/${tweet.id_str}`,
metadata,
customId: tweet.id_str,
})
importedCount++
await this.config.onProgress(`Imported ${importedCount} tweets, so far...`)
await this.config.onProgress(
`Imported ${importedCount} tweets, so far...`,
)
} catch (error) {
console.error("Error importing tweet:", error)
}
@ -167,13 +195,15 @@ export class TwitterImporter {
// Handle pagination
const instructions =
data.data?.bookmark_timeline_v2?.timeline?.instructions
const nextCursor = extractNextCursor(instructions || [])
data.data?.bookmark_timeline_v2?.timeline?.instructions ||
data.data?.bookmark_collection_timeline?.timeline?.instructions ||
[]
const nextCursor = extractNextCursor(instructions)
console.log("Next cursor:", nextCursor)
console.log("Tweets length:", tweets.length)
if (nextCursor && tweets.length > 0) {
if (nextCursor && tweets.length > 0 && !this.config.isFolderImport) {
await new Promise((resolve) => setTimeout(resolve, 1000)) // Rate limiting
await this.batchImportAll(nextCursor, importedCount, uniqueGroupId)
} else {

View file

@ -119,7 +119,19 @@ export interface Tweet {
export interface TwitterAPIResponse {
data: {
bookmark_timeline_v2: {
bookmark_timeline_v2?: {
timeline: {
instructions: Array<{
type: string
entries?: Array<{
entryId: string
sortIndex: string
content: Record<string, unknown>
}>
}>
}
}
bookmark_collection_timeline?: {
timeline: {
instructions: Array<{
type: string
@ -167,8 +179,49 @@ export const TWITTER_API_FEATURES = {
verified_phone_label_enabled: true,
}
// Twitter API features configuration for BookmarkFolderTimeline
export const TWITTER_BOOKMARK_FOLDER_FEATURES = {
rweb_video_screen_enabled: false,
payments_enabled: false,
profile_label_improvements_pcf_label_in_post_enabled: true,
responsive_web_profile_redirect_enabled: false,
rweb_tipjar_consumption_enabled: true,
verified_phone_label_enabled: false,
creator_subscriptions_tweet_preview_api_enabled: true,
responsive_web_graphql_timeline_navigation_enabled: true,
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
premium_content_api_read_enabled: false,
communities_web_enable_tweet_community_results_fetch: true,
c9s_tweet_anatomy_moderator_badge_enabled: true,
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
responsive_web_grok_analyze_post_followups_enabled: true,
responsive_web_jetfuel_frame: true,
responsive_web_grok_share_attachment_enabled: true,
articles_preview_enabled: true,
responsive_web_edit_tweet_api_enabled: true,
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
view_counts_everywhere_api_enabled: true,
longform_notetweets_consumption_enabled: true,
responsive_web_twitter_article_tweet_consumption_enabled: true,
tweet_awards_web_tipping_enabled: false,
responsive_web_grok_show_grok_translated_post: true,
responsive_web_grok_analysis_button_from_backend: true,
creator_subscriptions_quote_tweet_preview_enabled: false,
freedom_of_speech_not_reach_fetch_enabled: true,
standardized_nudges_misinfo: true,
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
longform_notetweets_rich_text_read_enabled: true,
longform_notetweets_inline_media_enabled: true,
responsive_web_grok_image_annotation_enabled: true,
responsive_web_grok_imagine_annotation_enabled: true,
responsive_web_grok_community_note_auto_translation_is_enabled: false,
responsive_web_enhance_cards_enabled: false,
}
export const BOOKMARKS_URL = `https://x.com/i/api/graphql/xLjCVTqYWz8CGSprLU349w/Bookmarks?features=${encodeURIComponent(JSON.stringify(TWITTER_API_FEATURES))}`
export const BOOKMARK_COLLECTION_URL = `https://x.com/i/api/graphql/I8Y9ni1dqP-ZSpwxqJQ--Q/BookmarkFolderTimeline?features=${encodeURIComponent(JSON.stringify(TWITTER_BOOKMARK_FOLDER_FEATURES))}`
/**
* Transform raw Twitter API response data into standardized Tweet format
*/
@ -264,7 +317,9 @@ export function getAllTweets(data: TwitterAPIResponse): Tweet[] {
try {
const instructions =
data.data?.bookmark_timeline_v2?.timeline?.instructions || []
data.data?.bookmark_timeline_v2?.timeline?.instructions ||
data.data?.bookmark_collection_timeline?.timeline?.instructions ||
[]
for (const instruction of instructions) {
if (instruction.type === "TimelineAddEntries" && instruction.entries) {
@ -375,3 +430,13 @@ export function buildRequestVariables(cursor?: string, count = 100) {
return variables
}
/**
* Build Twitter API request variables for bookmark collection
*/
export function buildBookmarkCollectionVariables(bookmarkCollectionId: string) {
return {
bookmark_collection_id: bookmarkCollectionId,
includePromotedContent: true,
}
}

View file

@ -11,6 +11,8 @@ export type ToastState = "loading" | "success" | "error"
* Message types for extension communication
*/
export interface ExtensionMessage {
isFolderImport?: boolean
bookmarkCollectionId?: string
action?: string
type?: string
data?: unknown
@ -18,6 +20,11 @@ export interface ExtensionMessage {
importedMessage?: string
totalImported?: number
actionSource?: string
selectedProject?: {
id: string
name: string
containerTag: string
}
}
/**

View file

@ -181,6 +181,7 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement {
align-items: center;
gap: 8px;
transition: all 0.2s ease;
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`
const iconUrl = browser.runtime.getURL("/icon-16.png")
@ -214,7 +215,6 @@ export function createSaveTweetElement(onClick: () => void): HTMLElement {
iconButton.style.cssText = `
display: inline-flex;
align-items: flex-end;
opacity: 0.7;
justify-content: center;
width: 20px;
height: 20px;
@ -378,6 +378,264 @@ export function createT3InputBarElement(onClick: () => void): HTMLElement {
return iconButton
}
/**
* Creates a project selection modal for Twitter folder imports
* @param projects - Array of available projects
* @param onImport - Callback when import is clicked with selected project
* @param onClose - Callback when modal is closed
* @returns HTMLElement - The modal element
*/
export function createProjectSelectionModal(
projects: Array<{ id: string; name: string; containerTag: string }>,
onImport: (project: {
id: string
name: string
containerTag: string
}) => void,
onClose: () => void,
): HTMLElement {
const modal = document.createElement("div")
modal.id = "sm-project-selection-modal"
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2147483648;
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`
const dialog = document.createElement("div")
dialog.style.cssText = `
background: #05070A;
border-radius: 12px;
padding: 24px;
max-width: 400px;
width: 90%;
box-shadow: 0 8px 32px rgba(5, 7, 10, 0.2);
position: relative;
`
const header = document.createElement("div")
header.style.cssText = `
margin-bottom: 20px;
`
const iconUrl = browser.runtime.getURL("/icon-16.png")
header.innerHTML = `
<div style="display: flex; flex-direction: column; gap: 8px;">
<h3 style="margin: 0; font-size: 16px; font-weight: 600; color: #ffffff; display: flex; align-items: center; gap: 8px;">
<img src="${iconUrl}" width="20" height="20" alt="Supermemory" style="border-radius: 4px;" />
Import to Supermemory
</h3>
<p style="margin: 0; font-size: 14px; font-weight: 400; color: #ffffff; opacity: 0.7;">
The project you want to import your bookmarks to.
</p>
</div>
`
const form = document.createElement("div")
form.style.cssText = `
display: flex;
flex-direction: column;
gap: 16px;
`
const selectContainer = document.createElement("div")
selectContainer.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
`
const label = document.createElement("label")
label.style.cssText = `
font-size: 14px;
font-weight: 500;
color: #ffffff;
`
label.textContent = "Select Project to import"
const select = document.createElement("select")
select.id = "project-select"
select.style.cssText = `
padding: 12px 40px 12px 16px;
border: none;
border-radius: 12px;
font-size: 14px;
background: rgba(91, 126, 245, 0.04);
box-shadow: -1px -1px 1px 0 rgba(82, 89, 102, 0.08) inset, 2px 2px 1px 0 rgba(0, 0, 0, 0.50) inset;
color: #ffffff;
cursor: pointer;
transition: border-color 0.2s ease;
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6,9 12,15 18,9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 16px center;
background-size: 16px;
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`
select.addEventListener("focus", () => {
select.style.borderColor = "#1A88FF"
})
select.addEventListener("blur", () => {
select.style.borderColor = "#374151"
})
// Add default option
const defaultOption = document.createElement("option")
defaultOption.value = ""
defaultOption.textContent = "Choose a project..."
defaultOption.disabled = true
defaultOption.selected = true
select.appendChild(defaultOption)
// Add project options
projects.forEach((project) => {
const option = document.createElement("option")
option.value = project.id
option.textContent = project.name
option.dataset.containerTag = project.containerTag
select.appendChild(option)
})
const buttonContainer = document.createElement("div")
buttonContainer.style.cssText = `
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 8px;
`
const cancelButton = document.createElement("button")
cancelButton.textContent = "Cancel"
cancelButton.style.cssText = `
padding: 10px 16px;
color: #ffffff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 10px;
border: none;
background: #05070A;
`
cancelButton.addEventListener("mouseenter", () => {
cancelButton.style.backgroundColor = "#f9fafb"
cancelButton.style.color = "#05070A"
})
cancelButton.addEventListener("mouseleave", () => {
cancelButton.style.backgroundColor = "#05070A"
cancelButton.style.color = "#ffffff"
})
const importButton = document.createElement("button")
importButton.textContent = "Import"
importButton.style.cssText = `
padding: 10px 16px;
border: none;
border-radius: 12px;
background: #d1d5db;
color: #9ca3af;
font-size: 14px;
font-weight: 500;
cursor: not-allowed;
transition: all 0.2s ease;
`
importButton.disabled = true
// Handle project selection
select.addEventListener("change", () => {
const selectedOption = select.options[select.selectedIndex]
if (selectedOption.value) {
importButton.disabled = false
importButton.style.cssText = `
padding: 10px 16px;
border: none;
border-radius: 12px;
background: linear-gradient(203deg, #0FF0D2 -49.88%, #5BD3FB -33.14%, #1E0FF0 81.81%);
box-shadow: 1px 1px 2px 1px #1A88FF inset, 0 2px 10px 0 rgba(5, 1, 0, 0.20);
color: #ffffff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
`
} else {
importButton.disabled = true
importButton.style.cssText = `
padding: 10px 16px;
border: none;
border-radius: 8px;
background: #d1d5db;
color: #9ca3af;
font-size: 14px;
font-weight: 500;
cursor: not-allowed;
transition: all 0.2s ease;
`
}
})
// Handle import button click
importButton.addEventListener("click", () => {
const selectedOption = select.options[select.selectedIndex]
if (selectedOption.value) {
const selectedProject = {
id: selectedOption.value,
name: selectedOption.textContent,
containerTag: selectedOption.dataset.containerTag || "",
}
onImport(selectedProject)
}
})
// Handle cancel button click
cancelButton.addEventListener("click", onClose)
// Handle overlay click to close
modal.addEventListener("click", (e) => {
if (e.target === modal) {
onClose()
}
})
// Handle escape key
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
onClose()
}
}
document.addEventListener("keydown", handleKeyDown)
// Clean up event listener when modal is removed
const observer = new MutationObserver(() => {
if (!document.contains(modal)) {
document.removeEventListener("keydown", handleKeyDown)
observer.disconnect()
}
})
observer.observe(document.body, { childList: true, subtree: true })
selectContainer.appendChild(label)
selectContainer.appendChild(select)
form.appendChild(selectContainer)
buttonContainer.appendChild(cancelButton)
buttonContainer.appendChild(importButton)
form.appendChild(buttonContainer)
dialog.appendChild(header)
dialog.appendChild(form)
modal.appendChild(dialog)
return modal
}
/**
* Utility functions for DOM manipulation
*/

View file

@ -11,7 +11,7 @@ export default defineConfig({
manifest: {
name: "supermemory",
homepage_url: "https://supermemory.ai",
version: "6.0.003",
version: "6.0.1",
permissions: ["contextMenus", "storage", "activeTab", "webRequest", "tabs"],
host_permissions: [
"*://x.com/*",