Skip to main content

codetether_agent/tui/ui/
sessions.rs

1use ratatui::{
2    Frame,
3    layout::{Constraint, Direction, Layout},
4    style::{Color, Style, Stylize},
5    text::{Line, Span},
6    widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
7};
8
9use super::status_bar::bus_status_badge_span;
10use crate::tui::app::state::App;
11
12pub fn render_sessions_view(f: &mut Frame, app: &mut App) {
13    let area = f.area();
14    let chunks = Layout::default()
15        .direction(Direction::Vertical)
16        .constraints([
17            Constraint::Length(3),
18            Constraint::Min(8),
19            Constraint::Length(3),
20        ])
21        .split(area);
22
23    let filter_display = if app.state.session_filter.is_empty() {
24        String::new()
25    } else {
26        format!(" [filter: {}]", app.state.session_filter)
27    };
28
29    let header = Paragraph::new(vec![Line::from(vec![
30        Span::raw(" Session picker ").black().on_cyan(),
31        Span::raw(" "),
32        Span::raw(app.state.status.clone()).dim(),
33    ])])
34    .block(Block::default().borders(Borders::ALL).title(format!(
35        " Sessions (↑↓ navigate, Enter load, Esc cancel){} ",
36        filter_display
37    )));
38    f.render_widget(header, chunks[0]);
39
40    let filtered = app.state.filtered_sessions();
41    let items: Vec<ListItem<'static>> = if filtered.is_empty() {
42        if app.state.session_filter.is_empty() {
43            vec![ListItem::new("No workspace sessions found")]
44        } else {
45            vec![ListItem::new(format!(
46                "No sessions matching '{}'",
47                app.state.session_filter
48            ))]
49        }
50    } else {
51        filtered
52            .iter()
53            .map(|(_, session)| {
54                let title = session
55                    .title
56                    .clone()
57                    .unwrap_or_else(|| "Untitled session".to_string());
58                let active_marker = if app.state.session_id.as_deref() == Some(session.id.as_str())
59                {
60                    " ●"
61                } else {
62                    ""
63                };
64                let summary = format!(
65                    "{}{}  •  {} msgs  •  {}",
66                    title,
67                    active_marker,
68                    session.message_count,
69                    session.updated_at.format("%Y-%m-%d %H:%M")
70                );
71                ListItem::new(summary)
72            })
73            .collect()
74    };
75
76    let mut state = ListState::default();
77    if !filtered.is_empty() {
78        state.select(Some(app.state.selected_session.min(filtered.len() - 1)));
79    }
80
81    let list = List::new(items)
82        .block(
83            Block::default()
84                .borders(Borders::ALL)
85                .title(" Available Sessions "),
86        )
87        .highlight_style(Style::default().bg(Color::DarkGray).fg(Color::Cyan).bold())
88        .highlight_symbol("▶ ");
89    f.render_stateful_widget(list, chunks[1], &mut state);
90
91    let help = Paragraph::new(Line::from(vec![
92        Span::styled(
93            " SESSION PICKER ",
94            Style::default().fg(Color::Black).bg(Color::Cyan),
95        ),
96        Span::raw(" "),
97        Span::styled("↑↓", Style::default().fg(Color::Yellow)),
98        Span::raw(": Nav "),
99        Span::styled("Enter", Style::default().fg(Color::Yellow)),
100        Span::raw(": Load "),
101        Span::styled("Type", Style::default().fg(Color::Yellow)),
102        Span::raw(": Filter "),
103        Span::styled("Backspace", Style::default().fg(Color::Yellow)),
104        Span::raw(": Edit "),
105        Span::styled("Esc", Style::default().fg(Color::Yellow)),
106        Span::raw(": Cancel | "),
107        bus_status_badge_span(app),
108    ]));
109    f.render_widget(help, chunks[2]);
110}