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
29 changes: 26 additions & 3 deletions codex-rs/app-server/src/request_processors/account_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ impl AccountRequestProcessor {
}
}

fn auth_storage_home(config: &Config) -> &Path {
config.auth_home.as_path()
}

async fn maybe_refresh_remote_installed_plugins_cache_for_current_config(
config_manager: &ConfigManager,
thread_manager: &Arc<ThreadManager>,
Expand Down Expand Up @@ -283,7 +287,7 @@ impl AccountRequestProcessor {
}

match login_with_api_key(
&self.config.codex_home,
Self::auth_storage_home(&self.config),
&params.api_key,
self.config.cli_auth_credentials_store_mode,
) {
Expand Down Expand Up @@ -330,7 +334,7 @@ impl AccountRequestProcessor {
open_browser: false,
codex_streamlined_login,
..LoginServerOptions::new(
config.codex_home.to_path_buf(),
Self::auth_storage_home(config).to_path_buf(),
CLIENT_ID.to_string(),
config.forced_chatgpt_workspace_id.clone(),
config.cli_auth_credentials_store_mode,
Expand Down Expand Up @@ -588,7 +592,7 @@ impl AccountRequestProcessor {
}

login_with_chatgpt_auth_tokens(
&self.config.codex_home,
Self::auth_storage_home(&self.config),
&access_token,
&chatgpt_account_id,
chatgpt_plan_type.as_deref(),
Expand Down Expand Up @@ -1016,7 +1020,26 @@ mod tests {
use super::*;
use codex_backend_client::TokenUsageProfileDailyBucket;
use codex_backend_client::TokenUsageProfileStats;
use codex_core::config::ConfigBuilder;
use pretty_assertions::assert_eq;
use tempfile::TempDir;

#[tokio::test]
async fn account_login_storage_uses_auth_home() {
let codex_home = TempDir::new().expect("codex home");
let auth_home = TempDir::new().expect("auth home");
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.auth_home(auth_home.path().to_path_buf())
.build()
.await
.expect("build config");

assert_eq!(
AccountRequestProcessor::auth_storage_home(&config),
auth_home.path()
);
}

#[test]
fn account_token_usage_response_maps_profile_stats_and_daily_buckets() {
Expand Down
12 changes: 12 additions & 0 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ use crate::version::CODEX_CLI_VERSION;
use crate::workspace_command::AppServerWorkspaceCommandRunner;
use crate::workspace_command::WorkspaceCommandRunner;
use codex_ansi_escape::ansi_escape_line;
use codex_app_server_client::AppServerClient;
use codex_app_server_client::AppServerRequestHandle;
use codex_app_server_client::TypedRequestError;
use codex_app_server_protocol::AddCreditsNudgeCreditType;
Expand Down Expand Up @@ -132,6 +133,8 @@ use codex_app_server_protocol::Turn;
use codex_app_server_protocol::TurnError as AppServerTurnError;
use codex_app_server_protocol::TurnStatus;
use codex_app_server_protocol::WriteStatus;
use codex_arg0::Arg0DispatchPaths;
use codex_config::CloudConfigBundleLoader;
use codex_config::ConfigLayerStackOrdering;
use codex_config::LoaderOverrides;
use codex_config::types::ApprovalsReviewer;
Expand Down Expand Up @@ -529,6 +532,9 @@ pub(crate) struct App {
feedback_audience: FeedbackAudience,
environment_manager: Arc<EnvironmentManager>,
app_server_target: AppServerTarget,
arg0_paths: Arg0DispatchPaths,
strict_config: bool,
cloud_config_bundle: CloudConfigBundleLoader,
/// Set when the user confirms an update; propagated on exit.
pub(crate) pending_update_action: Option<UpdateAction>,

Expand Down Expand Up @@ -719,9 +725,12 @@ impl App {
tui: &mut tui::Tui,
mut app_server: AppServerSession,
mut config: Config,
arg0_paths: Arg0DispatchPaths,
cli_kv_overrides: Vec<(String, TomlValue)>,
harness_overrides: ConfigOverrides,
loader_overrides: LoaderOverrides,
strict_config: bool,
cloud_config_bundle: CloudConfigBundleLoader,
initial_prompt: Option<String>,
initial_images: Vec<PathBuf>,
session_selection: SessionSelection,
Expand Down Expand Up @@ -1026,6 +1035,9 @@ See the Codex keymap documentation for supported actions and examples."
feedback_audience,
environment_manager,
app_server_target,
arg0_paths,
strict_config,
cloud_config_bundle,
pending_update_action: None,
pending_shutdown_exit_thread_id: None,
windows_sandbox: WindowsSandboxState::default(),
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/tui/src/app/event_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,9 @@ impl App {
.add_error_message(format!("Logout failed: {err}"));
}
},
AppEvent::SwitchAuthProfile { selection } => {
self.switch_auth_profile(tui, app_server, selection).await;
}
AppEvent::FatalExitRequest(message) => {
return Ok(AppRunControl::Exit(ExitReason::Fatal(message)));
}
Expand Down
201 changes: 178 additions & 23 deletions codex-rs/tui/src/app/session_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//! cache used for multi-agent navigation.

use super::*;
use crate::app_event::AuthProfileSelection;

impl App {
pub(super) async fn open_agent_picker(&mut self, app_server: &mut AppServerSession) {
Expand Down Expand Up @@ -473,20 +474,49 @@ impl App {
// until the new session is configured and any replayed turns have been rendered.
self.refresh_in_memory_config_from_disk_best_effort("starting a new thread")
.await;
let model = self.chat_widget.current_model().to_string();
let config = self.fresh_session_config();
self.start_fresh_session_with_config(
tui,
app_server,
config,
session_start_source,
initial_user_message,
/*cleanup_current_thread*/ true,
"To continue this session, run ",
"Failed to attach to fresh app-server thread",
"Failed to start a fresh session through the app server",
)
.await;
}

#[allow(clippy::too_many_arguments)]
async fn start_fresh_session_with_config(
&mut self,
tui: &mut tui::Tui,
app_server: &mut AppServerSession,
config: Config,
session_start_source: Option<ThreadStartSource>,
initial_user_message: Option<crate::chatwidget::UserMessage>,
cleanup_current_thread: bool,
resume_hint_prefix: &'static str,
attach_error_prefix: &'static str,
start_error_prefix: &'static str,
) -> bool {
let model = self.chat_widget.current_model().to_string();
let summary = session_summary(
self.chat_widget.token_usage(),
self.chat_widget.thread_id(),
self.chat_widget.thread_name(),
self.chat_widget.rollout_path().as_deref(),
);
self.shutdown_current_thread(app_server).await;
let tracked_thread_ids: Vec<ThreadId> =
self.thread_event_channels.keys().copied().collect();
for thread_id in tracked_thread_ids {
if let Err(err) = app_server.thread_unsubscribe(thread_id).await {
tracing::warn!("failed to unsubscribe tracked thread {thread_id}: {err}");
if cleanup_current_thread {
self.shutdown_current_thread(app_server).await;
let tracked_thread_ids: Vec<ThreadId> =
self.thread_event_channels.keys().copied().collect();
for thread_id in tracked_thread_ids {
if let Err(err) = app_server.thread_unsubscribe(thread_id).await {
tracing::warn!("failed to unsubscribe tracked thread {thread_id}: {err}");
}
}
}
self.config = config.clone();
Expand All @@ -495,7 +525,7 @@ impl App {
.await
{
Ok(started) => {
if let Err(err) = self
match self
.replace_chat_widget_with_app_server_thread(
tui,
app_server,
Expand All @@ -504,29 +534,37 @@ impl App {
)
.await
{
self.chat_widget.add_error_message(format!(
"Failed to attach to fresh app-server thread: {err}"
));
} else if let Some(summary) = summary {
let mut lines: Vec<Line<'static>> = Vec::new();
if let Some(usage_line) = summary.usage_line {
lines.push(usage_line.into());
Ok(()) => {
if let Some(summary) = summary {
let mut lines: Vec<Line<'static>> = Vec::new();
if let Some(usage_line) = summary.usage_line {
lines.push(usage_line.into());
}
if let Some(command) = summary.resume_hint {
let spans = vec![resume_hint_prefix.into(), command.cyan()];
lines.push(spans.into());
}
self.chat_widget.add_plain_history_lines(lines);
}
tui.frame_requester().schedule_frame();
true
}
if let Some(command) = summary.resume_hint {
let spans = vec!["To continue this session, run ".into(), command.cyan()];
lines.push(spans.into());
Err(err) => {
self.chat_widget
.add_error_message(format!("{attach_error_prefix}: {err}"));
tui.frame_requester().schedule_frame();
false
}
self.chat_widget.add_plain_history_lines(lines);
}
}
Err(err) => {
self.chat_widget.add_error_message(format!(
"Failed to start a fresh session through the app server: {err}"
));
self.chat_widget
.add_error_message(format!("{start_error_prefix}: {err}"));
self.config.model = Some(model);
tui.frame_requester().schedule_frame();
false
}
}
tui.frame_requester().schedule_frame();
}

pub(super) async fn replace_chat_widget_with_app_server_thread(
Expand Down Expand Up @@ -659,6 +697,123 @@ impl App {
config.service_tier = self.chat_widget.configured_service_tier();
config
}

pub(super) async fn switch_auth_profile(
&mut self,
tui: &mut tui::Tui,
app_server: &mut AppServerSession,
selection: AuthProfileSelection,
) {
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(),
);
self.chat_widget.add_info_message(
"Restart Codex Lab with `--auth-profile <name>` to use a profile with a shared or remote app server."
.to_string(),
/*hint*/ None,
);
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 =
match codex_login::profile_home(&self.config.codex_home, &profile_name) {
Ok(auth_home) => auth_home,
Err(err) => {
self.chat_widget.add_error_message(format!(
"Invalid auth profile {profile_name:?}: {err}"
));
return;
}
};
(auth_home, format!("`{profile_name}`"))
}
};

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,
);
return;
}

let mut config = self.fresh_session_config();
config.auth_home = match AbsolutePathBuf::from_absolute_path(auth_home) {
Ok(auth_home) => auth_home,
Err(err) => {
self.chat_widget.add_error_message(format!(
"Invalid auth profile home for {profile_label}: {err}"
));
return;
}
};

let replacement_client = match crate::start_embedded_app_server(
self.arg0_paths.clone(),
config.clone(),
self.cli_kv_overrides.clone(),
self.loader_overrides.clone(),
self.strict_config,
self.cloud_config_bundle.clone(),
self.feedback.clone(),
/*log_db*/ None,
self.state_db.clone(),
self.environment_manager.clone(),
)
.await
{
Ok(client) => AppServerClient::InProcess(client),
Err(err) => {
self.chat_widget.add_error_message(format!(
"Failed to start app server for auth profile {profile_label}: {err}"
));
return;
}
};

self.shutdown_current_thread(app_server).await;
let tracked_thread_ids: Vec<ThreadId> =
self.thread_event_channels.keys().copied().collect();
for thread_id in tracked_thread_ids {
if let Err(err) = app_server.thread_unsubscribe(thread_id).await {
tracing::warn!("failed to unsubscribe tracked thread {thread_id}: {err}");
}
}

if let Err(err) = app_server.replace_client(replacement_client).await {
self.chat_widget.add_error_message(format!(
"Failed to switch app server to auth profile {profile_label}: {err}"
));
return;
}

let switched = self
.start_fresh_session_with_config(
tui,
app_server,
config,
Some(ThreadStartSource::Clear),
/*initial_user_message*/ None,
/*cleanup_current_thread*/ false,
"To continue the previous session, run ",
"Failed to attach to auth profile session",
"Failed to start auth profile session",
)
.await;
if switched {
self.chat_widget.add_info_message(
format!("Using auth profile {profile_label} for this session."),
Some("The previous session remains resumable.".to_string()),
);
}
}

pub(super) async fn resume_target_session(
&mut self,
tui: &mut tui::Tui,
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/tui/src/app/test_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ pub(super) async fn make_test_app() -> App {
feedback_audience: FeedbackAudience::External,
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
app_server_target: crate::AppServerTarget::Embedded,
arg0_paths: Arg0DispatchPaths::default(),
strict_config: false,
cloud_config_bundle: CloudConfigBundleLoader::default(),
pending_update_action: None,
pending_shutdown_exit_thread_id: None,
windows_sandbox: WindowsSandboxState::default(),
Expand Down
Loading
Loading