From e0d0860d7ba0ab6a4c9b9f1562f6a767a6424d99 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Wed, 15 Jul 2026 06:22:31 +0700 Subject: [PATCH] fix(state): jangan bangun mention index di mode attach MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AppStateRest::new() men-spawn thread ignore::Walk untuk mention_index tanpa syarat, padahal mode --attach cuma dipakai untuk render lokal — handle_key dan logika mention berjalan di sisi daemon lewat IPC, jadi index di client attach tidak pernah dipakai. Ini bikin setiap --attach melakukan full workspace walk (sampai 50.000 entry) sia-sia. Pindahkan thread-spawn itu ke method terpisah spawn_mention_index_build(), dipanggil eksplisit dari run_single_process() dan run_daemon() setelah AppStateRest::new(), tapi sengaja tidak dipanggil dari run_attach(). --- src/app/state/rest.rs | 83 ++++++++++++++++++++++++------------------- src/main.rs | 2 ++ 2 files changed, 48 insertions(+), 37 deletions(-) diff --git a/src/app/state/rest.rs b/src/app/state/rest.rs index 1c44318..15acdea 100644 --- a/src/app/state/rest.rs +++ b/src/app/state/rest.rs @@ -206,45 +206,54 @@ impl AppStateRest { }); } - // Fire-and-forget background file index build for `@file` mention - // autocomplete. - // - // Flow: spawn OS thread -> `ignore::Walk` each workspace root, - // collecting file paths (workspace-index-prefixed for roots beyond - // the first, matching `resolve_path`'s `[N]path` convention) -> stop - // once 50,000 entries are collected -> store the result in - // `mention_index`. - // - // Why a raw thread and not a background tokio task: there is no - // persistent async runtime driving the render loop, and this is - // blocking filesystem I/O -- a dedicated thread keeps startup - // non-blocking. Not joined, same rationale as the LSP provisioning - // thread above: a slow/huge repo must not delay the TUI appearing. - { - let mention_index = state.mention_index.clone(); - let roots = state.workspace_roots.clone(); - std::thread::spawn(move || { - const MAX_MENTION_ENTRIES: usize = 50_000; - let mut paths = Vec::new(); - 'roots: for (i, root) in roots.iter().enumerate() { - for entry in ignore::Walk::new(root).flatten() { - if !entry.path().is_file() { - continue; - } - let rel = entry.path().strip_prefix(root).unwrap_or(entry.path()); - let rel_str = rel.display().to_string(); - let formatted = if i == 0 { rel_str } else { format!("[{i}]{rel_str}") }; - paths.push(formatted); - if paths.len() >= MAX_MENTION_ENTRIES { - break 'roots; - } + state + } + + /// Spawn the background thread that walks every workspace root and + /// populates `mention_index` for `@file` mention autocomplete. + /// + /// Why a separate method, not called from `new()`: the attach-only + /// TUI client also constructs an `AppStateRest` (for local rendering + /// state) but never runs tools or `handle_key` locally — it forwards + /// keystrokes to the daemon over IPC, which has its own `AppStateRest` + /// with its own index. Spawning this walk in the attach client would + /// waste a full workspace scan for an index nothing there consumes. + /// Callers that DO need the index (single-process mode, the daemon) + /// call this explicitly after construction. + /// + /// Flow: spawn OS thread -> `ignore::Walk` each workspace root, + /// collecting file paths (workspace-index-prefixed for roots beyond + /// the first, matching `resolve_path`'s `[N]path` convention) -> stop + /// once 50,000 entries are collected -> store the result in + /// `mention_index`. + /// + /// Why a raw thread and not a background tokio task: there is no + /// persistent async runtime driving the render loop, and this is + /// blocking filesystem I/O -- a dedicated thread keeps startup + /// non-blocking. Not joined, same rationale as the LSP provisioning + /// thread above: a slow/huge repo must not delay the TUI appearing. + pub fn spawn_mention_index_build(&self) { + let mention_index = self.mention_index.clone(); + let roots = self.workspace_roots.clone(); + std::thread::spawn(move || { + const MAX_MENTION_ENTRIES: usize = 50_000; + let mut paths = Vec::new(); + 'roots: for (i, root) in roots.iter().enumerate() { + for entry in ignore::Walk::new(root).flatten() { + if !entry.path().is_file() { + continue; + } + let rel = entry.path().strip_prefix(root).unwrap_or(entry.path()); + let rel_str = rel.display().to_string(); + let formatted = if i == 0 { rel_str } else { format!("[{i}]{rel_str}") }; + paths.push(formatted); + if paths.len() >= MAX_MENTION_ENTRIES { + break 'roots; } } - mention_index.set(paths); - }); - } - - state + } + mention_index.set(paths); + }); } /// Whether an agent turn is currently running. diff --git a/src/main.rs b/src/main.rs index f643a53..89fabb3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -109,6 +109,7 @@ fn run_single_process() -> Result<()> { &session_dir, store.memory_dir, ); + state.spawn_mention_index_build(); state.sessions = model::session::Session::list(&store.base_dir); @@ -442,6 +443,7 @@ fn run_daemon() -> Result<()> { &session_dir, store.memory_dir, ); + state.spawn_mention_index_build(); state.sessions = model::session::Session::list(&store.base_dir); let _rt = tokio::runtime::Runtime::new()?;