From 7f864192e8cc6de80e3dffefa57dc5eeed82be1d Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Mon, 30 Mar 2026 14:02:23 -0700 Subject: [PATCH] fix(tui): populate session list from disk on startup run_tui() now calls discover_sessions() before entering the event loop, converts each SessionRef to SessionListItem (sorted most-recent-first), and assigns to state.sessions. Failures silently yield an empty list. Fixes claudbg-zi1d Co-Authored-By: Claude Sonnet 4.6 --- ...-list-shows-no-sessions-statesessions-n.md | 41 +++++++++++++++++++ specs/FILTER.md | 27 ++++++++++++ src/tui/run.rs | 30 +++++++++++++- 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 .beans/claudbg-zi1d--tui-session-list-shows-no-sessions-statesessions-n.md create mode 100644 specs/FILTER.md diff --git a/.beans/claudbg-zi1d--tui-session-list-shows-no-sessions-statesessions-n.md b/.beans/claudbg-zi1d--tui-session-list-shows-no-sessions-statesessions-n.md new file mode 100644 index 0000000..f669ed4 --- /dev/null +++ b/.beans/claudbg-zi1d--tui-session-list-shows-no-sessions-statesessions-n.md @@ -0,0 +1,41 @@ +--- +# claudbg-zi1d +title: TUI session list shows no sessions — state.sessions never populated on startup +status: in-progress +type: bug +priority: high +created_at: 2026-03-30T17:05:02Z +updated_at: 2026-03-30T17:05:05Z +parent: claudbg-i6l2 +--- + +## Problem + +The TUI session list is always empty. `run_tui()` in `src/tui/run.rs` creates `AppState::new()` (which has `sessions: Vec::new()`) and enters the event loop without ever loading sessions from disk. + +## Root cause + +`discover_sessions()` in `src/parser/discovery.rs` is never called from the TUI startup path. The session list screen renders from `state.sessions` which stays empty forever. + +## Fix + +Before entering the event loop in `run_tui()`, call `discover_sessions()` and convert each `SessionRef` into a `SessionListItem`, then populate `state.sessions`. + +### SessionRef → SessionListItem mapping +- `short_id` = first 8 chars of `session_id` +- `full_id` = `session_id` +- `date` = `modified_at.format("%Y-%m-%d %H:%M:%S")` +- `project` = `project_path.unwrap_or_default()` +- `model` = `""` (leave empty — would require parsing JSONL) +- `msg_count` = 0 (leave as 0 for now) +- `agent_count` = call `discover_agents_for_session(&session_ref.file_path).map(|v| v.len()).unwrap_or(0)` + +Sort by `modified_at` descending (most recent first). + +### Error handling +If `discover_sessions()` fails, start with an empty list (don't crash). Log the error to stderr before entering the alternate screen, or silently ignore. + +## Relevant files +- `src/tui/run.rs` — `run_tui()` function, add session loading before the event loop +- `src/parser/discovery.rs` — `discover_sessions()` and `discover_agents_for_session()` +- `src/tui/state.rs` — `SessionListItem` struct diff --git a/specs/FILTER.md b/specs/FILTER.md new file mode 100644 index 0000000..aa7af9a --- /dev/null +++ b/specs/FILTER.md @@ -0,0 +1,27 @@ +# Filter Queries + +We want basic filtering to be able to narrow our view. +Use cases include only showing sesisons... +* with sub-agents +* with a certain minimum/maximum of messages +* in a given project +* with a given model +* between a given date range + +Proposed query language is a simple `key:value` for static single values, with support for globbing and by default fuzzy searching. +Examples: +* `model:haiku` equivalent to `model:*haiku*` +* `project:my-org/my-project` ~= `project:*my-org/my-project` +* `project:*` project is non-empty + +Also required is support for `>` and `<` to support ranges +Examples: +* `agents>0` +* `messages<10` +* `date>2026-03-20` + +We also need to support `AND` and `OR` for logical combinations +Examples: +* `date>2026-03-15 AND date<2026-03-20` sessions between 03-15 and 03-20. + +When a query uses a malformed syntax or a key which is not found, the command should fail to evaluate with an error displayed to the user. diff --git a/src/tui/run.rs b/src/tui/run.rs index 9444ba4..942f7ab 100644 --- a/src/tui/run.rs +++ b/src/tui/run.rs @@ -21,6 +21,7 @@ use ratatui::Terminal; use ratatui::backend::CrosstermBackend; use crate::error::Result; +use crate::parser::discovery::{discover_agents_for_session, discover_sessions}; use crate::tui::modals::help_modal::{handle_help_modal_event, render_help_modal}; use crate::tui::modals::quit_dialog::{handle_quit_dialog_event, render_quit_dialog}; use crate::tui::screens::session_list::{handle_session_list_event, render_session_list}; @@ -28,7 +29,7 @@ use crate::tui::screens::transcript::{ handle_transcript_event, load_transcript_for_agent, load_transcript_for_session, render_transcript, }; -use crate::tui::state::{AppState, Screen}; +use crate::tui::state::{AppState, Screen, SessionListItem}; // --------------------------------------------------------------------------- // RAII terminal guard @@ -193,6 +194,33 @@ pub fn run_tui() -> Result<()> { let mut guard = TerminalGuard::new()?; let mut state = AppState::new(); + // Populate the session list from disk before entering the event loop. + // Silently use an empty vec if discovery fails so the TUI still starts. + let mut session_refs = discover_sessions().unwrap_or_default(); + // Sort most-recent-first before converting to display items. + session_refs.sort_by(|a, b| b.modified_at.cmp(&a.modified_at)); + state.sessions = session_refs + .into_iter() + .map(|sr| { + let short_id = sr.session_id.chars().take(8).collect(); + let full_id = sr.session_id.clone(); + let date = sr.modified_at.format("%Y-%m-%d %H:%M:%S").to_string(); + let project = sr.project_path.clone().unwrap_or_default(); + let agent_count = discover_agents_for_session(&sr.file_path) + .map(|v| v.len()) + .unwrap_or(0); + SessionListItem { + short_id, + full_id, + date, + project, + model: String::new(), + msg_count: 0, + agent_count, + } + }) + .collect(); + loop { // Load transcript data lazily when entering a transcript screen. maybe_load_transcript(&mut state);