Skip to main content

codetether_agent/tui/
model_picker.rs

1use ratatui::{
2    Frame,
3    layout::{Constraint, Direction, Layout, Rect},
4    style::{Color, Style, Stylize},
5    text::{Line, Span},
6    widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
7};
8
9use crate::session::Session;
10use crate::tui::app::state::App;
11
12pub fn render_model_picker(f: &mut Frame, area: Rect, app: &mut App, session: &Session) {
13    let chunks = Layout::default()
14        .direction(Direction::Vertical)
15        .constraints([
16            Constraint::Length(3),
17            Constraint::Min(8),
18            Constraint::Length(3),
19            Constraint::Length(3),
20        ])
21        .split(area);
22
23    let filter_suffix = if app.state.model_filter.is_empty() {
24        String::new()
25    } else {
26        format!(" [filter: {}]", app.state.model_filter)
27    };
28
29    let header = Paragraph::new(vec![Line::from(vec![
30        Span::raw(" Model picker ").black().on_cyan(),
31        Span::raw(" "),
32        Span::raw(app.state.status.clone()).dim(),
33    ])])
34    .block(
35        Block::default()
36            .borders(Borders::ALL)
37            .title(format!(" Models{} ", filter_suffix)),
38    );
39    f.render_widget(header, chunks[0]);
40
41    let models = app.state.filtered_models();
42    let items: Vec<ListItem<'static>> = if models.is_empty() {
43        vec![ListItem::new("No models available")]
44    } else {
45        models
46            .iter()
47            .map(|model| ListItem::new((*model).to_string()))
48            .collect()
49    };
50
51    let mut list_state = ListState::default();
52    if !models.is_empty() {
53        list_state.select(Some(app.state.selected_model_index.min(models.len() - 1)));
54    }
55
56    let list = List::new(items)
57        .block(
58            Block::default()
59                .borders(Borders::ALL)
60                .title(" Available Models "),
61        )
62        .highlight_style(Style::default().bg(Color::DarkGray).fg(Color::Cyan).bold())
63        .highlight_symbol("▶ ");
64    f.render_stateful_widget(list, chunks[1], &mut list_state);
65
66    let current = app
67        .state
68        .selected_model()
69        .or(session.metadata.model.as_deref())
70        .unwrap_or("auto");
71    let current_widget = Paragraph::new(Line::from(vec![
72        Span::raw("Current selection: "),
73        Span::styled(current.to_string(), Style::default().fg(Color::Cyan).bold()),
74    ]))
75    .block(Block::default().borders(Borders::ALL).title(" Selection "));
76    f.render_widget(current_widget, chunks[2]);
77
78    let footer = Paragraph::new(Line::from(vec![
79        Span::styled(" MODEL ", Style::default().fg(Color::Black).bg(Color::Cyan)),
80        Span::raw(" "),
81        Span::styled("↑↓", Style::default().fg(Color::Yellow)),
82        Span::raw(": Nav "),
83        Span::styled("Type", Style::default().fg(Color::Yellow)),
84        Span::raw(": Filter "),
85        Span::styled("Backspace", Style::default().fg(Color::Yellow)),
86        Span::raw(": Edit "),
87        Span::styled("Enter", Style::default().fg(Color::Yellow)),
88        Span::raw(": Apply "),
89        Span::styled("Esc", Style::default().fg(Color::Yellow)),
90        Span::raw(": Cancel | "),
91        Span::raw(app.state.status.clone()).dim(),
92    ]))
93    .block(Block::default().borders(Borders::ALL).title(" Controls "));
94    f.render_widget(footer, chunks[3]);
95}