Files
codex/codex-rs/tui/src/file_search.rs
T
13e85b1549 fix: update file search directory when session CWD changes (#9279)
## Summary

Fixes #9041

- Adds update_search_dir() method to FileSearchManager to allow updating
the search directory after initialization
- Calls this method when the session CWD changes: new session, resume,
or fork

## Problem

The FileSearchManager was created once with the initial search_dir and
never updated. When a user:

1. Starts Codex in a non-git directory (e.g., /tmp/random)
2. Resumes or forks a session from a different workspace
3. The @filename lookup still searched the original directory

This caused no matches to be returned even when files existed in the
current workspace.

## Solution

Update FileSearchManager.search_dir whenever the session working
directory changes:
- AppEvent::NewSession: Use current config CWD
- SessionSelection::Resume: Use resumed session CWD
- SessionSelection::Fork: Use forked session CWD

## Test plan

- [ ] Start Codex in /tmp/test-dir (non-git)
- [ ] Resume a session from a project with actual files
- [ ] Verify @filename returns matches from the resumed session
directory

---------

Co-authored-by: Eric Traut <etraut@openai.com>
2026-01-30 14:59:20 -08:00

133 lines
3.9 KiB
Rust

//! Session-based orchestration for `@` file searches.
//!
//! `ChatComposer` publishes every change of the `@token` as
//! `AppEvent::StartFileSearch(query)`. This manager owns a single
//! `codex-file-search` session for the current search root, updates the query
//! on every keystroke, and drops the session when the query becomes empty.
use codex_file_search as file_search;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
pub(crate) struct FileSearchManager {
state: Arc<Mutex<SearchState>>,
search_dir: PathBuf,
app_tx: AppEventSender,
}
struct SearchState {
latest_query: String,
session: Option<file_search::FileSearchSession>,
session_token: usize,
}
impl FileSearchManager {
pub fn new(search_dir: PathBuf, tx: AppEventSender) -> Self {
Self {
state: Arc::new(Mutex::new(SearchState {
latest_query: String::new(),
session: None,
session_token: 0,
})),
search_dir,
app_tx: tx,
}
}
/// Updates the directory used for file searches.
/// This should be called when the session's CWD changes on resume.
/// Drops the current session so it will be recreated with the new directory on next query.
pub fn update_search_dir(&mut self, new_dir: PathBuf) {
self.search_dir = new_dir;
#[expect(clippy::unwrap_used)]
let mut st = self.state.lock().unwrap();
st.session.take();
st.latest_query.clear();
}
/// Call whenever the user edits the `@` token.
pub fn on_user_query(&self, query: String) {
#[expect(clippy::unwrap_used)]
let mut st = self.state.lock().unwrap();
if query == st.latest_query {
return;
}
st.latest_query.clear();
st.latest_query.push_str(&query);
if query.is_empty() {
st.session.take();
return;
}
if st.session.is_none() {
self.start_session_locked(&mut st);
}
if let Some(session) = st.session.as_ref() {
session.update_query(&query);
}
}
fn start_session_locked(&self, st: &mut SearchState) {
st.session_token = st.session_token.wrapping_add(1);
let session_token = st.session_token;
let reporter = Arc::new(TuiSessionReporter {
state: self.state.clone(),
app_tx: self.app_tx.clone(),
session_token,
});
let session = file_search::create_session(
&self.search_dir,
file_search::FileSearchOptions {
compute_indices: true,
..Default::default()
},
reporter,
);
match session {
Ok(session) => st.session = Some(session),
Err(err) => {
tracing::warn!("file search session failed to start: {err}");
st.session = None;
}
}
}
}
struct TuiSessionReporter {
state: Arc<Mutex<SearchState>>,
app_tx: AppEventSender,
session_token: usize,
}
impl TuiSessionReporter {
fn send_snapshot(&self, snapshot: &file_search::FileSearchSnapshot) {
#[expect(clippy::unwrap_used)]
let st = self.state.lock().unwrap();
if st.session_token != self.session_token
|| st.latest_query.is_empty()
|| snapshot.query.is_empty()
{
return;
}
let query = snapshot.query.clone();
drop(st);
self.app_tx.send(AppEvent::FileSearchResult {
query,
matches: snapshot.matches.clone(),
});
}
}
impl file_search::SessionReporter for TuiSessionReporter {
fn on_update(&self, snapshot: &file_search::FileSearchSnapshot) {
self.send_snapshot(snapshot);
}
fn on_complete(&self) {}
}