use crate::ascii_animation::AsciiAnimation; use crate::tui::FrameRequester; use crate::tui::Tui; use crate::tui::TuiEvent; use color_eyre::eyre::Result; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyModifiers; use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::prelude::Widget; use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::widgets::Clear; use ratatui::widgets::Paragraph; use ratatui::widgets::WidgetRef; use ratatui::widgets::Wrap; use tokio_stream::StreamExt; const MIN_ANIMATION_HEIGHT: u16 = 24; const MIN_ANIMATION_WIDTH: u16 = 60; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum ModelUpgradeDecision { Switch, KeepCurrent, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum ModelUpgradeOption { TryNewModel, KeepCurrent, } struct ModelUpgradePopup { highlighted: ModelUpgradeOption, decision: Option, animation: AsciiAnimation, } impl ModelUpgradePopup { fn new(request_frame: FrameRequester) -> Self { Self { highlighted: ModelUpgradeOption::TryNewModel, decision: None, animation: AsciiAnimation::new(request_frame), } } fn handle_key_event(&mut self, key_event: KeyEvent) { match key_event.code { KeyCode::Up | KeyCode::Char('k') => self.highlight(ModelUpgradeOption::TryNewModel), KeyCode::Down | KeyCode::Char('j') => self.highlight(ModelUpgradeOption::KeepCurrent), KeyCode::Char('1') => self.select(ModelUpgradeOption::TryNewModel), KeyCode::Char('2') => self.select(ModelUpgradeOption::KeepCurrent), KeyCode::Enter => self.select(self.highlighted), KeyCode::Esc => self.select(ModelUpgradeOption::KeepCurrent), KeyCode::Char('.') => { if key_event.modifiers.contains(KeyModifiers::CONTROL) { let _ = self.animation.pick_random_variant(); } } _ => {} } } fn highlight(&mut self, option: ModelUpgradeOption) { if self.highlighted != option { self.highlighted = option; self.animation.request_frame(); } } fn select(&mut self, option: ModelUpgradeOption) { self.decision = Some(option.into()); self.animation.request_frame(); } } impl From for ModelUpgradeDecision { fn from(option: ModelUpgradeOption) -> Self { match option { ModelUpgradeOption::TryNewModel => ModelUpgradeDecision::Switch, ModelUpgradeOption::KeepCurrent => ModelUpgradeDecision::KeepCurrent, } } } impl WidgetRef for &ModelUpgradePopup { fn render_ref(&self, area: Rect, buf: &mut Buffer) { Clear.render(area, buf); self.animation.schedule_next_frame(); // Skip the animation entirely when the viewport is too small so we don't clip frames. let show_animation = area.height >= MIN_ANIMATION_HEIGHT && area.width >= MIN_ANIMATION_WIDTH; let mut lines: Vec = Vec::new(); if show_animation { let frame = self.animation.current_frame(); lines.extend(frame.lines().map(Into::into)); // Spacer between animation and text content. lines.push("".into()); } lines.push(Line::from(vec![ " ".into(), "Introducing GPT-5-Codex".bold(), ])); lines.push("".into()); lines.push( " GPT-5-Codex works faster through easy tasks and harder on complex tasks,".into(), ); lines.push(" improves on code quality, and is more steerable with AGENTS.md.".into()); lines.push("".into()); let create_option = |index: usize, option: ModelUpgradeOption, text: &str| -> Line<'static> { if self.highlighted == option { Line::from(vec![ format!("> {}. ", index + 1).cyan(), text.to_owned().cyan(), ]) } else { format!(" {}. {text}", index + 1).into() } }; lines.push(create_option( 0, ModelUpgradeOption::TryNewModel, "Try the new GPT-5-Codex model", )); lines.push("".into()); lines.push(create_option( 1, ModelUpgradeOption::KeepCurrent, "Continue using current model", )); lines.push("".into()); lines.push( " Press Enter to confirm or Esc to keep your current model" .dim() .into(), ); Paragraph::new(lines) .wrap(Wrap { trim: false }) .render(area, buf); } } pub(crate) async fn run_model_upgrade_popup(tui: &mut Tui) -> Result { let mut popup = ModelUpgradePopup::new(tui.frame_requester()); tui.draw(u16::MAX, |frame| { frame.render_widget_ref(&popup, frame.area()); })?; let events = tui.event_stream(); tokio::pin!(events); while popup.decision.is_none() { if let Some(event) = events.next().await { match event { TuiEvent::Key(key_event) => popup.handle_key_event(key_event), TuiEvent::Draw => { let _ = tui.draw(u16::MAX, |frame| { frame.render_widget_ref(&popup, frame.area()); }); } _ => {} } } else { break; } } Ok(popup.decision.unwrap_or(ModelUpgradeDecision::KeepCurrent)) }