mirror of
https://github.com/openai/codex.git
synced 2026-05-02 20:32:04 +03:00
tui_app_server: cancel active login before Ctrl+C exit (#15673)
## Summary Fixes slow `Ctrl+C` exit from the ChatGPT browser-login screen in `tui_app_server`. ## Root cause Onboarding-level `Ctrl+C` quit bypassed the auth widget's cancel path. That let the active ChatGPT login keep running, and in-process app-server shutdown then waited on the stale login attempt before finishing. ## Changes - Extract a shared `cancel_active_attempt()` path in the auth widget - Use that path from onboarding-level `Ctrl+C` before exiting the TUI - Add focused tests for canceling browser-login and device-code attempts - Add app-server shutdown cleanup that explicitly drops any active login before draining background work
This commit is contained in:
@@ -163,37 +163,7 @@ impl KeyboardHandler for AuthModeWidget {
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
tracing::info!("Esc pressed");
|
||||
let mut sign_in_state = self.sign_in_state.write().unwrap();
|
||||
match &*sign_in_state {
|
||||
SignInState::ChatGptContinueInBrowser(state) => {
|
||||
let request_handle = self.app_server_request_handle.clone();
|
||||
let login_id = state.login_id.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = request_handle
|
||||
.request_typed::<codex_app_server_protocol::CancelLoginAccountResponse>(
|
||||
ClientRequest::CancelLoginAccount {
|
||||
request_id: onboarding_request_id(),
|
||||
params: CancelLoginAccountParams { login_id },
|
||||
},
|
||||
)
|
||||
.await;
|
||||
});
|
||||
*sign_in_state = SignInState::PickMode;
|
||||
drop(sign_in_state);
|
||||
self.set_error(/*message*/ None);
|
||||
self.request_frame.schedule_frame();
|
||||
}
|
||||
SignInState::ChatGptDeviceCode(state) => {
|
||||
if let Some(cancel) = &state.cancel {
|
||||
cancel.notify_one();
|
||||
}
|
||||
*sign_in_state = SignInState::PickMode;
|
||||
drop(sign_in_state);
|
||||
self.set_error(/*message*/ None);
|
||||
self.request_frame.schedule_frame();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
self.cancel_active_attempt();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -221,6 +191,36 @@ pub(crate) struct AuthModeWidget {
|
||||
}
|
||||
|
||||
impl AuthModeWidget {
|
||||
pub(crate) fn cancel_active_attempt(&self) {
|
||||
let mut sign_in_state = self.sign_in_state.write().unwrap();
|
||||
match &*sign_in_state {
|
||||
SignInState::ChatGptContinueInBrowser(state) => {
|
||||
let request_handle = self.app_server_request_handle.clone();
|
||||
let login_id = state.login_id.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = request_handle
|
||||
.request_typed::<codex_app_server_protocol::CancelLoginAccountResponse>(
|
||||
ClientRequest::CancelLoginAccount {
|
||||
request_id: onboarding_request_id(),
|
||||
params: CancelLoginAccountParams { login_id },
|
||||
},
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
SignInState::ChatGptDeviceCode(state) => {
|
||||
if let Some(cancel) = &state.cancel {
|
||||
cancel.notify_one();
|
||||
}
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
*sign_in_state = SignInState::PickMode;
|
||||
drop(sign_in_state);
|
||||
self.set_error(/*message*/ None);
|
||||
self.request_frame.schedule_frame();
|
||||
}
|
||||
|
||||
fn set_error(&self, message: Option<String>) {
|
||||
*self.error.write().unwrap() = message;
|
||||
}
|
||||
@@ -1001,6 +1001,50 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cancel_active_attempt_resets_browser_login_state() {
|
||||
let (widget, _tmp) = widget_forced_chatgpt().await;
|
||||
*widget.error.write().unwrap() = Some("still logging in".to_string());
|
||||
*widget.sign_in_state.write().unwrap() =
|
||||
SignInState::ChatGptContinueInBrowser(ContinueInBrowserState {
|
||||
login_id: "login-1".to_string(),
|
||||
auth_url: "https://auth.example.com".to_string(),
|
||||
});
|
||||
|
||||
widget.cancel_active_attempt();
|
||||
|
||||
assert_eq!(widget.error_message(), None);
|
||||
assert!(matches!(
|
||||
&*widget.sign_in_state.read().unwrap(),
|
||||
SignInState::PickMode
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cancel_active_attempt_notifies_device_code_login() {
|
||||
let (widget, _tmp) = widget_forced_chatgpt().await;
|
||||
let cancel = Arc::new(Notify::new());
|
||||
*widget.error.write().unwrap() = Some("still logging in".to_string());
|
||||
*widget.sign_in_state.write().unwrap() =
|
||||
SignInState::ChatGptDeviceCode(ContinueWithDeviceCodeState {
|
||||
device_code: None,
|
||||
cancel: Some(cancel.clone()),
|
||||
});
|
||||
|
||||
widget.cancel_active_attempt();
|
||||
|
||||
assert_eq!(widget.error_message(), None);
|
||||
assert!(matches!(
|
||||
&*widget.sign_in_state.read().unwrap(),
|
||||
SignInState::PickMode
|
||||
));
|
||||
assert!(
|
||||
tokio::time::timeout(std::time::Duration::from_millis(50), cancel.notified())
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
/// Collects all buffer cell symbols that contain the OSC 8 open sequence
|
||||
/// for the given URL. Returns the concatenated "inner" characters.
|
||||
fn collect_osc8_chars(buf: &Buffer, area: Rect, url: &str) -> String {
|
||||
|
||||
@@ -206,6 +206,14 @@ impl OnboardingScreen {
|
||||
self.should_exit
|
||||
}
|
||||
|
||||
fn cancel_auth_if_active(&self) {
|
||||
for step in &self.steps {
|
||||
if let Step::Auth(widget) = step {
|
||||
widget.cancel_active_attempt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn auth_widget_mut(&mut self) -> Option<&mut AuthModeWidget> {
|
||||
self.steps.iter_mut().find_map(|step| match step {
|
||||
Step::Auth(widget) => Some(widget),
|
||||
@@ -270,6 +278,7 @@ impl KeyboardHandler for OnboardingScreen {
|
||||
};
|
||||
if should_quit {
|
||||
if self.is_auth_in_progress() {
|
||||
self.cancel_auth_if_active();
|
||||
// If the user cancels the auth menu, exit the app rather than
|
||||
// leave the user at a prompt in an unauthed state.
|
||||
self.should_exit = true;
|
||||
|
||||
Reference in New Issue
Block a user