diff --git a/Cargo.lock b/Cargo.lock index a09459ddb6..59477b4d17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -158,8 +158,10 @@ dependencies = [ "acp_thread", "agent-client-protocol", "agent_servers", + "agent_settings", "anyhow", "assistant_tool", + "assistant_tools", "client", "clock", "cloud_llm_client", @@ -176,6 +178,7 @@ dependencies = [ "language_model", "language_models", "log", + "paths", "project", "prompt_store", "reqwest_client", diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 884378fbcc..062ba079ba 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -14,9 +14,11 @@ workspace = true [dependencies] acp_thread.workspace = true agent-client-protocol.workspace = true +agent_settings.workspace = true agent_servers.workspace = true anyhow.workspace = true assistant_tool.workspace = true +assistant_tools.workspace = true cloud_llm_client.workspace = true collections.workspace = true fs.workspace = true @@ -24,9 +26,11 @@ futures.workspace = true gpui.workspace = true handlebars = { workspace = true, features = ["rust-embed"] } indoc.workspace = true +language.workspace = true language_model.workspace = true language_models.workspace = true log.workspace = true +paths.workspace = true project.workspace = true prompt_store.workspace = true rust-embed.workspace = true diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 4583688ad3..fe91dc7d0f 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,5 +1,5 @@ use crate::{templates::Templates, AgentResponseEvent, Thread}; -use crate::{FindPathTool, ToolCallAuthorization}; +use crate::{EditFileTool, FindPathTool, ToolCallAuthorization}; use acp_thread::ModelSelector; use agent_client_protocol as acp; use anyhow::{anyhow, Context as _, Result}; @@ -412,9 +412,10 @@ impl acp_thread::AgentConnection for NativeAgentConnection { anyhow!("No default model configured. Please configure a default model in settings.") })?; - let thread = cx.new(|_| { - let mut thread = Thread::new(project.clone(), agent.project_context.clone(), action_log, agent.templates.clone(), default_model); + let thread = cx.new(|cx| { + let mut thread = Thread::new(project.clone(), agent.project_context.clone(), action_log.clone(), agent.templates.clone(), default_model); thread.add_tool(FindPathTool::new(project.clone())); + thread.add_tool(EditFileTool::new(project.clone(), cx.entity())); thread }); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 9be2860eec..1b7751bf9a 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1,4 +1,5 @@ use crate::templates::{SystemPromptTemplate, Template, Templates}; +use acp_thread::Diff; use agent_client_protocol as acp; use anyhow::{anyhow, Context as _, Result}; use assistant_tool::ActionLog; @@ -132,7 +133,7 @@ pub struct Thread { project_context: Rc>, templates: Arc, pub selected_model: Arc, - _action_log: Entity, + action_log: Entity, } impl Thread { @@ -152,7 +153,7 @@ impl Thread { project_context, templates, selected_model: default_model, - _action_log: action_log, + action_log, } } @@ -322,6 +323,10 @@ impl Thread { events_rx } + pub fn action_log(&self) -> &Entity { + &self.action_log + } + pub fn build_system_message(&self) -> AgentMessage { log::debug!("Building system message"); let prompt = SystemPromptTemplate { @@ -538,6 +543,11 @@ impl Thread { status: Some(acp::ToolCallStatus::InProgress), ..Default::default() }); + log::trace!( + "Running tool {:?} with input: {}", + tool_use.name, + serde_json::to_string_pretty(&tool_use.input).unwrap_or_default() + ); cx.update(|cx| tool.run(tool_use.input, tool_event_stream, cx))? .await }) @@ -586,7 +596,7 @@ impl Thread { self.messages.last_mut().unwrap() } - fn build_completion_request( + pub(crate) fn build_completion_request( &self, completion_intent: CompletionIntent, cx: &mut App, @@ -866,6 +876,12 @@ impl AgentResponseEventStream { .ok(); } + fn send_tool_call_diff(&self, tool_call_diff: ToolCallDiff) { + self.0 + .unbounded_send(Ok(AgentResponseEvent::ToolCallDiff(tool_call_diff))) + .ok(); + } + fn send_stop(&self, reason: StopReason) { match reason { StopReason::EndTurn => { @@ -909,4 +925,11 @@ impl ToolCallEventStream { pub fn send_update(&self, fields: acp::ToolCallUpdateFields) { self.stream.send_tool_call_update(&self.tool_use_id, fields); } + + pub fn send_diff(&self, diff: Entity) { + self.stream.send_tool_call_diff(ToolCallDiff { + tool_call_id: acp::ToolCallId(self.tool_use_id.to_string().into()), + diff, + }); + } } diff --git a/crates/agent2/src/tools.rs b/crates/agent2/src/tools.rs index 906992fc52..10e0a22ec5 100644 --- a/crates/agent2/src/tools.rs +++ b/crates/agent2/src/tools.rs @@ -1,3 +1,5 @@ +mod edit_file_tool; mod find_path_tool; +pub use edit_file_tool::*; pub use find_path_tool::*; diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs new file mode 100644 index 0000000000..10edba703e --- /dev/null +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -0,0 +1,1588 @@ +use acp_thread::Diff; +use agent_client_protocol as acp; +use anyhow::{anyhow, Context as _, Result}; +use assistant_tools::edit_agent::{EditAgent, EditAgentOutputEvent, EditFormat}; +use cloud_llm_client::CompletionIntent; +use collections::HashSet; +use gpui::{App, AppContext, AsyncApp, Entity, Task}; +use indoc::formatdoc; +use language::language_settings::{self, FormatOnSave}; +use paths; +use project::lsp_store::{FormatTrigger, LspFormatTarget}; +use project::{Project, ProjectPath}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Settings as _; +use smol::stream::StreamExt as _; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use ui::SharedString; +use util::ResultExt; + +use crate::{AgentTool, Thread, ToolCallEventStream}; + +/// This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead. +/// +/// Before using this tool: +/// +/// 1. Use the `read_file` tool to understand the file's contents and context +/// +/// 2. Verify the directory path is correct (only applicable when creating new files): +/// - Use the `list_directory` tool to verify the parent directory exists and is the correct location +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct EditFileToolInput { + /// A one-line, user-friendly markdown description of the edit. This will be + /// shown in the UI and also passed to another model to perform the edit. + /// + /// Be terse, but also descriptive in what you want to achieve with this + /// edit. Avoid generic instructions. + /// + /// NEVER mention the file path in this description. + /// + /// Fix API endpoint URLs + /// Update copyright year in `page_footer` + /// + /// Make sure to include this field before all the others in the input object + /// so that we can display it immediately. + pub display_description: String, + + /// The full path of the file to create or modify in the project. + /// + /// WARNING: When specifying which file path need changing, you MUST + /// start each path with one of the project's root directories. + /// + /// The following examples assume we have two root directories in the project: + /// - /a/b/backend + /// - /c/d/frontend + /// + /// + /// `backend/src/main.rs` + /// + /// Notice how the file path starts with `backend`. Without that, the path + /// would be ambiguous and the call would fail! + /// + /// + /// + /// `frontend/db.js` + /// + pub path: PathBuf, + + /// The mode of operation on the file. Possible values: + /// - 'edit': Make granular edits to an existing file. + /// - 'create': Create a new file if it doesn't exist. + /// - 'overwrite': Replace the entire contents of an existing file. + /// + /// When a file already exists or you just created it, prefer editing + /// it as opposed to recreating it from scratch. + pub mode: EditFileMode, + + /// The new content for the file (required for create and overwrite modes) + /// For edit mode, this field is not used - edits happen through the edit agent + #[serde(default)] + pub content: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum EditFileMode { + Edit, + Create, + Overwrite, +} + +pub struct EditFileTool { + project: Entity, + thread: Entity, +} + +impl EditFileTool { + pub fn new(project: Entity, thread: Entity) -> Self { + Self { project, thread } + } +} + +impl AgentTool for EditFileTool { + type Input = EditFileToolInput; + + fn name(&self) -> SharedString { + "edit_file".into() + } + + fn kind(&self) -> acp::ToolKind { + acp::ToolKind::Edit + } + + fn needs_authorization(&self, input: Self::Input, cx: &App) -> bool { + if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions { + return false; + } + + // If any path component matches the local settings folder, then this could affect + // the editor in ways beyond the project source, so prompt. + let local_settings_folder = paths::local_settings_folder_relative_path(); + let path = Path::new(&input.path); + if path + .components() + .any(|component| component.as_os_str() == local_settings_folder.as_os_str()) + { + return true; + } + + // It's also possible that the global config dir is configured to be inside the project, + // so check for that edge case too. + if let Ok(canonical_path) = std::fs::canonicalize(&input.path) { + if canonical_path.starts_with(paths::config_dir()) { + return true; + } + } + + // Check if path is inside the global config directory + // First check if it's already inside project - if not, try to canonicalize + let project_path = self.project.read(cx).find_project_path(&input.path, cx); + + // If the path is inside the project, and it's not one of the above edge cases, + // then no confirmation is necessary. Otherwise, confirmation is necessary. + project_path.is_none() + } + + fn run( + self: Arc, + input: Self::Input, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task> { + let project_path = match resolve_path(&input, self.project.clone(), cx) { + Ok(path) => path, + Err(err) => return Task::ready(Err(anyhow!(err))).into(), + }; + + let project = self.project.clone(); + let request = self.thread.update(cx, |thread, cx| { + thread.build_completion_request(CompletionIntent::ToolResults, cx) + }); + let thread = self.thread.read(cx); + let model = thread.selected_model.clone(); + let action_log = thread.action_log().clone(); + + cx.spawn(async move |cx: &mut AsyncApp| { + let edit_format = EditFormat::from_model(model.clone())?; + let edit_agent = EditAgent::new( + model, + project.clone(), + action_log.clone(), + // todo! move edit agent to this crate so we can use our templates? + assistant_tools::templates::Templates::new(), + edit_format, + ); + + let buffer = project + .update(cx, |project, cx| { + project.open_buffer(project_path.clone(), cx) + })? + .await?; + + let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?; + event_stream.send_diff(diff.clone()); + + let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; + let old_text = cx + .background_spawn({ + let old_snapshot = old_snapshot.clone(); + async move { Arc::new(old_snapshot.text()) } + }) + .await; + + + let mut events = if matches!(input.mode, EditFileMode::Edit) { + edit_agent.edit( + buffer.clone(), + input.display_description.clone(), + &request, + cx, + ).1 + } else { + edit_agent.overwrite( + buffer.clone(), + input.display_description.clone(), + &request, + cx, + ).1 + }; + + let mut hallucinated_old_text = false; + let mut ambiguous_ranges = Vec::new(); + while let Some(event) = events.next().await { + match event { + EditAgentOutputEvent::Edited => {}, + EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true, + EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges, + EditAgentOutputEvent::ResolvingEditRange(range) => { + diff.update(cx, |card, cx| card.reveal_range(range, cx))?; + } + } + } + + // If format_on_save is enabled, format the buffer + let format_on_save_enabled = buffer + .read_with(cx, |buffer, cx| { + let settings = language_settings::language_settings( + buffer.language().map(|l| l.name()), + buffer.file(), + cx, + ); + settings.format_on_save != FormatOnSave::Off + }) + .unwrap_or(false); + + if format_on_save_enabled { + action_log.update(cx, |log, cx| { + log.buffer_edited(buffer.clone(), cx); + })?; + + let format_task = project.update(cx, |project, cx| { + project.format( + HashSet::from_iter([buffer.clone()]), + LspFormatTarget::Buffers, + false, // Don't push to history since the tool did it. + FormatTrigger::Save, + cx, + ) + })?; + format_task.await.log_err(); + } + + project + .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))? + .await?; + + action_log.update(cx, |log, cx| { + log.buffer_edited(buffer.clone(), cx); + })?; + + let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; + let unified_diff = cx + .background_spawn({ + let new_snapshot = new_snapshot.clone(); + let old_text = old_text.clone(); + async move { + let new_text = new_snapshot.text(); + language::unified_diff(&old_text, &new_text) + } + }) + .await; + + diff.update(cx, |diff, cx| { + diff.finalize(cx); + }).ok(); + + let input_path = input.path.display(); + if unified_diff.is_empty() { + anyhow::ensure!( + !hallucinated_old_text, + formatdoc! {" + Some edits were produced but none of them could be applied. + Read the relevant sections of {input_path} again so that + I can perform the requested edits. + "} + ); + anyhow::ensure!( + ambiguous_ranges.is_empty(), + { + let line_numbers = ambiguous_ranges + .iter() + .map(|range| range.start.to_string()) + .collect::>() + .join(", "); + formatdoc! {" + matches more than one position in the file (lines: {line_numbers}). Read the + relevant sections of {input_path} again and extend so + that I can perform the requested edits. + "} + } + ); + Ok("No edits were made.".into()) + } else { + Ok(format!( + "Edited {}:\n\n```diff\n{}\n```", + input_path, unified_diff + )) + } + }) + } +} + +/// Validate that the file path is valid, meaning: +/// +/// - For `edit` and `overwrite`, the path must point to an existing file. +/// - For `create`, the file must not already exist, but it's parent dir must exist. +fn resolve_path( + input: &EditFileToolInput, + project: Entity, + cx: &mut App, +) -> Result { + let project = project.read(cx); + + match input.mode { + EditFileMode::Edit | EditFileMode::Overwrite => { + let path = project + .find_project_path(&input.path, cx) + .context("Can't edit file: path not found")?; + + let entry = project + .entry_for_path(&path, cx) + .context("Can't edit file: path not found")?; + + anyhow::ensure!(entry.is_file(), "Can't edit file: path is a directory"); + Ok(path) + } + + EditFileMode::Create => { + if let Some(path) = project.find_project_path(&input.path, cx) { + anyhow::ensure!( + project.entry_for_path(&path, cx).is_none(), + "Can't create file: file already exists" + ); + } + + let parent_path = input + .path + .parent() + .context("Can't create file: incorrect path")?; + + let parent_project_path = project.find_project_path(&parent_path, cx); + + let parent_entry = parent_project_path + .as_ref() + .and_then(|path| project.entry_for_path(&path, cx)) + .context("Can't create file: parent directory doesn't exist")?; + + anyhow::ensure!( + parent_entry.is_dir(), + "Can't create file: parent is not a directory" + ); + + let file_name = input + .path + .file_name() + .context("Can't create file: invalid filename")?; + + let new_file_path = parent_project_path.map(|parent| ProjectPath { + path: Arc::from(parent.path.join(file_name)), + ..parent + }); + + new_file_path.context("Can't create file") + } + } +} + +// todo! restore tests +// #[cfg(test)] +// mod tests { +// use super::*; +// use ::fs::Fs; +// use client::TelemetrySettings; +// use gpui::{TestAppContext, UpdateGlobal}; +// use language_model::fake_provider::FakeLanguageModel; +// use serde_json::json; +// use settings::SettingsStore; +// use std::fs; +// use util::path; + +// #[gpui::test] +// async fn test_edit_nonexistent_file(cx: &mut TestAppContext) { +// init_test(cx); + +// let fs = project::FakeFs::new(cx.executor()); +// fs.insert_tree("/root", json!({})).await; +// let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; +// let action_log = cx.new(|_| ActionLog::new(project.clone())); +// let model = Arc::new(FakeLanguageModel::default()); +// let result = cx +// .update(|cx| { +// let input = serde_json::to_value(EditFileToolInput { +// display_description: "Some edit".into(), +// path: "root/nonexistent_file.txt".into(), +// mode: EditFileMode::Edit, +// }) +// .unwrap(); +// Arc::new(EditFileTool) +// .run( +// input, +// Arc::default(), +// project.clone(), +// action_log, +// model, +// None, +// cx, +// ) +// .output +// }) +// .await; +// assert_eq!( +// result.unwrap_err().to_string(), +// "Can't edit file: path not found" +// ); +// } + +// #[gpui::test] +// async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) { +// let mode = &EditFileMode::Create; + +// let result = test_resolve_path(mode, "root/new.txt", cx); +// assert_resolved_path_eq(result.await, "new.txt"); + +// let result = test_resolve_path(mode, "new.txt", cx); +// assert_resolved_path_eq(result.await, "new.txt"); + +// let result = test_resolve_path(mode, "dir/new.txt", cx); +// assert_resolved_path_eq(result.await, "dir/new.txt"); + +// let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx); +// assert_eq!( +// result.await.unwrap_err().to_string(), +// "Can't create file: file already exists" +// ); + +// let result = test_resolve_path(mode, "root/dir/nonexistent_dir/new.txt", cx); +// assert_eq!( +// result.await.unwrap_err().to_string(), +// "Can't create file: parent directory doesn't exist" +// ); +// } + +// #[gpui::test] +// async fn test_resolve_path_for_editing_file(cx: &mut TestAppContext) { +// let mode = &EditFileMode::Edit; + +// let path_with_root = "root/dir/subdir/existing.txt"; +// let path_without_root = "dir/subdir/existing.txt"; +// let result = test_resolve_path(mode, path_with_root, cx); +// assert_resolved_path_eq(result.await, path_without_root); + +// let result = test_resolve_path(mode, path_without_root, cx); +// assert_resolved_path_eq(result.await, path_without_root); + +// let result = test_resolve_path(mode, "root/nonexistent.txt", cx); +// assert_eq!( +// result.await.unwrap_err().to_string(), +// "Can't edit file: path not found" +// ); + +// let result = test_resolve_path(mode, "root/dir", cx); +// assert_eq!( +// result.await.unwrap_err().to_string(), +// "Can't edit file: path is a directory" +// ); +// } + +// async fn test_resolve_path( +// mode: &EditFileMode, +// path: &str, +// cx: &mut TestAppContext, +// ) -> anyhow::Result { +// init_test(cx); + +// let fs = project::FakeFs::new(cx.executor()); +// fs.insert_tree( +// "/root", +// json!({ +// "dir": { +// "subdir": { +// "existing.txt": "hello" +// } +// } +// }), +// ) +// .await; +// let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + +// let input = EditFileToolInput { +// display_description: "Some edit".into(), +// path: path.into(), +// mode: mode.clone(), +// }; + +// let result = cx.update(|cx| resolve_path(&input, project, cx)); +// result +// } + +// fn assert_resolved_path_eq(path: anyhow::Result, expected: &str) { +// let actual = path +// .expect("Should return valid path") +// .path +// .to_str() +// .unwrap() +// .replace("\\", "/"); // Naive Windows paths normalization +// assert_eq!(actual, expected); +// } + +// #[test] +// fn still_streaming_ui_text_with_path() { +// let input = json!({ +// "path": "src/main.rs", +// "display_description": "", +// "old_string": "old code", +// "new_string": "new code" +// }); + +// assert_eq!(EditFileTool.still_streaming_ui_text(&input), "src/main.rs"); +// } + +// #[test] +// fn still_streaming_ui_text_with_description() { +// let input = json!({ +// "path": "", +// "display_description": "Fix error handling", +// "old_string": "old code", +// "new_string": "new code" +// }); + +// assert_eq!( +// EditFileTool.still_streaming_ui_text(&input), +// "Fix error handling", +// ); +// } + +// #[test] +// fn still_streaming_ui_text_with_path_and_description() { +// let input = json!({ +// "path": "src/main.rs", +// "display_description": "Fix error handling", +// "old_string": "old code", +// "new_string": "new code" +// }); + +// assert_eq!( +// EditFileTool.still_streaming_ui_text(&input), +// "Fix error handling", +// ); +// } + +// #[test] +// fn still_streaming_ui_text_no_path_or_description() { +// let input = json!({ +// "path": "", +// "display_description": "", +// "old_string": "old code", +// "new_string": "new code" +// }); + +// assert_eq!( +// EditFileTool.still_streaming_ui_text(&input), +// DEFAULT_UI_TEXT, +// ); +// } + +// #[test] +// fn still_streaming_ui_text_with_null() { +// let input = serde_json::Value::Null; + +// assert_eq!( +// EditFileTool.still_streaming_ui_text(&input), +// DEFAULT_UI_TEXT, +// ); +// } + +// fn init_test(cx: &mut TestAppContext) { +// cx.update(|cx| { +// let settings_store = SettingsStore::test(cx); +// cx.set_global(settings_store); +// language::init(cx); +// TelemetrySettings::register(cx); +// agent_settings::AgentSettings::register(cx); +// Project::init_settings(cx); +// }); +// } + +// fn init_test_with_config(cx: &mut TestAppContext, data_dir: &Path) { +// cx.update(|cx| { +// // Set custom data directory (config will be under data_dir/config) +// paths::set_custom_data_dir(data_dir.to_str().unwrap()); + +// let settings_store = SettingsStore::test(cx); +// cx.set_global(settings_store); +// language::init(cx); +// TelemetrySettings::register(cx); +// agent_settings::AgentSettings::register(cx); +// Project::init_settings(cx); +// }); +// } + +// #[gpui::test] +// async fn test_format_on_save(cx: &mut TestAppContext) { +// init_test(cx); + +// let fs = project::FakeFs::new(cx.executor()); +// fs.insert_tree("/root", json!({"src": {}})).await; + +// let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + +// // Set up a Rust language with LSP formatting support +// let rust_language = Arc::new(language::Language::new( +// language::LanguageConfig { +// name: "Rust".into(), +// matcher: language::LanguageMatcher { +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// ..Default::default() +// }, +// None, +// )); + +// // Register the language and fake LSP +// let language_registry = project.read_with(cx, |project, _| project.languages().clone()); +// language_registry.add(rust_language); + +// let mut fake_language_servers = language_registry.register_fake_lsp( +// "Rust", +// language::FakeLspAdapter { +// capabilities: lsp::ServerCapabilities { +// document_formatting_provider: Some(lsp::OneOf::Left(true)), +// ..Default::default() +// }, +// ..Default::default() +// }, +// ); + +// // Create the file +// fs.save( +// path!("/root/src/main.rs").as_ref(), +// &"initial content".into(), +// language::LineEnding::Unix, +// ) +// .await +// .unwrap(); + +// // Open the buffer to trigger LSP initialization +// let buffer = project +// .update(cx, |project, cx| { +// project.open_local_buffer(path!("/root/src/main.rs"), cx) +// }) +// .await +// .unwrap(); + +// // Register the buffer with language servers +// let _handle = project.update(cx, |project, cx| { +// project.register_buffer_with_language_servers(&buffer, cx) +// }); + +// const UNFORMATTED_CONTENT: &str = "fn main() {println!(\"Hello!\");}\n"; +// const FORMATTED_CONTENT: &str = +// "This file was formatted by the fake formatter in the test.\n"; + +// // Get the fake language server and set up formatting handler +// let fake_language_server = fake_language_servers.next().await.unwrap(); +// fake_language_server.set_request_handler::({ +// |_, _| async move { +// Ok(Some(vec![lsp::TextEdit { +// range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(1, 0)), +// new_text: FORMATTED_CONTENT.to_string(), +// }])) +// } +// }); + +// let action_log = cx.new(|_| ActionLog::new(project.clone())); +// let model = Arc::new(FakeLanguageModel::default()); + +// // First, test with format_on_save enabled +// cx.update(|cx| { +// SettingsStore::update_global(cx, |store, cx| { +// store.update_user_settings::( +// cx, +// |settings| { +// settings.defaults.format_on_save = Some(FormatOnSave::On); +// settings.defaults.formatter = +// Some(language::language_settings::SelectedFormatter::Auto); +// }, +// ); +// }); +// }); + +// // Have the model stream unformatted content +// let edit_result = { +// let edit_task = cx.update(|cx| { +// let input = serde_json::to_value(EditFileToolInput { +// display_description: "Create main function".into(), +// path: "root/src/main.rs".into(), +// mode: EditFileMode::Overwrite, +// }) +// .unwrap(); +// Arc::new(EditFileTool) +// .run( +// input, +// Arc::default(), +// project.clone(), +// action_log.clone(), +// model.clone(), +// None, +// cx, +// ) +// .output +// }); + +// // Stream the unformatted content +// cx.executor().run_until_parked(); +// model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string()); +// model.end_last_completion_stream(); + +// edit_task.await +// }; +// assert!(edit_result.is_ok()); + +// // Wait for any async operations (e.g. formatting) to complete +// cx.executor().run_until_parked(); + +// // Read the file to verify it was formatted automatically +// let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); +// assert_eq!( +// // Ignore carriage returns on Windows +// new_content.replace("\r\n", "\n"), +// FORMATTED_CONTENT, +// "Code should be formatted when format_on_save is enabled" +// ); + +// let stale_buffer_count = action_log.read_with(cx, |log, cx| log.stale_buffers(cx).count()); + +// assert_eq!( +// stale_buffer_count, 0, +// "BUG: Buffer is incorrectly marked as stale after format-on-save. Found {} stale buffers. \ +// This causes the agent to think the file was modified externally when it was just formatted.", +// stale_buffer_count +// ); + +// // Next, test with format_on_save disabled +// cx.update(|cx| { +// SettingsStore::update_global(cx, |store, cx| { +// store.update_user_settings::( +// cx, +// |settings| { +// settings.defaults.format_on_save = Some(FormatOnSave::Off); +// }, +// ); +// }); +// }); + +// // Stream unformatted edits again +// let edit_result = { +// let edit_task = cx.update(|cx| { +// let input = serde_json::to_value(EditFileToolInput { +// display_description: "Update main function".into(), +// path: "root/src/main.rs".into(), +// mode: EditFileMode::Overwrite, +// }) +// .unwrap(); +// Arc::new(EditFileTool) +// .run( +// input, +// Arc::default(), +// project.clone(), +// action_log.clone(), +// model.clone(), +// None, +// cx, +// ) +// .output +// }); + +// // Stream the unformatted content +// cx.executor().run_until_parked(); +// model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string()); +// model.end_last_completion_stream(); + +// edit_task.await +// }; +// assert!(edit_result.is_ok()); + +// // Wait for any async operations (e.g. formatting) to complete +// cx.executor().run_until_parked(); + +// // Verify the file was not formatted +// let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); +// assert_eq!( +// // Ignore carriage returns on Windows +// new_content.replace("\r\n", "\n"), +// UNFORMATTED_CONTENT, +// "Code should not be formatted when format_on_save is disabled" +// ); +// } + +// #[gpui::test] +// async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) { +// init_test(cx); + +// let fs = project::FakeFs::new(cx.executor()); +// fs.insert_tree("/root", json!({"src": {}})).await; + +// // Create a simple file with trailing whitespace +// fs.save( +// path!("/root/src/main.rs").as_ref(), +// &"initial content".into(), +// language::LineEnding::Unix, +// ) +// .await +// .unwrap(); + +// let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; +// let action_log = cx.new(|_| ActionLog::new(project.clone())); +// let model = Arc::new(FakeLanguageModel::default()); + +// // First, test with remove_trailing_whitespace_on_save enabled +// cx.update(|cx| { +// SettingsStore::update_global(cx, |store, cx| { +// store.update_user_settings::( +// cx, +// |settings| { +// settings.defaults.remove_trailing_whitespace_on_save = Some(true); +// }, +// ); +// }); +// }); + +// const CONTENT_WITH_TRAILING_WHITESPACE: &str = +// "fn main() { \n println!(\"Hello!\"); \n}\n"; + +// // Have the model stream content that contains trailing whitespace +// let edit_result = { +// let edit_task = cx.update(|cx| { +// let input = serde_json::to_value(EditFileToolInput { +// display_description: "Create main function".into(), +// path: "root/src/main.rs".into(), +// mode: EditFileMode::Overwrite, +// }) +// .unwrap(); +// Arc::new(EditFileTool) +// .run( +// input, +// Arc::default(), +// project.clone(), +// action_log.clone(), +// model.clone(), +// None, +// cx, +// ) +// .output +// }); + +// // Stream the content with trailing whitespace +// cx.executor().run_until_parked(); +// model.send_last_completion_stream_text_chunk( +// CONTENT_WITH_TRAILING_WHITESPACE.to_string(), +// ); +// model.end_last_completion_stream(); + +// edit_task.await +// }; +// assert!(edit_result.is_ok()); + +// // Wait for any async operations (e.g. formatting) to complete +// cx.executor().run_until_parked(); + +// // Read the file to verify trailing whitespace was removed automatically +// assert_eq!( +// // Ignore carriage returns on Windows +// fs.load(path!("/root/src/main.rs").as_ref()) +// .await +// .unwrap() +// .replace("\r\n", "\n"), +// "fn main() {\n println!(\"Hello!\");\n}\n", +// "Trailing whitespace should be removed when remove_trailing_whitespace_on_save is enabled" +// ); + +// // Next, test with remove_trailing_whitespace_on_save disabled +// cx.update(|cx| { +// SettingsStore::update_global(cx, |store, cx| { +// store.update_user_settings::( +// cx, +// |settings| { +// settings.defaults.remove_trailing_whitespace_on_save = Some(false); +// }, +// ); +// }); +// }); + +// // Stream edits again with trailing whitespace +// let edit_result = { +// let edit_task = cx.update(|cx| { +// let input = serde_json::to_value(EditFileToolInput { +// display_description: "Update main function".into(), +// path: "root/src/main.rs".into(), +// mode: EditFileMode::Overwrite, +// }) +// .unwrap(); +// Arc::new(EditFileTool) +// .run( +// input, +// Arc::default(), +// project.clone(), +// action_log.clone(), +// model.clone(), +// None, +// cx, +// ) +// .output +// }); + +// // Stream the content with trailing whitespace +// cx.executor().run_until_parked(); +// model.send_last_completion_stream_text_chunk( +// CONTENT_WITH_TRAILING_WHITESPACE.to_string(), +// ); +// model.end_last_completion_stream(); + +// edit_task.await +// }; +// assert!(edit_result.is_ok()); + +// // Wait for any async operations (e.g. formatting) to complete +// cx.executor().run_until_parked(); + +// // Verify the file still has trailing whitespace +// // Read the file again - it should still have trailing whitespace +// let final_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); +// assert_eq!( +// // Ignore carriage returns on Windows +// final_content.replace("\r\n", "\n"), +// CONTENT_WITH_TRAILING_WHITESPACE, +// "Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled" +// ); +// } + +// #[gpui::test] +// async fn test_needs_confirmation(cx: &mut TestAppContext) { +// init_test(cx); +// let tool = Arc::new(EditFileTool); +// let fs = project::FakeFs::new(cx.executor()); +// fs.insert_tree("/root", json!({})).await; + +// // Test 1: Path with .zed component should require confirmation +// let input_with_zed = json!({ +// "display_description": "Edit settings", +// "path": ".zed/settings.json", +// "mode": "edit" +// }); +// let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; +// cx.update(|cx| { +// assert!( +// tool.needs_confirmation(&input_with_zed, &project, cx), +// "Path with .zed component should require confirmation" +// ); +// }); + +// // Test 2: Absolute path should require confirmation +// let input_absolute = json!({ +// "display_description": "Edit file", +// "path": "/etc/hosts", +// "mode": "edit" +// }); +// cx.update(|cx| { +// assert!( +// tool.needs_confirmation(&input_absolute, &project, cx), +// "Absolute path should require confirmation" +// ); +// }); + +// // Test 3: Relative path without .zed should not require confirmation +// let input_relative = json!({ +// "display_description": "Edit file", +// "path": "root/src/main.rs", +// "mode": "edit" +// }); +// cx.update(|cx| { +// assert!( +// !tool.needs_confirmation(&input_relative, &project, cx), +// "Relative path without .zed should not require confirmation" +// ); +// }); + +// // Test 4: Path with .zed in the middle should require confirmation +// let input_zed_middle = json!({ +// "display_description": "Edit settings", +// "path": "root/.zed/tasks.json", +// "mode": "edit" +// }); +// cx.update(|cx| { +// assert!( +// tool.needs_confirmation(&input_zed_middle, &project, cx), +// "Path with .zed in any component should require confirmation" +// ); +// }); + +// // Test 5: When always_allow_tool_actions is enabled, no confirmation needed +// cx.update(|cx| { +// let mut settings = agent_settings::AgentSettings::get_global(cx).clone(); +// settings.always_allow_tool_actions = true; +// agent_settings::AgentSettings::override_global(settings, cx); + +// assert!( +// !tool.needs_confirmation(&input_with_zed, &project, cx), +// "When always_allow_tool_actions is true, no confirmation should be needed" +// ); +// assert!( +// !tool.needs_confirmation(&input_absolute, &project, cx), +// "When always_allow_tool_actions is true, no confirmation should be needed for absolute paths" +// ); +// }); +// } + +// #[gpui::test] +// async fn test_ui_text_shows_correct_context(cx: &mut TestAppContext) { +// // Set up a custom config directory for testing +// let temp_dir = tempfile::tempdir().unwrap(); +// init_test_with_config(cx, temp_dir.path()); + +// let tool = Arc::new(EditFileTool); + +// // Test ui_text shows context for various paths +// let test_cases = vec![ +// ( +// json!({ +// "display_description": "Update config", +// "path": ".zed/settings.json", +// "mode": "edit" +// }), +// "Update config (local settings)", +// ".zed path should show local settings context", +// ), +// ( +// json!({ +// "display_description": "Fix bug", +// "path": "src/.zed/local.json", +// "mode": "edit" +// }), +// "Fix bug (local settings)", +// "Nested .zed path should show local settings context", +// ), +// ( +// json!({ +// "display_description": "Update readme", +// "path": "README.md", +// "mode": "edit" +// }), +// "Update readme", +// "Normal path should not show additional context", +// ), +// ( +// json!({ +// "display_description": "Edit config", +// "path": "config.zed", +// "mode": "edit" +// }), +// "Edit config", +// ".zed as extension should not show context", +// ), +// ]; + +// for (input, expected_text, description) in test_cases { +// cx.update(|_cx| { +// let ui_text = tool.ui_text(&input); +// assert_eq!(ui_text, expected_text, "Failed for case: {}", description); +// }); +// } +// } + +// #[gpui::test] +// async fn test_needs_confirmation_outside_project(cx: &mut TestAppContext) { +// init_test(cx); +// let tool = Arc::new(EditFileTool); +// let fs = project::FakeFs::new(cx.executor()); + +// // Create a project in /project directory +// fs.insert_tree("/project", json!({})).await; +// let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + +// // Test file outside project requires confirmation +// let input_outside = json!({ +// "display_description": "Edit file", +// "path": "/outside/file.txt", +// "mode": "edit" +// }); +// cx.update(|cx| { +// assert!( +// tool.needs_confirmation(&input_outside, &project, cx), +// "File outside project should require confirmation" +// ); +// }); + +// // Test file inside project doesn't require confirmation +// let input_inside = json!({ +// "display_description": "Edit file", +// "path": "project/file.txt", +// "mode": "edit" +// }); +// cx.update(|cx| { +// assert!( +// !tool.needs_confirmation(&input_inside, &project, cx), +// "File inside project should not require confirmation" +// ); +// }); +// } + +// #[gpui::test] +// async fn test_needs_confirmation_config_paths(cx: &mut TestAppContext) { +// // Set up a custom data directory for testing +// let temp_dir = tempfile::tempdir().unwrap(); +// init_test_with_config(cx, temp_dir.path()); + +// let tool = Arc::new(EditFileTool); +// let fs = project::FakeFs::new(cx.executor()); +// fs.insert_tree("/home/user/myproject", json!({})).await; +// let project = Project::test(fs.clone(), [path!("/home/user/myproject").as_ref()], cx).await; + +// // Get the actual local settings folder name +// let local_settings_folder = paths::local_settings_folder_relative_path(); + +// // Test various config path patterns +// let test_cases = vec![ +// ( +// format!("{}/settings.json", local_settings_folder.display()), +// true, +// "Top-level local settings file".to_string(), +// ), +// ( +// format!( +// "myproject/{}/settings.json", +// local_settings_folder.display() +// ), +// true, +// "Local settings in project path".to_string(), +// ), +// ( +// format!("src/{}/config.toml", local_settings_folder.display()), +// true, +// "Local settings in subdirectory".to_string(), +// ), +// ( +// ".zed.backup/file.txt".to_string(), +// true, +// ".zed.backup is outside project".to_string(), +// ), +// ( +// "my.zed/file.txt".to_string(), +// true, +// "my.zed is outside project".to_string(), +// ), +// ( +// "myproject/src/file.zed".to_string(), +// false, +// ".zed as file extension".to_string(), +// ), +// ( +// "myproject/normal/path/file.rs".to_string(), +// false, +// "Normal file without config paths".to_string(), +// ), +// ]; + +// for (path, should_confirm, description) in test_cases { +// let input = json!({ +// "display_description": "Edit file", +// "path": path, +// "mode": "edit" +// }); +// cx.update(|cx| { +// assert_eq!( +// tool.needs_confirmation(&input, &project, cx), +// should_confirm, +// "Failed for case: {} - path: {}", +// description, +// path +// ); +// }); +// } +// } + +// #[gpui::test] +// async fn test_needs_confirmation_global_config(cx: &mut TestAppContext) { +// // Set up a custom data directory for testing +// let temp_dir = tempfile::tempdir().unwrap(); +// init_test_with_config(cx, temp_dir.path()); + +// let tool = Arc::new(EditFileTool); +// let fs = project::FakeFs::new(cx.executor()); + +// // Create test files in the global config directory +// let global_config_dir = paths::config_dir(); +// fs::create_dir_all(&global_config_dir).unwrap(); +// let global_settings_path = global_config_dir.join("settings.json"); +// fs::write(&global_settings_path, "{}").unwrap(); + +// fs.insert_tree("/project", json!({})).await; +// let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + +// // Test global config paths +// let test_cases = vec![ +// ( +// global_settings_path.to_str().unwrap().to_string(), +// true, +// "Global settings file should require confirmation", +// ), +// ( +// global_config_dir +// .join("keymap.json") +// .to_str() +// .unwrap() +// .to_string(), +// true, +// "Global keymap file should require confirmation", +// ), +// ( +// "project/normal_file.rs".to_string(), +// false, +// "Normal project file should not require confirmation", +// ), +// ]; + +// for (path, should_confirm, description) in test_cases { +// let input = json!({ +// "display_description": "Edit file", +// "path": path, +// "mode": "edit" +// }); +// cx.update(|cx| { +// assert_eq!( +// tool.needs_confirmation(&input, &project, cx), +// should_confirm, +// "Failed for case: {}", +// description +// ); +// }); +// } +// } + +// #[gpui::test] +// async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) { +// init_test(cx); +// let tool = Arc::new(EditFileTool); +// let fs = project::FakeFs::new(cx.executor()); + +// // Create multiple worktree directories +// fs.insert_tree( +// "/workspace/frontend", +// json!({ +// "src": { +// "main.js": "console.log('frontend');" +// } +// }), +// ) +// .await; +// fs.insert_tree( +// "/workspace/backend", +// json!({ +// "src": { +// "main.rs": "fn main() {}" +// } +// }), +// ) +// .await; +// fs.insert_tree( +// "/workspace/shared", +// json!({ +// ".zed": { +// "settings.json": "{}" +// } +// }), +// ) +// .await; + +// // Create project with multiple worktrees +// let project = Project::test( +// fs.clone(), +// [ +// path!("/workspace/frontend").as_ref(), +// path!("/workspace/backend").as_ref(), +// path!("/workspace/shared").as_ref(), +// ], +// cx, +// ) +// .await; + +// // Test files in different worktrees +// let test_cases = vec![ +// ("frontend/src/main.js", false, "File in first worktree"), +// ("backend/src/main.rs", false, "File in second worktree"), +// ( +// "shared/.zed/settings.json", +// true, +// ".zed file in third worktree", +// ), +// ("/etc/hosts", true, "Absolute path outside all worktrees"), +// ( +// "../outside/file.txt", +// true, +// "Relative path outside worktrees", +// ), +// ]; + +// for (path, should_confirm, description) in test_cases { +// let input = json!({ +// "display_description": "Edit file", +// "path": path, +// "mode": "edit" +// }); +// cx.update(|cx| { +// assert_eq!( +// tool.needs_confirmation(&input, &project, cx), +// should_confirm, +// "Failed for case: {} - path: {}", +// description, +// path +// ); +// }); +// } +// } + +// #[gpui::test] +// async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) { +// init_test(cx); +// let tool = Arc::new(EditFileTool); +// let fs = project::FakeFs::new(cx.executor()); +// fs.insert_tree( +// "/project", +// json!({ +// ".zed": { +// "settings.json": "{}" +// }, +// "src": { +// ".zed": { +// "local.json": "{}" +// } +// } +// }), +// ) +// .await; +// let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + +// // Test edge cases +// let test_cases = vec![ +// // Empty path - find_project_path returns Some for empty paths +// ("", false, "Empty path is treated as project root"), +// // Root directory +// ("/", true, "Root directory should be outside project"), +// // Parent directory references - find_project_path resolves these +// ( +// "project/../other", +// false, +// "Path with .. is resolved by find_project_path", +// ), +// ( +// "project/./src/file.rs", +// false, +// "Path with . should work normally", +// ), +// // Windows-style paths (if on Windows) +// #[cfg(target_os = "windows")] +// ("C:\\Windows\\System32\\hosts", true, "Windows system path"), +// #[cfg(target_os = "windows")] +// ("project\\src\\main.rs", false, "Windows-style project path"), +// ]; + +// for (path, should_confirm, description) in test_cases { +// let input = json!({ +// "display_description": "Edit file", +// "path": path, +// "mode": "edit" +// }); +// cx.update(|cx| { +// assert_eq!( +// tool.needs_confirmation(&input, &project, cx), +// should_confirm, +// "Failed for case: {} - path: {}", +// description, +// path +// ); +// }); +// } +// } + +// #[gpui::test] +// async fn test_ui_text_with_all_path_types(cx: &mut TestAppContext) { +// init_test(cx); +// let tool = Arc::new(EditFileTool); + +// // Test UI text for various scenarios +// let test_cases = vec![ +// ( +// json!({ +// "display_description": "Update config", +// "path": ".zed/settings.json", +// "mode": "edit" +// }), +// "Update config (local settings)", +// ".zed path should show local settings context", +// ), +// ( +// json!({ +// "display_description": "Fix bug", +// "path": "src/.zed/local.json", +// "mode": "edit" +// }), +// "Fix bug (local settings)", +// "Nested .zed path should show local settings context", +// ), +// ( +// json!({ +// "display_description": "Update readme", +// "path": "README.md", +// "mode": "edit" +// }), +// "Update readme", +// "Normal path should not show additional context", +// ), +// ( +// json!({ +// "display_description": "Edit config", +// "path": "config.zed", +// "mode": "edit" +// }), +// "Edit config", +// ".zed as extension should not show context", +// ), +// ]; + +// for (input, expected_text, description) in test_cases { +// cx.update(|_cx| { +// let ui_text = tool.ui_text(&input); +// assert_eq!(ui_text, expected_text, "Failed for case: {}", description); +// }); +// } +// } + +// #[gpui::test] +// async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) { +// init_test(cx); +// let tool = Arc::new(EditFileTool); +// let fs = project::FakeFs::new(cx.executor()); +// fs.insert_tree( +// "/project", +// json!({ +// "existing.txt": "content", +// ".zed": { +// "settings.json": "{}" +// } +// }), +// ) +// .await; +// let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + +// // Test different EditFileMode values +// let modes = vec![ +// EditFileMode::Edit, +// EditFileMode::Create, +// EditFileMode::Overwrite, +// ]; + +// for mode in modes { +// // Test .zed path with different modes +// let input_zed = json!({ +// "display_description": "Edit settings", +// "path": "project/.zed/settings.json", +// "mode": mode +// }); +// cx.update(|cx| { +// assert!( +// tool.needs_confirmation(&input_zed, &project, cx), +// ".zed path should require confirmation regardless of mode: {:?}", +// mode +// ); +// }); + +// // Test outside path with different modes +// let input_outside = json!({ +// "display_description": "Edit file", +// "path": "/outside/file.txt", +// "mode": mode +// }); +// cx.update(|cx| { +// assert!( +// tool.needs_confirmation(&input_outside, &project, cx), +// "Outside path should require confirmation regardless of mode: {:?}", +// mode +// ); +// }); + +// // Test normal path with different modes +// let input_normal = json!({ +// "display_description": "Edit file", +// "path": "project/normal.txt", +// "mode": mode +// }); +// cx.update(|cx| { +// assert!( +// !tool.needs_confirmation(&input_normal, &project, cx), +// "Normal path should not require confirmation regardless of mode: {:?}", +// mode +// ); +// }); +// } +// } + +// #[gpui::test] +// async fn test_always_allow_tool_actions_bypasses_all_checks(cx: &mut TestAppContext) { +// // Set up with custom directories for deterministic testing +// let temp_dir = tempfile::tempdir().unwrap(); +// init_test_with_config(cx, temp_dir.path()); + +// let tool = Arc::new(EditFileTool); +// let fs = project::FakeFs::new(cx.executor()); +// fs.insert_tree("/project", json!({})).await; +// let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + +// // Enable always_allow_tool_actions +// cx.update(|cx| { +// let mut settings = agent_settings::AgentSettings::get_global(cx).clone(); +// settings.always_allow_tool_actions = true; +// agent_settings::AgentSettings::override_global(settings, cx); +// }); + +// // Test that all paths that normally require confirmation are bypassed +// let global_settings_path = paths::config_dir().join("settings.json"); +// fs::create_dir_all(paths::config_dir()).unwrap(); +// fs::write(&global_settings_path, "{}").unwrap(); + +// let test_cases = vec![ +// ".zed/settings.json", +// "project/.zed/config.toml", +// global_settings_path.to_str().unwrap(), +// "/etc/hosts", +// "/absolute/path/file.txt", +// "../outside/project.txt", +// ]; + +// for path in test_cases { +// let input = json!({ +// "display_description": "Edit file", +// "path": path, +// "mode": "edit" +// }); +// cx.update(|cx| { +// assert!( +// !tool.needs_confirmation(&input, &project, cx), +// "Path {} should not require confirmation when always_allow_tool_actions is true", +// path +// ); +// }); +// } + +// // Disable always_allow_tool_actions and verify confirmation is required again +// cx.update(|cx| { +// let mut settings = agent_settings::AgentSettings::get_global(cx).clone(); +// settings.always_allow_tool_actions = false; +// agent_settings::AgentSettings::override_global(settings, cx); +// }); + +// // Verify .zed path requires confirmation again +// let input = json!({ +// "display_description": "Edit file", +// "path": ".zed/settings.json", +// "mode": "edit" +// }); +// cx.update(|cx| { +// assert!( +// tool.needs_confirmation(&input, &project, cx), +// ".zed path should require confirmation when always_allow_tool_actions is false" +// ); +// }); +// } +// } diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs index 90bb2e9b7c..bf668e6918 100644 --- a/crates/assistant_tools/src/assistant_tools.rs +++ b/crates/assistant_tools/src/assistant_tools.rs @@ -2,7 +2,7 @@ mod copy_path_tool; mod create_directory_tool; mod delete_path_tool; mod diagnostics_tool; -mod edit_agent; +pub mod edit_agent; mod edit_file_tool; mod fetch_tool; mod find_path_tool; @@ -14,7 +14,7 @@ mod open_tool; mod project_notifications_tool; mod read_file_tool; mod schema; -mod templates; +pub mod templates; mod terminal_tool; mod thinking_tool; mod ui;