Skip to content
Closed
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
21 changes: 13 additions & 8 deletions codex-rs/file-search/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ pub fn run(

Box::new(move |entry| {
if let Some(path) = get_file_path(&entry, search_directory) {
best_list.insert(path);
best_list.insert(&path);
}

processed += 1;
Expand All @@ -221,20 +221,25 @@ pub fn run(
})
});

fn get_file_path<'a>(
entry_result: &'a Result<ignore::DirEntry, ignore::Error>,
fn get_file_path(
entry_result: &Result<ignore::DirEntry, ignore::Error>,
search_directory: &std::path::Path,
) -> Option<&'a str> {
) -> Option<String> {
let entry = match entry_result {
Ok(e) => e,
Err(_) => return None,
};
if entry.file_type().is_some_and(|ft| ft.is_dir()) {
return None;
}
let is_dir = entry.file_type().is_some_and(|ft| ft.is_dir());
let path = entry.path();
match path.strip_prefix(search_directory) {
Ok(rel_path) => rel_path.to_str(),
Ok(rel_path) => {
let path_str = rel_path.to_str()?;
if is_dir {
Some(format!("{}/", path_str))
} else {
Some(path_str.to_string())
}
}
Err(_) => None,
}
}
Expand Down
144 changes: 143 additions & 1 deletion codex-rs/protocol/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,50 @@ fn unsupported_image_error_placeholder(path: &std::path::Path, mime: &str) -> Co
}
}

/// Format a directory listing as a tree with visual characters.
/// Limited to immediate children (depth 1), truncated at `limit` items.
fn format_directory_tree(path: &std::path::Path, limit: usize) -> std::io::Result<String> {
let mut entries: Vec<_> = std::fs::read_dir(path)?.filter_map(|e| e.ok()).collect();
entries.sort_by_key(|e| e.file_name());

let total = entries.len();
let truncated = total > limit;
let display_count = if truncated { limit - 1 } else { total };

// Use the full path (not just filename) so the agent knows the exact location.
let dir_name = path.to_string_lossy().into_owned();
let mut result = format!("{}/\n", dir_name);

for (i, entry) in entries.iter().take(display_count).enumerate() {
let is_last = !truncated && i == display_count - 1;
let prefix = if is_last {
"\u{2514}\u{2500}"
} else {
"\u{251C}\u{2500}"
};
let suffix = if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
"/"
} else {
""
};
result.push_str(&format!(
"{} {}{}\n",
prefix,
entry.file_name().to_string_lossy(),
suffix
));
}

if truncated {
result.push_str(&format!(
"...\n\u{2514}\u{2500} ({} more items)\n",
total - display_count
));
}

Ok(result)
}

impl From<ResponseInputItem> for ResponseItem {
fn from(item: ResponseInputItem) -> Self {
match item {
Expand Down Expand Up @@ -331,6 +375,16 @@ impl From<Vec<UserInput>> for ResponseInputItem {
}
},
UserInput::Skill { .. } => None, // Skill bodies are injected later in core
UserInput::LocalDirectory { path } => match format_directory_tree(&path, 50) {
Ok(listing) => Some(ContentItem::InputText { text: listing }),
Err(err) => Some(ContentItem::InputText {
text: format!(
"Codex could not read the directory at `{}`: {}",
path.display(),
err
),
}),
},
})
.collect::<Vec<ContentItem>>(),
}
Expand Down Expand Up @@ -502,7 +556,6 @@ fn convert_content_blocks_to_items(
) -> Option<Vec<FunctionCallOutputContentItem>> {
let mut saw_image = false;
let mut items = Vec::with_capacity(blocks.len());
tracing::warn!("Blocks: {:?}", blocks);
for block in blocks {
match block {
ContentBlock::TextContent(text) => {
Expand Down Expand Up @@ -867,4 +920,93 @@ mod tests {

Ok(())
}

#[test]
fn local_directory_formats_tree() -> Result<()> {
let dir = tempdir()?;
std::fs::write(dir.path().join("file1.txt"), b"content")?;
std::fs::write(dir.path().join("file2.rs"), b"fn main() {}")?;
std::fs::create_dir(dir.path().join("subdir"))?;

let item = ResponseInputItem::from(vec![UserInput::LocalDirectory {
path: dir.path().to_path_buf(),
}]);

match item {
ResponseInputItem::Message { content, .. } => {
assert_eq!(content.len(), 1);
match &content[0] {
ContentItem::InputText { text } => {
assert!(
text.contains("file1.txt"),
"should contain file1.txt: {text}"
);
assert!(text.contains("file2.rs"), "should contain file2.rs: {text}");
assert!(text.contains("subdir/"), "should contain subdir/: {text}");
assert!(
text.contains("\u{251C}\u{2500}") || text.contains("\u{2514}\u{2500}"),
"should contain tree chars: {text}"
);
}
other => panic!("expected text but found {other:?}"),
}
}
other => panic!("expected message but got {other:?}"),
}
Ok(())
}

#[test]
fn local_directory_truncates_large_dirs() -> Result<()> {
let dir = tempdir()?;
for i in 0..55 {
std::fs::write(dir.path().join(format!("file{:03}.txt", i)), b"x")?;
}

let item = ResponseInputItem::from(vec![UserInput::LocalDirectory {
path: dir.path().to_path_buf(),
}]);

match item {
ResponseInputItem::Message { content, .. } => match &content[0] {
ContentItem::InputText { text } => {
assert!(
text.contains("more items"),
"should show truncation: {text}"
);
assert!(
!text.contains("file054.txt"),
"should not show last items: {text}"
);
}
other => panic!("expected text but found {other:?}"),
},
other => panic!("expected message but got {other:?}"),
}
Ok(())
}

#[test]
fn local_directory_read_error_adds_placeholder() -> Result<()> {
let missing = std::path::PathBuf::from("/nonexistent/path/to/dir");

let item = ResponseInputItem::from(vec![UserInput::LocalDirectory {
path: missing.clone(),
}]);

match item {
ResponseInputItem::Message { content, .. } => match &content[0] {
ContentItem::InputText { text } => {
assert!(text.contains("could not read"), "error text: {text}");
assert!(
text.contains(&missing.display().to_string()),
"should contain path: {text}"
);
}
other => panic!("expected text but found {other:?}"),
},
other => panic!("expected message but got {other:?}"),
}
Ok(())
}
}
6 changes: 6 additions & 0 deletions codex-rs/protocol/src/user_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,10 @@ pub enum UserInput {
name: String,
path: std::path::PathBuf,
},

/// Local directory path provided by the user. This will be converted to
/// a text listing during request serialization.
LocalDirectory {
path: std::path::PathBuf,
},
}
Loading
Loading