Skip to main content

rab/agent/ui/
model_selector.rs

1use crate::agent::ui::theme::RabTheme;
2use crate::tui::Component;
3use crate::tui::components::select_list::{SelectItem, SelectList, SelectListTheme};
4
5/// Full-screen model selector using tui::SelectList.
6pub struct ModelSelector {
7    select_list: SelectList,
8    theme: RabTheme,
9    pub selected_model: Option<String>,
10}
11
12impl ModelSelector {
13    pub fn new(models: Vec<String>, current_model: &str, theme: &RabTheme) -> Self {
14        let items: Vec<SelectItem> = models
15            .iter()
16            .map(|m| {
17                let display = m.strip_prefix("opencode_go::").unwrap_or(m);
18                SelectItem::new(m.clone(), display.to_string())
19            })
20            .collect();
21
22        let current_index = models
23            .iter()
24            .position(|m| m == current_model || format!("opencode_go::{}", m) == current_model)
25            .unwrap_or(0);
26
27        let list_theme = SelectListTheme {
28            selected_prefix: Box::new(|s| format!("\x1b[38;2;138;190;183m\x1b[1m> {}\x1b[0m", s)),
29            selected_text: Box::new(|s| format!("\x1b[38;2;138;190;183m\x1b[1m{}\x1b[0m", s)),
30            normal_text: Box::new(|s| format!("  \x1b[38;2;212;212;212m{}\x1b[0m", s)),
31            description: Box::new(|s| format!("\x1b[38;2;128;128;128m{}\x1b[0m", s)),
32            scroll_info: crate::tui::Style::new().fg("\x1b[38;2;80;80;80m".to_string()),
33            no_match: crate::tui::Style::new().fg("\x1b[38;2;255;255;0m".to_string()),
34            hint: crate::tui::Style::new().fg("\x1b[38;2;128;128;128m".to_string()),
35        };
36
37        let max_visible = models.len().clamp(5, 20);
38        let mut select_list = SelectList::new(items, max_visible, list_theme, None);
39        select_list = select_list.with_search();
40
41        // Set initial selection
42        for _ in 0..current_index {
43            select_list.handle_input(&crossterm::event::KeyEvent::new(
44                crossterm::event::KeyCode::Down,
45                crossterm::event::KeyModifiers::NONE,
46            ));
47        }
48
49        Self {
50            select_list,
51            theme: theme.clone(),
52            selected_model: None,
53        }
54    }
55}
56
57impl Component for ModelSelector {
58    fn render(&mut self, width: usize) -> Vec<String> {
59        let mut lines = vec![
60            self.theme.bold(&self.theme.accent("  Select Model")),
61            String::new(),
62            self.theme.dim("  Type to search…"),
63            String::new(),
64        ];
65
66        // List
67        lines.extend(self.select_list.render(width));
68
69        lines
70    }
71
72    fn handle_input(&mut self, key: &crossterm::event::KeyEvent) -> bool {
73        let kb = crate::tui::keybindings::get_keybindings();
74
75        if kb.matches(key, crate::tui::keybindings::ACTION_SELECT_CONFIRM) {
76            if let Some(item) = self.select_list.selected_item() {
77                self.selected_model = Some(item.value.clone());
78            }
79            return true;
80        }
81
82        if kb.matches(key, crate::tui::keybindings::ACTION_SELECT_CANCEL) {
83            self.selected_model = None;
84            return true;
85        }
86
87        self.select_list.handle_input(key)
88    }
89}