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
59 changes: 57 additions & 2 deletions codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ pub struct ConfigBuilder {
cli_overrides: Option<Vec<(String, TomlValue)>>,
harness_overrides: Option<ConfigOverrides>,
loader_overrides: Option<LoaderOverrides>,
fallback_cwd: Option<PathBuf>,
}

impl ConfigBuilder {
Expand All @@ -422,21 +423,29 @@ impl ConfigBuilder {
self
}

pub fn fallback_cwd(mut self, fallback_cwd: Option<PathBuf>) -> Self {
self.fallback_cwd = fallback_cwd;
self
}

pub async fn build(self) -> std::io::Result<Config> {
let Self {
codex_home,
cli_overrides,
harness_overrides,
loader_overrides,
fallback_cwd,
} = self;
let codex_home = codex_home.map_or_else(find_codex_home, std::io::Result::Ok)?;
let cli_overrides = cli_overrides.unwrap_or_default();
let harness_overrides = harness_overrides.unwrap_or_default();
let mut harness_overrides = harness_overrides.unwrap_or_default();
let loader_overrides = loader_overrides.unwrap_or_default();
let cwd = match harness_overrides.cwd.as_deref() {
let cwd_override = harness_overrides.cwd.as_deref().or(fallback_cwd.as_deref());
let cwd = match cwd_override {
Some(path) => AbsolutePathBuf::try_from(path)?,
None => AbsolutePathBuf::current_dir()?,
};
harness_overrides.cwd = Some(cwd.to_path_buf());
let config_layer_stack =
load_config_layers_state(&codex_home, Some(cwd), &cli_overrides, loader_overrides)
.await?;
Expand Down Expand Up @@ -2301,6 +2310,52 @@ trust_level = "trusted"
Ok(())
}

#[tokio::test]
async fn project_profile_overrides_user_profile() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let workspace = TempDir::new()?;
let workspace_key = workspace.path().to_string_lossy().replace('\\', "\\\\");
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
format!(
r#"
profile = "global"

[profiles.global]
model = "gpt-global"

[profiles.project]
model = "gpt-project"

[projects."{workspace_key}"]
trust_level = "trusted"
"#,
),
)?;
let project_config_dir = workspace.path().join(".codex");
std::fs::create_dir_all(&project_config_dir)?;
std::fs::write(
project_config_dir.join(CONFIG_TOML_FILE),
r#"
profile = "project"
"#,
)?;

let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.harness_overrides(ConfigOverrides {
cwd: Some(workspace.path().to_path_buf()),
..Default::default()
})
.build()
.await?;

assert_eq!(config.active_profile.as_deref(), Some("project"));
assert_eq!(config.model.as_deref(), Some("gpt-project"));

Ok(())
}

#[test]
fn profile_sandbox_mode_overrides_base() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ pub use rollout::list::ThreadSortKey;
pub use rollout::list::ThreadsPage;
pub use rollout::list::parse_cursor;
pub use rollout::list::read_head_for_summary;
pub use rollout::list::read_session_meta_line;
mod function_tool;
mod state;
mod tasks;
Expand Down
19 changes: 19 additions & 0 deletions codex-rs/core/src/rollout/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::protocol::EventMsg;
use codex_file_search as file_search;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::RolloutLine;
use codex_protocol::protocol::SessionMetaLine;
use codex_protocol::protocol::SessionSource;

/// Returned page of thread (thread) summaries.
Expand Down Expand Up @@ -744,6 +745,24 @@ pub async fn read_head_for_summary(path: &Path) -> io::Result<Vec<serde_json::Va
Ok(summary.head)
}

/// Read the SessionMetaLine from the head of a rollout file for reuse by
/// callers that need the session metadata (e.g. to derive a cwd for config).
pub async fn read_session_meta_line(path: &Path) -> io::Result<SessionMetaLine> {
let head = read_head_for_summary(path).await?;
let Some(first) = head.first() else {
return Err(io::Error::other(format!(
"rollout at {} is empty",
path.display()
)));
};
serde_json::from_value::<SessionMetaLine>(first.clone()).map_err(|_| {
io::Error::other(format!(
"rollout at {} does not start with session metadata",
path.display()
))
})
}

