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
1 change: 1 addition & 0 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ mod session_lifecycle;
mod session_picker;
mod side;
mod startup;
pub(crate) mod startup_bootstrap;
mod startup_prompts;
mod startup_warnings;
mod thread_event_buffer;
Expand Down
94 changes: 64 additions & 30 deletions codex-rs/tui/src/app/startup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
//! Explicit local launch permissions remain runtime overrides across new sessions and reconnects.

use super::reconnect::ReconnectState;
use super::startup_bootstrap::bootstrap_server_owned_start;
use super::startup_bootstrap::uses_server_owned_fresh_bootstrap;
use super::*;
use crate::session_start::SessionStartAction;
use crate::session_start::SessionStartConfig;
Expand Down Expand Up @@ -56,13 +58,14 @@ pub(super) struct FreshStartupDefaults {
pub(super) async fn prepare_fresh_startup_config(
config: &mut Config,
app_server: &AppServerSession,
app_server_target: &AppServerTarget,
cli_kv_overrides: &[(String, TomlValue)],
harness_overrides: &ConfigOverrides,
environments: &EnvironmentManager,
) -> Result<FreshStartupDefaults> {
let defaults_cwd = match app_server.thread_params_mode() {
crate::app_server_session::ThreadParamsMode::Embedded => config.cwd.as_path(),
crate::app_server_session::ThreadParamsMode::Remote => {
let defaults_cwd = match app_server_target {
AppServerTarget::Embedded | AppServerTarget::LocalDaemon { .. } => config.cwd.as_path(),
AppServerTarget::Remote { .. } => {
app_server.remote_cwd_override().unwrap_or(Path::new("."))
}
};
Expand Down Expand Up @@ -106,8 +109,8 @@ pub(super) fn startup_model(
) -> String {
config.model.clone().unwrap_or_else(|| {
if server_defaults_read {
// Bootstrap was seeded with local config, which may differ from a cleared server
// model. Use the server's model catalog when config/read returned model: null.
// Legacy bootstrap paths may be seeded with local config, which can differ from a
// cleared server model. Use the server catalog when config/read returned model: null.
bootstrap
.available_models
.iter()
Expand Down Expand Up @@ -269,22 +272,27 @@ impl App {
let harness_overrides =
normalize_harness_overrides_for_cwd(harness_overrides, &config.cwd)?;
app_server.model_provider_override = harness_overrides.model_provider.clone();
let bootstrap = match startup_bootstrap {
Some(bootstrap) => bootstrap,
None => match startup_draft
.run_until(tui, app_server.bootstrap(&config))
.await
{
Ok(bootstrap) => bootstrap?,
Err(err) => return shutdown_on_startup_error(app_server, err).await,
},
};
tracing::debug!(
has_platform_family = app_server.app_server_platform_family().is_some(),
has_platform_os = app_server.app_server_platform_os().is_some(),
"connected app-server platform"
let fresh_start = matches!(
&session_selection,
SessionSelection::StartFresh | SessionSelection::Exit
);
let bootstrap_ms = bootstrap.duration.as_millis();
let server_owned_fresh_bootstrap = uses_server_owned_fresh_bootstrap(
&app_server_target,
&session_selection,
&loader_overrides,
);
let mut bootstrap = startup_bootstrap;
if bootstrap.is_none() && !server_owned_fresh_bootstrap {
bootstrap = Some(
match startup_draft
.run_until(tui, app_server.bootstrap(&config))
.await
{
Ok(bootstrap) => bootstrap?,
Err(err) => return shutdown_on_startup_error(app_server, err).await,
},
);
}
if matches!(&session_selection, SessionSelection::Fork(_)) {
// The app server resolves omitted overrides from the fork destination's config.
if harness_overrides.model.is_none()
Expand All @@ -299,16 +307,14 @@ impl App {
config.model_reasoning_effort = None;
}
}
let startup_defaults = if matches!(
&session_selection,
SessionSelection::StartFresh | SessionSelection::Exit
) {
let startup_defaults = if fresh_start {
match startup_draft
.run_until(
tui,
prepare_fresh_startup_config(
&mut config,
&app_server,
&app_server_target,
&cli_kv_overrides,
&harness_overrides,
&environment_manager,
Expand All @@ -323,19 +329,47 @@ impl App {
} else {
FreshStartupDefaults::default()
};
if matches!(&session_selection, SessionSelection::AgentsOverview) {
let mut launch_choices = crate::app_server_session::StartupLaunchChoices::from_launch(
&cli_kv_overrides,
&harness_overrides,
&loader_overrides,
);
let bootstrap = match bootstrap {
Some(bootstrap) => bootstrap,
None => match startup_draft
.run_until(
tui,
bootstrap_server_owned_start(
&mut app_server,
&mut config,
&mut launch_choices,
startup_defaults.server_defaults_read,
&cli_kv_overrides,
&harness_overrides,
),
)
.await
{
Ok(bootstrap) => bootstrap?,
Err(err) => return shutdown_on_startup_error(app_server, err).await,
},
};
tracing::debug!(
has_platform_family = app_server.app_server_platform_family().is_some(),
has_platform_os = app_server.app_server_platform_os().is_some(),
"connected app-server platform"
);
let bootstrap_ms = bootstrap.duration.as_millis();
if !server_owned_fresh_bootstrap
&& matches!(&session_selection, SessionSelection::AgentsOverview)
{
apply_managed_new_thread_defaults(
&mut config,
app_server.managed_new_thread_defaults(),
&cli_kv_overrides,
&harness_overrides,
);
}
let mut launch_choices = crate::app_server_session::StartupLaunchChoices::from_launch(
&cli_kv_overrides,
&harness_overrides,
&loader_overrides,
);
let mut model = startup_model(&config, &bootstrap, startup_defaults.server_defaults_read);
let available_models = bootstrap.available_models;
let remote_connection = crate::status::remote_connection::remote_connection_status_value(
Expand Down
56 changes: 56 additions & 0 deletions codex-rs/tui/src/app/startup_bootstrap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//! Server-owned model selection for connected fresh starts.
//!
//! Embedded and explicit-profile starts retain legacy behavior. Unsupported `config/read` never
//! restores an implicit local model, while managed new-thread defaults may satisfy an empty catalog.

use super::*;
use crate::app_server_session::StartupLaunchChoices;

pub(crate) fn uses_server_owned_fresh_bootstrap(
target: &AppServerTarget,
selection: &SessionSelection,
loader_overrides: &LoaderOverrides,
) -> bool {
!matches!(target, AppServerTarget::Embedded)
&& matches!(
selection,
SessionSelection::StartFresh | SessionSelection::Exit
)
&& loader_overrides.user_config_profile.is_none()
}

pub(super) async fn bootstrap_server_owned_start(
app_server: &mut AppServerSession,
config: &mut Config,
launch_choices: &mut StartupLaunchChoices,
server_defaults_read: bool,
cli_kv_overrides: &[(String, TomlValue)],
harness_overrides: &ConfigOverrides,
) -> Result<AppServerBootstrap> {
if !server_defaults_read {
config.model = launch_choices.model.clone();
if launch_choices.effort.is_none() {
config.model_reasoning_effort = None;
}
}
let bootstrap = if launch_choices.model.is_none() && launch_choices.effort.is_none() {
app_server.bootstrap_for_new_thread(config).await
} else {
app_server.bootstrap(config).await
}?;
let previous = (config.model.clone(), config.model_reasoning_effort.clone());
apply_managed_new_thread_defaults(
config,
app_server.managed_new_thread_defaults(),
cli_kv_overrides,
harness_overrides,
);
if (config.model.clone(), config.model_reasoning_effort.clone()) != previous {
launch_choices.model = config.model.clone();
launch_choices.effort = config
.model_reasoning_effort
.as_ref()
.map(|effort| serde_json::json!(effort));
}
Ok(bootstrap)
}
1 change: 1 addition & 0 deletions codex-rs/tui/src/app/tests/projectless_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ async fn local_projectless_defaults_respect_trust_scope_and_explicit_settings()
let startup = crate::app::startup::prepare_fresh_startup_config(
&mut config,
&server,
&AppServerTarget::Embedded,
&[],
&overrides,
&EnvironmentManager::default_for_tests(),
Expand Down
39 changes: 35 additions & 4 deletions codex-rs/tui/src/app/tests/startup_defaults_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@
use super::*;
use crate::app::startup::prepare_fresh_startup_config;
use crate::app::startup::startup_model;
use crate::app::startup_bootstrap::bootstrap_server_owned_start;
use pretty_assertions::assert_eq;

fn remote_target() -> AppServerTarget {
AppServerTarget::Remote {
endpoint: crate::RemoteAppServerEndpoint::WebSocket {
websocket_url: "ws://127.0.0.1:1".into(),
auth_token: None,
},
}
}

async fn run_startup_for_test(
tui: &mut crate::tui::Tui,
server: AppServerSession,
Expand Down Expand Up @@ -268,7 +278,7 @@ async fn fresh_startup_uses_server_defaults_with_explicit_and_managed_precedence
Some("low"),
None,
),
("managed", true, None, None, None),
("managed", true, Some("managed-model"), Some("medium"), None),
] {
let client_home = tempdir()?;
let server_home = tempdir()?;
Expand All @@ -287,7 +297,11 @@ requires_openai_auth = false
)?;
std::fs::write(
server_home.path().join("config.toml"),
"model = \"server-model\"\nmodel_reasoning_effort = \"high\"\n",
if choice == "managed" {
""
} else {
"model = \"server-model\"\nmodel_reasoning_effort = \"high\"\n"
},
)?;
if managed {
std::fs::write(
Expand Down Expand Up @@ -341,6 +355,7 @@ requires_openai_auth = false
let mut server_config = config.clone();
server_config.codex_home = server_home.path().to_path_buf().abs();
server_config.sqlite = SqliteConfig::new_for_testing(server_home.path().abs());
server_config.model_catalog = Some(Default::default());
let (mut server, requests, proxy) = start_recording_app_server_with_history(
&server_config,
HistoryCapabilities::Current,
Expand All @@ -355,18 +370,28 @@ requires_openai_auth = false
)
.await?;
server = server.with_remote_cwd_override(Some(server_config.cwd.to_path_buf()));
server.bootstrap(&config).await?;
let>;
assert!(
prepare_fresh_startup_config(
&mut config,
&server,
&target,
&cli_kv_overrides,
&harness_overrides,
&EnvironmentManager::default_for_tests()
&EnvironmentManager::default_for_tests(),
)
.await?
.server_defaults_read
);
bootstrap_server_owned_start(
&mut server,
&mut config,
&mut launch_choices,
/*server_defaults_read*/ true,
&cli_kv_overrides,
&harness_overrides,
)
.await?;
if choice == "migration" {
let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
apply_accepted_model_migration(
Expand Down Expand Up @@ -470,11 +495,17 @@ async fn fresh_startup_reads_destination_and_cleared_model_uses_catalog() -> Res
server.start_thread(&config).await?;
}
assert!(config.config_layer_stack.is_projectless());
let remote {
remote_target()
} else {
AppServerTarget::Embedded
};
let bootstrap = server.bootstrap(&config).await?;
assert_eq!(bootstrap.default_model, "stale-client-model");
let defaults_read = prepare_fresh_startup_config(
&mut config,
&server,
&target,
&[],
&ConfigOverrides::default(),
&EnvironmentManager::default_for_tests(),
Expand Down
35 changes: 35 additions & 0 deletions codex-rs/tui/src/app_server_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@ pub(crate) enum ForkPermissionMode {
OverrideFromCurrentConfig,
}

enum EmptyCatalogFallback {
Error,
ManagedNewThread,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ForkPresentation {
Regular,
Expand Down Expand Up @@ -642,13 +647,36 @@ impl AppServerSession {
Ok(bootstrap)
}

pub(crate) async fn bootstrap_for_new_thread(
&mut self,
config: &Config,
) -> Result<AppServerBootstrap> {
let started_at = Instant::now();
let account = self.read_account().await?;
let mut bootstrap = self
.bootstrap_with_account_inner(config, account, EmptyCatalogFallback::ManagedNewThread)
.await?;
bootstrap.duration = started_at.elapsed();
Ok(bootstrap)
}

/// Bootstraps using a previously read account.
///
/// Callers must discard a prefetched account after authentication, server, or provider changes.
pub(crate) async fn bootstrap_with_account(
&mut self,
config: &Config,
account: GetAccountResponse,
) -> Result<AppServerBootstrap> {
self.bootstrap_with_account_inner(config, account, EmptyCatalogFallback::Error)
.await
}

async fn bootstrap_with_account_inner(
&mut self,
config: &Config,
account: GetAccountResponse,
fallback: EmptyCatalogFallback,
) -> Result<AppServerBootstrap> {
let started_at = Instant::now();
// `hooks/list` holds the global config queue during startup. Submit models and config
Expand Down Expand Up @@ -708,6 +736,13 @@ impl AppServerSession {
.map(|model| model.model.clone())
})
.or_else(|| available_models.first().map(|model| model.model.clone()))
.or_else(|| match fallback {
EmptyCatalogFallback::Error => None,
EmptyCatalogFallback::ManagedNewThread => self
.managed_new_thread_defaults
.as_ref()
.and_then(|defaults| defaults.model.clone()),
})
.wrap_err("No models are available. Set `model` explicitly or check your model catalog configuration.")?;
self.default_model = Some(default_model.clone());
self.available_models = available_models.clone();
Expand Down
Loading
Loading