feat(file): cancellable list_dir with timeout (control-plane workstream)#2035
Conversation
Moves the synchronous fs::read_dir loop into a tokio::spawn_blocking worker with CancellationToken and timeout. The tool honors the standard context cancel token so in-flight directory listings are interrupted on user cancel or engine stop, and a 30-second fallback timeout prevents hung NFS/CIFS mounts from freezing the tool surface. - list_dir_entries runs the blocking loop with periodic cancellation checks - run_blocking_list_dir wraps it in spawn_blocking + tokio::select! - Existing tests pass; adds test_list_dir_respects_cancel_token and test_list_dir_blocking_wrapper_reports_timeout - ToolError::Timeout variant used for timeout reports Part of the v0.8.45 control-plane cancellation workstream.
There was a problem hiding this comment.
Code Review
This pull request introduces cancellation and timeout support for the ListDirTool and FileSearchTool. Both tools now execute their blocking file system operations within tokio::task::spawn_blocking and utilize CancellationToken and a 30-second timeout to ensure they do not hang indefinitely or ignore shutdown signals. New tests have been added to verify that both tools correctly respect cancellation tokens and report timeouts. I have no feedback to provide.
There was a problem hiding this comment.
Pull request overview
This PR advances the v0.8.45 control-plane cancellation workstream by making filesystem traversal tools stop blocking the async runtime: it moves list_dir (and also file_search) onto tokio::spawn_blocking, adds CancellationToken checks, and enforces a 30s fallback timeout to avoid hung mounts freezing tool execution.
Changes:
- Refactors
list_dirto run the blockingfs::read_dirloop in aspawn_blockingworker with cancellation checks and a 30s timeout. - Refactors
file_searchtraversal similarly, adding cancellation checks during walking/scoring and a 30s timeout wrapper. - Adds unit tests covering pre-cancelled execution and timeout reporting for both tools.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| crates/tui/src/tools/file.rs | Makes list_dir cancellable/timeout-safe by wrapping blocking directory iteration in spawn_blocking + cancellation/timeout handling; adds tests. |
| crates/tui/src/tools/file_search.rs | Makes file_search cancellable/timeout-safe via spawn_blocking wrapper with cancellation/timeout handling; adds tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let task = tokio::task::spawn_blocking(list_dir); | ||
| let result = match cancel_token { | ||
| Some(token) => { | ||
| tokio::select! { | ||
| biased; | ||
| () = token.cancelled() => return Err(list_dir_cancelled()), | ||
| result = tokio::time::timeout(timeout, task) => result, | ||
| } | ||
| } | ||
| None => tokio::time::timeout(timeout, task).await, |
| let task = tokio::task::spawn_blocking(search); | ||
| let result = match cancel_token { | ||
| Some(token) => { | ||
| tokio::select! { | ||
| biased; | ||
| () = token.cancelled() => return Err(file_search_cancelled()), | ||
| result = tokio::time::timeout(timeout, task) => result, | ||
| } | ||
| } | ||
| None => tokio::time::timeout(timeout, task).await, | ||
| }; | ||
|
|
||
| let joined = result.map_err(|_| file_search_timeout(timeout))?; |
| let matches = search_files_async( | ||
| query.to_string(), | ||
| base_path, | ||
| extensions, | ||
| exclude_patterns, | ||
| limit, | ||
| context.cancel_token.clone(), | ||
| FILE_SEARCH_TIMEOUT, | ||
| ) | ||
| .await?; |
| let err = run_blocking_list_dir(Duration::from_millis(1), None, || { | ||
| std::thread::sleep(Duration::from_millis(50)); |
| let err = run_blocking_file_search(Duration::from_millis(1), None, || { | ||
| std::thread::sleep(Duration::from_millis(50)); |
Replace the sequential-spawn-index whale-nickname system with a deterministic hash-based naming scheme that maps each agent ID to a stable whale species name. The same agent ID always gets the same friendly name — even across session restarts for persisted agents. - whale_name_for_id(id): hash agent ID → WHALE_NICKNAMES index - assign_unique_whale_name(id, active_names): deterministic with collision avoidance, appends numeric suffix when base name is taken - Expand WHALE_NICKNAMES from 25 to ~45 Cetacea species including baleen whales, toothed whales, and select dolphins (Delphinidae); porpoises excluded as labels that don't carry well - SubAgent::new now accepts a pre-generated id parameter so the spawn method can hash it before construction - SubAgentsView popup now shows friendly nickname next to raw agent ID (dimmed) instead of hiding it - live_subagent_result accepts optional nickname parameter - whale_nickname_for_index kept as legacy public API for test snapshots 137 sub-agent tests pass. Taxonomy source: Society for Marine Mammalogy (2025).
Wires the /balance command into the slash-command registry and dispatch table. The command handler currently returns a placeholder message per provider; the async HTTP dispatch (calling provider balance endpoints for DeepSeek, OpenRouter, Novita) lands in a follow-up PR. - New commands/balance.rs module with provider-aware handler - Command registration in COMMANDS array - Dispatch via 'balance' match arm Scout findings (agent_e154c802) mapped the endpoint URLs and the AppAction async dispatch pattern; the full implementation follows.
Summary
Collects the v0.8.45 control-plane slices currently on this branch:
file_searchtraversal cancellable so user stop/cancel requests can interrupt long walks.list_dircancellable and timeout-safe with a blocking worker and 30-second fallback timeout./balanceslash command scaffold for v0.8.45 provider billing: audit cost math and add /balance command #2019 with honest placeholder behavior until provider balance network dispatch lands.Stewardship / credit
tools::js_executiontimeouts, so this branch keeps the local cancellation implementation while preserving the contributor context.Testing
cargo fmt --all -- --checkgit diff --checkcargo clippy -p codewhale-tui --bin codewhale-tui --locked -- -D warningscargo test -p codewhale-tui --bin codewhale-tui file_search --lockedcargo test -p codewhale-tui --bin codewhale-tui list_dir --lockedcargo test -p codewhale-tui --bin codewhale-tui subagent --lockedcargo test -p codewhale-tui --bin codewhale-tui balance_command --lockedcargo test -p codewhale-tui --bin codewhale-tui every_registered_command_dispatches_to_a_handler --lockedcargo test -p codewhale-tui --bin codewhale-tui localization --lockedFollow-up
/balancestill needs the provider capability layer, auth handling, response parsing, and copyable receipt UI before v0.8.45 provider billing: audit cost math and add /balance command #2019 can close.