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