replace login screen with a simple prompt (#1713)
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 05 PM" src="https://github.com/user-attachments/assets/f19e5595-6ef9-4a2d-b409-aa61b30d3628" />
This commit is contained in:
@@ -106,7 +106,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
|
|||||||
None => {
|
None => {
|
||||||
let mut tui_cli = cli.interactive;
|
let mut tui_cli = cli.interactive;
|
||||||
prepend_config_flags(&mut tui_cli.config_overrides, cli.config_overrides);
|
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));
|
println!("{}", codex_core::protocol::FinalOutput::from(usage));
|
||||||
}
|
}
|
||||||
Some(Subcommand::Exec(mut exec_cli)) => {
|
Some(Subcommand::Exec(mut exec_cli)) => {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use std::io::Write;
|
|||||||
use std::os::unix::fs::OpenOptionsExt;
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
|
use std::time::Duration;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
const SOURCE_FOR_PYTHON_SERVER: &str = include_str!("./login_with_chatgpt.py");
|
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)?;
|
let auth_dot_json: AuthDotJson = serde_json::from_str(&contents)?;
|
||||||
|
|
||||||
if is_expired(&auth_dot_json) {
|
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;
|
let mut auth_dot_json = auth_dot_json;
|
||||||
auth_dot_json.tokens.id_token = refresh_response.id_token;
|
auth_dot_json.tokens.id_token = refresh_response.id_token;
|
||||||
if let Some(refresh_token) = refresh_response.refresh_token {
|
if let Some(refresh_token) = refresh_response.refresh_token {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ use crate::file_search::FileSearchManager;
|
|||||||
use crate::get_git_diff::get_git_diff;
|
use crate::get_git_diff::get_git_diff;
|
||||||
use crate::git_warning_screen::GitWarningOutcome;
|
use crate::git_warning_screen::GitWarningOutcome;
|
||||||
use crate::git_warning_screen::GitWarningScreen;
|
use crate::git_warning_screen::GitWarningScreen;
|
||||||
use crate::login_screen::LoginScreen;
|
|
||||||
use crate::scroll_event_helper::ScrollEventHelper;
|
use crate::scroll_event_helper::ScrollEventHelper;
|
||||||
use crate::slash_command::SlashCommand;
|
use crate::slash_command::SlashCommand;
|
||||||
use crate::tui;
|
use crate::tui;
|
||||||
@@ -37,8 +36,6 @@ enum AppState<'a> {
|
|||||||
/// `AppState`.
|
/// `AppState`.
|
||||||
widget: Box<ChatWidget<'a>>,
|
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.
|
/// The start-up warning that recommends running codex inside a Git repo.
|
||||||
GitWarning { screen: GitWarningScreen },
|
GitWarning { screen: GitWarningScreen },
|
||||||
}
|
}
|
||||||
@@ -74,7 +71,6 @@ impl App<'_> {
|
|||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
config: Config,
|
config: Config,
|
||||||
initial_prompt: Option<String>,
|
initial_prompt: Option<String>,
|
||||||
show_login_screen: bool,
|
|
||||||
show_git_warning: bool,
|
show_git_warning: bool,
|
||||||
initial_images: Vec<std::path::PathBuf>,
|
initial_images: Vec<std::path::PathBuf>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -138,18 +134,7 @@ impl App<'_> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let (app_state, chat_args) = if show_login_screen {
|
let (app_state, chat_args) = if show_git_warning {
|
||||||
(
|
|
||||||
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 {
|
|
||||||
(
|
(
|
||||||
AppState::GitWarning {
|
AppState::GitWarning {
|
||||||
screen: GitWarningScreen::new(),
|
screen: GitWarningScreen::new(),
|
||||||
@@ -243,7 +228,7 @@ impl App<'_> {
|
|||||||
AppState::Chat { widget } => {
|
AppState::Chat { widget } => {
|
||||||
widget.on_ctrl_c();
|
widget.on_ctrl_c();
|
||||||
}
|
}
|
||||||
AppState::Login { .. } | AppState::GitWarning { .. } => {
|
AppState::GitWarning { .. } => {
|
||||||
// No-op.
|
// No-op.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,7 +249,7 @@ impl App<'_> {
|
|||||||
self.dispatch_key_event(key_event);
|
self.dispatch_key_event(key_event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AppState::Login { .. } | AppState::GitWarning { .. } => {
|
AppState::GitWarning { .. } => {
|
||||||
self.app_event_tx.send(AppEvent::ExitRequest);
|
self.app_event_tx.send(AppEvent::ExitRequest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,11 +273,11 @@ impl App<'_> {
|
|||||||
}
|
}
|
||||||
AppEvent::CodexOp(op) => match &mut self.app_state {
|
AppEvent::CodexOp(op) => match &mut self.app_state {
|
||||||
AppState::Chat { widget } => widget.submit_op(op),
|
AppState::Chat { widget } => widget.submit_op(op),
|
||||||
AppState::Login { .. } | AppState::GitWarning { .. } => {}
|
AppState::GitWarning { .. } => {}
|
||||||
},
|
},
|
||||||
AppEvent::LatestLog(line) => match &mut self.app_state {
|
AppEvent::LatestLog(line) => match &mut self.app_state {
|
||||||
AppState::Chat { widget } => widget.update_latest_log(line),
|
AppState::Chat { widget } => widget.update_latest_log(line),
|
||||||
AppState::Login { .. } | AppState::GitWarning { .. } => {}
|
AppState::GitWarning { .. } => {}
|
||||||
},
|
},
|
||||||
AppEvent::DispatchCommand(command) => match command {
|
AppEvent::DispatchCommand(command) => match command {
|
||||||
SlashCommand::New => {
|
SlashCommand::New => {
|
||||||
@@ -348,9 +333,7 @@ impl App<'_> {
|
|||||||
pub(crate) fn token_usage(&self) -> codex_core::protocol::TokenUsage {
|
pub(crate) fn token_usage(&self) -> codex_core::protocol::TokenUsage {
|
||||||
match &self.app_state {
|
match &self.app_state {
|
||||||
AppState::Chat { widget } => widget.token_usage().clone(),
|
AppState::Chat { widget } => widget.token_usage().clone(),
|
||||||
AppState::Login { .. } | AppState::GitWarning { .. } => {
|
AppState::GitWarning { .. } => codex_core::protocol::TokenUsage::default(),
|
||||||
codex_core::protocol::TokenUsage::default()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,9 +344,6 @@ impl App<'_> {
|
|||||||
AppState::Chat { widget } => {
|
AppState::Chat { widget } => {
|
||||||
terminal.draw(|frame| frame.render_widget_ref(&**widget, frame.area()))?;
|
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 } => {
|
AppState::GitWarning { screen } => {
|
||||||
terminal.draw(|frame| frame.render_widget_ref(&*screen, frame.area()))?;
|
terminal.draw(|frame| frame.render_widget_ref(&*screen, frame.area()))?;
|
||||||
}
|
}
|
||||||
@@ -378,7 +358,6 @@ impl App<'_> {
|
|||||||
AppState::Chat { widget } => {
|
AppState::Chat { widget } => {
|
||||||
widget.handle_key_event(key_event);
|
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) {
|
AppState::GitWarning { screen } => match screen.handle_key_event(key_event) {
|
||||||
GitWarningOutcome::Continue => {
|
GitWarningOutcome::Continue => {
|
||||||
// User accepted – switch to chat view.
|
// User accepted – switch to chat view.
|
||||||
@@ -409,21 +388,21 @@ impl App<'_> {
|
|||||||
fn dispatch_paste_event(&mut self, pasted: String) {
|
fn dispatch_paste_event(&mut self, pasted: String) {
|
||||||
match &mut self.app_state {
|
match &mut self.app_state {
|
||||||
AppState::Chat { widget } => widget.handle_paste(pasted),
|
AppState::Chat { widget } => widget.handle_paste(pasted),
|
||||||
AppState::Login { .. } | AppState::GitWarning { .. } => {}
|
AppState::GitWarning { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_scroll_event(&mut self, scroll_delta: i32) {
|
fn dispatch_scroll_event(&mut self, scroll_delta: i32) {
|
||||||
match &mut self.app_state {
|
match &mut self.app_state {
|
||||||
AppState::Chat { widget } => widget.handle_scroll_delta(scroll_delta),
|
AppState::Chat { widget } => widget.handle_scroll_delta(scroll_delta),
|
||||||
AppState::Login { .. } | AppState::GitWarning { .. } => {}
|
AppState::GitWarning { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_codex_event(&mut self, event: Event) {
|
fn dispatch_codex_event(&mut self, event: Event) {
|
||||||
match &mut self.app_state {
|
match &mut self.app_state {
|
||||||
AppState::Chat { widget } => widget.handle_codex_event(event),
|
AppState::Chat { widget } => widget.handle_codex_event(event),
|
||||||
AppState::Login { .. } | AppState::GitWarning { .. } => {}
|
AppState::GitWarning { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use codex_core::util::is_inside_git_repo;
|
|||||||
use codex_login::try_read_openai_api_key;
|
use codex_login::try_read_openai_api_key;
|
||||||
use log_layer::TuiLogLayer;
|
use log_layer::TuiLogLayer;
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::Write;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tracing_appender::non_blocking;
|
use tracing_appender::non_blocking;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
@@ -35,7 +36,6 @@ mod git_warning_screen;
|
|||||||
mod history_cell;
|
mod history_cell;
|
||||||
mod insert_history;
|
mod insert_history;
|
||||||
mod log_layer;
|
mod log_layer;
|
||||||
mod login_screen;
|
|
||||||
mod markdown;
|
mod markdown;
|
||||||
mod scroll_event_helper;
|
mod scroll_event_helper;
|
||||||
mod slash_command;
|
mod slash_command;
|
||||||
@@ -47,7 +47,7 @@ mod user_approval_widget;
|
|||||||
|
|
||||||
pub use cli::Cli;
|
pub use cli::Cli;
|
||||||
|
|
||||||
pub fn run_main(
|
pub async fn run_main(
|
||||||
cli: Cli,
|
cli: Cli,
|
||||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||||
) -> std::io::Result<codex_core::protocol::TokenUsage> {
|
) -> std::io::Result<codex_core::protocol::TokenUsage> {
|
||||||
@@ -142,7 +142,25 @@ pub fn run_main(
|
|||||||
.with(tui_layer)
|
.with(tui_layer)
|
||||||
.try_init();
|
.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
|
// Determine whether we need to display the "not a git repo" warning
|
||||||
// modal. The flag is shown when the current working directory is *not*
|
// 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.
|
// `--allow-no-git-exec` flag.
|
||||||
let show_git_warning = !cli.skip_git_repo_check && !is_inside_git_repo(&config);
|
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()))
|
.map_err(|err| std::io::Error::other(err.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_ratatui_app(
|
fn run_ratatui_app(
|
||||||
cli: Cli,
|
cli: Cli,
|
||||||
config: Config,
|
config: Config,
|
||||||
show_login_screen: bool,
|
|
||||||
show_git_warning: bool,
|
show_git_warning: bool,
|
||||||
mut log_rx: tokio::sync::mpsc::UnboundedReceiver<String>,
|
mut log_rx: tokio::sync::mpsc::UnboundedReceiver<String>,
|
||||||
) -> color_eyre::Result<codex_core::protocol::TokenUsage> {
|
) -> color_eyre::Result<codex_core::protocol::TokenUsage> {
|
||||||
@@ -172,13 +189,7 @@ fn run_ratatui_app(
|
|||||||
terminal.clear()?;
|
terminal.clear()?;
|
||||||
|
|
||||||
let Cli { prompt, images, .. } = cli;
|
let Cli { prompt, images, .. } = cli;
|
||||||
let mut app = App::new(
|
let mut app = App::new(config.clone(), prompt, show_git_warning, images);
|
||||||
config.clone(),
|
|
||||||
prompt,
|
|
||||||
show_login_screen,
|
|
||||||
show_git_warning,
|
|
||||||
images,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Bridge log receiver into the AppEvent channel so latest log lines update the UI.
|
// Bridge log receiver into the AppEvent channel so latest log lines update the UI.
|
||||||
{
|
{
|
||||||
@@ -210,26 +221,17 @@ fn restore() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::unwrap_used)]
|
async fn should_show_login_screen(config: &Config) -> bool {
|
||||||
fn should_show_login_screen(config: &Config) -> bool {
|
|
||||||
if is_in_need_of_openai_api_key(config) {
|
if is_in_need_of_openai_api_key(config) {
|
||||||
// Reading the OpenAI API key is an async operation because it may need
|
// Reading the OpenAI API key is an async operation because it may need
|
||||||
// to refresh the token. Block on it.
|
// to refresh the token. Block on it.
|
||||||
let codex_home = config.codex_home.clone();
|
let codex_home = config.codex_home.clone();
|
||||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
if let Ok(openai_api_key) = try_read_openai_api_key(&codex_home).await {
|
||||||
tokio::spawn(async move {
|
set_openai_api_key(openai_api_key);
|
||||||
match try_read_openai_api_key(&codex_home).await {
|
false
|
||||||
Ok(openai_api_key) => {
|
} else {
|
||||||
set_openai_api_key(openai_api_key);
|
true
|
||||||
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()
|
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
.config_overrides
|
.config_overrides
|
||||||
.raw_overrides
|
.raw_overrides
|
||||||
.splice(0..0, top_cli.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));
|
println!("{}", codex_core::protocol::FinalOutput::from(usage));
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user