diff --git a/codex-rs/cli/tests/daemon_startup.rs b/codex-rs/cli/tests/daemon_startup.rs index 2cb900d239..319a43eb4a 100644 --- a/codex-rs/cli/tests/daemon_startup.rs +++ b/codex-rs/cli/tests/daemon_startup.rs @@ -182,6 +182,7 @@ async fn daemon_startup(command: &str) -> Result<()> { .success(), "failed to prepare CLI test executable" ); + let bedrock_onboarding = matches!(command, "bedrock" | "bedrock-running"); fs::write( home.path().join("config.toml"), format!( @@ -195,6 +196,8 @@ async fn daemon_startup(command: &str) -> Result<()> { [projects.{}]\ntrust_level = \"{}\"\n", if command == "discovered" { "tui.status_line=[\"model\",\"thread-id\",\"thread-name\"]\nfeatures.daemon_auto_start=false" + } else if bedrock_onboarding { + "features.gcp_setup_wizard=false" } else { "" }, @@ -210,7 +213,6 @@ async fn daemon_startup(command: &str) -> Result<()> { let persisted = command == "mismatch-disable-persisted"; let disabling = matches!(command, "mismatch-disable" | "mismatch-disable-persisted"); let restart = command == "mismatch-restart" || disabling; - let bedrock_onboarding = matches!(command, "bedrock" | "bedrock-running"); if !bedrock_onboarding { fs::write( home.path().join("auth.json"), @@ -361,7 +363,8 @@ async fn daemon_startup(command: &str) -> Result<()> { if restart { "Server:Localbackgroundserver" } else { "Model:" } } } else if bedrock_onboarding { - "UseAmazonBedrock" + // With GCP disabled, only Bedrock makes the sign-in menu advertise another provider. + "UseanAPIkeyoranotherprovider" } else { args.extend([command.into(), "--strict-config".into()]); if command == "resume" { diff --git a/codex-rs/tui/src/onboarding/auth.rs b/codex-rs/tui/src/onboarding/auth.rs index 07c3629b2d..22f9f19591 100644 --- a/codex-rs/tui/src/onboarding/auth.rs +++ b/codex-rs/tui/src/onboarding/auth.rs @@ -43,9 +43,11 @@ use codex_protocol::config_types::ForcedLoginMethod; use std::cell::Cell; use std::sync::Arc; use std::sync::RwLock; +use std::time::Instant; use uuid::Uuid; use crate::LoginStatus; +use crate::activation_key_guard::ActivationKeyGuard; use crate::key_hint::KeyBinding; use crate::key_hint::KeyBindingListExt; use crate::motion::MotionMode; @@ -95,11 +97,18 @@ pub(crate) enum SignInState { pub(crate) enum SignInOption { ChatGpt, DeviceCode, + OtherMethods, ApiKey, Bedrock, Gcp, } +impl SignInOption { + fn is_alternative(self) -> bool { + !matches!(self, Self::ChatGpt | Self::DeviceCode | Self::OtherMethods) + } +} + const API_KEY_DISABLED_MESSAGE: &str = "API key login is disabled."; pub(super) fn onboarding_request_id() -> codex_app_server_protocol::RequestId { codex_app_server_protocol::RequestId::String(Uuid::new_v4().to_string()) @@ -181,6 +190,14 @@ impl ContinueWithDeviceCodeState { impl KeyboardHandler for AuthModeWidget { fn handle_key_event(&mut self, key_event: KeyEvent) { + if self + .activation_key_guard + .as_mut() + .is_some_and(|guard| guard.consume(KeyBinding::from_event(key_event), key_event.kind)) + || key_event.kind == KeyEventKind::Release + { + return; + } if self.handle_gcp_key_event(&key_event) || self.handle_bedrock_key_event(&key_event) { return; } @@ -196,31 +213,26 @@ impl KeyboardHandler for AuthModeWidget { self.move_highlight(/*delta*/ 1); return; } + if key_event.kind != KeyEventKind::Press { + return; + } if keys::SELECT_FIRST.is_pressed(key_event) { - self.select_option_by_index(/*index*/ 0); + self.select_option_by_index(/*index*/ 0, key_event); return; } if keys::SELECT_SECOND.is_pressed(key_event) { - self.select_option_by_index(/*index*/ 1); + self.select_option_by_index(/*index*/ 1, key_event); return; } if keys::SELECT_THIRD.is_pressed(key_event) { - self.select_option_by_index(/*index*/ 2); - return; - } - if keys::SELECT_FOURTH.is_pressed(key_event) { - self.select_option_by_index(/*index*/ 3); - return; - } - if keys::SELECT_FIFTH.is_pressed(key_event) { - self.select_option_by_index(/*index*/ 4); + self.select_option_by_index(/*index*/ 2, key_event); return; } if keys::CONFIRM.is_pressed(key_event) { let sign_in_state = { (*self.sign_in_state.read().unwrap()).clone() }; match sign_in_state { SignInState::PickMode => { - self.handle_sign_in_option(self.highlighted_mode); + self.activate_highlighted_mode(key_event); } SignInState::ChatGptSuccessMessage => { *self.sign_in_state.write().unwrap() = SignInState::ChatGptSuccess; @@ -230,8 +242,14 @@ impl KeyboardHandler for AuthModeWidget { return; } if keys::CANCEL.is_pressed(key_event) { - tracing::info!("Cancel onboarding auth step"); - self.cancel_active_attempt(); + if matches!(*self.sign_in_state.read().unwrap(), SignInState::PickMode) + && self.highlighted_mode.is_alternative() + { + self.highlighted_mode = SignInOption::OtherMethods; + self.set_error(/*message*/ None); + } else { + self.cancel_active_attempt(); + } } } @@ -261,6 +279,7 @@ impl KeyboardHandler for AuthModeWidget { pub(crate) struct AuthModeWidget { pub request_frame: FrameRequester, pub highlighted_mode: SignInOption, + pub(super) activation_key_guard: Option, pub error: Arc>>, pub sign_in_state: Arc>, pub login_status: LoginStatus, @@ -361,42 +380,34 @@ impl AuthModeWidget { } fn displayed_sign_in_options(&self) -> Vec { + if self.highlighted_mode.is_alternative() { + let mut options = vec![SignInOption::ApiKey]; + if self.bedrock_setup_enabled { + options.push(SignInOption::Bedrock); + } + if self.gcp_setup_enabled { + options.push(SignInOption::Gcp); + } + return options; + } let mut options = vec![SignInOption::ChatGpt]; if self.is_chatgpt_login_allowed() { options.push(SignInOption::DeviceCode); } if self.is_api_login_allowed() { - options.push(SignInOption::ApiKey); - if self.bedrock_setup_enabled { - options.push(SignInOption::Bedrock); - } - if self.gcp_setup_enabled { - options.push(SignInOption::Gcp); - } - } - options - } - - fn selectable_sign_in_options(&self) -> Vec { - let mut options = Vec::new(); - if self.is_chatgpt_login_allowed() { - options.push(SignInOption::ChatGpt); - options.push(SignInOption::DeviceCode); - } - if self.is_api_login_allowed() { - options.push(SignInOption::ApiKey); - if self.bedrock_setup_enabled { - options.push(SignInOption::Bedrock); - } - if self.gcp_setup_enabled { - options.push(SignInOption::Gcp); - } + options.push(SignInOption::OtherMethods); } options } fn move_highlight(&mut self, delta: isize) { - let options = self.selectable_sign_in_options(); + if !matches!(*self.sign_in_state.read().unwrap(), SignInState::PickMode) { + return; + } + let mut options = self.displayed_sign_in_options(); + if !self.is_chatgpt_login_allowed() { + options.retain(|option| *option != SignInOption::ChatGpt); + } if options.is_empty() { return; } @@ -410,18 +421,38 @@ impl AuthModeWidget { self.highlighted_mode = options[next_index]; } - fn select_option_by_index(&mut self, index: usize) { + fn select_option_by_index(&mut self, index: usize, key_event: KeyEvent) { if !matches!(*self.sign_in_state.read().unwrap(), SignInState::PickMode) { return; } let options = self.displayed_sign_in_options(); - if let Some(option) = options.get(index).copied() { - self.handle_sign_in_option(option); + if let Some(option) = options.get(index).copied() + && (option != SignInOption::ChatGpt || self.is_chatgpt_login_allowed()) + { + self.highlighted_mode = option; + self.activate_highlighted_mode(key_event); } } + fn activate_highlighted_mode(&mut self, key_event: KeyEvent) { + let option = self.highlighted_mode; + if option == SignInOption::OtherMethods || option.is_alternative() { + // Enter and printable shortcuts can repeat as plain presses without a release. + self.activation_key_guard = Some(ActivationKeyGuard::new_at( + KeyBinding::from_event(key_event), + Instant::now(), + /*key_releases_reported*/ false, + )); + } + self.handle_sign_in_option(option); + } + fn handle_sign_in_option(&mut self, option: SignInOption) { match option { + SignInOption::OtherMethods => { + self.highlighted_mode = SignInOption::ApiKey; + self.set_error(/*message*/ None); + } SignInOption::ChatGpt => { if self.is_chatgpt_login_allowed() { self.start_chatgpt_login(); @@ -466,7 +497,9 @@ impl AuthModeWidget { } fn render_pick_mode(&self, area: Rect, buf: &mut Buffer) { - let mut lines: Vec = if self.bedrock_setup_enabled || self.gcp_setup_enabled { + let mut lines: Vec = if self.highlighted_mode.is_alternative() { + vec![" Sign in another way".into(), "".into()] + } else if self.bedrock_setup_enabled || self.gcp_setup_enabled { vec![" Choose how you want to use Codex.".into(), "".into()] } else { vec![ @@ -523,6 +556,18 @@ impl AuthModeWidget { for (idx, option) in self.displayed_sign_in_options().into_iter().enumerate() { match option { + SignInOption::OtherMethods => { + lines.extend(create_mode_item( + idx, + option, + "Sign in another way", + if self.bedrock_setup_enabled || self.gcp_setup_enabled { + "Use an API key or another provider" + } else { + "Use an OpenAI API key" + }, + )); + } SignInOption::ChatGpt => { lines.extend(create_mode_item( idx, @@ -581,6 +626,13 @@ impl AuthModeWidget { self.confirm_binding().into(), " to continue".dim(), ])); + if self.highlighted_mode.is_alternative() { + lines.push(Line::from(vec![ + " Press ".dim(), + self.cancel_binding().into(), + " to go back".dim(), + ])); + } if let Some(err) = self.error_message() { lines.push("".into()); lines.push(err.red().into()); @@ -813,7 +865,9 @@ impl AuthModeWidget { *guard = SignInState::PickMode; self.set_error(/*message*/ None); should_request_frame = true; - } else if keys::CONFIRM.is_pressed(*key_event) { + } else if key_event.kind == KeyEventKind::Press + && keys::CONFIRM.is_pressed(*key_event) + { let trimmed = state.value.trim().to_string(); if trimmed.is_empty() { self.set_error(Some("API key cannot be empty".to_string())); @@ -1321,6 +1375,7 @@ mod tests { let widget = AuthModeWidget { request_frame: FrameRequester::test_dummy(), highlighted_mode: SignInOption::ChatGpt, + activation_key_guard: None, error: Arc::new(RwLock::new(None)), sign_in_state: Arc::new(RwLock::new(SignInState::PickMode)), login_status: LoginStatus::NotAuthenticated, @@ -1383,7 +1438,7 @@ mod tests { vec![ SignInOption::ChatGpt, SignInOption::DeviceCode, - SignInOption::ApiKey, + SignInOption::OtherMethods, ] ); @@ -1404,15 +1459,13 @@ mod tests { &widget.app_server_target, Feature::BedrockSetupWizard, ); + widget.handle_key_event(KeyCode::Char('3').into()); assert_eq!( widget.displayed_sign_in_options(), - vec![ - SignInOption::ChatGpt, - SignInOption::DeviceCode, - SignInOption::ApiKey, - SignInOption::Bedrock, - ] + vec![SignInOption::ApiKey, SignInOption::Bedrock] ); + widget.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); + widget.move_highlight(/*delta*/ 1); let area = Rect::new(0, 0, 76, 19); let mut buffer = Buffer::empty(area); @@ -1438,11 +1491,8 @@ mod tests { 2. Sign in with Device Code Sign in from another device with a one-time code - 3. Use an OpenAI API key - Pay for what you use - - 4. Use Amazon Bedrock - Connect using your AWS credentials + 3. Sign in another way + Use an API key or another provider Press enter to continue "###); @@ -1753,6 +1803,9 @@ mod tests { } } +#[cfg(test)] +mod activation_key_tests; + #[cfg(test)] #[path = "gcp_tests.rs"] mod gcp_tests; diff --git a/codex-rs/tui/src/onboarding/auth/activation_key_tests.rs b/codex-rs/tui/src/onboarding/auth/activation_key_tests.rs new file mode 100644 index 0000000000..9ffd018622 --- /dev/null +++ b/codex-rs/tui/src/onboarding/auth/activation_key_tests.rs @@ -0,0 +1,55 @@ +#![cfg(test)] + +//! Regression coverage for a key held across the sign-in menus. + +use super::*; +use pretty_assertions::assert_eq; + +#[tokio::test] +async fn held_enter_requires_a_fresh_confirmation_to_save_a_prefilled_api_key() { + let (mut widget, _home) = tests::widget_forced_chatgpt().await; + widget.auth_config.forced_login_method = None; + let enter = |kind| KeyEvent::new_with_kind(KeyCode::Enter, KeyModifiers::NONE, kind); + + widget.handle_key_event(KeyCode::Up.into()); + widget.handle_key_event(enter(KeyEventKind::Press)); + widget.handle_key_event(enter(KeyEventKind::Repeat)); + widget.handle_key_event(enter(KeyEventKind::Press)); + widget.handle_key_event(KeyCode::Down.into()); + widget.handle_key_event(enter(KeyEventKind::Repeat)); + assert_eq!(widget.highlighted_mode, SignInOption::ApiKey); + assert!(matches!( + *widget.sign_in_state.read().unwrap(), + SignInState::PickMode + )); + + widget.handle_key_event(enter(KeyEventKind::Release)); + widget.handle_key_event(enter(KeyEventKind::Press)); + { + let mut state = widget.sign_in_state.write().unwrap(); + let SignInState::ApiKeyEntry(input) = &mut *state else { + panic!("expected API key entry"); + }; + input.value = "test-api-key".into(); + input.prepopulated_from_env = true; + } + let (request_frame, mut frames) = FrameRequester::test_channel(); + widget.request_frame = request_frame; + widget.handle_key_event(enter(KeyEventKind::Press)); + widget.handle_key_event(enter(KeyEventKind::Repeat)); + widget.handle_key_event(KeyCode::Left.into()); + widget.handle_key_event(enter(KeyEventKind::Repeat)); + assert_eq!( + frames.try_recv(), + Err(tokio::sync::mpsc::error::TryRecvError::Empty) + ); + + widget.handle_key_event(enter(KeyEventKind::Release)); + widget.handle_key_event(enter(KeyEventKind::Press)); + frames.try_recv().expect("fresh Enter should start saving"); + frames.recv().await.expect("API key save should finish"); + assert!(matches!( + *widget.sign_in_state.read().unwrap(), + SignInState::ApiKeyConfigured + )); +} diff --git a/codex-rs/tui/src/onboarding/gcp_tests.rs b/codex-rs/tui/src/onboarding/gcp_tests.rs index 178bfbb22d..128e0be56b 100644 --- a/codex-rs/tui/src/onboarding/gcp_tests.rs +++ b/codex-rs/tui/src/onboarding/gcp_tests.rs @@ -39,30 +39,29 @@ async fn gcp_menu_gating_and_setup_flow() { Rect::new(0, 0, 76, 8), ); + let press = |key| KeyEvent::new(key, KeyModifiers::NONE); widget.gcp_setup_enabled = true; + widget.handle_key_event(press(KeyCode::Char('3'))); assert_eq!( widget.displayed_sign_in_options(), - vec![ - SignInOption::ChatGpt, - SignInOption::DeviceCode, - SignInOption::ApiKey, - SignInOption::Gcp - ] + vec![SignInOption::ApiKey, SignInOption::Gcp] ); widget.bedrock_setup_enabled = true; - assert_eq!( - widget.selectable_sign_in_options(), - vec![ - SignInOption::ChatGpt, - SignInOption::DeviceCode, - SignInOption::ApiKey, - SignInOption::Bedrock, - SignInOption::Gcp - ] - ); + widget.handle_key_event(KeyEvent::new_with_kind( + KeyCode::Char('3'), + KeyModifiers::NONE, + KeyEventKind::Repeat, + )); + assert_eq!(widget.highlighted_mode, SignInOption::ApiKey); + assert!(matches!( + *widget.sign_in_state.read().unwrap(), + SignInState::PickMode + )); snapshot(&widget, "gcp_sign_in_menu", Rect::new(0, 0, 76, 21)); - widget.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)); - snapshot(&widget, "gcp_sign_in_menu_short", Rect::new(0, 0, 76, 14)); + widget.handle_key_event(press(KeyCode::Up)); + snapshot(&widget, "gcp_sign_in_menu_short", Rect::new(0, 0, 76, 8)); + widget.handle_key_event(press(KeyCode::Esc)); + assert_eq!(widget.highlighted_mode, SignInOption::OtherMethods); let area = Rect::new(0, 0, 76, 12); widget.auth_config.forced_login_method = Some(ForcedLoginMethod::Chatgpt); @@ -79,16 +78,28 @@ async fn gcp_menu_gating_and_setup_flow() { *widget.sign_in_state.write().unwrap() = SignInState::ChatGptDeviceCode(ContinueWithDeviceCodeState::pending("login".into())); - widget.handle_key_event(KeyEvent::new(KeyCode::Char('5'), KeyModifiers::NONE)); + widget.handle_key_event(press(KeyCode::Char('3'))); assert!(matches!( &*widget.sign_in_state.read().unwrap(), SignInState::ChatGptDeviceCode(_) )); widget.cancel_active_attempt(); - widget.handle_key_event(KeyEvent::new(KeyCode::Char('5'), KeyModifiers::NONE)); + widget.handle_key_event(press(KeyCode::Char('3'))); + widget.handle_key_event(press(KeyCode::Char('1'))); + widget.handle_key_event(press(KeyCode::Esc)); + assert_eq!(widget.highlighted_mode, SignInOption::ApiKey); + widget.handle_key_event(press(KeyCode::Char('3'))); + widget.handle_key_event(press(KeyCode::Esc)); + assert_eq!(widget.highlighted_mode, SignInOption::Gcp); + widget.handle_key_event(press(KeyCode::Enter)); assert!(widget.is_text_entry_active()); assert!(widget.should_suppress_printable_quit()); + widget.handle_key_event(KeyEvent::new_with_kind( + KeyCode::Enter, + KeyModifiers::NONE, + KeyEventKind::Release, + )); widget.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); assert_eq!( widget.error_message().as_deref(), diff --git a/codex-rs/tui/src/onboarding/keys.rs b/codex-rs/tui/src/onboarding/keys.rs index cdeb97787b..6037a370b0 100644 --- a/codex-rs/tui/src/onboarding/keys.rs +++ b/codex-rs/tui/src/onboarding/keys.rs @@ -23,8 +23,6 @@ pub(crate) const SELECT_SECOND: [KeyBinding; 2] = [ key_hint::plain(KeyCode::Char('n')), ]; pub(crate) const SELECT_THIRD: [KeyBinding; 1] = [key_hint::plain(KeyCode::Char('3'))]; -pub(crate) const SELECT_FOURTH: [KeyBinding; 1] = [key_hint::plain(KeyCode::Char('4'))]; -pub(crate) const SELECT_FIFTH: [KeyBinding; 1] = [key_hint::plain(KeyCode::Char('5'))]; pub(crate) const CONFIRM: [KeyBinding; 1] = [key_hint::plain(KeyCode::Enter)]; pub(crate) const CANCEL: [KeyBinding; 1] = [key_hint::plain(KeyCode::Esc)]; pub(crate) const COPY_LINK: [KeyBinding; 1] = [key_hint::plain(KeyCode::Char('c'))]; diff --git a/codex-rs/tui/src/onboarding/onboarding_screen.rs b/codex-rs/tui/src/onboarding/onboarding_screen.rs index 0507eea7ed..db40dd0d71 100644 --- a/codex-rs/tui/src/onboarding/onboarding_screen.rs +++ b/codex-rs/tui/src/onboarding/onboarding_screen.rs @@ -173,12 +173,13 @@ impl OnboardingScreen { if auth_config.is_login_method_allowed(ForcedLoginMethod::Chatgpt) { SignInOption::ChatGpt } else { - SignInOption::ApiKey + SignInOption::OtherMethods }; if let Some(app_server_request_handle) = app_server_request_handle { steps.push(Step::Auth(AuthModeWidget { request_frame: tui.frame_requester(), highlighted_mode, + activation_key_guard: None, error: Arc::new(RwLock::new(None)), sign_in_state: Arc::new(RwLock::new(SignInState::PickMode)), login_status, @@ -352,7 +353,10 @@ impl KeyboardHandler for OnboardingScreen { /// starts, while cloud provider fields suppress them from the first character. /// Control/alt quit chords still work as emergency exits. fn handle_key_event(&mut self, key_event: KeyEvent) { - if !matches!(key_event.kind, KeyEventKind::Press | KeyEventKind::Repeat) { + if key_event.kind == KeyEventKind::Release { + if let Some(widget) = self.auth_widget_mut() { + widget.handle_key_event(key_event); + } return; } let text_entry_context = self.text_entry_context(); diff --git a/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__auth__gcp_tests__gcp_sign_in_menu.snap b/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__auth__gcp_tests__gcp_sign_in_menu.snap index 5ac231753e..a145097c1d 100644 --- a/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__auth__gcp_tests__gcp_sign_in_menu.snap +++ b/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__auth__gcp_tests__gcp_sign_in_menu.snap @@ -2,21 +2,16 @@ source: tui/src/onboarding/gcp_tests.rs expression: terminal.backend().to_string() --- - Choose how you want to use Codex. + Sign in another way -> 1. Sign in with ChatGPT - Usage included with Plus, Pro, Business, and Enterprise plans - - 2. Sign in with Device Code - Sign in from another device with a one-time code - - 3. Use an OpenAI API key +> 1. Use an OpenAI API key Pay for what you use - 4. Use Amazon Bedrock + 2. Use Amazon Bedrock Connect using your AWS credentials - 5. Use Google Cloud Vertex AI + 3. Use Google Cloud Vertex AI Connect using your Google Cloud credentials Press enter to continue + Press esc to go back diff --git a/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__auth__gcp_tests__gcp_sign_in_menu_short.snap b/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__auth__gcp_tests__gcp_sign_in_menu_short.snap index 15cb896296..795fee620e 100644 --- a/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__auth__gcp_tests__gcp_sign_in_menu_short.snap +++ b/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__auth__gcp_tests__gcp_sign_in_menu_short.snap @@ -2,11 +2,10 @@ source: tui/src/onboarding/gcp_tests.rs expression: terminal.backend().to_string() --- - Choose how you want to use Codex. + Sign in another way - 1. Sign in with ChatGPT - 2. Sign in with Device Code - 3. Use an OpenAI API key - 4. Use Amazon Bedrock -> 5. Use Google Cloud Vertex AI + 1. Use an OpenAI API key + 2. Use Amazon Bedrock +> 3. Use Google Cloud Vertex AI Press enter to continue + Press esc to go back diff --git a/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__auth__gcp_tests__sign_in_menu_short_without_cloud_wizards.snap b/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__auth__gcp_tests__sign_in_menu_short_without_cloud_wizards.snap index ec3a01943d..8c6c8e0d17 100644 --- a/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__auth__gcp_tests__sign_in_menu_short_without_cloud_wizards.snap +++ b/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__auth__gcp_tests__sign_in_menu_short_without_cloud_wizards.snap @@ -7,5 +7,5 @@ expression: terminal.backend().to_string() > 1. Sign in with ChatGPT 2. Sign in with Device Code - 3. Use an OpenAI API key + 3. Sign in another way Press enter to continue