mirror of
https://github.com/openai/codex.git
synced 2026-04-30 19:32:04 +03:00
267 lines
8.1 KiB
Rust
267 lines
8.1 KiB
Rust
use crate::render::Insets;
|
||
use crate::render::renderable::ColumnRenderable;
|
||
use crate::render::renderable::Renderable;
|
||
use crate::render::renderable::RenderableExt as _;
|
||
use crate::tui::FrameRequester;
|
||
use crate::tui::Tui;
|
||
use crate::tui::TuiEvent;
|
||
use crossterm::event::KeyCode;
|
||
use crossterm::event::KeyEvent;
|
||
use crossterm::event::KeyEventKind;
|
||
use crossterm::event::KeyModifiers;
|
||
use ratatui::prelude::Stylize as _;
|
||
use ratatui::prelude::Widget;
|
||
use ratatui::text::Line;
|
||
use ratatui::widgets::Clear;
|
||
use ratatui::widgets::Paragraph;
|
||
use ratatui::widgets::WidgetRef;
|
||
use ratatui::widgets::Wrap;
|
||
use tokio_stream::StreamExt;
|
||
|
||
/// Outcome of the migration prompt.
|
||
pub(crate) enum ModelMigrationOutcome {
|
||
Accepted,
|
||
Exit,
|
||
}
|
||
|
||
pub(crate) async fn run_model_migration_prompt(tui: &mut Tui) -> ModelMigrationOutcome {
|
||
// Render the prompt on the terminal's alternate screen so exiting or cancelling
|
||
// does not leave a large blank region in the normal scrollback. This does not
|
||
// change the prompt's appearance – only where it is drawn.
|
||
struct AltScreenGuard<'a> {
|
||
tui: &'a mut Tui,
|
||
}
|
||
impl<'a> AltScreenGuard<'a> {
|
||
fn enter(tui: &'a mut Tui) -> Self {
|
||
let _ = tui.enter_alt_screen();
|
||
Self { tui }
|
||
}
|
||
}
|
||
impl Drop for AltScreenGuard<'_> {
|
||
fn drop(&mut self) {
|
||
let _ = self.tui.leave_alt_screen();
|
||
}
|
||
}
|
||
|
||
let alt = AltScreenGuard::enter(tui);
|
||
|
||
let mut screen = ModelMigrationScreen::new(alt.tui.frame_requester());
|
||
|
||
let _ = alt.tui.draw(u16::MAX, |frame| {
|
||
frame.render_widget_ref(&screen, frame.area());
|
||
});
|
||
|
||
let events = alt.tui.event_stream();
|
||
tokio::pin!(events);
|
||
|
||
while !screen.is_done() {
|
||
if let Some(event) = events.next().await {
|
||
match event {
|
||
TuiEvent::Key(key_event) => screen.handle_key(key_event),
|
||
TuiEvent::Paste(_) => {}
|
||
TuiEvent::Draw => {
|
||
let _ = alt.tui.draw(u16::MAX, |frame| {
|
||
frame.render_widget_ref(&screen, frame.area());
|
||
});
|
||
}
|
||
}
|
||
} else {
|
||
screen.accept();
|
||
break;
|
||
}
|
||
}
|
||
|
||
screen.outcome()
|
||
}
|
||
|
||
struct ModelMigrationScreen {
|
||
request_frame: FrameRequester,
|
||
done: bool,
|
||
should_exit: bool,
|
||
}
|
||
|
||
impl ModelMigrationScreen {
|
||
fn new(request_frame: FrameRequester) -> Self {
|
||
Self {
|
||
request_frame,
|
||
done: false,
|
||
should_exit: false,
|
||
}
|
||
}
|
||
|
||
fn accept(&mut self) {
|
||
self.done = true;
|
||
self.request_frame.schedule_frame();
|
||
}
|
||
|
||
fn handle_key(&mut self, key_event: KeyEvent) {
|
||
if key_event.kind == KeyEventKind::Release {
|
||
return;
|
||
}
|
||
|
||
if key_event.modifiers.contains(KeyModifiers::CONTROL)
|
||
&& matches!(key_event.code, KeyCode::Char('c') | KeyCode::Char('d'))
|
||
{
|
||
self.should_exit = true;
|
||
self.done = true;
|
||
self.request_frame.schedule_frame();
|
||
return;
|
||
}
|
||
|
||
if matches!(key_event.code, KeyCode::Esc | KeyCode::Enter) {
|
||
self.accept();
|
||
}
|
||
}
|
||
|
||
fn is_done(&self) -> bool {
|
||
self.done
|
||
}
|
||
|
||
fn outcome(&self) -> ModelMigrationOutcome {
|
||
if self.should_exit {
|
||
ModelMigrationOutcome::Exit
|
||
} else {
|
||
ModelMigrationOutcome::Accepted
|
||
}
|
||
}
|
||
}
|
||
|
||
impl WidgetRef for &ModelMigrationScreen {
|
||
fn render_ref(&self, area: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) {
|
||
Clear.render(area, buf);
|
||
|
||
let mut column = ColumnRenderable::new();
|
||
|
||
column.push("");
|
||
column.push(Line::from(vec![
|
||
"> ".into(),
|
||
"Introducing our gpt-5.1 models".bold(),
|
||
]));
|
||
column.push(Line::from(""));
|
||
|
||
column.push(
|
||
Paragraph::new(Line::from(
|
||
"We've upgraded our family of models supported in Codex to gpt-5.1, gpt-5.1-codex and gpt-5.1-codex-mini.",
|
||
))
|
||
.wrap(Wrap { trim: false })
|
||
.inset(Insets::tlbr(0, 2, 0, 0)),
|
||
);
|
||
column.push(Line::from(""));
|
||
column.push(
|
||
Paragraph::new(Line::from(
|
||
"You can continue using legacy models by specifying them directly with the -m option or in your config.toml.",
|
||
))
|
||
.wrap(Wrap { trim: false })
|
||
.inset(Insets::tlbr(0, 2, 0, 0)),
|
||
);
|
||
column.push(Line::from(""));
|
||
column.push(
|
||
Line::from(vec![
|
||
"Learn more at ".into(),
|
||
"www.openai.com/index/gpt-5-1".cyan().underlined(),
|
||
".".into(),
|
||
])
|
||
.inset(Insets::tlbr(0, 2, 0, 0)),
|
||
);
|
||
column.push(Line::from(""));
|
||
column.push(
|
||
Line::from(vec!["Press enter to continue".dim()]).inset(Insets::tlbr(0, 2, 0, 0)),
|
||
);
|
||
|
||
column.render(area, buf);
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::ModelMigrationScreen;
|
||
use crate::custom_terminal::Terminal;
|
||
use crate::test_backend::VT100Backend;
|
||
use crate::tui::FrameRequester;
|
||
use crossterm::event::KeyCode;
|
||
use crossterm::event::KeyEvent;
|
||
use insta::assert_snapshot;
|
||
use ratatui::layout::Rect;
|
||
|
||
#[test]
|
||
fn prompt_snapshot() {
|
||
let width: u16 = 60;
|
||
let height: u16 = 12;
|
||
let backend = VT100Backend::new(width, height);
|
||
let mut terminal = Terminal::with_options(backend).expect("terminal");
|
||
terminal.set_viewport_area(Rect::new(0, 0, width, height));
|
||
|
||
let screen = ModelMigrationScreen::new(FrameRequester::test_dummy());
|
||
|
||
{
|
||
let mut frame = terminal.get_frame();
|
||
frame.render_widget_ref(&screen, frame.area());
|
||
}
|
||
terminal.flush().expect("flush");
|
||
|
||
assert_snapshot!("model_migration_prompt", terminal.backend());
|
||
}
|
||
|
||
#[test]
|
||
fn prompt_snapshot_gpt5_family() {
|
||
let backend = VT100Backend::new(65, 12);
|
||
let mut terminal = Terminal::with_options(backend).expect("terminal");
|
||
terminal.set_viewport_area(Rect::new(0, 0, 65, 12));
|
||
|
||
let screen = ModelMigrationScreen::new(FrameRequester::test_dummy());
|
||
{
|
||
let mut frame = terminal.get_frame();
|
||
frame.render_widget_ref(&screen, frame.area());
|
||
}
|
||
terminal.flush().expect("flush");
|
||
assert_snapshot!("model_migration_prompt_gpt5_family", terminal.backend());
|
||
}
|
||
|
||
#[test]
|
||
fn prompt_snapshot_gpt5_codex() {
|
||
let backend = VT100Backend::new(60, 12);
|
||
let mut terminal = Terminal::with_options(backend).expect("terminal");
|
||
terminal.set_viewport_area(Rect::new(0, 0, 60, 12));
|
||
|
||
let screen = ModelMigrationScreen::new(FrameRequester::test_dummy());
|
||
{
|
||
let mut frame = terminal.get_frame();
|
||
frame.render_widget_ref(&screen, frame.area());
|
||
}
|
||
terminal.flush().expect("flush");
|
||
assert_snapshot!("model_migration_prompt_gpt5_codex", terminal.backend());
|
||
}
|
||
|
||
#[test]
|
||
fn prompt_snapshot_gpt5_codex_mini() {
|
||
let backend = VT100Backend::new(60, 12);
|
||
let mut terminal = Terminal::with_options(backend).expect("terminal");
|
||
terminal.set_viewport_area(Rect::new(0, 0, 60, 12));
|
||
|
||
let screen = ModelMigrationScreen::new(FrameRequester::test_dummy());
|
||
{
|
||
let mut frame = terminal.get_frame();
|
||
frame.render_widget_ref(&screen, frame.area());
|
||
}
|
||
terminal.flush().expect("flush");
|
||
assert_snapshot!("model_migration_prompt_gpt5_codex_mini", terminal.backend());
|
||
}
|
||
|
||
#[test]
|
||
fn escape_key_accepts_prompt() {
|
||
let mut screen = ModelMigrationScreen::new(FrameRequester::test_dummy());
|
||
|
||
// Simulate pressing Escape
|
||
screen.handle_key(KeyEvent::new(
|
||
KeyCode::Esc,
|
||
crossterm::event::KeyModifiers::NONE,
|
||
));
|
||
assert!(screen.is_done());
|
||
// Esc should not be treated as Exit – it accepts like Enter.
|
||
assert!(matches!(
|
||
screen.outcome(),
|
||
super::ModelMigrationOutcome::Accepted
|
||
));
|
||
}
|
||
}
|