#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub struct FileMetadata { /// Last modified time of the file pub modified_time: SystemTime, /// File size in bytes for additional verification pub size: u64, /// Content hash for detecting changes even when timestamp/size are identical pub content_hash: u64,}impl FileMetadata { pub fn from_file(path: &Path) -> std::io::Result<Self> { let metadata = fs::metadata(path)?; let modified_time = metadata.modified()?; let size = metadata.len(); // Compute content hash for comprehensive change detection let content = fs::read(path)?; let mut hasher = std::collections::hash_map::DefaultHasher::new(); content.hash(&mut hasher); let content_hash = hasher.finish(); Ok(Self { modified_time, size, content_hash }) } pub fn has_changed(&self, path: &Path) -> std::io::Result<bool> { let current = Self::from_file(path)?; Ok(current != *self) }}
The content hash provides protection against edge cases where timestamp and size might be identical but content differs (e.g., rapid successive writes).
async fn save(&self, snapshot: StoreSnapshot) -> KanbanResult<PersistenceMetadata> { // Check for external file modifications before saving if self.path.exists() { let current_metadata = FileMetadata::from_file(&self.path)?; // Compare with last known metadata let guard = self.lock_metadata(); if let Some(last_known) = *guard { if last_known != current_metadata { return Err(KanbanError::ConflictDetected { path: self.path.to_string_lossy().to_string(), source: None, }); } } } // Proceed with atomic write... AtomicWriter::write_atomic(&self.path, &json_bytes).await?; // Update last known metadata after successful write if let Ok(new_metadata) = FileMetadata::from_file(&self.path) { let mut guard = self.lock_metadata(); *guard = Some(new_metadata); } Ok(snapshot.metadata)}
feat(tui): Implement conflict resolution dialog and event loop integration
feat(tui): Add ExternalChangeDetected dialog
When a conflict is detected, the user is prompted with options:
┌───────────────────────────────────────────────┐│ External Changes Detected │├───────────────────────────────────────────────┤│ ││ The file has been modified by another ││ instance. You have unsaved local changes. ││ ││ What would you like to do? ││ ││ [R] Reload and discard local changes ││ [O] Overwrite with local changes ││ [C] Cancel ││ │└───────────────────────────────────────────────┘
Options:
Reload (R): Discard local changes and reload from disk
Overwrite (O): Save local changes, overwriting external changes
Cancel (C): Do nothing, keep current state
The “Overwrite” option implements last-write-wins semantics. External changes will be lost.
# Time 0: Both instances load the same fileInstance A: Loads kanban.json (metadata: mtime=10:00:00)Instance B: Loads kanban.json (metadata: mtime=10:00:00)# Time 1: Instance A makes changes and savesInstance A: Edits card "ABC" title → SavesFile state: mtime=10:00:05# Time 2: Instance B tries to saveInstance B: Edits card "XYZ" status → Attempts saveInstance B: Detects conflict (expected mtime=10:00:00, actual=10:00:05)Instance B: Shows conflict dialog# Time 3: User chooses "Overwrite"Instance B: Saves with overwriteFile state: Contains Instance B's changes, Instance A's changes lost
To avoid conflicts, use the TUI for interactive work and the CLI for scripted operations. The TUI will automatically reload when CLI commands execute.
Last-Write-Wins SemanticsConflicts are resolved by overwriting, not merging. If two instances edit different cards simultaneously, the last save wins and the other changes are lost.Future versions may add operational transformation or CRDTs for true collaborative editing.
Network File SystemsFile watching may have delays on network file systems (NFS, CIFS). The watcher relies on OS-native notifications which may be slower or less reliable over networks.