Compare commits

...

16 Commits

Author SHA1 Message Date
pap
cec51479de functions return either path or content, therefore function returning content re-use path functions 2025-08-07 17:04:26 +01:00
pap-openai
b3130e324d Merge branch 'main' into feature/agents-md-launch 2025-08-07 16:55:10 +01:00
pap
5bfd4242b8 remove agents.md at launch but show in /status 2025-08-07 16:54:58 +01:00
pap
4697623888 fmt 2025-08-07 12:23:04 +01:00
pap
159510fa38 status rendering 2025-08-07 12:17:15 +01:00
pap
5f41b6040f Merge branch 'main' into feature/agents-md-launch 2025-08-07 12:06:09 +01:00
pap
a4aeba1282 fixing rendering behavior 2025-08-07 11:41:17 +01:00
Ed Bayes
4d0dc29f3e Update copy (#1935)
Updated copy

---------

Co-authored-by: pap-openai <pap@openai.com>
2025-08-07 11:36:48 +01:00
Charlie Weems
39f47f2733 Fix opacity of agents.md string 2025-08-07 02:07:34 -07:00
Charlie Weems
7b94347761 Merge branch 'main' into feature/agents-md-launch 2025-08-07 02:03:17 -07:00
pap-openai
d4db71466f Merge branch 'main' into feature/agents-md-launch 2025-08-07 08:53:15 +01:00
easong-openai
ca4cfbe9ef Merge branch 'main' into feature/agents-md-launch 2025-08-07 00:42:04 -07:00
pap
c974655475 fixing /status output 2025-08-07 00:57:02 +01:00
pap
b75fb20be4 fmt 2025-08-07 00:42:05 +01:00
pap
773ab49050 make the display better 2025-08-07 00:39:02 +01:00
pap
72f54926ad showing agents.md at start 2025-08-07 00:20:56 +01:00
4 changed files with 196 additions and 75 deletions

View File

@@ -29,6 +29,9 @@ use toml::Value as TomlValue;
/// the context window. /// the context window.
pub(crate) const PROJECT_DOC_MAX_BYTES: usize = 32 * 1024; // 32 KiB pub(crate) const PROJECT_DOC_MAX_BYTES: usize = 32 * 1024; // 32 KiB
/// Candidate filenames for user-provided instructions stored in CODEX_HOME.
pub(crate) const USER_INSTRUCTIONS_CANDIDATE_FILENAMES: &[&str] = &["AGENTS.md"];
/// Application configuration loaded from disk and merged with overrides. /// Application configuration loaded from disk and merged with overrides.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct Config { pub struct Config {
@@ -576,13 +579,9 @@ impl Config {
} }
fn load_instructions(codex_dir: Option<&Path>) -> Option<String> { fn load_instructions(codex_dir: Option<&Path>) -> Option<String> {
let mut p = match codex_dir { let dir = codex_dir?;
Some(p) => p.to_path_buf(), let path = find_user_instructions_path(dir)?;
None => return None, std::fs::read_to_string(&path).ok().and_then(|s| {
};
p.push("AGENTS.md");
std::fs::read_to_string(&p).ok().and_then(|s| {
let s = s.trim(); let s = s.trim();
if s.is_empty() { if s.is_empty() {
None None
@@ -674,6 +673,20 @@ pub fn log_dir(cfg: &Config) -> std::io::Result<PathBuf> {
Ok(p) Ok(p)
} }
/// Returns the path to the first non-empty user instructions file inside the
/// provided CODEX_HOME directory, if any.
pub(crate) fn find_user_instructions_path(codex_dir: &Path) -> Option<PathBuf> {
for name in USER_INSTRUCTIONS_CANDIDATE_FILENAMES {
let candidate = codex_dir.join(name);
if let Ok(s) = std::fs::read_to_string(&candidate) {
if !s.trim().is_empty() {
return Some(candidate);
}
}
}
None
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)] #![allow(clippy::expect_used, clippy::unwrap_used)]

View File

@@ -38,7 +38,7 @@ mod models;
mod openai_model_info; mod openai_model_info;
mod openai_tools; mod openai_tools;
pub mod plan_tool; pub mod plan_tool;
mod project_doc; pub mod project_doc;
pub mod protocol; pub mod protocol;
mod rollout; mod rollout;
pub(crate) mod safety; pub(crate) mod safety;

View File

@@ -12,7 +12,9 @@
//! exists, the search stops we do **not** walk past the Git root. //! exists, the search stops we do **not** walk past the Git root.
use crate::config::Config; use crate::config::Config;
use crate::config::find_user_instructions_path;
use std::path::Path; use std::path::Path;
use std::path::PathBuf;
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
use tracing::error; use tracing::error;
@@ -50,86 +52,166 @@ pub(crate) async fn get_user_instructions(config: &Config) -> Option<String> {
/// `Err` so callers can decide how to handle them. /// `Err` so callers can decide how to handle them.
async fn find_project_doc(config: &Config) -> std::io::Result<Option<String>> { async fn find_project_doc(config: &Config) -> std::io::Result<Option<String>> {
let max_bytes = config.project_doc_max_bytes; let max_bytes = config.project_doc_max_bytes;
if max_bytes == 0 {
// Attempt to load from the working directory first. return Ok(None);
if let Some(doc) = load_first_candidate(&config.cwd, CANDIDATE_FILENAMES, max_bytes).await? {
return Ok(Some(doc));
} }
// Walk up towards the filesystem root, stopping once we encounter the Git if let Some(path) = find_project_doc_path_async(config).await {
// repository root. The presence of **either** a `.git` *file* or return read_file_with_limit(&path, max_bytes).await;
// *directory* counts.
let mut dir = config.cwd.clone();
// Canonicalize the path so that we do not end up in an infinite loop when
// `cwd` contains `..` components.
if let Ok(canon) = dir.canonicalize() {
dir = canon;
}
while let Some(parent) = dir.parent() {
// `.git` can be a *file* (for worktrees or submodules) or a *dir*.
let git_marker = dir.join(".git");
let git_exists = match tokio::fs::metadata(&git_marker).await {
Ok(_) => true,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => false,
Err(e) => return Err(e),
};
if git_exists {
// We are at the repo root attempt one final load.
if let Some(doc) = load_first_candidate(&dir, CANDIDATE_FILENAMES, max_bytes).await? {
return Ok(Some(doc));
}
break;
}
dir = parent.to_path_buf();
} }
Ok(None) Ok(None)
} }
/// Attempt to load the first candidate file found in `dir`. Returns the file /// Lightweight description of where user and project instruction files were
/// contents (truncated if it exceeds `max_bytes`) when successful. /// sourced from. Paths are absolute and only present when a corresponding file
async fn load_first_candidate( /// exists and is non-empty.
dir: &Path, #[derive(Debug, Clone, PartialEq, Eq)]
names: &[&str], pub struct InstructionsInfo {
max_bytes: usize, pub user_instructions_path: Option<PathBuf>,
) -> std::io::Result<Option<String>> { pub project_instructions_path: Option<PathBuf>,
for name in names { }
let candidate = dir.join(name);
let file = match tokio::fs::File::open(&candidate).await { /// Asynchronously collect the paths of the user instructions (from
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue, /// `CODEX_HOME/AGENTS.md`) and the project instructions (nearest `AGENTS.md`
Err(e) => return Err(e), /// discovered by [`find_project_doc`]'s search algorithm).
Ok(f) => f, ///
}; /// - If `project_doc_max_bytes == 0` we consider project docs disabled and the
/// path will be `None` even if a file exists.
/// - Empty files are treated as "not set".
pub async fn collect_instructions_info(config: &Config) -> InstructionsInfo {
let user_instructions_path = find_user_instructions_path(&config.codex_home);
let size = file.metadata().await?.len(); let project_instructions_path = if config.project_doc_max_bytes == 0 {
None
} else {
find_project_doc_path_async(config).await
};
let reader = tokio::io::BufReader::new(file); InstructionsInfo {
let mut data = Vec::with_capacity(std::cmp::min(size as usize, max_bytes)); user_instructions_path,
let mut limited = reader.take(max_bytes as u64); project_instructions_path,
limited.read_to_end(&mut data).await?; }
}
if size as usize > max_bytes { /// Internal helper that mirrors the search performed by [`find_project_doc`] but
tracing::warn!( /// returns the discovered file path instead of the contents.
"Project doc `{}` exceeds {max_bytes} bytes - truncating.", async fn find_project_doc_path_async(config: &Config) -> Option<PathBuf> {
candidate.display(), // Attempt in cwd first.
); if let Some(p) = find_first_candidate_path_async(&config.cwd, CANDIDATE_FILENAMES).await {
} return Some(p);
let contents = String::from_utf8_lossy(&data).to_string();
if contents.trim().is_empty() {
// Empty file treat as not found.
continue;
}
return Ok(Some(contents));
} }
Ok(None) // Walk up towards git root, then stop.
let mut dir = match config.cwd.canonicalize() {
Ok(c) => c,
Err(_) => config.cwd.clone(),
};
while let Some(parent) = dir.parent() {
let git_marker = dir.join(".git");
let git_exists = match tokio::fs::metadata(&git_marker).await {
Ok(_) => true,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => false,
Err(_) => false,
};
if git_exists {
return find_first_candidate_path_async(&dir, CANDIDATE_FILENAMES).await;
}
dir = parent.to_path_buf();
}
None
}
async fn find_first_candidate_path_async(dir: &Path, names: &[&str]) -> Option<PathBuf> {
for name in names {
let candidate = dir.join(name);
match tokio::fs::read_to_string(&candidate).await {
Ok(s) if !s.trim().is_empty() => return Some(candidate),
_ => continue,
}
}
None
}
/// Synchronous variant for UI code that isn't async-aware. Mirrors
/// [`collect_instructions_info`] using blocking filesystem APIs.
pub fn collect_instructions_info_sync(config: &Config) -> InstructionsInfo {
let user_instructions_path = find_user_instructions_path(&config.codex_home);
let project_instructions_path = if config.project_doc_max_bytes == 0 {
None
} else {
find_project_doc_path_sync(config)
};
InstructionsInfo {
user_instructions_path,
project_instructions_path,
}
}
fn find_project_doc_path_sync(config: &Config) -> Option<PathBuf> {
// Try cwd first
if let Some(p) = find_first_candidate_path_sync(&config.cwd, CANDIDATE_FILENAMES) {
return Some(p);
}
let mut dir = config
.cwd
.canonicalize()
.unwrap_or_else(|_| config.cwd.clone());
while let Some(parent) = dir.parent() {
let git_marker = dir.join(".git");
let git_exists = match std::fs::metadata(&git_marker) {
Ok(_) => true,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => false,
Err(_) => false,
};
if git_exists {
return find_first_candidate_path_sync(&dir, CANDIDATE_FILENAMES);
}
dir = parent.to_path_buf();
}
None
}
fn find_first_candidate_path_sync(dir: &Path, names: &[&str]) -> Option<PathBuf> {
for name in names {
let candidate = dir.join(name);
match std::fs::read_to_string(&candidate) {
Ok(s) if !s.trim().is_empty() => return Some(candidate),
_ => continue,
}
}
None
}
/// Read a file with a maximum byte limit; empty content returns None.
async fn read_file_with_limit(path: &Path, max_bytes: usize) -> std::io::Result<Option<String>> {
let file = match tokio::fs::File::open(path).await {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(e),
Ok(f) => f,
};
let size = file.metadata().await?.len();
let reader = tokio::io::BufReader::new(file);
let mut data = Vec::with_capacity(std::cmp::min(size as usize, max_bytes));
let mut limited = reader.take(max_bytes as u64);
limited.read_to_end(&mut data).await?;
if size as usize > max_bytes {
tracing::warn!(
"Project doc `{}` exceeds {max_bytes} bytes - truncating.",
path.display(),
);
}
let contents = String::from_utf8_lossy(&data).to_string();
if contents.trim().is_empty() {
return Ok(None);
}
Ok(Some(contents))
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -11,6 +11,7 @@ use codex_core::config::Config;
use codex_core::plan_tool::PlanItemArg; use codex_core::plan_tool::PlanItemArg;
use codex_core::plan_tool::StepStatus; use codex_core::plan_tool::StepStatus;
use codex_core::plan_tool::UpdatePlanArgs; use codex_core::plan_tool::UpdatePlanArgs;
use codex_core::project_doc::collect_instructions_info_sync;
use codex_core::protocol::FileChange; use codex_core::protocol::FileChange;
use codex_core::protocol::McpInvocation; use codex_core::protocol::McpInvocation;
use codex_core::protocol::SandboxPolicy; use codex_core::protocol::SandboxPolicy;
@@ -511,6 +512,31 @@ impl HistoryCell {
None => config.cwd.display().to_string(), None => config.cwd.display().to_string(),
}; };
lines.push(Line::from(vec![" • Path: ".into(), cwd_str.into()])); lines.push(Line::from(vec![" • Path: ".into(), cwd_str.into()]));
// If instructions are configured, show the source paths at the top.
let info = collect_instructions_info_sync(config);
if info.user_instructions_path.is_some() || info.project_instructions_path.is_some() {
let render_path = |p: &std::path::PathBuf| -> String {
match relativize_to_home(p) {
Some(rel) if !rel.as_os_str().is_empty() => format!("~/{}", rel.display()),
_ => p.display().to_string(),
}
};
let mut parts: Vec<String> = Vec::new();
if let Some(p) = info.user_instructions_path.as_ref() {
parts.push(render_path(p));
}
if let Some(p) = info.project_instructions_path.as_ref() {
parts.push(render_path(p));
}
let joined = if parts.len() == 2 {
format!("{} + {}", parts[0], parts[1])
} else {
parts.join("")
};
lines.push(Line::from(vec![" • AGENTS.md: ".into(), joined.into()]));
}
// Approval mode (as-is) // Approval mode (as-is)
lines.push(Line::from(vec![ lines.push(Line::from(vec![
" • Approval Mode: ".into(), " • Approval Mode: ".into(),