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(()) })