fix(state): jangan bangun mention index di mode attach

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().
This commit is contained in:
asepharyana
2026-07-15 06:32:57 +07:00
parent dcfc5b9ec0
commit e0d0860d7b
2 changed files with 48 additions and 37 deletions
+46 -37
View File
@@ -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.
+2
View File
@@ -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()?;