From 8bd09e371a0d644c287a71ce358106411bbff660 Mon Sep 17 00:00:00 2001 From: GCWing Date: Tue, 3 Mar 2026 22:05:30 +0800 Subject: [PATCH 1/2] fix(fs): true debounce in file watcher and robust path filtering Made-with: Cursor --- .../infrastructure/filesystem/file_watcher.rs | 53 ++++++++++--------- .../file-system/services/FileSystemService.ts | 9 +++- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/src/crates/core/src/infrastructure/filesystem/file_watcher.rs b/src/crates/core/src/infrastructure/filesystem/file_watcher.rs index 796a8b8c..3b7a0023 100644 --- a/src/crates/core/src/infrastructure/filesystem/file_watcher.rs +++ b/src/crates/core/src/infrastructure/filesystem/file_watcher.rs @@ -167,33 +167,38 @@ impl FileWatcher { let config = self.config.clone(); let watched_paths = self.watched_paths.clone(); - tokio::spawn(async move { - let mut last_flush = std::time::Instant::now(); - - while let Ok(event) = rx.recv() { - match event { - Ok(event) => { - if Self::should_ignore_event(&event, &watched_paths).await { - continue; - } - - if let Some(file_event) = Self::convert_event(&event) { - { - let mut buffer = lock_event_buffer(&event_buffer); - buffer.push(file_event); - } - - let now = std::time::Instant::now(); - if now.duration_since(last_flush).as_millis() as u64 - >= config.debounce_interval_ms - { - Self::flush_events_static(&event_buffer, &emitter_arc).await; - last_flush = now; + // Run on a dedicated blocking thread to avoid starving the async runtime. + // True debounce: accumulate events, then flush once the stream goes quiet for + // `debounce_interval_ms`. A 50 ms poll interval keeps latency low even for + // single-event bursts (e.g. one `fs::write` from an agentic tool). + tokio::task::spawn_blocking(move || { + let rt = tokio::runtime::Handle::current(); + let debounce = std::time::Duration::from_millis(config.debounce_interval_ms); + let poll = std::time::Duration::from_millis(50); + let mut last_event_time: Option = None; + + loop { + match rx.recv_timeout(poll) { + Ok(Ok(event)) => { + let ignore = + rt.block_on(Self::should_ignore_event(&event, &watched_paths)); + if !ignore { + if let Some(file_event) = Self::convert_event(&event) { + lock_event_buffer(&event_buffer).push(file_event); + last_event_time = Some(std::time::Instant::now()); } } } - Err(e) => { - eprintln!("Watch error: {:?}", e); + Ok(Err(e)) => eprintln!("Watch error: {:?}", e), + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {} + Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break, + } + + // Flush only after events have been quiet for the debounce window. + if let Some(t) = last_event_time { + if t.elapsed() >= debounce { + rt.block_on(Self::flush_events_static(&event_buffer, &emitter_arc)); + last_event_time = None; } } } diff --git a/src/web-ui/src/tools/file-system/services/FileSystemService.ts b/src/web-ui/src/tools/file-system/services/FileSystemService.ts index ce9ae833..74b0ec35 100644 --- a/src/web-ui/src/tools/file-system/services/FileSystemService.ts +++ b/src/web-ui/src/tools/file-system/services/FileSystemService.ts @@ -72,6 +72,12 @@ class FileSystemService implements IFileSystemService { let unlisten: UnlistenFn | null = null; let isActive = true; + // Normalize to forward-slash, lower-case, no trailing slash for robust comparison. + const normalizeForCompare = (p: string) => + p.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase(); + + const normalizedRoot = normalizeForCompare(rootPath); + const initWatcher = async () => { try { unlisten = await listen('file-system-changed', (event) => { @@ -80,7 +86,8 @@ class FileSystemService implements IFileSystemService { const events = event.payload; events.forEach((fileEvent) => { - if (!fileEvent.path.startsWith(rootPath)) { + const normalizedEventPath = normalizeForCompare(fileEvent.path); + if (!normalizedEventPath.startsWith(normalizedRoot)) { return; } From 5221098c47c1964fb26a4d599c4f6c86729ff509 Mon Sep 17 00:00:00 2001 From: GCWing Date: Tue, 3 Mar 2026 22:09:24 +0800 Subject: [PATCH 2/2] fix(fs): preserve path case sensitivity in file-watch event filter Made-with: Cursor --- .../src/tools/file-system/services/FileSystemService.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/web-ui/src/tools/file-system/services/FileSystemService.ts b/src/web-ui/src/tools/file-system/services/FileSystemService.ts index 74b0ec35..c39aedd0 100644 --- a/src/web-ui/src/tools/file-system/services/FileSystemService.ts +++ b/src/web-ui/src/tools/file-system/services/FileSystemService.ts @@ -72,9 +72,10 @@ class FileSystemService implements IFileSystemService { let unlisten: UnlistenFn | null = null; let isActive = true; - // Normalize to forward-slash, lower-case, no trailing slash for robust comparison. + // Normalize separators and trailing slash for robust cross-platform comparison. + // Case is preserved intentionally: paths are case-sensitive. const normalizeForCompare = (p: string) => - p.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase(); + p.replace(/\\/g, '/').replace(/\/+$/, ''); const normalizedRoot = normalizeForCompare(rootPath);