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

544 lines
20 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PR #1713: replace login screen with a simple prompt
- URL: https://github.com/openai/codex/pull/1713
- Author: nornagon-openai
- Created: 2025-07-28 21:44:12 UTC
- Updated: 2025-07-29 00:25:21 UTC
- Changes: +47/-107, Files changed: 6, Commits: 7
## 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.
<img width="1283" height="635" alt="Screenshot 2025-07-28 at 2 54 05PM" src="https://github.com/user-attachments/assets/f19e5595-6ef9-4a2d-b409-aa61b30d3628" />
## Full Diff
```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
- Created: 2025-07-28 22:22:11 UTC | Link: https://github.com/openai/codex/pull/1713#discussion_r2237962280
```diff
@@ -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...
>
> ```suggestion
> if !trimmed.is_empty() && !trimmed.eq_ignore_ascii_case("y") {
> ```
- Created: 2025-07-28 22:29:13 UTC | Link: https://github.com/openai/codex/pull/1713#discussion_r2237969504
```diff
@@ -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?
- Created: 2025-07-28 22:30:21 UTC | Link: https://github.com/openai/codex/pull/1713#discussion_r2237970644
```diff
@@ -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?
- Created: 2025-07-28 22:31:25 UTC | Link: https://github.com/openai/codex/pull/1713#discussion_r2237971705
```diff
@@ -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.
- Created: 2025-07-28 23:49:20 UTC | Link: https://github.com/openai/codex/pull/1713#discussion_r2238040525
```diff
@@ -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?
- Created: 2025-07-29 00:07:38 UTC | Link: https://github.com/openai/codex/pull/1713#discussion_r2238055661
```diff
@@ -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?