Fix deletions not sticking (git sync resurrection), persist folder
collapse state, make merge provider picker visibly clickable Deleted conversations coming back: exportAllConversations() only ever wrote files for conversations that currently exist — it never removed the exported markdown file for a conversation that had been deleted locally. That file just sits in the sync repo forever, so every future pull+import (including on every app startup) silently resurrects it, since importAllConversations() only skips an import when a matching local ID already exists. Fixed by having export also delete orphaned files (conversation ID no longer present locally), and added GitSyncService.syncAfterDeletion() — a debounced export+push triggered right after any delete/bulk-delete/merge-cleanup, so the removal reaches the remote promptly instead of waiting on an unrelated future auto-save. Existing duplicates need one more manual delete to clear, but they'll stay gone after that. Folder collapse state now persists (SettingsService.collapsedFolderIds, JSON-encoded like favoriteModelIds) and is restored on app launch, in both the sidebar and the advanced conversation list. Merge model picker: the provider switcher was legitimate (it does load each provider's own catalog independently) but looked like plain text — no chevron, no button styling — so it wasn't obviously clickable. Restyled to match HeaderView's provider menu affordance (icon + label + chevron on a colored pill).
This commit is contained in:
@@ -174,9 +174,40 @@ class GitSyncService {
|
||||
log.debug("Exported: \(filename)")
|
||||
}
|
||||
|
||||
// Remove files for conversations that no longer exist locally (e.g. deleted since
|
||||
// the last export). Without this, a deletion is never reflected in the sync repo,
|
||||
// so importAllConversations() silently resurrects it on every future pull.
|
||||
let currentIds = Set(conversations.map { $0.id.uuidString })
|
||||
let existingFiles = (try? FileManager.default.contentsOfDirectory(atPath: conversationsDir)) ?? []
|
||||
let mdFilesWithContent: [(filename: String, markdown: String)] = existingFiles
|
||||
.filter { $0.hasSuffix(".md") }
|
||||
.compactMap { filename in
|
||||
guard let markdown = try? String(contentsOfFile: conversationsDir + "/" + filename, encoding: .utf8)
|
||||
else { return nil }
|
||||
return (filename, markdown)
|
||||
}
|
||||
for filename in Self.orphanedExportFilenames(currentIds: currentIds, files: mdFilesWithContent) {
|
||||
try? FileManager.default.removeItem(atPath: conversationsDir + "/" + filename)
|
||||
log.info("Removed orphaned export for deleted conversation: \(filename)")
|
||||
}
|
||||
|
||||
await updateStatus()
|
||||
}
|
||||
|
||||
/// Given the current conversation IDs and the (filename, markdown content) pairs found in
|
||||
/// the sync repo's conversations directory, returns the filenames whose export ID doesn't
|
||||
/// match any current conversation — i.e. files safe to delete because their conversation
|
||||
/// was removed from the database since the last export.
|
||||
nonisolated static func orphanedExportFilenames(
|
||||
currentIds: Set<String>,
|
||||
files: [(filename: String, markdown: String)]
|
||||
) -> [String] {
|
||||
files.compactMap { file in
|
||||
guard let export = try? ConversationExport.fromMarkdown(file.markdown) else { return nil }
|
||||
return currentIds.contains(export.id) ? nil : file.filename
|
||||
}
|
||||
}
|
||||
|
||||
/// Import conversations from markdown files
|
||||
func importAllConversations() async throws -> (imported: Int, skipped: Int, errors: Int) {
|
||||
try ensureCloned()
|
||||
@@ -424,6 +455,15 @@ class GitSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fire-and-forget sync trigger for conversation-deletion call sites. No-ops when sync
|
||||
/// isn't configured. Deletions otherwise only reach the sync repo on the next incidental
|
||||
/// auto-sync (or never, if the app is closed first) — this makes the removal propagate
|
||||
/// promptly instead of the deleted conversation silently reappearing on next pull+import.
|
||||
func syncAfterDeletion() {
|
||||
guard settings.syncConfigured else { return }
|
||||
Task { await autoSync() }
|
||||
}
|
||||
|
||||
/// Perform auto-sync with debouncing (export + push)
|
||||
/// Debounces multiple rapid sync requests to avoid spamming git
|
||||
func autoSync() async {
|
||||
|
||||
Reference in New Issue
Block a user