Make AGENTS.md discovery FS-aware (#15826)

## Summary
- make AGENTS.md discovery and loading fully FS-aware and remove the
non-FS discover helper
- migrate remote-aware codex-core tests to use TestEnv workspace setup
instead of syncing a local workspace copy
- add AGENTS.md corner-case coverage, including directory fallbacks and
remote-aware integration coverage

## Testing
- cargo test -p codex-core project_doc -- --nocapture
- cargo test -p codex-core hierarchical_agents -- --nocapture
- cargo test -p codex-core agents_md -- --nocapture
- cargo test -p codex-tui status -- --nocapture
- cargo test -p codex-tui-app-server status -- --nocapture
- just fix
- just fmt
- just bazel-lock-update
- just bazel-lock-check
- just argument-comment-lint
- remote Linux executor tests in progress via scripts/test-remote-env.sh
This commit is contained in:
pakrym-oai
2026-04-06 20:26:21 -07:00
committed by GitHub
parent 232db0613a
commit 4bb507d2c4
21 changed files with 545 additions and 171 deletions
+15
View File
@@ -7303,6 +7303,8 @@ impl ChatWidget {
.values()
.cloned()
.collect();
let config = self.config.clone();
let frame_requester = self.frame_requester.clone();
let (cell, handle) = crate::status::new_status_output_with_rate_limits_handle(
&self.config,
self.status_account_display.as_ref(),
@@ -7317,8 +7319,21 @@ impl ChatWidget {
self.model_display_name(),
collaboration_mode,
reasoning_effort_override,
"<none>".to_string(),
refreshing_rate_limits,
);
let agents_summary_handle = handle.clone();
tokio::spawn(async move {
let agents_summary = match crate::status::discover_agents_summary(&config).await {
Ok(summary) => summary,
Err(err) => {
tracing::warn!(error = %err, "failed to discover project docs for /status");
"<none>".to_string()
}
};
agents_summary_handle.finish_agents_summary_discovery(agents_summary);
frame_requester.schedule_frame();
});
if let Some(request_id) = request_id {
self.refreshing_status_outputs.push((request_id, handle));
}
+28 -6
View File
@@ -28,7 +28,6 @@ use super::format::line_display_width;
use super::format::push_label;
use super::format::truncate_line_to_width;
use super::helpers::compose_account_display;
use super::helpers::compose_agents_summary;
use super::helpers::compose_model_display;
use super::helpers::format_directory_display;
use super::helpers::format_tokens_compact;
@@ -68,10 +67,20 @@ struct StatusRateLimitState {
#[derive(Debug, Clone)]
pub(crate) struct StatusHistoryHandle {
agents_summary: Arc<RwLock<String>>,
rate_limit_state: Arc<RwLock<StatusRateLimitState>>,
}
impl StatusHistoryHandle {
pub(crate) fn finish_agents_summary_discovery(&self, agents_summary: String) {
#[expect(clippy::expect_used)]
let mut current = self
.agents_summary
.write()
.expect("status history agents summary state poisoned");
*current = agents_summary;
}
pub(crate) fn finish_rate_limit_refresh(
&self,
rate_limits: &[RateLimitSnapshotDisplay],
@@ -98,7 +107,7 @@ struct StatusHistoryCell {
model_details: Vec<String>,
directory: PathBuf,
permissions: String,
agents_summary: String,
agents_summary: Arc<RwLock<String>>,
collaboration_mode: Option<String>,
model_provider: Option<String>,
account: Option<StatusAccountDisplay>,
@@ -177,6 +186,7 @@ pub(crate) fn new_status_output_with_rate_limits(
model_name,
collaboration_mode,
reasoning_effort_override,
"<none>".to_string(),
refreshing_rate_limits,
)
.0
@@ -197,6 +207,7 @@ pub(crate) fn new_status_output_with_rate_limits_handle(
model_name: &str,
collaboration_mode: Option<&str>,
reasoning_effort_override: Option<Option<ReasoningEffort>>,
agents_summary: String,
refreshing_rate_limits: bool,
) -> (CompositeHistoryCell, StatusHistoryHandle) {
let command = PlainHistoryCell::new(vec!["/status".magenta().into()]);
@@ -214,6 +225,7 @@ pub(crate) fn new_status_output_with_rate_limits_handle(
model_name,
collaboration_mode,
reasoning_effort_override,
agents_summary,
refreshing_rate_limits,
);
@@ -239,6 +251,7 @@ impl StatusHistoryCell {
model_name: &str,
collaboration_mode: Option<&str>,
reasoning_effort_override: Option<Option<ReasoningEffort>>,
agents_summary: String,
refreshing_rate_limits: bool,
) -> (Self, StatusHistoryHandle) {
let mut config_entries = vec![
@@ -302,7 +315,6 @@ impl StatusHistoryCell {
} else {
format!("Custom ({sandbox}, {approval})")
};
let agents_summary = compose_agents_summary(config);
let model_provider = format_model_provider(config);
let account = compose_account_display(account_display);
let session_id = session_id.as_ref().map(std::string::ToString::to_string);
@@ -333,6 +345,7 @@ impl StatusHistoryCell {
rate_limits,
refreshing_rate_limits,
}));
let agents_summary = Arc::new(RwLock::new(agents_summary));
(
Self {
@@ -340,7 +353,6 @@ impl StatusHistoryCell {
model_details,
directory: config.cwd.to_path_buf(),
permissions,
agents_summary,
collaboration_mode: collaboration_mode.map(ToString::to_string),
model_provider,
account,
@@ -348,9 +360,13 @@ impl StatusHistoryCell {
session_id,
forked_from,
token_usage,
agents_summary: agents_summary.clone(),
rate_limit_state: rate_limit_state.clone(),
},
StatusHistoryHandle { rate_limit_state },
StatusHistoryHandle {
agents_summary,
rate_limit_state,
},
)
}
@@ -564,6 +580,12 @@ impl HistoryCell for StatusHistoryCell {
.rate_limit_state
.read()
.expect("status history rate-limit state poisoned");
#[expect(clippy::expect_used)]
let agents_summary = self
.agents_summary
.read()
.expect("status history agents summary state poisoned")
.clone();
if self.model_provider.is_some() {
push_label(&mut labels, &mut seen, "Model provider");
@@ -625,7 +647,7 @@ impl HistoryCell for StatusHistoryCell {
}
lines.push(formatter.line("Directory", vec![Span::from(directory_value)]));
lines.push(formatter.line("Permissions", vec![Span::from(self.permissions.clone())]));
lines.push(formatter.line("Agents.md", vec![Span::from(self.agents_summary.clone())]));
lines.push(formatter.line("Agents.md", vec![Span::from(agents_summary)]));
if let Some(account_value) = account_value {
lines.push(formatter.line("Account", vec![Span::from(account_value)]));
+47 -43
View File
@@ -5,7 +5,10 @@ use chrono::DateTime;
use chrono::Local;
use codex_core::config::Config;
use codex_core::project_doc::discover_project_doc_paths;
use codex_exec_server::LOCAL_FS;
use codex_protocol::account::PlanType;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::io;
use std::path::Path;
use unicode_width::UnicodeWidthStr;
@@ -33,51 +36,52 @@ pub(crate) fn compose_model_display(
(model_name.to_string(), details)
}
pub(crate) fn compose_agents_summary(config: &Config) -> String {
match discover_project_doc_paths(config) {
Ok(paths) => {
let mut rels: Vec<String> = Vec::new();
for p in paths {
let file_name = p
.file_name()
.map(|name| name.to_string_lossy().to_string())
.unwrap_or_else(|| "<unknown>".to_string());
let display = if let Some(parent) = p.parent() {
if parent == config.cwd.as_path() {
file_name.clone()
} else {
let mut cur = config.cwd.as_path();
let mut ups = 0usize;
let mut reached = false;
while let Some(c) = cur.parent() {
if cur == parent {
reached = true;
break;
}
cur = c;
ups += 1;
}
if reached {
let up = format!("..{}", std::path::MAIN_SEPARATOR);
format!("{}{}", up.repeat(ups), file_name)
} else if let Ok(stripped) = p.strip_prefix(&config.cwd) {
normalize_agents_display_path(stripped)
} else {
normalize_agents_display_path(&p)
}
}
} else {
normalize_agents_display_path(&p)
};
rels.push(display);
}
if rels.is_empty() {
"<none>".to_string()
pub(crate) async fn discover_agents_summary(config: &Config) -> io::Result<String> {
let paths = discover_project_doc_paths(config, LOCAL_FS.as_ref()).await?;
Ok(compose_agents_summary(config, &paths))
}
pub(crate) fn compose_agents_summary(config: &Config, paths: &[AbsolutePathBuf]) -> String {
let mut rels: Vec<String> = Vec::new();
for p in paths {
let file_name = p
.file_name()
.map(|name| name.to_string_lossy().to_string())
.unwrap_or_else(|| "<unknown>".to_string());
let display = if let Some(parent) = p.parent() {
if parent.as_path() == config.cwd.as_path() {
file_name.clone()
} else {
rels.join(", ")
let mut cur = config.cwd.as_path();
let mut ups = 0usize;
let mut reached = false;
while let Some(c) = cur.parent() {
if cur == parent.as_path() {
reached = true;
break;
}
cur = c;
ups += 1;
}
if reached {
let up = format!("..{}", std::path::MAIN_SEPARATOR);
format!("{}{}", up.repeat(ups), file_name)
} else if let Ok(stripped) = p.strip_prefix(&config.cwd) {
normalize_agents_display_path(stripped)
} else {
normalize_agents_display_path(p)
}
}
}
Err(_) => "<none>".to_string(),
} else {
normalize_agents_display_path(p)
};
rels.push(display);
}
if rels.is_empty() {
"<none>".to_string()
} else {
rels.join(", ")
}
}
+1
View File
@@ -19,6 +19,7 @@ pub(crate) use card::new_status_output;
#[cfg(test)]
pub(crate) use card::new_status_output_with_rate_limits;
pub(crate) use card::new_status_output_with_rate_limits_handle;
pub(crate) use helpers::discover_agents_summary;
pub(crate) use helpers::format_directory_display;
pub(crate) use helpers::format_tokens_compact;
pub(crate) use helpers::plan_type_display_name;