mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
Compare commits
3 Commits
response-a
...
nornagon/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00d9203eeb | ||
|
|
52f8d08cef | ||
|
|
22aefec50e |
7
codex-rs/Cargo.lock
generated
7
codex-rs/Cargo.lock
generated
@@ -887,6 +887,7 @@ dependencies = [
|
||||
"mcp-types",
|
||||
"once_cell",
|
||||
"path-clean",
|
||||
"pathdiff",
|
||||
"pretty_assertions",
|
||||
"rand 0.8.5",
|
||||
"ratatui",
|
||||
@@ -3130,6 +3131,12 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathdiff"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
|
||||
@@ -43,7 +43,7 @@ mod models;
|
||||
mod openai_model_info;
|
||||
mod openai_tools;
|
||||
pub mod plan_tool;
|
||||
mod project_doc;
|
||||
pub mod project_doc;
|
||||
pub mod protocol;
|
||||
mod rollout;
|
||||
pub(crate) mod safety;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
use crate::config::Config;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tracing::error;
|
||||
|
||||
@@ -26,12 +27,19 @@ const PROJECT_DOC_SEPARATOR: &str = "\n\n--- project-doc ---\n\n";
|
||||
/// Combines `Config::instructions` and `AGENTS.md` (if present) into a single
|
||||
/// string of instructions.
|
||||
pub(crate) async fn get_user_instructions(config: &Config) -> Option<String> {
|
||||
match find_project_doc(config).await {
|
||||
Ok(Some(project_doc)) => match &config.user_instructions {
|
||||
Some(original_instructions) => Some(format!(
|
||||
"{original_instructions}{PROJECT_DOC_SEPARATOR}{project_doc}"
|
||||
)),
|
||||
None => Some(project_doc),
|
||||
match find_project_doc(&config.cwd).await {
|
||||
Ok(Some(path)) => match read_limited(&path, config.project_doc_max_bytes).await {
|
||||
Ok(Some(project_doc)) => match &config.user_instructions {
|
||||
Some(original_instructions) => Some(format!(
|
||||
"{original_instructions}{PROJECT_DOC_SEPARATOR}{project_doc}"
|
||||
)),
|
||||
None => Some(project_doc),
|
||||
},
|
||||
Ok(None) => config.user_instructions.clone(),
|
||||
Err(e) => {
|
||||
error!("error trying to read project doc: {e:#}");
|
||||
config.user_instructions.clone()
|
||||
}
|
||||
},
|
||||
Ok(None) => config.user_instructions.clone(),
|
||||
Err(e) => {
|
||||
@@ -41,25 +49,31 @@ pub(crate) async fn get_user_instructions(config: &Config) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to locate and load the project documentation. Currently, the search
|
||||
/// starts from `Config::cwd`, but if we may want to consider other directories
|
||||
/// in the future, e.g., additional writable directories in the `SandboxPolicy`.
|
||||
///
|
||||
/// On success returns `Ok(Some(contents))`. If no documentation file is found
|
||||
/// the function returns `Ok(None)`. Unexpected I/O failures bubble up as
|
||||
/// `Err` so callers can decide how to handle them.
|
||||
async fn find_project_doc(config: &Config) -> std::io::Result<Option<String>> {
|
||||
let max_bytes = config.project_doc_max_bytes;
|
||||
// Same as `find_project_doc` but sync.
|
||||
pub fn find_project_doc_sync(cwd: &Path) -> std::io::Result<Option<PathBuf>> {
|
||||
let cwd = cwd.to_path_buf();
|
||||
let handle = tokio::runtime::Handle::current();
|
||||
#[allow(clippy::expect_used)]
|
||||
std::thread::spawn(move || handle.block_on(async { find_project_doc(&cwd).await }))
|
||||
.join()
|
||||
.expect("join should not fail")
|
||||
}
|
||||
|
||||
// Attempt to load from the working directory first.
|
||||
if let Some(doc) = load_first_candidate(&config.cwd, CANDIDATE_FILENAMES, max_bytes).await? {
|
||||
return Ok(Some(doc));
|
||||
/// Attempt to locate the project documentation file.
|
||||
///
|
||||
/// On success returns `Ok(Some(path))`. If no documentation file is found the
|
||||
/// function returns `Ok(None)`. Unexpected I/O failures bubble up as `Err` so
|
||||
/// callers can decide how to handle them.
|
||||
pub(crate) async fn find_project_doc(cwd: &Path) -> std::io::Result<Option<PathBuf>> {
|
||||
// Attempt to locate the file in the working directory first.
|
||||
if let Some(path) = find_candidate_in_dir(cwd).await? {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
|
||||
// Walk up towards the filesystem root, stopping once we encounter the Git
|
||||
// repository root. The presence of **either** a `.git` *file* or
|
||||
// *directory* counts.
|
||||
let mut dir = config.cwd.clone();
|
||||
let mut dir = cwd.to_path_buf();
|
||||
|
||||
// Canonicalize the path so that we do not end up in an infinite loop when
|
||||
// `cwd` contains `..` components.
|
||||
@@ -77,9 +91,8 @@ async fn find_project_doc(config: &Config) -> std::io::Result<Option<String>> {
|
||||
};
|
||||
|
||||
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));
|
||||
if let Some(path) = find_candidate_in_dir(&dir).await? {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -90,48 +103,50 @@ async fn find_project_doc(config: &Config) -> std::io::Result<Option<String>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Attempt to load the first candidate file found in `dir`. Returns the file
|
||||
/// contents (truncated if it exceeds `max_bytes`) when successful.
|
||||
async fn load_first_candidate(
|
||||
dir: &Path,
|
||||
names: &[&str],
|
||||
max_bytes: usize,
|
||||
) -> std::io::Result<Option<String>> {
|
||||
for name in names {
|
||||
async fn find_candidate_in_dir(dir: &Path) -> std::io::Result<Option<PathBuf>> {
|
||||
for name in CANDIDATE_FILENAMES {
|
||||
let candidate = dir.join(name);
|
||||
|
||||
let file = match tokio::fs::File::open(&candidate).await {
|
||||
match tokio::fs::metadata(&candidate).await {
|
||||
Ok(_) => return Ok(Some(candidate)),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
|
||||
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.",
|
||||
candidate.display(),
|
||||
);
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/// Read the project documentation from `path`, returning the contents truncated
|
||||
/// to `max_bytes`. Empty files are treated as absent.
|
||||
async fn read_limited(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)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
@@ -44,6 +44,7 @@ lazy_static = "1"
|
||||
once_cell = "1"
|
||||
mcp-types = { path = "../mcp-types" }
|
||||
path-clean = "1.0.1"
|
||||
pathdiff = "0.2"
|
||||
ratatui = { version = "0.29.0", features = [
|
||||
"scrolling-regions",
|
||||
"unstable-rendered-line-info",
|
||||
|
||||
@@ -13,6 +13,7 @@ use codex_core::parse_command::ParsedCommand;
|
||||
use codex_core::plan_tool::PlanItemArg;
|
||||
use codex_core::plan_tool::StepStatus;
|
||||
use codex_core::plan_tool::UpdatePlanArgs;
|
||||
use codex_core::project_doc::find_project_doc_sync;
|
||||
use codex_core::protocol::FileChange;
|
||||
use codex_core::protocol::McpInvocation;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
@@ -24,6 +25,7 @@ use image::DynamicImage;
|
||||
use image::ImageReader;
|
||||
use mcp_types::EmbeddedResourceResource;
|
||||
use mcp_types::ResourceLink;
|
||||
use pathdiff::diff_paths;
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::style::Modifier;
|
||||
@@ -161,7 +163,9 @@ pub(crate) fn new_session_info(
|
||||
None => config.cwd.display().to_string(),
|
||||
};
|
||||
|
||||
let lines: Vec<Line<'static>> = vec![
|
||||
let agents_path = find_project_doc_sync(&config.cwd).unwrap_or_default();
|
||||
|
||||
let mut lines: Vec<Line<'static>> = vec![
|
||||
Line::from(vec![
|
||||
Span::raw(">_ ").dim(),
|
||||
Span::styled(
|
||||
@@ -170,15 +174,38 @@ pub(crate) fn new_session_info(
|
||||
),
|
||||
Span::raw(format!(" {cwd_str}")).dim(),
|
||||
]),
|
||||
Line::from("".dim()),
|
||||
Line::from(" To get started, describe a task or try one of these commands:".dim()),
|
||||
Line::from("".dim()),
|
||||
Line::from(format!(" /init - {}", SlashCommand::Init.description()).dim()),
|
||||
Line::from(format!(" /status - {}", SlashCommand::Status.description()).dim()),
|
||||
Line::from(format!(" /diff - {}", SlashCommand::Diff.description()).dim()),
|
||||
Line::from(format!(" /prompts - {}", SlashCommand::Prompts.description()).dim()),
|
||||
Line::from("".dim()),
|
||||
Line::from(""),
|
||||
];
|
||||
if let Some(p) = &agents_path {
|
||||
let instructions_display_path = diff_paths(p, &config.cwd)
|
||||
.map(|rel| rel.display().to_string())
|
||||
.unwrap_or_else(|| p.display().to_string());
|
||||
lines.push(Line::from(vec![
|
||||
" Found ".dim(),
|
||||
instructions_display_path.into(),
|
||||
".".dim(),
|
||||
]));
|
||||
lines.push(Line::from(""));
|
||||
}
|
||||
lines.push(Line::from(
|
||||
" To get started, describe a task or try one of these commands:".dim(),
|
||||
));
|
||||
lines.push(Line::from(""));
|
||||
if agents_path.is_none() {
|
||||
lines.push(Line::from(
|
||||
format!(" /init - {}", SlashCommand::Init.description()).dim(),
|
||||
));
|
||||
}
|
||||
lines.push(Line::from(
|
||||
format!(" /status - {}", SlashCommand::Status.description()).dim(),
|
||||
));
|
||||
lines.push(Line::from(
|
||||
format!(" /diff - {}", SlashCommand::Diff.description()).dim(),
|
||||
));
|
||||
lines.push(Line::from(
|
||||
format!(" /prompts - {}", SlashCommand::Prompts.description()).dim(),
|
||||
));
|
||||
lines.push(Line::from("".dim()));
|
||||
PlainHistoryCell { lines }
|
||||
} else if config.model == model {
|
||||
PlainHistoryCell { lines: Vec::new() }
|
||||
|
||||
Reference in New Issue
Block a user