Simple directory mentions (#14970)

- Adds simple support for directory mentions in the TUI.
- Codex App/VS Code will require minor change to recognize a directory
mention as such and change the link behavior.
- Directory mentions have a trailing slash to differentiate from
extensionless files


<img width="972" height="382" alt="image"
src="https://github.com/user-attachments/assets/8035b1eb-0978-465b-8d7a-4db2e5feca39"
/>
<img width="978" height="228" alt="image"
src="https://github.com/user-attachments/assets/af22cf0b-dd10-4440-9bee-a09915f6ba52"
/>
This commit is contained in:
canvrno-oai 2026-03-18 22:24:09 -07:00 committed by GitHub
parent 42e932d7bf
commit 10eb3ec7fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 131 additions and 6 deletions

View file

@ -1,6 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"FuzzyFileSearchMatchType": {
"enum": [
"file",
"directory"
],
"type": "string"
},
"FuzzyFileSearchResult": {
"description": "Superset of [`codex_file_search::FileMatch`]",
"properties": {
@ -18,6 +25,9 @@
"null"
]
},
"match_type": {
"$ref": "#/definitions/FuzzyFileSearchMatchType"
},
"path": {
"type": "string"
},
@ -32,6 +42,7 @@
},
"required": [
"file_name",
"match_type",
"path",
"root",
"score"

View file

@ -1,6 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"FuzzyFileSearchMatchType": {
"enum": [
"file",
"directory"
],
"type": "string"
},
"FuzzyFileSearchResult": {
"description": "Superset of [`codex_file_search::FileMatch`]",
"properties": {
@ -18,6 +25,9 @@
"null"
]
},
"match_type": {
"$ref": "#/definitions/FuzzyFileSearchMatchType"
},
"path": {
"type": "string"
},
@ -32,6 +42,7 @@
},
"required": [
"file_name",
"match_type",
"path",
"root",
"score"

View file

@ -964,6 +964,13 @@
],
"type": "object"
},
"FuzzyFileSearchMatchType": {
"enum": [
"file",
"directory"
],
"type": "string"
},
"FuzzyFileSearchResult": {
"description": "Superset of [`codex_file_search::FileMatch`]",
"properties": {
@ -981,6 +988,9 @@
"null"
]
},
"match_type": {
"$ref": "#/definitions/FuzzyFileSearchMatchType"
},
"path": {
"type": "string"
},
@ -995,6 +1005,7 @@
},
"required": [
"file_name",
"match_type",
"path",
"root",
"score"

View file

@ -2034,6 +2034,13 @@
"title": "FileChangeRequestApprovalResponse",
"type": "object"
},
"FuzzyFileSearchMatchType": {
"enum": [
"file",
"directory"
],
"type": "string"
},
"FuzzyFileSearchParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@ -2093,6 +2100,9 @@
"null"
]
},
"match_type": {
"$ref": "#/definitions/FuzzyFileSearchMatchType"
},
"path": {
"type": "string"
},
@ -2107,6 +2117,7 @@
},
"required": [
"file_name",
"match_type",
"path",
"root",
"score"

View file

@ -4327,6 +4327,13 @@
}
]
},
"FuzzyFileSearchMatchType": {
"enum": [
"file",
"directory"
],
"type": "string"
},
"FuzzyFileSearchParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@ -4370,6 +4377,9 @@
"null"
]
},
"match_type": {
"$ref": "#/definitions/FuzzyFileSearchMatchType"
},
"path": {
"type": "string"
},
@ -4384,6 +4394,7 @@
},
"required": [
"file_name",
"match_type",
"path",
"root",
"score"

View file

@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type FuzzyFileSearchMatchType = "file" | "directory";

View file

@ -1,8 +1,9 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { FuzzyFileSearchMatchType } from "./FuzzyFileSearchMatchType";
/**
* Superset of [`codex_file_search::FileMatch`]
*/
export type FuzzyFileSearchResult = { root: string, path: string, file_name: string, score: number, indices: Array<number> | null, };
export type FuzzyFileSearchResult = { root: string, path: string, match_type: FuzzyFileSearchMatchType, file_name: string, score: number, indices: Array<number> | null, };

View file

@ -18,6 +18,7 @@ export type { FileChange } from "./FileChange";
export type { ForcedLoginMethod } from "./ForcedLoginMethod";
export type { FunctionCallOutputBody } from "./FunctionCallOutputBody";
export type { FunctionCallOutputContentItem } from "./FunctionCallOutputContentItem";
export type { FuzzyFileSearchMatchType } from "./FuzzyFileSearchMatchType";
export type { FuzzyFileSearchParams } from "./FuzzyFileSearchParams";
export type { FuzzyFileSearchResponse } from "./FuzzyFileSearchResponse";
export type { FuzzyFileSearchResult } from "./FuzzyFileSearchResult";

View file

@ -800,11 +800,20 @@ pub struct FuzzyFileSearchParams {
pub struct FuzzyFileSearchResult {
pub root: String,
pub path: String,
pub match_type: FuzzyFileSearchMatchType,
pub file_name: String,
pub score: u32,
pub indices: Option<Vec<u32>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
pub enum FuzzyFileSearchMatchType {
File,
Directory,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
pub struct FuzzyFileSearchResponse {
pub files: Vec<FuzzyFileSearchResult>,

View file

@ -5,6 +5,7 @@ use std::sync::Mutex;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use codex_app_server_protocol::FuzzyFileSearchMatchType;
use codex_app_server_protocol::FuzzyFileSearchResult;
use codex_app_server_protocol::FuzzyFileSearchSessionCompletedNotification;
use codex_app_server_protocol::FuzzyFileSearchSessionUpdatedNotification;
@ -60,6 +61,10 @@ pub(crate) async fn run_fuzzy_file_search(
FuzzyFileSearchResult {
root: m.root.to_string_lossy().to_string(),
path: m.path.to_string_lossy().to_string(),
match_type: match m.match_type {
file_search::MatchType::File => FuzzyFileSearchMatchType::File,
file_search::MatchType::Directory => FuzzyFileSearchMatchType::Directory,
},
file_name: file_name.to_string_lossy().to_string(),
score: m.score,
indices: m.indices,
@ -231,6 +236,10 @@ fn collect_files(snapshot: &file_search::FileSearchSnapshot) -> Vec<FuzzyFileSea
FuzzyFileSearchResult {
root: m.root.to_string_lossy().to_string(),
path: m.path.to_string_lossy().to_string(),
match_type: match m.match_type {
file_search::MatchType::File => FuzzyFileSearchMatchType::File,
file_search::MatchType::Directory => FuzzyFileSearchMatchType::Directory,
},
file_name: file_name.to_string_lossy().to_string(),
score: m.score,
indices: m.indices.clone(),

View file

@ -257,6 +257,7 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> {
{
"root": root_path.clone(),
"path": "abexy",
"match_type": "file",
"file_name": "abexy",
"score": 84,
"indices": [0, 1, 2],
@ -264,6 +265,7 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> {
{
"root": root_path.clone(),
"path": sub_abce_rel,
"match_type": "file",
"file_name": "abce",
"score": expected_score,
"indices": [4, 5, 7],
@ -271,6 +273,7 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> {
{
"root": root_path.clone(),
"path": "abcde",
"match_type": "file",
"file_name": "abcde",
"score": 71,
"indices": [0, 1, 4],

View file

@ -41,7 +41,9 @@ pub use cli::Cli;
/// A single match result returned from the search.
///
/// * `score` Relevance score returned by `nucleo`.
/// * `path` Path to the matched file (relative to the search directory).
/// * `path` Path to the matched entry (file or directory), relative to the
/// search directory.
/// * `match_type` Whether this match is a file or directory.
/// * `indices` Optional list of character indices that matched the query.
/// These are only filled when the caller of [`run`] sets
/// `options.compute_indices` to `true`. The indices vector follows the
@ -52,11 +54,19 @@ pub use cli::Cli;
pub struct FileMatch {
pub score: u32,
pub path: PathBuf,
pub match_type: MatchType,
pub root: PathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
pub indices: Option<Vec<u32>>, // Sorted & deduplicated when present
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum MatchType {
File,
Directory,
}
impl FileMatch {
pub fn full_path(&self) -> PathBuf {
self.root.join(&self.path)
@ -386,7 +396,7 @@ fn get_file_path<'a>(path: &'a Path, search_directories: &[PathBuf]) -> Option<(
rel_path.to_str().map(|p| (root_idx, p))
}
/// Walks the search directories and feeds discovered file paths into `nucleo`
/// Walks the search directories and feeds discovered paths into `nucleo`
/// via the injector.
///
/// The walker uses `require_git(true)` to match git's own ignore semantics:
@ -448,9 +458,6 @@ fn walker_worker(
Ok(entry) => entry,
Err(_) => return ignore::WalkState::Continue,
};
if entry.file_type().is_some_and(|ft| ft.is_dir()) {
return ignore::WalkState::Continue;
}
let path = entry.path();
let Some(full_path) = path.to_str() else {
return ignore::WalkState::Continue;
@ -552,9 +559,15 @@ fn matcher_worker(
} else {
None
};
let match_type = if Path::new(full_path).is_dir() {
MatchType::Directory
} else {
MatchType::File
};
Some(FileMatch {
score: match_.score,
path: PathBuf::from(relative_path),
match_type,
root: inner.search_directories[root_idx].clone(),
indices,
})
@ -961,6 +974,33 @@ mod tests {
);
}
#[test]
fn run_returns_directory_matches_for_query() {
let dir = tempfile::tempdir().unwrap();
fs::create_dir_all(dir.path().join("docs/guides")).unwrap();
fs::write(dir.path().join("docs/guides/intro.md"), "intro").unwrap();
fs::write(dir.path().join("docs/readme.md"), "readme").unwrap();
let results = run(
"guides",
vec![dir.path().to_path_buf()],
FileSearchOptions {
limit: NonZero::new(20).unwrap(),
exclude: Vec::new(),
threads: NonZero::new(2).unwrap(),
compute_indices: false,
respect_gitignore: true,
},
None,
)
.expect("run ok");
assert!(results.matches.iter().any(|m| {
m.path == std::path::Path::new("docs").join("guides")
&& m.match_type == MatchType::Directory
}));
}
#[test]
fn cancel_exits_run() {
let dir = create_temp_tree(200);

View file

@ -7253,6 +7253,7 @@ mod tests {
vec![FileMatch {
score: 1,
path: PathBuf::from("src/main.rs"),
match_type: codex_file_search::MatchType::File,
root: PathBuf::from("/tmp"),
indices: None,
}],

View file

@ -7268,6 +7268,7 @@ mod tests {
vec![FileMatch {
score: 1,
path: PathBuf::from("src/main.rs"),
match_type: codex_file_search::MatchType::File,
root: PathBuf::from("/tmp"),
indices: None,
}],