diff --git a/crates/agent/src/active_thread.rs b/crates/agent/src/active_thread.rs index 8e5cc146bb5686f270c19d9dacb651a83f7d18a2..bb88f20947bf18c2853a70aa72e3ee31b44af6c4 100644 --- a/crates/agent/src/active_thread.rs +++ b/crates/agent/src/active_thread.rs @@ -3252,15 +3252,18 @@ impl ActiveThread { .map(|tool_use| tool_use.status.clone()) { self.thread.update(cx, |thread, cx| { - thread.run_tool( - c.tool_use_id.clone(), - c.ui_text.clone(), - c.input.clone(), - &c.messages, - c.tool.clone(), - Some(window.window_handle()), - cx, - ); + if let Some(configured) = thread.get_or_init_configured_model(cx) { + thread.run_tool( + c.tool_use_id.clone(), + c.ui_text.clone(), + c.input.clone(), + &c.messages, + c.tool.clone(), + configured.model, + Some(window.window_handle()), + cx, + ); + } }); } } diff --git a/crates/agent/src/context_server_tool.rs b/crates/agent/src/context_server_tool.rs index decba03cdd273f5588406db1a17e1459bee1afb1..aedb92ea11dff970995a84e1db065359f836df3f 100644 --- a/crates/agent/src/context_server_tool.rs +++ b/crates/agent/src/context_server_tool.rs @@ -4,7 +4,7 @@ use anyhow::{Result, anyhow, bail}; use assistant_tool::{ActionLog, Tool, ToolResult, ToolSource}; use context_server::{ContextServerId, types}; use gpui::{AnyWindowHandle, App, Entity, Task}; -use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; +use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; use project::{Project, context_server_store::ContextServerStore}; use ui::IconName; @@ -75,6 +75,7 @@ impl Tool for ContextServerTool { _messages: &[LanguageModelRequestMessage], _project: Entity, _action_log: Entity, + _model: Arc, _window: Option, cx: &mut App, ) -> ToolResult { diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 246177efeaede075a0bbed78b51703bf196257a6..f82a7a339a06f4804b8349ce3895d739b018a541 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -1888,7 +1888,7 @@ impl Thread { model: Arc, ) -> Vec { self.auto_capture_telemetry(cx); - let request = self.to_completion_request(model, cx); + let request = self.to_completion_request(model.clone(), cx); let messages = Arc::new(request.messages); let pending_tool_uses = self .tool_use @@ -1918,6 +1918,7 @@ impl Thread { tool_use.input.clone(), &messages, tool, + model.clone(), window, cx, ); @@ -2012,10 +2013,19 @@ impl Thread { input: serde_json::Value, messages: &[LanguageModelRequestMessage], tool: Arc, + model: Arc, window: Option, cx: &mut Context, ) { - let task = self.spawn_tool_use(tool_use_id.clone(), messages, input, tool, window, cx); + let task = self.spawn_tool_use( + tool_use_id.clone(), + messages, + input, + tool, + model, + window, + cx, + ); self.tool_use .run_pending_tool(tool_use_id, ui_text.into(), task); } @@ -2026,6 +2036,7 @@ impl Thread { messages: &[LanguageModelRequestMessage], input: serde_json::Value, tool: Arc, + model: Arc, window: Option, cx: &mut Context, ) -> Task<()> { @@ -2039,6 +2050,7 @@ impl Thread { messages, self.project.clone(), self.action_log.clone(), + model, window, cx, ) diff --git a/crates/assistant_settings/src/assistant_settings.rs b/crates/assistant_settings/src/assistant_settings.rs index 7cce692b26b20aea9fc3170138e2196c2230d830..0fd2cd99867fd003693fea5248d447a0a2589fd0 100644 --- a/crates/assistant_settings/src/assistant_settings.rs +++ b/crates/assistant_settings/src/assistant_settings.rs @@ -106,11 +106,6 @@ impl AssistantSettings { .and_then(|m| m.temperature) } - pub fn stream_edits(&self, _cx: &App) -> bool { - // TODO: Remove the `stream_edits` setting. - true - } - pub fn are_live_diffs_enabled(&self, _cx: &App) -> bool { false } diff --git a/crates/assistant_tool/src/assistant_tool.rs b/crates/assistant_tool/src/assistant_tool.rs index 68a4f9746d9b5cf578161726da48c350d25d1048..54872c82f19512c15d5cde6d8e51787f6ad2a017 100644 --- a/crates/assistant_tool/src/assistant_tool.rs +++ b/crates/assistant_tool/src/assistant_tool.rs @@ -18,6 +18,7 @@ use gpui::IntoElement; use gpui::Window; use gpui::{App, Entity, SharedString, Task, WeakEntity}; use icons::IconName; +use language_model::LanguageModel; use language_model::LanguageModelRequestMessage; use language_model::LanguageModelToolSchemaFormat; use project::Project; @@ -208,6 +209,7 @@ pub trait Tool: 'static + Send + Sync { messages: &[LanguageModelRequestMessage], project: Entity, action_log: Entity, + model: Arc, window: Option, cx: &mut App, ) -> ToolResult; diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs index 72cc459d9601e991391950a1dbb09cfd3e514d8f..f9ef5749e131f6038ad2d6b30d1cf910ebdb4410 100644 --- a/crates/assistant_tools/src/assistant_tools.rs +++ b/crates/assistant_tools/src/assistant_tools.rs @@ -1,6 +1,5 @@ mod copy_path_tool; mod create_directory_tool; -mod create_file_tool; mod delete_path_tool; mod diagnostics_tool; mod edit_agent; @@ -13,9 +12,7 @@ mod move_path_tool; mod now_tool; mod open_tool; mod read_file_tool; -mod replace; mod schema; -mod streaming_edit_file_tool; mod templates; mod terminal_tool; mod thinking_tool; @@ -24,14 +21,12 @@ mod web_search_tool; use std::sync::Arc; -use assistant_settings::AssistantSettings; use assistant_tool::ToolRegistry; use copy_path_tool::CopyPathTool; use gpui::{App, Entity}; use http_client::HttpClientWithUrl; use language_model::LanguageModelRegistry; use move_path_tool::MovePathTool; -use settings::{Settings, SettingsStore}; use web_search_tool::WebSearchTool; pub(crate) use templates::*; @@ -39,21 +34,19 @@ pub(crate) use templates::*; use crate::create_directory_tool::CreateDirectoryTool; use crate::delete_path_tool::DeletePathTool; use crate::diagnostics_tool::DiagnosticsTool; +use crate::edit_file_tool::EditFileTool; use crate::fetch_tool::FetchTool; use crate::find_path_tool::FindPathTool; use crate::grep_tool::GrepTool; use crate::list_directory_tool::ListDirectoryTool; use crate::now_tool::NowTool; use crate::read_file_tool::ReadFileTool; -use crate::streaming_edit_file_tool::StreamingEditFileTool; use crate::thinking_tool::ThinkingTool; -pub use create_file_tool::{CreateFileTool, CreateFileToolInput}; -pub use edit_file_tool::{EditFileTool, EditFileToolInput}; +pub use edit_file_tool::EditFileToolInput; pub use find_path_tool::FindPathToolInput; pub use open_tool::OpenTool; pub use read_file_tool::ReadFileToolInput; -pub use streaming_edit_file_tool::StreamingEditFileToolInput; pub use terminal_tool::TerminalTool; pub fn init(http_client: Arc, cx: &mut App) { @@ -74,10 +67,7 @@ pub fn init(http_client: Arc, cx: &mut App) { registry.register_tool(GrepTool); registry.register_tool(ThinkingTool); registry.register_tool(FetchTool::new(http_client)); - - register_edit_file_tool(cx); - cx.observe_global::(register_edit_file_tool) - .detach(); + registry.register_tool(EditFileTool); register_web_search_tool(&LanguageModelRegistry::global(cx), cx); cx.subscribe( @@ -104,29 +94,16 @@ fn register_web_search_tool(registry: &Entity, cx: &mut A } } -fn register_edit_file_tool(cx: &mut App) { - let registry = ToolRegistry::global(cx); - - registry.unregister_tool(CreateFileTool); - registry.unregister_tool(EditFileTool); - registry.unregister_tool(StreamingEditFileTool); - - if AssistantSettings::get_global(cx).stream_edits(cx) { - registry.register_tool(StreamingEditFileTool); - } else { - registry.register_tool(CreateFileTool); - registry.register_tool(EditFileTool); - } -} - #[cfg(test)] mod tests { use super::*; + use assistant_settings::AssistantSettings; use client::Client; use clock::FakeSystemClock; use http_client::FakeHttpClient; use schemars::JsonSchema; use serde::Serialize; + use settings::Settings; #[test] fn test_json_schema() { diff --git a/crates/assistant_tools/src/copy_path_tool.rs b/crates/assistant_tools/src/copy_path_tool.rs index 8839ef7fc29e88ac6563c6fd24e8d4f3650513f8..12d2fdcacad234a56629c34f3e231958a3047bf0 100644 --- a/crates/assistant_tools/src/copy_path_tool.rs +++ b/crates/assistant_tools/src/copy_path_tool.rs @@ -3,8 +3,8 @@ use anyhow::{Result, anyhow}; use assistant_tool::{ActionLog, Tool, ToolResult}; use gpui::AnyWindowHandle; use gpui::{App, AppContext, Entity, Task}; -use language_model::LanguageModelRequestMessage; use language_model::LanguageModelToolSchemaFormat; +use language_model::{LanguageModel, LanguageModelRequestMessage}; use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -77,6 +77,7 @@ impl Tool for CopyPathTool { _messages: &[LanguageModelRequestMessage], project: Entity, _action_log: Entity, + _model: Arc, _window: Option, cx: &mut App, ) -> ToolResult { diff --git a/crates/assistant_tools/src/create_directory_tool.rs b/crates/assistant_tools/src/create_directory_tool.rs index 7354ef3eb7d7a596f7091a60fd2d9a243093433e..c47752037c12495e1a015e4b67d5d3d4759c1df1 100644 --- a/crates/assistant_tools/src/create_directory_tool.rs +++ b/crates/assistant_tools/src/create_directory_tool.rs @@ -3,8 +3,7 @@ use anyhow::{Result, anyhow}; use assistant_tool::{ActionLog, Tool, ToolResult}; use gpui::AnyWindowHandle; use gpui::{App, Entity, Task}; -use language_model::LanguageModelRequestMessage; -use language_model::LanguageModelToolSchemaFormat; +use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -65,6 +64,7 @@ impl Tool for CreateDirectoryTool { _messages: &[LanguageModelRequestMessage], project: Entity, _action_log: Entity, + _model: Arc, _window: Option, cx: &mut App, ) -> ToolResult { diff --git a/crates/assistant_tools/src/create_file_tool.rs b/crates/assistant_tools/src/create_file_tool.rs deleted file mode 100644 index ae76fcb4597fa857c64a79d77c11635db8de9a15..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/create_file_tool.rs +++ /dev/null @@ -1,195 +0,0 @@ -use crate::schema::json_schema_for; -use anyhow::{Result, anyhow}; -use assistant_tool::{ActionLog, Tool, ToolResult}; -use gpui::AnyWindowHandle; -use gpui::{App, Entity, Task}; -use language_model::LanguageModelRequestMessage; -use language_model::LanguageModelToolSchemaFormat; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; -use ui::IconName; -use util::markdown::MarkdownInlineCode; - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct CreateFileToolInput { - /// The path where the file should be created. - /// - /// - /// If the project has the following structure: - /// - /// - directory1/ - /// - directory2/ - /// - /// You can create a new file by providing a path of "directory1/new_file.txt" - /// - /// - /// Make sure to include this field before the `contents` field in the input object - /// so that we can display it immediately. - pub path: String, - - /// The text contents of the file to create. - /// - /// - /// To create a file with the text "Hello, World!", provide contents of "Hello, World!" - /// - pub contents: String, -} - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -struct PartialInput { - #[serde(default)] - path: String, - #[serde(default)] - contents: String, -} - -pub struct CreateFileTool; - -const DEFAULT_UI_TEXT: &str = "Create file"; - -impl Tool for CreateFileTool { - fn name(&self) -> String { - "create_file".into() - } - - fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { - false - } - - fn description(&self) -> String { - include_str!("./create_file_tool/description.md").into() - } - - fn icon(&self) -> IconName { - IconName::FileCreate - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - json_schema_for::(format) - } - - fn ui_text(&self, input: &serde_json::Value) -> String { - match serde_json::from_value::(input.clone()) { - Ok(input) => { - let path = MarkdownInlineCode(&input.path); - format!("Create file {path}") - } - Err(_) => DEFAULT_UI_TEXT.to_string(), - } - } - - fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String { - match serde_json::from_value::(input.clone()).ok() { - Some(input) if !input.path.is_empty() => input.path, - _ => DEFAULT_UI_TEXT.to_string(), - } - } - - fn run( - self: Arc, - input: serde_json::Value, - _messages: &[LanguageModelRequestMessage], - project: Entity, - action_log: Entity, - _window: Option, - cx: &mut App, - ) -> ToolResult { - let input = match serde_json::from_value::(input) { - Ok(input) => input, - Err(err) => return Task::ready(Err(anyhow!(err))).into(), - }; - let project_path = match project.read(cx).find_project_path(&input.path, cx) { - Some(project_path) => project_path, - None => { - return Task::ready(Err(anyhow!("Path to create was outside the project"))).into(); - } - }; - let contents: Arc = input.contents.as_str().into(); - let destination_path: Arc = input.path.as_str().into(); - - cx.spawn(async move |cx| { - let buffer = project - .update(cx, |project, cx| { - project.open_buffer(project_path.clone(), cx) - })? - .await - .map_err(|err| anyhow!("Unable to open buffer for {destination_path}: {err}"))?; - cx.update(|cx| { - action_log.update(cx, |action_log, cx| { - action_log.buffer_created(buffer.clone(), cx) - }); - buffer.update(cx, |buffer, cx| buffer.set_text(contents, cx)); - action_log.update(cx, |action_log, cx| { - action_log.buffer_edited(buffer.clone(), cx) - }); - })?; - - project - .update(cx, |project, cx| project.save_buffer(buffer, cx))? - .await - .map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?; - - Ok(format!("Created file {destination_path}").into()) - }) - .into() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[test] - fn still_streaming_ui_text_with_path() { - let tool = CreateFileTool; - let input = json!({ - "path": "src/main.rs", - "contents": "fn main() {\n println!(\"Hello, world!\");\n}" - }); - - assert_eq!(tool.still_streaming_ui_text(&input), "src/main.rs"); - } - - #[test] - fn still_streaming_ui_text_without_path() { - let tool = CreateFileTool; - let input = json!({ - "path": "", - "contents": "fn main() {\n println!(\"Hello, world!\");\n}" - }); - - assert_eq!(tool.still_streaming_ui_text(&input), DEFAULT_UI_TEXT); - } - - #[test] - fn still_streaming_ui_text_with_null() { - let tool = CreateFileTool; - let input = serde_json::Value::Null; - - assert_eq!(tool.still_streaming_ui_text(&input), DEFAULT_UI_TEXT); - } - - #[test] - fn ui_text_with_valid_input() { - let tool = CreateFileTool; - let input = json!({ - "path": "src/main.rs", - "contents": "fn main() {\n println!(\"Hello, world!\");\n}" - }); - - assert_eq!(tool.ui_text(&input), "Create file `src/main.rs`"); - } - - #[test] - fn ui_text_with_invalid_input() { - let tool = CreateFileTool; - let input = json!({ - "invalid": "field" - }); - - assert_eq!(tool.ui_text(&input), DEFAULT_UI_TEXT); - } -} diff --git a/crates/assistant_tools/src/create_file_tool/description.md b/crates/assistant_tools/src/create_file_tool/description.md deleted file mode 100644 index fc470829ff91a6b56a9a1fe46550c308d9cb74e0..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/create_file_tool/description.md +++ /dev/null @@ -1,3 +0,0 @@ -Creates a new file at the specified path within the project, containing the given text content. Returns confirmation that the file was created. - -This tool is the most efficient way to create new files within the project, so it should always be chosen whenever it's necessary to create a new file in the project with specific text content, or whenever a file in the project needs such a drastic change that you would prefer to replace the entire thing instead of making individual edits. This tool should not be used when making changes to parts of an existing file but not all of it. In those cases, it's better to use another approach to edit the file. diff --git a/crates/assistant_tools/src/delete_path_tool.rs b/crates/assistant_tools/src/delete_path_tool.rs index 931a989d490f13407ee8f69b2d853133592f2384..3674cdb8b96eeeee431e8a74e26bcf3044d361eb 100644 --- a/crates/assistant_tools/src/delete_path_tool.rs +++ b/crates/assistant_tools/src/delete_path_tool.rs @@ -3,7 +3,7 @@ use anyhow::{Result, anyhow}; use assistant_tool::{ActionLog, Tool, ToolResult}; use futures::{SinkExt, StreamExt, channel::mpsc}; use gpui::{AnyWindowHandle, App, AppContext, Entity, Task}; -use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; +use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; use project::{Project, ProjectPath}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -62,6 +62,7 @@ impl Tool for DeletePathTool { _messages: &[LanguageModelRequestMessage], project: Entity, action_log: Entity, + _model: Arc, _window: Option, cx: &mut App, ) -> ToolResult { diff --git a/crates/assistant_tools/src/diagnostics_tool.rs b/crates/assistant_tools/src/diagnostics_tool.rs index 702d5f427756e8ae03e027844e2b9724442ba1fd..2b684634d32dceeaadc6137a65a73cd3b4e29010 100644 --- a/crates/assistant_tools/src/diagnostics_tool.rs +++ b/crates/assistant_tools/src/diagnostics_tool.rs @@ -3,7 +3,7 @@ use anyhow::{Result, anyhow}; use assistant_tool::{ActionLog, Tool, ToolResult}; use gpui::{AnyWindowHandle, App, Entity, Task}; use language::{DiagnosticSeverity, OffsetRangeExt}; -use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; +use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -82,6 +82,7 @@ impl Tool for DiagnosticsTool { _messages: &[LanguageModelRequestMessage], project: Entity, action_log: Entity, + _model: Arc, _window: Option, cx: &mut App, ) -> ToolResult { diff --git a/crates/assistant_tools/src/edit_agent/evals.rs b/crates/assistant_tools/src/edit_agent/evals.rs index 8b5c8c8d059e422a7a7a74c45c3a3a3cc020ed1f..3c2b85ca6092785b3c3be62ad92cdbde8c87b973 100644 --- a/crates/assistant_tools/src/edit_agent/evals.rs +++ b/crates/assistant_tools/src/edit_agent/evals.rs @@ -1,8 +1,5 @@ use super::*; -use crate::{ - ReadFileToolInput, grep_tool::GrepToolInput, - streaming_edit_file_tool::StreamingEditFileToolInput, -}; +use crate::{ReadFileToolInput, edit_file_tool::EditFileToolInput, grep_tool::GrepToolInput}; use Role::*; use anyhow::anyhow; use client::{Client, UserStore}; @@ -69,7 +66,7 @@ fn eval_extract_handle_command_output() { [tool_use( "tool_2", "edit_file", - StreamingEditFileToolInput { + EditFileToolInput { display_description: edit_description.into(), path: input_file_path.into(), create_or_overwrite: false, @@ -125,7 +122,7 @@ fn eval_delete_run_git_blame() { [tool_use( "tool_2", "edit_file", - StreamingEditFileToolInput { + EditFileToolInput { display_description: edit_description.into(), path: input_file_path.into(), create_or_overwrite: false, @@ -240,7 +237,7 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() { [tool_use( "tool_4", "edit_file", - StreamingEditFileToolInput { + EditFileToolInput { display_description: edit_description.into(), path: input_file_path.into(), create_or_overwrite: false, @@ -316,7 +313,7 @@ fn eval_disable_cursor_blinking() { [tool_use( "tool_4", "edit_file", - StreamingEditFileToolInput { + EditFileToolInput { display_description: edit_description.into(), path: input_file_path.into(), create_or_overwrite: false, @@ -506,7 +503,7 @@ fn eval_from_pixels_constructor() { [tool_use( "tool_5", "edit_file", - StreamingEditFileToolInput { + EditFileToolInput { display_description: edit_description.into(), path: input_file_path.into(), create_or_overwrite: false, @@ -583,7 +580,7 @@ fn eval_zode() { tool_use( "tool_3", "edit_file", - StreamingEditFileToolInput { + EditFileToolInput { display_description: edit_description.into(), path: input_file_path.into(), create_or_overwrite: true, @@ -828,7 +825,7 @@ fn eval_add_overwrite_test() { tool_use( "tool_5", "edit_file", - StreamingEditFileToolInput { + EditFileToolInput { display_description: edit_description.into(), path: input_file_path.into(), create_or_overwrite: false, diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index 033ac34d5e8be103bf45d15ea4ee1e19b2ed02f3..29586f7d17c404a19640b188a7cfaef836ed7377 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -1,24 +1,26 @@ use crate::{ - replace::{replace_exact, replace_with_flexible_indent}, + Templates, + edit_agent::{EditAgent, EditAgentOutputEvent}, schema::json_schema_for, - streaming_edit_file_tool::StreamingEditFileToolOutput, }; -use anyhow::{Context as _, Result, anyhow}; +use anyhow::{Result, anyhow}; use assistant_tool::{ ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultOutput, ToolUseStatus, }; use buffer_diff::{BufferDiff, BufferDiffSnapshot}; use editor::{Editor, EditorElement, EditorMode, EditorStyle, MultiBuffer, PathKey}; +use futures::StreamExt; use gpui::{ - Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Context, Entity, EntityId, - Task, TextStyle, WeakEntity, pulsating_between, + Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, EntityId, Task, + TextStyle, WeakEntity, pulsating_between, }; +use indoc::formatdoc; use language::{ Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Rope, TextBuffer, language_settings::SoftWrap, }; -use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; -use project::{AgentLocation, Project}; +use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; +use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; @@ -28,7 +30,7 @@ use std::{ time::Duration, }; use theme::ThemeSettings; -use ui::{Disclosure, Tooltip, Window, prelude::*}; +use ui::{Disclosure, Tooltip, prelude::*}; use util::ResultExt; use workspace::Workspace; @@ -36,7 +38,13 @@ pub struct EditFileTool; #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct EditFileToolInput { - /// A user-friendly markdown description of what's being replaced. This will be shown in the UI. + /// 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` @@ -45,7 +53,7 @@ pub struct EditFileToolInput { /// so that we can display it immediately. pub display_description: String, - /// The full path of the file to modify in the project. + /// 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. @@ -66,11 +74,19 @@ pub struct EditFileToolInput { /// pub path: PathBuf, - /// The text to replace. - pub old_string: String, + /// If true, this tool will recreate the file from scratch. + /// If false, this tool will produce granular edits to an existing file. + /// + /// When a file already exists or you just created it, always prefer editing + /// it as opposed to recreating it from scratch. + pub create_or_overwrite: bool, +} - /// The text to replace it with. - pub new_string: String, +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct EditFileToolOutput { + pub original_path: PathBuf, + pub new_text: String, + pub old_text: String, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] @@ -79,10 +95,6 @@ struct PartialInput { path: String, #[serde(default)] display_description: String, - #[serde(default)] - old_string: String, - #[serde(default)] - new_string: String, } const DEFAULT_UI_TEXT: &str = "Editing file"; @@ -134,9 +146,10 @@ impl Tool for EditFileTool { fn run( self: Arc, input: serde_json::Value, - _messages: &[LanguageModelRequestMessage], + messages: &[LanguageModelRequestMessage], project: Entity, action_log: Entity, + model: Arc, window: Option, cx: &mut App, ) -> ToolResult { @@ -145,6 +158,14 @@ impl Tool for EditFileTool { Err(err) => return Task::ready(Err(anyhow!(err))).into(), }; + let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else { + return Task::ready(Err(anyhow!( + "Path {} not found in project", + input.path.display() + ))) + .into(); + }; + let card = window.and_then(|window| { window .update(cx, |_, window, cx| { @@ -156,12 +177,9 @@ impl Tool for EditFileTool { }); let card_clone = card.clone(); - let task: Task> = cx.spawn(async move |cx: &mut AsyncApp| { - let project_path = project.read_with(cx, |project, cx| { - project - .find_project_path(&input.path, cx) - .context("Path not found in project") - })??; + let messages = messages.to_vec(); + let task = cx.spawn(async move |cx: &mut AsyncApp| { + let edit_agent = EditAgent::new(model, project.clone(), action_log, Templates::new()); let buffer = project .update(cx, |project, cx| { @@ -169,144 +187,113 @@ impl Tool for EditFileTool { })? .await?; - // Set the agent's location to the top of the file - project - .update(cx, |project, cx| { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position: language::Anchor::MIN, - }), - cx, - ); - }) - .ok(); - - let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; - - if input.old_string.is_empty() { - return Err(anyhow!( - "`old_string` can't be empty, use another tool if you want to create a file." - )); - } - - if input.old_string == input.new_string { - return Err(anyhow!( - "The `old_string` and `new_string` are identical, so no changes would be made." - )); + let exists = buffer.read_with(cx, |buffer, _| { + buffer + .file() + .as_ref() + .map_or(false, |file| file.disk_state().exists()) + })?; + if !input.create_or_overwrite && !exists { + return Err(anyhow!("{} not found", input.path.display())); } - let result = cx - .background_spawn(async move { - // Try to match exactly - let diff = replace_exact(&input.old_string, &input.new_string, &snapshot) - .await - // If that fails, try being flexible about indentation - .or_else(|| { - replace_with_flexible_indent( - &input.old_string, - &input.new_string, - &snapshot, - ) - })?; - - if diff.edits.is_empty() { - return None; - } - - let old_text = snapshot.text(); - - Some((old_text, diff)) + 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 { old_snapshot.text() } }) .await; - let Some((old_text, diff)) = result else { - let err = buffer.read_with(cx, |buffer, _cx| { - let file_exists = buffer - .file() - .map_or(false, |file| file.disk_state().exists()); - - if !file_exists { - anyhow!("{} does not exist", input.path.display()) - } else if buffer.is_empty() { - anyhow!( - "{} is empty, so the provided `old_string` wasn't found.", - input.path.display() - ) - } else { - anyhow!("Failed to match the provided `old_string`") - } - })?; - - return Err(err); + let (output, mut events) = if input.create_or_overwrite { + edit_agent.overwrite( + buffer.clone(), + input.display_description.clone(), + messages, + cx, + ) + } else { + edit_agent.edit( + buffer.clone(), + input.display_description.clone(), + messages, + cx, + ) }; - let snapshot = cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - let base_version = diff.base_version.clone(); - let snapshot = buffer.update(cx, |buffer, cx| { - buffer.finalize_last_transaction(); - buffer.apply_diff(diff, cx); - buffer.finalize_last_transaction(); - buffer.snapshot() - }); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - - // Set the agent's location to the position of the first edit - if let Some(first_edit) = snapshot.edits_since::(&base_version).next() { - let position = snapshot.anchor_before(first_edit.new.start); - project.update(cx, |project, cx| { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position, - }), - cx, - ); - }) + let mut hallucinated_old_text = false; + while let Some(event) = events.next().await { + match event { + EditAgentOutputEvent::Edited => { + if let Some(card) = card_clone.as_ref() { + let new_snapshot = + buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; + let new_text = cx + .background_spawn({ + let new_snapshot = new_snapshot.clone(); + async move { new_snapshot.text() } + }) + .await; + card.update(cx, |card, cx| { + card.set_diff( + project_path.path.clone(), + old_text.clone(), + new_text, + cx, + ); + }) + .log_err(); + } + } + EditAgentOutputEvent::OldTextNotFound(_) => hallucinated_old_text = true, } - - snapshot - })?; + } + output.await?; project - .update(cx, |project, cx| project.save_buffer(buffer, cx))? + .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))? .await?; - let new_text = snapshot.text(); - let diff_str = cx - .background_spawn({ - let old_text = old_text.clone(); - let new_text = new_text.clone(); - async move { language::unified_diff(&old_text, &new_text) } - }) - .await; + let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; + let new_text = cx.background_spawn({ + let new_snapshot = new_snapshot.clone(); + async move { new_snapshot.text() } + }); + let diff = cx.background_spawn(async move { + language::unified_diff(&old_snapshot.text(), &new_snapshot.text()) + }); + let (new_text, diff) = futures::join!(new_text, diff); + + let output = EditFileToolOutput { + original_path: project_path.path.to_path_buf(), + new_text: new_text.clone(), + old_text: old_text.clone(), + }; if let Some(card) = card_clone { card.update(cx, |card, cx| { - card.set_diff( - project_path.path.clone(), - old_text.clone(), - new_text.clone(), - cx, - ); + card.set_diff(project_path.path.clone(), old_text, new_text, cx); }) .log_err(); } - Ok(ToolResultOutput { - content: format!( - "Edited {}:\n\n```diff\n{}\n```", - input.path.display(), - diff_str - ), - output: serde_json::to_value(StreamingEditFileToolOutput { - original_path: input.path, - new_text, - old_text, + let input_path = input.path.display(); + if diff.is_empty() { + if hallucinated_old_text { + Err(anyhow!(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. + "})) + } else { + Ok("No edits were made.".to_string().into()) + } + } else { + Ok(ToolResultOutput { + content: format!("Edited {}:\n\n```diff\n{}\n```", input_path, diff), + output: serde_json::to_value(output).ok(), }) - .ok(), - }) + } }); ToolResult { @@ -322,7 +309,7 @@ impl Tool for EditFileTool { window: &mut Window, cx: &mut App, ) -> Option { - let output = match serde_json::from_value::(output) { + let output = match serde_json::from_value::(output) { Ok(output) => output, Err(_) => return None, }; @@ -852,7 +839,40 @@ async fn build_buffer_diff( #[cfg(test)] mod tests { use super::*; + use fs::FakeFs; + use gpui::TestAppContext; + use language_model::fake_provider::FakeLanguageModel; use serde_json::json; + use settings::SettingsStore; + use util::path; + + #[gpui::test] + async fn test_edit_nonexistent_file(cx: &mut TestAppContext) { + init_test(cx); + + let fs = 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(), + create_or_overwrite: false, + }) + .unwrap(); + Arc::new(EditFileTool) + .run(input, &[], project.clone(), action_log, model, None, cx) + .output + }) + .await; + assert_eq!( + result.unwrap_err().to_string(), + "root/nonexistent_file.txt not found" + ); + } #[test] fn still_streaming_ui_text_with_path() { @@ -920,4 +940,13 @@ mod tests { 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); + Project::init_settings(cx); + }); + } } diff --git a/crates/assistant_tools/src/edit_file_tool/description.md b/crates/assistant_tools/src/edit_file_tool/description.md index 51f2db7808684c2f19bd6ccbf7fad9882a6d3b96..27f8e49dd626a2d1a5266b90413a3a5f8e02e6d8 100644 --- a/crates/assistant_tools/src/edit_file_tool/description.md +++ b/crates/assistant_tools/src/edit_file_tool/description.md @@ -1,4 +1,4 @@ -This is a tool for editing files. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead. For larger edits, use the `create_file` tool to overwrite files. +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: @@ -6,40 +6,3 @@ Before using this tool: 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 - -To make a file edit, provide the following: -1. path: The full path to the file you wish to modify in the project. This path must include the root directory in the project. -2. old_string: The text to replace (must be unique within the file, and must match the file contents exactly, including all whitespace and indentation) -3. new_string: The edited text, which will replace the old_string in the file. - -The tool will replace ONE occurrence of old_string with new_string in the specified file. - -CRITICAL REQUIREMENTS FOR USING THIS TOOL: - -1. UNIQUENESS: The old_string MUST uniquely identify the specific instance you want to change. This means: - - Include AT LEAST 3-5 lines of context BEFORE the change point - - Include AT LEAST 3-5 lines of context AFTER the change point - - Include all whitespace, indentation, and surrounding code exactly as it appears in the file - -2. SINGLE INSTANCE: This tool can only change ONE instance at a time. If you need to change multiple instances: - - Make separate calls to this tool for each instance - - Each call must uniquely identify its specific instance using extensive context - -3. VERIFICATION: Before using this tool: - - Check how many instances of the target text exist in the file - - If multiple instances exist, gather enough context to uniquely identify each one - - Plan separate tool calls for each instance - -WARNING: If you do not follow these requirements: - - The tool will fail if old_string matches multiple locations - - The tool will fail if old_string doesn't match exactly (including whitespace) - - You may change the wrong instance if you don't include enough context - -When making edits: - - Ensure the edit results in idiomatic, correct code - - Do not leave the code in a broken state - - Always use fully-qualified project paths (starting with the name of one of the project's root directories) - -If you want to create a new file, use the `create_file` tool instead of this tool. Don't pass an empty `old_string`. - -Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each. diff --git a/crates/assistant_tools/src/fetch_tool.rs b/crates/assistant_tools/src/fetch_tool.rs index 92a403d8684da070f05cde29ddf31089167fdeb6..50fa6d225f1d04951f4e3e7b39f42fdd1632a493 100644 --- a/crates/assistant_tools/src/fetch_tool.rs +++ b/crates/assistant_tools/src/fetch_tool.rs @@ -9,7 +9,7 @@ use futures::AsyncReadExt as _; use gpui::{AnyWindowHandle, App, AppContext as _, Entity, Task}; use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown}; use http_client::{AsyncBody, HttpClientWithUrl}; -use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; +use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -145,6 +145,7 @@ impl Tool for FetchTool { _messages: &[LanguageModelRequestMessage], _project: Entity, _action_log: Entity, + _model: Arc, _window: Option, cx: &mut App, ) -> ToolResult { diff --git a/crates/assistant_tools/src/find_path_tool.rs b/crates/assistant_tools/src/find_path_tool.rs index 9422ad21789250783232ebf52d3552cf64b61e98..491b3facc32f2271557fa45581c94b589525d224 100644 --- a/crates/assistant_tools/src/find_path_tool.rs +++ b/crates/assistant_tools/src/find_path_tool.rs @@ -7,7 +7,7 @@ use gpui::{ AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window, }; use language; -use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; +use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -76,6 +76,7 @@ impl Tool for FindPathTool { _messages: &[LanguageModelRequestMessage], project: Entity, _action_log: Entity, + _model: Arc, _window: Option, cx: &mut App, ) -> ToolResult { diff --git a/crates/assistant_tools/src/grep_tool.rs b/crates/assistant_tools/src/grep_tool.rs index e821a7fda4d4e491aab52cf4eaae06008b6cce27..860ae582dd4b6730d24ad18dfb4507c164884602 100644 --- a/crates/assistant_tools/src/grep_tool.rs +++ b/crates/assistant_tools/src/grep_tool.rs @@ -4,7 +4,7 @@ use assistant_tool::{ActionLog, Tool, ToolResult}; use futures::StreamExt; use gpui::{AnyWindowHandle, App, Entity, Task}; use language::{OffsetRangeExt, ParseStatus, Point}; -use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; +use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; use project::{ Project, search::{SearchQuery, SearchResult}, @@ -99,6 +99,7 @@ impl Tool for GrepTool { _messages: &[LanguageModelRequestMessage], project: Entity, _action_log: Entity, + _model: Arc, _window: Option, cx: &mut App, ) -> ToolResult { @@ -281,6 +282,7 @@ mod tests { use assistant_tool::Tool; use gpui::{AppContext, TestAppContext}; use language::{Language, LanguageConfig, LanguageMatcher}; + use language_model::fake_provider::FakeLanguageModel; use project::{FakeFs, Project}; use settings::SettingsStore; use unindent::Unindent; @@ -743,7 +745,8 @@ mod tests { ) -> String { let tool = Arc::new(GrepTool); let action_log = cx.new(|_cx| ActionLog::new(project.clone())); - let task = cx.update(|cx| tool.run(input, &[], project, action_log, None, cx)); + let model = Arc::new(FakeLanguageModel::default()); + let task = cx.update(|cx| tool.run(input, &[], project, action_log, model, None, cx)); match task.output.await { Ok(result) => { diff --git a/crates/assistant_tools/src/list_directory_tool.rs b/crates/assistant_tools/src/list_directory_tool.rs index a988a145e40016fa62bda0489a8bd6c079b98c53..046d43f7852805f5117bc87bca22d19aca3409ea 100644 --- a/crates/assistant_tools/src/list_directory_tool.rs +++ b/crates/assistant_tools/src/list_directory_tool.rs @@ -2,7 +2,7 @@ use crate::schema::json_schema_for; use anyhow::{Result, anyhow}; use assistant_tool::{ActionLog, Tool, ToolResult}; use gpui::{AnyWindowHandle, App, Entity, Task}; -use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; +use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -76,6 +76,7 @@ impl Tool for ListDirectoryTool { _messages: &[LanguageModelRequestMessage], project: Entity, _action_log: Entity, + _model: Arc, _window: Option, cx: &mut App, ) -> ToolResult { diff --git a/crates/assistant_tools/src/move_path_tool.rs b/crates/assistant_tools/src/move_path_tool.rs index fba73d0b8dc8200568f088d224214c40be15f5f6..c27e02794679ad20e652c4bee79d690d234b4e5e 100644 --- a/crates/assistant_tools/src/move_path_tool.rs +++ b/crates/assistant_tools/src/move_path_tool.rs @@ -2,7 +2,7 @@ use crate::schema::json_schema_for; use anyhow::{Result, anyhow}; use assistant_tool::{ActionLog, Tool, ToolResult}; use gpui::{AnyWindowHandle, App, AppContext, Entity, Task}; -use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; +use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -89,6 +89,7 @@ impl Tool for MovePathTool { _messages: &[LanguageModelRequestMessage], project: Entity, _action_log: Entity, + _model: Arc, _window: Option, cx: &mut App, ) -> ToolResult { diff --git a/crates/assistant_tools/src/now_tool.rs b/crates/assistant_tools/src/now_tool.rs index bcef45b1048e0606bc599a258ad465387886a562..ee77101dfdd175990bc83736244d10909b4f2c8c 100644 --- a/crates/assistant_tools/src/now_tool.rs +++ b/crates/assistant_tools/src/now_tool.rs @@ -5,7 +5,7 @@ use anyhow::{Result, anyhow}; use assistant_tool::{ActionLog, Tool, ToolResult}; use chrono::{Local, Utc}; use gpui::{AnyWindowHandle, App, Entity, Task}; -use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; +use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -59,6 +59,7 @@ impl Tool for NowTool { _messages: &[LanguageModelRequestMessage], _project: Entity, _action_log: Entity, + _model: Arc, _window: Option, _cx: &mut App, ) -> ToolResult { diff --git a/crates/assistant_tools/src/open_tool.rs b/crates/assistant_tools/src/open_tool.rs index c4e3fa822fd32ab45bd3d40a1ae10f5fedd8f168..b88a2a99401e6f9750101749e02d04431ffe9a1e 100644 --- a/crates/assistant_tools/src/open_tool.rs +++ b/crates/assistant_tools/src/open_tool.rs @@ -2,7 +2,7 @@ use crate::schema::json_schema_for; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::{ActionLog, Tool, ToolResult}; use gpui::{AnyWindowHandle, App, AppContext, Entity, Task}; -use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; +use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -52,6 +52,7 @@ impl Tool for OpenTool { _messages: &[LanguageModelRequestMessage], project: Entity, _action_log: Entity, + _model: Arc, _window: Option, cx: &mut App, ) -> ToolResult { diff --git a/crates/assistant_tools/src/read_file_tool.rs b/crates/assistant_tools/src/read_file_tool.rs index 72718ba1361c8ba9d7eedef24509e2c3da73e538..e14cc62df5adb8dccb9c3d4e0340479db3033663 100644 --- a/crates/assistant_tools/src/read_file_tool.rs +++ b/crates/assistant_tools/src/read_file_tool.rs @@ -7,7 +7,7 @@ use gpui::{AnyWindowHandle, App, Entity, Task}; use indoc::formatdoc; use itertools::Itertools; use language::{Anchor, Point}; -use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; +use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; use project::{AgentLocation, Project}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -86,6 +86,7 @@ impl Tool for ReadFileTool { _messages: &[LanguageModelRequestMessage], project: Entity, action_log: Entity, + _model: Arc, _window: Option, cx: &mut App, ) -> ToolResult { @@ -97,27 +98,22 @@ impl Tool for ReadFileTool { let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else { return Task::ready(Err(anyhow!("Path {} not found in project", &input.path))).into(); }; - let Some(worktree) = project - .read(cx) - .worktree_for_id(project_path.worktree_id, cx) - else { - return Task::ready(Err(anyhow!("Worktree not found for project path"))).into(); - }; - let exists = worktree.update(cx, |worktree, cx| { - worktree.file_exists(&project_path.path, cx) - }); let file_path = input.path.clone(); cx.spawn(async move |cx| { - if !exists.await? { - return Err(anyhow!("{} not found", file_path)); - } - let buffer = cx .update(|cx| { project.update(cx, |project, cx| project.open_buffer(project_path, cx)) })? .await?; + if buffer.read_with(cx, |buffer, _| { + buffer + .file() + .as_ref() + .map_or(true, |file| !file.disk_state().exists()) + })? { + return Err(anyhow!("{} not found", file_path)); + } project.update(cx, |project, cx| { project.set_agent_location( @@ -145,9 +141,13 @@ impl Tool for ReadFileTool { let lines = text.split('\n').skip(start_row as usize); if let Some(end) = input.end_line { let count = end.saturating_sub(start).saturating_add(1); // Ensure at least 1 line - Itertools::intersperse(lines.take(count as usize), "\n").collect::().into() + Itertools::intersperse(lines.take(count as usize), "\n") + .collect::() + .into() } else { - Itertools::intersperse(lines, "\n").collect::().into() + Itertools::intersperse(lines, "\n") + .collect::() + .into() } })?; @@ -184,15 +184,20 @@ impl Tool for ReadFileTool { } else { // File is too big, so return the outline // and a suggestion to read again with line numbers. - let outline = outline::file_outline(project, file_path, action_log, None, cx).await?; + let outline = + outline::file_outline(project, file_path, action_log, None, cx).await?; Ok(formatdoc! {" - This file was too big to read all at once. Here is an outline of its symbols: + This file was too big to read all at once. + + Here is an outline of its symbols: {outline} - Using the line numbers in this outline, you can call this tool again while specifying - the start_line and end_line fields to see the implementations of symbols in the outline." - }.into()) + Using the line numbers in this outline, you can call this tool again + while specifying the start_line and end_line fields to see the + implementations of symbols in the outline." + } + .into()) } } }) @@ -205,6 +210,7 @@ mod test { use super::*; use gpui::{AppContext, TestAppContext}; use language::{Language, LanguageConfig, LanguageMatcher}; + use language_model::fake_provider::FakeLanguageModel; use project::{FakeFs, Project}; use serde_json::json; use settings::SettingsStore; @@ -218,13 +224,14 @@ mod test { 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 = json!({ "path": "root/nonexistent_file.txt" }); Arc::new(ReadFileTool) - .run(input, &[], project.clone(), action_log, None, cx) + .run(input, &[], project.clone(), action_log, model, None, cx) .output }) .await; @@ -248,13 +255,14 @@ mod test { .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 = json!({ "path": "root/small_file.txt" }); Arc::new(ReadFileTool) - .run(input, &[], project.clone(), action_log, None, cx) + .run(input, &[], project.clone(), action_log, model, None, cx) .output }) .await; @@ -277,6 +285,7 @@ mod test { let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(Arc::new(rust_lang())); let action_log = cx.new(|_| ActionLog::new(project.clone())); + let model = Arc::new(FakeLanguageModel::default()); let result = cx .update(|cx| { @@ -284,13 +293,21 @@ mod test { "path": "root/large_file.rs" }); Arc::new(ReadFileTool) - .run(input, &[], project.clone(), action_log.clone(), None, cx) + .run( + input, + &[], + project.clone(), + action_log.clone(), + model.clone(), + None, + cx, + ) .output }) .await; let content = result.unwrap(); assert_eq!( - content.lines().skip(2).take(6).collect::>(), + content.lines().skip(4).take(6).collect::>(), vec![ "struct Test0 [L1-4]", " a [L2]", @@ -308,7 +325,7 @@ mod test { "offset": 1 }); Arc::new(ReadFileTool) - .run(input, &[], project.clone(), action_log, None, cx) + .run(input, &[], project.clone(), action_log, model, None, cx) .output }) .await; @@ -325,7 +342,7 @@ mod test { pretty_assertions::assert_eq!( content .lines() - .skip(2) + .skip(4) .take(expected_content.len()) .collect::>(), expected_content @@ -346,6 +363,7 @@ mod test { .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 = json!({ @@ -354,7 +372,7 @@ mod test { "end_line": 4 }); Arc::new(ReadFileTool) - .run(input, &[], project.clone(), action_log, None, cx) + .run(input, &[], project.clone(), action_log, model, None, cx) .output }) .await; @@ -375,6 +393,7 @@ mod test { .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()); // start_line of 0 should be treated as 1 let result = cx @@ -385,7 +404,15 @@ mod test { "end_line": 2 }); Arc::new(ReadFileTool) - .run(input, &[], project.clone(), action_log.clone(), None, cx) + .run( + input, + &[], + project.clone(), + action_log.clone(), + model.clone(), + None, + cx, + ) .output }) .await; @@ -400,7 +427,15 @@ mod test { "end_line": 0 }); Arc::new(ReadFileTool) - .run(input, &[], project.clone(), action_log.clone(), None, cx) + .run( + input, + &[], + project.clone(), + action_log.clone(), + model.clone(), + None, + cx, + ) .output }) .await; @@ -415,7 +450,7 @@ mod test { "end_line": 2 }); Arc::new(ReadFileTool) - .run(input, &[], project.clone(), action_log, None, cx) + .run(input, &[], project.clone(), action_log, model, None, cx) .output }) .await; diff --git a/crates/assistant_tools/src/replace.rs b/crates/assistant_tools/src/replace.rs deleted file mode 100644 index fa34a6df9db7ba66fd8e1c6c71e3e7f8290f8eec..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/replace.rs +++ /dev/null @@ -1,872 +0,0 @@ -use language::{BufferSnapshot, Diff, Point, ToOffset}; -use project::search::SearchQuery; -use std::iter; -use util::{ResultExt as _, paths::PathMatcher}; - -/// Performs an exact string replacement in a buffer, requiring precise character-for-character matching. -/// Uses the search functionality to locate the first occurrence of the exact string. -/// Returns None if no exact match is found in the buffer. -pub async fn replace_exact(old: &str, new: &str, snapshot: &BufferSnapshot) -> Option { - let query = SearchQuery::text( - old, - false, - true, - true, - PathMatcher::new(iter::empty::<&str>()).ok()?, - PathMatcher::new(iter::empty::<&str>()).ok()?, - false, - None, - ) - .log_err()?; - - let matches = query.search(&snapshot, None).await; - - if matches.is_empty() { - return None; - } - - let edit_range = matches[0].clone(); - let diff = language::text_diff(&old, &new); - - let edits = diff - .into_iter() - .map(|(old_range, text)| { - let start = edit_range.start + old_range.start; - let end = edit_range.start + old_range.end; - (start..end, text) - }) - .collect::>(); - - let diff = language::Diff { - base_version: snapshot.version().clone(), - line_ending: snapshot.line_ending(), - edits, - }; - - Some(diff) -} - -/// Performs a replacement that's indentation-aware - matches text content ignoring leading whitespace differences. -/// When replacing, preserves the indentation level found in the buffer at each matching line. -/// Returns None if no match found or if indentation is offset inconsistently across matched lines. -pub fn replace_with_flexible_indent(old: &str, new: &str, buffer: &BufferSnapshot) -> Option { - let (old_lines, old_min_indent) = lines_with_min_indent(old); - let (new_lines, new_min_indent) = lines_with_min_indent(new); - let min_indent = old_min_indent.min(new_min_indent); - - let old_lines = drop_lines_prefix(&old_lines, min_indent); - let new_lines = drop_lines_prefix(&new_lines, min_indent); - - let max_row = buffer.max_point().row; - - 'windows: for start_row in 0..max_row + 1 { - let end_row = start_row + old_lines.len().saturating_sub(1) as u32; - - if end_row > max_row { - // The buffer ends before fully matching the pattern - return None; - } - - let start_point = Point::new(start_row, 0); - let end_point = Point::new(end_row, buffer.line_len(end_row)); - let range = start_point.to_offset(buffer)..end_point.to_offset(buffer); - - let window_text = buffer.text_for_range(range.clone()); - let mut window_lines = window_text.lines(); - let mut old_lines_iter = old_lines.iter(); - - let mut common_mismatch = None; - - #[derive(Eq, PartialEq)] - enum Mismatch { - OverIndented(String), - UnderIndented(String), - } - - while let (Some(window_line), Some(old_line)) = (window_lines.next(), old_lines_iter.next()) - { - let line_trimmed = window_line.trim_start(); - - if line_trimmed != old_line.trim_start() { - continue 'windows; - } - - if line_trimmed.is_empty() { - continue; - } - - let line_mismatch = if window_line.len() > old_line.len() { - let prefix = window_line[..window_line.len() - old_line.len()].to_string(); - Mismatch::UnderIndented(prefix) - } else { - let prefix = old_line[..old_line.len() - window_line.len()].to_string(); - Mismatch::OverIndented(prefix) - }; - - match &common_mismatch { - Some(common_mismatch) if common_mismatch != &line_mismatch => { - continue 'windows; - } - Some(_) => (), - None => common_mismatch = Some(line_mismatch), - } - } - - if let Some(common_mismatch) = &common_mismatch { - let line_ending = buffer.line_ending(); - let replacement = new_lines - .iter() - .map(|new_line| { - if new_line.trim().is_empty() { - new_line.to_string() - } else { - match common_mismatch { - Mismatch::UnderIndented(prefix) => prefix.to_string() + new_line, - Mismatch::OverIndented(prefix) => new_line - .strip_prefix(prefix) - .unwrap_or(new_line) - .to_string(), - } - } - }) - .collect::>() - .join(line_ending.as_str()); - - let diff = Diff { - base_version: buffer.version().clone(), - line_ending, - edits: vec![(range, replacement.into())], - }; - - return Some(diff); - } - } - - None -} - -fn drop_lines_prefix<'a>(lines: &'a [&str], prefix_len: usize) -> Vec<&'a str> { - lines - .iter() - .map(|line| line.get(prefix_len..).unwrap_or("")) - .collect() -} - -fn lines_with_min_indent(input: &str) -> (Vec<&str>, usize) { - let mut lines = Vec::new(); - let mut min_indent: Option = None; - - for line in input.lines() { - lines.push(line); - if !line.trim().is_empty() { - let indent = line.len() - line.trim_start().len(); - min_indent = Some(min_indent.map_or(indent, |m| m.min(indent))); - } - } - - (lines, min_indent.unwrap_or(0)) -} - -#[cfg(test)] -mod replace_exact_tests { - use super::*; - use gpui::TestAppContext; - use gpui::prelude::*; - - #[gpui::test] - async fn basic(cx: &mut TestAppContext) { - let result = test_replace_exact(cx, "let x = 41;", "let x = 41;", "let x = 42;").await; - assert_eq!(result, Some("let x = 42;".to_string())); - } - - #[gpui::test] - async fn no_match(cx: &mut TestAppContext) { - let result = test_replace_exact(cx, "let x = 41;", "let y = 42;", "let y = 43;").await; - assert_eq!(result, None); - } - - #[gpui::test] - async fn multi_line(cx: &mut TestAppContext) { - let whole = "fn example() {\n let x = 41;\n println!(\"x = {}\", x);\n}"; - let old_text = " let x = 41;\n println!(\"x = {}\", x);"; - let new_text = " let x = 42;\n println!(\"x = {}\", x);"; - let result = test_replace_exact(cx, whole, old_text, new_text).await; - assert_eq!( - result, - Some("fn example() {\n let x = 42;\n println!(\"x = {}\", x);\n}".to_string()) - ); - } - - #[gpui::test] - async fn multiple_occurrences(cx: &mut TestAppContext) { - let whole = "let x = 41;\nlet y = 41;\nlet z = 41;"; - let result = test_replace_exact(cx, whole, "let x = 41;", "let x = 42;").await; - assert_eq!( - result, - Some("let x = 42;\nlet y = 41;\nlet z = 41;".to_string()) - ); - } - - #[gpui::test] - async fn empty_buffer(cx: &mut TestAppContext) { - let result = test_replace_exact(cx, "", "let x = 41;", "let x = 42;").await; - assert_eq!(result, None); - } - - #[gpui::test] - async fn partial_match(cx: &mut TestAppContext) { - let whole = "let x = 41; let y = 42;"; - let result = test_replace_exact(cx, whole, "let x = 41", "let x = 42").await; - assert_eq!(result, Some("let x = 42; let y = 42;".to_string())); - } - - #[gpui::test] - async fn whitespace_sensitive(cx: &mut TestAppContext) { - let result = test_replace_exact(cx, "let x = 41;", " let x = 41;", "let x = 42;").await; - assert_eq!(result, None); - } - - #[gpui::test] - async fn entire_buffer(cx: &mut TestAppContext) { - let result = test_replace_exact(cx, "let x = 41;", "let x = 41;", "let x = 42;").await; - assert_eq!(result, Some("let x = 42;".to_string())); - } - - async fn test_replace_exact( - cx: &mut TestAppContext, - whole: &str, - old: &str, - new: &str, - ) -> Option { - let buffer = cx.new(|cx| language::Buffer::local(whole, cx)); - - let buffer_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot()); - - let diff = replace_exact(old, new, &buffer_snapshot).await; - diff.map(|diff| { - buffer.update(cx, |buffer, cx| { - let _ = buffer.apply_diff(diff, cx); - buffer.text() - }) - }) - } -} - -#[cfg(test)] -mod flexible_indent_tests { - use super::*; - use gpui::TestAppContext; - use gpui::prelude::*; - use unindent::Unindent; - - #[gpui::test] - fn test_underindented_single_line(cx: &mut TestAppContext) { - let cur = " let a = 41;".to_string(); - let old = " let a = 41;".to_string(); - let new = " let a = 42;".to_string(); - let exp = " let a = 42;".to_string(); - - let result = test_replace_with_flexible_indent(cx, &cur, &old, &new); - - assert_eq!(result, Some(exp.to_string())) - } - - #[gpui::test] - fn test_overindented_single_line(cx: &mut TestAppContext) { - let cur = " let a = 41;".to_string(); - let old = " let a = 41;".to_string(); - let new = " let a = 42;".to_string(); - let exp = " let a = 42;".to_string(); - - let result = test_replace_with_flexible_indent(cx, &cur, &old, &new); - - assert_eq!(result, Some(exp.to_string())) - } - - #[gpui::test] - fn test_underindented_multi_line(cx: &mut TestAppContext) { - let whole = r#" - fn test() { - let x = 5; - println!("x = {}", x); - let y = 10; - } - "# - .unindent(); - - let old = r#" - let x = 5; - println!("x = {}", x); - "# - .unindent(); - - let new = r#" - let x = 42; - println!("New value: {}", x); - "# - .unindent(); - - let expected = r#" - fn test() { - let x = 42; - println!("New value: {}", x); - let y = 10; - } - "# - .unindent(); - - assert_eq!( - test_replace_with_flexible_indent(cx, &whole, &old, &new), - Some(expected.to_string()) - ); - } - - #[gpui::test] - fn test_overindented_multi_line(cx: &mut TestAppContext) { - let cur = r#" - fn foo() { - let a = 41; - let b = 3.13; - } - "# - .unindent(); - - // 6 space indent instead of 4 - let old = " let a = 41;\n let b = 3.13;"; - let new = " let a = 42;\n let b = 3.14;"; - - let expected = r#" - fn foo() { - let a = 42; - let b = 3.14; - } - "# - .unindent(); - - let result = test_replace_with_flexible_indent(cx, &cur, &old, &new); - - assert_eq!(result, Some(expected.to_string())) - } - - #[gpui::test] - fn test_replace_inconsistent_indentation(cx: &mut TestAppContext) { - let whole = r#" - fn test() { - if condition { - println!("{}", 43); - } - } - "# - .unindent(); - - let old = r#" - if condition { - println!("{}", 43); - "# - .unindent(); - - let new = r#" - if condition { - println!("{}", 42); - "# - .unindent(); - - assert_eq!( - test_replace_with_flexible_indent(cx, &whole, &old, &new), - None - ); - } - - #[gpui::test] - fn test_replace_with_empty_lines(cx: &mut TestAppContext) { - // Test with empty lines - let whole = r#" - fn test() { - let x = 5; - - println!("x = {}", x); - } - "# - .unindent(); - - let old = r#" - let x = 5; - - println!("x = {}", x); - "# - .unindent(); - - let new = r#" - let x = 10; - - println!("New x: {}", x); - "# - .unindent(); - - let expected = r#" - fn test() { - let x = 10; - - println!("New x: {}", x); - } - "# - .unindent(); - - assert_eq!( - test_replace_with_flexible_indent(cx, &whole, &old, &new), - Some(expected.to_string()) - ); - } - - #[gpui::test] - fn test_replace_no_match(cx: &mut TestAppContext) { - let whole = r#" - fn test() { - let x = 5; - } - "# - .unindent(); - - let old = r#" - let y = 10; - "# - .unindent(); - - let new = r#" - let y = 20; - "# - .unindent(); - - assert_eq!( - test_replace_with_flexible_indent(cx, &whole, &old, &new), - None - ); - } - - #[gpui::test] - fn test_replace_whole_ends_before_matching_old(cx: &mut TestAppContext) { - let whole = r#" - fn test() { - let x = 5; - "# - .unindent(); - - let old = r#" - let x = 5; - println!("x = {}", x); - "# - .unindent(); - - let new = r#" - let x = 10; - println!("x = {}", x); - "# - .unindent(); - - // Should return None because whole doesn't fully contain the old text - assert_eq!( - test_replace_with_flexible_indent(cx, &whole, &old, &new), - None - ); - } - - #[gpui::test] - fn test_replace_whole_is_shorter_than_old(cx: &mut TestAppContext) { - let whole = r#" - let x = 5; - "# - .unindent(); - - let old = r#" - let x = 5; - let y = 10; - "# - .unindent(); - - let new = r#" - let x = 5; - let y = 20; - "# - .unindent(); - - assert_eq!( - test_replace_with_flexible_indent(cx, &whole, &old, &new), - None - ); - } - - #[gpui::test] - fn test_replace_old_is_empty(cx: &mut TestAppContext) { - let whole = r#" - fn test() { - let x = 5; - } - "# - .unindent(); - - let old = ""; - let new = r#" - let y = 10; - "# - .unindent(); - - assert_eq!( - test_replace_with_flexible_indent(cx, &whole, &old, &new), - None - ); - } - - #[gpui::test] - fn test_replace_whole_is_empty(cx: &mut TestAppContext) { - let whole = ""; - let old = r#" - let x = 5; - "# - .unindent(); - - let new = r#" - let x = 10; - "# - .unindent(); - - assert_eq!( - test_replace_with_flexible_indent(cx, &whole, &old, &new), - None - ); - } - - #[test] - fn test_lines_with_min_indent() { - // Empty string - assert_eq!(lines_with_min_indent(""), (vec![], 0)); - - // Single line without indentation - assert_eq!(lines_with_min_indent("hello"), (vec!["hello"], 0)); - - // Multiple lines with no indentation - assert_eq!( - lines_with_min_indent("line1\nline2\nline3"), - (vec!["line1", "line2", "line3"], 0) - ); - - // Multiple lines with consistent indentation - assert_eq!( - lines_with_min_indent(" line1\n line2\n line3"), - (vec![" line1", " line2", " line3"], 2) - ); - - // Multiple lines with varying indentation - assert_eq!( - lines_with_min_indent(" line1\n line2\n line3"), - (vec![" line1", " line2", " line3"], 2) - ); - - // Lines with mixed indentation and empty lines - assert_eq!( - lines_with_min_indent(" line1\n\n line2"), - (vec![" line1", "", " line2"], 2) - ); - } - - #[gpui::test] - fn test_replace_with_missing_indent_uneven_match(cx: &mut TestAppContext) { - let whole = r#" - fn test() { - if true { - let x = 5; - println!("x = {}", x); - } - } - "# - .unindent(); - - let old = r#" - let x = 5; - println!("x = {}", x); - "# - .unindent(); - - let new = r#" - let x = 42; - println!("x = {}", x); - "# - .unindent(); - - let expected = r#" - fn test() { - if true { - let x = 42; - println!("x = {}", x); - } - } - "# - .unindent(); - - assert_eq!( - test_replace_with_flexible_indent(cx, &whole, &old, &new), - Some(expected.to_string()) - ); - } - - #[gpui::test] - fn test_replace_big_example(cx: &mut TestAppContext) { - let whole = r#" - #[cfg(test)] - mod tests { - use super::*; - - #[test] - fn test_is_valid_age() { - assert!(is_valid_age(0)); - assert!(!is_valid_age(151)); - } - } - "# - .unindent(); - - let old = r#" - #[test] - fn test_is_valid_age() { - assert!(is_valid_age(0)); - assert!(!is_valid_age(151)); - } - "# - .unindent(); - - let new = r#" - #[test] - fn test_is_valid_age() { - assert!(is_valid_age(0)); - assert!(!is_valid_age(151)); - } - - #[test] - fn test_group_people_by_age() { - let people = vec![ - Person::new("Young One", 5, "young@example.com").unwrap(), - Person::new("Teen One", 15, "teen@example.com").unwrap(), - Person::new("Teen Two", 18, "teen2@example.com").unwrap(), - Person::new("Adult One", 25, "adult@example.com").unwrap(), - ]; - - let groups = group_people_by_age(&people); - - assert_eq!(groups.get(&0).unwrap().len(), 1); // One person in 0-9 - assert_eq!(groups.get(&10).unwrap().len(), 2); // Two people in 10-19 - assert_eq!(groups.get(&20).unwrap().len(), 1); // One person in 20-29 - } - "# - .unindent(); - let expected = r#" - #[cfg(test)] - mod tests { - use super::*; - - #[test] - fn test_is_valid_age() { - assert!(is_valid_age(0)); - assert!(!is_valid_age(151)); - } - - #[test] - fn test_group_people_by_age() { - let people = vec![ - Person::new("Young One", 5, "young@example.com").unwrap(), - Person::new("Teen One", 15, "teen@example.com").unwrap(), - Person::new("Teen Two", 18, "teen2@example.com").unwrap(), - Person::new("Adult One", 25, "adult@example.com").unwrap(), - ]; - - let groups = group_people_by_age(&people); - - assert_eq!(groups.get(&0).unwrap().len(), 1); // One person in 0-9 - assert_eq!(groups.get(&10).unwrap().len(), 2); // Two people in 10-19 - assert_eq!(groups.get(&20).unwrap().len(), 1); // One person in 20-29 - } - } - "# - .unindent(); - assert_eq!( - test_replace_with_flexible_indent(cx, &whole, &old, &new), - Some(expected.to_string()) - ); - } - - #[test] - fn test_drop_lines_prefix() { - // Empty array - assert_eq!(drop_lines_prefix(&[], 2), Vec::<&str>::new()); - - // Zero prefix length - assert_eq!( - drop_lines_prefix(&["line1", "line2"], 0), - vec!["line1", "line2"] - ); - - // Normal prefix drop - assert_eq!( - drop_lines_prefix(&[" line1", " line2"], 2), - vec!["line1", "line2"] - ); - - // Prefix longer than some lines - assert_eq!(drop_lines_prefix(&[" line1", "a"], 2), vec!["line1", ""]); - - // Prefix longer than all lines - assert_eq!(drop_lines_prefix(&["a", "b"], 5), vec!["", ""]); - - // Mixed length lines - assert_eq!( - drop_lines_prefix(&[" line1", " line2", " line3"], 2), - vec![" line1", "line2", " line3"] - ); - } - - #[gpui::test] - async fn test_replace_exact_basic(cx: &mut TestAppContext) { - let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx)); - let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot()); - - let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await; - assert!(diff.is_some()); - - let diff = diff.unwrap(); - assert_eq!(diff.edits.len(), 1); - - let result = buffer.update(cx, |buffer, cx| { - let _ = buffer.apply_diff(diff, cx); - buffer.text() - }); - - assert_eq!(result, "let x = 42;"); - } - - #[gpui::test] - async fn test_replace_exact_no_match(cx: &mut TestAppContext) { - let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx)); - let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot()); - - let diff = replace_exact("let y = 42;", "let y = 43;", &snapshot).await; - assert!(diff.is_none()); - } - - #[gpui::test] - async fn test_replace_exact_multi_line(cx: &mut TestAppContext) { - let buffer = cx.new(|cx| { - language::Buffer::local( - "fn example() {\n let x = 41;\n println!(\"x = {}\", x);\n}", - cx, - ) - }); - let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot()); - - let old_text = " let x = 41;\n println!(\"x = {}\", x);"; - let new_text = " let x = 42;\n println!(\"x = {}\", x);"; - let diff = replace_exact(old_text, new_text, &snapshot).await; - assert!(diff.is_some()); - - let diff = diff.unwrap(); - let result = buffer.update(cx, |buffer, cx| { - let _ = buffer.apply_diff(diff, cx); - buffer.text() - }); - - assert_eq!( - result, - "fn example() {\n let x = 42;\n println!(\"x = {}\", x);\n}" - ); - } - - #[gpui::test] - async fn test_replace_exact_multiple_occurrences(cx: &mut TestAppContext) { - let buffer = - cx.new(|cx| language::Buffer::local("let x = 41;\nlet y = 41;\nlet z = 41;", cx)); - let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot()); - - // Should replace only the first occurrence - let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await; - assert!(diff.is_some()); - - let diff = diff.unwrap(); - let result = buffer.update(cx, |buffer, cx| { - let _ = buffer.apply_diff(diff, cx); - buffer.text() - }); - - assert_eq!(result, "let x = 42;\nlet y = 41;\nlet z = 41;"); - } - - #[gpui::test] - async fn test_replace_exact_empty_buffer(cx: &mut TestAppContext) { - let buffer = cx.new(|cx| language::Buffer::local("", cx)); - let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot()); - - let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await; - assert!(diff.is_none()); - } - - #[gpui::test] - async fn test_replace_exact_partial_match(cx: &mut TestAppContext) { - let buffer = cx.new(|cx| language::Buffer::local("let x = 41; let y = 42;", cx)); - let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot()); - - // Verify substring replacement actually works - let diff = replace_exact("let x = 41", "let x = 42", &snapshot).await; - assert!(diff.is_some()); - - let diff = diff.unwrap(); - let result = buffer.update(cx, |buffer, cx| { - let _ = buffer.apply_diff(diff, cx); - buffer.text() - }); - - assert_eq!(result, "let x = 42; let y = 42;"); - } - - #[gpui::test] - async fn test_replace_exact_whitespace_sensitive(cx: &mut TestAppContext) { - let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx)); - let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot()); - - let diff = replace_exact(" let x = 41;", "let x = 42;", &snapshot).await; - assert!(diff.is_none()); - } - - #[gpui::test] - async fn test_replace_exact_entire_buffer(cx: &mut TestAppContext) { - let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx)); - let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot()); - - let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await; - assert!(diff.is_some()); - - let diff = diff.unwrap(); - let result = buffer.update(cx, |buffer, cx| { - let _ = buffer.apply_diff(diff, cx); - buffer.text() - }); - - assert_eq!(result, "let x = 42;"); - } - - fn test_replace_with_flexible_indent( - cx: &mut TestAppContext, - whole: &str, - old: &str, - new: &str, - ) -> Option { - // Create a local buffer with the test content - let buffer = cx.new(|cx| language::Buffer::local(whole, cx)); - - // Get the buffer snapshot - let buffer_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot()); - - // Call replace_flexible and transform the result - replace_with_flexible_indent(old, new, &buffer_snapshot).map(|diff| { - buffer.update(cx, |buffer, cx| { - let _ = buffer.apply_diff(diff, cx); - buffer.text() - }) - }) - } -} diff --git a/crates/assistant_tools/src/streaming_edit_file_tool.rs b/crates/assistant_tools/src/streaming_edit_file_tool.rs deleted file mode 100644 index 60d0d354aac1de0204213a126d7f0894569bdd9b..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/streaming_edit_file_tool.rs +++ /dev/null @@ -1,397 +0,0 @@ -use crate::{ - Templates, - edit_agent::{EditAgent, EditAgentOutputEvent}, - edit_file_tool::EditFileToolCard, - schema::json_schema_for, -}; -use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolResult, ToolResultOutput}; -use futures::StreamExt; -use gpui::{AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task}; -use indoc::formatdoc; -use language_model::{ - LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolSchemaFormat, -}; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::{path::PathBuf, sync::Arc}; -use ui::prelude::*; -use util::ResultExt; - -pub struct StreamingEditFileTool; - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct StreamingEditFileToolInput { - /// 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: - /// - backend - /// - frontend - /// - /// - /// `backend/src/main.rs` - /// - /// Notice how the file path starts with root-1. Without that, the path - /// would be ambiguous and the call would fail! - /// - /// - /// - /// `frontend/db.js` - /// - pub path: PathBuf, - - /// If true, this tool will recreate the file from scratch. - /// If false, this tool will produce granular edits to an existing file. - /// - /// When a file already exists or you just created it, always prefer editing - /// it as opposed to recreating it from scratch. - pub create_or_overwrite: bool, -} - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct StreamingEditFileToolOutput { - pub original_path: PathBuf, - pub new_text: String, - pub old_text: String, -} - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -struct PartialInput { - #[serde(default)] - path: String, - #[serde(default)] - display_description: String, -} - -const DEFAULT_UI_TEXT: &str = "Editing file"; - -impl Tool for StreamingEditFileTool { - fn name(&self) -> String { - "edit_file".into() - } - - fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { - false - } - - fn description(&self) -> String { - include_str!("streaming_edit_file_tool/description.md").to_string() - } - - fn icon(&self) -> IconName { - IconName::Pencil - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - json_schema_for::(format) - } - - fn ui_text(&self, input: &serde_json::Value) -> String { - match serde_json::from_value::(input.clone()) { - Ok(input) => input.display_description, - Err(_) => "Editing file".to_string(), - } - } - - fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String { - if let Some(input) = serde_json::from_value::(input.clone()).ok() { - let description = input.display_description.trim(); - if !description.is_empty() { - return description.to_string(); - } - - let path = input.path.trim(); - if !path.is_empty() { - return path.to_string(); - } - } - - DEFAULT_UI_TEXT.to_string() - } - - fn run( - self: Arc, - input: serde_json::Value, - messages: &[LanguageModelRequestMessage], - project: Entity, - action_log: Entity, - window: Option, - cx: &mut App, - ) -> ToolResult { - let input = match serde_json::from_value::(input) { - Ok(input) => input, - Err(err) => return Task::ready(Err(anyhow!(err))).into(), - }; - - let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else { - return Task::ready(Err(anyhow!( - "Path {} not found in project", - input.path.display() - ))) - .into(); - }; - let Some(worktree) = project - .read(cx) - .worktree_for_id(project_path.worktree_id, cx) - else { - return Task::ready(Err(anyhow!("Worktree not found for project path"))).into(); - }; - let exists = worktree.update(cx, |worktree, cx| { - worktree.file_exists(&project_path.path, cx) - }); - - let card = window.and_then(|window| { - window - .update(cx, |_, window, cx| { - cx.new(|cx| { - EditFileToolCard::new(input.path.clone(), project.clone(), window, cx) - }) - }) - .ok() - }); - - let card_clone = card.clone(); - let messages = messages.to_vec(); - let task = cx.spawn(async move |cx: &mut AsyncApp| { - if !input.create_or_overwrite && !exists.await? { - return Err(anyhow!("{} not found", input.path.display())); - } - - let model = cx - .update(|cx| LanguageModelRegistry::read_global(cx).default_model())? - .context("default model not set")? - .model; - let edit_agent = EditAgent::new(model, project.clone(), action_log, Templates::new()); - - let buffer = project - .update(cx, |project, cx| { - project.open_buffer(project_path.clone(), cx) - })? - .await?; - - 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 { old_snapshot.text() } - }) - .await; - - let (output, mut events) = if input.create_or_overwrite { - edit_agent.overwrite( - buffer.clone(), - input.display_description.clone(), - messages, - cx, - ) - } else { - edit_agent.edit( - buffer.clone(), - input.display_description.clone(), - messages, - cx, - ) - }; - - let mut hallucinated_old_text = false; - while let Some(event) = events.next().await { - match event { - EditAgentOutputEvent::Edited => { - if let Some(card) = card_clone.as_ref() { - let new_snapshot = - buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; - let new_text = cx - .background_spawn({ - let new_snapshot = new_snapshot.clone(); - async move { new_snapshot.text() } - }) - .await; - card.update(cx, |card, cx| { - card.set_diff( - project_path.path.clone(), - old_text.clone(), - new_text, - cx, - ); - }) - .log_err(); - } - } - EditAgentOutputEvent::OldTextNotFound(_) => hallucinated_old_text = true, - } - } - output.await?; - - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))? - .await?; - - let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; - let new_text = cx.background_spawn({ - let new_snapshot = new_snapshot.clone(); - async move { new_snapshot.text() } - }); - let diff = cx.background_spawn(async move { - language::unified_diff(&old_snapshot.text(), &new_snapshot.text()) - }); - let (new_text, diff) = futures::join!(new_text, diff); - - let output = StreamingEditFileToolOutput { - original_path: project_path.path.to_path_buf(), - new_text: new_text.clone(), - old_text: old_text.clone(), - }; - - if let Some(card) = card_clone { - card.update(cx, |card, cx| { - card.set_diff(project_path.path.clone(), old_text, new_text, cx); - }) - .log_err(); - } - - let input_path = input.path.display(); - if diff.is_empty() { - if hallucinated_old_text { - Err(anyhow!(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. - "})) - } else { - Ok("No edits were made.".to_string().into()) - } - } else { - Ok(ToolResultOutput { - content: format!("Edited {}:\n\n```diff\n{}\n```", input_path, diff), - output: serde_json::to_value(output).ok(), - }) - } - }); - - ToolResult { - output: task, - card: card.map(AnyToolCard::from), - } - } - - fn deserialize_card( - self: Arc, - output: serde_json::Value, - project: Entity, - window: &mut Window, - cx: &mut App, - ) -> Option { - let output = match serde_json::from_value::(output) { - Ok(output) => output, - Err(_) => return None, - }; - - let card = cx.new(|cx| { - let mut card = EditFileToolCard::new(output.original_path.clone(), project, window, cx); - card.set_diff( - output.original_path.into(), - output.old_text, - output.new_text, - cx, - ); - card - }); - - Some(card.into()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[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!( - StreamingEditFileTool.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!( - StreamingEditFileTool.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!( - StreamingEditFileTool.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!( - StreamingEditFileTool.still_streaming_ui_text(&input), - DEFAULT_UI_TEXT, - ); - } - - #[test] - fn still_streaming_ui_text_with_null() { - let input = serde_json::Value::Null; - - assert_eq!( - StreamingEditFileTool.still_streaming_ui_text(&input), - DEFAULT_UI_TEXT, - ); - } -} diff --git a/crates/assistant_tools/src/streaming_edit_file_tool/description.md b/crates/assistant_tools/src/streaming_edit_file_tool/description.md deleted file mode 100644 index 27f8e49dd626a2d1a5266b90413a3a5f8e02e6d8..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/streaming_edit_file_tool/description.md +++ /dev/null @@ -1,8 +0,0 @@ -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 diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index e5c09d942f79d5cbee76f5379d7236702a7f1921..e14f91ac0f48fdc0fef257b281c061da888aac72 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -4,7 +4,7 @@ use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus}; use futures::{FutureExt as _, future::Shared}; use gpui::{AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, WeakEntity, Window}; use language::LineEnding; -use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; +use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; use portable_pty::{CommandBuilder, PtySize, native_pty_system}; use project::{Project, terminals::TerminalKind}; use schemars::JsonSchema; @@ -110,6 +110,7 @@ impl Tool for TerminalTool { _messages: &[LanguageModelRequestMessage], project: Entity, _action_log: Entity, + _model: Arc, window: Option, cx: &mut App, ) -> ToolResult { @@ -598,6 +599,7 @@ mod tests { use editor::EditorSettings; use fs::RealFs; use gpui::{BackgroundExecutor, TestAppContext}; + use language_model::fake_provider::FakeLanguageModel; use pretty_assertions::assert_eq; use serde_json::json; use settings::{Settings, SettingsStore}; @@ -639,6 +641,7 @@ mod tests { let project: Entity = Project::test(fs, [tree.path().join("project").as_path()], cx).await; let action_log = cx.update(|cx| cx.new(|_| ActionLog::new(project.clone()))); + let model = Arc::new(FakeLanguageModel::default()); let input = TerminalToolInput { command: "cat".to_owned(), @@ -656,6 +659,7 @@ mod tests { &[], project.clone(), action_log.clone(), + model, None, cx, ) @@ -681,6 +685,7 @@ mod tests { let project: Entity = Project::test(fs, [tree.path().join("project").as_path()], cx).await; let action_log = cx.update(|cx| cx.new(|_| ActionLog::new(project.clone()))); + let model = Arc::new(FakeLanguageModel::default()); let check = |input, expected, cx: &mut App| { let headless_result = TerminalTool::run( @@ -689,6 +694,7 @@ mod tests { &[], project.clone(), action_log.clone(), + model.clone(), None, cx, ); diff --git a/crates/assistant_tools/src/thinking_tool.rs b/crates/assistant_tools/src/thinking_tool.rs index ae0cf3194555e33cf4275ca4a9b202c55a0492ae..4369a6285e62aef7c42e10fb6a461cd46e224a28 100644 --- a/crates/assistant_tools/src/thinking_tool.rs +++ b/crates/assistant_tools/src/thinking_tool.rs @@ -4,7 +4,7 @@ use crate::schema::json_schema_for; use anyhow::{Result, anyhow}; use assistant_tool::{ActionLog, Tool, ToolResult}; use gpui::{AnyWindowHandle, App, Entity, Task}; -use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; +use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -50,6 +50,7 @@ impl Tool for ThinkingTool { _messages: &[LanguageModelRequestMessage], _project: Entity, _action_log: Entity, + _model: Arc, _window: Option, _cx: &mut App, ) -> ToolResult { diff --git a/crates/assistant_tools/src/web_search_tool.rs b/crates/assistant_tools/src/web_search_tool.rs index 747c62f508f338bbb63eac285284fd1216346fe3..93412255e953a82794b279284f3ed31e0e9f8a19 100644 --- a/crates/assistant_tools/src/web_search_tool.rs +++ b/crates/assistant_tools/src/web_search_tool.rs @@ -8,7 +8,7 @@ use futures::{Future, FutureExt, TryFutureExt}; use gpui::{ AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window, }; -use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; +use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -56,6 +56,7 @@ impl Tool for WebSearchTool { _messages: &[LanguageModelRequestMessage], _project: Entity, _action_log: Entity, + _model: Arc, _window: Option, cx: &mut App, ) -> ToolResult { diff --git a/crates/eval/src/examples/comment_translation.rs b/crates/eval/src/examples/comment_translation.rs index 9796afaad6b5a2c5dba786b59d5d01be0e116dc4..72a3e865a8d5fbb88f31596d93e27526c0c5b4db 100644 --- a/crates/eval/src/examples/comment_translation.rs +++ b/crates/eval/src/examples/comment_translation.rs @@ -1,7 +1,7 @@ use crate::example::{Example, ExampleContext, ExampleMetadata, JudgeAssertion}; use anyhow::Result; use assistant_settings::AgentProfileId; -use assistant_tools::StreamingEditFileToolInput; +use assistant_tools::EditFileToolInput; use async_trait::async_trait; pub struct CommentTranslation; @@ -34,8 +34,7 @@ impl Example for CommentTranslation { for message in thread.messages() { for tool_use in thread.tool_uses_for_message(message.id, cx) { if tool_use.name == "edit_file" { - let input: StreamingEditFileToolInput = - serde_json::from_value(tool_use.input)?; + let input: EditFileToolInput = serde_json::from_value(tool_use.input)?; if input.create_or_overwrite { create_or_overwrite_count += 1; } diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index bda946498d3586d57bac1e8e04cc84112ada593a..e990dcf33a778720ec2cc319b43d60a8f6565891 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -806,23 +806,6 @@ impl Worktree { } } - pub fn file_exists(&self, path: &Path, cx: &Context) -> Task> { - match self { - Worktree::Local(this) => { - let fs = this.fs.clone(); - let path = this.absolutize(path); - cx.background_spawn(async move { - let path = path?; - let metadata = fs.metadata(&path).await?; - Ok(metadata.map_or(false, |metadata| !metadata.is_dir)) - }) - } - Worktree::Remote(_) => Task::ready(Err(anyhow!( - "remote worktrees can't yet check file existence" - ))), - } - } - pub fn load_file(&self, path: &Path, cx: &Context) -> Task> { match self { Worktree::Local(this) => this.load_file(path, cx),