Login flow polish (#3632)

# Description
- Update sign in flow

# Tests
- Passes CI

---------

Co-authored-by: Michael Bolin <mbolin@openai.com>
This commit is contained in:
Ed Bayes
2025-09-15 00:42:53 -07:00
committed by GitHub
parent 2d52e3b40a
commit b9af1d2b16
12 changed files with 168 additions and 108 deletions

View File

@@ -3,14 +3,14 @@
             *'`+*+\~/_*,                           *'`+*+\~/_*,             
            ^_,||/~~-~+\,,                         ^_,||/~~-~+\,,            
           |__/\|;_.\,''\\,                       |__/\|;_.\,''\\,           
           / ;||"|^ ',/_/|/                       / ;||"|^  /_/|/           
          |` '|*~//\  !`_"|                      |` '|*~//\   `_"|           
          \  ~*"*||~|*   |/,                     \  ~*"*||~|*   |/,          
          "  ||\+/+||-_`.\||                     "  ||\+/+||-_ .\||          
          "  ~\ \\|;~~+\+;||                     "  ~\ \\|;~~+\+;||          
          |  ,|\,|_/_*___|*`                     |  ,|\,|_/_*___|*`          
           , "|||||""!\,"\|`                      , "|||||""!\,"\|`          
           \`',\,*" _~"",//                       \`',\,*"  "",//           
            |' |||~*,:,/|/`                        |' |||~*,:,/|/`           
             ;`**/|+;_!//'                          ;`**/|+;_!//'            
              *, _*\_,;*                             *, _*\_,;*              

View File

@@ -2,15 +2,15 @@
                _=+"**+~_                              _=+"**+~_             
               /^||*||\|=\                            /^||*||\|=\            
              |//"\/=\|| '\                          |//"\/=\|| '\           
             /// ;*_\|\||**|                        /// ;' \|\||**|          
             ||;_/*'=||*/|`\                        ||;_ =||*/|`\          
            |||*|' |\= !| ~.|                      |||*|  /|= !| ~.|         
            \\| /`,||||/*", |                      \\|  ,||||/*", |         
            |/; |`||/|||"; `|                      |/; |`||/|||"; `|         
            \\|~|+~/^||"*+  /                      \\|~|+~/^||"*+  /         
            *"__,==\*|._| ,_|                      *"__,==\*|._| ,_|         
            |||+""/*\|;";.~|`                      |||+""/*\|;";.~|`         
             ||* |`  `//,  /                        ||* |   `//,  /          
             \|*  |  /,/_,|                         \|*  |  /,/_,|           
              \|~"_*~//+_|                           \|~"_*~//+_|            
               ':._=:__;*                             ':._=:__;*             

View File

@@ -2,11 +2,11 @@
                ,=+++;;~,_                             ,=+++;;~,_            
             _;**|~~*=*|,"^,                        _;**|~~*=*|,"^,          
            ,*\/_==`+,"|||_"\                      ,*\/_==`+,"|||_"\         
           /|/_/|"_` '|;\~||=\                    /|/_/|"   |;\~||=\        
           |/_ ~    |"/\=\//  ,                   |/_ ~     "/\=\//  ,       
          `=*,/`   ,:/| /,=/|./                  `=*,/`   ,:/| /,=/|./       
          *!;/|   ,//|_ *"||/=|                  *!;/|   ,//|_ *"||/=|       
          -"=|!   !//||/|,||=;*                  -"=|!   !//||/ ,||=;*       
          ,/*/\==+~\_|\^:\||| |                  ,/*/\==+~\_|\^:\||| |       
           |"_;__|/*\/||\!\+'+\                   |"_;__|/*\/||\!\+'+\       
          \\\/"""****\_|*//\ \'                  \\\/"""****\_|*//\ \'       

View File

@@ -1,10 +1,10 @@
                                                                             
               _==+==+;~,_                            _==+==+;~,_            
            _+"_;,++~__,"+;;_                      _+"_;,++~__,"+;;_         
          _/:|*"*=","._"+//\ *                   _/:|*"*=" "._"+//\ *        
         ,||*.,^"* '\`_/=~\;\\\,                ,||*.,^" _/=~\;\\\,      
        _\// /|  _"+_\/~/|_\;\\_               _\// /|   _\/~/|_\;\\_      
        /\| ,,~.___/,*,|'-/^/`/~!              /\| ,, _/,*,|'-/^/`/~!     
        ||\:/     +/*/|"_/"*|=|=,              ||\:/     +/*/|"_/"*|=|=,     
        "\-~|     ^\"||;^   |;|"               "\-~|     ^\"||;^   |;|"      
       \"" ,\==;=;+~|,|*/\, |*|`              \"" ,\==;=;+~|,|*/\, |*|`      

View File

@@ -5,7 +5,7 @@
        _*\*/|,+|     ' =^||\                  _*\*/|,+|     ' =^||\         
        ' /|/,\|/\      .'\||,                 ' /|/,\|/\      .'\||,        
       |',|\^^_\|_*      \+|||                |',|\^^_\|_*      \+|||        
       |*||| '_;\|`|     |^|/|,               |*||| '_;\|`|      ^|/|,       
       \,||/  |+\/|,*.    .`/||               \,||/  |+\/|,*.    .`/||       
       \ \||_^ !/*=|~/+,+,,\~||               \ \||_^ !/*=|~/+,+,,\~||       
       !  \*/=_|",|,||;|=__||='               !  \*/=_|",|,||;|=__||='       

View File

@@ -3,10 +3,10 @@
            /*_/||*"=!_-\_                         /*_/||*"=!_-\_            
           / ,||/*^^=/!\_~,                       / ,||/*^^=/!\_~,           
          " ||/;=_ _^,|\^|+,                     " ||/;=_ _^,|\^|+,          
         /*";\*"|*, *.'+:+||                    /*";\*"|*,  +:+||          
         | |||"^|\;__ |'|*|||                   | |||"^|\;*   '|*|||         
         ` ~*\|**|\\,".,/ `||                   ` ~*\ **|\\," / `||         
        |  ~/_,\~||_/=\_| !||                  |  ~/_ ~||_/= | !||         
        !  ",|" /|"|~~|+|~,||`                 !  ",|" /|"|~~|+|~,||`        
         |_|||_,|^|_||||__|||                   |_|||_,|^|_||||__|||         
         " `^/\\|/"****||"/\|                   " `^/\\|/"****||"/\|         

View File

@@ -0,0 +1,71 @@
use std::time::Duration;
// Embed animation frames for each variant at compile time.
macro_rules! frames_for {
($dir:literal) => {
[
include_str!(concat!("../frames/", $dir, "/frame_1.txt")),
include_str!(concat!("../frames/", $dir, "/frame_2.txt")),
include_str!(concat!("../frames/", $dir, "/frame_3.txt")),
include_str!(concat!("../frames/", $dir, "/frame_4.txt")),
include_str!(concat!("../frames/", $dir, "/frame_5.txt")),
include_str!(concat!("../frames/", $dir, "/frame_6.txt")),
include_str!(concat!("../frames/", $dir, "/frame_7.txt")),
include_str!(concat!("../frames/", $dir, "/frame_8.txt")),
include_str!(concat!("../frames/", $dir, "/frame_9.txt")),
include_str!(concat!("../frames/", $dir, "/frame_10.txt")),
include_str!(concat!("../frames/", $dir, "/frame_11.txt")),
include_str!(concat!("../frames/", $dir, "/frame_12.txt")),
include_str!(concat!("../frames/", $dir, "/frame_13.txt")),
include_str!(concat!("../frames/", $dir, "/frame_14.txt")),
include_str!(concat!("../frames/", $dir, "/frame_15.txt")),
include_str!(concat!("../frames/", $dir, "/frame_16.txt")),
include_str!(concat!("../frames/", $dir, "/frame_17.txt")),
include_str!(concat!("../frames/", $dir, "/frame_18.txt")),
include_str!(concat!("../frames/", $dir, "/frame_19.txt")),
include_str!(concat!("../frames/", $dir, "/frame_20.txt")),
include_str!(concat!("../frames/", $dir, "/frame_21.txt")),
include_str!(concat!("../frames/", $dir, "/frame_22.txt")),
include_str!(concat!("../frames/", $dir, "/frame_23.txt")),
include_str!(concat!("../frames/", $dir, "/frame_24.txt")),
include_str!(concat!("../frames/", $dir, "/frame_25.txt")),
include_str!(concat!("../frames/", $dir, "/frame_26.txt")),
include_str!(concat!("../frames/", $dir, "/frame_27.txt")),
include_str!(concat!("../frames/", $dir, "/frame_28.txt")),
include_str!(concat!("../frames/", $dir, "/frame_29.txt")),
include_str!(concat!("../frames/", $dir, "/frame_30.txt")),
include_str!(concat!("../frames/", $dir, "/frame_31.txt")),
include_str!(concat!("../frames/", $dir, "/frame_32.txt")),
include_str!(concat!("../frames/", $dir, "/frame_33.txt")),
include_str!(concat!("../frames/", $dir, "/frame_34.txt")),
include_str!(concat!("../frames/", $dir, "/frame_35.txt")),
include_str!(concat!("../frames/", $dir, "/frame_36.txt")),
]
};
}
pub(crate) const FRAMES_DEFAULT: [&str; 36] = frames_for!("default");
pub(crate) const FRAMES_CODEX: [&str; 36] = frames_for!("codex");
pub(crate) const FRAMES_OPENAI: [&str; 36] = frames_for!("openai");
pub(crate) const FRAMES_BLOCKS: [&str; 36] = frames_for!("blocks");
pub(crate) const FRAMES_DOTS: [&str; 36] = frames_for!("dots");
pub(crate) const FRAMES_HASH: [&str; 36] = frames_for!("hash");
pub(crate) const FRAMES_HBARS: [&str; 36] = frames_for!("hbars");
pub(crate) const FRAMES_VBARS: [&str; 36] = frames_for!("vbars");
pub(crate) const FRAMES_SHAPES: [&str; 36] = frames_for!("shapes");
pub(crate) const FRAMES_SLUG: [&str; 36] = frames_for!("slug");
pub(crate) const ALL_VARIANTS: &[&[&str]] = &[
&FRAMES_DEFAULT,
&FRAMES_CODEX,
&FRAMES_OPENAI,
&FRAMES_BLOCKS,
&FRAMES_DOTS,
&FRAMES_HASH,
&FRAMES_HBARS,
&FRAMES_VBARS,
&FRAMES_SHAPES,
&FRAMES_SLUG,
];
pub(crate) const FRAME_TICK_DEFAULT: Duration = Duration::from_millis(80);

View File

@@ -42,6 +42,7 @@ pub mod custom_terminal;
mod diff_render; mod diff_render;
mod exec_command; mod exec_command;
mod file_search; mod file_search;
mod frames;
mod get_git_diff; mod get_git_diff;
mod history_cell; mod history_cell;
pub mod insert_history; pub mod insert_history;

View File

@@ -1,3 +1,5 @@
use crate::frames::ALL_VARIANTS as FRAME_VARIANTS;
use crate::frames::FRAME_TICK_DEFAULT;
use crate::tui::FrameRequester; use crate::tui::FrameRequester;
use crate::tui::Tui; use crate::tui::Tui;
use crate::tui::TuiEvent; use crate::tui::TuiEvent;
@@ -19,75 +21,7 @@ use ratatui::widgets::Wrap;
use std::time::Duration; use std::time::Duration;
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
// Embed animation frames for each variant at compile time. const FRAME_TICK: Duration = FRAME_TICK_DEFAULT;
macro_rules! frames_for {
($dir:literal) => {
[
include_str!(concat!("../frames/", $dir, "/frame_1.txt")),
include_str!(concat!("../frames/", $dir, "/frame_2.txt")),
include_str!(concat!("../frames/", $dir, "/frame_3.txt")),
include_str!(concat!("../frames/", $dir, "/frame_4.txt")),
include_str!(concat!("../frames/", $dir, "/frame_5.txt")),
include_str!(concat!("../frames/", $dir, "/frame_6.txt")),
include_str!(concat!("../frames/", $dir, "/frame_7.txt")),
include_str!(concat!("../frames/", $dir, "/frame_8.txt")),
include_str!(concat!("../frames/", $dir, "/frame_9.txt")),
include_str!(concat!("../frames/", $dir, "/frame_10.txt")),
include_str!(concat!("../frames/", $dir, "/frame_11.txt")),
include_str!(concat!("../frames/", $dir, "/frame_12.txt")),
include_str!(concat!("../frames/", $dir, "/frame_13.txt")),
include_str!(concat!("../frames/", $dir, "/frame_14.txt")),
include_str!(concat!("../frames/", $dir, "/frame_15.txt")),
include_str!(concat!("../frames/", $dir, "/frame_16.txt")),
include_str!(concat!("../frames/", $dir, "/frame_17.txt")),
include_str!(concat!("../frames/", $dir, "/frame_18.txt")),
include_str!(concat!("../frames/", $dir, "/frame_19.txt")),
include_str!(concat!("../frames/", $dir, "/frame_20.txt")),
include_str!(concat!("../frames/", $dir, "/frame_21.txt")),
include_str!(concat!("../frames/", $dir, "/frame_22.txt")),
include_str!(concat!("../frames/", $dir, "/frame_23.txt")),
include_str!(concat!("../frames/", $dir, "/frame_24.txt")),
include_str!(concat!("../frames/", $dir, "/frame_25.txt")),
include_str!(concat!("../frames/", $dir, "/frame_26.txt")),
include_str!(concat!("../frames/", $dir, "/frame_27.txt")),
include_str!(concat!("../frames/", $dir, "/frame_28.txt")),
include_str!(concat!("../frames/", $dir, "/frame_29.txt")),
include_str!(concat!("../frames/", $dir, "/frame_30.txt")),
include_str!(concat!("../frames/", $dir, "/frame_31.txt")),
include_str!(concat!("../frames/", $dir, "/frame_32.txt")),
include_str!(concat!("../frames/", $dir, "/frame_33.txt")),
include_str!(concat!("../frames/", $dir, "/frame_34.txt")),
include_str!(concat!("../frames/", $dir, "/frame_35.txt")),
include_str!(concat!("../frames/", $dir, "/frame_36.txt")),
]
};
}
const FRAMES_DEFAULT: [&str; 36] = frames_for!("default");
const FRAMES_CODEX: [&str; 36] = frames_for!("codex");
const FRAMES_OPENAI: [&str; 36] = frames_for!("openai");
const FRAMES_BLOCKS: [&str; 36] = frames_for!("blocks");
const FRAMES_DOTS: [&str; 36] = frames_for!("dots");
const FRAMES_HASH: [&str; 36] = frames_for!("hash");
const FRAMES_HBARS: [&str; 36] = frames_for!("hbars");
const FRAMES_VBARS: [&str; 36] = frames_for!("vbars");
const FRAMES_SHAPES: [&str; 36] = frames_for!("shapes");
const FRAMES_SLUG: [&str; 36] = frames_for!("slug");
const VARIANTS: &[&[&str]] = &[
&FRAMES_DEFAULT,
&FRAMES_CODEX,
&FRAMES_OPENAI,
&FRAMES_BLOCKS,
&FRAMES_DOTS,
&FRAMES_HASH,
&FRAMES_HBARS,
&FRAMES_VBARS,
&FRAMES_SHAPES,
&FRAMES_SLUG,
];
const FRAME_TICK: Duration = Duration::from_millis(60);
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ModelUpgradeDecision { pub(crate) enum ModelUpgradeDecision {
@@ -156,11 +90,11 @@ impl ModelUpgradePopup {
} }
fn frames(&self) -> &'static [&'static str] { fn frames(&self) -> &'static [&'static str] {
VARIANTS[self.variant_idx] FRAME_VARIANTS[self.variant_idx]
} }
fn pick_random_variant(&mut self) { fn pick_random_variant(&mut self) {
let total = VARIANTS.len(); let total = FRAME_VARIANTS.len();
if total <= 1 { if total <= 1 {
return; return;
} }
@@ -196,13 +130,11 @@ impl WidgetRef for &ModelUpgradePopup {
lines.push("".into()); lines.push("".into());
lines.push( lines.push(
format!( format!(" Codex is now powered by {SWIFTFOX_MODEL_DISPLAY_NAME}, a new model that is")
" Codex is now powered by {SWIFTFOX_MODEL_DISPLAY_NAME}, a new model that is" .into(),
)
.into(),
); );
lines.push(Line::from(vec![ lines.push(Line::from(vec![
" ".into(), " ".into(),
"faster, a better collaborator, ".bold(), "faster, a better collaborator, ".bold(),
"and ".into(), "and ".into(),
"more steerable.".bold(), "more steerable.".bold(),
@@ -226,6 +158,7 @@ impl WidgetRef for &ModelUpgradePopup {
ModelUpgradeOption::TryNewModel, ModelUpgradeOption::TryNewModel,
&format!("Yes, switch me to {SWIFTFOX_MODEL_DISPLAY_NAME}"), &format!("Yes, switch me to {SWIFTFOX_MODEL_DISPLAY_NAME}"),
)); ));
lines.push("".into());
lines.push(create_option( lines.push(create_option(
1, 1,
ModelUpgradeOption::KeepCurrent, ModelUpgradeOption::KeepCurrent,

View File

@@ -137,12 +137,12 @@ impl AuthModeWidget {
fn render_pick_mode(&self, area: Rect, buf: &mut Buffer) { fn render_pick_mode(&self, area: Rect, buf: &mut Buffer) {
let mut lines: Vec<Line> = vec![ let mut lines: Vec<Line> = vec![
Line::from(vec![ Line::from(vec![
"> ".into(), " ".into(),
"Sign in with ChatGPT to use Codex as part of your paid plan".bold(), "Sign in with ChatGPT to use Codex as part of your paid plan".into(),
]), ]),
Line::from(vec![ Line::from(vec![
" ".into(), " ".into(),
"or connect an API key for usage-based billing".bold(), "or connect an API key for usage-based billing".into(),
]), ]),
"".into(), "".into(),
]; ];
@@ -182,6 +182,7 @@ impl AuthModeWidget {
"Sign in with ChatGPT", "Sign in with ChatGPT",
"Usage included with Plus, Pro, and Team plans", "Usage included with Plus, Pro, and Team plans",
)); ));
lines.push("".into());
lines.extend(create_mode_item( lines.extend(create_mode_item(
1, 1,
AuthMode::ApiKey, AuthMode::ApiKey,
@@ -205,7 +206,7 @@ impl AuthModeWidget {
} }
fn render_continue_in_browser(&self, area: Rect, buf: &mut Buffer) { fn render_continue_in_browser(&self, area: Rect, buf: &mut Buffer) {
let mut spans = vec!["> ".into()]; let mut spans = vec![" ".into()];
// Schedule a follow-up frame to keep the shimmer animation going. // Schedule a follow-up frame to keep the shimmer animation going.
self.request_frame self.request_frame
.schedule_frame_in(std::time::Duration::from_millis(100)); .schedule_frame_in(std::time::Duration::from_millis(100));
@@ -217,7 +218,8 @@ impl AuthModeWidget {
&& !state.auth_url.is_empty() && !state.auth_url.is_empty()
{ {
lines.push(" If the link doesn't open automatically, open the following link to authenticate:".into()); lines.push(" If the link doesn't open automatically, open the following link to authenticate:".into());
lines.push(vec![" ".into(), state.auth_url.as_str().cyan().underlined()].into()); lines.push("".into());
lines.push(Line::from(state.auth_url.as_str().cyan().underlined()));
lines.push("".into()); lines.push("".into());
} }
@@ -231,7 +233,7 @@ impl AuthModeWidget {
let lines = vec![ let lines = vec![
"✓ Signed in with your ChatGPT account".fg(Color::Green).into(), "✓ Signed in with your ChatGPT account".fg(Color::Green).into(),
"".into(), "".into(),
"> Before you start:".into(), " Before you start:".into(),
"".into(), "".into(),
" Decide how much autonomy you want to grant Codex".into(), " Decide how much autonomy you want to grant Codex".into(),
Line::from(vec![ Line::from(vec![

View File

@@ -24,6 +24,7 @@ use crate::tui::TuiEvent;
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
use std::sync::Arc; use std::sync::Arc;
use std::sync::RwLock; use std::sync::RwLock;
use std::time::Instant;
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
enum Step { enum Step {
@@ -74,6 +75,8 @@ impl OnboardingScreen {
let codex_home = config.codex_home; let codex_home = config.codex_home;
let mut steps: Vec<Step> = vec![Step::Welcome(WelcomeWidget { let mut steps: Vec<Step> = vec![Step::Welcome(WelcomeWidget {
is_logged_in: !matches!(login_status, LoginStatus::NotAuthenticated), is_logged_in: !matches!(login_status, LoginStatus::NotAuthenticated),
request_frame: tui.frame_requester(),
start: Instant::now(),
})]; })];
if show_login_screen { if show_login_screen {
steps.push(Step::Auth(AuthModeWidget { steps.push(Step::Auth(AuthModeWidget {

View File

@@ -3,23 +3,62 @@ use ratatui::layout::Rect;
use ratatui::prelude::Widget; use ratatui::prelude::Widget;
use ratatui::style::Stylize; use ratatui::style::Stylize;
use ratatui::text::Line; use ratatui::text::Line;
use ratatui::widgets::Paragraph;
use ratatui::widgets::WidgetRef; use ratatui::widgets::WidgetRef;
use ratatui::widgets::Wrap;
use crate::frames::FRAME_TICK_DEFAULT;
use crate::frames::FRAMES_DEFAULT;
use crate::onboarding::onboarding_screen::StepStateProvider; use crate::onboarding::onboarding_screen::StepStateProvider;
use crate::tui::FrameRequester;
use super::onboarding_screen::StepState; use super::onboarding_screen::StepState;
use std::time::Duration;
use std::time::Instant;
const FRAME_TICK: Duration = FRAME_TICK_DEFAULT;
pub(crate) struct WelcomeWidget { pub(crate) struct WelcomeWidget {
pub is_logged_in: bool, pub is_logged_in: bool,
pub request_frame: FrameRequester,
pub start: Instant,
} }
impl WidgetRef for &WelcomeWidget { impl WidgetRef for &WelcomeWidget {
fn render_ref(&self, area: Rect, buf: &mut Buffer) { fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let line = Line::from(vec![ let elapsed_ms = self.start.elapsed().as_millis();
">_ ".into(),
"Welcome to Codex, OpenAI's command-line coding agent".bold(), // Align next draw to the next FRAME_TICK boundary to reduce jitter.
]); {
line.render(area, buf); let tick_ms = FRAME_TICK.as_millis();
let rem_ms = elapsed_ms % tick_ms;
let delay_ms = if rem_ms == 0 {
tick_ms
} else {
tick_ms - rem_ms
};
// Safe cast: delay_ms < tick_ms and FRAME_TICK is small.
self.request_frame
.schedule_frame_in(Duration::from_millis(delay_ms as u64));
}
let frames = &FRAMES_DEFAULT;
let idx = ((elapsed_ms / FRAME_TICK.as_millis()) % frames.len() as u128) as usize;
let mut lines: Vec<Line> = Vec::with_capacity(frames.len() + 2);
lines.extend(frames[idx].lines().map(|l| l.into()));
lines.push("".into());
lines.push(Line::from(vec![
" ".into(),
"Welcome to ".into(),
"Codex".bold(),
", OpenAI's command-line coding agent".into(),
]));
Paragraph::new(lines)
.wrap(Wrap { trim: false })
.render(area, buf);
} }
} }
@@ -31,3 +70,14 @@ impl StepStateProvider for WelcomeWidget {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
/// A number of things break down if FRAME_TICK is zero.
#[test]
fn frame_tick_must_be_nonzero() {
assert!(FRAME_TICK.as_millis() > 0);
}
}