From c0db825ab9ed9ff63d23087b59fca5b00e26421b Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Thu, 26 Mar 2026 18:32:51 +0000 Subject: [PATCH 1/8] Add task: Desktop macOS window lifecycle & crash resilience (#48) Investigation and phased plan for fixing window close behavior, file watcher crash resilience, and Screen Time/App Nap issues. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...p-window-lifecycle-and-crash-resilience.md | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docs/tasks-todo/task-x-desktop-window-lifecycle-and-crash-resilience.md diff --git a/docs/tasks-todo/task-x-desktop-window-lifecycle-and-crash-resilience.md b/docs/tasks-todo/task-x-desktop-window-lifecycle-and-crash-resilience.md new file mode 100644 index 0000000..f0afc43 --- /dev/null +++ b/docs/tasks-todo/task-x-desktop-window-lifecycle-and-crash-resilience.md @@ -0,0 +1,70 @@ +# Desktop: Fix macOS Window Lifecycle & Crash Resilience + +**Product:** `tdn-desktop/` +**GitHub Issue:** #48 + +## Background + +Closing the Desktop app's main window kills the entire process, making it impossible to reopen via the dock icon or use the quick pane shortcut. Additionally, the app crashes when macOS Screen Time blocks it, during overnight App Nap suspension, or when the user dismisses Screen Time restrictions ("Ignore for today"). These are likely related — the window lifecycle is wrong, and the file watcher has no resilience to process suspension. + +## Phased Approach + +### Phase 1: Fix macOS window close behavior (Issue #48) + +**Goal:** Closing the main window hides the app instead of quitting. Dock icon click reopens it. Cmd+Q and Quit menu item still quit properly. + +**Changes in `src-tauri/src/lib.rs`:** + +- In `on_window_event` / `handle_run_event`: intercept `CloseRequested` for the main window, call `api.prevent_close()`, then `app_handle.hide()` (uses `NSApplication.hide()` which integrates with dock reshow) +- Add `RunEvent::Reopen` handler: show and focus the main window when `has_visible_windows` is false +- **Do NOT call `unregister_global_shortcuts()` on window close** — only on actual app quit. Quick pane shortcut must keep working while the app is hidden +- Move cleanup logic (save window state, hide quick pane, unregister shortcuts) to actual quit path +- Ensure Cmd+Q and the Quit menu item call `app_handle.exit(0)` so the app actually terminates (workaround for `ExitRequested` not firing reliably on macOS — see tauri-apps/tauri#9198) + +**Watch out for:** +- `tauri_plugin_window_state` + `prevent_exit()` can cause infinite `windowDidMove` loop (tauri-apps/tauri discussions#11489) — test this carefully +- The quick-pane NSPanel is already denylisted from window-state plugin due to `is_maximized()` crash (plugins-workspace#1546) + +**Key references:** +- tauri-apps/tauri#3084 — `RunEvent::Reopen` feature +- tauri-apps/tauri PR#4865 — implementation +- tauri-apps/tauri#9198 — `ExitRequested` unreliable on macOS +- tauri-apps/tauri#13511 — `prevent_exit()` blocks normal termination + +### Phase 2: File watcher error recovery & periodic rescan + +**Goal:** The vault file watcher recovers from crashes and a periodic rescan catches missed changes. + +**Changes in `src-tauri/src/vault/manager.rs`:** + +- Add error handling in the debouncer callback — detect when the watcher has died and trigger a rebuild (drop old watcher, full rescan, create new watcher) +- Add a periodic vault rescan timer (e.g. every 5 minutes) as a safety net. This is the Syncthing pattern and what Apple recommends for mission-critical apps. It should be lightweight — compare file mtimes against the in-memory index +- Handle the `Rescan` event kind from notify (maps to `kFSEventStreamEventFlagMustScanSubDirs`) — trigger a full directory scan when this fires, as it means events were coalesced or dropped + +**Why this matters:** When Screen Time SIGSTOPs the process or App Nap suspends it, FSEvents queue up in `fseventsd`. On resume they arrive all at once, possibly coalesced. The debouncer may not handle this burst, and if the watcher thread panics the app has no recovery path today. + +### Phase 3: Upgrade notify dependencies + +**Goal:** Pick up recent FSEvents crash fixes. + +- Upgrade `notify` and `notify-debouncer-full` to latest versions +- notify 9.0.0-rc.1/rc.2 include: preventing panics in the FSEvents callback, fixing stream start errors, fixing empty path crashes, making StreamContextInfo Send +- Test thoroughly after upgrade — the rc versions may have breaking API changes + +**References:** +- notify-rs/notify CHANGELOG +- notify-rs/notify#283 (watcher panic on suspend/resume) + +### Phase 4: App Nap and sleep/wake handling (optional) + +**Goal:** Prevent aggressive App Nap suspension and rebuild the watcher after system sleep. + +This phase may not be necessary if Phases 1-3 resolve the overnight crashes. Evaluate after those are done. + +- **App Nap prevention:** Use `NSProcessInfo.beginActivityWithOptions:reason:` with `NSActivityUserInitiated` to prevent macOS from aggressively suspending the app while the file watcher is active +- **Sleep/wake detection:** Listen for `NSWorkspaceDidWakeNotification` and rebuild the file watcher on wake +- Both require `objc2` crate calls from Rust + +**References:** +- Apple QA1340: Sleep/Wake Notifications +- Electron issue electron/electron#973 (App Nap) From b45c476d28beb3bd15b9cde5f613f584853066d4 Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Thu, 26 Mar 2026 18:59:56 +0000 Subject: [PATCH 2/8] Fix macOS window close: hide app instead of quitting (#48) - Intercept CloseRequested on macOS to prevent_close + hide the app - Add RunEvent::Reopen handler to show window on dock icon click - Move cleanup (hide quick pane, unregister shortcuts) to RunEvent::Exit so it runs on actual quit (Cmd+Q, menu Quit) to prevent known crashes Co-Authored-By: Claude Opus 4.6 (1M context) --- tdn-desktop/src-tauri/src/lib.rs | 72 ++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 23 deletions(-) diff --git a/tdn-desktop/src-tauri/src/lib.rs b/tdn-desktop/src-tauri/src/lib.rs index 4f60d20..cc5953a 100644 --- a/tdn-desktop/src-tauri/src/lib.rs +++ b/tdn-desktop/src-tauri/src/lib.rs @@ -222,31 +222,57 @@ fn setup_vault(app: &mut App) { } } -/// Handle application run events, particularly window close cleanup. +/// Handle application run events. +/// +/// On macOS, closing the main window hides the app instead of quitting, +/// following standard macOS behavior. The app can be reopened via the dock icon. +/// On other platforms, closing the main window quits the app normally. fn handle_run_event(app_handle: &AppHandle, event: RunEvent) { - if let RunEvent::WindowEvent { - label, - event: WindowEvent::CloseRequested { .. }, - .. - } = &event - { - if label == "main" { - handle_main_window_close(app_handle); + match &event { + RunEvent::WindowEvent { + label, + event: WindowEvent::CloseRequested { api, .. }, + .. + } if label == "main" => { + log::info!("Main window close requested"); + save_window_state(app_handle); + + // On macOS, hide the app instead of closing — standard macOS behavior. + // The app stays running for dock icon reopen and global shortcuts. + // Cmd+Q and the Quit menu item bypass CloseRequested entirely, + // so they still quit the app normally. + #[cfg(target_os = "macos")] + { + api.prevent_close(); + if let Err(e) = app_handle.hide() { + log::warn!("Failed to hide app: {e}"); + } + } + + #[cfg(not(target_os = "macos"))] + { _ = api; } } + RunEvent::Reopen { + has_visible_windows, + .. + } => { + if !*has_visible_windows { + log::info!("App reopen requested - showing main window"); + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + } + } + } + RunEvent::Exit => { + log::info!("Application exiting - performing cleanup"); + hide_quick_pane(app_handle); + unregister_global_shortcuts(app_handle); + } + _ => {} } } -/// Perform cleanup when the main window is closed. -fn handle_main_window_close(app_handle: &AppHandle) { - log::info!("Main window close requested - performing cleanup"); - - save_window_state(app_handle); - hide_quick_pane(app_handle); - unregister_global_shortcuts(app_handle); - - log::info!("Cleanup complete, allowing close to proceed"); -} - /// Save window state before closing. #[cfg(desktop)] fn save_window_state(app_handle: &AppHandle) { @@ -262,13 +288,13 @@ fn save_window_state(app_handle: &AppHandle) { #[cfg(not(desktop))] fn save_window_state(_app_handle: &AppHandle) {} -/// Hide the quick-pane panel before main window closes. +/// Hide the quick-pane panel during app cleanup. #[cfg(target_os = "macos")] fn hide_quick_pane(app_handle: &AppHandle) { use tauri_nspanel::ManagerExt; if let Ok(panel) = app_handle.get_webview_panel("quick-pane") { - log::debug!("Hiding quick-pane panel before close"); + log::debug!("Hiding quick-pane panel"); panel.hide(); } } @@ -276,7 +302,7 @@ fn hide_quick_pane(app_handle: &AppHandle) { #[cfg(not(target_os = "macos"))] fn hide_quick_pane(_app_handle: &AppHandle) {} -/// Unregister all global shortcuts. +/// Unregister all global shortcuts during app cleanup. #[cfg(desktop)] fn unregister_global_shortcuts(app_handle: &AppHandle) { use tauri_plugin_global_shortcut::GlobalShortcutExt; From bf5a47bd7e4c31c98331612a7e5be6eb390e1373 Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Thu, 26 Mar 2026 19:12:18 +0000 Subject: [PATCH 3/8] Restore window position on reopen to fix jump-on-drag The window-state plugin only auto-restores on startup, not after a hide/show cycle. Explicitly call restore_state() when reopening the main window via the dock icon. Co-Authored-By: Claude Opus 4.6 (1M context) --- tdn-desktop/src-tauri/src/lib.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tdn-desktop/src-tauri/src/lib.rs b/tdn-desktop/src-tauri/src/lib.rs index cc5953a..8c79209 100644 --- a/tdn-desktop/src-tauri/src/lib.rs +++ b/tdn-desktop/src-tauri/src/lib.rs @@ -260,6 +260,13 @@ fn handle_run_event(app_handle: &AppHandle, event: RunEvent) { log::info!("App reopen requested - showing main window"); if let Some(window) = app_handle.get_webview_window("main") { let _ = window.show(); + // Restore saved position/size — the window-state plugin only + // auto-restores on startup, not after a hide/show cycle. + #[cfg(desktop)] + { + use tauri_plugin_window_state::{StateFlags, WindowExt}; + let _ = window.restore_state(StateFlags::all()); + } let _ = window.set_focus(); } } From 0a1c2f7a3ccf9f18ca2b5c72abb6b80488b9dfa1 Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Thu, 26 Mar 2026 19:21:50 +0000 Subject: [PATCH 4/8] Hide main window (not app) on close to fix quick pane interaction Use window.hide() instead of app_handle.hide() so the quick pane can be shown independently without triggering a system-level app unhide. Cmd+H still hides the whole app via NSApplication as normal. Co-Authored-By: Claude Opus 4.6 (1M context) --- tdn-desktop/src-tauri/src/lib.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tdn-desktop/src-tauri/src/lib.rs b/tdn-desktop/src-tauri/src/lib.rs index 8c79209..0c90760 100644 --- a/tdn-desktop/src-tauri/src/lib.rs +++ b/tdn-desktop/src-tauri/src/lib.rs @@ -237,15 +237,16 @@ fn handle_run_event(app_handle: &AppHandle, event: RunEvent) { log::info!("Main window close requested"); save_window_state(app_handle); - // On macOS, hide the app instead of closing — standard macOS behavior. - // The app stays running for dock icon reopen and global shortcuts. - // Cmd+Q and the Quit menu item bypass CloseRequested entirely, - // so they still quit the app normally. + // On macOS, hide the main window instead of closing — standard macOS behavior. + // We hide the window (not the app) so the quick pane can be shown independently + // without triggering a system-level app unhide. Cmd+H still hides the whole app + // via NSApplication.hide() as normal. Cmd+Q and the Quit menu item bypass + // CloseRequested entirely, so they still quit the app normally. #[cfg(target_os = "macos")] { api.prevent_close(); - if let Err(e) = app_handle.hide() { - log::warn!("Failed to hide app: {e}"); + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.hide(); } } From f21bae3665625e0ae47ae809b4465ec52811d6f9 Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Thu, 26 Mar 2026 19:22:50 +0000 Subject: [PATCH 5/8] Update task doc with Phase 1 implementation details Document all decisions and fixes from Phase 1 including the window.hide() vs app_handle.hide() choice, RunEvent::Exit cleanup, and window state restore on reopen. Includes behavior summary table for reference when applying the same pattern to other Tauri apps. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...p-window-lifecycle-and-crash-resilience.md | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/docs/tasks-todo/task-x-desktop-window-lifecycle-and-crash-resilience.md b/docs/tasks-todo/task-x-desktop-window-lifecycle-and-crash-resilience.md index f0afc43..4d5ea41 100644 --- a/docs/tasks-todo/task-x-desktop-window-lifecycle-and-crash-resilience.md +++ b/docs/tasks-todo/task-x-desktop-window-lifecycle-and-crash-resilience.md @@ -9,27 +9,56 @@ Closing the Desktop app's main window kills the entire process, making it imposs ## Phased Approach -### Phase 1: Fix macOS window close behavior (Issue #48) +### Phase 1: Fix macOS window close behavior (Issue #48) — DONE -**Goal:** Closing the main window hides the app instead of quitting. Dock icon click reopens it. Cmd+Q and Quit menu item still quit properly. +**Goal:** Closing the main window hides it instead of quitting. Dock icon click reopens it. Quick pane works independently. Cmd+Q and Quit menu item still quit properly. -**Changes in `src-tauri/src/lib.rs`:** +**What was implemented in `src-tauri/src/lib.rs`:** -- In `on_window_event` / `handle_run_event`: intercept `CloseRequested` for the main window, call `api.prevent_close()`, then `app_handle.hide()` (uses `NSApplication.hide()` which integrates with dock reshow) -- Add `RunEvent::Reopen` handler: show and focus the main window when `has_visible_windows` is false -- **Do NOT call `unregister_global_shortcuts()` on window close** — only on actual app quit. Quick pane shortcut must keep working while the app is hidden -- Move cleanup logic (save window state, hide quick pane, unregister shortcuts) to actual quit path -- Ensure Cmd+Q and the Quit menu item call `app_handle.exit(0)` so the app actually terminates (workaround for `ExitRequested` not firing reliably on macOS — see tauri-apps/tauri#9198) +1. **Intercept `CloseRequested` for the main window on macOS:** + - Call `api.prevent_close()` so the window isn't destroyed + - Call `window.hide()` on the main window (NOT `app_handle.hide()` — see below) + - Call `save_window_state()` to persist position/size before hiding -**Watch out for:** -- `tauri_plugin_window_state` + `prevent_exit()` can cause infinite `windowDidMove` loop (tauri-apps/tauri discussions#11489) — test this carefully -- The quick-pane NSPanel is already denylisted from window-state plugin due to `is_maximized()` crash (plugins-workspace#1546) +2. **Hide the window, not the app:** + - We use `window.hide()` (hides only the main window) rather than `app_handle.hide()` (which calls `NSApplication.hide()` and sets the system-level hidden state) + - This is critical because showing an NSPanel while the app is in the system "hidden" state causes macOS to unhide the entire app, including the main window + - With `window.hide()`, the app is still "running" but not "hidden" — the quick pane can be shown independently without the main window reappearing + - Cmd+H still works normally (it calls `NSApplication.hide()` at the system level, and interacting with the app after Cmd+H naturally brings everything back — standard macOS behavior) + +3. **Add `RunEvent::Reopen` handler:** + - When the dock icon is clicked and there are no visible windows, show the main window + - Explicitly call `window.restore_state(StateFlags::all())` after showing — the `tauri-plugin-window-state` plugin only auto-restores on app startup, not after a hide/show cycle. Without this, the window could appear at stale/off-screen coordinates and jump when dragged. + - Focus the window after restoring + +4. **Move cleanup to `RunEvent::Exit`:** + - `hide_quick_pane()` and `unregister_global_shortcuts()` now run in `RunEvent::Exit` instead of on window close + - This ensures cleanup happens on actual quit (Cmd+Q, menu Quit) regardless of how the quit was initiated + - These cleanup functions exist to prevent known crashes during app teardown — removing them causes crashes on quit + - `RunEvent::Exit` fires reliably before the process exits, unlike `RunEvent::ExitRequested` which doesn't fire for Cmd+Q on macOS (tauri-apps/tauri#9198) + - We do NOT use `prevent_exit()` anywhere, which avoids the infinite `windowDidMove` loop issue with `tauri_plugin_window_state` (tauri-apps/tauri discussions#11489) + +5. **Non-macOS behavior unchanged:** On other platforms, closing the main window still quits the app after running cleanup. **Key references:** - tauri-apps/tauri#3084 — `RunEvent::Reopen` feature - tauri-apps/tauri PR#4865 — implementation - tauri-apps/tauri#9198 — `ExitRequested` unreliable on macOS - tauri-apps/tauri#13511 — `prevent_exit()` blocks normal termination +- tauri-apps/tauri discussions#11489 — `window-state` + `prevent_exit()` infinite loop +- plugins-workspace#1546 — quick-pane NSPanel denylisted from window-state plugin + +**Behavior summary:** + +| Action | Result | +|--------|--------| +| Red X (close button) | Main window hides. App stays running. Quick pane shortcut works. | +| Dock icon click (window hidden) | Main window shows at saved position. | +| Quick pane shortcut (window hidden) | Only the quick pane appears. Main window stays hidden. | +| Cmd+H | macOS hides entire app (system-level). Standard behavior. | +| Dock icon click (after Cmd+H) | macOS unhides the app. Main window reappears. | +| Quick pane shortcut (after Cmd+H) | macOS unhides entire app. Both main window and quick pane appear. | +| Cmd+Q / menu Quit | Cleanup runs via `RunEvent::Exit`, then app exits. | ### Phase 2: File watcher error recovery & periodic rescan From c4550db36a1fa651e1841b85687cd46ac99ea95d Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Fri, 27 Mar 2026 17:09:58 +0000 Subject: [PATCH 6/8] Upgrade notify-debouncer-full from 0.5 to 0.7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Picks up the event deduplication fix (0.7.0) which prevents duplicate vault-changed events from being emitted. notify 9.x FSEvents crash fixes are still in RC — they'll arrive when a future debouncer version picks up stable notify 9.0. Co-Authored-By: Claude Opus 4.6 (1M context) --- tdn-desktop/src-tauri/Cargo.lock | 4 ++-- tdn-desktop/src-tauri/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tdn-desktop/src-tauri/Cargo.lock b/tdn-desktop/src-tauri/Cargo.lock index 57ce1c2..78bfdb7 100644 --- a/tdn-desktop/src-tauri/Cargo.lock +++ b/tdn-desktop/src-tauri/Cargo.lock @@ -2704,9 +2704,9 @@ dependencies = [ [[package]] name = "notify-debouncer-full" -version = "0.5.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d88b1a7538054351c8258338df7c931a590513fb3745e8c15eb9ff4199b8d1" +checksum = "c02b49179cfebc9932238d04d6079912d26de0379328872846118a0fa0dbb302" dependencies = [ "file-id", "log", diff --git a/tdn-desktop/src-tauri/Cargo.toml b/tdn-desktop/src-tauri/Cargo.toml index 759bd5d..a312204 100644 --- a/tdn-desktop/src-tauri/Cargo.toml +++ b/tdn-desktop/src-tauri/Cargo.toml @@ -45,7 +45,7 @@ rayon = "1" # Parallel file scanning globset = "0.4" # Ignore patterns uuid = { version = "1", features = ["v4"] } # Unique ID generation xxhash-rust = { version = "0.8", features = ["xxh64"] } # Stable cross-version hashing -notify-debouncer-full = "0.5" # File watching with debouncing +notify-debouncer-full = "0.7" # File watching with debouncing parking_lot = "0.12" # Better RwLock implementation trash = "5" # Cross-platform trash/recycle bin From fc89bf28d2eb72c2e466ac1a14ecae5fe2d7bdf1 Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Fri, 27 Mar 2026 17:19:09 +0000 Subject: [PATCH 7/8] Add file watcher error recovery and periodic vault rescan (#48) Phase 2: Make the vault file watcher resilient to crashes and missed events. - Watcher error callback now emits vault-changed to trigger a refresh, picking up any events that were lost during the error - Add periodic vault rescan (every 5 minutes) as a safety net for silent watcher death, App Nap suspension, or FSEvents coalescing - Export VAULT_CHANGED_EVENT constant for use across modules Co-Authored-By: Claude Opus 4.6 (1M context) --- tdn-desktop/src-tauri/src/lib.rs | 38 +++++++++++++++++++++- tdn-desktop/src-tauri/src/vault/manager.rs | 12 +++++-- tdn-desktop/src-tauri/src/vault/mod.rs | 2 +- 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/tdn-desktop/src-tauri/src/lib.rs b/tdn-desktop/src-tauri/src/lib.rs index 0c90760..ad2f29e 100644 --- a/tdn-desktop/src-tauri/src/lib.rs +++ b/tdn-desktop/src-tauri/src/lib.rs @@ -14,7 +14,8 @@ pub mod vault; mod apple_intelligence; use std::error::Error; -use tauri::{App, AppHandle, Manager, RunEvent, WindowEvent}; +use std::time::Duration; +use tauri::{App, AppHandle, Emitter, Manager, RunEvent, WindowEvent}; use vault::VaultManager; // Re-export only what's needed externally @@ -220,6 +221,41 @@ fn setup_vault(app: &mut App) { } else { log::info!("Vault not configured - user needs to set directory paths in preferences"); } + + // Start periodic rescan as a safety net for missed file watcher events + start_periodic_rescan(app.handle().clone()); +} + +/// Interval between periodic vault rescans. +const RESCAN_INTERVAL: Duration = Duration::from_secs(5 * 60); + +/// Periodically rescan the vault to catch any changes the file watcher may have missed. +/// +/// This handles cases where the watcher dies silently, events are lost during +/// macOS App Nap / Screen Time suspension, or FSEvents coalesces events. +fn start_periodic_rescan(app_handle: AppHandle) { + std::thread::spawn(move || { + loop { + std::thread::sleep(RESCAN_INTERVAL); + + let vault_manager = app_handle.state::(); + if !vault_manager.is_configured() { + continue; + } + + log::debug!("Periodic vault rescan running"); + match vault_manager.refresh() { + Ok(()) => { + if let Err(e) = app_handle.emit(vault::VAULT_CHANGED_EVENT, ()) { + log::error!("Failed to emit vault-changed event after rescan: {e}"); + } + } + Err(e) => { + log::warn!("Periodic vault rescan failed: {e:?}"); + } + } + } + }); } /// Handle application run events. diff --git a/tdn-desktop/src-tauri/src/vault/manager.rs b/tdn-desktop/src-tauri/src/vault/manager.rs index a851e68..cb9baab 100644 --- a/tdn-desktop/src-tauri/src/vault/manager.rs +++ b/tdn-desktop/src-tauri/src/vault/manager.rs @@ -27,7 +27,7 @@ use crate::vault::{ }; /// Event emitted when vault data changes (for frontend cache invalidation) -const VAULT_CHANGED_EVENT: &str = "vault-changed"; +pub const VAULT_CHANGED_EVENT: &str = "vault-changed"; /// Debounce interval for file watcher const DEBOUNCE_DURATION: Duration = Duration::from_millis(100); @@ -264,9 +264,17 @@ impl VaultManager { } } Err(errors) => { - for e in errors { + for e in &errors { warn!("File watcher error: {e}"); } + // Emit vault-changed so the frontend triggers a refresh, + // picking up any events the watcher may have missed. + if !errors.is_empty() { + warn!("Triggering vault refresh due to watcher errors"); + if let Err(e) = app_handle.emit(VAULT_CHANGED_EVENT, ()) { + error!("Failed to emit vault-changed event after watcher error: {e}"); + } + } } } }) diff --git a/tdn-desktop/src-tauri/src/vault/mod.rs b/tdn-desktop/src-tauri/src/vault/mod.rs index e808e2e..256480d 100644 --- a/tdn-desktop/src-tauri/src/vault/mod.rs +++ b/tdn-desktop/src-tauri/src/vault/mod.rs @@ -25,7 +25,7 @@ pub use entities::{ ProjectUpdate, Task, TaskStatus, TaskUpdate, }; pub use error::VaultError; -pub use manager::{VaultIndex, VaultManager}; +pub use manager::{VaultIndex, VaultManager, VAULT_CHANGED_EVENT}; pub use scanner::{ parse_area_file, parse_project_file, parse_task_file, scan_areas, scan_projects, scan_tasks, VaultConfig, From df701dab8da71e81385ee31ea90d68074fd37587 Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Fri, 27 Mar 2026 17:57:35 +0000 Subject: [PATCH 8/8] Bump MSRV to 1.85 for notify-debouncer-full 0.7 Co-Authored-By: Claude Opus 4.6 (1M context) --- tdn-desktop/src-tauri/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tdn-desktop/src-tauri/Cargo.toml b/tdn-desktop/src-tauri/Cargo.toml index a312204..ef9ae48 100644 --- a/tdn-desktop/src-tauri/Cargo.toml +++ b/tdn-desktop/src-tauri/Cargo.toml @@ -5,7 +5,7 @@ description = "A desktop application for managing tasks" authors = ["dannysmith"] license = "AGPL-3.0-only" edition = "2021" -rust-version = "1.82" +rust-version = "1.85" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html