From b7c54dd4639d7a4b0e3cc500d811f116328e8220 Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Tue, 31 Mar 2026 22:46:02 -0700 Subject: [PATCH] fix(claudbg-u28r): use blue/white search highlight for light mode legibility Also close claudbg-4g3l, claudbg-ltt0, claudbg-37cj as already implemented. Fixes pre-existing fmt issues in agents.rs, sessions.rs, filter.rs, and TUI files. Co-Authored-By: Claude Sonnet 4.6 --- ...r-global-flag-and-no-color-env-var-supp.md | 9 +- ...output-to-10-by-default-with-limit-flag.md | 9 +- ...o-sub-command-defaults-to-sessions-list.md | 9 +- ...highlight-text-unreadable-in-light-mode.md | 9 +- src/commands/agents.rs | 14 +- src/commands/sessions.rs | 25 +- src/filter.rs | 287 +++++++++++++++--- src/tui/modals/quit_dialog.rs | 4 +- src/tui/run.rs | 4 +- src/tui/screens/session_list.rs | 68 +++-- src/tui/screens/transcript.rs | 100 +++--- 11 files changed, 403 insertions(+), 135 deletions(-) diff --git a/.beans/claudbg-37cj--add-no-color-global-flag-and-no-color-env-var-supp.md b/.beans/claudbg-37cj--add-no-color-global-flag-and-no-color-env-var-supp.md index 0024637..09eb59b 100644 --- a/.beans/claudbg-37cj--add-no-color-global-flag-and-no-color-env-var-supp.md +++ b/.beans/claudbg-37cj--add-no-color-global-flag-and-no-color-env-var-supp.md @@ -1,11 +1,16 @@ --- # claudbg-37cj title: Add --[no-]color global flag and NO_COLOR env var support -status: todo +status: completed type: task +priority: normal created_at: 2026-03-31T00:32:57Z -updated_at: 2026-03-31T00:32:57Z +updated_at: 2026-04-01T05:44:59Z parent: claudbg-qpfe --- Add `--color` / `--no-color` as a global CLI flag (available on all commands). When not specified, auto-detect: enable color if stdout is a tty and NO_COLOR is unset/empty. Honor the NO_COLOR environment variable (any non-empty value → disable color), per the NO_COLOR spec. This flag controls all ANSI output in the CLI (transcripts etc). + +## Summary of Changes + +Already implemented prior to this bean being created. `GlobalOpts` in `src/cli.rs` has `--color`/`--no-color` flags and `color_enabled()` respects `NO_COLOR` env var per spec. 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 index b48e15c..6f8942c 100644 --- 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 @@ -1,10 +1,15 @@ --- # claudbg-4g3l title: Limit list output to 10 by default with --limit flag -status: todo +status: completed type: feature +priority: normal created_at: 2026-03-31T00:32:40Z -updated_at: 2026-03-31T00:32:40Z +updated_at: 2026-04-01T05:44:56Z --- 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. + +## Summary of Changes + +Already implemented prior to this bean being created. The `Limit` enum with `Count(10)` default is in `src/cli.rs`, applied via `--limit` flag on both `sessions list` and `agents list`. diff --git a/.beans/claudbg-ltt0--sessions-no-sub-command-defaults-to-sessions-list.md b/.beans/claudbg-ltt0--sessions-no-sub-command-defaults-to-sessions-list.md index c783240..0bbefda 100644 --- a/.beans/claudbg-ltt0--sessions-no-sub-command-defaults-to-sessions-list.md +++ b/.beans/claudbg-ltt0--sessions-no-sub-command-defaults-to-sessions-list.md @@ -1,10 +1,15 @@ --- # claudbg-ltt0 title: 'sessions: no sub-command defaults to sessions list' -status: todo +status: completed type: feature +priority: normal created_at: 2026-03-31T00:32:35Z -updated_at: 2026-03-31T00:32:35Z +updated_at: 2026-04-01T05:44:58Z --- Running `claudbg sessions` with no sub-command should behave identically to `claudbg sessions list`. Note: `claudbg agents` does NOT get this treatment since agents list requires a session ID. + +## Summary of Changes + +Already implemented prior to this bean being created. `src/main.rs` uses `cmd.unwrap_or(SessionsCmd::List { limit: Default::default(), filter: vec![] })` so `claudbg sessions` defaults to `sessions list`. diff --git a/.beans/claudbg-u28r--tui-search-highlight-text-unreadable-in-light-mode.md b/.beans/claudbg-u28r--tui-search-highlight-text-unreadable-in-light-mode.md index 246552e..2e69470 100644 --- a/.beans/claudbg-u28r--tui-search-highlight-text-unreadable-in-light-mode.md +++ b/.beans/claudbg-u28r--tui-search-highlight-text-unreadable-in-light-mode.md @@ -1,10 +1,15 @@ --- # claudbg-u28r title: 'TUI: search highlight text unreadable in light mode' -status: todo +status: completed type: bug +priority: normal created_at: 2026-03-31T23:45:15Z -updated_at: 2026-03-31T23:45:15Z +updated_at: 2026-04-01T05:45:54Z --- In light mode, the selected/highlighted search match text is too dark to read against the highlight background. The search highlight colors need to be adjusted to be legible in both light and dark mode. + +## Summary of Changes + +Changed `SEARCH_HIGHLIGHT` style in `src/tui/screens/transcript.rs` from `bg(Color::Yellow).fg(Color::Black)` to `bg(Color::Blue).fg(Color::White)`. Blue/white is universally readable in both light and dark terminal themes. diff --git a/src/commands/agents.rs b/src/commands/agents.rs index 2827389..e8ea396 100644 --- a/src/commands/agents.rs +++ b/src/commands/agents.rs @@ -82,7 +82,11 @@ fn render_entry_text(entry: &crate::models::session::RawEntry, opts: &crate::cli let cap = if opts.verbose { usize::MAX } else { 120 }; let boundary = input_preview.floor_char_boundary(cap); let input_short = &input_preview[..boundary]; - let ellipsis = if !opts.verbose && input_preview.len() > 120 { "…" } else { "" }; + let ellipsis = if !opts.verbose && input_preview.len() > 120 { + "…" + } else { + "" + }; let label = crate::output::color::blue(&format!("[tool: {name}]"), color); println!("{label} {input_short}{ellipsis}"); } @@ -565,7 +569,13 @@ 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", crate::cli::Limit::default(), vec![], &opts).await; + let result = list( + "nonexistent-session-id-xyz", + crate::cli::Limit::default(), + vec![], + &opts, + ) + .await; assert!( matches!(result, Err(crate::error::AppError::NotFound(_))), "expected NotFound, got: {result:?}" diff --git a/src/commands/sessions.rs b/src/commands/sessions.rs index ff6a8db..be995c8 100644 --- a/src/commands/sessions.rs +++ b/src/commands/sessions.rs @@ -121,7 +121,11 @@ fn render_entry_text(entry: &crate::models::session::RawEntry, opts: &crate::cli let cap = if opts.verbose { usize::MAX } else { 120 }; let boundary = input_preview.floor_char_boundary(cap); let input_short = &input_preview[..boundary]; - let ellipsis = if !opts.verbose && input_preview.len() > 120 { "…" } else { "" }; + let ellipsis = if !opts.verbose && input_preview.len() > 120 { + "…" + } else { + "" + }; let label = crate::output::color::blue(&format!("[tool: {name}]"), color); println!("{label} {input_short}{ellipsis}"); } @@ -328,12 +332,10 @@ pub async fn list( let rows = limit.apply(rows); let output = match opts.output { - crate::cli::OutputFormat::Table => { - crate::output::render_table( - &["ID", "Date", "Project", "Model", "Messages", "Sub-agents"], - &rows, - )? - } + crate::cli::OutputFormat::Table => crate::output::render_table( + &["ID", "Date", "Project", "Model", "Messages", "Sub-agents"], + &rows, + )?, crate::cli::OutputFormat::Json => { let objects: Vec = rows .iter() @@ -351,7 +353,14 @@ pub async fn list( crate::output::render_json(&objects)? } crate::cli::OutputFormat::Xml => crate::output::render_xml_rows( - &["session_id", "date", "project", "model", "messages", "subagents"], + &[ + "session_id", + "date", + "project", + "model", + "messages", + "subagents", + ], &rows, )?, }; diff --git a/src/filter.rs b/src/filter.rs index cea6cad..2ad9015 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -84,7 +84,9 @@ impl SessionRow for crate::tui::state::SessionListItem { } fn date(&self) -> Option { // `date` field is e.g. "2026-03-30 14:22:01"; parse just the date portion. - self.date.get(..10).and_then(|s| NaiveDate::parse_from_str(s, "%Y-%m-%d").ok()) + self.date + .get(..10) + .and_then(|s| NaiveDate::parse_from_str(s, "%Y-%m-%d").ok()) } } @@ -217,9 +219,7 @@ fn parse_atom(atom: &str) -> Result { })?; // For string keys (model/project/id) only `:` is allowed. - if matches!(key, Key::Model | Key::Project | Key::Id) - && !matches!(op, Op::Colon) - { + if matches!(key, Key::Model | Key::Project | Key::Id) && !matches!(op, Op::Colon) { return Err(AppError::Parse(format!( "key '{key_str}' only supports ':' operator" ))); @@ -622,29 +622,64 @@ mod tests { #[test] fn match_model_substring() { let f = Filter::parse("model:haiku").unwrap(); - let row = make_row("claude-haiku-4-5", "/proj", "aaaaaaaa-0000-0000-0000-000000000000", 5, 0, "2026-03-30 10:00:00"); + let row = make_row( + "claude-haiku-4-5", + "/proj", + "aaaaaaaa-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-30 10:00:00", + ); assert!(f.matches(&row)); } #[test] fn match_model_substring_case_insensitive() { let f = Filter::parse("model:HAIKU").unwrap(); - let row = make_row("claude-haiku-4-5", "/proj", "aaaaaaaa-0000-0000-0000-000000000000", 5, 0, "2026-03-30 10:00:00"); + let row = make_row( + "claude-haiku-4-5", + "/proj", + "aaaaaaaa-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-30 10:00:00", + ); assert!(f.matches(&row)); } #[test] fn no_match_model_substring() { let f = Filter::parse("model:sonnet").unwrap(); - let row = make_row("claude-haiku-4-5", "/proj", "aaaaaaaa-0000-0000-0000-000000000000", 5, 0, "2026-03-30 10:00:00"); + let row = make_row( + "claude-haiku-4-5", + "/proj", + "aaaaaaaa-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-30 10:00:00", + ); assert!(!f.matches(&row)); } #[test] fn match_model_wildcard() { let f = Filter::parse("model:*").unwrap(); - let row_with_model = make_row("claude-haiku-4-5", "/proj", "aaaaaaaa-0000-0000-0000-000000000000", 5, 0, "2026-03-30 10:00:00"); - let row_empty_model = make_row("", "/proj", "bbbbbbbb-0000-0000-0000-000000000000", 5, 0, "2026-03-30 10:00:00"); + let row_with_model = make_row( + "claude-haiku-4-5", + "/proj", + "aaaaaaaa-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-30 10:00:00", + ); + let row_empty_model = make_row( + "", + "/proj", + "bbbbbbbb-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-30 10:00:00", + ); assert!(f.matches(&row_with_model)); assert!(!f.matches(&row_empty_model)); } @@ -652,14 +687,28 @@ mod tests { #[test] fn match_project_substring() { let f = Filter::parse("project:my-org").unwrap(); - let row = make_row("claude-sonnet-4-6", "/home/pop/my-org/repo", "aaaaaaaa-0000-0000-0000-000000000000", 5, 0, "2026-03-30 10:00:00"); + let row = make_row( + "claude-sonnet-4-6", + "/home/pop/my-org/repo", + "aaaaaaaa-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-30 10:00:00", + ); assert!(f.matches(&row)); } #[test] fn match_id_substring() { let f = Filter::parse("id:aaaaaaaa").unwrap(); - let row = make_row("claude-sonnet-4-6", "/proj", "aaaaaaaa-0000-0000-0000-000000000000", 5, 0, "2026-03-30 10:00:00"); + let row = make_row( + "claude-sonnet-4-6", + "/proj", + "aaaaaaaa-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-30 10:00:00", + ); assert!(f.matches(&row)); } @@ -668,8 +717,22 @@ mod tests { #[test] fn match_agents_gt() { let f = Filter::parse("agents>0").unwrap(); - let with_agents = make_row("model", "/proj", "aaaaaaaa-0000-0000-0000-000000000000", 5, 2, "2026-03-30 10:00:00"); - let no_agents = make_row("model", "/proj", "bbbbbbbb-0000-0000-0000-000000000000", 5, 0, "2026-03-30 10:00:00"); + let with_agents = make_row( + "model", + "/proj", + "aaaaaaaa-0000-0000-0000-000000000000", + 5, + 2, + "2026-03-30 10:00:00", + ); + let no_agents = make_row( + "model", + "/proj", + "bbbbbbbb-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-30 10:00:00", + ); assert!(f.matches(&with_agents)); assert!(!f.matches(&no_agents)); } @@ -677,8 +740,22 @@ mod tests { #[test] fn match_messages_lt() { let f = Filter::parse("messages<10").unwrap(); - let few = make_row("model", "/proj", "aaaaaaaa-0000-0000-0000-000000000000", 5, 0, "2026-03-30 10:00:00"); - let many = make_row("model", "/proj", "bbbbbbbb-0000-0000-0000-000000000000", 20, 0, "2026-03-30 10:00:00"); + let few = make_row( + "model", + "/proj", + "aaaaaaaa-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-30 10:00:00", + ); + let many = make_row( + "model", + "/proj", + "bbbbbbbb-0000-0000-0000-000000000000", + 20, + 0, + "2026-03-30 10:00:00", + ); assert!(f.matches(&few)); assert!(!f.matches(&many)); } @@ -686,8 +763,22 @@ mod tests { #[test] fn match_messages_colon_equality() { let f = Filter::parse("messages:5").unwrap(); - let five = make_row("model", "/proj", "aaaaaaaa-0000-0000-0000-000000000000", 5, 0, "2026-03-30 10:00:00"); - let six = make_row("model", "/proj", "bbbbbbbb-0000-0000-0000-000000000000", 6, 0, "2026-03-30 10:00:00"); + let five = make_row( + "model", + "/proj", + "aaaaaaaa-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-30 10:00:00", + ); + let six = make_row( + "model", + "/proj", + "bbbbbbbb-0000-0000-0000-000000000000", + 6, + 0, + "2026-03-30 10:00:00", + ); assert!(f.matches(&five)); assert!(!f.matches(&six)); } @@ -697,8 +788,22 @@ mod tests { #[test] fn match_date_gt() { let f = Filter::parse("date>2026-03-15").unwrap(); - let after = make_row("model", "/proj", "aaaaaaaa-0000-0000-0000-000000000000", 5, 0, "2026-03-20 10:00:00"); - let before = make_row("model", "/proj", "bbbbbbbb-0000-0000-0000-000000000000", 5, 0, "2026-03-10 10:00:00"); + let after = make_row( + "model", + "/proj", + "aaaaaaaa-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-20 10:00:00", + ); + let before = make_row( + "model", + "/proj", + "bbbbbbbb-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-10 10:00:00", + ); assert!(f.matches(&after)); assert!(!f.matches(&before)); } @@ -706,8 +811,22 @@ mod tests { #[test] fn match_date_lt() { let f = Filter::parse("date<2026-03-20").unwrap(); - let before = make_row("model", "/proj", "aaaaaaaa-0000-0000-0000-000000000000", 5, 0, "2026-03-15 10:00:00"); - let after = make_row("model", "/proj", "bbbbbbbb-0000-0000-0000-000000000000", 5, 0, "2026-03-25 10:00:00"); + let before = make_row( + "model", + "/proj", + "aaaaaaaa-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-15 10:00:00", + ); + let after = make_row( + "model", + "/proj", + "bbbbbbbb-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-25 10:00:00", + ); assert!(f.matches(&before)); assert!(!f.matches(&after)); } @@ -715,8 +834,22 @@ mod tests { #[test] fn match_date_colon_equality() { let f = Filter::parse("date:2026-03-20").unwrap(); - let exact = make_row("model", "/proj", "aaaaaaaa-0000-0000-0000-000000000000", 5, 0, "2026-03-20 10:00:00"); - let other = make_row("model", "/proj", "bbbbbbbb-0000-0000-0000-000000000000", 5, 0, "2026-03-21 10:00:00"); + let exact = make_row( + "model", + "/proj", + "aaaaaaaa-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-20 10:00:00", + ); + let other = make_row( + "model", + "/proj", + "bbbbbbbb-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-21 10:00:00", + ); assert!(f.matches(&exact)); assert!(!f.matches(&other)); } @@ -724,8 +857,22 @@ mod tests { #[test] fn match_date_range_and() { let f = Filter::parse("date>2026-03-15 AND date<2026-03-20").unwrap(); - let inside = make_row("model", "/proj", "aaaaaaaa-0000-0000-0000-000000000000", 5, 0, "2026-03-17 10:00:00"); - let outside = make_row("model", "/proj", "bbbbbbbb-0000-0000-0000-000000000000", 5, 0, "2026-03-22 10:00:00"); + let inside = make_row( + "model", + "/proj", + "aaaaaaaa-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-17 10:00:00", + ); + let outside = make_row( + "model", + "/proj", + "bbbbbbbb-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-22 10:00:00", + ); assert!(f.matches(&inside)); assert!(!f.matches(&outside)); } @@ -735,9 +882,30 @@ mod tests { #[test] fn match_and_both_must_match() { let f = Filter::parse("model:haiku AND agents>0").unwrap(); - let haiku_agents = make_row("claude-haiku-4-5", "/proj", "aaaaaaaa-0000-0000-0000-000000000000", 5, 2, "2026-03-30 10:00:00"); - let haiku_no_agents = make_row("claude-haiku-4-5", "/proj", "bbbbbbbb-0000-0000-0000-000000000000", 5, 0, "2026-03-30 10:00:00"); - let sonnet_agents = make_row("claude-sonnet-4-6", "/proj", "cccccccc-0000-0000-0000-000000000000", 5, 2, "2026-03-30 10:00:00"); + let haiku_agents = make_row( + "claude-haiku-4-5", + "/proj", + "aaaaaaaa-0000-0000-0000-000000000000", + 5, + 2, + "2026-03-30 10:00:00", + ); + let haiku_no_agents = make_row( + "claude-haiku-4-5", + "/proj", + "bbbbbbbb-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-30 10:00:00", + ); + let sonnet_agents = make_row( + "claude-sonnet-4-6", + "/proj", + "cccccccc-0000-0000-0000-000000000000", + 5, + 2, + "2026-03-30 10:00:00", + ); assert!(f.matches(&haiku_agents)); assert!(!f.matches(&haiku_no_agents)); assert!(!f.matches(&sonnet_agents)); @@ -746,9 +914,30 @@ mod tests { #[test] fn match_or_either_matches() { let f = Filter::parse("model:haiku OR model:sonnet").unwrap(); - let haiku = make_row("claude-haiku-4-5", "/proj", "aaaaaaaa-0000-0000-0000-000000000000", 5, 0, "2026-03-30 10:00:00"); - let sonnet = make_row("claude-sonnet-4-6", "/proj", "bbbbbbbb-0000-0000-0000-000000000000", 5, 0, "2026-03-30 10:00:00"); - let opus = make_row("claude-opus-4-5", "/proj", "cccccccc-0000-0000-0000-000000000000", 5, 0, "2026-03-30 10:00:00"); + let haiku = make_row( + "claude-haiku-4-5", + "/proj", + "aaaaaaaa-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-30 10:00:00", + ); + let sonnet = make_row( + "claude-sonnet-4-6", + "/proj", + "bbbbbbbb-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-30 10:00:00", + ); + let opus = make_row( + "claude-opus-4-5", + "/proj", + "cccccccc-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-30 10:00:00", + ); assert!(f.matches(&haiku)); assert!(f.matches(&sonnet)); assert!(!f.matches(&opus)); @@ -759,11 +948,32 @@ mod tests { // "A OR B AND C" should parse as "A OR (B AND C)" — AND binds tighter. let f = Filter::parse("model:opus OR model:haiku AND agents>0").unwrap(); // matches opus (left of OR) regardless of agents - let opus_no_agents = make_row("claude-opus-4-5", "/proj", "aaaaaaaa-0000-0000-0000-000000000000", 5, 0, "2026-03-30 10:00:00"); + let opus_no_agents = make_row( + "claude-opus-4-5", + "/proj", + "aaaaaaaa-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-30 10:00:00", + ); // haiku without agents — does NOT match right side (haiku AND agents>0) - let haiku_no_agents = make_row("claude-haiku-4-5", "/proj", "bbbbbbbb-0000-0000-0000-000000000000", 5, 0, "2026-03-30 10:00:00"); + let haiku_no_agents = make_row( + "claude-haiku-4-5", + "/proj", + "bbbbbbbb-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-30 10:00:00", + ); // haiku with agents — matches right side - let haiku_agents = make_row("claude-haiku-4-5", "/proj", "cccccccc-0000-0000-0000-000000000000", 5, 3, "2026-03-30 10:00:00"); + let haiku_agents = make_row( + "claude-haiku-4-5", + "/proj", + "cccccccc-0000-0000-0000-000000000000", + 5, + 3, + "2026-03-30 10:00:00", + ); assert!(f.matches(&opus_no_agents)); assert!(!f.matches(&haiku_no_agents)); assert!(f.matches(&haiku_agents)); @@ -775,7 +985,14 @@ mod tests { fn tokens_filter_returns_false_for_session_list_item() { // SessionListItem does not track token count; tokens filter always false. let f = Filter::parse("tokens>100").unwrap(); - let row = make_row("model", "/proj", "aaaaaaaa-0000-0000-0000-000000000000", 5, 0, "2026-03-30 10:00:00"); + let row = make_row( + "model", + "/proj", + "aaaaaaaa-0000-0000-0000-000000000000", + 5, + 0, + "2026-03-30 10:00:00", + ); assert!(!f.matches(&row)); } } diff --git a/src/tui/modals/quit_dialog.rs b/src/tui/modals/quit_dialog.rs index 96c1668..89ec95d 100644 --- a/src/tui/modals/quit_dialog.rs +++ b/src/tui/modals/quit_dialog.rs @@ -41,9 +41,7 @@ pub fn render_quit_dialog(f: &mut Frame, area: Rect) { // Clear the background behind the dialog. f.render_widget(Clear, dialog_area); - let block = Block::default() - .title(" Quit? ") - .borders(Borders::ALL); + let block = Block::default().title(" Quit? ").borders(Borders::ALL); let paragraph = Paragraph::new(" q = yes Esc = no ") .block(block) diff --git a/src/tui/run.rs b/src/tui/run.rs index 1be5ac5..e16b978 100644 --- a/src/tui/run.rs +++ b/src/tui/run.rs @@ -12,13 +12,13 @@ use std::io::{self, Stdout}; use std::time::Duration; +use ratatui::Terminal; +use ratatui::backend::CrosstermBackend; use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind}; use ratatui::crossterm::execute; use ratatui::crossterm::terminal::{ EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode, }; -use ratatui::Terminal; -use ratatui::backend::CrosstermBackend; use crate::error::Result; use crate::parser::discovery::{discover_agents_for_session, discover_sessions}; diff --git a/src/tui/screens/session_list.rs b/src/tui/screens/session_list.rs index daf3522..ea328d0 100644 --- a/src/tui/screens/session_list.rs +++ b/src/tui/screens/session_list.rs @@ -26,7 +26,11 @@ fn truncate_project(path: &str, max_chars: usize) -> String { path.to_string() } else { // Keep the last (max_chars - 1) chars and prepend the ellipsis. - let keep: String = path.chars().rev().take(max_chars - 1).collect::>() + let keep: String = path + .chars() + .rev() + .take(max_chars - 1) + .collect::>() .into_iter() .rev() .collect(); @@ -58,7 +62,7 @@ pub fn render_session_list(f: &mut Frame, area: Rect, state: &AppState) { let widths = [ Constraint::Length(8), Constraint::Length(20), - Constraint::Min(10), // project — gets remaining space + Constraint::Min(10), // project — gets remaining space Constraint::Length(20), Constraint::Length(6), Constraint::Length(7), @@ -72,7 +76,7 @@ pub fn render_session_list(f: &mut Frame, area: Rect, state: &AppState) { // Use 30 chars as a safe default; the Min constraint will expand it. let project_max: usize = table_area .width - .saturating_sub(68) // subtract fixed columns + separators + borders + .saturating_sub(68) // subtract fixed columns + separators + borders .max(10) as usize; let project_display_max = project_max + 30; // generous — actual render clips @@ -110,9 +114,7 @@ pub fn render_session_list(f: &mut Frame, area: Rect, state: &AppState) { format!(" Sessions [filter: {}] ", state.filter_active) }; - let block = Block::default() - .title(table_title) - .borders(Borders::ALL); + let block = Block::default().title(table_title).borders(Borders::ALL); let highlight_style = Style::default().add_modifier(Modifier::REVERSED); @@ -242,8 +244,7 @@ pub fn handle_session_list_event(event: Event, state: &mut AppState) -> bool { KeyCode::Down | KeyCode::Char('j') => { let visible = filtered_session_indices(state); if !visible.is_empty() { - state.list_selected = - (state.list_selected + 1).min(visible.len() - 1); + state.list_selected = (state.list_selected + 1).min(visible.len() - 1); } true } @@ -303,7 +304,11 @@ fn handle_filter_input_event(code: KeyCode, state: &mut AppState) -> bool { let query = state.filter_input.trim().to_string(); // Record in history (skip duplicate consecutive entries). if !query.is_empty() { - let is_dup = state.filter_history.last().map(|l| l == &query).unwrap_or(false); + let is_dup = state + .filter_history + .last() + .map(|l| l == &query) + .unwrap_or(false); if !is_dup { state.filter_history.push(query.clone()); AppState::append_history_to_disk(&query); @@ -428,8 +433,14 @@ mod tests { #[test] fn down_increments_selection() { let mut state = AppState::new(); - state.sessions.push(make_item("aaaaaaaa", "aaaaaaaa-0000-0000-0000-000000000000")); - state.sessions.push(make_item("bbbbbbbb", "bbbbbbbb-0000-0000-0000-000000000000")); + state.sessions.push(make_item( + "aaaaaaaa", + "aaaaaaaa-0000-0000-0000-000000000000", + )); + state.sessions.push(make_item( + "bbbbbbbb", + "bbbbbbbb-0000-0000-0000-000000000000", + )); state.list_selected = 0; let consumed = handle_session_list_event(press(KeyCode::Down), &mut state); assert!(consumed); @@ -439,7 +450,10 @@ mod tests { #[test] fn down_clamps_at_end() { let mut state = AppState::new(); - state.sessions.push(make_item("aaaaaaaa", "aaaaaaaa-0000-0000-0000-000000000000")); + state.sessions.push(make_item( + "aaaaaaaa", + "aaaaaaaa-0000-0000-0000-000000000000", + )); state.list_selected = 0; handle_session_list_event(press(KeyCode::Down), &mut state); // Should remain at 0 (only one item). @@ -449,8 +463,14 @@ mod tests { #[test] fn up_decrements_selection() { let mut state = AppState::new(); - state.sessions.push(make_item("aaaaaaaa", "aaaaaaaa-0000-0000-0000-000000000000")); - state.sessions.push(make_item("bbbbbbbb", "bbbbbbbb-0000-0000-0000-000000000000")); + state.sessions.push(make_item( + "aaaaaaaa", + "aaaaaaaa-0000-0000-0000-000000000000", + )); + state.sessions.push(make_item( + "bbbbbbbb", + "bbbbbbbb-0000-0000-0000-000000000000", + )); state.list_selected = 1; let consumed = handle_session_list_event(press(KeyCode::Up), &mut state); assert!(consumed); @@ -468,8 +488,14 @@ mod tests { #[test] fn j_increments_selection() { let mut state = AppState::new(); - state.sessions.push(make_item("aaaaaaaa", "aaaaaaaa-0000-0000-0000-000000000000")); - state.sessions.push(make_item("bbbbbbbb", "bbbbbbbb-0000-0000-0000-000000000000")); + state.sessions.push(make_item( + "aaaaaaaa", + "aaaaaaaa-0000-0000-0000-000000000000", + )); + state.sessions.push(make_item( + "bbbbbbbb", + "bbbbbbbb-0000-0000-0000-000000000000", + )); state.list_selected = 0; handle_session_list_event(press(KeyCode::Char('j')), &mut state); assert_eq!(state.list_selected, 1); @@ -478,8 +504,14 @@ mod tests { #[test] fn k_decrements_selection() { let mut state = AppState::new(); - state.sessions.push(make_item("aaaaaaaa", "aaaaaaaa-0000-0000-0000-000000000000")); - state.sessions.push(make_item("bbbbbbbb", "bbbbbbbb-0000-0000-0000-000000000000")); + state.sessions.push(make_item( + "aaaaaaaa", + "aaaaaaaa-0000-0000-0000-000000000000", + )); + state.sessions.push(make_item( + "bbbbbbbb", + "bbbbbbbb-0000-0000-0000-000000000000", + )); state.list_selected = 1; handle_session_list_event(press(KeyCode::Char('k')), &mut state); assert_eq!(state.list_selected, 0); diff --git a/src/tui/screens/transcript.rs b/src/tui/screens/transcript.rs index 2bc3fef..ebe8130 100644 --- a/src/tui/screens/transcript.rs +++ b/src/tui/screens/transcript.rs @@ -169,11 +169,13 @@ pub fn build_chat_lines(entries: &[RawEntry], color_enabled: bool) -> Vec { - let input_str = - serde_json::to_string(input).unwrap_or_default(); + let input_str = serde_json::to_string(input).unwrap_or_default(); let truncated = truncate_str(&input_str, TOOL_INPUT_TRUNCATE); - let ellipsis = - if input_str.len() > TOOL_INPUT_TRUNCATE { "…" } else { "" }; + let ellipsis = if input_str.len() > TOOL_INPUT_TRUNCATE { + "…" + } else { + "" + }; let tool_style = if color_enabled { Style::default().fg(Color::Cyan) } else { @@ -187,20 +189,23 @@ pub fn build_chat_lines(entries: &[RawEntry], color_enabled: bool) -> Vec { - let err_flag = - if is_error.unwrap_or(false) { " (error)" } else { "" }; + let err_flag = if is_error.unwrap_or(false) { + " (error)" + } else { + "" + }; let preview = content .as_ref() .and_then(|c| c.as_str().map(|s| s.to_string())) .unwrap_or_else(|| { - content - .as_ref() - .map(|c| c.to_string()) - .unwrap_or_default() + content.as_ref().map(|c| c.to_string()).unwrap_or_default() }); let truncated = truncate_str(&preview, TOOL_RESULT_TRUNCATE); - let ellipsis = - if preview.len() > TOOL_RESULT_TRUNCATE { "…" } else { "" }; + let ellipsis = if preview.len() > TOOL_RESULT_TRUNCATE { + "…" + } else { + "" + }; let style = if color_enabled { if is_error.unwrap_or(false) { Style::default().fg(Color::Red) @@ -221,10 +226,7 @@ pub fn build_chat_lines(entries: &[RawEntry], color_enabled: bool) -> Vec { lines.push(Line::from("[unknown block]".to_string())); @@ -243,7 +245,7 @@ pub fn build_chat_lines(entries: &[RawEntry], color_enabled: bool) -> Vec {short_id} {agent_type}"), @@ -507,8 +508,7 @@ pub fn render_transcript(f: &mut Frame, area: Rect, state: &AppState) { .collect() }; - let subagents_paragraph = Paragraph::new(Text::from(subagent_lines)) - .block(subagents_block); + let subagents_paragraph = Paragraph::new(Text::from(subagent_lines)).block(subagents_block); f.render_widget(subagents_paragraph, body_chunks[1]); @@ -571,8 +571,7 @@ pub fn handle_transcript_event(event: Event, state: &mut AppState) -> bool { if !state.search_match_lines.is_empty() { state.search_current_match = (state.search_current_match + 1) % state.search_match_lines.len(); - state.transcript_scroll = - state.search_match_lines[state.search_current_match]; + state.transcript_scroll = state.search_match_lines[state.search_current_match]; } true } @@ -583,8 +582,7 @@ pub fn handle_transcript_event(event: Event, state: &mut AppState) -> bool { .search_current_match .checked_sub(1) .unwrap_or(state.search_match_lines.len() - 1); - state.transcript_scroll = - state.search_match_lines[state.search_current_match]; + state.transcript_scroll = state.search_match_lines[state.search_current_match]; } true } @@ -603,8 +601,7 @@ pub fn handle_transcript_event(event: Event, state: &mut AppState) -> bool { Focus::SubagentsPanel => { if !state.subagents.is_empty() { let max = state.subagents.len() - 1; - state.subagent_selected = - (state.subagent_selected + 1).min(max); + state.subagent_selected = (state.subagent_selected + 1).min(max); } true } @@ -724,16 +721,14 @@ pub fn load_transcript_for_session(session_id: &str, state: &mut AppState) { }; // Find the session whose ID matches (supports full UUID or 8-char prefix). - let session_ref = sessions.into_iter().find(|s| { - s.session_id == session_id || s.session_id.starts_with(session_id) - }); + let session_ref = sessions + .into_iter() + .find(|s| s.session_id == session_id || s.session_id.starts_with(session_id)); let Some(sr) = session_ref else { return }; // Discover sub-agents for the sidebar (best-effort). - if let Ok(agents) = - crate::parser::discovery::discover_agents_for_session(&sr.file_path) - { + if let Ok(agents) = crate::parser::discovery::discover_agents_for_session(&sr.file_path) { state.subagents = agents; } @@ -755,21 +750,16 @@ pub fn load_transcript_for_session(session_id: &str, state: &mut AppState) { /// /// Walks all discovered agents to find the one whose `agent_id` matches, then /// reads and parses its JSONL file. -pub fn load_transcript_for_agent( - parent_session_id: &str, - agent_id: &str, - state: &mut AppState, -) { +pub fn load_transcript_for_agent(parent_session_id: &str, agent_id: &str, state: &mut AppState) { let sessions = match crate::parser::discovery::discover_sessions() { Ok(s) => s, Err(_) => return, }; // Find the parent session. - let session_ref = sessions.into_iter().find(|s| { - s.session_id == parent_session_id - || s.session_id.starts_with(parent_session_id) - }); + let session_ref = sessions + .into_iter() + .find(|s| s.session_id == parent_session_id || s.session_id.starts_with(parent_session_id)); let Some(sr) = session_ref else { return }; @@ -905,11 +895,7 @@ mod tests { let lines = build_header_lines("abc12345-0000-0000-0000-000000000000", &[]); // Should have 1 line (no tool calls line when empty). assert_eq!(lines.len(), 1); - let text: String = lines[0] - .spans - .iter() - .map(|s| s.content.as_ref()) - .collect(); + let text: String = lines[0].spans.iter().map(|s| s.content.as_ref()).collect(); assert!(text.contains("abc12345")); assert!(text.contains("Tokens: in=0 out=0")); } @@ -930,11 +916,7 @@ mod tests { }); let lines = build_header_lines("abc12345", &[entry]); assert_eq!(lines.len(), 2); - let tools_text: String = lines[1] - .spans - .iter() - .map(|s| s.content.as_ref()) - .collect(); + let tools_text: String = lines[1].spans.iter().map(|s| s.content.as_ref()).collect(); assert!(tools_text.contains("Bash")); }