diff --git a/codex-rs/core/src/config_loader/mod.rs b/codex-rs/core/src/config_loader/mod.rs index eba89fdb3d..838db0d5ff 100644 --- a/codex-rs/core/src/config_loader/mod.rs +++ b/codex-rs/core/src/config_loader/mod.rs @@ -25,6 +25,7 @@ use codex_protocol::config_types::TrustLevel; use codex_protocol::protocol::AskForApproval; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBufGuard; +use dunce::canonicalize as normalize_path; use serde::Deserialize; use std::io; use std::path::Path; @@ -236,6 +237,7 @@ pub async fn load_config_layers_state( &cwd, &project_trust_context.project_root, &project_trust_context, + codex_home, ) .await?; layers.extend(project_layers); @@ -671,7 +673,11 @@ async fn load_project_layers( cwd: &AbsolutePathBuf, project_root: &AbsolutePathBuf, trust_context: &ProjectTrustContext, + codex_home: &Path, ) -> io::Result> { + let codex_home_abs = AbsolutePathBuf::from_absolute_path(codex_home)?; + let codex_home_normalized = + normalize_path(codex_home_abs.as_path()).unwrap_or_else(|_| codex_home_abs.to_path_buf()); let mut dirs = cwd .as_path() .ancestors() @@ -702,6 +708,11 @@ async fn load_project_layers( let layer_dir = AbsolutePathBuf::from_absolute_path(dir)?; let decision = trust_context.decision_for_dir(&layer_dir); let dot_codex_abs = AbsolutePathBuf::from_absolute_path(&dot_codex)?; + let dot_codex_normalized = + normalize_path(dot_codex_abs.as_path()).unwrap_or_else(|_| dot_codex_abs.to_path_buf()); + if dot_codex_abs == codex_home_abs || dot_codex_normalized == codex_home_normalized { + continue; + } let config_file = dot_codex_abs.join(CONFIG_TOML_FILE)?; match tokio::fs::read_to_string(&config_file).await { Ok(contents) => { diff --git a/codex-rs/core/src/config_loader/tests.rs b/codex-rs/core/src/config_loader/tests.rs index 38d104fc19..355057f184 100644 --- a/codex-rs/core/src/config_loader/tests.rs +++ b/codex-rs/core/src/config_loader/tests.rs @@ -755,6 +755,101 @@ async fn project_layer_is_added_when_dot_codex_exists_without_config_toml() -> s Ok(()) } +#[tokio::test] +async fn codex_home_is_not_loaded_as_project_layer_from_home_dir() -> std::io::Result<()> { + let tmp = tempdir()?; + let home_dir = tmp.path().join("home"); + let codex_home = home_dir.join(".codex"); + tokio::fs::create_dir_all(&codex_home).await?; + tokio::fs::write(codex_home.join(CONFIG_TOML_FILE), "foo = \"user\"\n").await?; + + let cwd = AbsolutePathBuf::from_absolute_path(&home_dir)?; + let layers = load_config_layers_state( + &codex_home, + Some(cwd), + &[] as &[(String, TomlValue)], + LoaderOverrides::default(), + ) + .await?; + + let project_layers: Vec<_> = layers + .get_layers( + super::ConfigLayerStackOrdering::HighestPrecedenceFirst, + true, + ) + .into_iter() + .filter(|layer| matches!(layer.name, super::ConfigLayerSource::Project { .. })) + .collect(); + let expected: Vec<&ConfigLayerEntry> = Vec::new(); + assert_eq!(expected, project_layers); + assert_eq!( + layers.effective_config().get("foo"), + Some(&TomlValue::String("user".to_string())) + ); + + Ok(()) +} + +#[tokio::test] +async fn codex_home_within_project_tree_is_not_double_loaded() -> std::io::Result<()> { + let tmp = tempdir()?; + let project_root = tmp.path().join("project"); + let nested = project_root.join("child"); + let project_dot_codex = project_root.join(".codex"); + let nested_dot_codex = nested.join(".codex"); + + tokio::fs::create_dir_all(&nested_dot_codex).await?; + tokio::fs::create_dir_all(project_root.join(".git")).await?; + tokio::fs::write(nested_dot_codex.join(CONFIG_TOML_FILE), "foo = \"child\"\n").await?; + + tokio::fs::create_dir_all(&project_dot_codex).await?; + make_config_for_test(&project_dot_codex, &project_root, TrustLevel::Trusted, None).await?; + let user_config_path = project_dot_codex.join(CONFIG_TOML_FILE); + let user_config_contents = tokio::fs::read_to_string(&user_config_path).await?; + tokio::fs::write( + &user_config_path, + format!("foo = \"user\"\n{user_config_contents}"), + ) + .await?; + + let cwd = AbsolutePathBuf::from_absolute_path(&nested)?; + let layers = load_config_layers_state( + &project_dot_codex, + Some(cwd), + &[] as &[(String, TomlValue)], + LoaderOverrides::default(), + ) + .await?; + + let project_layers: Vec<_> = layers + .get_layers( + super::ConfigLayerStackOrdering::HighestPrecedenceFirst, + true, + ) + .into_iter() + .filter(|layer| matches!(layer.name, super::ConfigLayerSource::Project { .. })) + .collect(); + + let child_config: TomlValue = toml::from_str("foo = \"child\"\n").expect("parse child config"); + assert_eq!( + vec![&ConfigLayerEntry { + name: super::ConfigLayerSource::Project { + dot_codex_folder: AbsolutePathBuf::from_absolute_path(&nested_dot_codex)?, + }, + config: child_config.clone(), + version: version_for_toml(&child_config), + disabled_reason: None, + }], + project_layers + ); + assert_eq!( + layers.effective_config().get("foo"), + Some(&TomlValue::String("child".to_string())) + ); + + Ok(()) +} + #[tokio::test] async fn project_layers_disabled_when_untrusted_or_unknown() -> std::io::Result<()> { let tmp = tempdir()?; diff --git a/codex-rs/core/templates/collaboration_mode/plan.md b/codex-rs/core/templates/collaboration_mode/plan.md index 395b6c368b..f600bc9c7b 100644 --- a/codex-rs/core/templates/collaboration_mode/plan.md +++ b/codex-rs/core/templates/collaboration_mode/plan.md @@ -32,7 +32,6 @@ Actions that gather truth, reduce ambiguity, or validate feasibility without cha Actions that implement the plan or change repo-tracked state. Examples: * Editing or writing files -* Generating, updating, or accepting snapshots * Running formatters or linters that rewrite files * Applying patches, migrations, or codegen that updates repo-tracked files * Side-effectful commands whose purpose is to carry out the plan rather than refine it @@ -60,17 +59,13 @@ Do not ask questions that can be answered from the repo or system (for example, Every assistant turn MUST be exactly one of: A) a `request_user_input` tool call (questions/options only), OR -B) a non-final status update with no questions and no plan content, OR -C) the final output: a titled, plan-only document. +B) the final output: a titled, plan-only document. Rules: * No questions in free text (only via `request_user_input`). * Never mix a `request_user_input` call with plan content. -* Status updates must not include questions or plan content. -* Internal tool/repo exploration is allowed privately before A, B, or C. - -Status updates should be frequent during exploration. Provide 1-2 sentence updates that summarize discoveries, assumption changes, or why you are changing direction. Use Parallel tools for exploration. +* Internal tool/repo exploration is allowed privately before A or B. ## Ask a lot, but never ask trivia @@ -113,19 +108,15 @@ When you present the official plan, wrap it in a `` block so the Example: -# Plan title -- Step 1 -- Step 2 +plan content -The final plan must be plan-only and include: +plan content should be human and agent digestible. The final plan must be plan-only and include: * A clear title -* A TL;DR section (3–5 bullets) -* Exact file paths to change -* Exact structures or shapes to introduce or modify -* Exact function, method, type, and variable names and signatures -* Test cases +* tldr section. don't necessary call it tldr. +* Important changes or additions of signatures, structs, types. +* Test cases and scenarios * Explicit assumptions and defaults chosen where needed Do not ask "should I proceed?" in the final output. The user can easily switch out of Plan mode and request implementation if you have included a `` block in your response. Alternatively, they can decide to stay in Plan mode and continue refining the plan. diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index c65ce4498d..7ed9d0b035 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1154,6 +1154,7 @@ impl App { return Ok(AppExitInfo { token_usage: app.token_usage(), thread_id: app.chat_widget.thread_id(), + thread_name: app.chat_widget.thread_name(), update_action: app.pending_update_action, exit_reason, }); diff --git a/codex-rs/tui/src/style.rs b/codex-rs/tui/src/style.rs index 2b4ab4c4d3..fc8fd4c501 100644 --- a/codex-rs/tui/src/style.rs +++ b/codex-rs/tui/src/style.rs @@ -40,10 +40,5 @@ pub fn user_message_bg(terminal_bg: (u8, u8, u8)) -> Color { #[allow(clippy::disallowed_methods)] pub fn proposed_plan_bg(terminal_bg: (u8, u8, u8)) -> Color { - let (top, alpha) = if is_light(terminal_bg) { - ((0, 110, 150), 0.08) - } else { - ((80, 170, 220), 0.2) - }; - best_color(blend(top, terminal_bg, alpha)) + user_message_bg(terminal_bg) }