diff --git a/codex-rs/tui/src/api_provision.rs b/codex-rs/tui/src/api_provision.rs index 1f671ae341..6e00ab9107 100644 --- a/codex-rs/tui/src/api_provision.rs +++ b/codex-rs/tui/src/api_provision.rs @@ -14,33 +14,26 @@ use codex_login::start_api_provisioning; use codex_login::upsert_dotenv_api_key; use codex_login::validate_dotenv_target; use codex_protocol::config_types::ForcedLoginMethod; +use ratatui::style::Stylize; +use ratatui::text::Line; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; use crate::history_cell; use crate::history_cell::PlainHistoryCell; -pub(crate) struct ApiProvisionStartMessage { - pub(crate) message: String, - pub(crate) hint: Option, -} - pub(crate) fn start_command( app_event_tx: AppEventSender, auth_manager: Arc, codex_home: PathBuf, cwd: PathBuf, forced_login_method: Option, -) -> Result { +) -> Result { if read_openai_api_key_from_env().is_some() { return Ok(existing_shell_api_key_message()); } let dotenv_path = cwd.join(".env.local"); - let start_hint = format!( - "Codex will save {OPENAI_API_KEY_ENV_VAR} to {path} and hot-apply it here when allowed.", - path = dotenv_path.display() - ); validate_dotenv_target(&dotenv_path).map_err(|err| { format!( "Unable to prepare {} for {OPENAI_API_KEY_ENV_VAR}: {err}", @@ -48,14 +41,13 @@ pub(crate) fn start_command( ) })?; - let session = start_api_provisioning(ApiProvisionOptions::default()) + let options = ApiProvisionOptions::default(); + let callback_port = options.callback_port; + let session = start_api_provisioning(options) .map_err(|err| format!("Failed to start API provisioning: {err}"))?; - if !session.open_browser() { - return Err( - "Failed to open your browser for API provisioning. Try again from a desktop session or use the helper binary." - .to_string(), - ); - } + let _ = session.open_browser(); + let start_message = + continue_in_browser_message(session.auth_url(), &dotenv_path, callback_port); let app_event_tx_for_task = app_event_tx; let dotenv_path_for_task = dotenv_path; @@ -71,21 +63,61 @@ pub(crate) fn start_command( app_event_tx_for_task.send(AppEvent::InsertHistoryCell(Box::new(cell))); }); - Ok(ApiProvisionStartMessage { - message: "Opening your browser to provision a project API key.".to_string(), - hint: Some(start_hint), - }) + Ok(start_message) } -fn existing_shell_api_key_message() -> ApiProvisionStartMessage { - ApiProvisionStartMessage { - message: format!( +fn existing_shell_api_key_message() -> PlainHistoryCell { + history_cell::new_info_event( + format!( "{OPENAI_API_KEY_ENV_VAR} is already set in this Codex session; skipping API provisioning." ), - hint: Some(format!( + Some(format!( "This Codex session already inherited {OPENAI_API_KEY_ENV_VAR} from its shell environment. Unset it and run /api-provision again if you want Codex to provision and save a different key." )), - } + ) +} + +fn continue_in_browser_message( + auth_url: &str, + dotenv_path: &Path, + callback_port: u16, +) -> PlainHistoryCell { + let mut lines = vec![ + vec![ + "• ".dim(), + "Finish API provisioning via your browser.".into(), + ] + .into(), + "".into(), + ]; + + lines.push( + " If the link doesn't open automatically, open the following link to authenticate:".into(), + ); + lines.push("".into()); + lines.push(Line::from(vec![ + " ".into(), + auth_url.to_string().cyan().underlined(), + ])); + lines.push("".into()); + lines.push( + format!( + " Codex will save {OPENAI_API_KEY_ENV_VAR} to {} and hot-apply it here when allowed.", + dotenv_path.display() + ) + .dark_gray() + .into(), + ); + lines.push("".into()); + lines.push( + format!( + " On a remote or headless machine, forward localhost:{callback_port} to this Codex host before opening the link." + ) + .dark_gray() + .into(), + ); + + PlainHistoryCell::new(lines) } async fn complete_command( @@ -229,19 +261,24 @@ mod tests { assert_snapshot!(render_cell(&cell)); } + #[test] + fn continue_in_browser_message_snapshot() { + let cell = continue_in_browser_message( + "https://auth.openai.com/oauth/authorize?client_id=abc", + Path::new("/tmp/workspace/.env.local"), + /*callback_port*/ 5000, + ); + + assert_snapshot!(render_cell(&cell)); + } + #[test] fn existing_shell_api_key_message_mentions_openai_api_key() { - let message = existing_shell_api_key_message(); + let cell = existing_shell_api_key_message(); assert_eq!( - message.message, - "OPENAI_API_KEY is already set in this Codex session; skipping API provisioning." - ); - assert_eq!( - message.hint, - Some( - "This Codex session already inherited OPENAI_API_KEY from its shell environment. Unset it and run /api-provision again if you want Codex to provision and save a different key.".to_string() - ) + render_cell(&cell), + "• OPENAI_API_KEY is already set in this Codex session; skipping API provisioning. This Codex session already inherited OPENAI_API_KEY from its shell environment. Unset it and run /api-provision again if you want Codex to provision and save a different key." ); } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index b2ef264d76..eae30d528a 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -4743,7 +4743,8 @@ impl ChatWidget { self.config.forced_login_method, ) { Ok(start_message) => { - self.add_info_message(start_message.message, start_message.hint); + self.add_boxed_history(Box::new(start_message)); + self.request_redraw(); } Err(err) => { self.add_error_message(err); diff --git a/codex-rs/tui/src/snapshots/codex_tui__api_provision__tests__continue_in_browser_message_snapshot.snap b/codex-rs/tui/src/snapshots/codex_tui__api_provision__tests__continue_in_browser_message_snapshot.snap new file mode 100644 index 0000000000..c20739631a --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__api_provision__tests__continue_in_browser_message_snapshot.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/api_provision.rs +expression: render_cell(&cell) +--- +• Finish API provisioning via your browser. + + If the link doesn't open automatically, open the following link to authenticate: + + https://auth.openai.com/oauth/authorize?client_id=abc + + Codex will save OPENAI_API_KEY to /tmp/workspace/.env.local and hot-apply it here when allowed. + + On a remote or headless machine, forward localhost:5000 to this Codex host before opening the link.