sync mode dropdown — entire vault or selected folders

This commit is contained in:
Sreeram Sreedhar 2026-05-21 17:50:51 -04:00
parent d5aa92167d
commit b6a09f8f88
4 changed files with 85 additions and 6 deletions

View file

@ -12,6 +12,8 @@ const DEFAULT_SETTINGS: SupermemorySettings = {
connectionId: "",
syncOnSave: true,
syncOnStartup: true,
syncMode: "all",
includedFolders: "",
}
export default class SupermemoryPlugin extends Plugin {
@ -101,7 +103,9 @@ export default class SupermemoryPlugin extends Plugin {
if (!connectionId) return
if (!this.syncEngine) {
this.syncEngine = new SyncEngine(this.app, connectionId)
this.syncEngine = new SyncEngine(this.app, connectionId, this.settings)
} else {
this.syncEngine.updateSettings(this.settings)
}
await this.syncEngine.fullSync()
@ -118,5 +122,6 @@ export default class SupermemoryPlugin extends Plugin {
async saveSettings() {
await this.saveData(this.settings)
configure(this.settings)
this.syncEngine?.updateSettings(this.settings)
}
}

View file

@ -1,5 +1,6 @@
import { App, PluginSettingTab, Setting } from "obsidian"
import type SupermemoryPlugin from "./main"
import type { SyncMode } from "./types"
export class SupermemorySettingTab extends PluginSettingTab {
plugin: SupermemoryPlugin
@ -39,5 +40,37 @@ export class SupermemorySettingTab extends PluginSettingTab {
await this.plugin.saveSettings()
}),
)
new Setting(containerEl)
.setName("Sync mode")
.setDesc("Which notes in your vault to sync to Supermemory.")
.addDropdown((dropdown) =>
dropdown
.addOption("all", "Entire vault")
.addOption("folders", "Only selected folders")
.setValue(this.plugin.settings.syncMode)
.onChange(async (value) => {
this.plugin.settings.syncMode = value as SyncMode
await this.plugin.saveSettings()
this.display()
}),
)
if (this.plugin.settings.syncMode === "folders") {
new Setting(containerEl)
.setName("Folders to sync")
.setDesc(
"Comma-separated folder paths (e.g. Projects, Daily). Subfolders are included.",
)
.addText((text) =>
text
.setPlaceholder("Projects, Daily")
.setValue(this.plugin.settings.includedFolders)
.onChange(async (value) => {
this.plugin.settings.includedFolders = value
await this.plugin.saveSettings()
}),
)
}
}
}

View file

@ -1,5 +1,6 @@
import { type App, Notice, TFile, parseFrontMatterEntry, parseFrontMatterTags } from "obsidian"
import { type App, Notice, TFile, parseFrontMatterTags } from "obsidian"
import { pushDeletions, pushNotes, type NotePayload } from "./api"
import type { SupermemorySettings } from "./types"
const BATCH_SIZE = 50
const DEBOUNCE_MS = 3000
@ -12,13 +13,41 @@ interface SyncState {
export class SyncEngine {
private app: App
private connectionId: string
private settings: SupermemorySettings
private state: SyncState = { syncing: false, lastFullSync: 0 }
private pendingChanges: Map<string, "upsert" | "delete"> = new Map()
private debounceTimer: ReturnType<typeof setTimeout> | null = null
constructor(app: App, connectionId: string) {
constructor(app: App, connectionId: string, settings: SupermemorySettings) {
this.app = app
this.connectionId = connectionId
this.settings = settings
}
updateSettings(settings: SupermemorySettings) {
this.settings = settings
}
private shouldSync(file: TFile): boolean {
if (file.extension !== "md") return false
const mode = this.settings.syncMode
if (mode === "all") return true
if (mode === "folders") {
const folders = this.settings.includedFolders
.split(",")
.map((f) => f.trim().replace(/^\/+|\/+$/g, ""))
.filter((f) => f.length > 0)
if (folders.length === 0) return false
return folders.some(
(folder) =>
file.path === `${folder}.md` ||
file.path.startsWith(`${folder}/`),
)
}
return false
}
async fullSync(): Promise<{ queued: number; failed: number }> {
@ -32,7 +61,9 @@ export class SyncEngine {
let totalFailed = 0
try {
const files = this.app.vault.getMarkdownFiles()
const files = this.app.vault
.getMarkdownFiles()
.filter((f) => this.shouldSync(f))
const batches = this.chunk(files, BATCH_SIZE)
for (const batch of batches) {
@ -60,13 +91,16 @@ export class SyncEngine {
}
onFileChange(file: TFile) {
if (!(file instanceof TFile) || file.extension !== "md") return
if (!(file instanceof TFile) || !this.shouldSync(file)) return
this.pendingChanges.set(file.path, "upsert")
this.schedulePush()
}
onFileDelete(file: TFile) {
if (!(file instanceof TFile) || file.extension !== "md") return
// Always allow deletes through (file may have been previously synced
// before its folder was excluded). The server safely no-ops on unknown
// customIds.
this.pendingChanges.set(file.path, "delete")
this.schedulePush()
}
@ -74,7 +108,9 @@ export class SyncEngine {
onFileRename(file: TFile, oldPath: string) {
if (!(file instanceof TFile) || file.extension !== "md") return
this.pendingChanges.set(oldPath, "delete")
this.pendingChanges.set(file.path, "upsert")
if (this.shouldSync(file)) {
this.pendingChanges.set(file.path, "upsert")
}
this.schedulePush()
}

View file

@ -1,3 +1,5 @@
export type SyncMode = "all" | "folders"
export interface SupermemorySettings {
apiKey: string
apiBaseUrl: string
@ -6,4 +8,7 @@ export interface SupermemorySettings {
connectionId: string
syncOnSave: boolean
syncOnStartup: boolean
syncMode: SyncMode
/** Comma-separated folder paths; only used when syncMode === "folders" */
includedFolders: string
}