async fn file_modified_time(path: &Path) -> io::Result<Option<OffsetDateTime>> {
let meta = tokio::fs::metadata(path).await?;
let modified = meta.modified().ok();
Expand Down
68 changes: 51 additions & 17 deletions codex-rs/tui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ use codex_core::RolloutRecorder;
use codex_core::ThreadSortKey;
use codex_core::auth::enforce_login_restrictions;
use codex_core::config::Config;
use codex_core::config::ConfigBuilder;
use codex_core::config::ConfigOverrides;
use codex_core::config::find_codex_home;
use codex_core::config::load_config_as_toml_with_cli_overrides;
use codex_core::config::resolve_oss_provider;
use codex_core::find_thread_path_by_id_str;
use codex_core::get_platform_sandbox;
use codex_core::protocol::AskForApproval;
use codex_core::read_session_meta_line;
use codex_core::terminal::Multiplexer;
use codex_protocol::config_types::AltScreenMode;
use codex_protocol::config_types::SandboxMode;
Expand Down Expand Up @@ -246,7 +248,6 @@ pub async fn run_main(
std::process::exit(1);
}

let active_profile = config.active_profile.clone();
let log_dir = codex_core::config::log_dir(&config)?;
std::fs::create_dir_all(&log_dir)?;
// Open (or create) your log file, appending to it.
Expand Down Expand Up @@ -336,24 +337,16 @@ pub async fn run_main(
.with(otel_tracing_layer)
.try_init();

run_ratatui_app(
cli,
config,
overrides,
cli_kv_overrides,
active_profile,
feedback,
)
.await
.map_err(|err| std::io::Error::other(err.to_string()))
run_ratatui_app(cli, config, overrides, cli_kv_overrides, feedback)
.await
.map_err(|err| std::io::Error::other(err.to_string()))
}

async fn run_ratatui_app(
cli: Cli,
initial_config: Config,
overrides: ConfigOverrides,
cli_kv_overrides: Vec<(String, toml::Value)>,
active_profile: Option<String>,
feedback: codex_feedback::CodexFeedback,
) -> color_eyre::Result<AppExitInfo> {
color_eyre::install()?;
Expand Down Expand Up @@ -404,15 +397,15 @@ async fn run_ratatui_app(
initial_config.cli_auth_credentials_store_mode,
);
let login_status = get_login_status(&initial_config);
let should_show_trust_screen = should_show_trust_screen(&initial_config);
let should_show_trust_screen_flag = should_show_trust_screen(&initial_config);
let should_show_onboarding =
should_show_onboarding(login_status, &initial_config, should_show_trust_screen);
should_show_onboarding(login_status, &initial_config, should_show_trust_screen_flag);

let config = if should_show_onboarding {
let onboarding_result = run_onboarding_app(
OnboardingScreenArgs {
show_login_screen: should_show_login_screen(login_status, &initial_config),
show_trust_screen: should_show_trust_screen,
show_trust_screen: should_show_trust_screen_flag,
login_status,
auth_manager: auth_manager.clone(),
config: initial_config.clone(),
Expand All @@ -437,7 +430,7 @@ async fn run_ratatui_app(
.map(|d| d == TrustDirectorySelection::Trust)
.unwrap_or(false)
{
load_config_or_exit(cli_kv_overrides, overrides).await
load_config_or_exit(cli_kv_overrides.clone(), overrides.clone()).await
} else {
initial_config
}
Expand Down Expand Up @@ -570,6 +563,33 @@ async fn run_ratatui_app(
resume_picker::SessionSelection::StartFresh
};

let config = match &session_selection {
resume_picker::SessionSelection::Resume(path)
| resume_picker::SessionSelection::Fork(path) => {
let history_cwd = match read_session_meta_line(path).await {
Ok(meta_line) => Some(meta_line.meta.cwd),
Err(err) => {
let rollout_path = path.display().to_string();
tracing::warn!(
%rollout_path,
%err,
"Failed to read session metadata from rollout"
);
None
}
};
load_config_or_exit_with_fallback_cwd(
cli_kv_overrides.clone(),
overrides.clone(),
history_cwd,
)
.await
}
_ => config,
};
let active_profile = config.active_profile.clone();
let should_show_trust_screen = should_show_trust_screen(&config);

let Cli {
prompt,
images,
Expand Down Expand Up @@ -671,9 +691,23 @@ fn get_login_status(config: &Config) -> LoginStatus {
async fn load_config_or_exit(
cli_kv_overrides: Vec<(String, toml::Value)>,
overrides: ConfigOverrides,
) -> Config {
load_config_or_exit_with_fallback_cwd(cli_kv_overrides, overrides, None).await
}

async fn load_config_or_exit_with_fallback_cwd(
cli_kv_overrides: Vec<(String, toml::Value)>,
overrides: ConfigOverrides,
fallback_cwd: Option<PathBuf>,
) -> Config {
#[allow(clippy::print_stderr)]
match Config::load_with_cli_overrides_and_harness_overrides(cli_kv_overrides, overrides).await {
match ConfigBuilder::default()
.cli_overrides(cli_kv_overrides)
.harness_overrides(overrides)
.fallback_cwd(fallback_cwd)
.build()
.await
{
Ok(config) => config,
Err(err) => {
eprintln!("Error loading configuration: {err}");
Expand Down
Loading
Loading