Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d076a0b
build: add fff-search
jsgrrchg Oct 9, 2026
0c6b94e
test(engine): add a workspace search benchmark
jsgrrchg Oct 9, 2026
be67fc7
feat(engine): add WorkspaceSearch on top of fff
jsgrrchg Oct 9, 2026
5a11499
feat(engine): serve SearchFiles (@) from fff
jsgrrchg Oct 9, 2026
bf019b9
feat(engine): serve SearchWorkspaceFiles (file tree) from fff
jsgrrchg Oct 9, 2026
e81c6e6
chore: drop nucleo-matcher
jsgrrchg Oct 9, 2026
c56fd02
feat(rpc): add WarmWorkspaceSearch
jsgrrchg Oct 9, 2026
83b3a59
test(ui): add the missing images field to a ToolItem fixture
jsgrrchg Oct 9, 2026
259bf06
feat(ui): warm workspace search on chat focus
jsgrrchg Oct 9, 2026
3ded7cd
feat(rpc): add SearchWorkspaceContent
jsgrrchg Oct 9, 2026
7b11b6d
feat(ui): scored thread search for the command palette
jsgrrchg Oct 9, 2026
f6464a4
refactor(ui): split the command palette into sections and tabs
jsgrrchg Oct 9, 2026
af67633
feat(ui): command palette tabs with mod-1…4
jsgrrchg Oct 9, 2026
5d3f9ec
feat(ui): empty-state sections in the command palette
jsgrrchg Oct 9, 2026
32be256
feat(ui): ranked thread search in the command palette
jsgrrchg Oct 9, 2026
c592906
feat(ui): search file names from the command palette
jsgrrchg Oct 9, 2026
92327f6
feat(ui): search file contents from the command palette
jsgrrchg Oct 9, 2026
dd854c4
perf(engine): return an evicted index's memory to the allocator
jsgrrchg Oct 9, 2026
b2e8dde
perf(engine): cap each search index's content cache at 16 MB
jsgrrchg Oct 9, 2026
7ed0d35
docs: workspace search on fff
jsgrrchg Oct 9, 2026
2df2729
feat(rpc): add SearchHomeFolders
jsgrrchg Oct 10, 2026
0cfd6d1
feat(ui): search folders under home when adding a project
jsgrrchg Oct 10, 2026
1da2e3c
perf(engine): index workspace paths only, grep contents without a big…
jsgrrchg Oct 10, 2026
eab3cc7
perf(engine): keep idle search indexes 15 min, up to 7, warmed once o…
jsgrrchg Oct 10, 2026
daa7203
fix(ui): refresh palette content search while indexing
jsgrrchg Oct 10, 2026
d3435bb
fix(engine): preserve literal workspace content queries
jsgrrchg Oct 10, 2026
31d647b
fix(ui): refresh palette file search while indexing
jsgrrchg Oct 10, 2026
bc12118
fix(ui): refresh home folder search while indexing
jsgrrchg Oct 10, 2026
b971da9
fix: handle extended Windows and UNC paths in home folder picker
jsgrrchg Oct 10, 2026
b2573ce
fix(ui): convert content search byte columns for editor navigation
jsgrrchg Oct 10, 2026
4c7c7dc
fix(engine): normalize Windows separators in name searches
jsgrrchg Oct 10, 2026
098d878
fix(ui): back off palette content refreshes while indexing
jsgrrchg Oct 10, 2026
8aeb334
perf(ui): skip the palette's index probe once the workspace is ready
jsgrrchg Oct 10, 2026
e88194b
fix(ui): match Windows folder paths regardless of drive letter case
jsgrrchg Oct 10, 2026
b6013cc
Merge remote-tracking branch 'origin/main' into zeron/unified-workspa…
jsgrrchg Oct 10, 2026
ee468e8
Merge remote-tracking branch 'origin/main' into zeron/unified-workspa…
jsgrrchg Oct 10, 2026
508313b
docs(ui): describe the home index release without pins
jsgrrchg Oct 10, 2026
987393b
Merge remote-tracking branch 'upstream/main' into feat/workspace-sear…
jsgrrchg Oct 10, 2026
8e53b29
Workspace search: review fixes and palette polish
wingleeio Oct 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
579 changes: 546 additions & 33 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,9 @@ ttf-parser = "0.25.1"
# Windows system proxy for provider usage probes and other HTTP clients.
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json", "stream", "system-proxy"] }
ignore = "0.4"
nucleo-matcher = "0.3"
# Workspace file/content search index (@ mentions, file tree, cmd+K).
# Pinned exactly: crates.io also carries `0.11.1-nightly.*` prereleases.
fff-search = "=0.11.0"

[profile.release]
# Distribution profile (scripts/package-linux.sh): thin LTO buys most of fat
Expand Down
3 changes: 2 additions & 1 deletion crates/engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ tokio-util = { workspace = true, features = ["rt"] }
sha2.workspace = true
reqwest.workspace = true
ignore.workspace = true
nucleo-matcher.workspace = true
fff-search.workspace = true

tempfile.workspace = true

[target.'cfg(windows)'.dependencies]
dunce = "1"
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_Pipes", "Win32_System_Threading"] }

[dev-dependencies]
Expand Down
159 changes: 159 additions & 0 deletions crates/engine/examples/workspace_search_bench.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
//! Workspace search benchmark: the `SearchFiles` (`@`),
//! `SearchWorkspaceFiles` (file tree) and `SearchWorkspaceContent` (cmd+K)
//! paths against one folder.
//!
//! ```text
//! BENCH_ROOT=/path/to/checkout cargo run --release -p zeron-engine \
//! --example workspace_search_bench
//! ```
//!
//! `BENCH_QUERIES` (comma separated) overrides the default queries and
//! `BENCH_RUNS` the number of timed runs per query (default 10, after one
//! warm-up). RSS comes from `/proc/self/status`, so it prints only on Linux.
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};

