Sync folder structure via Git Sync (folders.json), plus bugs found testing it

Folders and conversation→folder assignments now sync across machines:
- Folder gains updatedAt (v11 migration) to resolve renames/reparents
  last-write-wins across machines.
- New folders.json manifest at the sync repo root: folder tree +
  conversationId→folderId assignments, imported before conversation
  files so new conversations land in the right folder immediately.
- Local folders missing from the manifest are pruned (reparent-safe),
  guarded the same way conversation-orphan cleanup already is against
  an empty/stale manifest wiping everything.

Three real bugs found and fixed during live multi-machine testing:
- Sidebar never refreshed after Git Sync imported conversations/folders
  directly into the database — only reloaded on launch or when the
  advanced conversation list closed, with no equivalent hook for the
  Settings sheet.
- "Sync Now" exported before pulling, so it could write folders.json
  as an untracked file that then collided with the remote's tracked
  copy on the next pull ("untracked working tree files would be
  overwritten by merge"). Reordered to pull → import → export → push.
- Folder assignment only applied to brand-new conversations during
  import, so any conversation already synced to a machine before this
  feature existed never got filed — which in practice is every
  conversation on a second machine, not an edge case. Now backfills
  a folder assignment for existing conversations that aren't filed
  anywhere locally yet, without clobbering an already-set folderId.

Also renamed the "Initialize Repository" button to "Clone Repository"
(it's always been a git clone, not new-repo creation) across the UI,
localization catalog, and Help Book.
This commit is contained in:
2026-08-03 11:48:23 +02:00
parent c3abc5a748
commit 6480a50eee
10 changed files with 362 additions and 27 deletions
+4 -1
View File
@@ -29,19 +29,22 @@ struct Folder: Identifiable, Codable, Sendable {
var sortOrder: Int
let createdAt: Date
var parentId: UUID?
var updatedAt: Date
nonisolated init(
id: UUID = UUID(),
name: String,
sortOrder: Int = 0,
createdAt: Date = Date(),
parentId: UUID? = nil
parentId: UUID? = nil,
updatedAt: Date? = nil
) {
self.id = id
self.name = name
self.sortOrder = sortOrder
self.createdAt = createdAt
self.parentId = parentId
self.updatedAt = updatedAt ?? createdAt
}
}
+18
View File
@@ -68,6 +68,24 @@ struct SyncStatus: Equatable {
var remoteStatus: String? // "up-to-date", "ahead 3", "behind 2", etc.
}
/// Serialized as `folders.json` at the sync repo root. Unlike conversation exports, this is a
/// manifest meant to be machine-written/read only (the repo's README already warns against manual
/// edits), so plain JSON is used rather than the hand-rolled markdown format no need for that
/// format's human-readability tradeoffs here.
nonisolated struct FolderSyncManifest: Codable {
nonisolated struct FolderEntry: Codable {
let id: String
let name: String
let parentId: String?
let createdAt: Date
let updatedAt: Date
}
var folders: [FolderEntry]
/// conversationId -> folderId. Only present for conversations actually filed in a folder.
var assignments: [String: String]
}
nonisolated struct ConversationExport {
let id: String
let name: String