feat(claudbg-4g3l): add --limit flag to list commands, default 10

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
Elijah Voigt 2 months ago
parent 970ed0895f
commit 9c9f61efd1

@ -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 <session-id>` should default to showing only the 10 most recent entries (sorted most-recent-first). Add a `--limit=<N|all>` flag to override: accepts an integer (e.g. `--limit=50`) or the keyword `all` to show everything.

@ -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. /// Output format for all commands.
#[derive(Debug, Clone, Default, clap::ValueEnum)] #[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<Self> {
if s.eq_ignore_ascii_case("all") {
return Ok(Limit::All);
}
s.parse::<usize>()
.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<T>(&self, items: Vec<T>) -> Vec<T> {
match self {
Limit::All => items,
Limit::Count(n) => items.into_iter().take(*n).collect(),
}
}
}
/// Top-level CLI entry point for claudbg. /// Top-level CLI entry point for claudbg.
#[derive(Debug, clap::Parser)] #[derive(Debug, clap::Parser)]
#[command(name = "claudbg", about = "Claude Code session inspector")] #[command(name = "claudbg", about = "Claude Code session inspector")]
@ -110,7 +150,11 @@ pub enum Commands {
#[derive(Debug, clap::Subcommand)] #[derive(Debug, clap::Subcommand)]
pub enum SessionsCmd { pub enum SessionsCmd {
/// List all sessions, most recent first. /// 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 raw messages from a session.
Dump { Dump {
/// Session ID or 8-char prefix. /// Session ID or 8-char prefix.
@ -136,6 +180,9 @@ pub enum AgentsCmd {
List { List {
/// Parent session ID or 8-char prefix. /// Parent session ID or 8-char prefix.
session_id: String, 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 raw messages from an agent run.
Dump { Dump {
@ -219,4 +266,63 @@ mod tests {
let fmt = OutputFormat::default(); let fmt = OutputFormat::default();
assert!(matches!(fmt, OutputFormat::Table)); 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]);
}
} }

@ -147,7 +147,8 @@ fn find_agent(session_id: &str, agent_id: &str) -> Result<crate::parser::discove
/// ///
/// Respects `opts.output` and `opts.verbose` (verbose shows full UUIDs and /// Respects `opts.output` and `opts.verbose` (verbose shows full UUIDs and
/// full file paths instead of 8-char prefixes and basenames). /// full file paths instead of 8-char prefixes and basenames).
pub async fn list(session_id: &str, opts: &crate::cli::GlobalOpts) -> 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 sessions = crate::parser::discovery::discover_sessions()?;
let session_ref = sessions let session_ref = sessions
.iter() .iter()
@ -185,6 +186,8 @@ pub async fn list(session_id: &str, opts: &crate::cli::GlobalOpts) -> Result<()>
}) })
.collect(); .collect();
let rows = limit.apply(rows);
let output = match opts.output { let output = match opts.output {
crate::cli::OutputFormat::Table => { crate::cli::OutputFormat::Table => {
crate::output::render_table(&["Agent ID", "Type", "File", "Modified"], &rows)? 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. // SAFETY: single-threaded test context; no other threads read HOME concurrently.
unsafe { std::env::set_var("HOME", dir.path()) }; unsafe { std::env::set_var("HOME", dir.path()) };
let opts = default_opts(); 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!( assert!(
matches!(result, Err(crate::error::AppError::NotFound(_))), matches!(result, Err(crate::error::AppError::NotFound(_))),
"expected NotFound, got: {result:?}" "expected NotFound, got: {result:?}"
@ -547,7 +550,7 @@ mod tests {
// SAFETY: single-threaded test context; no other threads read HOME concurrently. // SAFETY: single-threaded test context; no other threads read HOME concurrently.
unsafe { std::env::set_var("HOME", dir.path()) }; unsafe { std::env::set_var("HOME", dir.path()) };
let opts = default_opts(); 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:?}"); assert!(result.is_ok(), "expected Ok, got: {result:?}");
} }

@ -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 /// Discovers all session files, syncs them to the DB, then queries the DB
/// for a summary sorted most-recent-first. Respects `opts.output` and /// for a summary sorted most-recent-first. Respects `opts.output` and
/// `opts.verbose` (verbose shows full UUIDs instead of 8-char prefixes). /// `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_path = crate::db::connection::default_db_path();
let db = crate::db::connection::open_db(&db_path, false).await?; 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 { let output = match opts.output {
crate::cli::OutputFormat::Table => { crate::cli::OutputFormat::Table => {
crate::output::render_table( crate::output::render_table(
@ -676,7 +679,7 @@ mod tests {
// SAFETY: single-threaded test context; no other threads read HOME concurrently. // SAFETY: single-threaded test context; no other threads read HOME concurrently.
unsafe { std::env::set_var("HOME", dir.path()) }; unsafe { std::env::set_var("HOME", dir.path()) };
let opts = default_opts(); 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()); assert!(result.is_ok(), "list failed: {:?}", result.err());
} }
@ -692,7 +695,7 @@ mod tests {
output: OutputFormat::Json, output: OutputFormat::Json,
..default_opts() ..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()); assert!(result.is_ok(), "list json failed: {:?}", result.err());
} }
@ -708,7 +711,7 @@ mod tests {
output: OutputFormat::Xml, output: OutputFormat::Xml,
..default_opts() ..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()); assert!(result.is_ok(), "list xml failed: {:?}", result.err());
} }

@ -8,8 +8,8 @@ use claudbg::error::Result;
async fn main() -> Result<()> { async fn main() -> Result<()> {
let cli = Cli::parse(); let cli = Cli::parse();
match cli.command { match cli.command {
Commands::Sessions { cmd } => match cmd.unwrap_or(SessionsCmd::List) { Commands::Sessions { cmd } => match cmd.unwrap_or(SessionsCmd::List { limit: Default::default() }) {
SessionsCmd::List => claudbg::commands::sessions::list(&cli.global).await?, SessionsCmd::List { limit } => claudbg::commands::sessions::list(limit, &cli.global).await?,
SessionsCmd::Dump { id, follow } => { SessionsCmd::Dump { id, follow } => {
claudbg::commands::sessions::dump(&id, follow, &cli.global).await? claudbg::commands::sessions::dump(&id, follow, &cli.global).await?
} }
@ -18,8 +18,8 @@ async fn main() -> Result<()> {
} }
}, },
Commands::Agents { cmd } => match cmd { Commands::Agents { cmd } => match cmd {
AgentsCmd::List { session_id } => { AgentsCmd::List { session_id, limit } => {
claudbg::commands::agents::list(&session_id, &cli.global).await? claudbg::commands::agents::list(&session_id, limit, &cli.global).await?
} }
AgentsCmd::Dump { AgentsCmd::Dump {
session_id, session_id,

Loading…
Cancel
Save