Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,14 @@ pub(crate) struct App {
// Serialize hook enablement writes per hook so stale completions cannot
// persist an older toggle after a newer one.
pending_hook_enabled_writes: HashMap<String, Option<bool>>,
pending_auth_profile_login: Option<PendingAuthProfileLogin>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PendingAuthProfileLogin {
pub(crate) login_id: String,
pub(crate) profile_name: String,
pub(crate) profile_label: String,
}

#[derive(Debug, Clone, PartialEq)]
Expand Down Expand Up @@ -1055,6 +1063,7 @@ See the Codex keymap documentation for supported actions and examples."
pending_startup_thread_start,
pending_plugin_enabled_writes: HashMap::new(),
pending_hook_enabled_writes: HashMap::new(),
pending_auth_profile_login: None,
};
if let Some(entry) = startup_hooks_browser {
app.chat_widget.open_hooks_browser(entry);
Expand Down Expand Up @@ -1189,7 +1198,7 @@ See the Codex keymap documentation for supported actions and examples."
}
app_server_event = app_server.next_event(), if listen_for_app_server_events => {
match app_server_event {
Some(event) => app.handle_app_server_event(&app_server, event).await,
Some(event) => app.handle_app_server_event(&mut app_server, event).await,
None => {
listen_for_app_server_events = false;
tracing::warn!("app-server event stream closed");
Expand Down
70 changes: 68 additions & 2 deletions codex-rs/tui/src/app/app_server_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ use crate::app_event::ConnectorsSnapshot;
use crate::app_server_session::AppServerSession;
use crate::app_server_session::status_account_display_from_auth_mode;
use codex_app_server_client::AppServerEvent;
use codex_app_server_protocol::AccountLoginCompletedNotification;
use codex_app_server_protocol::AccountUpdatedNotification;
use codex_app_server_protocol::AuthMode;
use codex_app_server_protocol::BackgroundAutoReviewStatus;
use codex_app_server_protocol::ServerNotification;
Expand All @@ -32,7 +34,7 @@ impl App {

pub(super) async fn handle_app_server_event(
&mut self,
app_server_client: &AppServerSession,
app_server_client: &mut AppServerSession,
event: AppServerEvent,
) {
match event {
Expand Down Expand Up @@ -62,7 +64,7 @@ impl App {

async fn handle_server_notification_event(
&mut self,
app_server_client: &AppServerSession,
app_server_client: &mut AppServerSession,
notification: ServerNotification,
) {
match &notification {
Expand All @@ -82,6 +84,10 @@ impl App {
.on_rolling_rate_limit_snapshot(notification.rate_limits.clone());
return;
}
ServerNotification::AccountLoginCompleted(notification) => {
self.handle_auth_profile_login_completed(notification);
return;
}
ServerNotification::AccountUpdated(notification) => {
self.chat_widget.update_account_state(
status_account_display_from_auth_mode(
Expand All @@ -93,6 +99,7 @@ impl App {
.auth_mode
.is_some_and(AuthMode::has_chatgpt_account),
);
self.maybe_record_completed_auth_profile_login(notification);
return;
}
ServerNotification::ExternalAgentConfigImportCompleted(_) => {
Expand Down Expand Up @@ -210,4 +217,63 @@ impl App {
tracing::warn!("failed to enqueue app-server request: {err}");
}
}

fn handle_auth_profile_login_completed(
&mut self,
notification: &AccountLoginCompletedNotification,
) {
let Some(login_id) = notification.login_id.as_deref() else {
return;
};
let Some(pending) = self.pending_auth_profile_login.as_ref() else {
return;
};
if pending.login_id != login_id {
return;
}
if !notification.success {
let pending = self.pending_auth_profile_login.take().unwrap();
self.chat_widget.add_error_message(format!(
"Login for auth profile {} failed.",
pending.profile_label
));
if let Some(error) = notification.error.as_ref() {
self.chat_widget
.add_info_message(error.clone(), /*hint*/ None);
}
}
}

fn maybe_record_completed_auth_profile_login(
&mut self,
notification: &AccountUpdatedNotification,
) {
if !notification
.auth_mode
.is_some_and(AuthMode::has_chatgpt_account)
{
return;
}
let Some(pending) = self.pending_auth_profile_login.take() else {
return;
};

if let Err(err) = codex_login::record_auth_profile_login(
&self.config.codex_home,
&pending.profile_name,
/*account_id*/ None,
/*email*/ None,
) {
self.chat_widget.add_error_message(format!(
"Logged in, but failed to update auth profile {}: {err}",
pending.profile_label
));
return;
}

self.chat_widget.add_info_message(
format!("Logged in to auth profile {}.", pending.profile_label),
/*hint*/ None,
);
}
}
4 changes: 4 additions & 0 deletions codex-rs/tui/src/app/event_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ impl App {
)
.await;
}
AppEvent::SetComposerText { text } => {
self.chat_widget
.set_composer_text(text, Vec::new(), Vec::new());
}
AppEvent::OpenResumePicker => {
let picker_app_server = match crate::start_app_server_for_picker(
&self.config,
Expand Down
105 changes: 98 additions & 7 deletions codex-rs/tui/src/app/session_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

use super::*;
use crate::app_event::AuthProfileSelection;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::LoginAccountParams;
use codex_app_server_protocol::LoginAccountResponse;

impl App {
pub(super) async fn open_agent_picker(&mut self, app_server: &mut AppServerSession) {
Expand Down Expand Up @@ -704,6 +707,8 @@ impl App {
app_server: &mut AppServerSession,
selection: AuthProfileSelection,
) {
self.pending_auth_profile_login = None;

if !matches!(self.app_server_target, crate::AppServerTarget::Embedded) {
self.chat_widget.add_error_message(
"/login profile switching requires an embedded Codex Lab app server.".to_string(),
Expand All @@ -716,13 +721,18 @@ impl App {
return;
}

let (auth_home, profile_label) = match selection {
AuthProfileSelection::Default => {
(self.config.codex_home.to_path_buf(), "default".to_string())
}
AuthProfileSelection::Named { profile_name } => {
let (auth_home, profile_name, profile_label) = match selection {
AuthProfileSelection::Default => (
self.config.codex_home.to_path_buf(),
None,
"default".to_string(),
),
AuthProfileSelection::Named {
ref profile_name, ..
} => {
let auth_home =
match codex_login::profile_home(&self.config.codex_home, &profile_name) {
match codex_login::profile_home(&self.config.codex_home, profile_name.as_str())
{
Ok(auth_home) => auth_home,
Err(err) => {
self.chat_widget.add_error_message(format!(
Expand All @@ -731,15 +741,31 @@ impl App {
return;
}
};
(auth_home, format!("`{profile_name}`"))
(
auth_home,
Some(profile_name.clone()),
format!("`{profile_name}`"),
)
}
};

let login_after_switch = matches!(
&selection,
AuthProfileSelection::Named {
login_after_switch: true,
..
}
);

if auth_home == self.config.auth_home.as_path() {
self.chat_widget.add_info_message(
format!("Auth profile {profile_label} is already active."),
/*hint*/ None,
);
if login_after_switch {
self.start_auth_profile_login(app_server, profile_name.as_deref(), &profile_label)
.await;
}
return;
}

Expand Down Expand Up @@ -811,6 +837,71 @@ impl App {
format!("Using auth profile {profile_label} for this session."),
Some("The previous session remains resumable.".to_string()),
);
if login_after_switch {
self.start_auth_profile_login(app_server, profile_name.as_deref(), &profile_label)
.await;
}
}
}

async fn start_auth_profile_login(
&mut self,
app_server: &mut AppServerSession,
profile_name: Option<&str>,
profile_label: &str,
) {
let request_handle = app_server.request_handle();
let response = request_handle
.request_typed::<LoginAccountResponse>(ClientRequest::LoginAccount {
request_id: app_server.next_request_id(),
params: LoginAccountParams::Chatgpt {
codex_streamlined_login: false,
},
})
.await;

match response {
Ok(LoginAccountResponse::Chatgpt { login_id, auth_url }) => {
if let Some(profile_name) = profile_name {
self.pending_auth_profile_login = Some(PendingAuthProfileLogin {
login_id,
profile_name: profile_name.to_string(),
profile_label: profile_label.to_string(),
});
}
self.open_url_in_browser(auth_url.clone());
self.chat_widget.add_info_message(
format!("Started ChatGPT login for auth profile {profile_label}."),
Some(format!("If your browser did not open, visit {auth_url}")),
);
}
Ok(LoginAccountResponse::ApiKey {}) => {
self.chat_widget.add_info_message(
format!("API key login configured for auth profile {profile_label}."),
/*hint*/ None,
);
}
Ok(LoginAccountResponse::ChatgptDeviceCode {
verification_url,
user_code,
..
}) => {
self.chat_widget.add_info_message(
format!("Started device-code login for auth profile {profile_label}."),
Some(format!("Visit {verification_url} and enter {user_code}.")),
);
}
Ok(LoginAccountResponse::ChatgptAuthTokens {}) => {
self.chat_widget.add_info_message(
format!("ChatGPT tokens configured for auth profile {profile_label}."),
/*hint*/ None,
);
}
Err(err) => {
self.chat_widget.add_error_message(format!(
"Failed to start login for auth profile {profile_label}: {err}"
));
}
}
}

Expand Down
1 change: 1 addition & 0 deletions codex-rs/tui/src/app/test_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ pub(super) async fn make_test_app() -> App {
pending_startup_thread_start: false,
pending_plugin_enabled_writes: HashMap::new(),
pending_hook_enabled_writes: HashMap::new(),
pending_auth_profile_login: None,
}
}

Expand Down
8 changes: 5 additions & 3 deletions codex-rs/tui/src/app/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3359,7 +3359,7 @@ async fn side_thread_snapshot_skips_session_header_preamble() {
async fn side_thread_ignores_global_mcp_startup_notifications() {
let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;
while app_event_rx.try_recv().is_ok() {}
let app_server = crate::start_embedded_app_server_for_picker(app.chat_widget.config_ref())
let mut app_server = crate::start_embedded_app_server_for_picker(app.chat_widget.config_ref())
.await
.expect("embedded app server");
let parent_thread_id = ThreadId::new();
Expand All @@ -3371,7 +3371,7 @@ async fn side_thread_ignores_global_mcp_startup_notifications() {
app.sync_side_thread_ui();

app.handle_app_server_event(
&app_server,
&mut app_server,
codex_app_server_client::AppServerEvent::ServerNotification(
ServerNotification::McpServerStatusUpdated(McpServerStatusUpdatedNotification {
name: "sentry".to_string(),
Expand Down Expand Up @@ -3818,6 +3818,7 @@ async fn make_test_app() -> App {
pending_startup_thread_start: false,
pending_plugin_enabled_writes: HashMap::new(),
pending_hook_enabled_writes: HashMap::new(),
pending_auth_profile_login: None,
}
}

Expand Down Expand Up @@ -3885,6 +3886,7 @@ async fn make_test_app_with_channels() -> (
pending_startup_thread_start: false,
pending_plugin_enabled_writes: HashMap::new(),
pending_hook_enabled_writes: HashMap::new(),
pending_auth_profile_login: None,
},
rx,
op_rx,
Expand Down Expand Up @@ -5332,7 +5334,7 @@ async fn override_turn_context_sends_thread_settings_update() {
);

app.handle_app_server_event(
&app_server,
&mut app_server,
codex_app_server_client::AppServerEvent::ServerNotification(
ServerNotification::ThreadSettingsUpdated(notification),
),
Expand Down
10 changes: 9 additions & 1 deletion codex-rs/tui/src/app_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ pub(crate) struct HistoryLookupResponse {
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum AuthProfileSelection {
Default,
Named { profile_name: String },
Named {
profile_name: String,
login_after_switch: bool,
},
}

impl RealtimeAudioDeviceKind {
Expand Down Expand Up @@ -217,6 +220,11 @@ pub(crate) enum AppEvent {
text: String,
},

/// Replace the composer contents without submitting.
SetComposerText {
text: String,
},

/// Open the resume picker inside the running TUI session.
OpenResumePicker,

Expand Down
2 changes: 1 addition & 1 deletion codex-rs/tui/src/app_server_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1137,7 +1137,7 @@ impl AppServerSession {
self.client.request_handle()
}

fn next_request_id(&mut self) -> RequestId {
pub(crate) fn next_request_id(&mut self) -> RequestId {
let request_id = self.next_request_id;
self.next_request_id += 1;
RequestId::Integer(request_id)
Expand Down
Loading
Loading