diff --git a/.github/workflows/community_close_stale_issues.yml b/.github/workflows/community_close_stale_issues.yml
index d8ddb679ad..a38354c317 100644
--- a/.github/workflows/community_close_stale_issues.yml
+++ b/.github/workflows/community_close_stale_issues.yml
@@ -1,7 +1,7 @@
name: "Close Stale Issues"
on:
schedule:
- - cron: "0 7,9,11 * * 2"
+ - cron: "0 7,9,11 * * 3"
workflow_dispatch:
jobs:
diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml
index 8cd415e0c7..140e91eb9a 100644
--- a/.github/workflows/release_nightly.yml
+++ b/.github/workflows/release_nightly.yml
@@ -182,8 +182,7 @@ jobs:
runner: buildjet-16vcpu-ubuntu-2204
install_nix: true
- os: arm Mac
- # TODO: once other macs are provisioned for nix, remove that constraint from the runner
- runner: [macOS, ARM64, nix]
+ runner: [macOS, ARM64, test]
install_nix: false
- os: arm Linux
runner: buildjet-16vcpu-ubuntu-2204-arm
diff --git a/Cargo.lock b/Cargo.lock
index 2cbdc0a2a2..3f75c3ae59 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -491,8 +491,8 @@ dependencies = [
"prompt_store",
"proto",
"rand 0.8.5",
+ "release_channel",
"rope",
- "scripting_tool",
"serde",
"serde_json",
"settings",
@@ -699,6 +699,7 @@ dependencies = [
"collections",
"derive_more",
"gpui",
+ "icons",
"language",
"language_model",
"parking_lot",
@@ -3198,6 +3199,7 @@ dependencies = [
"extension",
"futures 0.3.31",
"gpui",
+ "icons",
"language_model",
"log",
"parking_lot",
@@ -3244,9 +3246,7 @@ name = "copilot"
version = "0.1.0"
dependencies = [
"anyhow",
- "async-compression",
"async-std",
- "async-tar",
"chrono",
"client",
"clock",
@@ -3274,7 +3274,6 @@ dependencies = [
"serde",
"serde_json",
"settings",
- "smol",
"strum",
"task",
"theme",
@@ -4521,12 +4520,6 @@ dependencies = [
"regex",
]
-[[package]]
-name = "env_home"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
-
[[package]]
name = "env_logger"
version = "0.10.2"
@@ -5646,7 +5639,6 @@ dependencies = [
"askpass",
"assistant_settings",
"buffer_diff",
- "chrono",
"collections",
"command_palette_hooks",
"component",
@@ -6499,6 +6491,14 @@ dependencies = [
"cc",
]
+[[package]]
+name = "icons"
+version = "0.1.0"
+dependencies = [
+ "serde",
+ "strum",
+]
+
[[package]]
name = "icu_collections"
version = "1.5.0"
@@ -7312,6 +7312,7 @@ dependencies = [
"google_ai",
"gpui",
"http_client",
+ "icons",
"image",
"log",
"open_ai",
@@ -7324,7 +7325,6 @@ dependencies = [
"strum",
"telemetry_events",
"thiserror 2.0.12",
- "ui",
"util",
]
@@ -7931,25 +7931,6 @@ dependencies = [
"url",
]
-[[package]]
-name = "lua-src"
-version = "547.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1edaf29e3517b49b8b746701e5648ccb5785cde1c119062cbabbc5d5cd115e42"
-dependencies = [
- "cc",
-]
-
-[[package]]
-name = "luajit-src"
-version = "210.5.12+a4f56a4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b3a8e7962a5368d5f264d045a5a255e90f9aa3fc1941ae15a8d2940d42cac671"
-dependencies = [
- "cc",
- "which 7.0.2",
-]
-
[[package]]
name = "lyon"
version = "1.0.1"
@@ -8365,34 +8346,6 @@ dependencies = [
"strum",
]
-[[package]]
-name = "mlua"
-version = "0.10.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3f763c1041eff92ffb5d7169968a327e1ed2ebfe425dac0ee5a35f29082534b"
-dependencies = [
- "bstr",
- "either",
- "futures-util",
- "mlua-sys",
- "num-traits",
- "parking_lot",
- "rustc-hash 2.1.1",
-]
-
-[[package]]
-name = "mlua-sys"
-version = "0.6.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1901c1a635a22fe9250ffcc4fcc937c16b47c2e9e71adba8784af8bca1f69594"
-dependencies = [
- "cc",
- "cfg-if",
- "lua-src",
- "luajit-src",
- "pkg-config",
-]
-
[[package]]
name = "msvc_spectre_libs"
version = "0.1.2"
@@ -9675,7 +9628,7 @@ name = "perplexity"
version = "0.1.0"
dependencies = [
"serde",
- "zed_extension_api 0.3.0",
+ "zed_extension_api 0.4.0",
]
[[package]]
@@ -12201,30 +12154,6 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152"
-[[package]]
-name = "scripting_tool"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "buffer_diff",
- "clock",
- "collections",
- "futures 0.3.31",
- "gpui",
- "language",
- "log",
- "mlua",
- "parking_lot",
- "project",
- "rand 0.8.5",
- "regex",
- "schemars",
- "serde",
- "serde_json",
- "settings",
- "util",
-]
-
[[package]]
name = "scrypt"
version = "0.11.0"
@@ -14276,10 +14205,11 @@ version = "0.1.0"
dependencies = [
"auto_update",
"call",
+ "chrono",
"client",
"collections",
+ "db",
"feature_flags",
- "git_ui",
"gpui",
"http_client",
"notifications",
@@ -14300,7 +14230,6 @@ dependencies = [
"windows 0.61.1",
"workspace",
"zed_actions",
- "zeta",
]
[[package]]
@@ -15008,6 +14937,7 @@ dependencies = [
"chrono",
"component",
"gpui",
+ "icons",
"itertools 0.14.0",
"linkme",
"menu",
@@ -16186,18 +16116,6 @@ dependencies = [
"winsafe",
]
-[[package]]
-name = "which"
-version = "7.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2774c861e1f072b3aadc02f8ba886c26ad6321567ecc294c935434cad06f1283"
-dependencies = [
- "either",
- "env_home",
- "rustix",
- "winsafe",
-]
-
[[package]]
name = "whoami"
version = "1.5.2"
@@ -17464,6 +17382,7 @@ dependencies = [
"theme_extension",
"theme_selector",
"time",
+ "title_bar",
"toolchain_selector",
"tree-sitter-md",
"tree-sitter-rust",
@@ -17513,7 +17432,7 @@ dependencies = [
[[package]]
name = "zed_extension_api"
-version = "0.3.0"
+version = "0.4.0"
dependencies = [
"serde",
"serde_json",
@@ -17571,7 +17490,7 @@ dependencies = [
name = "zed_test_extension"
version = "0.1.0"
dependencies = [
- "zed_extension_api 0.3.0",
+ "zed_extension_api 0.4.0",
]
[[package]]
@@ -17722,7 +17641,6 @@ dependencies = [
"anyhow",
"arrayvec",
"call",
- "chrono",
"client",
"clock",
"collections",
diff --git a/Cargo.toml b/Cargo.toml
index da56a70944..fa8b799a9e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -70,6 +70,7 @@ members = [
"crates/html_to_markdown",
"crates/http_client",
"crates/http_client_tls",
+ "crates/icons",
"crates/image_viewer",
"crates/indexed_docs",
"crates/inline_completion",
@@ -124,7 +125,6 @@ members = [
"crates/rope",
"crates/rpc",
"crates/schema_generator",
- "crates/scripting_tool",
"crates/search",
"crates/semantic_index",
"crates/semantic_version",
@@ -275,6 +275,7 @@ gpui_tokio = { path = "crates/gpui_tokio" }
html_to_markdown = { path = "crates/html_to_markdown" }
http_client = { path = "crates/http_client" }
http_client_tls = { path = "crates/http_client_tls" }
+icons = { path = "crates/icons" }
image_viewer = { path = "crates/image_viewer" }
indexed_docs = { path = "crates/indexed_docs" }
inline_completion = { path = "crates/inline_completion" }
@@ -329,7 +330,6 @@ reqwest_client = { path = "crates/reqwest_client" }
rich_text = { path = "crates/rich_text" }
rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
-scripting_tool = { path = "crates/scripting_tool" }
search = { path = "crates/search" }
semantic_index = { path = "crates/semantic_index" }
semantic_version = { path = "crates/semantic_version" }
diff --git a/assets/icons/arrow_right_left.svg b/assets/icons/arrow_right_left.svg
new file mode 100644
index 0000000000..30331960c9
--- /dev/null
+++ b/assets/icons/arrow_right_left.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/clipboard.svg b/assets/icons/clipboard.svg
new file mode 100644
index 0000000000..5c8842f3b7
--- /dev/null
+++ b/assets/icons/clipboard.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/cog.svg b/assets/icons/cog.svg
new file mode 100644
index 0000000000..03c0a290b7
--- /dev/null
+++ b/assets/icons/cog.svg
@@ -0,0 +1 @@
+
diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json
index 6b848292aa..ee507aa45f 100644
--- a/assets/keymaps/default-linux.json
+++ b/assets/keymaps/default-linux.json
@@ -195,7 +195,7 @@
"ctrl-shift-g": "search::SelectPreviousMatch",
"ctrl-alt-/": "assistant::ToggleModelSelector",
"ctrl-k h": "assistant::DeployHistory",
- "ctrl-k l": "assistant::DeployPromptLibrary",
+ "ctrl-k l": "assistant::OpenPromptLibrary",
"new": "assistant::NewChat",
"ctrl-t": "assistant::NewChat",
"ctrl-n": "assistant::NewChat"
diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json
index 63f258c5bc..3cd69d1444 100644
--- a/assets/keymaps/default-macos.json
+++ b/assets/keymaps/default-macos.json
@@ -241,7 +241,7 @@
"cmd-shift-g": "search::SelectPreviousMatch",
"cmd-alt-/": "assistant::ToggleModelSelector",
"cmd-k h": "assistant::DeployHistory",
- "cmd-k l": "assistant::DeployPromptLibrary",
+ "cmd-k l": "assistant::OpenPromptLibrary",
"cmd-t": "assistant::NewChat",
"cmd-n": "assistant::NewChat"
}
diff --git a/assets/prompts/assistant_system_prompt.hbs b/assets/prompts/assistant_system_prompt.hbs
index ecc0dfd09b..a377a1ae85 100644
--- a/assets/prompts/assistant_system_prompt.hbs
+++ b/assets/prompts/assistant_system_prompt.hbs
@@ -11,7 +11,7 @@ You should only perform actions that modify the user’s system if explicitly re
Be concise and direct in your responses.
-The user has opened a project that contains the following root directories/files:
+The user has opened a project that contains the following root directories/files. Whenever you specify a path in the project, it must be a relative path which begins with one of these root directories/files:
{{#each worktrees}}
- `{{root_name}}` (absolute path: `{{abs_path}}`)
diff --git a/assets/settings/default.json b/assets/settings/default.json
index b63f22247f..eca9fa5a68 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -25,7 +25,7 @@
// Features that can be globally enabled or disabled
"features": {
// Which edit prediction provider to use.
- "edit_prediction_provider": "copilot"
+ "edit_prediction_provider": "zed"
},
// The name of a font to use for rendering text in the editor
"buffer_font_family": "Zed Plex Mono",
@@ -184,6 +184,11 @@
// Whether to show the signature help after completion or a bracket pair inserted.
// If `auto_signature_help` is enabled, this setting will be treated as enabled also.
"show_signature_help_after_edits": false,
+ // What to do when go to definition yields no results.
+ //
+ // 1. Do nothing: `none`
+ // 2. Find references for the same symbol: `find_all_references` (default)
+ "go_to_definition_fallback": "find_all_references",
// Whether to show wrap guides (vertical rulers) in the editor.
// Setting this to true will show a guide at the 'preferred_line_length' value
// if 'soft_wrap' is set to 'preferred_line_length', and will show any
@@ -633,11 +638,15 @@
"name": "Code Writer",
"tools": {
"bash": true,
+ "copy-path": true,
+ "create-file": true,
"delete-path": true,
"diagnostics": true,
- "edit-files": true,
+ "find-replace-file": true,
+ "edit-files": false,
"fetch": true,
"list-directory": true,
+ "move-path": true,
"now": true,
"path-search": true,
"read-file": true,
@@ -645,7 +654,8 @@
"thinking": true
}
}
- }
+ },
+ "notify_when_agent_waiting": true
},
// The settings for slash commands.
"slash_commands": {
@@ -1255,6 +1265,14 @@
"allowed": true
}
},
+ "LaTeX": {
+ "format_on_save": "on",
+ "formatter": "language_server",
+ "language_servers": ["texlab", "..."],
+ "prettier": {
+ "allowed": false
+ }
+ },
"Markdown": {
"format_on_save": "off",
"use_on_type_format": false,
diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs
index 29833d1229..6621899b7c 100644
--- a/crates/assistant/src/assistant_panel.rs
+++ b/crates/assistant/src/assistant_panel.rs
@@ -38,7 +38,7 @@ use workspace::{
dock::{DockPosition, Panel, PanelEvent},
pane, DraggedSelection, Pane, ShowConfiguration, ToggleZoom, Workspace,
};
-use zed_actions::assistant::{DeployPromptLibrary, InlineAssist, ToggleFocus};
+use zed_actions::assistant::{InlineAssist, OpenPromptLibrary, ToggleFocus};
pub fn init(cx: &mut App) {
workspace::FollowableViewRegistry::register::(cx);
@@ -259,7 +259,7 @@ impl AssistantPanel {
menu.context(focus_handle.clone())
.action("New Chat", Box::new(NewChat))
.action("History", Box::new(DeployHistory))
- .action("Prompt Library", Box::new(DeployPromptLibrary))
+ .action("Prompt Library", Box::new(OpenPromptLibrary))
.action("Configure", Box::new(ShowConfiguration))
.action(zoom_label, Box::new(ToggleZoom))
}))
@@ -1028,7 +1028,7 @@ impl AssistantPanel {
fn deploy_prompt_library(
&mut self,
- _: &DeployPromptLibrary,
+ _: &OpenPromptLibrary,
_window: &mut Window,
cx: &mut Context,
) {
diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml
index 07b214bed0..d198bd19ae 100644
--- a/crates/assistant2/Cargo.toml
+++ b/crates/assistant2/Cargo.toml
@@ -61,8 +61,8 @@ project.workspace = true
prompt_library.workspace = true
prompt_store.workspace = true
proto.workspace = true
+release_channel.workspace = true
rope.workspace = true
-scripting_tool.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
diff --git a/crates/assistant2/src/active_thread.rs b/crates/assistant2/src/active_thread.rs
index 2d648f7027..72a678d085 100644
--- a/crates/assistant2/src/active_thread.rs
+++ b/crates/assistant2/src/active_thread.rs
@@ -3,8 +3,10 @@ use crate::thread::{
ThreadEvent, ThreadFeedback,
};
use crate::thread_store::ThreadStore;
-use crate::tool_use::{PendingToolUseStatus, ToolType, ToolUse, ToolUseStatus};
-use crate::ui::ContextPill;
+use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus};
+use crate::ui::{ContextPill, ToolReadyPopUp, ToolReadyPopupEvent};
+
+use assistant_settings::AssistantSettings;
use collections::HashMap;
use editor::{Editor, MultiBuffer};
use gpui::{
@@ -12,11 +14,11 @@ use gpui::{
Animation, AnimationExt, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
Entity, Focusable, Length, ListAlignment, ListOffset, ListState, ScrollHandle, StyleRefinement,
Subscription, Task, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
+ WindowHandle,
};
use language::{Buffer, LanguageRegistry};
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
use markdown::{Markdown, MarkdownStyle};
-use scripting_tool::{ScriptingTool, ScriptingToolInput};
use settings::Settings as _;
use std::sync::Arc;
use std::time::Duration;
@@ -37,12 +39,12 @@ pub struct ActiveThread {
messages: Vec,
list_state: ListState,
rendered_messages_by_id: HashMap,
- rendered_scripting_tool_uses: HashMap>,
rendered_tool_use_labels: HashMap>,
editing_message: Option<(MessageId, EditMessageState)>,
expanded_tool_uses: HashMap,
expanded_thinking_segments: HashMap<(MessageId, usize), bool>,
last_error: Option,
+ pop_ups: Vec>,
_subscriptions: Vec,
}
@@ -233,7 +235,6 @@ impl ActiveThread {
save_thread_task: None,
messages: Vec::new(),
rendered_messages_by_id: HashMap::default(),
- rendered_scripting_tool_uses: HashMap::default(),
rendered_tool_use_labels: HashMap::default(),
expanded_tool_uses: HashMap::default(),
expanded_thinking_segments: HashMap::default(),
@@ -246,6 +247,7 @@ impl ActiveThread {
}),
editing_message: None,
last_error: None,
+ pop_ups: Vec::new(),
_subscriptions: subscriptions,
};
@@ -260,26 +262,6 @@ impl ActiveThread {
cx,
);
}
-
- for tool_use in thread
- .read(cx)
- .scripting_tool_uses_for_message(message.id, cx)
- {
- this.render_tool_use_label_markdown(
- tool_use.id.clone(),
- tool_use.ui_text.clone(),
- window,
- cx,
- );
-
- this.render_scripting_tool_use_markdown(
- tool_use.id.clone(),
- tool_use.ui_text.as_ref(),
- tool_use.input.clone(),
- window,
- cx,
- );
- }
}
this
@@ -360,36 +342,6 @@ impl ActiveThread {
self.rendered_messages_by_id.remove(id);
}
- /// Renders the input of a scripting tool use to Markdown.
- ///
- /// Does nothing if the tool use does not correspond to the scripting tool.
- fn render_scripting_tool_use_markdown(
- &mut self,
- tool_use_id: LanguageModelToolUseId,
- tool_name: &str,
- tool_input: serde_json::Value,
- window: &mut Window,
- cx: &mut Context,
- ) {
- if tool_name != ScriptingTool::NAME {
- return;
- }
-
- let lua_script = serde_json::from_value::(tool_input)
- .map(|input| input.lua_script)
- .unwrap_or_default();
-
- let lua_script = render_markdown(
- format!("```lua\n{lua_script}\n```").into(),
- self.language_registry.clone(),
- window,
- cx,
- );
-
- self.rendered_scripting_tool_uses
- .insert(tool_use_id, lua_script);
- }
-
fn render_tool_use_label_markdown(
&mut self,
tool_use_id: LanguageModelToolUseId,
@@ -422,7 +374,14 @@ impl ActiveThread {
ThreadEvent::StreamedCompletion | ThreadEvent::SummaryChanged => {
self.save_thread(cx);
}
- ThreadEvent::DoneStreaming => {}
+ ThreadEvent::DoneStreaming => {
+ if !self.thread().read(cx).is_generating() {
+ self.show_notification("Your changes have been applied.", window, cx);
+ }
+ }
+ ThreadEvent::ToolConfirmationNeeded => {
+ self.show_notification("There's a tool confirmation needed.", window, cx);
+ }
ThreadEvent::StreamedAssistantText(message_id, text) => {
if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
rendered_message.append_text(text, window, cx);
@@ -476,13 +435,6 @@ impl ActiveThread {
window,
cx,
);
- self.render_scripting_tool_use_markdown(
- tool_use.id,
- tool_use.name.as_ref(),
- tool_use.input.clone(),
- window,
- cx,
- );
}
}
ThreadEvent::ToolFinished {
@@ -556,6 +508,59 @@ impl ActiveThread {
}
}
+ fn show_notification(
+ &mut self,
+ caption: impl Into,
+ window: &mut Window,
+ cx: &mut Context<'_, ActiveThread>,
+ ) {
+ if !window.is_window_active()
+ && self.pop_ups.is_empty()
+ && AssistantSettings::get_global(cx).notify_when_agent_waiting
+ {
+ let caption = caption.into();
+
+ for screen in cx.displays() {
+ let options = ToolReadyPopUp::window_options(screen, cx);
+
+ if let Some(screen_window) = cx
+ .open_window(options, |_, cx| {
+ cx.new(|_| ToolReadyPopUp::new(caption.clone()))
+ })
+ .log_err()
+ {
+ if let Some(pop_up) = screen_window.entity(cx).log_err() {
+ cx.subscribe_in(&pop_up, window, {
+ |this, _, event, window, cx| match event {
+ ToolReadyPopupEvent::Accepted => {
+ let handle = window.window_handle();
+ cx.activate(true); // Switch back to the Zed application
+
+ // If there are multiple Zed windows, activate the correct one.
+ cx.defer(move |cx| {
+ handle
+ .update(cx, |_view, window, _cx| {
+ window.activate_window();
+ })
+ .log_err();
+ });
+
+ this.dismiss_notifications(cx);
+ }
+ ToolReadyPopupEvent::Dismissed => {
+ this.dismiss_notifications(cx);
+ }
+ }
+ })
+ .detach();
+
+ self.pop_ups.push(screen_window);
+ }
+ }
+ }
+ }
+ }
+
/// Spawns a task to save the active thread.
///
/// Only one task to save the thread will be in flight at a time.
@@ -725,13 +730,9 @@ impl ActiveThread {
let checkpoint = thread.checkpoint_for_message(message_id);
let context = thread.context_for_message(message_id);
let tool_uses = thread.tool_uses_for_message(message_id, cx);
- let scripting_tool_uses = thread.scripting_tool_uses_for_message(message_id, cx);
// Don't render user messages that are just there for returning tool results.
- if message.role == Role::User
- && (thread.message_has_tool_results(message_id)
- || thread.message_has_scripting_tool_results(message_id))
- {
+ if message.role == Role::User && thread.message_has_tool_results(message_id) {
return Empty.into_any();
}
@@ -996,32 +997,23 @@ impl ActiveThread {
)
.child(div().p_2().child(message_content)),
),
- Role::Assistant => {
- v_flex()
- .id(("message-container", ix))
- .ml_2()
- .pl_2()
- .pr_4()
- .border_l_1()
- .border_color(cx.theme().colors().border_variant)
- .child(message_content)
- .when(
- !tool_uses.is_empty() || !scripting_tool_uses.is_empty(),
- |parent| {
- parent.child(
- v_flex()
- .children(
- tool_uses
- .into_iter()
- .map(|tool_use| self.render_tool_use(tool_use, cx)),
- )
- .children(scripting_tool_uses.into_iter().map(|tool_use| {
- self.render_scripting_tool_use(tool_use, cx)
- })),
- )
- },
+ Role::Assistant => v_flex()
+ .id(("message-container", ix))
+ .ml_2()
+ .pl_2()
+ .pr_4()
+ .border_l_1()
+ .border_color(cx.theme().colors().border_variant)
+ .child(message_content)
+ .when(!tool_uses.is_empty(), |parent| {
+ parent.child(
+ v_flex().children(
+ tool_uses
+ .into_iter()
+ .map(|tool_use| self.render_tool_use(tool_use, cx)),
+ ),
)
- }
+ }),
Role::System => div().id(("message-container", ix)).py_1().px_2().child(
v_flex()
.bg(colors.editor_background)
@@ -1095,6 +1087,7 @@ impl ActiveThread {
parent.child(
h_flex()
+ .pt_2p5()
.px_2p5()
.w_full()
.gap_1()
@@ -1323,21 +1316,6 @@ impl ActiveThread {
let lighter_border = cx.theme().colors().border.opacity(0.5);
- let tool_icon = match tool_use.name.as_ref() {
- "bash" => IconName::Terminal,
- "delete-path" => IconName::Trash,
- "diagnostics" => IconName::Warning,
- "edit-files" => IconName::Pencil,
- "fetch" => IconName::Globe,
- "list-directory" => IconName::Folder,
- "now" => IconName::Info,
- "path-search" => IconName::SearchCode,
- "read-file" => IconName::Eye,
- "regex-search" => IconName::Regex,
- "thinking" => IconName::Brain,
- _ => IconName::Terminal,
- };
-
div().py_2().child(
v_flex()
.rounded_lg()
@@ -1363,7 +1341,7 @@ impl ActiveThread {
h_flex()
.gap_1p5()
.child(
- Icon::new(tool_icon)
+ Icon::new(tool_use.icon)
.size(IconSize::XSmall)
.color(Color::Muted),
)
@@ -1537,145 +1515,6 @@ impl ActiveThread {
)
}
- fn render_scripting_tool_use(
- &self,
- tool_use: ToolUse,
- cx: &mut Context,
- ) -> impl IntoElement {
- let is_open = self
- .expanded_tool_uses
- .get(&tool_use.id)
- .copied()
- .unwrap_or_default();
-
- div().px_2p5().child(
- v_flex()
- .gap_1()
- .rounded_lg()
- .border_1()
- .border_color(cx.theme().colors().border)
- .child(
- h_flex()
- .justify_between()
- .py_0p5()
- .pl_1()
- .pr_2()
- .bg(cx.theme().colors().editor_foreground.opacity(0.02))
- .map(|element| {
- if is_open {
- element.border_b_1().rounded_t_md()
- } else {
- element.rounded_md()
- }
- })
- .border_color(cx.theme().colors().border)
- .child(
- h_flex()
- .gap_1()
- .child(Disclosure::new("tool-use-disclosure", is_open).on_click(
- cx.listener({
- let tool_use_id = tool_use.id.clone();
- move |this, _event, _window, _cx| {
- let is_open = this
- .expanded_tool_uses
- .entry(tool_use_id.clone())
- .or_insert(false);
-
- *is_open = !*is_open;
- }
- }),
- ))
- .child(
- h_flex()
- .gap_1p5()
- .child(
- Icon::new(IconName::Terminal)
- .size(IconSize::XSmall)
- .color(Color::Muted),
- )
- .child(
- div()
- .text_ui_sm(cx)
- .children(
- self.rendered_tool_use_labels
- .get(&tool_use.id)
- .cloned(),
- )
- .truncate(),
- ),
- ),
- )
- .child(
- Label::new(match tool_use.status {
- ToolUseStatus::Pending => "Pending",
- ToolUseStatus::Running => "Running",
- ToolUseStatus::Finished(_) => "Finished",
- ToolUseStatus::Error(_) => "Error",
- ToolUseStatus::NeedsConfirmation => "Asking Permission",
- })
- .size(LabelSize::XSmall)
- .buffer_font(cx),
- ),
- )
- .map(|parent| {
- if !is_open {
- return parent;
- }
-
- let lua_script_markdown =
- self.rendered_scripting_tool_uses.get(&tool_use.id).cloned();
-
- parent.child(
- v_flex()
- .child(
- v_flex()
- .gap_0p5()
- .py_1()
- .px_2p5()
- .border_b_1()
- .border_color(cx.theme().colors().border)
- .child(Label::new("Input:"))
- .map(|parent| {
- if let Some(markdown) = lua_script_markdown {
- parent.child(markdown)
- } else {
- parent.child(Label::new(
- "Failed to render script input to Markdown",
- ))
- }
- }),
- )
- .map(|parent| match tool_use.status {
- ToolUseStatus::Finished(output) => parent.child(
- v_flex()
- .gap_0p5()
- .py_1()
- .px_2p5()
- .child(Label::new("Result:"))
- .child(Label::new(output)),
- ),
- ToolUseStatus::Error(err) => parent.child(
- v_flex()
- .gap_0p5()
- .py_1()
- .px_2p5()
- .child(Label::new("Error:"))
- .child(Label::new(err)),
- ),
- ToolUseStatus::Pending | ToolUseStatus::Running => parent,
- ToolUseStatus::NeedsConfirmation => parent.child(
- v_flex()
- .gap_0p5()
- .py_1()
- .px_2p5()
- .child(Label::new("Asking Permission")),
- ),
- }),
- )
- }),
- )
- }
-
fn render_rules_item(&self, cx: &Context) -> AnyElement {
let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
else {
@@ -1751,7 +1590,7 @@ impl ActiveThread {
c.ui_text.clone(),
c.input.clone(),
&c.messages,
- c.tool_type.clone(),
+ c.tool.clone(),
cx,
);
});
@@ -1761,13 +1600,12 @@ impl ActiveThread {
fn handle_deny_tool(
&mut self,
tool_use_id: LanguageModelToolUseId,
- tool_type: ToolType,
_: &ClickEvent,
_window: &mut Window,
cx: &mut Context,
) {
self.thread.update(cx, |thread, cx| {
- thread.deny_tool_use(tool_use_id, tool_type, cx);
+ thread.deny_tool_use(tool_use_id, cx);
});
}
@@ -1802,7 +1640,7 @@ impl ActiveThread {
thread
.tools_needing_confirmation()
- .map(|(tool_type, tool)| {
+ .map(|tool| {
div()
.m_3()
.p_2()
@@ -1844,7 +1682,6 @@ impl ActiveThread {
move |this, event, window, cx| {
this.handle_deny_tool(
tool_id.clone(),
- tool_type.clone(),
event,
window,
cx,
@@ -1862,6 +1699,16 @@ impl ActiveThread {
.into_any()
})
}
+
+ fn dismiss_notifications(&mut self, cx: &mut Context<'_, ActiveThread>) {
+ for window in self.pop_ups.drain(..) {
+ window
+ .update(cx, |_, window, _| {
+ window.remove_window();
+ })
+ .ok();
+ }
+ }
}
impl Render for ActiveThread {
diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs
index 5224b097cc..6e97bd9d8e 100644
--- a/crates/assistant2/src/assistant.rs
+++ b/crates/assistant2/src/assistant.rs
@@ -32,6 +32,7 @@ use prompt_store::PromptBuilder;
use settings::Settings as _;
pub use crate::active_thread::ActiveThread;
+use crate::assistant_configuration::AddContextServerModal;
pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate};
pub use crate::inline_assistant::InlineAssistant;
pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent};
@@ -46,6 +47,7 @@ actions!(
RemoveAllContext,
OpenHistory,
OpenConfiguration,
+ AddContextServer,
RemoveSelectedThread,
Chat,
ChatMode,
@@ -86,6 +88,7 @@ pub fn init(
client.telemetry().clone(),
cx,
);
+ cx.observe_new(AddContextServerModal::register).detach();
feature_gate_assistant2_actions(cx);
}
diff --git a/crates/assistant2/src/assistant_configuration.rs b/crates/assistant2/src/assistant_configuration.rs
index c03b7c13d5..b215709be2 100644
--- a/crates/assistant2/src/assistant_configuration.rs
+++ b/crates/assistant2/src/assistant_configuration.rs
@@ -1,3 +1,5 @@
+mod add_context_server_modal;
+
use std::sync::Arc;
use assistant_tool::{ToolSource, ToolWorkingSet};
@@ -5,13 +7,14 @@ use collections::HashMap;
use context_server::manager::ContextServerManager;
use gpui::{Action, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription};
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
-use ui::{
- prelude::*, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Switch, Tooltip,
-};
+use ui::{prelude::*, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Switch};
use util::ResultExt as _;
-use zed_actions::assistant::DeployPromptLibrary;
use zed_actions::ExtensionCategoryFilter;
+pub(crate) use add_context_server_modal::AddContextServerModal;
+
+use crate::AddContextServer;
+
pub struct AssistantConfiguration {
focus_handle: FocusHandle,
configuration_views_by_provider: HashMap,
@@ -170,7 +173,6 @@ impl AssistantConfiguration {
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
- .mt_1()
.gap_2()
.flex_1()
.child(
@@ -309,8 +311,9 @@ impl AssistantConfiguration {
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
- .disabled(true)
- .tooltip(Tooltip::text("Not yet implemented")),
+ .on_click(|_event, window, cx| {
+ window.dispatch_action(AddContextServer.boxed_clone(), cx)
+ }),
),
)
.child(
@@ -352,33 +355,6 @@ impl Render for AssistantConfiguration {
.bg(cx.theme().colors().panel_background)
.size_full()
.overflow_y_scroll()
- .child(
- v_flex()
- .p(DynamicSpacing::Base16.rems(cx))
- .gap_2()
- .child(
- v_flex()
- .gap_0p5()
- .child(Headline::new("Prompt Library").size(HeadlineSize::Small))
- .child(
- Label::new("Create reusable prompts and tag which ones you want sent in every LLM interaction.")
- .color(Color::Muted),
- ),
- )
- .child(
- Button::new("open-prompt-library", "Open Prompt Library")
- .style(ButtonStyle::Filled)
- .layer(ElevationIndex::ModalSurface)
- .full_width()
- .icon(IconName::Book)
- .icon_size(IconSize::Small)
- .icon_position(IconPosition::Start)
- .on_click(|_event, window, cx| {
- window.dispatch_action(DeployPromptLibrary.boxed_clone(), cx)
- }),
- ),
- )
- .child(Divider::horizontal().color(DividerColor::Border))
.child(self.render_context_servers_section(cx))
.child(Divider::horizontal().color(DividerColor::Border))
.child(
diff --git a/crates/assistant2/src/assistant_configuration/add_context_server_modal.rs b/crates/assistant2/src/assistant_configuration/add_context_server_modal.rs
new file mode 100644
index 0000000000..1cfb97f4be
--- /dev/null
+++ b/crates/assistant2/src/assistant_configuration/add_context_server_modal.rs
@@ -0,0 +1,164 @@
+use context_server::{ContextServerSettings, ServerCommand, ServerConfig};
+use editor::Editor;
+use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity};
+use serde_json::json;
+use settings::update_settings_file;
+use ui::{prelude::*, Modal, ModalFooter, ModalHeader, Section, Tooltip};
+use workspace::{ModalView, Workspace};
+
+use crate::AddContextServer;
+
+pub struct AddContextServerModal {
+ workspace: WeakEntity,
+ name_editor: Entity,
+ command_editor: Entity,
+}
+
+impl AddContextServerModal {
+ pub fn register(
+ workspace: &mut Workspace,
+ _window: Option<&mut Window>,
+ _cx: &mut Context,
+ ) {
+ workspace.register_action(|workspace, _: &AddContextServer, window, cx| {
+ let workspace_handle = cx.entity().downgrade();
+ workspace.toggle_modal(window, cx, |window, cx| {
+ Self::new(workspace_handle, window, cx)
+ })
+ });
+ }
+
+ pub fn new(
+ workspace: WeakEntity,
+ window: &mut Window,
+ cx: &mut Context,
+ ) -> Self {
+ let name_editor = cx.new(|cx| Editor::single_line(window, cx));
+ let command_editor = cx.new(|cx| Editor::single_line(window, cx));
+
+ name_editor.update(cx, |editor, cx| {
+ editor.set_placeholder_text("Context server name", cx);
+ });
+
+ command_editor.update(cx, |editor, cx| {
+ editor.set_placeholder_text("Command to run the context server", cx);
+ });
+
+ Self {
+ name_editor,
+ command_editor,
+ workspace,
+ }
+ }
+
+ fn confirm(&mut self, cx: &mut Context) {
+ let name = self.name_editor.read(cx).text(cx).trim().to_string();
+ let command = self.command_editor.read(cx).text(cx).trim().to_string();
+
+ if name.is_empty() || command.is_empty() {
+ return;
+ }
+
+ let mut command_parts = command.split(' ').map(|part| part.trim().to_string());
+ let Some(path) = command_parts.next() else {
+ return;
+ };
+ let args = command_parts.collect::>();
+
+ if let Some(workspace) = self.workspace.upgrade() {
+ workspace.update(cx, |workspace, cx| {
+ let fs = workspace.app_state().fs.clone();
+ update_settings_file::(fs.clone(), cx, |settings, _| {
+ settings.context_servers.insert(
+ name.into(),
+ ServerConfig {
+ command: Some(ServerCommand {
+ path,
+ args,
+ env: None,
+ }),
+ settings: Some(json!({})),
+ },
+ );
+ });
+ });
+ }
+
+ cx.emit(DismissEvent);
+ }
+
+ fn cancel(&mut self, cx: &mut Context) {
+ cx.emit(DismissEvent);
+ }
+}
+
+impl ModalView for AddContextServerModal {}
+
+impl Focusable for AddContextServerModal {
+ fn focus_handle(&self, cx: &App) -> FocusHandle {
+ self.name_editor.focus_handle(cx).clone()
+ }
+}
+
+impl EventEmitter for AddContextServerModal {}
+
+impl Render for AddContextServerModal {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement {
+ let is_name_empty = self.name_editor.read(cx).text(cx).trim().is_empty();
+ let is_command_empty = self.command_editor.read(cx).text(cx).trim().is_empty();
+
+ div()
+ .elevation_3(cx)
+ .w(rems(34.))
+ .key_context("AddContextServerModal")
+ .on_action(cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(cx)))
+ .on_action(cx.listener(|this, _: &menu::Confirm, _window, cx| this.confirm(cx)))
+ .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
+ this.focus_handle(cx).focus(window);
+ }))
+ .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
+ .child(
+ Modal::new("add-context-server", None)
+ .header(ModalHeader::new().headline("Add Context Server"))
+ .section(
+ Section::new()
+ .child(
+ v_flex()
+ .gap_1()
+ .child(Label::new("Name"))
+ .child(self.name_editor.clone()),
+ )
+ .child(
+ v_flex()
+ .gap_1()
+ .child(Label::new("Command"))
+ .child(self.command_editor.clone()),
+ ),
+ )
+ .footer(
+ ModalFooter::new()
+ .start_slot(
+ Button::new("cancel", "Cancel").on_click(
+ cx.listener(|this, _event, _window, cx| this.cancel(cx)),
+ ),
+ )
+ .end_slot(
+ Button::new("add-server", "Add Server")
+ .disabled(is_name_empty || is_command_empty)
+ .map(|button| {
+ if is_name_empty {
+ button.tooltip(Tooltip::text("Name is required"))
+ } else if is_command_empty {
+ button.tooltip(Tooltip::text("Command is required"))
+ } else {
+ button
+ }
+ })
+ .on_click(
+ cx.listener(|this, _event, _window, cx| this.confirm(cx)),
+ ),
+ ),
+ ),
+ )
+ }
+}
diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs
index c38aaff44f..3c08bb60b4 100644
--- a/crates/assistant2/src/assistant_panel.rs
+++ b/crates/assistant2/src/assistant_panel.rs
@@ -14,9 +14,9 @@ use client::zed_urls;
use editor::{Editor, MultiBuffer};
use fs::Fs;
use gpui::{
- prelude::*, Action, AnyElement, App, AsyncWindowContext, Corner, Entity, EventEmitter,
- FocusHandle, Focusable, FontWeight, KeyContext, Pixels, Subscription, Task, UpdateGlobal,
- WeakEntity,
+ action_with_deprecated_aliases, prelude::*, Action, AnyElement, App, AsyncWindowContext,
+ Corner, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, Pixels,
+ Subscription, Task, UpdateGlobal, WeakEntity,
};
use language::LanguageRegistry;
use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
@@ -29,7 +29,7 @@ use ui::{prelude::*, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Ta
use util::ResultExt as _;
use workspace::dock::{DockPosition, Panel, PanelEvent};
use workspace::Workspace;
-use zed_actions::assistant::{DeployPromptLibrary, ToggleFocus};
+use zed_actions::assistant::ToggleFocus;
use crate::active_thread::ActiveThread;
use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
@@ -43,6 +43,12 @@ use crate::{
OpenHistory,
};
+action_with_deprecated_aliases!(
+ assistant,
+ OpenPromptLibrary,
+ ["assistant::DeployPromptLibrary"]
+);
+
pub fn init(cx: &mut App) {
cx.observe_new(
|workspace: &mut Workspace, _window, _cx: &mut Context| {
@@ -65,6 +71,14 @@ pub fn init(cx: &mut App) {
panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
}
})
+ .register_action(|workspace, _: &OpenPromptLibrary, window, cx| {
+ if let Some(panel) = workspace.panel::(cx) {
+ workspace.focus_panel::(window, cx);
+ panel.update(cx, |panel, cx| {
+ panel.deploy_prompt_library(&OpenPromptLibrary, window, cx)
+ });
+ }
+ })
.register_action(|workspace, _: &OpenConfiguration, window, cx| {
if let Some(panel) = workspace.panel::(cx) {
workspace.focus_panel::(window, cx);
@@ -303,7 +317,7 @@ impl AssistantPanel {
fn deploy_prompt_library(
&mut self,
- _: &DeployPromptLibrary,
+ _: &OpenPromptLibrary,
_window: &mut Window,
cx: &mut Context,
) {
diff --git a/crates/assistant2/src/context_picker.rs b/crates/assistant2/src/context_picker.rs
index 013fe717b2..d9dba7d0c8 100644
--- a/crates/assistant2/src/context_picker.rs
+++ b/crates/assistant2/src/context_picker.rs
@@ -1,19 +1,28 @@
+mod completion_provider;
mod fetch_context_picker;
mod file_context_picker;
mod thread_context_picker;
+use std::ops::Range;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{anyhow, Result};
-use editor::Editor;
+use editor::display_map::{Crease, FoldId};
+use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};
use file_context_picker::render_file_context_entry;
-use gpui::{App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity};
+use gpui::{
+ App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity,
+};
+use multi_buffer::MultiBufferRow;
use project::ProjectPath;
use thread_context_picker::{render_thread_context_entry, ThreadContextEntry};
-use ui::{prelude::*, ContextMenu, ContextMenuEntry, ContextMenuItem};
+use ui::{
+ prelude::*, ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor,
+};
use workspace::{notifications::NotifyResultExt, Workspace};
+pub use crate::context_picker::completion_provider::ContextPickerCompletionProvider;
use crate::context_picker::fetch_context_picker::FetchContextPicker;
use crate::context_picker::file_context_picker::FileContextPicker;
use crate::context_picker::thread_context_picker::ThreadContextPicker;
@@ -34,10 +43,31 @@ enum ContextPickerMode {
Thread,
}
+impl TryFrom<&str> for ContextPickerMode {
+ type Error = String;
+
+ fn try_from(value: &str) -> Result {
+ match value {
+ "file" => Ok(Self::File),
+ "fetch" => Ok(Self::Fetch),
+ "thread" => Ok(Self::Thread),
+ _ => Err(format!("Invalid context picker mode: {}", value)),
+ }
+ }
+}
+
impl ContextPickerMode {
+ pub fn mention_prefix(&self) -> &'static str {
+ match self {
+ Self::File => "file",
+ Self::Fetch => "fetch",
+ Self::Thread => "thread",
+ }
+ }
+
pub fn label(&self) -> &'static str {
match self {
- Self::File => "File/Directory",
+ Self::File => "Files & Directories",
Self::Fetch => "Fetch",
Self::Thread => "Thread",
}
@@ -63,7 +93,6 @@ enum ContextPickerState {
pub(super) struct ContextPicker {
mode: ContextPickerState,
workspace: WeakEntity,
- editor: WeakEntity,
context_store: WeakEntity,
thread_store: Option>,
confirm_behavior: ConfirmBehavior,
@@ -74,7 +103,6 @@ impl ContextPicker {
workspace: WeakEntity,
thread_store: Option>,
context_store: WeakEntity,
- editor: WeakEntity,
confirm_behavior: ConfirmBehavior,
window: &mut Window,
cx: &mut Context,
@@ -88,7 +116,6 @@ impl ContextPicker {
workspace,
context_store,
thread_store,
- editor,
confirm_behavior,
}
}
@@ -109,10 +136,7 @@ impl ContextPicker {
.enumerate()
.map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
- let mut modes = vec![ContextPickerMode::File, ContextPickerMode::Fetch];
- if self.allow_threads() {
- modes.push(ContextPickerMode::Thread);
- }
+ let modes = supported_context_picker_modes(&self.thread_store);
let menu = menu
.when(has_recent, |menu| {
@@ -174,7 +198,6 @@ impl ContextPicker {
FileContextPicker::new(
context_picker.clone(),
self.workspace.clone(),
- self.editor.clone(),
self.context_store.clone(),
self.confirm_behavior,
window,
@@ -278,7 +301,7 @@ impl ContextPicker {
};
let task = context_store.update(cx, |context_store, cx| {
- context_store.add_file_from_path(project_path.clone(), cx)
+ context_store.add_file_from_path(project_path.clone(), true, cx)
});
cx.spawn_in(window, async move |_, cx| task.await.notify_async_err(cx))
@@ -308,7 +331,7 @@ impl ContextPicker {
cx.spawn(async move |this, cx| {
let thread = open_thread_task.await?;
context_store.update(cx, |context_store, cx| {
- context_store.add_thread(thread, cx);
+ context_store.add_thread(thread, true, cx);
})?;
this.update(cx, |_this, cx| cx.notify())
@@ -328,7 +351,7 @@ impl ContextPicker {
let mut current_files = context_store.file_paths(cx);
- if let Some(active_path) = Self::active_singleton_buffer_path(&workspace, cx) {
+ if let Some(active_path) = active_singleton_buffer_path(&workspace, cx) {
current_files.insert(active_path);
}
@@ -384,16 +407,6 @@ impl ContextPicker {
recent
}
-
- fn active_singleton_buffer_path(workspace: &Workspace, cx: &App) -> Option {
- let active_item = workspace.active_item(cx)?;
-
- let editor = active_item.to_any().downcast::().ok()?.read(cx);
- let buffer = editor.buffer().read(cx).as_singleton()?;
-
- let path = buffer.read(cx).file()?.path().to_path_buf();
- Some(path)
- }
}
impl EventEmitter for ContextPicker {}
@@ -429,3 +442,212 @@ enum RecentEntry {
},
Thread(ThreadContextEntry),
}
+
+fn supported_context_picker_modes(
+ thread_store: &Option>,
+) -> Vec {
+ let mut modes = vec![ContextPickerMode::File, ContextPickerMode::Fetch];
+ if thread_store.is_some() {
+ modes.push(ContextPickerMode::Thread);
+ }
+ modes
+}
+
+fn active_singleton_buffer_path(workspace: &Workspace, cx: &App) -> Option {
+ let active_item = workspace.active_item(cx)?;
+
+ let editor = active_item.to_any().downcast::().ok()?.read(cx);
+ let buffer = editor.buffer().read(cx).as_singleton()?;
+
+ let path = buffer.read(cx).file()?.path().to_path_buf();
+ Some(path)
+}
+
+fn recent_context_picker_entries(
+ context_store: Entity,
+ thread_store: Option>,
+ workspace: Entity,
+ cx: &App,
+) -> Vec {
+ let mut recent = Vec::with_capacity(6);
+
+ let mut current_files = context_store.read(cx).file_paths(cx);
+
+ let workspace = workspace.read(cx);
+
+ if let Some(active_path) = active_singleton_buffer_path(workspace, cx) {
+ current_files.insert(active_path);
+ }
+
+ let project = workspace.project().read(cx);
+
+ recent.extend(
+ workspace
+ .recent_navigation_history_iter(cx)
+ .filter(|(path, _)| !current_files.contains(&path.path.to_path_buf()))
+ .take(4)
+ .filter_map(|(project_path, _)| {
+ project
+ .worktree_for_id(project_path.worktree_id, cx)
+ .map(|worktree| RecentEntry::File {
+ project_path,
+ path_prefix: worktree.read(cx).root_name().into(),
+ })
+ }),
+ );
+
+ let mut current_threads = context_store.read(cx).thread_ids();
+
+ if let Some(active_thread) = workspace
+ .panel::(cx)
+ .map(|panel| panel.read(cx).active_thread(cx))
+ {
+ current_threads.insert(active_thread.read(cx).id().clone());
+ }
+
+ if let Some(thread_store) = thread_store.and_then(|thread_store| thread_store.upgrade()) {
+ recent.extend(
+ thread_store
+ .read(cx)
+ .threads()
+ .into_iter()
+ .filter(|thread| !current_threads.contains(&thread.id))
+ .take(2)
+ .map(|thread| {
+ RecentEntry::Thread(ThreadContextEntry {
+ id: thread.id,
+ summary: thread.summary,
+ })
+ }),
+ );
+ }
+
+ recent
+}
+
+pub(crate) fn insert_crease_for_mention(
+ excerpt_id: ExcerptId,
+ crease_start: text::Anchor,
+ content_len: usize,
+ crease_label: SharedString,
+ crease_icon_path: SharedString,
+ editor_entity: Entity,
+ window: &mut Window,
+ cx: &mut App,
+) {
+ editor_entity.update(cx, |editor, cx| {
+ let snapshot = editor.buffer().read(cx).snapshot(cx);
+
+ let Some(start) = snapshot.anchor_in_excerpt(excerpt_id, crease_start) else {
+ return;
+ };
+
+ let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
+
+ let placeholder = FoldPlaceholder {
+ render: render_fold_icon_button(
+ crease_icon_path,
+ crease_label,
+ editor_entity.downgrade(),
+ ),
+ ..Default::default()
+ };
+
+ let render_trailer =
+ move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
+
+ let crease = Crease::inline(
+ start..end,
+ placeholder.clone(),
+ fold_toggle("mention"),
+ render_trailer,
+ );
+
+ editor.insert_creases(vec![crease.clone()], cx);
+ editor.fold_creases(vec![crease], false, window, cx);
+ });
+}
+
+fn render_fold_icon_button(
+ icon_path: SharedString,
+ label: SharedString,
+ editor: WeakEntity,
+) -> Arc, &mut App) -> AnyElement> {
+ Arc::new({
+ move |fold_id, fold_range, cx| {
+ let is_in_text_selection = editor.upgrade().is_some_and(|editor| {
+ editor.update(cx, |editor, cx| {
+ let snapshot = editor
+ .buffer()
+ .update(cx, |multi_buffer, cx| multi_buffer.snapshot(cx));
+
+ let is_in_pending_selection = || {
+ editor
+ .selections
+ .pending
+ .as_ref()
+ .is_some_and(|pending_selection| {
+ pending_selection
+ .selection
+ .range()
+ .includes(&fold_range, &snapshot)
+ })
+ };
+
+ let mut is_in_complete_selection = || {
+ editor
+ .selections
+ .disjoint_in_range::(fold_range.clone(), cx)
+ .into_iter()
+ .any(|selection| {
+ // This is needed to cover a corner case, if we just check for an existing
+ // selection in the fold range, having a cursor at the start of the fold
+ // marks it as selected. Non-empty selections don't cause this.
+ let length = selection.end - selection.start;
+ length > 0
+ })
+ };
+
+ is_in_pending_selection() || is_in_complete_selection()
+ })
+ });
+
+ ButtonLike::new(fold_id)
+ .style(ButtonStyle::Filled)
+ .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+ .toggle_state(is_in_text_selection)
+ .child(
+ h_flex()
+ .gap_1()
+ .child(
+ Icon::from_path(icon_path.clone())
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
+ .child(
+ Label::new(label.clone())
+ .size(LabelSize::Small)
+ .single_line(),
+ ),
+ )
+ .into_any_element()
+ }
+ })
+}
+
+fn fold_toggle(
+ name: &'static str,
+) -> impl Fn(
+ MultiBufferRow,
+ bool,
+ Arc,
+ &mut Window,
+ &mut App,
+) -> AnyElement {
+ move |row, is_folded, fold, _window, _cx| {
+ Disclosure::new((name, row.0 as u64), !is_folded)
+ .toggle_state(is_folded)
+ .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
+ .into_any_element()
+ }
+}
diff --git a/crates/assistant2/src/context_picker/completion_provider.rs b/crates/assistant2/src/context_picker/completion_provider.rs
new file mode 100644
index 0000000000..8570c47449
--- /dev/null
+++ b/crates/assistant2/src/context_picker/completion_provider.rs
@@ -0,0 +1,1024 @@
+use std::cell::RefCell;
+use std::ops::Range;
+use std::path::{Path, PathBuf};
+use std::rc::Rc;
+use std::sync::atomic::AtomicBool;
+use std::sync::Arc;
+
+use anyhow::Result;
+use editor::{CompletionProvider, Editor, ExcerptId};
+use file_icons::FileIcons;
+use gpui::{App, Entity, Task, WeakEntity};
+use http_client::HttpClientWithUrl;
+use language::{Buffer, CodeLabel, HighlightId};
+use lsp::CompletionContext;
+use project::{Completion, CompletionIntent, ProjectPath, WorktreeId};
+use rope::Point;
+use text::{Anchor, ToPoint};
+use ui::prelude::*;
+use workspace::Workspace;
+
+use crate::context::AssistantContext;
+use crate::context_store::ContextStore;
+use crate::thread_store::ThreadStore;
+
+use super::fetch_context_picker::fetch_url_content;
+use super::thread_context_picker::ThreadContextEntry;
+use super::{recent_context_picker_entries, supported_context_picker_modes, ContextPickerMode};
+
+pub struct ContextPickerCompletionProvider {
+ workspace: WeakEntity,
+ context_store: WeakEntity,
+ thread_store: Option>,
+ editor: WeakEntity,
+}
+
+impl ContextPickerCompletionProvider {
+ pub fn new(
+ workspace: WeakEntity,
+ context_store: WeakEntity,
+ thread_store: Option>,
+ editor: WeakEntity,
+ ) -> Self {
+ Self {
+ workspace,
+ context_store,
+ thread_store,
+ editor,
+ }
+ }
+
+ fn default_completions(
+ excerpt_id: ExcerptId,
+ source_range: Range,
+ context_store: Entity,
+ thread_store: Option>,
+ editor: Entity,
+ workspace: Entity,
+ cx: &App,
+ ) -> Vec {
+ let mut completions = Vec::new();
+
+ completions.extend(
+ recent_context_picker_entries(
+ context_store.clone(),
+ thread_store.clone(),
+ workspace.clone(),
+ cx,
+ )
+ .iter()
+ .filter_map(|entry| match entry {
+ super::RecentEntry::File {
+ project_path,
+ path_prefix: _,
+ } => Self::completion_for_path(
+ project_path.clone(),
+ true,
+ false,
+ excerpt_id,
+ source_range.clone(),
+ editor.clone(),
+ context_store.clone(),
+ workspace.clone(),
+ cx,
+ ),
+ super::RecentEntry::Thread(thread_context_entry) => {
+ let thread_store = thread_store
+ .as_ref()
+ .and_then(|thread_store| thread_store.upgrade())?;
+ Some(Self::completion_for_thread(
+ thread_context_entry.clone(),
+ excerpt_id,
+ source_range.clone(),
+ true,
+ editor.clone(),
+ context_store.clone(),
+ thread_store,
+ ))
+ }
+ }),
+ );
+
+ completions.extend(
+ supported_context_picker_modes(&thread_store)
+ .iter()
+ .map(|mode| {
+ Completion {
+ old_range: source_range.clone(),
+ new_text: format!("@{} ", mode.mention_prefix()),
+ label: CodeLabel::plain(mode.label().to_string(), None),
+ icon_path: Some(mode.icon().path().into()),
+ documentation: None,
+ source: project::CompletionSource::Custom,
+ // This ensures that when a user accepts this completion, the
+ // completion menu will still be shown after "@category " is
+ // inserted
+ confirm: Some(Arc::new(|_, _, _| true)),
+ }
+ }),
+ );
+ completions
+ }
+
+ fn full_path_for_entry(
+ worktree_id: WorktreeId,
+ path: &Path,
+ workspace: Entity,
+ cx: &App,
+ ) -> Option {
+ let worktree = workspace
+ .read(cx)
+ .project()
+ .read(cx)
+ .worktree_for_id(worktree_id, cx)?
+ .read(cx);
+
+ let mut full_path = PathBuf::from(worktree.root_name());
+ full_path.push(path);
+ Some(full_path)
+ }
+
+ fn build_code_label_for_full_path(
+ worktree_id: WorktreeId,
+ path: &Path,
+ workspace: Entity,
+ cx: &App,
+ ) -> Option {
+ let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
+ let mut label = CodeLabel::default();
+ let worktree = workspace
+ .read(cx)
+ .project()
+ .read(cx)
+ .worktree_for_id(worktree_id, cx)?;
+
+ let entry = worktree.read(cx).entry_for_path(&path)?;
+ let file_name = path.file_name()?.to_string_lossy();
+ label.push_str(&file_name, None);
+ if entry.is_dir() {
+ label.push_str("/ ", None);
+ } else {
+ label.push_str(" ", None);
+ };
+
+ let mut path_hint = PathBuf::from(worktree.read(cx).root_name());
+ if let Some(path_to_entry) = path.parent() {
+ path_hint.push(path_to_entry);
+ }
+ label.push_str(&path_hint.to_string_lossy(), comment_id);
+
+ label.filter_range = 0..label.text().len();
+
+ Some(label)
+ }
+
+ fn completion_for_thread(
+ thread_entry: ThreadContextEntry,
+ excerpt_id: ExcerptId,
+ source_range: Range,
+ recent: bool,
+ editor: Entity,
+ context_store: Entity,
+ thread_store: Entity,
+ ) -> Completion {
+ let icon_for_completion = if recent {
+ IconName::HistoryRerun
+ } else {
+ IconName::MessageCircle
+ };
+ let new_text = format!("@thread {}", thread_entry.summary);
+ let new_text_len = new_text.len();
+ Completion {
+ old_range: source_range.clone(),
+ new_text,
+ label: CodeLabel::plain(thread_entry.summary.to_string(), None),
+ documentation: None,
+ source: project::CompletionSource::Custom,
+ icon_path: Some(icon_for_completion.path().into()),
+ confirm: Some(confirm_completion_callback(
+ IconName::MessageCircle.path().into(),
+ thread_entry.summary.clone(),
+ excerpt_id,
+ source_range.start,
+ new_text_len,
+ editor.clone(),
+ move |cx| {
+ let thread_id = thread_entry.id.clone();
+ let context_store = context_store.clone();
+ let thread_store = thread_store.clone();
+ cx.spawn(async move |cx| {
+ let thread = thread_store
+ .update(cx, |thread_store, cx| {
+ thread_store.open_thread(&thread_id, cx)
+ })?
+ .await?;
+ context_store.update(cx, |context_store, cx| {
+ context_store.add_thread(thread, false, cx)
+ })
+ })
+ .detach_and_log_err(cx);
+ },
+ )),
+ }
+ }
+
+ fn completion_for_fetch(
+ source_range: Range,
+ url_to_fetch: SharedString,
+ excerpt_id: ExcerptId,
+ editor: Entity,
+ context_store: Entity,
+ http_client: Arc,
+ ) -> Completion {
+ let new_text = format!("@fetch {}", url_to_fetch);
+ let new_text_len = new_text.len();
+ Completion {
+ old_range: source_range.clone(),
+ new_text,
+ label: CodeLabel::plain(url_to_fetch.to_string(), None),
+ documentation: None,
+ source: project::CompletionSource::Custom,
+ icon_path: Some(IconName::Globe.path().into()),
+ confirm: Some(confirm_completion_callback(
+ IconName::Globe.path().into(),
+ url_to_fetch.clone(),
+ excerpt_id,
+ source_range.start,
+ new_text_len,
+ editor.clone(),
+ move |cx| {
+ let context_store = context_store.clone();
+ let http_client = http_client.clone();
+ let url_to_fetch = url_to_fetch.clone();
+ cx.spawn(async move |cx| {
+ if context_store.update(cx, |context_store, _| {
+ context_store.includes_url(&url_to_fetch).is_some()
+ })? {
+ return Ok(());
+ }
+ let content = cx
+ .background_spawn(fetch_url_content(
+ http_client,
+ url_to_fetch.to_string(),
+ ))
+ .await?;
+ context_store.update(cx, |context_store, _| {
+ context_store.add_fetched_url(url_to_fetch.to_string(), content)
+ })
+ })
+ .detach_and_log_err(cx);
+ },
+ )),
+ }
+ }
+
+ fn completion_for_path(
+ project_path: ProjectPath,
+ is_recent: bool,
+ is_directory: bool,
+ excerpt_id: ExcerptId,
+ source_range: Range,
+ editor: Entity,
+ context_store: Entity,
+ workspace: Entity,
+ cx: &App,
+ ) -> Option {
+ let label = Self::build_code_label_for_full_path(
+ project_path.worktree_id,
+ &project_path.path,
+ workspace.clone(),
+ cx,
+ )?;
+ let full_path = Self::full_path_for_entry(
+ project_path.worktree_id,
+ &project_path.path,
+ workspace.clone(),
+ cx,
+ )?;
+
+ let crease_icon_path = if is_directory {
+ FileIcons::get_folder_icon(false, cx).unwrap_or_else(|| IconName::Folder.path().into())
+ } else {
+ FileIcons::get_icon(&full_path, cx).unwrap_or_else(|| IconName::File.path().into())
+ };
+ let completion_icon_path = if is_recent {
+ IconName::HistoryRerun.path().into()
+ } else {
+ crease_icon_path.clone()
+ };
+
+ let crease_name = project_path
+ .path
+ .file_name()
+ .map(|file_name| file_name.to_string_lossy().to_string())
+ .unwrap_or_else(|| "untitled".to_string());
+
+ let new_text = format!("@file {}", full_path.to_string_lossy());
+ let new_text_len = new_text.len();
+ Some(Completion {
+ old_range: source_range.clone(),
+ new_text,
+ label,
+ documentation: None,
+ source: project::CompletionSource::Custom,
+ icon_path: Some(completion_icon_path),
+ confirm: Some(confirm_completion_callback(
+ crease_icon_path,
+ crease_name.into(),
+ excerpt_id,
+ source_range.start,
+ new_text_len,
+ editor,
+ move |cx| {
+ context_store.update(cx, |context_store, cx| {
+ let task = if is_directory {
+ context_store.add_directory(project_path.clone(), false, cx)
+ } else {
+ context_store.add_file_from_path(project_path.clone(), false, cx)
+ };
+ task.detach_and_log_err(cx);
+ })
+ },
+ )),
+ })
+ }
+}
+
+impl CompletionProvider for ContextPickerCompletionProvider {
+ fn completions(
+ &self,
+ excerpt_id: ExcerptId,
+ buffer: &Entity,
+ buffer_position: Anchor,
+ _trigger: CompletionContext,
+ _window: &mut Window,
+ cx: &mut Context,
+ ) -> Task>>> {
+ let state = buffer.update(cx, |buffer, _cx| {
+ let position = buffer_position.to_point(buffer);
+ let line_start = Point::new(position.row, 0);
+ let offset_to_line = buffer.point_to_offset(line_start);
+ let mut lines = buffer.text_for_range(line_start..position).lines();
+ let line = lines.next()?;
+ MentionCompletion::try_parse(line, offset_to_line)
+ });
+ let Some(state) = state else {
+ return Task::ready(Ok(None));
+ };
+
+ let Some(workspace) = self.workspace.upgrade() else {
+ return Task::ready(Ok(None));
+ };
+ let Some(context_store) = self.context_store.upgrade() else {
+ return Task::ready(Ok(None));
+ };
+
+ let snapshot = buffer.read(cx).snapshot();
+ let source_range = snapshot.anchor_after(state.source_range.start)
+ ..snapshot.anchor_before(state.source_range.end);
+
+ let thread_store = self.thread_store.clone();
+ let editor = self.editor.clone();
+ let http_client = workspace.read(cx).client().http_client().clone();
+
+ cx.spawn(async move |_, cx| {
+ let mut completions = Vec::new();
+
+ let MentionCompletion {
+ mode: category,
+ argument,
+ ..
+ } = state;
+
+ let query = argument.unwrap_or_else(|| "".to_string());
+ match category {
+ Some(ContextPickerMode::File) => {
+ let path_matches = cx
+ .update(|cx| {
+ super::file_context_picker::search_paths(
+ query,
+ Arc::new(AtomicBool::default()),
+ &workspace,
+ cx,
+ )
+ })?
+ .await;
+
+ completions.reserve(path_matches.len());
+ cx.update(|cx| {
+ completions.extend(path_matches.iter().filter_map(|mat| {
+ let editor = editor.upgrade()?;
+ Self::completion_for_path(
+ ProjectPath {
+ worktree_id: WorktreeId::from_usize(mat.worktree_id),
+ path: mat.path.clone(),
+ },
+ false,
+ mat.is_dir,
+ excerpt_id,
+ source_range.clone(),
+ editor.clone(),
+ context_store.clone(),
+ workspace.clone(),
+ cx,
+ )
+ }));
+ })?;
+ }
+ Some(ContextPickerMode::Fetch) => {
+ if let Some(editor) = editor.upgrade() {
+ if !query.is_empty() {
+ completions.push(Self::completion_for_fetch(
+ source_range.clone(),
+ query.into(),
+ excerpt_id,
+ editor.clone(),
+ context_store.clone(),
+ http_client.clone(),
+ ));
+ }
+
+ context_store.update(cx, |store, _| {
+ let urls = store.context().iter().filter_map(|context| {
+ if let AssistantContext::FetchedUrl(context) = context {
+ Some(context.url.clone())
+ } else {
+ None
+ }
+ });
+ for url in urls {
+ completions.push(Self::completion_for_fetch(
+ source_range.clone(),
+ url,
+ excerpt_id,
+ editor.clone(),
+ context_store.clone(),
+ http_client.clone(),
+ ));
+ }
+ })?;
+ }
+ }
+ Some(ContextPickerMode::Thread) => {
+ if let Some((thread_store, editor)) = thread_store
+ .and_then(|thread_store| thread_store.upgrade())
+ .zip(editor.upgrade())
+ {
+ let threads = cx
+ .update(|cx| {
+ super::thread_context_picker::search_threads(
+ query,
+ thread_store.clone(),
+ cx,
+ )
+ })?
+ .await;
+ for thread in threads {
+ completions.push(Self::completion_for_thread(
+ thread.clone(),
+ excerpt_id,
+ source_range.clone(),
+ false,
+ editor.clone(),
+ context_store.clone(),
+ thread_store.clone(),
+ ));
+ }
+ }
+ }
+ None => {
+ cx.update(|cx| {
+ if let Some(editor) = editor.upgrade() {
+ completions.extend(Self::default_completions(
+ excerpt_id,
+ source_range.clone(),
+ context_store.clone(),
+ thread_store.clone(),
+ editor,
+ workspace.clone(),
+ cx,
+ ));
+ }
+ })?;
+ }
+ }
+ Ok(Some(completions))
+ })
+ }
+
+ fn resolve_completions(
+ &self,
+ _buffer: Entity,
+ _completion_indices: Vec,
+ _completions: Rc>>,
+ _cx: &mut Context,
+ ) -> Task> {
+ Task::ready(Ok(true))
+ }
+
+ fn is_completion_trigger(
+ &self,
+ buffer: &Entity,
+ position: language::Anchor,
+ _: &str,
+ _: bool,
+ cx: &mut Context,
+ ) -> bool {
+ let buffer = buffer.read(cx);
+ let position = position.to_point(buffer);
+ let line_start = Point::new(position.row, 0);
+ let offset_to_line = buffer.point_to_offset(line_start);
+ let mut lines = buffer.text_for_range(line_start..position).lines();
+ if let Some(line) = lines.next() {
+ MentionCompletion::try_parse(line, offset_to_line)
+ .map(|completion| {
+ completion.source_range.start <= offset_to_line + position.column as usize
+ && completion.source_range.end >= offset_to_line + position.column as usize
+ })
+ .unwrap_or(false)
+ } else {
+ false
+ }
+ }
+
+ fn sort_completions(&self) -> bool {
+ false
+ }
+}
+
+fn confirm_completion_callback(
+ crease_icon_path: SharedString,
+ crease_text: SharedString,
+ excerpt_id: ExcerptId,
+ start: Anchor,
+ content_len: usize,
+ editor: Entity,
+ add_context_fn: impl Fn(&mut App) -> () + Send + Sync + 'static,
+) -> Arc bool + Send + Sync> {
+ Arc::new(move |_, window, cx| {
+ add_context_fn(cx);
+
+ let crease_text = crease_text.clone();
+ let crease_icon_path = crease_icon_path.clone();
+ let editor = editor.clone();
+ window.defer(cx, move |window, cx| {
+ crate::context_picker::insert_crease_for_mention(
+ excerpt_id,
+ start,
+ content_len,
+ crease_text,
+ crease_icon_path,
+ editor,
+ window,
+ cx,
+ );
+ });
+ false
+ })
+}
+
+#[derive(Debug, Default, PartialEq)]
+struct MentionCompletion {
+ source_range: Range,
+ mode: Option,
+ argument: Option,
+}
+
+impl MentionCompletion {
+ fn try_parse(line: &str, offset_to_line: usize) -> Option {
+ let last_mention_start = line.rfind('@')?;
+ if last_mention_start >= line.len() {
+ return Some(Self::default());
+ }
+ let rest_of_line = &line[last_mention_start + 1..];
+
+ let mut mode = None;
+ let mut argument = None;
+
+ let mut parts = rest_of_line.split_whitespace();
+ let mut end = last_mention_start + 1;
+ if let Some(mode_text) = parts.next() {
+ end += mode_text.len();
+ mode = ContextPickerMode::try_from(mode_text).ok();
+ match rest_of_line[mode_text.len()..].find(|c: char| !c.is_whitespace()) {
+ Some(whitespace_count) => {
+ if let Some(argument_text) = parts.next() {
+ argument = Some(argument_text.to_string());
+ end += whitespace_count + argument_text.len();
+ }
+ }
+ None => {
+ // Rest of line is entirely whitespace
+ end += rest_of_line.len() - mode_text.len();
+ }
+ }
+ }
+
+ Some(Self {
+ source_range: last_mention_start + offset_to_line..end + offset_to_line,
+ mode,
+ argument,
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use gpui::{Focusable, TestAppContext, VisualTestContext};
+ use project::{Project, ProjectPath};
+ use serde_json::json;
+ use settings::SettingsStore;
+ use std::{ops::Deref, path::PathBuf};
+ use util::{path, separator};
+ use workspace::AppState;
+
+ #[test]
+ fn test_mention_completion_parse() {
+ assert_eq!(MentionCompletion::try_parse("Lorem Ipsum", 0), None);
+
+ assert_eq!(
+ MentionCompletion::try_parse("Lorem @", 0),
+ Some(MentionCompletion {
+ source_range: 6..7,
+ mode: None,
+ argument: None,
+ })
+ );
+
+ assert_eq!(
+ MentionCompletion::try_parse("Lorem @file", 0),
+ Some(MentionCompletion {
+ source_range: 6..11,
+ mode: Some(ContextPickerMode::File),
+ argument: None,
+ })
+ );
+
+ assert_eq!(
+ MentionCompletion::try_parse("Lorem @file ", 0),
+ Some(MentionCompletion {
+ source_range: 6..12,
+ mode: Some(ContextPickerMode::File),
+ argument: None,
+ })
+ );
+
+ assert_eq!(
+ MentionCompletion::try_parse("Lorem @file main.rs", 0),
+ Some(MentionCompletion {
+ source_range: 6..19,
+ mode: Some(ContextPickerMode::File),
+ argument: Some("main.rs".to_string()),
+ })
+ );
+
+ assert_eq!(
+ MentionCompletion::try_parse("Lorem @file main.rs ", 0),
+ Some(MentionCompletion {
+ source_range: 6..19,
+ mode: Some(ContextPickerMode::File),
+ argument: Some("main.rs".to_string()),
+ })
+ );
+
+ assert_eq!(
+ MentionCompletion::try_parse("Lorem @file main.rs Ipsum", 0),
+ Some(MentionCompletion {
+ source_range: 6..19,
+ mode: Some(ContextPickerMode::File),
+ argument: Some("main.rs".to_string()),
+ })
+ );
+ }
+
+ #[gpui::test]
+ async fn test_context_completion_provider(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let app_state = cx.update(AppState::test);
+
+ cx.update(|cx| {
+ language::init(cx);
+ editor::init(cx);
+ workspace::init(app_state.clone(), cx);
+ Project::init_settings(cx);
+ });
+
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ path!("/dir"),
+ json!({
+ "editor": "",
+ "a": {
+ "one.txt": "",
+ "two.txt": "",
+ "three.txt": "",
+ "four.txt": ""
+ },
+ "b": {
+ "five.txt": "",
+ "six.txt": "",
+ "seven.txt": "",
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
+ let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
+ let workspace = window.root(cx).unwrap();
+
+ let worktree = project.update(cx, |project, cx| {
+ let mut worktrees = project.worktrees(cx).collect::>();
+ assert_eq!(worktrees.len(), 1);
+ worktrees.pop().unwrap()
+ });
+ let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
+
+ let mut cx = VisualTestContext::from_window(*window.deref(), cx);
+
+ let paths = vec![
+ separator!("a/one.txt"),
+ separator!("a/two.txt"),
+ separator!("a/three.txt"),
+ separator!("a/four.txt"),
+ separator!("b/five.txt"),
+ separator!("b/six.txt"),
+ separator!("b/seven.txt"),
+ ];
+ for path in paths {
+ workspace
+ .update_in(&mut cx, |workspace, window, cx| {
+ workspace.open_path(
+ ProjectPath {
+ worktree_id,
+ path: Path::new(path).into(),
+ },
+ None,
+ false,
+ window,
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+ }
+
+ //TODO: Construct the editor without an actual buffer that points to a file
+ let item = workspace
+ .update_in(&mut cx, |workspace, window, cx| {
+ workspace.open_path(
+ ProjectPath {
+ worktree_id,
+ path: PathBuf::from("editor").into(),
+ },
+ None,
+ true,
+ window,
+ cx,
+ )
+ })
+ .await
+ .expect("Could not open test file");
+
+ let editor = cx.update(|_, cx| {
+ item.act_as::(cx)
+ .expect("Opened test file wasn't an editor")
+ });
+
+ let context_store = cx.new(|_| ContextStore::new(workspace.downgrade()));
+
+ let editor_entity = editor.downgrade();
+ editor.update_in(&mut cx, |editor, window, cx| {
+ window.focus(&editor.focus_handle(cx));
+ editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
+ workspace.downgrade(),
+ context_store.downgrade(),
+ None,
+ editor_entity,
+ ))));
+ });
+
+ cx.simulate_input("Lorem ");
+
+ editor.update(&mut cx, |editor, cx| {
+ assert_eq!(editor.text(cx), "Lorem ");
+ assert!(!editor.has_visible_completions_menu());
+ });
+
+ cx.simulate_input("@");
+
+ editor.update(&mut cx, |editor, cx| {
+ assert_eq!(editor.text(cx), "Lorem @");
+ assert!(editor.has_visible_completions_menu());
+ assert_eq!(
+ current_completion_labels(editor),
+ &[
+ format!("seven.txt {}", separator!("dir/b")).as_str(),
+ format!("six.txt {}", separator!("dir/b")).as_str(),
+ format!("five.txt {}", separator!("dir/b")).as_str(),
+ format!("four.txt {}", separator!("dir/a")).as_str(),
+ "Files & Directories",
+ "Fetch"
+ ]
+ );
+ });
+
+ // Select and confirm "File"
+ editor.update_in(&mut cx, |editor, window, cx| {
+ assert!(editor.has_visible_completions_menu());
+ editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
+ editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
+ editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
+ editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
+ editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
+ });
+
+ cx.run_until_parked();
+
+ editor.update(&mut cx, |editor, cx| {
+ assert_eq!(editor.text(cx), "Lorem @file ");
+ assert!(editor.has_visible_completions_menu());
+ });
+
+ cx.simulate_input("one");
+
+ editor.update(&mut cx, |editor, cx| {
+ assert_eq!(editor.text(cx), "Lorem @file one");
+ assert!(editor.has_visible_completions_menu());
+ assert_eq!(
+ current_completion_labels(editor),
+ vec![format!("one.txt {}", separator!("dir/a")).as_str(),]
+ );
+ });
+
+ editor.update_in(&mut cx, |editor, window, cx| {
+ assert!(editor.has_visible_completions_menu());
+ editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
+ });
+
+ editor.update(&mut cx, |editor, cx| {
+ assert_eq!(
+ editor.text(cx),
+ format!("Lorem @file {}", separator!("dir/a/one.txt"))
+ );
+ assert!(!editor.has_visible_completions_menu());
+ assert_eq!(
+ crease_ranges(editor, cx),
+ vec![Point::new(0, 6)..Point::new(0, 25)]
+ );
+ });
+
+ cx.simulate_input(" ");
+
+ editor.update(&mut cx, |editor, cx| {
+ assert_eq!(
+ editor.text(cx),
+ format!("Lorem @file {} ", separator!("dir/a/one.txt"))
+ );
+ assert!(!editor.has_visible_completions_menu());
+ assert_eq!(
+ crease_ranges(editor, cx),
+ vec![Point::new(0, 6)..Point::new(0, 25)]
+ );
+ });
+
+ cx.simulate_input("Ipsum ");
+
+ editor.update(&mut cx, |editor, cx| {
+ assert_eq!(
+ editor.text(cx),
+ format!("Lorem @file {} Ipsum ", separator!("dir/a/one.txt"))
+ );
+ assert!(!editor.has_visible_completions_menu());
+ assert_eq!(
+ crease_ranges(editor, cx),
+ vec![Point::new(0, 6)..Point::new(0, 25)]
+ );
+ });
+
+ cx.simulate_input("@file ");
+
+ editor.update(&mut cx, |editor, cx| {
+ assert_eq!(
+ editor.text(cx),
+ format!("Lorem @file {} Ipsum @file ", separator!("dir/a/one.txt"))
+ );
+ assert!(editor.has_visible_completions_menu());
+ assert_eq!(
+ crease_ranges(editor, cx),
+ vec![Point::new(0, 6)..Point::new(0, 25)]
+ );
+ });
+
+ editor.update_in(&mut cx, |editor, window, cx| {
+ editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
+ });
+
+ cx.run_until_parked();
+
+ editor.update(&mut cx, |editor, cx| {
+ assert_eq!(
+ editor.text(cx),
+ format!(
+ "Lorem @file {} Ipsum @file {}",
+ separator!("dir/a/one.txt"),
+ separator!("dir/b/seven.txt")
+ )
+ );
+ assert!(!editor.has_visible_completions_menu());
+ assert_eq!(
+ crease_ranges(editor, cx),
+ vec![
+ Point::new(0, 6)..Point::new(0, 25),
+ Point::new(0, 32)..Point::new(0, 53)
+ ]
+ );
+ });
+
+ cx.simulate_input("\n@");
+
+ editor.update(&mut cx, |editor, cx| {
+ assert_eq!(
+ editor.text(cx),
+ format!(
+ "Lorem @file {} Ipsum @file {}\n@",
+ separator!("dir/a/one.txt"),
+ separator!("dir/b/seven.txt")
+ )
+ );
+ assert!(editor.has_visible_completions_menu());
+ assert_eq!(
+ crease_ranges(editor, cx),
+ vec![
+ Point::new(0, 6)..Point::new(0, 25),
+ Point::new(0, 32)..Point::new(0, 53)
+ ]
+ );
+ });
+
+ editor.update_in(&mut cx, |editor, window, cx| {
+ editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
+ });
+
+ cx.run_until_parked();
+
+ editor.update(&mut cx, |editor, cx| {
+ assert_eq!(
+ editor.text(cx),
+ format!(
+ "Lorem @file {} Ipsum @file {}\n@file {}",
+ separator!("dir/a/one.txt"),
+ separator!("dir/b/seven.txt"),
+ separator!("dir/b/six.txt"),
+ )
+ );
+ assert!(!editor.has_visible_completions_menu());
+ assert_eq!(
+ crease_ranges(editor, cx),
+ vec![
+ Point::new(0, 6)..Point::new(0, 25),
+ Point::new(0, 32)..Point::new(0, 53),
+ Point::new(1, 0)..Point::new(1, 19)
+ ]
+ );
+ });
+ }
+
+ fn crease_ranges(editor: &Editor, cx: &mut App) -> Vec> {
+ let snapshot = editor.buffer().read(cx).snapshot(cx);
+ editor.display_map.update(cx, |display_map, cx| {
+ display_map
+ .snapshot(cx)
+ .crease_snapshot
+ .crease_items_with_offsets(&snapshot)
+ .into_iter()
+ .map(|(_, range)| range)
+ .collect()
+ })
+ }
+
+ fn current_completion_labels(editor: &Editor) -> Vec {
+ let completions = editor.current_completions().expect("Missing completions");
+ completions
+ .into_iter()
+ .map(|completion| completion.label.text.to_string())
+ .collect::>()
+ }
+
+ pub(crate) fn init_test(cx: &mut TestAppContext) {
+ cx.update(|cx| {
+ let store = SettingsStore::test(cx);
+ cx.set_global(store);
+ theme::init(theme::LoadThemes::JustBase, cx);
+ client::init_settings(cx);
+ language::init(cx);
+ Project::init_settings(cx);
+ workspace::init_settings(cx);
+ editor::init_settings(cx);
+ });
+ }
+}
diff --git a/crates/assistant2/src/context_picker/fetch_context_picker.rs b/crates/assistant2/src/context_picker/fetch_context_picker.rs
index 35e92b5fe3..1a4b943946 100644
--- a/crates/assistant2/src/context_picker/fetch_context_picker.rs
+++ b/crates/assistant2/src/context_picker/fetch_context_picker.rs
@@ -81,77 +81,80 @@ impl FetchContextPickerDelegate {
url: String::new(),
}
}
+}
- async fn build_message(http_client: Arc, url: String) -> Result {
- let url = if !url.starts_with("https://") && !url.starts_with("http://") {
- format!("https://{url}")
- } else {
- url
- };
+pub(crate) async fn fetch_url_content(
+ http_client: Arc,
+ url: String,
+) -> Result {
+ let url = if !url.starts_with("https://") && !url.starts_with("http://") {
+ format!("https://{url}")
+ } else {
+ url
+ };
- let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
+ let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
- let mut body = Vec::new();
- response
- .body_mut()
- .read_to_end(&mut body)
- .await
- .context("error reading response body")?;
+ let mut body = Vec::new();
+ response
+ .body_mut()
+ .read_to_end(&mut body)
+ .await
+ .context("error reading response body")?;
- if response.status().is_client_error() {
- let text = String::from_utf8_lossy(body.as_slice());
- bail!(
- "status error {}, response: {text:?}",
- response.status().as_u16()
- );
+ if response.status().is_client_error() {
+ let text = String::from_utf8_lossy(body.as_slice());
+ bail!(
+ "status error {}, response: {text:?}",
+ response.status().as_u16()
+ );
+ }
+
+ let Some(content_type) = response.headers().get("content-type") else {
+ bail!("missing Content-Type header");
+ };
+ let content_type = content_type
+ .to_str()
+ .context("invalid Content-Type header")?;
+ let content_type = match content_type {
+ "text/html" => ContentType::Html,
+ "text/plain" => ContentType::Plaintext,
+ "application/json" => ContentType::Json,
+ _ => ContentType::Html,
+ };
+
+ match content_type {
+ ContentType::Html => {
+ let mut handlers: Vec = vec![
+ Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
+ Rc::new(RefCell::new(markdown::ParagraphHandler)),
+ Rc::new(RefCell::new(markdown::HeadingHandler)),
+ Rc::new(RefCell::new(markdown::ListHandler)),
+ Rc::new(RefCell::new(markdown::TableHandler::new())),
+ Rc::new(RefCell::new(markdown::StyledTextHandler)),
+ ];
+ if url.contains("wikipedia.org") {
+ use html_to_markdown::structure::wikipedia;
+
+ handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
+ handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
+ handlers.push(Rc::new(
+ RefCell::new(wikipedia::WikipediaCodeHandler::new()),
+ ));
+ } else {
+ handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
+ }
+
+ convert_html_to_markdown(&body[..], &mut handlers)
}
+ ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
+ ContentType::Json => {
+ let json: serde_json::Value = serde_json::from_slice(&body)?;
- let Some(content_type) = response.headers().get("content-type") else {
- bail!("missing Content-Type header");
- };
- let content_type = content_type
- .to_str()
- .context("invalid Content-Type header")?;
- let content_type = match content_type {
- "text/html" => ContentType::Html,
- "text/plain" => ContentType::Plaintext,
- "application/json" => ContentType::Json,
- _ => ContentType::Html,
- };
-
- match content_type {
- ContentType::Html => {
- let mut handlers: Vec = vec![
- Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
- Rc::new(RefCell::new(markdown::ParagraphHandler)),
- Rc::new(RefCell::new(markdown::HeadingHandler)),
- Rc::new(RefCell::new(markdown::ListHandler)),
- Rc::new(RefCell::new(markdown::TableHandler::new())),
- Rc::new(RefCell::new(markdown::StyledTextHandler)),
- ];
- if url.contains("wikipedia.org") {
- use html_to_markdown::structure::wikipedia;
-
- handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
- handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
- handlers.push(Rc::new(
- RefCell::new(wikipedia::WikipediaCodeHandler::new()),
- ));
- } else {
- handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
- }
-
- convert_html_to_markdown(&body[..], &mut handlers)
- }
- ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
- ContentType::Json => {
- let json: serde_json::Value = serde_json::from_slice(&body)?;
-
- Ok(format!(
- "```json\n{}\n```",
- serde_json::to_string_pretty(&json)?
- ))
- }
+ Ok(format!(
+ "```json\n{}\n```",
+ serde_json::to_string_pretty(&json)?
+ ))
}
}
}
@@ -208,7 +211,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
let confirm_behavior = self.confirm_behavior;
cx.spawn_in(window, async move |this, cx| {
let text = cx
- .background_spawn(Self::build_message(http_client, url.clone()))
+ .background_spawn(fetch_url_content(http_client, url.clone()))
.await?;
this.update_in(cx, |this, window, cx| {
diff --git a/crates/assistant2/src/context_picker/file_context_picker.rs b/crates/assistant2/src/context_picker/file_context_picker.rs
index f856f06e51..572987f8af 100644
--- a/crates/assistant2/src/context_picker/file_context_picker.rs
+++ b/crates/assistant2/src/context_picker/file_context_picker.rs
@@ -1,25 +1,15 @@
-use std::collections::BTreeSet;
-use std::ops::Range;
use std::path::Path;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
-use editor::actions::FoldAt;
-use editor::display_map::{Crease, FoldId};
-use editor::scroll::Autoscroll;
-use editor::{Anchor, AnchorRangeExt, Editor, FoldPlaceholder, ToPoint};
use file_icons::FileIcons;
use fuzzy::PathMatch;
use gpui::{
- AnyElement, App, AppContext, DismissEvent, Empty, Entity, FocusHandle, Focusable, Stateful,
- Task, WeakEntity,
+ App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
};
-use multi_buffer::{MultiBufferPoint, MultiBufferRow};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
-use rope::Point;
-use text::SelectionGoal;
-use ui::{prelude::*, ButtonLike, Disclosure, ListItem, TintColor, Tooltip};
+use ui::{prelude::*, ListItem, Tooltip};
use util::ResultExt as _;
use workspace::{notifications::NotifyResultExt, Workspace};
@@ -34,7 +24,6 @@ impl FileContextPicker {
pub fn new(
context_picker: WeakEntity,
workspace: WeakEntity,
- editor: WeakEntity,
context_store: WeakEntity,
confirm_behavior: ConfirmBehavior,
window: &mut Window,
@@ -43,7 +32,6 @@ impl FileContextPicker {
let delegate = FileContextPickerDelegate::new(
context_picker,
workspace,
- editor,
context_store,
confirm_behavior,
);
@@ -68,7 +56,6 @@ impl Render for FileContextPicker {
pub struct FileContextPickerDelegate {
context_picker: WeakEntity,
workspace: WeakEntity,
- editor: WeakEntity,
context_store: WeakEntity,
confirm_behavior: ConfirmBehavior,
matches: Vec,
@@ -79,95 +66,18 @@ impl FileContextPickerDelegate {
pub fn new(
context_picker: WeakEntity,
workspace: WeakEntity,
- editor: WeakEntity,
context_store: WeakEntity,
confirm_behavior: ConfirmBehavior,
) -> Self {
Self {
context_picker,
workspace,
- editor,
context_store,
confirm_behavior,
matches: Vec::new(),
selected_index: 0,
}
}
-
- fn search(
- &mut self,
- query: String,
- cancellation_flag: Arc,
- workspace: &Entity,
- cx: &mut Context>,
- ) -> Task> {
- if query.is_empty() {
- let workspace = workspace.read(cx);
- let project = workspace.project().read(cx);
- let recent_matches = workspace
- .recent_navigation_history(Some(10), cx)
- .into_iter()
- .filter_map(|(project_path, _)| {
- let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
- Some(PathMatch {
- score: 0.,
- positions: Vec::new(),
- worktree_id: project_path.worktree_id.to_usize(),
- path: project_path.path,
- path_prefix: worktree.read(cx).root_name().into(),
- distance_to_relative_ancestor: 0,
- is_dir: false,
- })
- });
-
- let file_matches = project.worktrees(cx).flat_map(|worktree| {
- let worktree = worktree.read(cx);
- let path_prefix: Arc = worktree.root_name().into();
- worktree.entries(false, 0).map(move |entry| PathMatch {
- score: 0.,
- positions: Vec::new(),
- worktree_id: worktree.id().to_usize(),
- path: entry.path.clone(),
- path_prefix: path_prefix.clone(),
- distance_to_relative_ancestor: 0,
- is_dir: entry.is_dir(),
- })
- });
-
- Task::ready(recent_matches.chain(file_matches).collect())
- } else {
- let worktrees = workspace.read(cx).visible_worktrees(cx).collect::>();
- let candidate_sets = worktrees
- .into_iter()
- .map(|worktree| {
- let worktree = worktree.read(cx);
-
- PathMatchCandidateSet {
- snapshot: worktree.snapshot(),
- include_ignored: worktree
- .root_entry()
- .map_or(false, |entry| entry.is_ignored),
- include_root_name: true,
- candidates: project::Candidates::Entries,
- }
- })
- .collect::>();
-
- let executor = cx.background_executor().clone();
- cx.foreground_executor().spawn(async move {
- fuzzy::match_path_sets(
- candidate_sets.as_slice(),
- query.as_str(),
- None,
- false,
- 100,
- &cancellation_flag,
- executor,
- )
- .await
- })
- }
- }
}
impl PickerDelegate for FileContextPickerDelegate {
@@ -204,7 +114,7 @@ impl PickerDelegate for FileContextPickerDelegate {
return Task::ready(());
};
- let search_task = self.search(query, Arc::::default(), &workspace, cx);
+ let search_task = search_paths(query, Arc::::default(), &workspace, cx);
cx.spawn_in(window, async move |this, cx| {
// TODO: This should be probably be run in the background.
@@ -222,14 +132,6 @@ impl PickerDelegate for FileContextPickerDelegate {
return;
};
- let file_name = mat
- .path
- .file_name()
- .map(|os_str| os_str.to_string_lossy().into_owned())
- .unwrap_or(mat.path_prefix.to_string());
-
- let full_path = mat.path.display().to_string();
-
let project_path = ProjectPath {
worktree_id: WorktreeId::from_usize(mat.worktree_id),
path: mat.path.clone(),
@@ -237,106 +139,13 @@ impl PickerDelegate for FileContextPickerDelegate {
let is_directory = mat.is_dir;
- let Some(editor_entity) = self.editor.upgrade() else {
- return;
- };
-
- editor_entity.update(cx, |editor, cx| {
- editor.transact(window, cx, |editor, window, cx| {
- // Move empty selections left by 1 column to select the `@`s, so they get overwritten when we insert.
- {
- let mut selections = editor.selections.all::(cx);
-
- for selection in selections.iter_mut() {
- if selection.is_empty() {
- let old_head = selection.head();
- let new_head = MultiBufferPoint::new(
- old_head.row,
- old_head.column.saturating_sub(1),
- );
- selection.set_head(new_head, SelectionGoal::None);
- }
- }
-
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
- s.select(selections)
- });
- }
-
- let start_anchors = {
- let snapshot = editor.buffer().read(cx).snapshot(cx);
- editor
- .selections
- .all::(cx)
- .into_iter()
- .map(|selection| snapshot.anchor_before(selection.start))
- .collect::>()
- };
-
- editor.insert(&full_path, window, cx);
-
- let end_anchors = {
- let snapshot = editor.buffer().read(cx).snapshot(cx);
- editor
- .selections
- .all::(cx)
- .into_iter()
- .map(|selection| snapshot.anchor_after(selection.end))
- .collect::>()
- };
-
- editor.insert("\n", window, cx); // Needed to end the fold
-
- let file_icon = if is_directory {
- FileIcons::get_folder_icon(false, cx)
- } else {
- FileIcons::get_icon(&Path::new(&full_path), cx)
- }
- .unwrap_or_else(|| SharedString::new(""));
-
- let placeholder = FoldPlaceholder {
- render: render_fold_icon_button(
- file_icon,
- file_name.into(),
- editor_entity.downgrade(),
- ),
- ..Default::default()
- };
-
- let render_trailer =
- move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
-
- let buffer = editor.buffer().read(cx).snapshot(cx);
- let mut rows_to_fold = BTreeSet::new();
- let crease_iter = start_anchors
- .into_iter()
- .zip(end_anchors)
- .map(|(start, end)| {
- rows_to_fold.insert(MultiBufferRow(start.to_point(&buffer).row));
-
- Crease::inline(
- start..end,
- placeholder.clone(),
- fold_toggle("tool-use"),
- render_trailer,
- )
- });
-
- editor.insert_creases(crease_iter, cx);
-
- for buffer_row in rows_to_fold {
- editor.fold_at(&FoldAt { buffer_row }, window, cx);
- }
- });
- });
-
let Some(task) = self
.context_store
.update(cx, |context_store, cx| {
if is_directory {
- context_store.add_directory(project_path, cx)
+ context_store.add_directory(project_path, true, cx)
} else {
- context_store.add_file_from_path(project_path, cx)
+ context_store.add_file_from_path(project_path, true, cx)
}
})
.ok()
@@ -390,6 +199,80 @@ impl PickerDelegate for FileContextPickerDelegate {
}
}
+pub(crate) fn search_paths(
+ query: String,
+ cancellation_flag: Arc,
+ workspace: &Entity,
+ cx: &App,
+) -> Task> {
+ if query.is_empty() {
+ let workspace = workspace.read(cx);
+ let project = workspace.project().read(cx);
+ let recent_matches = workspace
+ .recent_navigation_history(Some(10), cx)
+ .into_iter()
+ .filter_map(|(project_path, _)| {
+ let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
+ Some(PathMatch {
+ score: 0.,
+ positions: Vec::new(),
+ worktree_id: project_path.worktree_id.to_usize(),
+ path: project_path.path,
+ path_prefix: worktree.read(cx).root_name().into(),
+ distance_to_relative_ancestor: 0,
+ is_dir: false,
+ })
+ });
+
+ let file_matches = project.worktrees(cx).flat_map(|worktree| {
+ let worktree = worktree.read(cx);
+ let path_prefix: Arc = worktree.root_name().into();
+ worktree.entries(false, 0).map(move |entry| PathMatch {
+ score: 0.,
+ positions: Vec::new(),
+ worktree_id: worktree.id().to_usize(),
+ path: entry.path.clone(),
+ path_prefix: path_prefix.clone(),
+ distance_to_relative_ancestor: 0,
+ is_dir: entry.is_dir(),
+ })
+ });
+
+ Task::ready(recent_matches.chain(file_matches).collect())
+ } else {
+ let worktrees = workspace.read(cx).visible_worktrees(cx).collect::>();
+ let candidate_sets = worktrees
+ .into_iter()
+ .map(|worktree| {
+ let worktree = worktree.read(cx);
+
+ PathMatchCandidateSet {
+ snapshot: worktree.snapshot(),
+ include_ignored: worktree
+ .root_entry()
+ .map_or(false, |entry| entry.is_ignored),
+ include_root_name: true,
+ candidates: project::Candidates::Entries,
+ }
+ })
+ .collect::>();
+
+ let executor = cx.background_executor().clone();
+ cx.foreground_executor().spawn(async move {
+ fuzzy::match_path_sets(
+ candidate_sets.as_slice(),
+ query.as_str(),
+ None,
+ false,
+ 100,
+ &cancellation_flag,
+ executor,
+ )
+ .await
+ })
+ }
+}
+
pub fn render_file_context_entry(
id: ElementId,
path: &Path,
@@ -484,85 +367,3 @@ pub fn render_file_context_entry(
}
})
}
-
-fn render_fold_icon_button(
- icon: SharedString,
- label: SharedString,
- editor: WeakEntity,
-) -> Arc, &mut App) -> AnyElement> {
- Arc::new(move |fold_id, fold_range, cx| {
- let is_in_text_selection = editor.upgrade().is_some_and(|editor| {
- editor.update(cx, |editor, cx| {
- let snapshot = editor
- .buffer()
- .update(cx, |multi_buffer, cx| multi_buffer.snapshot(cx));
-
- let is_in_pending_selection = || {
- editor
- .selections
- .pending
- .as_ref()
- .is_some_and(|pending_selection| {
- pending_selection
- .selection
- .range()
- .includes(&fold_range, &snapshot)
- })
- };
-
- let mut is_in_complete_selection = || {
- editor
- .selections
- .disjoint_in_range::(fold_range.clone(), cx)
- .into_iter()
- .any(|selection| {
- // This is needed to cover a corner case, if we just check for an existing
- // selection in the fold range, having a cursor at the start of the fold
- // marks it as selected. Non-empty selections don't cause this.
- let length = selection.end - selection.start;
- length > 0
- })
- };
-
- is_in_pending_selection() || is_in_complete_selection()
- })
- });
-
- ButtonLike::new(fold_id)
- .style(ButtonStyle::Filled)
- .selected_style(ButtonStyle::Tinted(TintColor::Accent))
- .toggle_state(is_in_text_selection)
- .child(
- h_flex()
- .gap_1()
- .child(
- Icon::from_path(icon.clone())
- .size(IconSize::Small)
- .color(Color::Muted),
- )
- .child(
- Label::new(label.clone())
- .size(LabelSize::Small)
- .single_line(),
- ),
- )
- .into_any_element()
- })
-}
-
-fn fold_toggle(
- name: &'static str,
-) -> impl Fn(
- MultiBufferRow,
- bool,
- Arc,
- &mut Window,
- &mut App,
-) -> AnyElement {
- move |row, is_folded, fold, _window, _cx| {
- Disclosure::new((name, row.0 as u64), !is_folded)
- .toggle_state(is_folded)
- .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
- .into_any_element()
- }
-}
diff --git a/crates/assistant2/src/context_picker/thread_context_picker.rs b/crates/assistant2/src/context_picker/thread_context_picker.rs
index 82925492fb..6099029fd6 100644
--- a/crates/assistant2/src/context_picker/thread_context_picker.rs
+++ b/crates/assistant2/src/context_picker/thread_context_picker.rs
@@ -110,45 +110,11 @@ impl PickerDelegate for ThreadContextPickerDelegate {
window: &mut Window,
cx: &mut Context>,
) -> Task<()> {
- let Ok(threads) = self.thread_store.update(cx, |this, _cx| {
- this.threads()
- .into_iter()
- .map(|thread| ThreadContextEntry {
- id: thread.id,
- summary: thread.summary,
- })
- .collect::>()
- }) else {
+ let Some(threads) = self.thread_store.upgrade() else {
return Task::ready(());
};
- let executor = cx.background_executor().clone();
- let search_task = cx.background_spawn(async move {
- if query.is_empty() {
- threads
- } else {
- let candidates = threads
- .iter()
- .enumerate()
- .map(|(id, thread)| StringMatchCandidate::new(id, &thread.summary))
- .collect::>();
- let matches = fuzzy::match_strings(
- &candidates,
- &query,
- false,
- 100,
- &Default::default(),
- executor,
- )
- .await;
-
- matches
- .into_iter()
- .map(|mat| threads[mat.candidate_id].clone())
- .collect()
- }
- });
-
+ let search_task = search_threads(query, threads, cx);
cx.spawn_in(window, async move |this, cx| {
let matches = search_task.await;
this.update(cx, |this, cx| {
@@ -176,7 +142,9 @@ impl PickerDelegate for ThreadContextPickerDelegate {
this.update_in(cx, |this, window, cx| {
this.delegate
.context_store
- .update(cx, |context_store, cx| context_store.add_thread(thread, cx))
+ .update(cx, |context_store, cx| {
+ context_store.add_thread(thread, true, cx)
+ })
.ok();
match this.delegate.confirm_behavior {
@@ -248,3 +216,46 @@ pub fn render_thread_context_entry(
)
})
}
+
+pub(crate) fn search_threads(
+ query: String,
+ thread_store: Entity,
+ cx: &mut App,
+) -> Task> {
+ let threads = thread_store.update(cx, |this, _cx| {
+ this.threads()
+ .into_iter()
+ .map(|thread| ThreadContextEntry {
+ id: thread.id,
+ summary: thread.summary,
+ })
+ .collect::>()
+ });
+
+ let executor = cx.background_executor().clone();
+ cx.background_spawn(async move {
+ if query.is_empty() {
+ threads
+ } else {
+ let candidates = threads
+ .iter()
+ .enumerate()
+ .map(|(id, thread)| StringMatchCandidate::new(id, &thread.summary))
+ .collect::>();
+ let matches = fuzzy::match_strings(
+ &candidates,
+ &query,
+ false,
+ 100,
+ &Default::default(),
+ executor,
+ )
+ .await;
+
+ matches
+ .into_iter()
+ .map(|mat| threads[mat.candidate_id].clone())
+ .collect()
+ }
+ })
+}
diff --git a/crates/assistant2/src/context_store.rs b/crates/assistant2/src/context_store.rs
index c74658a678..3dfaaca3c8 100644
--- a/crates/assistant2/src/context_store.rs
+++ b/crates/assistant2/src/context_store.rs
@@ -64,6 +64,7 @@ impl ContextStore {
pub fn add_file_from_path(
&mut self,
project_path: ProjectPath,
+ remove_if_exists: bool,
cx: &mut Context,
) -> Task> {
let workspace = self.workspace.clone();
@@ -86,7 +87,9 @@ impl ContextStore {
let already_included = this.update(cx, |this, _cx| {
match this.will_include_buffer(buffer_id, &project_path.path) {
Some(FileInclusion::Direct(context_id)) => {
- this.remove_context(context_id);
+ if remove_if_exists {
+ this.remove_context(context_id);
+ }
true
}
Some(FileInclusion::InDirectory(_)) => true,
@@ -157,6 +160,7 @@ impl ContextStore {
pub fn add_directory(
&mut self,
project_path: ProjectPath,
+ remove_if_exists: bool,
cx: &mut Context,
) -> Task> {
let workspace = self.workspace.clone();
@@ -169,7 +173,9 @@ impl ContextStore {
let already_included = if let Some(context_id) = self.includes_directory(&project_path.path)
{
- self.remove_context(context_id);
+ if remove_if_exists {
+ self.remove_context(context_id);
+ }
true
} else {
false
@@ -256,9 +262,16 @@ impl ContextStore {
)));
}
- pub fn add_thread(&mut self, thread: Entity, cx: &mut Context) {
+ pub fn add_thread(
+ &mut self,
+ thread: Entity,
+ remove_if_exists: bool,
+ cx: &mut Context,
+ ) {
if let Some(context_id) = self.includes_thread(&thread.read(cx).id()) {
- self.remove_context(context_id);
+ if remove_if_exists {
+ self.remove_context(context_id);
+ }
} else {
self.insert_thread(thread, cx);
}
diff --git a/crates/assistant2/src/context_strip.rs b/crates/assistant2/src/context_strip.rs
index e1c5ecfacb..9a56780967 100644
--- a/crates/assistant2/src/context_strip.rs
+++ b/crates/assistant2/src/context_strip.rs
@@ -39,7 +39,6 @@ impl ContextStrip {
pub fn new(
context_store: Entity,
workspace: WeakEntity,
- editor: WeakEntity,
thread_store: Option>,
context_picker_menu_handle: PopoverMenuHandle,
suggest_context_kind: SuggestContextKind,
@@ -51,7 +50,6 @@ impl ContextStrip {
workspace.clone(),
thread_store.clone(),
context_store.downgrade(),
- editor.clone(),
ConfirmBehavior::KeepOpen,
window,
cx,
diff --git a/crates/assistant2/src/inline_prompt_editor.rs b/crates/assistant2/src/inline_prompt_editor.rs
index a2256574f2..45385e25ff 100644
--- a/crates/assistant2/src/inline_prompt_editor.rs
+++ b/crates/assistant2/src/inline_prompt_editor.rs
@@ -861,7 +861,6 @@ impl PromptEditor {
ContextStrip::new(
context_store.clone(),
workspace.clone(),
- prompt_editor.downgrade(),
thread_store.clone(),
context_picker_menu_handle.clone(),
SuggestContextKind::Thread,
@@ -1014,7 +1013,6 @@ impl PromptEditor {
ContextStrip::new(
context_store.clone(),
workspace.clone(),
- prompt_editor.downgrade(),
thread_store.clone(),
context_picker_menu_handle.clone(),
SuggestContextKind::Thread,
diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs
index 4f3e346918..a46024134a 100644
--- a/crates/assistant2/src/message_editor.rs
+++ b/crates/assistant2/src/message_editor.rs
@@ -2,7 +2,7 @@ use std::sync::Arc;
use collections::HashSet;
use editor::actions::MoveUp;
-use editor::{Editor, EditorElement, EditorEvent, EditorStyle};
+use editor::{ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle};
use fs::Fs;
use git::ExpandCommitEditor;
use git_ui::git_panel;
@@ -13,10 +13,8 @@ use gpui::{
use language_model::LanguageModelRegistry;
use language_model_selector::ToggleModelSelector;
use project::Project;
-use rope::Point;
use settings::Settings;
use std::time::Duration;
-use text::Bias;
use theme::ThemeSettings;
use ui::{
prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Tooltip,
@@ -25,7 +23,7 @@ use vim_mode_setting::VimModeSetting;
use workspace::Workspace;
use crate::assistant_model_selector::AssistantModelSelector;
-use crate::context_picker::{ConfirmBehavior, ContextPicker};
+use crate::context_picker::{ConfirmBehavior, ContextPicker, ContextPickerCompletionProvider};
use crate::context_store::{refresh_context_store_text, ContextStore};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::thread::{RequestKind, Thread};
@@ -68,16 +66,30 @@ impl MessageEditor {
let mut editor = Editor::auto_height(10, window, cx);
editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx);
editor.set_show_indent_guides(false, cx);
+ editor.set_context_menu_options(ContextMenuOptions {
+ min_entries_visible: 12,
+ max_entries_visible: 12,
+ placement: Some(ContextMenuPlacement::Above),
+ });
editor
});
+ let editor_entity = editor.downgrade();
+ editor.update(cx, |editor, _| {
+ editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
+ workspace.clone(),
+ context_store.downgrade(),
+ Some(thread_store.clone()),
+ editor_entity,
+ ))));
+ });
+
let inline_context_picker = cx.new(|cx| {
ContextPicker::new(
workspace.clone(),
Some(thread_store.clone()),
context_store.downgrade(),
- editor.downgrade(),
ConfirmBehavior::Close,
window,
cx,
@@ -88,7 +100,6 @@ impl MessageEditor {
ContextStrip::new(
context_store.clone(),
workspace.clone(),
- editor.downgrade(),
Some(thread_store.clone()),
context_picker_menu_handle.clone(),
SuggestContextKind::File,
@@ -98,7 +109,6 @@ impl MessageEditor {
});
let subscriptions = vec![
- cx.subscribe_in(&editor, window, Self::handle_editor_event),
cx.subscribe_in(
&inline_context_picker,
window,
@@ -232,34 +242,6 @@ impl MessageEditor {
.detach();
}
- fn handle_editor_event(
- &mut self,
- editor: &Entity,
- event: &EditorEvent,
- window: &mut Window,
- cx: &mut Context,
- ) {
- match event {
- EditorEvent::SelectionsChanged { .. } => {
- editor.update(cx, |editor, cx| {
- let snapshot = editor.buffer().read(cx).snapshot(cx);
- let newest_cursor = editor.selections.newest::(cx).head();
- if newest_cursor.column > 0 {
- let behind_cursor = snapshot.clip_point(
- Point::new(newest_cursor.row, newest_cursor.column - 1),
- Bias::Left,
- );
- let char_behind_cursor = snapshot.chars_at(behind_cursor).next();
- if char_behind_cursor == Some('@') {
- self.inline_context_picker_menu_handle.show(window, cx);
- }
- }
- });
- }
- _ => {}
- }
- }
-
fn handle_inline_context_picker_event(
&mut self,
_inline_context_picker: &Entity,
@@ -616,6 +598,7 @@ impl Render for MessageEditor {
background: editor_bg_color,
local_player: cx.theme().players().local(),
text: text_style,
+ syntax: cx.theme().syntax().clone(),
..Default::default()
},
)
diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs
index e19c7cecfb..182713cd26 100644
--- a/crates/assistant2/src/thread.rs
+++ b/crates/assistant2/src/thread.rs
@@ -23,7 +23,6 @@ use project::{Project, Worktree};
use prompt_store::{
AssistantSystemPromptContext, PromptBuilder, RulesFile, WorktreeInfoForSystemPrompt,
};
-use scripting_tool::{ScriptingSession, ScriptingTool};
use serde::{Deserialize, Serialize};
use settings::Settings;
use util::{maybe, post_inc, ResultExt as _, TryFutureExt as _};
@@ -34,7 +33,7 @@ use crate::thread_store::{
SerializedMessage, SerializedMessageSegment, SerializedThread, SerializedToolResult,
SerializedToolUse,
};
-use crate::tool_use::{PendingToolUse, PendingToolUseStatus, ToolType, ToolUse, ToolUseState};
+use crate::tool_use::{PendingToolUse, ToolUse, ToolUseState};
#[derive(Debug, Clone, Copy)]
pub enum RequestKind {
@@ -194,8 +193,6 @@ pub struct Thread {
action_log: Entity,
last_restore_checkpoint: Option,
pending_checkpoint: Option,
- scripting_session: Entity,
- scripting_tool_use: ToolUseState,
initial_project_snapshot: Shared>>>,
cumulative_token_usage: TokenUsage,
feedback: Option,
@@ -230,8 +227,6 @@ impl Thread {
last_restore_checkpoint: None,
pending_checkpoint: None,
tool_use: ToolUseState::new(tools.clone()),
- scripting_session: cx.new(|cx| ScriptingSession::new(project.clone(), cx)),
- scripting_tool_use: ToolUseState::new(tools),
action_log: cx.new(|_| ActionLog::new()),
initial_project_snapshot: {
let project_snapshot = Self::project_snapshot(project.clone(), cx);
@@ -274,14 +269,7 @@ impl Thread {
.unwrap_or(0),
);
let tool_use =
- ToolUseState::from_serialized_messages(tools.clone(), &serialized.messages, |name| {
- name != ScriptingTool::NAME
- });
- let scripting_tool_use =
- ToolUseState::from_serialized_messages(tools.clone(), &serialized.messages, |name| {
- name == ScriptingTool::NAME
- });
- let scripting_session = cx.new(|cx| ScriptingSession::new(project.clone(), cx));
+ ToolUseState::from_serialized_messages(tools.clone(), &serialized.messages, |_| true);
let mut this = Self {
id,
@@ -320,8 +308,6 @@ impl Thread {
tools,
tool_use,
action_log: cx.new(|_| ActionLog::new()),
- scripting_session,
- scripting_tool_use,
initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(),
// TODO: persist token usage?
cumulative_token_usage: TokenUsage::default(),
@@ -384,37 +370,17 @@ impl Thread {
.pending_tool_uses()
.into_iter()
.find(|tool_use| &tool_use.id == id)
- .or_else(|| {
- self.scripting_tool_use
- .pending_tool_uses()
- .into_iter()
- .find(|tool_use| &tool_use.id == id)
- })
}
- pub fn tools_needing_confirmation(&self) -> impl Iterator- {
+ pub fn tools_needing_confirmation(&self) -> impl Iterator
- {
self.tool_use
.pending_tool_uses()
.into_iter()
- .filter_map(|tool_use| {
- if let PendingToolUseStatus::NeedsConfirmation(confirmation) = &tool_use.status {
- Some((confirmation.tool_type.clone(), tool_use))
- } else {
- None
- }
- })
- .chain(
- self.scripting_tool_use
- .pending_tool_uses()
- .into_iter()
- .filter_map(|tool_use| {
- if tool_use.status.needs_confirmation() {
- Some((ToolType::ScriptingTool, tool_use))
- } else {
- None
- }
- }),
- )
+ .filter(|tool_use| tool_use.status.needs_confirmation())
+ }
+
+ pub fn has_pending_tool_uses(&self) -> bool {
+ !self.tool_use.pending_tool_uses().is_empty()
}
pub fn checkpoint_for_message(&self, id: MessageId) -> Option {
@@ -547,25 +513,18 @@ impl Thread {
/// Returns whether all of the tool uses have finished running.
pub fn all_tools_finished(&self) -> bool {
- let mut all_pending_tool_uses = self
- .tool_use
- .pending_tool_uses()
- .into_iter()
- .chain(self.scripting_tool_use.pending_tool_uses());
-
// If the only pending tool uses left are the ones with errors, then
// that means that we've finished running all of the pending tools.
- all_pending_tool_uses.all(|tool_use| tool_use.status.is_error())
+ self.tool_use
+ .pending_tool_uses()
+ .iter()
+ .all(|tool_use| tool_use.status.is_error())
}
pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec {
self.tool_use.tool_uses_for_message(id, cx)
}
- pub fn scripting_tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec {
- self.scripting_tool_use.tool_uses_for_message(id, cx)
- }
-
pub fn tool_results_for_message(&self, id: MessageId) -> Vec<&LanguageModelToolResult> {
self.tool_use.tool_results_for_message(id)
}
@@ -574,21 +533,10 @@ impl Thread {
self.tool_use.tool_result(id)
}
- pub fn scripting_tool_results_for_message(
- &self,
- id: MessageId,
- ) -> Vec<&LanguageModelToolResult> {
- self.scripting_tool_use.tool_results_for_message(id)
- }
-
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
self.tool_use.message_has_tool_results(message_id)
}
- pub fn message_has_scripting_tool_results(&self, message_id: MessageId) -> bool {
- self.scripting_tool_use.message_has_tool_results(message_id)
- }
-
pub fn insert_user_message(
&mut self,
text: impl Into,
@@ -709,7 +657,6 @@ impl Thread {
tool_uses: this
.tool_uses_for_message(message.id, cx)
.into_iter()
- .chain(this.scripting_tool_uses_for_message(message.id, cx))
.map(|tool_use| SerializedToolUse {
id: tool_use.id,
name: tool_use.name,
@@ -719,7 +666,6 @@ impl Thread {
tool_results: this
.tool_results_for_message(message.id)
.into_iter()
- .chain(this.scripting_tool_results_for_message(message.id))
.map(|tool_result| SerializedToolResult {
tool_use_id: tool_result.tool_use_id.clone(),
is_error: tool_result.is_error,
@@ -783,11 +729,12 @@ impl Thread {
// Note that Cline supports `.clinerules` being a directory, but that is not currently
// supported. This doesn't seem to occur often in GitHub repositories.
- const RULES_FILE_NAMES: [&'static str; 5] = [
+ const RULES_FILE_NAMES: [&'static str; 6] = [
".rules",
".cursorrules",
".windsurfrules",
".clinerules",
+ ".github/copilot-instructions.md",
"CLAUDE.md",
];
let selected_rules_file = RULES_FILE_NAMES
@@ -852,15 +799,6 @@ impl Thread {
let mut request = self.to_completion_request(request_kind, cx);
request.tools = {
let mut tools = Vec::new();
-
- if self.tools.is_scripting_tool_enabled() {
- tools.push(LanguageModelRequestTool {
- name: ScriptingTool::NAME.into(),
- description: ScriptingTool::DESCRIPTION.into(),
- input_schema: ScriptingTool::input_schema(),
- });
- }
-
tools.extend(self.tools().enabled_tools(cx).into_iter().map(|tool| {
LanguageModelRequestTool {
name: tool.name(),
@@ -921,8 +859,6 @@ impl Thread {
RequestKind::Chat => {
self.tool_use
.attach_tool_results(message.id, &mut request_message);
- self.scripting_tool_use
- .attach_tool_results(message.id, &mut request_message);
}
RequestKind::Summarize => {
// We don't care about tool use during summarization.
@@ -939,8 +875,6 @@ impl Thread {
RequestKind::Chat => {
self.tool_use
.attach_tool_uses(message.id, &mut request_message);
- self.scripting_tool_use
- .attach_tool_uses(message.id, &mut request_message);
}
RequestKind::Summarize => {
// We don't care about tool use during summarization.
@@ -1087,19 +1021,11 @@ impl Thread {
.iter()
.rfind(|message| message.role == Role::Assistant)
{
- if tool_use.name.as_ref() == ScriptingTool::NAME {
- thread.scripting_tool_use.request_tool_use(
- last_assistant_message.id,
- tool_use,
- cx,
- );
- } else {
- thread.tool_use.request_tool_use(
- last_assistant_message.id,
- tool_use,
- cx,
- );
- }
+ thread.tool_use.request_tool_use(
+ last_assistant_message.id,
+ tool_use,
+ cx,
+ );
}
}
}
@@ -1264,15 +1190,16 @@ impl Thread {
tool_use.ui_text.clone(),
tool_use.input.clone(),
messages.clone(),
- ToolType::NonScriptingTool(tool),
+ tool,
);
+ cx.emit(ThreadEvent::ToolConfirmationNeeded);
} else {
self.run_tool(
tool_use.id.clone(),
tool_use.ui_text.clone(),
tool_use.input.clone(),
&messages,
- ToolType::NonScriptingTool(tool),
+ tool,
cx,
);
}
@@ -1282,33 +1209,13 @@ impl Thread {
tool_use.ui_text.clone(),
tool_use.input.clone(),
&messages,
- ToolType::NonScriptingTool(tool),
+ tool,
cx,
);
}
}
- let pending_scripting_tool_uses = self
- .scripting_tool_use
- .pending_tool_uses()
- .into_iter()
- .filter(|tool_use| tool_use.status.is_idle())
- .cloned()
- .collect::>();
-
- for scripting_tool_use in pending_scripting_tool_uses.iter() {
- self.scripting_tool_use.confirm_tool_use(
- scripting_tool_use.id.clone(),
- scripting_tool_use.ui_text.clone(),
- scripting_tool_use.input.clone(),
- messages.clone(),
- ToolType::ScriptingTool,
- );
- }
-
pending_tool_uses
- .into_iter()
- .chain(pending_scripting_tool_uses)
}
pub fn run_tool(
@@ -1317,21 +1224,12 @@ impl Thread {
ui_text: impl Into,
input: serde_json::Value,
messages: &[LanguageModelRequestMessage],
- tool_type: ToolType,
+ tool: Arc,
cx: &mut Context<'_, Thread>,
) {
- match tool_type {
- ToolType::ScriptingTool => {
- let task = self.spawn_scripting_tool_use(tool_use_id.clone(), input, cx);
- self.scripting_tool_use
- .run_pending_tool(tool_use_id, ui_text.into(), task);
- }
- ToolType::NonScriptingTool(tool) => {
- let task = self.spawn_tool_use(tool_use_id.clone(), messages, input, tool, cx);
- self.tool_use
- .run_pending_tool(tool_use_id, ui_text.into(), task);
- }
- }
+ let task = self.spawn_tool_use(tool_use_id.clone(), messages, input, tool, cx);
+ self.tool_use
+ .run_pending_tool(tool_use_id, ui_text.into(), task);
}
fn spawn_tool_use(
@@ -1375,60 +1273,6 @@ impl Thread {
})
}
- fn spawn_scripting_tool_use(
- &mut self,
- tool_use_id: LanguageModelToolUseId,
- input: serde_json::Value,
- cx: &mut Context,
- ) -> Task<()> {
- let task = match ScriptingTool::deserialize_input(input) {
- Err(err) => Task::ready(Err(err.into())),
- Ok(input) => {
- let (script_id, script_task) =
- self.scripting_session.update(cx, move |session, cx| {
- session.run_script(input.lua_script, cx)
- });
-
- let session = self.scripting_session.clone();
- cx.spawn(async move |_, cx| {
- script_task.await;
-
- let message = session.read_with(cx, |session, _cx| {
- // Using a id to get the script output seems impractical.
- // Why not just include it in the Task result?
- // This is because we'll later report the script state as it runs,
- session
- .get(script_id)
- .output_message_for_llm()
- .expect("Script shouldn't still be running")
- })?;
-
- Ok(message)
- })
- }
- };
-
- cx.spawn({
- let tool_use_id = tool_use_id.clone();
- async move |thread, cx| {
- let output = task.await;
- thread
- .update(cx, |thread, cx| {
- let pending_tool_use = thread
- .scripting_tool_use
- .insert_tool_output(tool_use_id.clone(), output);
-
- cx.emit(ThreadEvent::ToolFinished {
- tool_use_id,
- pending_tool_use,
- canceled: false,
- });
- })
- .ok();
- }
- })
- }
-
pub fn attach_tool_results(
&mut self,
updated_context: Vec,
@@ -1730,22 +1574,12 @@ impl Thread {
self.cumulative_token_usage.clone()
}
- pub fn deny_tool_use(
- &mut self,
- tool_use_id: LanguageModelToolUseId,
- tool_type: ToolType,
- cx: &mut Context,
- ) {
+ pub fn deny_tool_use(&mut self, tool_use_id: LanguageModelToolUseId, cx: &mut Context) {
let err = Err(anyhow::anyhow!(
"Permission to run tool action denied by user"
));
- if let ToolType::ScriptingTool = tool_type {
- self.scripting_tool_use
- .insert_tool_output(tool_use_id.clone(), err);
- } else {
- self.tool_use.insert_tool_output(tool_use_id.clone(), err);
- }
+ self.tool_use.insert_tool_output(tool_use_id.clone(), err);
cx.emit(ThreadEvent::ToolFinished {
tool_use_id,
@@ -1786,6 +1620,7 @@ pub enum ThreadEvent {
canceled: bool,
},
CheckpointChanged,
+ ToolConfirmationNeeded,
}
impl EventEmitter for Thread {}
diff --git a/crates/assistant2/src/tool_selector.rs b/crates/assistant2/src/tool_selector.rs
index 1c0aae33fa..ffe4f533cf 100644
--- a/crates/assistant2/src/tool_selector.rs
+++ b/crates/assistant2/src/tool_selector.rs
@@ -4,7 +4,6 @@ use assistant_settings::{AgentProfile, AssistantSettings};
use assistant_tool::{ToolSource, ToolWorkingSet};
use gpui::{Entity, Subscription};
use indexmap::IndexMap;
-use scripting_tool::ScriptingTool;
use settings::{Settings as _, SettingsStore};
use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
@@ -51,8 +50,8 @@ impl ToolSelector {
menu = menu.toggleable_entry(profile.name.clone(), false, icon_position, None, {
let tools = tool_set.clone();
move |_window, cx| {
- tools.disable_source(ToolSource::Native, cx);
- tools.disable_scripting_tool();
+ tools.disable_all_tools(cx);
+
tools.enable(
ToolSource::Native,
&profile
@@ -62,8 +61,17 @@ impl ToolSelector {
.collect::>(),
);
- if profile.tools.contains_key(ScriptingTool::NAME) {
- tools.enable_scripting_tool();
+ for (context_server_id, preset) in &profile.context_servers {
+ tools.enable(
+ ToolSource::ContextServer {
+ id: context_server_id.clone().into(),
+ },
+ &preset
+ .tools
+ .iter()
+ .filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
+ .collect::>(),
+ )
}
}
});
@@ -98,11 +106,6 @@ impl ToolSelector {
.collect::>();
if ToolSource::Native == source {
- tools.push((
- ToolSource::Native,
- ScriptingTool::NAME.into(),
- tool_set.is_scripting_tool_enabled(),
- ));
tools.sort_by(|(_, name_a, _), (_, name_b, _)| name_a.cmp(name_b));
}
@@ -136,18 +139,10 @@ impl ToolSelector {
menu = menu.toggleable_entry(name.clone(), is_enabled, icon_position, None, {
let tools = tool_set.clone();
move |_window, _cx| {
- if name.as_ref() == ScriptingTool::NAME {
- if is_enabled {
- tools.disable_scripting_tool();
- } else {
- tools.enable_scripting_tool();
- }
+ if is_enabled {
+ tools.disable(source.clone(), &[name.clone()]);
} else {
- if is_enabled {
- tools.disable(source.clone(), &[name.clone()]);
- } else {
- tools.enable(source.clone(), &[name.clone()]);
- }
+ tools.enable(source.clone(), &[name.clone()]);
}
}
});
diff --git a/crates/assistant2/src/tool_use.rs b/crates/assistant2/src/tool_use.rs
index 7b7399c950..d13c711d85 100644
--- a/crates/assistant2/src/tool_use.rs
+++ b/crates/assistant2/src/tool_use.rs
@@ -10,7 +10,7 @@ use language_model::{
LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
LanguageModelToolUseId, MessageContent, Role,
};
-use scripting_tool::ScriptingTool;
+use ui::IconName;
use crate::thread::MessageId;
use crate::thread_store::SerializedMessage;
@@ -22,6 +22,7 @@ pub struct ToolUse {
pub ui_text: SharedString,
pub status: ToolUseStatus,
pub input: serde_json::Value,
+ pub icon: ui::IconName,
}
#[derive(Debug, Clone)]
@@ -180,12 +181,19 @@ impl ToolUseState {
}
})();
+ let icon = if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
+ tool.icon()
+ } else {
+ IconName::Cog
+ };
+
tool_uses.push(ToolUse {
id: tool_use.id.clone(),
name: tool_use.name.clone().into(),
ui_text: self.tool_ui_label(&tool_use.name, &tool_use.input, cx),
input: tool_use.input.clone(),
status,
+ icon,
})
}
@@ -200,8 +208,6 @@ impl ToolUseState {
) -> SharedString {
if let Some(tool) = self.tools.tool(tool_name, cx) {
tool.ui_text(input).into()
- } else if tool_name == ScriptingTool::NAME {
- "Run Lua Script".into()
} else {
"Unknown tool".into()
}
@@ -285,7 +291,7 @@ impl ToolUseState {
ui_text: impl Into>,
input: serde_json::Value,
messages: Arc>,
- tool_type: ToolType,
+ tool: Arc,
) {
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
let ui_text = ui_text.into();
@@ -294,7 +300,7 @@ impl ToolUseState {
tool_use_id,
input,
messages,
- tool_type,
+ tool,
ui_text,
};
tool_use.status = PendingToolUseStatus::NeedsConfirmation(Arc::new(confirmation));
@@ -398,19 +404,13 @@ pub struct PendingToolUse {
pub status: PendingToolUseStatus,
}
-#[derive(Debug, Clone)]
-pub enum ToolType {
- ScriptingTool,
- NonScriptingTool(Arc),
-}
-
#[derive(Debug, Clone)]
pub struct Confirmation {
pub tool_use_id: LanguageModelToolUseId,
pub input: serde_json::Value,
pub ui_text: Arc,
pub messages: Arc>,
- pub tool_type: ToolType,
+ pub tool: Arc,
}
#[derive(Debug, Clone)]
diff --git a/crates/assistant2/src/ui.rs b/crates/assistant2/src/ui.rs
index b10c09b300..390a6f8edc 100644
--- a/crates/assistant2/src/ui.rs
+++ b/crates/assistant2/src/ui.rs
@@ -1,3 +1,5 @@
mod context_pill;
+mod tool_ready_pop_up;
pub use context_pill::*;
+pub use tool_ready_pop_up::*;
diff --git a/crates/assistant2/src/ui/tool_ready_pop_up.rs b/crates/assistant2/src/ui/tool_ready_pop_up.rs
new file mode 100644
index 0000000000..4a43e29113
--- /dev/null
+++ b/crates/assistant2/src/ui/tool_ready_pop_up.rs
@@ -0,0 +1,115 @@
+use gpui::{
+ point, App, Context, EventEmitter, IntoElement, PlatformDisplay, Size, Window,
+ WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
+};
+use release_channel::ReleaseChannel;
+use std::rc::Rc;
+use theme;
+use ui::{prelude::*, Render};
+
+pub struct ToolReadyPopUp {
+ caption: SharedString,
+}
+
+impl ToolReadyPopUp {
+ pub fn new(caption: impl Into) -> Self {
+ Self {
+ caption: caption.into(),
+ }
+ }
+
+ pub fn window_options(screen: Rc, cx: &App) -> WindowOptions {
+ let size = Size {
+ width: px(440.),
+ height: px(72.),
+ };
+
+ let notification_margin_width = px(16.);
+ let notification_margin_height = px(-48.);
+
+ let bounds = gpui::Bounds:: {
+ origin: screen.bounds().top_right()
+ - point(
+ size.width + notification_margin_width,
+ notification_margin_height,
+ ),
+ size,
+ };
+
+ let app_id = ReleaseChannel::global(cx).app_id();
+
+ WindowOptions {
+ window_bounds: Some(WindowBounds::Windowed(bounds)),
+ titlebar: None,
+ focus: false,
+ show: true,
+ kind: WindowKind::PopUp,
+ is_movable: false,
+ display_id: Some(screen.id()),
+ window_background: WindowBackgroundAppearance::Transparent,
+ app_id: Some(app_id.to_owned()),
+ window_min_size: None,
+ window_decorations: Some(WindowDecorations::Client),
+ }
+ }
+}
+
+pub enum ToolReadyPopupEvent {
+ Accepted,
+ Dismissed,
+}
+
+impl EventEmitter for ToolReadyPopUp {}
+
+impl Render for ToolReadyPopUp {
+ fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement {
+ let ui_font = theme::setup_ui_font(window, cx);
+ let line_height = window.line_height();
+
+ h_flex()
+ .size_full()
+ .p_3()
+ .gap_4()
+ .justify_between()
+ .elevation_3(cx)
+ .text_ui(cx)
+ .font(ui_font)
+ .border_color(cx.theme().colors().border)
+ .rounded_xl()
+ .child(
+ h_flex()
+ .items_start()
+ .gap_2()
+ .child(
+ h_flex().h(line_height).justify_center().child(
+ Icon::new(IconName::Info)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ ),
+ )
+ .child(
+ v_flex()
+ .child(Headline::new("Agent Panel").size(HeadlineSize::XSmall))
+ .child(Label::new(self.caption.clone()).color(Color::Muted)),
+ ),
+ )
+ .child(
+ h_flex()
+ .gap_0p5()
+ .child(
+ Button::new("open", "View Panel")
+ .style(ButtonStyle::Tinted(ui::TintColor::Accent))
+ .on_click({
+ cx.listener(move |_this, _event, _, cx| {
+ cx.emit(ToolReadyPopupEvent::Accepted);
+ })
+ }),
+ )
+ .child(Button::new("dismiss", "Dismiss").on_click({
+ cx.listener(move |_, _event, _, cx| {
+ cx.emit(ToolReadyPopupEvent::Dismissed);
+ })
+ })),
+ )
+ }
+}
diff --git a/crates/assistant_context_editor/src/context_editor.rs b/crates/assistant_context_editor/src/context_editor.rs
index 8fc2433476..214001d13f 100644
--- a/crates/assistant_context_editor/src/context_editor.rs
+++ b/crates/assistant_context_editor/src/context_editor.rs
@@ -13,7 +13,7 @@ use editor::{
BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
},
- scroll::{Autoscroll, AutoscrollStrategy},
+ scroll::Autoscroll,
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, ProposedChangeLocation,
ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
};
@@ -414,12 +414,9 @@ impl ContextEditor {
cursor..cursor
};
self.editor.update(cx, |editor, cx| {
- editor.change_selections(
- Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
- window,
- cx,
- |selections| selections.select_ranges([new_selection]),
- );
+ editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
+ selections.select_ranges([new_selection])
+ });
});
// Avoid scrolling to the new cursor position so the assistant's output is stable.
cx.defer_in(window, |this, _, _| this.scroll_position = None);
diff --git a/crates/assistant_context_editor/src/slash_command.rs b/crates/assistant_context_editor/src/slash_command.rs
index d5139c8f37..a3897e3235 100644
--- a/crates/assistant_context_editor/src/slash_command.rs
+++ b/crates/assistant_context_editor/src/slash_command.rs
@@ -2,7 +2,7 @@ use crate::context_editor::ContextEditor;
use anyhow::Result;
pub use assistant_slash_command::SlashCommand;
use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWorkingSet};
-use editor::{CompletionProvider, Editor};
+use editor::{CompletionProvider, Editor, ExcerptId};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window};
use language::{Anchor, Buffer, ToPoint};
@@ -126,6 +126,7 @@ impl SlashCommandCompletionProvider {
)),
new_text,
label: command.label(cx),
+ icon_path: None,
confirm,
source: CompletionSource::Custom,
})
@@ -223,6 +224,7 @@ impl SlashCommandCompletionProvider {
last_argument_range.clone()
},
label: new_argument.label,
+ icon_path: None,
new_text,
documentation: None,
confirm,
@@ -241,6 +243,7 @@ impl SlashCommandCompletionProvider {
impl CompletionProvider for SlashCommandCompletionProvider {
fn completions(
&self,
+ _excerpt_id: ExcerptId,
buffer: &Entity,
buffer_position: Anchor,
_: editor::CompletionContext,
diff --git a/crates/assistant_settings/src/agent_profile.rs b/crates/assistant_settings/src/agent_profile.rs
index f208568916..2b7c91cd9f 100644
--- a/crates/assistant_settings/src/agent_profile.rs
+++ b/crates/assistant_settings/src/agent_profile.rs
@@ -9,12 +9,10 @@ pub struct AgentProfile {
/// The name of the profile.
pub name: SharedString,
pub tools: IndexMap, bool>,
- #[allow(dead_code)]
pub context_servers: IndexMap, ContextServerPreset>,
}
#[derive(Debug, Clone)]
pub struct ContextServerPreset {
- #[allow(dead_code)]
pub tools: IndexMap, bool>,
}
diff --git a/crates/assistant_settings/src/assistant_settings.rs b/crates/assistant_settings/src/assistant_settings.rs
index 1e37ef91c0..c937e75fe5 100644
--- a/crates/assistant_settings/src/assistant_settings.rs
+++ b/crates/assistant_settings/src/assistant_settings.rs
@@ -73,6 +73,7 @@ pub struct AssistantSettings {
pub enable_experimental_live_diffs: bool,
pub profiles: IndexMap, AgentProfile>,
pub always_allow_tool_actions: bool,
+ pub notify_when_agent_waiting: bool,
}
impl AssistantSettings {
@@ -175,6 +176,7 @@ impl AssistantSettingsContent {
enable_experimental_live_diffs: None,
profiles: None,
always_allow_tool_actions: None,
+ notify_when_agent_waiting: None,
},
VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
},
@@ -198,6 +200,7 @@ impl AssistantSettingsContent {
enable_experimental_live_diffs: None,
profiles: None,
always_allow_tool_actions: None,
+ notify_when_agent_waiting: None,
},
}
}
@@ -329,6 +332,7 @@ impl Default for VersionedAssistantSettingsContent {
enable_experimental_live_diffs: None,
profiles: None,
always_allow_tool_actions: None,
+ notify_when_agent_waiting: None,
})
}
}
@@ -372,6 +376,10 @@ pub struct AssistantSettingsContentV2 {
///
/// Default: false
always_allow_tool_actions: Option,
+ /// Whether to show a popup notification when the agent is waiting for user input.
+ ///
+ /// Default: true
+ notify_when_agent_waiting: Option,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
@@ -412,6 +420,13 @@ impl Default for LanguageModelSelection {
pub struct AgentProfileContent {
pub name: Arc,
pub tools: IndexMap, bool>,
+ #[serde(default)]
+ pub context_servers: IndexMap, ContextServerPresetContent>,
+}
+
+#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
+pub struct ContextServerPresetContent {
+ pub tools: IndexMap, bool>,
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
@@ -512,6 +527,10 @@ impl Settings for AssistantSettings {
&mut settings.always_allow_tool_actions,
value.always_allow_tool_actions,
);
+ merge(
+ &mut settings.notify_when_agent_waiting,
+ value.notify_when_agent_waiting,
+ );
if let Some(profiles) = value.profiles {
settings
@@ -522,7 +541,18 @@ impl Settings for AssistantSettings {
AgentProfile {
name: profile.name.into(),
tools: profile.tools,
- context_servers: IndexMap::default(),
+ context_servers: profile
+ .context_servers
+ .into_iter()
+ .map(|(context_server_id, preset)| {
+ (
+ context_server_id,
+ ContextServerPreset {
+ tools: preset.tools.clone(),
+ },
+ )
+ })
+ .collect(),
},
)
}));
@@ -593,6 +623,7 @@ mod tests {
enable_experimental_live_diffs: None,
profiles: None,
always_allow_tool_actions: None,
+ notify_when_agent_waiting: None,
}),
)
},
diff --git a/crates/assistant_tool/Cargo.toml b/crates/assistant_tool/Cargo.toml
index 040a906bf3..73ee3ffccb 100644
--- a/crates/assistant_tool/Cargo.toml
+++ b/crates/assistant_tool/Cargo.toml
@@ -13,10 +13,11 @@ path = "src/assistant_tool.rs"
[dependencies]
anyhow.workspace = true
-collections.workspace = true
clock.workspace = true
+collections.workspace = true
derive_more.workspace = true
gpui.workspace = true
+icons.workspace = true
language.workspace = true
language_model.workspace = true
parking_lot.workspace = true
diff --git a/crates/assistant_tool/src/assistant_tool.rs b/crates/assistant_tool/src/assistant_tool.rs
index 20fdb05439..906903acbb 100644
--- a/crates/assistant_tool/src/assistant_tool.rs
+++ b/crates/assistant_tool/src/assistant_tool.rs
@@ -1,14 +1,16 @@
mod tool_registry;
mod tool_working_set;
+use std::fmt::{self, Debug, Formatter};
+use std::sync::Arc;
+
use anyhow::Result;
use collections::{HashMap, HashSet};
use gpui::{App, Context, Entity, SharedString, Task};
+use icons::IconName;
use language::Buffer;
use language_model::LanguageModelRequestMessage;
use project::Project;
-use std::fmt::{self, Debug, Formatter};
-use std::sync::Arc;
pub use crate::tool_registry::*;
pub use crate::tool_working_set::*;
@@ -33,6 +35,9 @@ pub trait Tool: 'static + Send + Sync {
/// Returns the description of the tool.
fn description(&self) -> String;
+ /// Returns the icon for the tool.
+ fn icon(&self) -> IconName;
+
/// Returns the source of the tool.
fn source(&self) -> ToolSource {
ToolSource::Native
diff --git a/crates/assistant_tool/src/tool_working_set.rs b/crates/assistant_tool/src/tool_working_set.rs
index 74d4d4d932..2c1acad053 100644
--- a/crates/assistant_tool/src/tool_working_set.rs
+++ b/crates/assistant_tool/src/tool_working_set.rs
@@ -15,26 +15,14 @@ pub struct ToolWorkingSet {
state: Mutex,
}
+#[derive(Default)]
struct WorkingSetState {
context_server_tools_by_id: HashMap>,
context_server_tools_by_name: HashMap>,
disabled_tools_by_source: HashMap>>,
- is_scripting_tool_disabled: bool,
next_tool_id: ToolId,
}
-impl Default for WorkingSetState {
- fn default() -> Self {
- Self {
- context_server_tools_by_id: HashMap::default(),
- context_server_tools_by_name: HashMap::default(),
- disabled_tools_by_source: HashMap::default(),
- is_scripting_tool_disabled: true,
- next_tool_id: ToolId::default(),
- }
- }
-}
-
impl ToolWorkingSet {
pub fn tool(&self, name: &str, cx: &App) -> Option> {
self.state
@@ -55,7 +43,7 @@ impl ToolWorkingSet {
pub fn are_all_tools_enabled(&self) -> bool {
let state = self.state.lock();
- state.disabled_tools_by_source.is_empty() && !state.is_scripting_tool_disabled
+ state.disabled_tools_by_source.is_empty()
}
pub fn are_all_tools_from_source_enabled(&self, source: &ToolSource) -> bool {
@@ -70,7 +58,6 @@ impl ToolWorkingSet {
pub fn enable_all_tools(&self) {
let mut state = self.state.lock();
state.disabled_tools_by_source.clear();
- state.enable_scripting_tool();
}
pub fn disable_all_tools(&self, cx: &App) {
@@ -124,21 +111,6 @@ impl ToolWorkingSet {
.retain(|id, _| !tool_ids_to_remove.contains(id));
state.tools_changed();
}
-
- pub fn is_scripting_tool_enabled(&self) -> bool {
- let state = self.state.lock();
- !state.is_scripting_tool_disabled
- }
-
- pub fn enable_scripting_tool(&self) {
- let mut state = self.state.lock();
- state.enable_scripting_tool();
- }
-
- pub fn disable_scripting_tool(&self) {
- let mut state = self.state.lock();
- state.disable_scripting_tool();
- }
}
impl WorkingSetState {
@@ -240,15 +212,5 @@ impl WorkingSetState {
self.disable(source, &tool_names);
}
-
- self.disable_scripting_tool();
- }
-
- fn enable_scripting_tool(&mut self) {
- self.is_scripting_tool_disabled = false;
- }
-
- fn disable_scripting_tool(&mut self) {
- self.is_scripting_tool_disabled = true;
}
}
diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs
index 53611c8fbf..5d2b2965e6 100644
--- a/crates/assistant_tools/src/assistant_tools.rs
+++ b/crates/assistant_tools/src/assistant_tools.rs
@@ -1,28 +1,35 @@
mod bash_tool;
+mod copy_path_tool;
+mod create_file_tool;
mod delete_path_tool;
mod diagnostics_tool;
mod edit_files_tool;
mod fetch_tool;
+mod find_replace_file_tool;
mod list_directory_tool;
mod move_path_tool;
mod now_tool;
mod path_search_tool;
mod read_file_tool;
mod regex_search_tool;
+mod replace;
mod thinking_tool;
use std::sync::Arc;
use assistant_tool::ToolRegistry;
+use copy_path_tool::CopyPathTool;
use gpui::App;
use http_client::HttpClientWithUrl;
use move_path_tool::MovePathTool;
use crate::bash_tool::BashTool;
+use crate::create_file_tool::CreateFileTool;
use crate::delete_path_tool::DeletePathTool;
use crate::diagnostics_tool::DiagnosticsTool;
use crate::edit_files_tool::EditFilesTool;
use crate::fetch_tool::FetchTool;
+use crate::find_replace_file_tool::FindReplaceFileTool;
use crate::list_directory_tool::ListDirectoryTool;
use crate::now_tool::NowTool;
use crate::path_search_tool::PathSearchTool;
@@ -36,7 +43,10 @@ pub fn init(http_client: Arc, cx: &mut App) {
let registry = ToolRegistry::global(cx);
registry.register_tool(BashTool);
+ registry.register_tool(CreateFileTool);
+ registry.register_tool(CopyPathTool);
registry.register_tool(DeletePathTool);
+ registry.register_tool(FindReplaceFileTool);
registry.register_tool(MovePathTool);
registry.register_tool(DiagnosticsTool);
registry.register_tool(EditFilesTool);
diff --git a/crates/assistant_tools/src/bash_tool.rs b/crates/assistant_tools/src/bash_tool.rs
index 51c8a024b3..19cf5c198c 100644
--- a/crates/assistant_tools/src/bash_tool.rs
+++ b/crates/assistant_tools/src/bash_tool.rs
@@ -6,6 +6,7 @@ use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
+use ui::IconName;
use util::command::new_smol_command;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@@ -31,6 +32,10 @@ impl Tool for BashTool {
include_str!("./bash_tool/description.md").to_string()
}
+ fn icon(&self) -> IconName {
+ IconName::Terminal
+ }
+
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(BashToolInput);
serde_json::to_value(&schema).unwrap()
@@ -38,7 +43,7 @@ impl Tool for BashTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::(input.clone()) {
- Ok(input) => format!("`$ {}`", input.command),
+ Ok(input) => format!("`{}`", input.command),
Err(_) => "Run bash command".to_string(),
}
}
diff --git a/crates/assistant_tools/src/copy_path_tool.rs b/crates/assistant_tools/src/copy_path_tool.rs
new file mode 100644
index 0000000000..d4cb85421b
--- /dev/null
+++ b/crates/assistant_tools/src/copy_path_tool.rs
@@ -0,0 +1,119 @@
+use anyhow::{anyhow, Result};
+use assistant_tool::{ActionLog, Tool};
+use gpui::{App, AppContext, Entity, Task};
+use language_model::LanguageModelRequestMessage;
+use project::Project;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use std::sync::Arc;
+use ui::IconName;
+
+#[derive(Debug, Serialize, Deserialize, JsonSchema)]
+pub struct CopyPathToolInput {
+ /// The source path of the file or directory to copy.
+ /// If a directory is specified, its contents will be copied recursively (like `cp -r`).
+ ///
+ ///
+ /// If the project has the following files:
+ ///
+ /// - directory1/a/something.txt
+ /// - directory2/a/things.txt
+ /// - directory3/a/other.txt
+ ///
+ /// You can copy the first file by providing a source_path of "directory1/a/something.txt"
+ ///
+ pub source_path: String,
+
+ /// The destination path where the file or directory should be copied to.
+ ///
+ ///
+ /// To copy "directory1/a/something.txt" to "directory2/b/copy.txt",
+ /// provide a destination_path of "directory2/b/copy.txt"
+ ///
+ pub destination_path: String,
+}
+
+pub struct CopyPathTool;
+
+impl Tool for CopyPathTool {
+ fn name(&self) -> String {
+ "copy-path".into()
+ }
+
+ fn needs_confirmation(&self) -> bool {
+ true
+ }
+
+ fn description(&self) -> String {
+ include_str!("./copy_path_tool/description.md").into()
+ }
+
+ fn icon(&self) -> IconName {
+ IconName::Clipboard
+ }
+
+ fn input_schema(&self) -> serde_json::Value {
+ let schema = schemars::schema_for!(CopyPathToolInput);
+ serde_json::to_value(&schema).unwrap()
+ }
+
+ fn ui_text(&self, input: &serde_json::Value) -> String {
+ match serde_json::from_value::(input.clone()) {
+ Ok(input) => {
+ let src = input.source_path.as_str();
+ let dest = input.destination_path.as_str();
+ format!("Copy `{src}` to `{dest}`")
+ }
+ Err(_) => "Copy path".to_string(),
+ }
+ }
+
+ fn run(
+ self: Arc,
+ input: serde_json::Value,
+ _messages: &[LanguageModelRequestMessage],
+ project: Entity,
+ _action_log: Entity,
+ cx: &mut App,
+ ) -> Task> {
+ let input = match serde_json::from_value::(input) {
+ Ok(input) => input,
+ Err(err) => return Task::ready(Err(anyhow!(err))),
+ };
+ let copy_task = project.update(cx, |project, cx| {
+ match project
+ .find_project_path(&input.source_path, cx)
+ .and_then(|project_path| project.entry_for_path(&project_path, cx))
+ {
+ Some(entity) => match project.find_project_path(&input.destination_path, cx) {
+ Some(project_path) => {
+ project.copy_entry(entity.id, None, project_path.path, cx)
+ }
+ None => Task::ready(Err(anyhow!(
+ "Destination path {} was outside the project.",
+ input.destination_path
+ ))),
+ },
+ None => Task::ready(Err(anyhow!(
+ "Source path {} was not found in the project.",
+ input.source_path
+ ))),
+ }
+ });
+
+ cx.background_spawn(async move {
+ match copy_task.await {
+ Ok(_) => Ok(format!(
+ "Copied {} to {}",
+ input.source_path, input.destination_path
+ )),
+ Err(err) => Err(anyhow!(
+ "Failed to copy {} to {}: {}",
+ input.source_path,
+ input.destination_path,
+ err
+ )),
+ }
+ })
+ }
+}
diff --git a/crates/assistant_tools/src/copy_path_tool/description.md b/crates/assistant_tools/src/copy_path_tool/description.md
new file mode 100644
index 0000000000..a5105e6f18
--- /dev/null
+++ b/crates/assistant_tools/src/copy_path_tool/description.md
@@ -0,0 +1,6 @@
+Copies a file or directory in the project, and returns confirmation that the copy succeeded.
+Directory contents will be copied recursively (like `cp -r`).
+
+This tool should be used when it's desirable to create a copy of a file or directory without modifying the original.
+It's much more efficient than doing this by separately reading and then writing the file or directory's contents,
+so this tool should be preferred over that approach whenever copying is the goal.
diff --git a/crates/assistant_tools/src/create_file_tool.rs b/crates/assistant_tools/src/create_file_tool.rs
new file mode 100644
index 0000000000..dc1b5c0f03
--- /dev/null
+++ b/crates/assistant_tools/src/create_file_tool.rs
@@ -0,0 +1,111 @@
+use anyhow::{anyhow, Result};
+use assistant_tool::{ActionLog, Tool};
+use gpui::{App, Entity, Task};
+use language_model::LanguageModelRequestMessage;
+use project::Project;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use std::sync::Arc;
+use ui::IconName;
+
+#[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"
+ ///
+ 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,
+}
+
+pub struct CreateFileTool;
+
+impl Tool for CreateFileTool {
+ fn name(&self) -> String {
+ "create-file".into()
+ }
+
+ fn needs_confirmation(&self) -> bool {
+ true
+ }
+
+ fn description(&self) -> String {
+ include_str!("./create_file_tool/description.md").into()
+ }
+
+ fn icon(&self) -> IconName {
+ IconName::File
+ }
+
+ fn input_schema(&self) -> serde_json::Value {
+ let schema = schemars::schema_for!(CreateFileToolInput);
+ serde_json::to_value(&schema).unwrap()
+ }
+
+ fn ui_text(&self, input: &serde_json::Value) -> String {
+ match serde_json::from_value::(input.clone()) {
+ Ok(input) => {
+ let path = input.path.as_str();
+ format!("Create file `{path}`")
+ }
+ Err(_) => "Create file".to_string(),
+ }
+ }
+
+ fn run(
+ self: Arc,
+ input: serde_json::Value,
+ _messages: &[LanguageModelRequestMessage],
+ project: Entity,
+ _action_log: Entity,
+ cx: &mut App,
+ ) -> Task> {
+ let input = match serde_json::from_value::(input) {
+ Ok(input) => input,
+ Err(err) => return Task::ready(Err(anyhow!(err))),
+ };
+ 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"))),
+ };
+ let contents: Arc = input.contents.as_str().into();
+ let destination_path: Arc = input.path.as_str().into();
+
+ cx.spawn(async move |cx| {
+ project
+ .update(cx, |project, cx| {
+ project.create_entry(project_path.clone(), false, cx)
+ })?
+ .await
+ .map_err(|err| anyhow!("Unable to create {destination_path}: {err}"))?;
+ 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}"))?;
+ buffer.update(cx, |buffer, cx| {
+ buffer.set_text(contents, cx);
+ })?;
+
+ project
+ .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
+ .await
+ .map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?;
+
+ Ok(format!("Created file {destination_path}"))
+ })
+ }
+}
diff --git a/crates/assistant_tools/src/create_file_tool/description.md b/crates/assistant_tools/src/create_file_tool/description.md
new file mode 100644
index 0000000000..fc470829ff
--- /dev/null
+++ b/crates/assistant_tools/src/create_file_tool/description.md
@@ -0,0 +1,3 @@
+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 cc13d34e80..3bd55e6c3a 100644
--- a/crates/assistant_tools/src/delete_path_tool.rs
+++ b/crates/assistant_tools/src/delete_path_tool.rs
@@ -6,6 +6,7 @@ use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
+use ui::IconName;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DeletePathToolInput {
@@ -38,6 +39,10 @@ impl Tool for DeletePathTool {
include_str!("./delete_path_tool/description.md").into()
}
+ fn icon(&self) -> IconName {
+ IconName::Trash
+ }
+
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(DeletePathToolInput);
serde_json::to_value(&schema).unwrap()
diff --git a/crates/assistant_tools/src/diagnostics_tool.rs b/crates/assistant_tools/src/diagnostics_tool.rs
index 95aec472a5..a3638a431f 100644
--- a/crates/assistant_tools/src/diagnostics_tool.rs
+++ b/crates/assistant_tools/src/diagnostics_tool.rs
@@ -11,6 +11,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
+use ui::IconName;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DiagnosticsToolInput {
@@ -45,6 +46,10 @@ impl Tool for DiagnosticsTool {
include_str!("./diagnostics_tool/description.md").into()
}
+ fn icon(&self) -> IconName {
+ IconName::Warning
+ }
+
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(DiagnosticsToolInput);
serde_json::to_value(&schema).unwrap()
diff --git a/crates/assistant_tools/src/edit_files_tool.rs b/crates/assistant_tools/src/edit_files_tool.rs
index dad870851f..08b275c24a 100644
--- a/crates/assistant_tools/src/edit_files_tool.rs
+++ b/crates/assistant_tools/src/edit_files_tool.rs
@@ -1,23 +1,23 @@
mod edit_action;
pub mod log;
-mod replace;
+use crate::replace::{replace_exact, replace_with_flexible_indent};
use anyhow::{anyhow, Context, Result};
use assistant_tool::{ActionLog, Tool};
use collections::HashSet;
use edit_action::{EditAction, EditActionParser};
-use futures::StreamExt;
-use gpui::{App, AsyncApp, Entity, Task};
+use futures::{channel::mpsc, SinkExt, StreamExt};
+use gpui::{App, AppContext, AsyncApp, Entity, Task};
use language_model::{
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, MessageContent, Role,
};
use log::{EditToolLog, EditToolRequestId};
use project::Project;
-use replace::{replace_exact, replace_with_flexible_indent};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::Write;
use std::sync::Arc;
+use ui::IconName;
use util::ResultExt;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@@ -87,6 +87,10 @@ impl Tool for EditFilesTool {
include_str!("./edit_files_tool/description.md").into()
}
+ fn icon(&self) -> IconName {
+ IconName::Pencil
+ }
+
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(EditFilesToolInput);
serde_json::to_value(&schema).unwrap()
@@ -149,22 +153,36 @@ impl Tool for EditFilesTool {
struct EditToolRequest {
parser: EditActionParser,
- output: String,
- changed_buffers: HashSet>,
- bad_searches: Vec,
+ editor_response: EditorResponse,
project: Entity,
action_log: Entity,
tool_log: Option<(Entity