From f66704a88f3415c84c985aa3feab09e95178bc8a Mon Sep 17 00:00:00 2001
From: Jeremy Rose <172423086+nornagon-openai@users.noreply.github.com>
Date: Mon, 28 Jul 2025 17:25:14 -0700
Subject: [PATCH] replace login screen with a simple prompt (#1713)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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.
---
codex-rs/cli/src/main.rs | 2 +-
codex-rs/login/src/lib.rs | 7 +++-
codex-rs/tui/src/app.rs | 39 +++++----------------
codex-rs/tui/src/lib.rs | 58 +++++++++++++++++---------------
codex-rs/tui/src/login_screen.rs | 46 -------------------------
codex-rs/tui/src/main.rs | 2 +-
6 files changed, 47 insertions(+), 107 deletions(-)
delete mode 100644 codex-rs/tui/src/login_screen.rs
diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs
index efda03bd..6dd596ff 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) -> 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 99d2f7f9..ab92ecf6 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 {
/// `AppState`.
widget: Box>,
},
- /// 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,
- show_login_screen: bool,
show_git_warning: bool,
initial_images: Vec,
) -> 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 905f0aaf..1f660b1a 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,
) -> std::io::Result {
@@ -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,
) -> color_eyre::Result {
@@ -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 1bd11c19..00000000
--- 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 480e56e8..209febf0 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(())
})