diff --git a/.beans/claudbg-4g3l--limit-list-output-to-10-by-default-with-limit-flag.md b/.beans/claudbg-4g3l--limit-list-output-to-10-by-default-with-limit-flag.md new file mode 100644 index 0000000..b48e15c --- /dev/null +++ b/.beans/claudbg-4g3l--limit-list-output-to-10-by-default-with-limit-flag.md @@ -0,0 +1,10 @@ +--- +# claudbg-4g3l +title: Limit list output to 10 by default with --limit flag +status: todo +type: feature +created_at: 2026-03-31T00:32:40Z +updated_at: 2026-03-31T00:32:40Z +--- + +Both `sessions list` and `agents list ` should default to showing only the 10 most recent entries (sorted most-recent-first). Add a `--limit=` flag to override: accepts an integer (e.g. `--limit=50`) or the keyword `all` to show everything. diff --git a/src/cli.rs b/src/cli.rs index 5615c95..b4f7ea8 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,4 +1,4 @@ -//! CLI types: output format, include list, global options, and command structure. +//! CLI types: output format, include list, limit, global options, and command structure. /// Output format for all commands. #[derive(Debug, Clone, Default, clap::ValueEnum)] @@ -51,6 +51,46 @@ impl std::str::FromStr for IncludeList { } } +/// How many entries to show in list commands. +/// +/// Accepts an integer (e.g. `10`) or the keyword `all` to show every entry. +#[derive(Debug, Clone)] +pub enum Limit { + /// Show at most N entries. + Count(usize), + /// Show all entries without truncation. + All, +} + +impl Default for Limit { + fn default() -> Self { + Limit::Count(10) + } +} + +impl std::str::FromStr for Limit { + type Err = crate::error::AppError; + + fn from_str(s: &str) -> crate::error::Result { + if s.eq_ignore_ascii_case("all") { + return Ok(Limit::All); + } + s.parse::() + .map(Limit::Count) + .map_err(|_| crate::error::AppError::InvalidArg(format!("invalid limit: {s}"))) + } +} + +impl Limit { + /// Apply this limit to a vector, returning a truncated or full slice. + pub fn apply(&self, items: Vec) -> Vec { + match self { + Limit::All => items, + Limit::Count(n) => items.into_iter().take(*n).collect(), + } + } +} + /// Top-level CLI entry point for claudbg. #[derive(Debug, clap::Parser)] #[command(name = "claudbg", about = "Claude Code session inspector")] @@ -110,7 +150,11 @@ pub enum Commands { #[derive(Debug, clap::Subcommand)] pub enum SessionsCmd { /// List all sessions, most recent first. - List, + List { + /// Maximum number of sessions to show, or "all" for no limit. + #[arg(long, default_value = "10")] + limit: Limit, + }, /// Dump raw messages from a session. Dump { /// Session ID or 8-char prefix. @@ -136,6 +180,9 @@ pub enum AgentsCmd { List { /// Parent session ID or 8-char prefix. session_id: String, + /// Maximum number of agents to show, or "all" for no limit. + #[arg(long, default_value = "10")] + limit: Limit, }, /// Dump raw messages from an agent run. Dump { @@ -219,4 +266,63 @@ mod tests { let fmt = OutputFormat::default(); assert!(matches!(fmt, OutputFormat::Table)); } + + /// Parsing a number produces `Limit::Count`. + #[test] + fn limit_parse_count() { + let limit = Limit::from_str("5").unwrap(); + assert!(matches!(limit, Limit::Count(5))); + } + + /// Parsing `"all"` produces `Limit::All`. + #[test] + fn limit_parse_all() { + let limit = Limit::from_str("all").unwrap(); + assert!(matches!(limit, Limit::All)); + } + + /// Parsing `"ALL"` (case-insensitive) also produces `Limit::All`. + #[test] + fn limit_parse_all_uppercase() { + let limit = Limit::from_str("ALL").unwrap(); + assert!(matches!(limit, Limit::All)); + } + + /// Parsing an invalid string returns an error. + #[test] + fn limit_parse_invalid() { + let result = Limit::from_str("banana"); + assert!(result.is_err()); + } + + /// Default limit is `Count(10)`. + #[test] + fn limit_default_is_ten() { + let limit = Limit::default(); + assert!(matches!(limit, Limit::Count(10))); + } + + /// `Limit::Count(3).apply` truncates a longer vec. + #[test] + fn limit_apply_count_truncates() { + let v = vec![1, 2, 3, 4, 5]; + let result = Limit::Count(3).apply(v); + assert_eq!(result, vec![1, 2, 3]); + } + + /// `Limit::All.apply` returns the full vec unchanged. + #[test] + fn limit_apply_all_returns_all() { + let v = vec![1, 2, 3, 4, 5]; + let result = Limit::All.apply(v); + assert_eq!(result, vec![1, 2, 3, 4, 5]); + } + + /// `Limit::Count(N).apply` on a shorter vec returns the full vec. + #[test] + fn limit_apply_count_shorter_vec_unchanged() { + let v = vec![1, 2]; + let result = Limit::Count(10).apply(v); + assert_eq!(result, vec![1, 2]); + } } diff --git a/src/commands/agents.rs b/src/commands/agents.rs index 4fd8c8b..61fd215 100644 --- a/src/commands/agents.rs +++ b/src/commands/agents.rs @@ -147,7 +147,8 @@ fn find_agent(session_id: &str, agent_id: &str) -> Result Result<()> { +/// The `limit` controls how many rows are returned (default 10, or all). +pub async fn list(session_id: &str, limit: crate::cli::Limit, opts: &crate::cli::GlobalOpts) -> Result<()> { let sessions = crate::parser::discovery::discover_sessions()?; let session_ref = sessions .iter() @@ -185,6 +186,8 @@ pub async fn list(session_id: &str, opts: &crate::cli::GlobalOpts) -> Result<()> }) .collect(); + let rows = limit.apply(rows); + let output = match opts.output { crate::cli::OutputFormat::Table => { crate::output::render_table(&["Agent ID", "Type", "File", "Modified"], &rows)? @@ -491,7 +494,7 @@ mod tests { // SAFETY: single-threaded test context; no other threads read HOME concurrently. unsafe { std::env::set_var("HOME", dir.path()) }; let opts = default_opts(); - let result = list("nonexistent-session-id-xyz", &opts).await; + let result = list("nonexistent-session-id-xyz", crate::cli::Limit::default(), &opts).await; assert!( matches!(result, Err(crate::error::AppError::NotFound(_))), "expected NotFound, got: {result:?}" @@ -547,7 +550,7 @@ mod tests { // SAFETY: single-threaded test context; no other threads read HOME concurrently. unsafe { std::env::set_var("HOME", dir.path()) }; let opts = default_opts(); - let result = list("aaaabbbb", &opts).await; + let result = list("aaaabbbb", crate::cli::Limit::default(), &opts).await; assert!(result.is_ok(), "expected Ok, got: {result:?}"); } diff --git a/src/commands/sessions.rs b/src/commands/sessions.rs index f2d89ed..8537b1a 100644 --- a/src/commands/sessions.rs +++ b/src/commands/sessions.rs @@ -151,7 +151,8 @@ fn render_entry_text(entry: &crate::models::session::RawEntry, opts: &crate::cli /// Discovers all session files, syncs them to the DB, then queries the DB /// for a summary sorted most-recent-first. Respects `opts.output` and /// `opts.verbose` (verbose shows full UUIDs instead of 8-char prefixes). -pub async fn list(opts: &crate::cli::GlobalOpts) -> Result<()> { +/// The `limit` controls how many rows are returned (default 10, or all). +pub async fn list(limit: crate::cli::Limit, opts: &crate::cli::GlobalOpts) -> Result<()> { let db_path = crate::db::connection::default_db_path(); let db = crate::db::connection::open_db(&db_path, false).await?; @@ -231,6 +232,8 @@ pub async fn list(opts: &crate::cli::GlobalOpts) -> Result<()> { ]); } + let rows = limit.apply(rows); + let output = match opts.output { crate::cli::OutputFormat::Table => { crate::output::render_table( @@ -676,7 +679,7 @@ mod tests { // SAFETY: single-threaded test context; no other threads read HOME concurrently. unsafe { std::env::set_var("HOME", dir.path()) }; let opts = default_opts(); - let result = list(&opts).await; + let result = list(crate::cli::Limit::default(), &opts).await; assert!(result.is_ok(), "list failed: {:?}", result.err()); } @@ -692,7 +695,7 @@ mod tests { output: OutputFormat::Json, ..default_opts() }; - let result = list(&opts).await; + let result = list(crate::cli::Limit::default(), &opts).await; assert!(result.is_ok(), "list json failed: {:?}", result.err()); } @@ -708,7 +711,7 @@ mod tests { output: OutputFormat::Xml, ..default_opts() }; - let result = list(&opts).await; + let result = list(crate::cli::Limit::default(), &opts).await; assert!(result.is_ok(), "list xml failed: {:?}", result.err()); } diff --git a/src/main.rs b/src/main.rs index 1fcb697..b8082f2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,8 +8,8 @@ use claudbg::error::Result; async fn main() -> Result<()> { let cli = Cli::parse(); match cli.command { - Commands::Sessions { cmd } => match cmd.unwrap_or(SessionsCmd::List) { - SessionsCmd::List => claudbg::commands::sessions::list(&cli.global).await?, + Commands::Sessions { cmd } => match cmd.unwrap_or(SessionsCmd::List { limit: Default::default() }) { + SessionsCmd::List { limit } => claudbg::commands::sessions::list(limit, &cli.global).await?, SessionsCmd::Dump { id, follow } => { claudbg::commands::sessions::dump(&id, follow, &cli.global).await? } @@ -18,8 +18,8 @@ async fn main() -> Result<()> { } }, Commands::Agents { cmd } => match cmd { - AgentsCmd::List { session_id } => { - claudbg::commands::agents::list(&session_id, &cli.global).await? + AgentsCmd::List { session_id, limit } => { + claudbg::commands::agents::list(&session_id, limit, &cli.global).await? } AgentsCmd::Dump { session_id,