From 10eb3ec7fccaf805c7162d8370b5b99bf57ddc48 Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Wed, 18 Mar 2026 22:24:09 -0700 Subject: [PATCH] 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 image image --- .../schema/json/FuzzyFileSearchResponse.json | 11 ++++ ...yFileSearchSessionUpdatedNotification.json | 11 ++++ .../schema/json/ServerNotification.json | 11 ++++ .../codex_app_server_protocol.schemas.json | 11 ++++ .../codex_app_server_protocol.v2.schemas.json | 11 ++++ .../typescript/FuzzyFileSearchMatchType.ts | 5 ++ .../typescript/FuzzyFileSearchResult.ts | 3 +- .../schema/typescript/index.ts | 1 + .../src/protocol/common.rs | 9 ++++ codex-rs/app-server/src/fuzzy_file_search.rs | 9 ++++ .../tests/suite/fuzzy_file_search.rs | 3 ++ codex-rs/file-search/src/lib.rs | 50 +++++++++++++++++-- codex-rs/tui/src/bottom_pane/chat_composer.rs | 1 + .../src/bottom_pane/chat_composer.rs | 1 + 14 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchMatchType.ts diff --git a/codex-rs/app-server-protocol/schema/json/FuzzyFileSearchResponse.json b/codex-rs/app-server-protocol/schema/json/FuzzyFileSearchResponse.json index 3309b9fb5..3c91a79c6 100644 --- a/codex-rs/app-server-protocol/schema/json/FuzzyFileSearchResponse.json +++ b/codex-rs/app-server-protocol/schema/json/FuzzyFileSearchResponse.json @@ -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" diff --git a/codex-rs/app-server-protocol/schema/json/FuzzyFileSearchSessionUpdatedNotification.json b/codex-rs/app-server-protocol/schema/json/FuzzyFileSearchSessionUpdatedNotification.json index f4ce29b5a..b69ad9b28 100644 --- a/codex-rs/app-server-protocol/schema/json/FuzzyFileSearchSessionUpdatedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/FuzzyFileSearchSessionUpdatedNotification.json @@ -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" diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 8bb9f2548..3a6babc78 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -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" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index df25bf911..bbbf7810b 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -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" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index a932ee039..496e7f139 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -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" diff --git a/codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchMatchType.ts b/codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchMatchType.ts new file mode 100644 index 000000000..60e92f925 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchMatchType.ts @@ -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"; diff --git a/codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchResult.ts b/codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchResult.ts index e841dbfa0..0ff6bf451 100644 --- a/codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchResult.ts +++ b/codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchResult.ts @@ -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 | null, }; +export type FuzzyFileSearchResult = { root: string, path: string, match_type: FuzzyFileSearchMatchType, file_name: string, score: number, indices: Array | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index 09e75abed..73f2cc8e5 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -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"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 6a35ad78e..5df79060c 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -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>, } +#[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, diff --git a/codex-rs/app-server/src/fuzzy_file_search.rs b/codex-rs/app-server/src/fuzzy_file_search.rs index d40d3fc24..f8cd61e3a 100644 --- a/codex-rs/app-server/src/fuzzy_file_search.rs +++ b/codex-rs/app-server/src/fuzzy_file_search.rs @@ -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 FuzzyFileSearchMatchType::File, + file_search::MatchType::Directory => FuzzyFileSearchMatchType::Directory, + }, file_name: file_name.to_string_lossy().to_string(), score: m.score, indices: m.indices.clone(), diff --git a/codex-rs/app-server/tests/suite/fuzzy_file_search.rs b/codex-rs/app-server/tests/suite/fuzzy_file_search.rs index 0070c2b30..692304c6f 100644 --- a/codex-rs/app-server/tests/suite/fuzzy_file_search.rs +++ b/codex-rs/app-server/tests/suite/fuzzy_file_search.rs @@ -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], diff --git a/codex-rs/file-search/src/lib.rs b/codex-rs/file-search/src/lib.rs index 8391ccd26..64f4c19f7 100644 --- a/codex-rs/file-search/src/lib.rs +++ b/codex-rs/file-search/src/lib.rs @@ -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>, // 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); diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 6aa250b52..ee0a7bb63 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -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, }], diff --git a/codex-rs/tui_app_server/src/bottom_pane/chat_composer.rs b/codex-rs/tui_app_server/src/bottom_pane/chat_composer.rs index f796c040d..b86c029b5 100644 --- a/codex-rs/tui_app_server/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui_app_server/src/bottom_pane/chat_composer.rs @@ -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, }],