perf(tui): recheck user command folders at most once a second

Found by a read-only Gemini 4 Argon performance review (agy;
CW/artifacts/improve-sweep-20261018/agy-gemini.md, finding 1), confirmed
by reading user_registry.rs.

While a `/` command is in the composer, render() reaches
with_registry_for_workspace through visible_slash_menu_entries on every
frame. That built command_dirs_snapshot_with_plugins before checking
whether anything had changed: two is_workspace_trusted calls (each reads
and parses ~/.codewhale/config.toml and canonicalizes every [projects]
path), plus metadata and read_dir over up to eight command and workflow
directories and every plugin command directory. During streaming that is
dozens of times a second.

The registry state now records when the snapshot was last compared.
Within SNAPSHOT_RECHECK_INTERVAL (1 s) a read for the same workspace and
extension epoch returns the cached registry without touching the disk.
install_plugin_registry still forces a reload (initialized = false), and
an extension-epoch change still reloads at once. Under cfg(test) the
interval is zero, so tests that write a command file and read it back are
unchanged. UserCommandRegistryState drops its unused Clone derive (the new
Mutex field is not Clone).

Live check (debug TUI, cw7087 harness): typed `/zzh` (no match), wrote
~/.codewhale/commands/zzhello.md, then after about 2.5 s the slash menu
listed "/zzhello  test command for the registry recheck"
(slash-after-file.txt in the sweep folder).

Checks: cargo test -p codewhale-tui --lib -- commands:: slash_menu
user_registry user_commands: 1234 passed, 1 failed. The failure is
diagnostics_adapter_projects_full_context_and_prepared_tool_json, which
also failed under the full parallel run earlier today; alone it passed
3/3. cargo check clean; clippy (tui, CI flags) clean.

Not addressed from the same finding: with_registry_for_plugins still
clones the registry and verifies each plugin command's state file on
every call.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NBKbhx19BRbZtvWK8rVKjz
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-18 19:28:53 -07:00
1 parent 9700a17cab
commit d5f6699cc9
3 files changed
+41 -1

No files matched your search

+3
View File
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- While a slash command is in the composer, the TUI rereads your command
folders and `config.toml` at most once a second instead of on every frame.
A command file you add still shows up in completion within about a second.
- In a session long enough to fold its oldest transcript cells (past
5,000), tool details, expanded tool runs, sub-agent cards and a streaming
reply keep pointing at the right rows. Each fold had shifted them one row
+3
View File
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- While a slash command is in the composer, the TUI rereads your command
folders and `config.toml` at most once a second instead of on every frame.
A command file you add still shows up in completion within about a second.
- In a session long enough to fold its oldest transcript cells (past
5,000), tool details, expanded tool runs, sub-agent cards and a streaming
reply keep pointing at the right rows. Each fold had shifted them one row
+35 -1
View File
@@ -17,7 +17,7 @@ use super::user_commands;
static USER_COMMAND_REGISTRY: OnceLock<RwLock<UserCommandRegistryState>> = OnceLock::new();
#[derive(Debug, Clone, Default)]
#[derive(Debug, Default)]
struct UserCommandRegistryState {
initialized: bool,
workspace: Option<PathBuf>,
@@ -29,6 +29,28 @@ struct UserCommandRegistryState {
/// reloads when the live extension commands may have changed.
extension_epoch: u64,
registry: UserCommandRegistry,
snapshot_checked_at: std::sync::Mutex<Option<std::time::Instant>>,
}
#[cfg(not(test))]
const SNAPSHOT_RECHECK_INTERVAL: std::time::Duration = std::time::Duration::from_secs(1);
#[cfg(test)]
const SNAPSHOT_RECHECK_INTERVAL: std::time::Duration = std::time::Duration::ZERO;
impl UserCommandRegistryState {
fn checked_recently(&self) -> bool {
self.snapshot_checked_at
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_some_and(|at| at.elapsed() < SNAPSHOT_RECHECK_INTERVAL)
}
fn mark_checked(&self) {
*self
.snapshot_checked_at
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(std::time::Instant::now());
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -790,6 +812,16 @@ pub fn with_registry_for_workspace<R>(
) -> R {
let workspace = normalize_workspace(workspace);
let lock = registry_lock();
{
let guard = lock.read().expect("user command registry lock poisoned");
if guard.initialized
&& guard.workspace == workspace
&& guard.extension_epoch == crate::extension_host::command::epoch()
&& guard.checked_recently()
{
return f(&guard.registry);
}
}
let (plugin_sources, plugin_errors) = {
let guard = lock.read().expect("user command registry lock poisoned");
if guard.plugin_workspace == workspace {
@@ -806,6 +838,7 @@ pub fn with_registry_for_workspace<R>(
{
let guard = lock.read().expect("user command registry lock poisoned");
if !registry_needs_reload(&guard, &workspace, &snapshot, extension_epoch) {
guard.mark_checked();
return f(&guard.registry);
}
}
@@ -827,6 +860,7 @@ pub fn with_registry_for_workspace<R>(
guard.extension_epoch = extension_epoch;
guard.registry = replacement;
}
guard.mark_checked();
f(&guard.registry)
}