mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-07 08:27:12 +00:00
Use indicatif formatters for human-readable durations, tokens, and bytes
Replace hand-rolled format_duration_human, format_tokens_human, and format_token_count with indicatif's HumanDuration, HumanCount, and HumanBytes. Token counts now display as comma-separated (e.g. "1,234") instead of abbreviated (e.g. "1.2k"), and byte counts show units (e.g. "1.50 KiB"). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d373ffd10f
commit
66d3f62131
9 changed files with 69 additions and 81 deletions
27
Cargo.lock
generated
27
Cargo.lock
generated
|
|
@ -113,6 +113,7 @@ dependencies = [
|
|||
"futures",
|
||||
"glob",
|
||||
"htmd",
|
||||
"indicatif",
|
||||
"jsonschema",
|
||||
"libc",
|
||||
"paste",
|
||||
|
|
@ -327,6 +328,7 @@ dependencies = [
|
|||
"dotenvy",
|
||||
"futures",
|
||||
"git2",
|
||||
"indicatif",
|
||||
"nom",
|
||||
"predicates",
|
||||
"rand 0.8.5",
|
||||
|
|
@ -2020,6 +2022,19 @@ dependencies = [
|
|||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indicatif"
|
||||
version = "0.18.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb"
|
||||
dependencies = [
|
||||
"console 0.16.2",
|
||||
"portable-atomic",
|
||||
"unicode-width",
|
||||
"unit-prefix",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.11.0"
|
||||
|
|
@ -2811,6 +2826,12 @@ version = "0.3.32"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.4"
|
||||
|
|
@ -4683,6 +4704,12 @@ version = "0.2.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "unit-prefix"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ semver = "1"
|
|||
aho-corasick = "1"
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "chrono"] }
|
||||
dirs = "6"
|
||||
indicatif = "0.18"
|
||||
toml = "0.8"
|
||||
jsonwebtoken = "9"
|
||||
daytona-sdk = { git = "https://github.com/brynary/daytona-sdk-rust", package = "daytona-sdk" }
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ tokio-util.workspace = true
|
|||
tracing.workspace = true
|
||||
dirs = "6"
|
||||
glob = "0.3"
|
||||
indicatif.workspace = true
|
||||
shell-escape = "0.1"
|
||||
htmd = "0.5"
|
||||
bollard = { workspace = true, optional = true }
|
||||
|
|
|
|||
|
|
@ -239,15 +239,11 @@ fn print_summary(session: &Session, styles: &Styles) {
|
|||
total_tokens += usage.total_tokens;
|
||||
}
|
||||
}
|
||||
let token_str = if total_tokens >= 1000 {
|
||||
format!("{}k tokens", total_tokens / 1000)
|
||||
} else {
|
||||
format!("{total_tokens} tokens")
|
||||
};
|
||||
eprintln!(
|
||||
"{}",
|
||||
styles.dim.apply_to(format!(
|
||||
"Done ({turn_count} turns, {tool_call_count} tool calls, {token_str})"
|
||||
"Done ({turn_count} turns, {tool_call_count} tool calls, {} tokens)",
|
||||
indicatif::HumanCount(total_tokens as u64),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ base64.workspace = true
|
|||
regex.workspace = true
|
||||
scopeguard = "1"
|
||||
git2.workspace = true
|
||||
indicatif.workspace = true
|
||||
tokio-util.workspace = true
|
||||
tracing.workspace = true
|
||||
[dev-dependencies]
|
||||
|
|
|
|||
|
|
@ -363,18 +363,14 @@ impl CodergenBackend for AgentApiBackend {
|
|||
|
||||
// Print session summary to stderr.
|
||||
if self.verbose {
|
||||
let total_tokens = total_usage.input_tokens + total_usage.output_tokens;
|
||||
let token_str = if total_tokens >= 1000 {
|
||||
format!("{}k tokens", total_tokens / 1000)
|
||||
} else {
|
||||
format!("{total_tokens} tokens")
|
||||
};
|
||||
let total_tokens = (total_usage.input_tokens + total_usage.output_tokens) as u64;
|
||||
let reuse_label = if is_reused { " (reused session)" } else { "" };
|
||||
eprintln!(
|
||||
"{}",
|
||||
self.styles.dim.apply_to(format!(
|
||||
"[{}] Done ({turn_count} turns, {tool_call_count} tool calls, {token_str}{reuse_label})",
|
||||
"[{}] Done ({turn_count} turns, {tool_call_count} tool calls, {} tokens{reuse_label})",
|
||||
node.id,
|
||||
indicatif::HumanCount(total_tokens),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use std::path::Path;
|
|||
|
||||
use arc_util::terminal::Styles;
|
||||
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||
use indicatif::{HumanBytes, HumanCount};
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
|
@ -168,26 +169,6 @@ pub fn print_diagnostics(diagnostics: &[Diagnostic], styles: &Styles) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Format milliseconds into a human-readable duration string.
|
||||
///
|
||||
/// - < 1000ms: `123ms`
|
||||
/// - < 60s: `12.3s`
|
||||
/// - >= 60s: `1m 23s`
|
||||
#[must_use]
|
||||
pub fn format_duration_human(ms: u64) -> String {
|
||||
if ms < 1000 {
|
||||
format!("{ms}ms")
|
||||
} else if ms < 60_000 {
|
||||
let secs = ms as f64 / 1000.0;
|
||||
format!("{secs:.1}s")
|
||||
} else {
|
||||
let total_secs = ms / 1000;
|
||||
let minutes = total_secs / 60;
|
||||
let secs = total_secs % 60;
|
||||
format!("{minutes}m {secs}s")
|
||||
}
|
||||
}
|
||||
|
||||
/// One-line summary of a workflow run event for `-v` output (dimmed).
|
||||
#[must_use]
|
||||
pub fn format_event_summary(event: &WorkflowRunEvent, styles: &Styles) -> String {
|
||||
|
|
@ -254,12 +235,11 @@ pub fn format_event_summary(event: &WorkflowRunEvent, styles: &Styles) -> String
|
|||
));
|
||||
}
|
||||
if let Some(u) = usage {
|
||||
let total = u.input_tokens + u.output_tokens;
|
||||
let tokens_str = format_tokens_human(total);
|
||||
let total = (u.input_tokens + u.output_tokens) as u64;
|
||||
if let Some(cost) = compute_stage_cost(u) {
|
||||
s.push_str(&format!(" tokens={tokens_str} cost={}", format_cost(cost)));
|
||||
s.push_str(&format!(" tokens={} cost={}", HumanCount(total), format_cost(cost)));
|
||||
} else {
|
||||
s.push_str(&format!(" tokens={tokens_str}"));
|
||||
s.push_str(&format!(" tokens={}", HumanCount(total)));
|
||||
}
|
||||
}
|
||||
if let Some(ref f) = failure {
|
||||
|
|
@ -385,14 +365,13 @@ pub fn format_event_summary(event: &WorkflowRunEvent, styles: &Styles) -> String
|
|||
tool_call_count,
|
||||
..
|
||||
} => {
|
||||
let total = usage.input_tokens + usage.output_tokens;
|
||||
let tokens_str = format_tokens_human(total);
|
||||
let mut s = format!("[ASSISTANT_MESSAGE] stage={stage} model={model} tokens={tokens_str} tool_calls={tool_call_count}");
|
||||
let total = (usage.input_tokens + usage.output_tokens) as u64;
|
||||
let mut s = format!("[ASSISTANT_MESSAGE] stage={stage} model={model} tokens={} tool_calls={tool_call_count}", HumanCount(total));
|
||||
if let Some(cache_read) = usage.cache_read_tokens {
|
||||
s.push_str(&format!(" cache_read={}", format_tokens_human(cache_read)));
|
||||
s.push_str(&format!(" cache_read={}", HumanCount(cache_read as u64)));
|
||||
}
|
||||
if let Some(reasoning) = usage.reasoning_tokens {
|
||||
s.push_str(&format!(" reasoning={}", format_tokens_human(reasoning)));
|
||||
s.push_str(&format!(" reasoning={}", HumanCount(reasoning as u64)));
|
||||
}
|
||||
s
|
||||
}
|
||||
|
|
@ -558,7 +537,7 @@ pub fn format_event_summary(event: &WorkflowRunEvent, styles: &Styles) -> String
|
|||
format!("[STALL_WATCHDOG_TIMEOUT] node={node} idle_seconds={idle_seconds}")
|
||||
}
|
||||
WorkflowRunEvent::AssetsCaptured { node_id, files_copied, total_bytes, files_skipped } => {
|
||||
format!("[ASSETS_CAPTURED] node={node_id} files_copied={files_copied} total_bytes={total_bytes} files_skipped={files_skipped}")
|
||||
format!("[ASSETS_CAPTURED] node={node_id} files_copied={files_copied} total_bytes={} files_skipped={files_skipped}", HumanBytes(*total_bytes))
|
||||
}
|
||||
};
|
||||
format!("{}", styles.dim.apply_to(body))
|
||||
|
|
@ -582,15 +561,6 @@ pub fn format_cost(cost: f64) -> String {
|
|||
format!("${cost:.2}")
|
||||
}
|
||||
|
||||
/// Format a token count for human display (e.g. `"15.2k"` or `"850"`).
|
||||
#[must_use]
|
||||
pub fn format_tokens_human(tokens: i64) -> String {
|
||||
if tokens >= 1000 {
|
||||
format!("{:.1}k", tokens as f64 / 1000.0)
|
||||
} else {
|
||||
tokens.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
|
|
|||
|
|
@ -27,9 +27,12 @@ use super::backend::AgentApiBackend;
|
|||
use super::cli_backend::{BackendRouter, AgentCliBackend};
|
||||
use super::run_config;
|
||||
use super::run_config::{RunDefaults, WorkflowRunConfig};
|
||||
use indicatif::{HumanCount, HumanDuration};
|
||||
use std::time::Duration;
|
||||
|
||||
use super::{
|
||||
compute_stage_cost, format_cost, format_duration_human,
|
||||
format_event_summary, format_tokens_human, print_diagnostics, read_dot_file,
|
||||
compute_stage_cost, format_cost,
|
||||
format_event_summary, print_diagnostics, read_dot_file,
|
||||
SandboxProvider, RunArgs,
|
||||
};
|
||||
|
||||
|
|
@ -374,19 +377,19 @@ pub async fn run_command(
|
|||
.. // node_id and other fields
|
||||
} => {
|
||||
let mut line = format!(
|
||||
"Stage \"{name}\" completed ({status}) in {duration}",
|
||||
duration = format_duration_human(*duration_ms),
|
||||
"Stage \"{name}\" completed ({status}) in {}",
|
||||
HumanDuration(Duration::from_millis(*duration_ms)),
|
||||
);
|
||||
if let Some(u) = usage {
|
||||
let total = u.input_tokens + u.output_tokens;
|
||||
let tokens_str = format_tokens_human(total);
|
||||
let total = (u.input_tokens + u.output_tokens) as u64;
|
||||
if let Some(cost) = compute_stage_cost(u) {
|
||||
line.push_str(&format!(
|
||||
" \u{2014} {tokens_str} tokens ({})",
|
||||
" \u{2014} {} tokens ({})",
|
||||
HumanCount(total),
|
||||
format_cost(cost)
|
||||
));
|
||||
} else {
|
||||
line.push_str(&format!(" \u{2014} {tokens_str} tokens"));
|
||||
line.push_str(&format!(" \u{2014} {} tokens", HumanCount(total)));
|
||||
}
|
||||
}
|
||||
eprintln!("{}", styles.dim.apply_to(line));
|
||||
|
|
@ -719,7 +722,7 @@ pub async fn run_command(
|
|||
"Status: {}",
|
||||
status_color.apply_to(&status_str),
|
||||
);
|
||||
eprintln!("Duration: {}", format_duration_human(run_duration_ms));
|
||||
eprintln!("Duration: {}", HumanDuration(Duration::from_millis(run_duration_ms)));
|
||||
|
||||
let acc = accumulator.lock().unwrap();
|
||||
let total_tokens = acc.total_input_tokens + acc.total_output_tokens;
|
||||
|
|
@ -728,18 +731,18 @@ pub async fn run_command(
|
|||
eprintln!(
|
||||
"Cost: {} ({} tokens)",
|
||||
format_cost(acc.total_cost),
|
||||
format_tokens_human(total_tokens)
|
||||
HumanCount(total_tokens as u64)
|
||||
);
|
||||
} else {
|
||||
eprintln!("Tokens: {}", format_tokens_human(total_tokens));
|
||||
eprintln!("Tokens: {}", HumanCount(total_tokens as u64));
|
||||
}
|
||||
if acc.total_cache_read_tokens > 0 {
|
||||
eprintln!(
|
||||
"{}",
|
||||
styles.dim.apply_to(format!(
|
||||
"Cache: {} read, {} write",
|
||||
format_tokens_human(acc.total_cache_read_tokens),
|
||||
format_tokens_human(acc.total_cache_write_tokens),
|
||||
HumanCount(acc.total_cache_read_tokens as u64),
|
||||
HumanCount(acc.total_cache_write_tokens as u64),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
|
@ -748,7 +751,7 @@ pub async fn run_command(
|
|||
"{}",
|
||||
styles.dim.apply_to(format!(
|
||||
"Reasoning: {} tokens",
|
||||
format_tokens_human(acc.total_reasoning_tokens),
|
||||
HumanCount(acc.total_reasoning_tokens as u64),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
|
@ -1045,7 +1048,7 @@ async fn run_from_branch(
|
|||
);
|
||||
eprintln!(
|
||||
"Duration: {}",
|
||||
super::format_duration_human(run_duration_ms)
|
||||
HumanDuration(Duration::from_millis(run_duration_ms))
|
||||
);
|
||||
eprintln!(
|
||||
"{} {}",
|
||||
|
|
|
|||
|
|
@ -88,14 +88,7 @@ fn format_value(val: &serde_json::Value) -> String {
|
|||
}
|
||||
|
||||
fn format_token_count(tokens: i64) -> String {
|
||||
if tokens >= 1000 {
|
||||
let k = tokens as f64 / 1000.0;
|
||||
// One decimal place, strip trailing zero after decimal
|
||||
let formatted = format!("{k:.1}");
|
||||
format!("{formatted}k")
|
||||
} else {
|
||||
tokens.to_string()
|
||||
}
|
||||
indicatif::HumanCount(tokens as u64).to_string()
|
||||
}
|
||||
|
||||
/// Returns the set of context keys that are rendered inline under a stage's
|
||||
|
|
@ -841,7 +834,7 @@ mod tests {
|
|||
"should show model name"
|
||||
);
|
||||
assert!(
|
||||
preamble.contains("1.2k tokens in"),
|
||||
preamble.contains("1,234 tokens in"),
|
||||
"should show token count"
|
||||
);
|
||||
assert!(
|
||||
|
|
@ -1507,7 +1500,7 @@ mod tests {
|
|||
preamble.contains("Model: claude-sonnet-4-20250514"),
|
||||
"should show model"
|
||||
);
|
||||
assert!(preamble.contains("1.5k in"), "should show formatted tokens");
|
||||
assert!(preamble.contains("1,500 in"), "should show formatted tokens");
|
||||
assert!(
|
||||
preamble.contains("Files touched: src/lib.rs"),
|
||||
"should show files"
|
||||
|
|
@ -1588,10 +1581,10 @@ mod tests {
|
|||
fn format_token_count_formatting() {
|
||||
assert_eq!(format_token_count(500), "500");
|
||||
assert_eq!(format_token_count(999), "999");
|
||||
assert_eq!(format_token_count(1000), "1.0k");
|
||||
assert_eq!(format_token_count(1234), "1.2k");
|
||||
assert_eq!(format_token_count(1500), "1.5k");
|
||||
assert_eq!(format_token_count(10000), "10.0k");
|
||||
assert_eq!(format_token_count(1000), "1,000");
|
||||
assert_eq!(format_token_count(1234), "1,234");
|
||||
assert_eq!(format_token_count(1500), "1,500");
|
||||
assert_eq!(format_token_count(10000), "10,000");
|
||||
}
|
||||
|
||||
// --- is_context_key_excluded ---
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue