Merge pull request #2181 from Hmbown/fix/search-excludes-and-version-hint

fix: grep_files skips large dirs; add version-update footer notification
This commit is contained in:
Hunter Bown
2026-05-26 07:59:57 -05:00
committed by GitHub
4 changed files with 86 additions and 1 deletions
+12 -1
View File
@@ -115,17 +115,28 @@ impl ToolSpec for GrepFilesTool {
let exclude_patterns: Vec<String> =
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(),
]
},
+5
View File
@@ -1076,6 +1076,10 @@ pub struct App {
pub status_toasts: VecDeque<StatusToast>,
/// Sticky status toast used for important warnings/errors.
pub sticky_status: Option<StatusToast>,
/// 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<String>,
/// Last status text already promoted from `status_message` into toast state.
pub last_status_message_seen: Option<String>,
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,
+7
View File
@@ -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),
+62
View File
@@ -893,7 +893,59 @@ 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<tokio::task::JoinHandle<Option<String>>> = 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');
// 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(&current)) {
(Some(l), Some(c)) => l > c,
_ => latest != current,
};
if newer {
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;
}
@@ -7939,5 +7991,15 @@ fn extract_reasoning_header(text: &str) -> Option<String> {
}
}
/// 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::<u32>().ok()?;
let minor = parts.next()?.parse::<u32>().ok()?;
let patch = parts.next().unwrap_or("0").parse::<u32>().ok()?;
Some((major, minor, patch))
}
#[cfg(test)]
mod tests;