Files
codex/prs/bolinfest/PR-1713.md
2025-09-02 15:17:45 -07:00

20 KiB
Raw Blame History

PR #1713: replace login screen with a simple prompt

Description

Perhaps there was an intention to make the login screen prettier, but it feels quite silly right now to just have a screen that says "press q", so replace it with something that lets the user directly login without having to quit the app.

Screenshot 2025-07-28 at 2 54 05 PM

Full Diff

diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs
index efda03bda4..6dd596ff9f 100644
--- a/codex-rs/cli/src/main.rs
+++ b/codex-rs/cli/src/main.rs
@@ -106,7 +106,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
         None => {
             let mut tui_cli = cli.interactive;
             prepend_config_flags(&mut tui_cli.config_overrides, cli.config_overrides);
-            let usage = codex_tui::run_main(tui_cli, codex_linux_sandbox_exe)?;
+            let usage = codex_tui::run_main(tui_cli, codex_linux_sandbox_exe).await?;
             println!("{}", codex_core::protocol::FinalOutput::from(usage));
         }
         Some(Subcommand::Exec(mut exec_cli)) => {
diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs
index 99d2f7f983..ab92ecf616 100644
--- a/codex-rs/login/src/lib.rs
+++ b/codex-rs/login/src/lib.rs
@@ -9,6 +9,7 @@ use std::io::Write;
 use std::os::unix::fs::OpenOptionsExt;
 use std::path::Path;
 use std::process::Stdio;
+use std::time::Duration;
 use tokio::process::Command;
 
 const SOURCE_FOR_PYTHON_SERVER: &str = include_str!("./login_with_chatgpt.py");
@@ -73,7 +74,11 @@ pub async fn try_read_auth_json(codex_home: &Path) -> std::io::Result<AuthDotJso
     let auth_dot_json: AuthDotJson = serde_json::from_str(&contents)?;
 
     if is_expired(&auth_dot_json) {
-        let refresh_response = try_refresh_token(&auth_dot_json).await?;
+        let refresh_response =
+            tokio::time::timeout(Duration::from_secs(60), try_refresh_token(&auth_dot_json))
+                .await
+                .map_err(|_| std::io::Error::other("timed out while refreshing OpenAI API key"))?
+                .map_err(std::io::Error::other)?;
         let mut auth_dot_json = auth_dot_json;
         auth_dot_json.tokens.id_token = refresh_response.id_token;
         if let Some(refresh_token) = refresh_response.refresh_token {
diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs
index e7097e6af0..b671075ba8 100644
--- a/codex-rs/tui/src/app.rs
+++ b/codex-rs/tui/src/app.rs
@@ -5,7 +5,6 @@ use crate::file_search::FileSearchManager;
 use crate::get_git_diff::get_git_diff;
 use crate::git_warning_screen::GitWarningOutcome;
 use crate::git_warning_screen::GitWarningScreen;
-use crate::login_screen::LoginScreen;
 use crate::scroll_event_helper::ScrollEventHelper;
 use crate::slash_command::SlashCommand;
 use crate::tui;
@@ -37,8 +36,6 @@ enum AppState<'a> {
         /// `AppState`.
         widget: Box<ChatWidget<'a>>,
     },
-    /// The login screen for the OpenAI provider.
-    Login { screen: LoginScreen },
     /// The start-up warning that recommends running codex inside a Git repo.
     GitWarning { screen: GitWarningScreen },
 }
@@ -74,7 +71,6 @@ impl App<'_> {
     pub(crate) fn new(
         config: Config,
         initial_prompt: Option<String>,
-        show_login_screen: bool,
         show_git_warning: bool,
         initial_images: Vec<std::path::PathBuf>,
     ) -> Self {
@@ -138,18 +134,7 @@ impl App<'_> {
             });
         }
 
