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 <noreply@anthropic.com>
main
Elijah Voigt 2 months ago
parent 3dd463b3ae
commit 7f864192e8

@ -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

@ -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.

@ -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);

Loading…
Cancel
Save