v0.2.12 : ligne de statut — temps dexecution de la derniere commande (⚡ ms/s) + ls parallele (tailles de dossiers reparties sur les coeurs, un seul appel de metadonnees par entree) — 130 entrees instantanees

This commit is contained in:
2026-08-16 10:29:38 -04:00
parent 0f4ba71219
commit 93c32b60ba
4 changed files with 99 additions and 56 deletions
Generated
+1 -1
View File
@@ -21,7 +21,7 @@ dependencies = [
[[package]] [[package]]
name = "agent-manager" name = "agent-manager"
version = "0.2.11" version = "0.2.12"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "agent-manager" name = "agent-manager"
version = "0.2.11" version = "0.2.12"
edition = "2021" edition = "2021"
description = "Manage local AI coding agents: list, install, start, stop, update — with automatic dependency handling and a YAML-driven catalog." description = "Manage local AI coding agents: list, install, start, stop, update — with automatic dependency handling and a YAML-driven catalog."
license = "MIT" license = "MIT"
+74 -48
View File
@@ -51,9 +51,8 @@ pub fn dir_size(path: &Path) -> Option<u64> {
Some(total) Some(total)
} }
fn modified_str(path: &Path) -> String { fn modified_text(meta: &std::fs::Metadata) -> String {
std::fs::metadata(path) meta.modified()
.and_then(|m| m.modified())
.ok() .ok()
.map(|t| { .map(|t| {
let dt: chrono::DateTime<chrono::Local> = t.into(); let dt: chrono::DateTime<chrono::Local> = t.into();
@@ -94,59 +93,86 @@ fn is_executable(path: &Path, meta: &std::fs::Metadata) -> bool {
} }
} }
/// One ls row: (sort key, cells). A single symlink_metadata call per entry
/// (the modification time comes from the same metadata).
fn row_for(p: &Path, color: bool) -> Option<(String, Vec<Cell>)> {
let meta = std::fs::symlink_metadata(p).ok()?;
let file_type = meta.file_type();
let name = p.file_name().and_then(|n| n.to_str())?.to_string();
let (kind, style) = if file_type.is_symlink() {
("symlink", if p.exists() { 3 } else { 4 })
} else if file_type.is_dir() {
("dir", 1)
} else if is_executable(p, &meta) {
("file", 2)
} else {
("file", 0)
};
let (size_text, size_bytes) = if file_type.is_dir() {
match dir_size(p) {
Some(s) => (format_size(s), Some(s)),
None => ("—".to_string(), None),
}
} else {
(format_size(meta.len()), Some(meta.len()))
};
let mut display = name.clone();
if file_type.is_dir() {
display.push('/');
} else if file_type.is_symlink() {
if let Ok(target) = std::fs::read_link(p) {
display.push_str(&format!(" -> {}", target.display()));
}
}
Some((
name.to_lowercase(),
vec![
Cell::str(paint(&display, color, style)),
Cell::str(kind.to_string()),
Cell::bytes(size_bytes.unwrap_or(0), size_text),
Cell::str(modified_text(&meta)),
],
))
}
/// Build the ls table (columns: name, type, size, modified). Directories /// Build the ls table (columns: name, type, size, modified). Directories
/// come first, then alphabetical order. /// come first, then alphabetical order.
///
/// Rows are built in parallel: recursive directory sizes dominate the cost,
/// so a listing with many subdirectories stays fast (like Nushell, which
/// also parallelizes its listing).
pub fn ls_table(path: &Path, color: bool) -> Result<DataTable> { pub fn ls_table(path: &Path, color: bool) -> Result<DataTable> {
let rd = std::fs::read_dir(path) let rd = std::fs::read_dir(path)
.with_context(|| format!("cannot read {}", path.display()))?; .with_context(|| format!("cannot read {}", path.display()))?;
let mut rows: Vec<(String, Vec<Cell>)> = Vec::new(); let entries: Vec<PathBuf> = rd.flatten().map(|e| e.path()).collect();
for entry in rd.flatten() {
let p = entry.path(); let threads = std::thread::available_parallelism()
let meta = match std::fs::symlink_metadata(&p) { .map(|n| n.get())
Ok(m) => m, .unwrap_or(4)
Err(_) => continue, .min(8);
}; let chunk_size = entries.len().div_ceil(threads).max(1);
let file_type = meta.file_type(); let rows: Vec<(String, Vec<Cell>)> = std::thread::scope(|scope| {
let name = p let mut handles = Vec::new();
.file_name() for chunk in entries.chunks(chunk_size) {
.map(|n| n.to_string_lossy().to_string()) let chunk: Vec<PathBuf> = chunk.to_vec();
.unwrap_or_default(); handles.push(scope.spawn(move || {
let (kind, style) = if file_type.is_symlink() { chunk
("symlink", if p.exists() { 3 } else { 4 }) .into_iter()
} else if file_type.is_dir() { .filter_map(|p| row_for(&p, color))
("dir", 1) .collect::<Vec<_>>()
} else if is_executable(&p, &meta) { }));
("file", 2) }
} else { let mut out = Vec::new();
("file", 0) for handle in handles {
}; if let Ok(mut rows) = handle.join() {
let (size_text, size_bytes) = if file_type.is_dir() { out.append(&mut rows);
match dir_size(&p) {
Some(s) => (format_size(s), Some(s)),
None => ("—".to_string(), None),
}
} else {
(format_size(meta.len()), Some(meta.len()))
};
let mut display = name.clone();
if file_type.is_dir() {
display.push('/');
} else if file_type.is_symlink() {
if let Ok(target) = std::fs::read_link(&p) {
display.push_str(&format!(" -> {}", target.display()));
} }
} }
rows.push(( out
name.to_lowercase(), });
vec![
Cell::str(paint(&display, color, style)),
Cell::str(kind.to_string()),
Cell::bytes(size_bytes.unwrap_or(0), size_text),
Cell::str(modified_str(&p)),
],
));
}
// Directories first, then alphabetical. // Directories first, then alphabetical.
let mut rows = rows;
rows.sort_by(|a, b| { rows.sort_by(|a, b| {
let a_dir = a.1[1].text == "dir"; let a_dir = a.1[1].text == "dir";
let b_dir = b.1[1].text == "dir"; let b_dir = b.1[1].text == "dir";
+23 -6
View File
@@ -475,7 +475,9 @@ pub fn banner(app: &App, session: &ShellSession, session_id: &str) -> String {
} }
/// One-line status shown before every prompt (Hermes-style status bar). /// One-line status shown before every prompt (Hermes-style status bar).
fn print_status(app: &App, session: &ShellSession, started: Instant) { /// The second parameter is the duration of the previous command (None before
/// the first one).
fn print_status(app: &App, session: &ShellSession, started: Instant, last: Option<Duration>) {
let color = app.color(); let color = app.color();
let sep = dim(" │ ", color); let sep = dim(" │ ", color);
let cwd = shorten_head(&current_dir_string(), 26); let cwd = shorten_head(&current_dir_string(), 26);
@@ -493,9 +495,16 @@ fn print_status(app: &App, session: &ShellSession, started: Instant) {
), ),
color, color,
); );
let last_text = match last {
Some(d) if d.as_secs() >= 1 => format_elapsed(d),
Some(d) if d.as_millis() >= 1 => format!("{}ms", d.as_millis()),
Some(d) => format!("{}µs", d.as_micros()),
None => "—".to_string(),
};
let last_part = dim(&format!("⚡ {last_text}"), color);
let elapsed = dim(&format_elapsed(started.elapsed()), color); let elapsed = dim(&format_elapsed(started.elapsed()), color);
println!( println!(
"{}am v{}{sep}{shell_part}{sep}{counts}{sep}⏲ {elapsed}", "{}am v{}{sep}{shell_part}{sep}{counts}{sep}{last_part}{sep}⏲ {elapsed}",
dim(" ⚙ ", color), dim(" ⚙ ", color),
env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_VERSION"),
); );
@@ -694,8 +703,9 @@ fn run_with_editor(app: &App, session: &mut ShellSession) -> Result<i32> {
} }
let started = Instant::now(); let started = Instant::now();
let mut last: Option<DataTable> = None; let mut last: Option<DataTable> = None;
let mut last_duration: Option<Duration> = None;
loop { loop {
print_status(app, session, started); print_status(app, session, started, last_duration);
match rl.readline("❯ ") { match rl.readline("❯ ") {
Ok(line) => { Ok(line) => {
let trimmed = line.trim().to_string(); let trimmed = line.trim().to_string();
@@ -703,7 +713,10 @@ fn run_with_editor(app: &App, session: &mut ShellSession) -> Result<i32> {
continue; continue;
} }
let _ = rl.add_history_entry(trimmed.as_str()); let _ = rl.add_history_entry(trimmed.as_str());
match handle_input(app, session, &mut last, &trimmed) { let t = Instant::now();
let result = handle_input(app, session, &mut last, &trimmed);
last_duration = Some(t.elapsed());
match result {
Ok(true) => break, Ok(true) => break,
Ok(false) => {} Ok(false) => {}
Err(e) => app.log.error(&format!("{e:#}")), Err(e) => app.log.error(&format!("{e:#}")),
@@ -725,8 +738,9 @@ fn run_with_editor(app: &App, session: &mut ShellSession) -> Result<i32> {
fn run_plain(app: &App, session: &mut ShellSession) -> Result<i32> { fn run_plain(app: &App, session: &mut ShellSession) -> Result<i32> {
let started = Instant::now(); let started = Instant::now();
let mut last: Option<DataTable> = None; let mut last: Option<DataTable> = None;
let mut last_duration: Option<Duration> = None;
loop { loop {
print_status(app, session, started); print_status(app, session, started, last_duration);
eprint!("❯ "); eprint!("❯ ");
std::io::stderr().flush().ok(); std::io::stderr().flush().ok();
let mut line = String::new(); let mut line = String::new();
@@ -737,7 +751,10 @@ fn run_plain(app: &App, session: &mut ShellSession) -> Result<i32> {
if line.is_empty() { if line.is_empty() {
continue; continue;
} }
match handle_input(app, session, &mut last, &line) { let t = Instant::now();
let result = handle_input(app, session, &mut last, &line);
last_duration = Some(t.elapsed());
match result {
Ok(true) => break, Ok(true) => break,
Ok(false) => {} Ok(false) => {}
Err(e) => app.log.error(&format!("{e:#}")), Err(e) => app.log.error(&format!("{e:#}")),