tenseleyflow/claudex / 68a6e58

Browse files

perf: suppress file-watcher events for pty-owned sessions + 2s debounce

Authored by espadonne
SHA
68a6e580aaaae26ed8b0adf8e58001a3da5789ec
Parents
6b46cf2
Tree
202f435

2 changed files

StatusFile+-
M src-tauri/src/commands.rs 50 6
M src/lib/store/sessions.ts 9 1
src-tauri/src/commands.rsmodified
@@ -864,7 +864,7 @@ pub fn initialize(app: &AppHandle) -> Result<AppState, Box<dyn std::error::Error
864864
     }
865865
 
866866
     let active_turns = Arc::new(DashMap::new());
867
-    let active_ptys = Arc::new(DashMap::new());
867
+    let active_ptys: Arc<DashMap<String, PtyEntry>> = Arc::new(DashMap::new());
868868
 
869869
     // Start the watcher, even if projects_root doesn't exist yet —
870870
     // `spawn_watcher` errors in that case so we handle it.
@@ -872,9 +872,10 @@ pub fn initialize(app: &AppHandle) -> Result<AppState, Box<dyn std::error::Error
872872
         let (handle, mut rx) = spawn_watcher(&projects_root)?;
873873
         let app_clone = app.clone();
874874
         let cache_clone = cache.clone();
875
+        let ptys_clone = active_ptys.clone();
875876
         tauri::async_runtime::spawn(async move {
876877
             while let Some(change) = rx.recv().await {
877
-                handle_change(&app_clone, &cache_clone, change);
878
+                handle_change(&app_clone, &cache_clone, &ptys_clone, change);
878879
             }
879880
         });
880881
         handle
@@ -889,9 +890,10 @@ pub fn initialize(app: &AppHandle) -> Result<AppState, Box<dyn std::error::Error
889890
         let (handle, mut rx) = spawn_watcher(&fallback_root)?;
890891
         let app_clone = app.clone();
891892
         let cache_clone = cache.clone();
893
+        let ptys_clone = active_ptys.clone();
892894
         tauri::async_runtime::spawn(async move {
893895
             while let Some(change) = rx.recv().await {
894
-                handle_change(&app_clone, &cache_clone, change);
896
+                handle_change(&app_clone, &cache_clone, &ptys_clone, change);
895897
             }
896898
         });
897899
         handle
@@ -909,7 +911,22 @@ pub fn initialize(app: &AppHandle) -> Result<AppState, Box<dyn std::error::Error
909911
     })
910912
 }
911913
 
912
-fn handle_change(app: &AppHandle, cache: &SummaryCache, change: SessionChange) {
914
+fn handle_change(
915
+    app: &AppHandle,
916
+    cache: &SummaryCache,
917
+    active_ptys: &Arc<DashMap<String, PtyEntry>>,
918
+    change: SessionChange,
919
+) {
920
+    // When a PTY is actively writing to this session's JSONL file,
921
+    // we'd otherwise re-enter the rescan → list_projects →
922
+    // summarize → sidebar re-render cycle every few hundred ms for
923
+    // the duration of the subprocess's run. The PTY is already the
924
+    // canonical writer and the user sees it via the xterm view, so
925
+    // there's nothing new for the sidebar to learn. Skip the event
926
+    // entirely for PTY-owned sessions to cut the write-storm at the
927
+    // source. Cache invalidation still runs so non-PTY callers
928
+    // (e.g. a manual rescan) see fresh data.
929
+    let skip_emit = path_matches_live_pty(&change, active_ptys);
913930
     match &change {
914931
         SessionChange::Removed(path) => cache.remove(path),
915932
         SessionChange::Added(path) | SessionChange::Modified(path) => {
@@ -917,14 +934,41 @@ fn handle_change(app: &AppHandle, cache: &SummaryCache, change: SessionChange) {
917934
             cache.remove(path);
918935
         }
919936
     }
920
-    // Tell the webview something changed. v0 frontend responds with
921
-    // a `rescan` invocation.
937
+    if skip_emit {
938
+        return;
939
+    }
940
+    // Tell the webview something changed. The frontend debounces
941
+    // and responds with a `rescan` invocation.
922942
     let payload = ChangePayload::from(&change);
923943
     if let Err(e) = app.emit("sessions:changed", payload) {
924944
         tracing::warn!(error = %e, "failed to emit sessions:changed");
925945
     }
926946
 }
927947
 
948
+/// Return true if the file path carrying this change is the JSONL
949
+/// transcript of a session currently being driven by a live PTY.
950
+/// Used by `handle_change` to suppress file-watcher events for
951
+/// actively-written sessions so they don't thrash the sidebar.
952
+fn path_matches_live_pty(
953
+    change: &SessionChange,
954
+    active_ptys: &Arc<DashMap<String, PtyEntry>>,
955
+) -> bool {
956
+    let path = match change {
957
+        SessionChange::Added(p)
958
+        | SessionChange::Modified(p)
959
+        | SessionChange::Removed(p) => p,
960
+    };
961
+    let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
962
+        return false;
963
+    };
964
+    for entry in active_ptys.iter() {
965
+        if entry.session_id.as_deref() == Some(stem) {
966
+            return true;
967
+        }
968
+    }
969
+    false
970
+}
971
+
928972
 #[derive(Debug, Clone, serde::Serialize)]
929973
 #[serde(tag = "kind", rename_all = "snake_case")]
930974
 enum ChangePayload {
src/lib/store/sessions.tsmodified
@@ -312,11 +312,19 @@ export const useSessionStore = create<SessionStore>((set, get) => ({
312312
     if (watcherAttached) return;
313313
     watcherAttached = true;
314314
     await onSessionsChanged(() => {
315
+      // 2s debounce is a compromise between feeling responsive when
316
+      // the user finishes a claude turn in the real CLI outside
317
+      // claudex (so we eventually pick up the new session on disk)
318
+      // and avoiding a rescan storm while PTYs are actively
319
+      // writing. The Rust side also suppresses file-watcher events
320
+      // for sessions owned by a live PTY, so this mostly fires for
321
+      // out-of-band changes (external claude runs, git operations,
322
+      // archive pruning).
315323
       if (pendingRescan) clearTimeout(pendingRescan);
316324
       pendingRescan = setTimeout(() => {
317325
         pendingRescan = null;
318326
         void get().rescan();
319
-      }, 500);
327
+      }, 2000);
320328
     });
321329
   },
322330