use zeron_engine::Repos;

const DEFAULT_QUERIES: &[&str] = &[
"composer",
"repos",
"cmdpal",
"Cargo.toml",
"workspace_files",
];

fn main() -> Result<(), Box<dyn std::error::Error>> {
let root = std::env::var_os("BENCH_ROOT")
.map(PathBuf::from)
.ok_or("set BENCH_ROOT to the folder to search")?;
let root = std::fs::canonicalize(root)?;
let queries: Vec<String> = match std::env::var("BENCH_QUERIES") {
Ok(list) => list
.split(',')
.map(str::trim)
.filter(|q| !q.is_empty())
.map(str::to_owned)
.collect(),
Err(_) => DEFAULT_QUERIES.iter().map(|q| (*q).to_owned()).collect(),
};
let runs: usize = std::env::var("BENCH_RUNS")
.ok()
.and_then(|runs| runs.parse().ok())
.unwrap_or(10)
.max(1);
let runtime = tokio::runtime::Runtime::new()?;
let data = tempfile::tempdir()?;
let repos = Repos::with_worktrees_root(data.path(), "bench", data.path().join("worktrees"));

println!("root: {}", root.display());
print_rss("baseline");

let search = repos.workspace_search();
let started = Instant::now();
search.warm(&root)?;
if !search.wait_until_indexed(&root, Duration::from_secs(600)) {
return Err("index did not finish within 10 minutes".into());
}
println!(
"index ready in {:?}",
started.elapsed()
);
print_rss("after index");

println!();
println!(
"{:<38} {:>10} {:>10} {:>10} {:>8}",
"query", "min", "median", "max", "results"
);
for query in &queries {
let (timings, results) = measure(runs, || {
runtime
.block_on(repos.search_files(root.clone(), query.clone(), Vec::new()))
.map(|matches| matches.len())
})?;
report(&format!("SearchFiles {query}"), &timings, results);
}
for query in &queries {
let (timings, results) = measure(runs, || tree_search(&repos, &root, query, false))?;
report(&format!("SearchWorkspaceFiles {query}"), &timings, results);
}
for query in &queries {
let (timings, results) = measure(runs, || tree_search(&repos, &root, query, true))?;
report(&format!("…including ignored {query}"), &timings, results);
}
for query in &queries {
let (timings, results) = measure(runs, || {
search
.search_content(&root, query, 100, 3)
.map(|found| found.matches.len())
})?;
report(
&format!("SearchWorkspaceContent {query}"),
&timings,
results,
);
}
print_rss("after all queries");
search.release_all();
// Release runs off-thread; give it and the allocator a moment.
std::thread::sleep(Duration::from_secs(2));
println!("live indexes after release: {}", search.live());
print_rss("after evicting the index");
Ok(())
}

fn tree_search(
repos: &Repos,
root: &Path,
query: &str,
include_ignored: bool,
) -> Result<usize, Box<dyn std::error::Error>> {
Ok(zeron_engine::workspace_files::bench_search_root(
repos.workspace_search(),
root,
query,
include_ignored,
zeron_engine::workspace_files::MAX_SEARCH_RESULTS,
)?
.len())
}

/// One warm-up, then `runs` timed calls.
fn measure<E: Into<Box<dyn std::error::Error>>>(
runs: usize,
mut search: impl FnMut() -> Result<usize, E>,
) -> Result<(Vec<Duration>, usize), Box<dyn std::error::Error>> {
search().map_err(Into::into)?;
let mut timings = Vec::with_capacity(runs);
let mut results = 0;
for _ in 0..runs {
let started = Instant::now();
results = search().map_err(Into::into)?;
timings.push(started.elapsed());
}
timings.sort();
Ok((timings, results))
}

fn report(label: &str, timings: &[Duration], results: usize) {
println!(
"{label:<38} {:>10} {:>10} {:>10} {results:>8}",
format_duration(timings[0]),
format_duration(timings[timings.len() / 2]),
format_duration(timings[timings.len() - 1]),
);
}

fn format_duration(duration: Duration) -> String {
format!("{:.2}ms", duration.as_secs_f64() * 1000.0)
}

fn print_rss(label: &str) {
let Ok(status) = std::fs::read_to_string("/proc/self/status") else {
return;
};
if let Some(line) = status.lines().find(|line| line.starts_with("VmRSS:")) {
println!("RSS {label}: {}", line.trim_start_matches("VmRSS:").trim());
}
}
2 changes: 2 additions & 0 deletions crates/engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub mod uploads;
pub mod voice;
pub mod workspace_files;
pub mod workspace_host;
pub mod workspace_search;

pub use agent_accounts::{AgentAccounts, AgentAccountsConfig};
pub use auth::{Auth, AuthConfig, AuthState, AuthUser, OrgMembership};
Expand Down Expand Up @@ -76,6 +77,7 @@ pub use workspace_files::WorkspaceFiles;
pub use workspace_host::{
DEFAULT_ORG_ID, DEFAULT_USER_ID, WORKSPACE_DOC_ID, WorkspaceHost, WorkspaceHostConfig,
};
pub use workspace_search::WorkspaceSearch;

pub(crate) const LEGACY_UNKNOWN_DEVICE_NAME: &str = "unknown-device";

Expand Down
Loading
Loading