codetether_agent/tui/ui/
sessions.rs1use 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}