-        let (app_state, chat_args) = if show_login_screen {
-            (
-                AppState::Login {
-                    screen: LoginScreen::new(app_event_tx.clone(), config.codex_home.clone()),
-                },
-                Some(ChatWidgetArgs {
-                    config: config.clone(),
-                    initial_prompt,
-                    initial_images,
-                }),
-            )
-        } else if show_git_warning {
+        let (app_state, chat_args) = if show_git_warning {
             (
                 AppState::GitWarning {
                     screen: GitWarningScreen::new(),
@@ -243,7 +228,7 @@ impl App<'_> {
                                 AppState::Chat { widget } => {
                                     widget.on_ctrl_c();
                                 }
-                                AppState::Login { .. } | AppState::GitWarning { .. } => {
+                                AppState::GitWarning { .. } => {
                                     // No-op.
                                 }
                             }
@@ -264,7 +249,7 @@ impl App<'_> {
                                         self.dispatch_key_event(key_event);
                                     }
                                 }
-                                AppState::Login { .. } | AppState::GitWarning { .. } => {
+                                AppState::GitWarning { .. } => {
                                     self.app_event_tx.send(AppEvent::ExitRequest);
                                 }
                             }
@@ -288,11 +273,11 @@ impl App<'_> {
                 }
                 AppEvent::CodexOp(op) => match &mut self.app_state {
                     AppState::Chat { widget } => widget.submit_op(op),
-                    AppState::Login { .. } | AppState::GitWarning { .. } => {}
+                    AppState::GitWarning { .. } => {}
                 },
                 AppEvent::LatestLog(line) => match &mut self.app_state {
                     AppState::Chat { widget } => widget.update_latest_log(line),
-                    AppState::Login { .. } | AppState::GitWarning { .. } => {}
+                    AppState::GitWarning { .. } => {}
                 },
                 AppEvent::DispatchCommand(command) => match command {
                     SlashCommand::New => {
@@ -348,9 +333,7 @@ impl App<'_> {
     pub(crate) fn token_usage(&self) -> codex_core::protocol::TokenUsage {
         match &self.app_state {
             AppState::Chat { widget } => widget.token_usage().clone(),
-            AppState::Login { .. } | AppState::GitWarning { .. } => {
-                codex_core::protocol::TokenUsage::default()
-            }
+            AppState::GitWarning { .. } => codex_core::protocol::TokenUsage::default(),
         }
     }
 
@@ -361,9 +344,6 @@ impl App<'_> {
             AppState::Chat { widget } => {
                 terminal.draw(|frame| frame.render_widget_ref(&**widget, frame.area()))?;
             }
-            AppState::Login { screen } => {
-                terminal.draw(|frame| frame.render_widget_ref(&*screen, frame.area()))?;
-            }
             AppState::GitWarning { screen } => {
                 terminal.draw(|frame| frame.render_widget_ref(&*screen, frame.area()))?;
             }
@@ -378,7 +358,6 @@ impl App<'_> {
             AppState::Chat { widget } => {
                 widget.handle_key_event(key_event);
             }
-            AppState::Login { screen } => screen.handle_key_event(key_event),
             AppState::GitWarning { screen } => match screen.handle_key_event(key_event) {
                 GitWarningOutcome::Continue => {
                     // User accepted  switch to chat view.
@@ -409,21 +388,21 @@ impl App<'_> {
     fn dispatch_paste_event(&mut self, pasted: String) {
         match &mut self.app_state {
             AppState::Chat { widget } => widget.handle_paste(pasted),
-            AppState::Login { .. } | AppState::GitWarning { .. } => {}
+            AppState::GitWarning { .. } => {}
         }
     }
 
     fn dispatch_scroll_event(&mut self, scroll_delta: i32) {
         match &mut self.app_state {
             AppState::Chat { widget } => widget.handle_scroll_delta(scroll_delta),
-            AppState::Login { .. } | AppState::GitWarning { .. } => {}
+            AppState::GitWarning { .. } => {}
         }
     }
 
     fn dispatch_codex_event(&mut self, event: Event) {
         match &mut self.app_state {
             AppState::Chat { widget } => widget.handle_codex_event(event),
-            AppState::Login { .. } | AppState::GitWarning { .. } => {}
+            AppState::GitWarning { .. } => {}
         }
     }
 }
diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs
index 905f0aaf0b..1f660b1aaf 100644
--- a/codex-rs/tui/src/lib.rs
+++ b/codex-rs/tui/src/lib.rs
@@ -14,6 +14,7 @@ use codex_core::util::is_inside_git_repo;
 use codex_login::try_read_openai_api_key;
 use log_layer::TuiLogLayer;
 use std::fs::OpenOptions;
+use std::io::Write;
 use std::path::PathBuf;
 use tracing_appender::non_blocking;
 use tracing_subscriber::EnvFilter;
@@ -35,7 +36,6 @@ mod git_warning_screen;
 mod history_cell;
 mod insert_history;
 mod log_layer;
-mod login_screen;
 mod markdown;
 mod scroll_event_helper;
 mod slash_command;
@@ -47,7 +47,7 @@ mod user_approval_widget;
 
 pub use cli::Cli;
 
-pub fn run_main(
+pub async fn run_main(
     cli: Cli,
     codex_linux_sandbox_exe: Option<PathBuf>,
 ) -> std::io::Result<codex_core::protocol::TokenUsage> {
@@ -142,7 +142,25 @@ pub fn run_main(
         .with(tui_layer)
         .try_init();
 
-    let show_login_screen = should_show_login_screen(&config);
+    let show_login_screen = should_show_login_screen(&config).await;
+    if show_login_screen {
+        std::io::stdout().write_all(
+            b"Oh dear, we don't seem to have an API key.\nTerribly sorry, but may I open a browser window for you to log in? [Yn] ",
+        )?;
+        std::io::stdout().flush()?;
+        let mut input = String::new();
+        std::io::stdin().read_line(&mut input)?;
+        let trimmed = input.trim();
+        if !(trimmed.is_empty() || trimmed.eq_ignore_ascii_case("y")) {
+            std::io::stdout().write_all(b"Right-o, fair enough. See you next time!\n")?;
+            std::process::exit(1);
+        }
+        // Spawn a task to run the login command.
+        // Block until the login command is finished.
+        let new_key = codex_login::login_with_chatgpt(&config.codex_home, false).await?;
+        set_openai_api_key(new_key);
+        std::io::stdout().write_all(b"Excellent, looks like that worked. Let's get started!\n")?;
+    }
 
     // Determine whether we need to display the "not a git repo" warning
     // modal. The flag is shown when the current working directory is *not*
@@ -150,14 +168,13 @@ pub fn run_main(
     // `--allow-no-git-exec` flag.
     let show_git_warning = !cli.skip_git_repo_check && !is_inside_git_repo(&config);
 
-    run_ratatui_app(cli, config, show_login_screen, show_git_warning, log_rx)
+    run_ratatui_app(cli, config, show_git_warning, log_rx)
         .map_err(|err| std::io::Error::other(err.to_string()))
 }
 
 fn run_ratatui_app(
     cli: Cli,
     config: Config,
-    show_login_screen: bool,
     show_git_warning: bool,
     mut log_rx: tokio::sync::mpsc::UnboundedReceiver<String>,
 ) -> color_eyre::Result<codex_core::protocol::TokenUsage> {
@@ -172,13 +189,7 @@ fn run_ratatui_app(
     terminal.clear()?;
 
     let Cli { prompt, images, .. } = cli;
-    let mut app = App::new(
-        config.clone(),
-        prompt,
-        show_login_screen,
-        show_git_warning,
-        images,
-    );
+    let mut app = App::new(config.clone(), prompt, show_git_warning, images);
 
     // Bridge log receiver into the AppEvent channel so latest log lines update the UI.
     {
@@ -210,26 +221,17 @@ fn restore() {
     }
 }
 
-#[allow(clippy::unwrap_used)]
-fn should_show_login_screen(config: &Config) -> bool {
+async fn should_show_login_screen(config: &Config) -> bool {
     if is_in_need_of_openai_api_key(config) {
         // Reading the OpenAI API key is an async operation because it may need
         // to refresh the token. Block on it.
         let codex_home = config.codex_home.clone();
-        let (tx, rx) = tokio::sync::oneshot::channel();
-        tokio::spawn(async move {
-            match try_read_openai_api_key(&codex_home).await {
-                Ok(openai_api_key) => {
-                    set_openai_api_key(openai_api_key);
-                    tx.send(false).unwrap();
-                }
-                Err(_) => {
-                    tx.send(true).unwrap();
-                }
-            }
-        });
-        // TODO(mbolin): Impose some sort of timeout.
-        tokio::task::block_in_place(|| rx.blocking_recv()).unwrap()
+        if let Ok(openai_api_key) = try_read_openai_api_key(&codex_home).await {
+            set_openai_api_key(openai_api_key);
+            false
+        } else {
+            true
+        }
     } else {
         false
     }
diff --git a/codex-rs/tui/src/login_screen.rs b/codex-rs/tui/src/login_screen.rs
deleted file mode 100644
index 1bd11c19d3..0000000000
--- a/codex-rs/tui/src/login_screen.rs
+++ /dev/null
@@ -1,46 +0,0 @@
-use std::path::PathBuf;
-
-use crossterm::event::KeyCode;
-use crossterm::event::KeyEvent;
-use ratatui::buffer::Buffer;
-use ratatui::layout::Rect;
-use ratatui::widgets::Paragraph;
-use ratatui::widgets::Widget as _;
-use ratatui::widgets::WidgetRef;
-
-use crate::app_event::AppEvent;
-use crate::app_event_sender::AppEventSender;
-
-pub(crate) struct LoginScreen {
-    app_event_tx: AppEventSender,
-
-    /// Use this with login_with_chatgpt() in login/src/lib.rs and, if
-    /// successful, update the in-memory config via
-    /// codex_core::openai_api_key::set_openai_api_key().
-    #[allow(dead_code)]
-    codex_home: PathBuf,
-}
-
-impl LoginScreen {
-    pub(crate) fn new(app_event_tx: AppEventSender, codex_home: PathBuf) -> Self {
-        Self {
-            app_event_tx,
-            codex_home,
-        }
-    }
-
-    pub(crate) fn handle_key_event(&mut self, key_event: KeyEvent) {
-        if let KeyCode::Char('q') = key_event.code {
-            self.app_event_tx.send(AppEvent::ExitRequest);
-        }
-    }
-}
-
-impl WidgetRef for &LoginScreen {
-    fn render_ref(&self, area: Rect, buf: &mut Buffer) {
-        let text = Paragraph::new(
-            "Login using `codex login` and then run this command again. 'q' to quit.",
-        );
-        text.render(area, buf);
-    }
-}
diff --git a/codex-rs/tui/src/main.rs b/codex-rs/tui/src/main.rs
index 480e56e88e..209febf035 100644
--- a/codex-rs/tui/src/main.rs
+++ b/codex-rs/tui/src/main.rs
@@ -21,7 +21,7 @@ fn main() -> anyhow::Result<()> {
             .config_overrides
             .raw_overrides
             .splice(0..0, top_cli.config_overrides.raw_overrides);
-        let usage = run_main(inner, codex_linux_sandbox_exe)?;
+        let usage = run_main(inner, codex_linux_sandbox_exe).await?;
         println!("{}", codex_core::protocol::FinalOutput::from(usage));
         Ok(())
     })

Review Comments

codex-rs/tui/src/lib.rs

@@ -142,22 +142,39 @@ pub fn run_main(
         .with(tui_layer)
         .try_init();
 
-    let show_login_screen = should_show_login_screen(&config);
+    let show_login_screen = should_show_login_screen(&config).await;
+    if show_login_screen {
+        std::io::stdout().write_all(
+            b"Oh dear, we don't seem to have an API key.\nTerribly sorry, but may I open a browser window for you to log in? [Yn] ",
+        )?;
+        std::io::stdout().flush()?;
+        let mut input = String::new();
+        std::io::stdin().read_line(&mut input)?;
+        let trimmed = input.trim();
+        if !(trimmed.is_empty() || trimmed.eq_ignore_ascii_case("y")) {

My mind wants to deMorgan this to...

        if !trimmed.is_empty() && !trimmed.eq_ignore_ascii_case("y") {
@@ -47,7 +47,7 @@ mod user_approval_widget;
 
 pub use cli::Cli;
 
-pub fn run_main(
+pub async fn run_main(

Hmm, so I wasn't clear on how async-friendly Ratatui is, which is why I avoided it here.

I guess this is fine?

@@ -211,25 +222,23 @@ fn restore() {
 }
 
 #[allow(clippy::unwrap_used)]
-fn should_show_login_screen(config: &Config) -> bool {
+async fn should_show_login_screen(config: &Config) -> bool {
     if is_in_need_of_openai_api_key(config) {
         // Reading the OpenAI API key is an async operation because it may need
         // to refresh the token. Block on it.
         let codex_home = config.codex_home.clone();
         let (tx, rx) = tokio::sync::oneshot::channel();
-        tokio::spawn(async move {
-            match try_read_openai_api_key(&codex_home).await {
-                Ok(openai_api_key) => {
-                    set_openai_api_key(openai_api_key);
-                    tx.send(false).unwrap();
-                }
-                Err(_) => {
-                    tx.send(true).unwrap();
-                }
+        match try_read_openai_api_key(&codex_home).await {

If you aren't going to tokio::spawn(), we don't need the oneshot, do we?

@@ -211,25 +222,23 @@ fn restore() {
 }
 
 #[allow(clippy::unwrap_used)]
-fn should_show_login_screen(config: &Config) -> bool {
+async fn should_show_login_screen(config: &Config) -> bool {
     if is_in_need_of_openai_api_key(config) {
         // Reading the OpenAI API key is an async operation because it may need
         // to refresh the token. Block on it.
         let codex_home = config.codex_home.clone();
         let (tx, rx) = tokio::sync::oneshot::channel();
-        tokio::spawn(async move {
-            match try_read_openai_api_key(&codex_home).await {
-                Ok(openai_api_key) => {
-                    set_openai_api_key(openai_api_key);
-                    tx.send(false).unwrap();
-                }
-                Err(_) => {
-                    tx.send(true).unwrap();
-                }
+        match try_read_openai_api_key(&codex_home).await {
+            Ok(openai_api_key) => {
+                set_openai_api_key(openai_api_key);
+                tx.send(false).unwrap();
             }
-        });
+            Err(_) => {
+                tx.send(true).unwrap();
+            }
+        }
         // TODO(mbolin): Impose some sort of timeout.
-        tokio::task::block_in_place(|| rx.blocking_recv()).unwrap()
+        rx.await.unwrap()

You can leverage tokio::time::timeout pretty easily to address this TODO.

@@ -210,26 +222,25 @@ fn restore() {
     }
 }
 
-#[allow(clippy::unwrap_used)]
-fn should_show_login_screen(config: &Config) -> bool {
+#[allow(clippy::expect_used)]
+#[allow(clippy::print_stderr)]
+async fn should_show_login_screen(config: &Config) -> bool {
     if is_in_need_of_openai_api_key(config) {
         // Reading the OpenAI API key is an async operation because it may need
         // to refresh the token. Block on it.
         let codex_home = config.codex_home.clone();
-        let (tx, rx) = tokio::sync::oneshot::channel();
-        tokio::spawn(async move {
-            match try_read_openai_api_key(&codex_home).await {
-                Ok(openai_api_key) => {
-                    set_openai_api_key(openai_api_key);
-                    tx.send(false).unwrap();
-                }
-                Err(_) => {
-                    tx.send(true).unwrap();
-                }
-            }
-        });
-        // TODO(mbolin): Impose some sort of timeout.
-        tokio::task::block_in_place(|| rx.blocking_recv()).unwrap()
+        if let Ok(openai_api_key) = tokio::time::timeout(
+            Duration::from_secs(60),
+            try_read_openai_api_key(&codex_home),
+        )
+        .await
+        .expect("timed out while refreshing OpenAI API key")

If we timeout, we don't want to panic, do we?

@@ -210,26 +221,19 @@ fn restore() {
     }
 }
 
-#[allow(clippy::unwrap_used)]
-fn should_show_login_screen(config: &Config) -> bool {
+#[allow(clippy::expect_used)]

These are no longer necessary, correct?