From e0c8e8d89a9bffec44662389b28fc2a96d037f86 Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Tue, 26 May 2026 07:31:53 -0500 Subject: [PATCH 1/2] fix: grep_files skips large dirs; add version-update footer notification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit grep_files default excludes now include bare directory names (e.g. "node_modules") alongside the "dir/*" variants. The glob matcher requires a '/' to match "dir/*", so the bare form skips the directory traversal entirely instead of descending and filtering each file — fixes 7MB result payloads from node_modules (#2200). Added a background version check that fetches the latest GitHub release tag once per TUI session. When a newer version is available, the footer renders a persistent toast: "vX.Y.Z available — run `codewhale update` and restart" Silent on network errors (5s timeout). --- crates/tui/src/tools/search.rs | 13 +++++++++- crates/tui/src/tui/app.rs | 5 ++++ crates/tui/src/tui/footer_ui.rs | 7 +++++ crates/tui/src/tui/ui.rs | 45 +++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 1 deletion(-) diff --git a/crates/tui/src/tools/search.rs b/crates/tui/src/tools/search.rs index b4fc8d1f..98387cb8 100644 --- a/crates/tui/src/tools/search.rs +++ b/crates/tui/src/tools/search.rs @@ -115,17 +115,28 @@ impl ToolSpec for GrepFilesTool { let exclude_patterns: Vec = input.get("exclude").and_then(|v| v.as_array()).map_or_else( || { - // Default exclusions for common non-code directories + // Default exclusions for common non-code directories. + // Bare directory names skip the directory traversal entirely; + // `dir/*` filters files inside if the directory is already + // being walked (belt-and-suspenders — see #2200). vec![ + "node_modules".to_string(), "node_modules/*".to_string(), + ".git".to_string(), ".git/*".to_string(), + "target".to_string(), "target/*".to_string(), "*.min.js".to_string(), "*.min.css".to_string(), + "dist".to_string(), "dist/*".to_string(), + "build".to_string(), "build/*".to_string(), + "__pycache__".to_string(), "__pycache__/*".to_string(), + ".venv".to_string(), ".venv/*".to_string(), + "venv".to_string(), "venv/*".to_string(), ] }, diff --git a/crates/tui/src/tui/app.rs b/crates/tui/src/tui/app.rs index 68cc3539..0969c8f8 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -1076,6 +1076,10 @@ pub struct App { pub status_toasts: VecDeque, /// Sticky status toast used for important warnings/errors. pub sticky_status: Option, + /// Version-update hint shown in the footer when a newer release + /// is available. Set by a background GitHub API check after app + /// startup; `None` until the check completes or if up-to-date. + pub version_hint: Option, /// Last status text already promoted from `status_message` into toast state. pub last_status_message_seen: Option, pub model: String, @@ -1801,6 +1805,7 @@ impl App { status_message: None, status_toasts: VecDeque::new(), sticky_status: None, + version_hint: None, last_status_message_seen: None, model, auto_model, diff --git a/crates/tui/src/tui/footer_ui.rs b/crates/tui/src/tui/footer_ui.rs index 0269c8de..e8df159c 100644 --- a/crates/tui/src/tui/footer_ui.rs +++ b/crates/tui/src/tui/footer_ui.rs @@ -44,6 +44,13 @@ pub(crate) fn render_footer(f: &mut Frame, area: Rect, app: &mut App) { None }; let toast = quit_prompt.or_else(|| { + // Version-update hint takes precedence over ephemeral status toasts + // so the user sees it even when status traffic would hide it. + app.version_hint.as_ref().map(|hint| FooterToast { + text: hint.clone(), + color: palette::STATUS_INFO, + }) + }).or_else(|| { app.active_status_toast().map(|toast| FooterToast { text: toast.text, color: status_color(toast.level), diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 61cb195c..552f302b 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -893,7 +893,52 @@ async fn run_event_loop( .checked_sub(Duration::from_secs(60)) .unwrap_or_else(Instant::now); + // Fire-and-forget version check — runs once per session in the + // background. On success, `app.version_hint` is set and the footer + // renders the update recommendation on the next frame. + let mut version_check: Option>> = Some({ + let current = env!("CARGO_PKG_VERSION").to_string(); + tokio::spawn(async move { + let client = match reqwest::Client::builder() + .user_agent("codewhale-version-check") + .timeout(std::time::Duration::from_secs(5)) + .build() + { + Ok(c) => c, + Err(_) => return None, + }; + let resp = client + .get("https://api.github.com/repos/Hmbown/CodeWhale/releases/latest") + .header("Accept", "application/vnd.github+json") + .send() + .await + .ok()?; + let json: serde_json::Value = resp.json().await.ok()?; + let tag = json["tag_name"].as_str()?; + let latest = tag.trim_start_matches('v'); + if latest != current { + Some(format!( + "v{latest} available — run `codewhale update` and restart" + )) + } else { + None + } + }) + }); + loop { + // Drain the version-check handle once; re-assign None so we + // don't poll it again. + let mut done = false; + if let Some(ref handle) = version_check { + done = handle.is_finished(); + } + if done { + if let Ok(Some(hint)) = version_check.take().unwrap().await { + app.version_hint = Some(hint); + } + } + if !drain_web_config_events(&mut web_config_session, app, config, &engine_handle).await { web_config_session = None; } From 923911ae1deff5890551d24a67c098a5c38c3760 Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Tue, 26 May 2026 07:58:40 -0500 Subject: [PATCH 2/2] fix: use semver comparison for version-update hint Replaced naive with parse_semver tuple comparison so dev builds (e.g. "0.8.46-pre") don't trigger false update hints. Falls back to string compare when either side is non-semver. Caught by Gemini Code Assist review on PR #2181. --- crates/tui/src/tui/ui.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 552f302b..316bbd34 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -916,7 +916,14 @@ async fn run_event_loop( let json: serde_json::Value = resp.json().await.ok()?; let tag = json["tag_name"].as_str()?; let latest = tag.trim_start_matches('v'); - if latest != current { + // Compare semver so dev builds (e.g. "0.8.46-pre") don't + // trigger false hints. Falls back to string compare on + // unparseable versions. + let newer = match (parse_semver(latest), parse_semver(¤t)) { + (Some(l), Some(c)) => l > c, + _ => latest != current, + }; + if newer { Some(format!( "v{latest} available — run `codewhale update` and restart" )) @@ -7984,5 +7991,15 @@ fn extract_reasoning_header(text: &str) -> Option { } } +/// Parse a `major.minor.patch` version string into a comparable tuple. +/// Returns `None` on any parse failure (non-semver, dev suffixes, etc.). +fn parse_semver(v: &str) -> Option<(u32, u32, u32)> { + let mut parts = v.splitn(3, '.'); + let major = parts.next()?.parse::().ok()?; + let minor = parts.next()?.parse::().ok()?; + let patch = parts.next().unwrap_or("0").parse::().ok()?; + Some((major, minor, patch)) +} + #[cfg(test)] mod tests;