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 exec_command;
mod file_search;
mod frames;
mod get_git_diff;
mod history_cell;
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::Tui;
use crate::tui::TuiEvent;
@@ -19,75 +21,7 @@ use ratatui::widgets::Wrap;
use std::time::Duration;
use tokio_stream::StreamExt;
// 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")),
]
};
}
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);
const FRAME_TICK: Duration = FRAME_TICK_DEFAULT;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ModelUpgradeDecision {
@@ -156,11 +90,11 @@ impl ModelUpgradePopup {
}
fn frames(&self) -> &'static [&'static str] {
VARIANTS[self.variant_idx]
FRAME_VARIANTS[self.variant_idx]
}
fn pick_random_variant(&mut self) {
let total = VARIANTS.len();
let total = FRAME_VARIANTS.len();
if total <= 1 {
return;
}
@@ -196,13 +130,11 @@ impl WidgetRef for &ModelUpgradePopup {
lines.push("".into());
lines.push(
format!(
" Codex is now powered by {SWIFTFOX_MODEL_DISPLAY_NAME}, a new model that is"
)
.into(),
format!(" Codex is now powered by {SWIFTFOX_MODEL_DISPLAY_NAME}, a new model that is")
.into(),
);
lines.push(Line::from(vec![
" ".into(),
" ".into(),
"faster, a better collaborator, ".bold(),
"and ".into(),
"more steerable.".bold(),
@@ -226,6 +158,7 @@ impl WidgetRef for &ModelUpgradePopup {
ModelUpgradeOption::TryNewModel,
&format!("Yes, switch me to {SWIFTFOX_MODEL_DISPLAY_NAME}"),
));
lines.push("".into());
lines.push(create_option(
1,
ModelUpgradeOption::KeepCurrent,

View File

@@ -137,12 +137,12 @@ impl AuthModeWidget {
fn render_pick_mode(&self, area: Rect, buf: &mut Buffer) {
let mut lines: Vec<Line> = vec![
Line::from(vec![
"> ".into(),
"Sign in with ChatGPT to use Codex as part of your paid plan".bold(),
" ".into(),
"Sign in with ChatGPT to use Codex as part of your paid plan".into(),
]),
Line::from(vec![
" ".into(),
"or connect an API key for usage-based billing".bold(),
"or connect an API key for usage-based billing".into(),
]),
"".into(),
];
@@ -182,6 +182,7 @@ impl AuthModeWidget {
"Sign in with ChatGPT",
"Usage included with Plus, Pro, and Team plans",
));
lines.push("".into());
lines.extend(create_mode_item(
1,
AuthMode::ApiKey,
@@ -205,7 +206,7 @@ impl AuthModeWidget {
}
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.
self.request_frame
.schedule_frame_in(std::time::Duration::from_millis(100));
@@ -217,7 +218,8 @@ impl AuthModeWidget {
&& !state.auth_url.is_empty()
{
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());
}
@@ -231,7 +233,7 @@ impl AuthModeWidget {
let lines = vec![
"✓ Signed in with your ChatGPT account".fg(Color::Green).into(),
"".into(),
"> Before you start:".into(),
" Before you start:".into(),
"".into(),
" Decide how much autonomy you want to grant Codex".into(),
Line::from(vec![

View File

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

View File

@@ -3,23 +3,62 @@ use ratatui::layout::Rect;
use ratatui::prelude::Widget;
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::widgets::Paragraph;
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::tui::FrameRequester;
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 is_logged_in: bool,
pub request_frame: FrameRequester,
pub start: Instant,
}
impl WidgetRef for &WelcomeWidget {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let line = Line::from(vec![
">_ ".into(),
"Welcome to Codex, OpenAI's command-line coding agent".bold(),
]);
line.render(area, buf);
let elapsed_ms = self.start.elapsed().as_millis();
// Align next draw to the next FRAME_TICK boundary to reduce jitter.
{
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);
}
}