mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
initial commit
This commit is contained in:
109
codex-rs/Cargo.lock
generated
109
codex-rs/Cargo.lock
generated
@@ -1056,6 +1056,7 @@ dependencies = [
|
||||
"libc",
|
||||
"maplit",
|
||||
"mcp-types",
|
||||
"notify",
|
||||
"openssl-sys",
|
||||
"os_info",
|
||||
"portable-pty",
|
||||
@@ -1645,7 +1646,7 @@ dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"crossterm_winapi",
|
||||
"futures-core",
|
||||
"mio",
|
||||
"mio 1.0.4",
|
||||
"parking_lot",
|
||||
"rustix 0.38.44",
|
||||
"signal-hook",
|
||||
@@ -2299,6 +2300,18 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fixed_decimal"
|
||||
version = "0.7.0"
|
||||
@@ -2371,6 +2384,15 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
@@ -3057,6 +3079,26 @@ version = "2.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
@@ -3257,6 +3299,26 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
|
||||
dependencies = [
|
||||
"kqueue-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue-sys"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lalrpop"
|
||||
version = "0.19.12"
|
||||
@@ -3334,6 +3396,7 @@ checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3534,6 +3597,18 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.4"
|
||||
@@ -3656,6 +3731,25 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "6.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"crossbeam-channel",
|
||||
"filetime",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio 0.8.11",
|
||||
"walkdir",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.1"
|
||||
@@ -5380,7 +5474,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"mio 1.0.4",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
@@ -5948,7 +6042,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"io-uring",
|
||||
"libc",
|
||||
"mio",
|
||||
"mio 1.0.4",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
@@ -6846,6 +6940,15 @@ dependencies = [
|
||||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
|
||||
@@ -76,6 +76,7 @@ codex-utils-string = { path = "utils/string" }
|
||||
core_test_support = { path = "core/tests/common" }
|
||||
mcp-types = { path = "mcp-types" }
|
||||
mcp_test_support = { path = "mcp-server/tests/common" }
|
||||
notify = "6"
|
||||
|
||||
# External
|
||||
allocative = "0.3.3"
|
||||
|
||||
@@ -35,6 +35,7 @@ futures = { workspace = true }
|
||||
indexmap = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
mcp-types = { workspace = true }
|
||||
notify = { workspace = true }
|
||||
os_info = { workspace = true }
|
||||
portable-pty = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
|
||||
@@ -57,6 +57,7 @@ use crate::exec_command::WriteStdinParams;
|
||||
use crate::executor::Executor;
|
||||
use crate::executor::ExecutorConfig;
|
||||
use crate::executor::normalize_exec_result;
|
||||
use crate::file_change_notifier::FileChangeWatcher;
|
||||
use crate::mcp::auth::compute_auth_statuses;
|
||||
use crate::mcp_connection_manager::McpConnectionManager;
|
||||
use crate::model_family::find_family_for_model;
|
||||
@@ -462,6 +463,14 @@ impl Session {
|
||||
is_review_mode: false,
|
||||
final_output_json_schema: None,
|
||||
};
|
||||
let (file_change_watcher, file_change_collector) =
|
||||
match FileChangeWatcher::start(turn_context.cwd.clone()) {
|
||||
Ok((watcher, collector)) => (Some(watcher), Some(collector)),
|
||||
Err(err) => {
|
||||
warn!("file change notifications disabled: {err:#}");
|
||||
(None, None)
|
||||
}
|
||||
};
|
||||
let services = SessionServices {
|
||||
mcp_connection_manager,
|
||||
session_manager: ExecSessionManager::default(),
|
||||
@@ -475,6 +484,8 @@ impl Session {
|
||||
turn_context.cwd.clone(),
|
||||
config.codex_linux_sandbox_exe.clone(),
|
||||
)),
|
||||
file_change_collector,
|
||||
_file_change_watcher: file_change_watcher,
|
||||
};
|
||||
|
||||
let sess = Arc::new(Session {
|
||||
@@ -729,6 +740,7 @@ impl Session {
|
||||
Some(turn_context.approval_policy),
|
||||
Some(turn_context.sandbox_policy.clone()),
|
||||
Some(self.user_shell().clone()),
|
||||
None,
|
||||
)));
|
||||
items
|
||||
}
|
||||
@@ -986,6 +998,13 @@ impl Session {
|
||||
self.send_event(event).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn take_changed_files(&self) -> Vec<PathBuf> {
|
||||
match &self.services.file_change_collector {
|
||||
Some(collector) => collector.drain().await,
|
||||
None => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn notify_stream_error(&self, sub_id: &str, message: impl Into<String>) {
|
||||
let event = Event {
|
||||
id: sub_id.to_string(),
|
||||
@@ -1227,6 +1246,7 @@ async fn submission_loop(
|
||||
sandbox_policy,
|
||||
// Shell is not configurable from turn to turn
|
||||
None,
|
||||
None,
|
||||
))])
|
||||
.await;
|
||||
}
|
||||
@@ -1659,6 +1679,22 @@ pub(crate) async fn run_task(
|
||||
} else {
|
||||
sess.record_input_and_rollout_usermsg(&initial_input_for_turn)
|
||||
.await;
|
||||
let changed_files = sess.take_changed_files().await;
|
||||
if !changed_files.is_empty() {
|
||||
let response_item: ResponseItem =
|
||||
EnvironmentContext::new(None, None, None, None, Some(changed_files)).into();
|
||||
sess.record_conversation_items(std::slice::from_ref(&response_item))
|
||||
.await;
|
||||
for msg in
|
||||
map_response_item_to_event_messages(&response_item, sess.show_raw_agent_reasoning())
|
||||
{
|
||||
let event = Event {
|
||||
id: sub_id.clone(),
|
||||
msg,
|
||||
};
|
||||
sess.send_event(event).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut last_agent_message: Option<String> = None;
|
||||
@@ -2450,6 +2486,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::config::ConfigOverrides;
|
||||
use crate::config::ConfigToml;
|
||||
use crate::file_change_notifier::collector_for_tests;
|
||||
|
||||
use crate::protocol::CompactedItem;
|
||||
use crate::protocol::InitialHistory;
|
||||
@@ -2780,6 +2817,8 @@ mod tests {
|
||||
turn_context.cwd.clone(),
|
||||
None,
|
||||
)),
|
||||
file_change_collector: None,
|
||||
_file_change_watcher: None,
|
||||
};
|
||||
let session = Session {
|
||||
conversation_id,
|
||||
@@ -2853,6 +2892,8 @@ mod tests {
|
||||
config.cwd.clone(),
|
||||
None,
|
||||
)),
|
||||
file_change_collector: None,
|
||||
_file_change_watcher: None,
|
||||
};
|
||||
let session = Arc::new(Session {
|
||||
conversation_id,
|
||||
@@ -3004,6 +3045,46 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_task_emits_environment_context_with_changed_files() {
|
||||
let (mut session, turn_context) = make_session_and_context();
|
||||
let collector = collector_for_tests(turn_context.cwd.clone());
|
||||
collector
|
||||
.record_for_tests(PathBuf::from("tracked.rs"))
|
||||
.await;
|
||||
session.services.file_change_collector = Some(collector);
|
||||
session.services._file_change_watcher = None;
|
||||
|
||||
let session = Arc::new(session);
|
||||
let turn_context = Arc::new(turn_context);
|
||||
let input = vec![InputItem::Text {
|
||||
text: "hello".to_string(),
|
||||
}];
|
||||
let _ = run_task(
|
||||
Arc::clone(&session),
|
||||
Arc::clone(&turn_context),
|
||||
"sub".to_string(),
|
||||
input,
|
||||
)
|
||||
.await;
|
||||
|
||||
let history = session.history_snapshot().await;
|
||||
let mut found = false;
|
||||
for item in history {
|
||||
if let ResponseItem::Message { content, .. } = item {
|
||||
for part in content {
|
||||
if let ContentItem::InputText { text } = part
|
||||
&& text.contains("<changed_files>")
|
||||
&& text.contains("tracked.rs")
|
||||
{
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(found, "missing changed_files environment context");
|
||||
}
|
||||
|
||||
fn sample_rollout(
|
||||
session: &Session,
|
||||
turn_context: &TurnContext,
|
||||
|
||||
@@ -29,6 +29,7 @@ pub(crate) struct EnvironmentContext {
|
||||
pub network_access: Option<NetworkAccess>,
|
||||
pub writable_roots: Option<Vec<PathBuf>>,
|
||||
pub shell: Option<Shell>,
|
||||
pub changed_files: Option<Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
impl EnvironmentContext {
|
||||
@@ -37,6 +38,7 @@ impl EnvironmentContext {
|
||||
approval_policy: Option<AskForApproval>,
|
||||
sandbox_policy: Option<SandboxPolicy>,
|
||||
shell: Option<Shell>,
|
||||
changed_files: Option<Vec<PathBuf>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cwd,
|
||||
@@ -70,6 +72,8 @@ impl EnvironmentContext {
|
||||
_ => None,
|
||||
},
|
||||
shell,
|
||||
changed_files: changed_files
|
||||
.and_then(|paths| if paths.is_empty() { None } else { Some(paths) }),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +89,7 @@ impl EnvironmentContext {
|
||||
writable_roots,
|
||||
// should compare all fields except shell
|
||||
shell: _,
|
||||
changed_files,
|
||||
} = other;
|
||||
|
||||
self.cwd == *cwd
|
||||
@@ -92,6 +97,7 @@ impl EnvironmentContext {
|
||||
&& self.sandbox_mode == *sandbox_mode
|
||||
&& self.network_access == *network_access
|
||||
&& self.writable_roots == *writable_roots
|
||||
&& self.changed_files == *changed_files
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +109,7 @@ impl From<&TurnContext> for EnvironmentContext {
|
||||
Some(turn_context.sandbox_policy.clone()),
|
||||
// Shell is not configurable from turn to turn
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -155,6 +162,14 @@ impl EnvironmentContext {
|
||||
{
|
||||
lines.push(format!(" <shell>{shell_name}</shell>"));
|
||||
}
|
||||
if let Some(changed_files) = &self.changed_files
|
||||
&& !changed_files.is_empty() {
|
||||
lines.push(" <changed_files>".to_string());
|
||||
for path in changed_files {
|
||||
lines.push(format!(" <path>{}</path>", path.to_string_lossy()));
|
||||
}
|
||||
lines.push(" </changed_files>".to_string());
|
||||
}
|
||||
lines.push(ENVIRONMENT_CONTEXT_CLOSE_TAG.to_string());
|
||||
lines.join("\n")
|
||||
}
|
||||
@@ -196,6 +211,7 @@ mod tests {
|
||||
Some(AskForApproval::OnRequest),
|
||||
Some(workspace_write_policy(vec!["/repo", "/tmp"], false)),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let expected = r#"<environment_context>
|
||||
@@ -219,6 +235,7 @@ mod tests {
|
||||
Some(AskForApproval::Never),
|
||||
Some(SandboxPolicy::ReadOnly),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let expected = r#"<environment_context>
|
||||
@@ -237,6 +254,7 @@ mod tests {
|
||||
Some(AskForApproval::OnFailure),
|
||||
Some(SandboxPolicy::DangerFullAccess),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let expected = r#"<environment_context>
|
||||
@@ -256,12 +274,14 @@ mod tests {
|
||||
Some(AskForApproval::OnRequest),
|
||||
Some(workspace_write_policy(vec!["/repo"], false)),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let context2 = EnvironmentContext::new(
|
||||
Some(PathBuf::from("/repo")),
|
||||
Some(AskForApproval::Never),
|
||||
Some(workspace_write_policy(vec!["/repo"], true)),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert!(!context1.equals_except_shell(&context2));
|
||||
}
|
||||
@@ -273,12 +293,14 @@ mod tests {
|
||||
Some(AskForApproval::OnRequest),
|
||||
Some(SandboxPolicy::new_read_only_policy()),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let context2 = EnvironmentContext::new(
|
||||
Some(PathBuf::from("/repo")),
|
||||
Some(AskForApproval::OnRequest),
|
||||
Some(SandboxPolicy::new_workspace_write_policy()),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!context1.equals_except_shell(&context2));
|
||||
@@ -291,12 +313,14 @@ mod tests {
|
||||
Some(AskForApproval::OnRequest),
|
||||
Some(workspace_write_policy(vec!["/repo", "/tmp", "/var"], false)),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let context2 = EnvironmentContext::new(
|
||||
Some(PathBuf::from("/repo")),
|
||||
Some(AskForApproval::OnRequest),
|
||||
Some(workspace_write_policy(vec!["/repo", "/tmp"], true)),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!context1.equals_except_shell(&context2));
|
||||
@@ -312,6 +336,7 @@ mod tests {
|
||||
shell_path: "/bin/bash".into(),
|
||||
bashrc_path: "/home/user/.bashrc".into(),
|
||||
})),
|
||||
None,
|
||||
);
|
||||
let context2 = EnvironmentContext::new(
|
||||
Some(PathBuf::from("/repo")),
|
||||
@@ -321,6 +346,7 @@ mod tests {
|
||||
shell_path: "/bin/zsh".into(),
|
||||
zshrc_path: "/home/user/.zshrc".into(),
|
||||
})),
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(context1.equals_except_shell(&context2));
|
||||
|
||||
177
codex-rs/core/src/file_change_notifier.rs
Normal file
177
codex-rs/core/src/file_change_notifier.rs
Normal file
@@ -0,0 +1,177 @@
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use dunce::canonicalize;
|
||||
use notify::Config;
|
||||
use notify::Event;
|
||||
use notify::EventKind;
|
||||
use notify::RecommendedWatcher;
|
||||
use notify::RecursiveMode;
|
||||
use notify::Watcher;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::warn;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct FileChangeCollector {
|
||||
root: Arc<PathBuf>,
|
||||
changes: Arc<Mutex<HashSet<PathBuf>>>,
|
||||
}
|
||||
|
||||
impl FileChangeCollector {
|
||||
fn new(root: PathBuf) -> Self {
|
||||
Self {
|
||||
root: Arc::new(root),
|
||||
changes: Arc::new(Mutex::new(HashSet::new())),
|
||||
}
|
||||
}
|
||||
|
||||
fn root_path(&self) -> &Path {
|
||||
self.root.as_ref()
|
||||
}
|
||||
|
||||
pub(crate) async fn record_event(&self, event: Event) {
|
||||
let Event { kind, paths, .. } = event;
|
||||
if should_skip(kind) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut normalized_paths = Vec::new();
|
||||
for path in paths {
|
||||
if let Some(rel) = self.normalize_path(&path) {
|
||||
normalized_paths.push(rel);
|
||||
}
|
||||
}
|
||||
|
||||
if normalized_paths.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut guard = self.changes.lock().await;
|
||||
guard.extend(normalized_paths);
|
||||
}
|
||||
|
||||
fn normalize_path(&self, path: &Path) -> Option<PathBuf> {
|
||||
let rel = path.strip_prefix(self.root_path()).ok()?;
|
||||
if should_ignore(rel) {
|
||||
return None;
|
||||
}
|
||||
Some(rel.to_path_buf())
|
||||
}
|
||||
|
||||
pub(crate) async fn drain(&self) -> Vec<PathBuf> {
|
||||
let mut guard = self.changes.lock().await;
|
||||
let mut entries: Vec<PathBuf> = guard.drain().collect();
|
||||
entries.sort();
|
||||
entries
|
||||
}
|
||||
}
|
||||
|
||||
fn should_skip(kind: EventKind) -> bool {
|
||||
matches!(
|
||||
kind,
|
||||
EventKind::Access(_) | EventKind::Other | EventKind::Any
|
||||
)
|
||||
}
|
||||
|
||||
fn should_ignore(path: &Path) -> bool {
|
||||
path.components().any(|component| {
|
||||
if let std::path::Component::Normal(part) = component {
|
||||
matches!(
|
||||
part.to_str(),
|
||||
Some(".git") | Some(".codex") | Some("target") | Some(".DS_Store") | Some(".idea")
|
||||
)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) struct FileChangeWatcher {
|
||||
_watcher: RecommendedWatcher,
|
||||
task: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl FileChangeWatcher {
|
||||
pub(crate) fn start(root: PathBuf) -> Result<(Self, FileChangeCollector)> {
|
||||
let canonical_root = canonicalize(&root)
|
||||
.with_context(|| format!("failed to canonicalize {}", root.display()))?;
|
||||
let collector = FileChangeCollector::new(canonical_root.clone());
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
let mut watcher = RecommendedWatcher::new(
|
||||
move |res| {
|
||||
let _ = tx.send(res);
|
||||
},
|
||||
Config::default(),
|
||||
)
|
||||
.context("failed to initialize file watcher")?;
|
||||
watcher
|
||||
.watch(&canonical_root, RecursiveMode::Recursive)
|
||||
.with_context(|| format!("failed to watch path {}", canonical_root.display()))?;
|
||||
|
||||
let collector_clone = collector.clone();
|
||||
let task = tokio::spawn(async move {
|
||||
while let Some(event_result) = rx.recv().await {
|
||||
match event_result {
|
||||
Ok(event) => collector_clone.record_event(event).await,
|
||||
Err(err) => warn!("file watcher error: {err}"),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok((
|
||||
Self {
|
||||
_watcher: watcher,
|
||||
task,
|
||||
},
|
||||
collector,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FileChangeWatcher {
|
||||
fn drop(&mut self) {
|
||||
if self.task.is_finished() {
|
||||
return;
|
||||
}
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn collector_for_tests(root: PathBuf) -> FileChangeCollector {
|
||||
FileChangeCollector::new(root)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl FileChangeCollector {
|
||||
pub(crate) async fn record_for_tests(&self, path: PathBuf) {
|
||||
let mut guard = self.changes.lock().await;
|
||||
guard.insert(path);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn drain_returns_empty_when_no_changes() {
|
||||
let collector = collector_for_tests(PathBuf::from("."));
|
||||
let drained = collector.drain().await;
|
||||
assert!(drained.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn record_and_drain_changes() {
|
||||
let collector = collector_for_tests(PathBuf::from("/tmp/root"));
|
||||
collector.record_for_tests(PathBuf::from("file.txt")).await;
|
||||
let drained = collector.drain().await;
|
||||
assert_eq!(drained, vec![PathBuf::from("file.txt")]);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ pub mod exec;
|
||||
mod exec_command;
|
||||
pub mod exec_env;
|
||||
pub mod executor;
|
||||
mod file_change_notifier;
|
||||
mod flags;
|
||||
pub mod git_info;
|
||||
pub mod landlock;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::RolloutRecorder;
|
||||
use crate::exec_command::ExecSessionManager;
|
||||
use crate::executor::Executor;
|
||||
use crate::file_change_notifier::FileChangeCollector;
|
||||
use crate::file_change_notifier::FileChangeWatcher;
|
||||
use crate::mcp_connection_manager::McpConnectionManager;
|
||||
use crate::unified_exec::UnifiedExecSessionManager;
|
||||
use crate::user_notification::UserNotifier;
|
||||
@@ -15,4 +17,6 @@ pub(crate) struct SessionServices {
|
||||
pub(crate) user_shell: crate::shell::Shell,
|
||||
pub(crate) show_raw_agent_reasoning: bool,
|
||||
pub(crate) executor: Executor,
|
||||
pub(crate) file_change_collector: Option<FileChangeCollector>,
|
||||
pub(crate) _file_change_watcher: Option<FileChangeWatcher>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user