Remove agent1 code (#40495)

Release Notes:

- N/A
This commit is contained in:
Bennet Fenner
2025-10-17 18:49:11 +02:00
committed by GitHub
parent 73e028c01c
commit 3f1319162a
175 changed files with 5472 additions and 23939 deletions

View File

@@ -48,7 +48,7 @@
"remove_trailing_whitespace_on_save": true, "remove_trailing_whitespace_on_save": true,
"ensure_final_newline_on_save": true, "ensure_final_newline_on_save": true,
"file_scan_exclusions": [ "file_scan_exclusions": [
"crates/assistant_tools/src/edit_agent/evals/fixtures", "crates/agent/src/edit_agent/evals/fixtures",
"crates/eval/worktrees/", "crates/eval/worktrees/",
"crates/eval/repos/", "crates/eval/repos/",
"**/.git", "**/.git",

266
Cargo.lock generated
View File

@@ -139,90 +139,14 @@ dependencies = [
[[package]] [[package]]
name = "agent" name = "agent"
version = "0.1.0" version = "0.1.0"
dependencies = [
"action_log",
"agent_settings",
"anyhow",
"assistant_context",
"assistant_tool",
"assistant_tools",
"chrono",
"client",
"cloud_llm_client",
"collections",
"component",
"context_server",
"convert_case 0.8.0",
"fs",
"futures 0.3.31",
"git",
"gpui",
"heed",
"http_client",
"icons",
"indoc",
"language",
"language_model",
"log",
"parking_lot",
"paths",
"postage",
"pretty_assertions",
"project",
"prompt_store",
"rand 0.9.1",
"ref-cast",
"rope",
"schemars 1.0.1",
"serde",
"serde_json",
"settings",
"smol",
"sqlez",
"telemetry",
"text",
"theme",
"thiserror 2.0.12",
"time",
"util",
"uuid",
"workspace",
"workspace-hack",
"zed_env_vars",
"zstd 0.11.2+zstd.1.5.2",
]
[[package]]
name = "agent-client-protocol"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3aaa2bd05a2401887945f8bfd70026e90bc3cf96c62ab9eba2779835bf21dc60"
dependencies = [
"anyhow",
"async-broadcast",
"async-trait",
"futures 0.3.31",
"log",
"parking_lot",
"schemars 1.0.1",
"serde",
"serde_json",
]
[[package]]
name = "agent2"
version = "0.1.0"
dependencies = [ dependencies = [
"acp_thread", "acp_thread",
"action_log", "action_log",
"agent",
"agent-client-protocol", "agent-client-protocol",
"agent_servers", "agent_servers",
"agent_settings", "agent_settings",
"anyhow", "anyhow",
"assistant_context", "assistant_context",
"assistant_tool",
"assistant_tools",
"chrono", "chrono",
"client", "client",
"clock", "clock",
@@ -231,6 +155,7 @@ dependencies = [
"context_server", "context_server",
"ctor", "ctor",
"db", "db",
"derive_more",
"editor", "editor",
"env_logger 0.11.8", "env_logger 0.11.8",
"fs", "fs",
@@ -254,14 +179,19 @@ dependencies = [
"pretty_assertions", "pretty_assertions",
"project", "project",
"prompt_store", "prompt_store",
"rand 0.9.1",
"regex",
"reqwest_client", "reqwest_client",
"rust-embed", "rust-embed",
"schemars 1.0.1", "schemars 1.0.1",
"serde", "serde",
"serde_json", "serde_json",
"settings", "settings",
"smallvec",
"smol", "smol",
"sqlez", "sqlez",
"streaming_diff",
"strsim",
"task", "task",
"telemetry", "telemetry",
"tempfile", "tempfile",
@@ -283,6 +213,23 @@ dependencies = [
"zstd 0.11.2+zstd.1.5.2", "zstd 0.11.2+zstd.1.5.2",
] ]
[[package]]
name = "agent-client-protocol"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3aaa2bd05a2401887945f8bfd70026e90bc3cf96c62ab9eba2779835bf21dc60"
dependencies = [
"anyhow",
"async-broadcast",
"async-trait",
"futures 0.3.31",
"log",
"parking_lot",
"schemars 1.0.1",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "agent_servers" name = "agent_servers"
version = "0.1.0" version = "0.1.0"
@@ -356,7 +303,6 @@ dependencies = [
"action_log", "action_log",
"agent", "agent",
"agent-client-protocol", "agent-client-protocol",
"agent2",
"agent_servers", "agent_servers",
"agent_settings", "agent_settings",
"ai_onboarding", "ai_onboarding",
@@ -365,8 +311,6 @@ dependencies = [
"assistant_context", "assistant_context",
"assistant_slash_command", "assistant_slash_command",
"assistant_slash_commands", "assistant_slash_commands",
"assistant_tool",
"assistant_tools",
"audio", "audio",
"buffer_diff", "buffer_diff",
"chrono", "chrono",
@@ -411,6 +355,7 @@ dependencies = [
"prompt_store", "prompt_store",
"proto", "proto",
"rand 0.9.1", "rand 0.9.1",
"ref-cast",
"release_channel", "release_channel",
"rope", "rope",
"rules_library", "rules_library",
@@ -965,106 +910,6 @@ dependencies = [
"zlog", "zlog",
] ]
[[package]]
name = "assistant_tool"
version = "0.1.0"
dependencies = [
"action_log",
"anyhow",
"buffer_diff",
"clock",
"collections",
"ctor",
"derive_more",
"gpui",
"icons",
"indoc",
"language",
"language_model",
"log",
"parking_lot",
"pretty_assertions",
"project",
"rand 0.9.1",
"regex",
"serde",
"serde_json",
"settings",
"text",
"util",
"workspace",
"workspace-hack",
"zlog",
]
[[package]]
name = "assistant_tools"
version = "0.1.0"
dependencies = [
"action_log",
"agent_settings",
"anyhow",
"assistant_tool",
"buffer_diff",
"chrono",
"client",
"clock",
"cloud_llm_client",
"collections",
"component",
"derive_more",
"diffy",
"editor",
"feature_flags",
"fs",
"futures 0.3.31",
"gpui",
"gpui_tokio",
"handlebars 4.5.0",
"html_to_markdown",
"http_client",
"indoc",
"itertools 0.14.0",
"language",
"language_model",
"language_models",
"log",
"lsp",
"markdown",
"open",
"paths",
"portable-pty",
"pretty_assertions",
"project",
"prompt_store",
"rand 0.9.1",
"regex",
"reqwest_client",
"rust-embed",
"schemars 1.0.1",
"serde",
"serde_json",
"settings",
"smallvec",
"smol",
"streaming_diff",
"strsim",
"task",
"tempfile",
"terminal",
"terminal_view",
"theme",
"tree-sitter-rust",
"ui",
"unindent",
"util",
"watch",
"web_search",
"workspace",
"workspace-hack",
"zlog",
]
[[package]] [[package]]
name = "async-attributes" name = "async-attributes"
version = "1.1.2" version = "1.1.2"
@@ -5819,63 +5664,6 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "eval"
version = "0.1.0"
dependencies = [
"agent",
"agent_settings",
"agent_ui",
"anyhow",
"assistant_tool",
"assistant_tools",
"async-trait",
"buffer_diff",
"chrono",
"clap",
"client",
"cloud_llm_client",
"collections",
"debug_adapter_extension",
"dirs 4.0.0",
"dotenvy",
"env_logger 0.11.8",
"extension",
"fs",
"futures 0.3.31",
"gpui",
"gpui_tokio",
"handlebars 4.5.0",
"language",
"language_extension",
"language_model",
"language_models",
"languages",
"markdown",
"node_runtime",
"pathdiff",
"paths",
"pretty_assertions",
"project",
"prompt_store",
"regex",
"release_channel",
"reqwest_client",
"serde",
"serde_json",
"settings",
"shellexpand 2.1.2",
"smol",
"telemetry",
"terminal_view",
"toml 0.8.20",
"unindent",
"util",
"uuid",
"watch",
"workspace-hack",
]
[[package]] [[package]]
name = "event-listener" name = "event-listener"
version = "2.5.3" version = "2.5.3"
@@ -8987,7 +8775,6 @@ dependencies = [
"open_router", "open_router",
"parking_lot", "parking_lot",
"proto", "proto",
"schemars 1.0.1",
"serde", "serde",
"serde_json", "serde_json",
"settings", "settings",
@@ -14006,10 +13793,9 @@ name = "remote_server"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"action_log", "action_log",
"agent",
"anyhow", "anyhow",
"askpass", "askpass",
"assistant_tool",
"assistant_tools",
"cargo_toml", "cargo_toml",
"clap", "clap",
"client", "client",
@@ -21242,14 +21028,12 @@ version = "0.210.0"
dependencies = [ dependencies = [
"acp_tools", "acp_tools",
"activity_indicator", "activity_indicator",
"agent",
"agent_settings", "agent_settings",
"agent_ui", "agent_ui",
"anyhow", "anyhow",
"ashpd 0.11.0", "ashpd 0.11.0",
"askpass", "askpass",
"assets", "assets",
"assistant_tools",
"audio", "audio",
"auto_update", "auto_update",
"auto_update_ui", "auto_update_ui",

View File

@@ -6,7 +6,6 @@ members = [
"crates/action_log", "crates/action_log",
"crates/activity_indicator", "crates/activity_indicator",
"crates/agent", "crates/agent",
"crates/agent2",
"crates/agent_servers", "crates/agent_servers",
"crates/agent_settings", "crates/agent_settings",
"crates/agent_ui", "crates/agent_ui",
@@ -17,8 +16,6 @@ members = [
"crates/assistant_context", "crates/assistant_context",
"crates/assistant_slash_command", "crates/assistant_slash_command",
"crates/assistant_slash_commands", "crates/assistant_slash_commands",
"crates/assistant_tool",
"crates/assistant_tools",
"crates/audio", "crates/audio",
"crates/auto_update", "crates/auto_update",
"crates/auto_update_helper", "crates/auto_update_helper",
@@ -61,7 +58,7 @@ members = [
"crates/edit_prediction_context", "crates/edit_prediction_context",
"crates/zeta2_tools", "crates/zeta2_tools",
"crates/editor", "crates/editor",
"crates/eval", # "crates/eval",
"crates/explorer_command_injector", "crates/explorer_command_injector",
"crates/extension", "crates/extension",
"crates/extension_api", "crates/extension_api",
@@ -240,7 +237,6 @@ acp_tools = { path = "crates/acp_tools" }
acp_thread = { path = "crates/acp_thread" } acp_thread = { path = "crates/acp_thread" }
action_log = { path = "crates/action_log" } action_log = { path = "crates/action_log" }
agent = { path = "crates/agent" } agent = { path = "crates/agent" }
agent2 = { path = "crates/agent2" }
activity_indicator = { path = "crates/activity_indicator" } activity_indicator = { path = "crates/activity_indicator" }
agent_ui = { path = "crates/agent_ui" } agent_ui = { path = "crates/agent_ui" }
agent_settings = { path = "crates/agent_settings" } agent_settings = { path = "crates/agent_settings" }
@@ -253,8 +249,6 @@ assets = { path = "crates/assets" }
assistant_context = { path = "crates/assistant_context" } assistant_context = { path = "crates/assistant_context" }
assistant_slash_command = { path = "crates/assistant_slash_command" } assistant_slash_command = { path = "crates/assistant_slash_command" }
assistant_slash_commands = { path = "crates/assistant_slash_commands" } assistant_slash_commands = { path = "crates/assistant_slash_commands" }
assistant_tool = { path = "crates/assistant_tool" }
assistant_tools = { path = "crates/assistant_tools" }
audio = { path = "crates/audio" } audio = { path = "crates/audio" }
auto_update = { path = "crates/auto_update" } auto_update = { path = "crates/auto_update" }
auto_update_helper = { path = "crates/auto_update_helper" } auto_update_helper = { path = "crates/auto_update_helper" }

View File

@@ -269,14 +269,14 @@
} }
}, },
{ {
"context": "AgentPanel && prompt_editor", "context": "AgentPanel && text_thread",
"bindings": { "bindings": {
"ctrl-n": "agent::NewTextThread", "ctrl-n": "agent::NewTextThread",
"ctrl-alt-t": "agent::NewThread" "ctrl-alt-t": "agent::NewThread"
} }
}, },
{ {
"context": "AgentPanel && external_agent_thread", "context": "AgentPanel && acp_thread",
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"ctrl-n": "agent::NewExternalAgentThread", "ctrl-n": "agent::NewExternalAgentThread",

View File

@@ -307,7 +307,7 @@
} }
}, },
{ {
"context": "AgentPanel && prompt_editor", "context": "AgentPanel && text_thread",
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"cmd-n": "agent::NewTextThread", "cmd-n": "agent::NewTextThread",
@@ -315,7 +315,7 @@
} }
}, },
{ {
"context": "AgentPanel && external_agent_thread", "context": "AgentPanel && acp_thread",
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"cmd-n": "agent::NewExternalAgentThread", "cmd-n": "agent::NewExternalAgentThread",

View File

@@ -270,7 +270,7 @@
} }
}, },
{ {
"context": "AgentPanel && prompt_editor", "context": "AgentPanel && text_thread",
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"ctrl-n": "agent::NewTextThread", "ctrl-n": "agent::NewTextThread",
@@ -278,7 +278,7 @@
} }
}, },
{ {
"context": "AgentPanel && external_agent_thread", "context": "AgentPanel && acp_thread",
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"ctrl-n": "agent::NewExternalAgentThread", "ctrl-n": "agent::NewExternalAgentThread",

View File

@@ -3,7 +3,7 @@ avoid-breaking-exported-api = false
ignore-interior-mutability = [ ignore-interior-mutability = [
# Suppresses clippy::mutable_key_type, which is a false positive as the Eq # Suppresses clippy::mutable_key_type, which is a false positive as the Eq
# and Hash impls do not use fields with interior mutability. # and Hash impls do not use fields with interior mutability.
"agent::context::AgentContextKey" "agent_ui::context::AgentContextKey"
] ]
disallowed-methods = [ disallowed-methods = [
{ path = "std::process::Command::spawn", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::spawn" }, { path = "std::process::Command::spawn", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::spawn" },

View File

@@ -5,74 +5,100 @@ edition.workspace = true
publish.workspace = true publish.workspace = true
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
[lib]
path = "src/agent.rs"
[features]
test-support = ["db/test-support"]
e2e = []
[lints] [lints]
workspace = true workspace = true
[lib]
path = "src/agent.rs"
doctest = false
[features]
test-support = [
"gpui/test-support",
"language/test-support",
]
[dependencies] [dependencies]
acp_thread.workspace = true
action_log.workspace = true action_log.workspace = true
agent-client-protocol.workspace = true
agent_servers.workspace = true
agent_settings.workspace = true agent_settings.workspace = true
anyhow.workspace = true anyhow.workspace = true
assistant_context.workspace = true assistant_context.workspace = true
assistant_tool.workspace = true
chrono.workspace = true chrono.workspace = true
client.workspace = true client.workspace = true
cloud_llm_client.workspace = true cloud_llm_client.workspace = true
collections.workspace = true collections.workspace = true
component.workspace = true
context_server.workspace = true context_server.workspace = true
convert_case.workspace = true db.workspace = true
derive_more.workspace = true
fs.workspace = true fs.workspace = true
futures.workspace = true futures.workspace = true
git.workspace = true git.workspace = true
gpui.workspace = true gpui.workspace = true
heed.workspace = true handlebars = { workspace = true, features = ["rust-embed"] }
html_to_markdown.workspace = true
http_client.workspace = true http_client.workspace = true
icons.workspace = true
indoc.workspace = true indoc.workspace = true
itertools.workspace = true
language.workspace = true language.workspace = true
language_model.workspace = true language_model.workspace = true
language_models.workspace = true
log.workspace = true log.workspace = true
open.workspace = true
parking_lot.workspace = true
paths.workspace = true paths.workspace = true
postage.workspace = true
project.workspace = true project.workspace = true
prompt_store.workspace = true prompt_store.workspace = true
ref-cast.workspace = true regex.workspace = true
rope.workspace = true rust-embed.workspace = true
schemars.workspace = true schemars.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
settings.workspace = true settings.workspace = true
smallvec.workspace = true
smol.workspace = true smol.workspace = true
sqlez.workspace = true sqlez.workspace = true
streaming_diff.workspace = true
strsim.workspace = true
task.workspace = true
telemetry.workspace = true telemetry.workspace = true
terminal.workspace = true
text.workspace = true text.workspace = true
theme.workspace = true
thiserror.workspace = true thiserror.workspace = true
time.workspace = true ui.workspace = true
util.workspace = true util.workspace = true
uuid.workspace = true uuid.workspace = true
watch.workspace = true
web_search.workspace = true
workspace-hack.workspace = true workspace-hack.workspace = true
zed_env_vars.workspace = true zed_env_vars.workspace = true
zstd.workspace = true zstd.workspace = true
[dev-dependencies] [dev-dependencies]
assistant_tools.workspace = true agent_servers = { workspace = true, "features" = ["test-support"] }
assistant_context = { workspace = true, "features" = ["test-support"] }
client = { workspace = true, "features" = ["test-support"] }
clock = { workspace = true, "features" = ["test-support"] }
context_server = { workspace = true, "features" = ["test-support"] }
ctor.workspace = true
db = { workspace = true, "features" = ["test-support"] }
editor = { workspace = true, "features" = ["test-support"] }
env_logger.workspace = true
fs = { workspace = true, "features" = ["test-support"] }
git = { workspace = true, "features" = ["test-support"] }
gpui = { workspace = true, "features" = ["test-support"] } gpui = { workspace = true, "features" = ["test-support"] }
indoc.workspace = true gpui_tokio.workspace = true
language = { workspace = true, "features" = ["test-support"] } language = { workspace = true, "features" = ["test-support"] }
language_model = { workspace = true, "features" = ["test-support"] } language_model = { workspace = true, "features" = ["test-support"] }
parking_lot.workspace = true lsp = { workspace = true, "features" = ["test-support"] }
pretty_assertions.workspace = true pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] } project = { workspace = true, "features" = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }
rand.workspace = true rand.workspace = true
reqwest_client.workspace = true
settings = { workspace = true, "features" = ["test-support"] }
tempfile.workspace = true
terminal = { workspace = true, "features" = ["test-support"] }
theme = { workspace = true, "features" = ["test-support"] }
tree-sitter-rust.workspace = true
unindent = { workspace = true }
worktree = { workspace = true, "features" = ["test-support"] }
zlog.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -1,341 +0,0 @@
use std::sync::Arc;
use agent_settings::{AgentProfileId, AgentProfileSettings, AgentSettings};
use assistant_tool::{Tool, ToolSource, ToolWorkingSet, UniqueToolName};
use collections::IndexMap;
use convert_case::{Case, Casing};
use fs::Fs;
use gpui::{App, Entity, SharedString};
use settings::{Settings, update_settings_file};
use util::ResultExt;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AgentProfile {
id: AgentProfileId,
tool_set: Entity<ToolWorkingSet>,
}
pub type AvailableProfiles = IndexMap<AgentProfileId, SharedString>;
impl AgentProfile {
pub fn new(id: AgentProfileId, tool_set: Entity<ToolWorkingSet>) -> Self {
Self { id, tool_set }
}
/// Saves a new profile to the settings.
pub fn create(
name: String,
base_profile_id: Option<AgentProfileId>,
fs: Arc<dyn Fs>,
cx: &App,
) -> AgentProfileId {
let id = AgentProfileId(name.to_case(Case::Kebab).into());
let base_profile =
base_profile_id.and_then(|id| AgentSettings::get_global(cx).profiles.get(&id).cloned());
let profile_settings = AgentProfileSettings {
name: name.into(),
tools: base_profile
.as_ref()
.map(|profile| profile.tools.clone())
.unwrap_or_default(),
enable_all_context_servers: base_profile
.as_ref()
.map(|profile| profile.enable_all_context_servers)
.unwrap_or_default(),
context_servers: base_profile
.map(|profile| profile.context_servers)
.unwrap_or_default(),
};
update_settings_file(fs, cx, {
let id = id.clone();
move |settings, _cx| {
profile_settings.save_to_settings(id, settings).log_err();
}
});
id
}
/// Returns a map of AgentProfileIds to their names
pub fn available_profiles(cx: &App) -> AvailableProfiles {
let mut profiles = AvailableProfiles::default();
for (id, profile) in AgentSettings::get_global(cx).profiles.iter() {
profiles.insert(id.clone(), profile.name.clone());
}
profiles
}
pub fn id(&self) -> &AgentProfileId {
&self.id
}
pub fn enabled_tools(&self, cx: &App) -> Vec<(UniqueToolName, Arc<dyn Tool>)> {
let Some(settings) = AgentSettings::get_global(cx).profiles.get(&self.id) else {
return Vec::new();
};
self.tool_set
.read(cx)
.tools(cx)
.into_iter()
.filter(|(_, tool)| Self::is_enabled(settings, tool.source(), tool.name()))
.collect()
}
pub fn is_tool_enabled(&self, source: ToolSource, tool_name: String, cx: &App) -> bool {
let Some(settings) = AgentSettings::get_global(cx).profiles.get(&self.id) else {
return false;
};
Self::is_enabled(settings, source, tool_name)
}
fn is_enabled(settings: &AgentProfileSettings, source: ToolSource, name: String) -> bool {
match source {
ToolSource::Native => *settings.tools.get(name.as_str()).unwrap_or(&false),
ToolSource::ContextServer { id } => settings
.context_servers
.get(id.as_ref())
.and_then(|preset| preset.tools.get(name.as_str()).copied())
.unwrap_or(settings.enable_all_context_servers),
}
}
}
#[cfg(test)]
mod tests {
use agent_settings::ContextServerPreset;
use assistant_tool::ToolRegistry;
use collections::IndexMap;
use gpui::SharedString;
use gpui::{AppContext, TestAppContext};
use http_client::FakeHttpClient;
use project::Project;
use settings::{Settings, SettingsStore};
use super::*;
#[gpui::test]
async fn test_enabled_built_in_tools_for_profile(cx: &mut TestAppContext) {
init_test_settings(cx);
let id = AgentProfileId::default();
let profile_settings = cx.read(|cx| {
AgentSettings::get_global(cx)
.profiles
.get(&id)
.unwrap()
.clone()
});
let tool_set = default_tool_set(cx);
let profile = AgentProfile::new(id, tool_set);
let mut enabled_tools = cx
.read(|cx| profile.enabled_tools(cx))
.into_iter()
.map(|(_, tool)| tool.name())
.collect::<Vec<_>>();
enabled_tools.sort();
let mut expected_tools = profile_settings
.tools
.into_iter()
.filter_map(|(tool, enabled)| enabled.then_some(tool.to_string()))
// Provider dependent
.filter(|tool| tool != "web_search")
.collect::<Vec<_>>();
// Plus all registered MCP tools
expected_tools.extend(["enabled_mcp_tool".into(), "disabled_mcp_tool".into()]);
expected_tools.sort();
assert_eq!(enabled_tools, expected_tools);
}
#[gpui::test]
async fn test_custom_mcp_settings(cx: &mut TestAppContext) {
init_test_settings(cx);
let id = AgentProfileId("custom_mcp".into());
let profile_settings = cx.read(|cx| {
AgentSettings::get_global(cx)
.profiles
.get(&id)
.unwrap()
.clone()
});
let tool_set = default_tool_set(cx);
let profile = AgentProfile::new(id, tool_set);
let mut enabled_tools = cx
.read(|cx| profile.enabled_tools(cx))
.into_iter()
.map(|(_, tool)| tool.name())
.collect::<Vec<_>>();
enabled_tools.sort();
let mut expected_tools = profile_settings.context_servers["mcp"]
.tools
.iter()
.filter_map(|(key, enabled)| enabled.then(|| key.to_string()))
.collect::<Vec<_>>();
expected_tools.sort();
assert_eq!(enabled_tools, expected_tools);
}
#[gpui::test]
async fn test_only_built_in(cx: &mut TestAppContext) {
init_test_settings(cx);
let id = AgentProfileId("write_minus_mcp".into());
let profile_settings = cx.read(|cx| {
AgentSettings::get_global(cx)
.profiles
.get(&id)
.unwrap()
.clone()
});
let tool_set = default_tool_set(cx);
let profile = AgentProfile::new(id, tool_set);
let mut enabled_tools = cx
.read(|cx| profile.enabled_tools(cx))
.into_iter()
.map(|(_, tool)| tool.name())
.collect::<Vec<_>>();
enabled_tools.sort();
let mut expected_tools = profile_settings
.tools
.into_iter()
.filter_map(|(tool, enabled)| enabled.then_some(tool.to_string()))
// Provider dependent
.filter(|tool| tool != "web_search")
.collect::<Vec<_>>();
expected_tools.sort();
assert_eq!(enabled_tools, expected_tools);
}
fn init_test_settings(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
Project::init_settings(cx);
AgentSettings::register(cx);
language_model::init_settings(cx);
ToolRegistry::default_global(cx);
assistant_tools::init(FakeHttpClient::with_404_response(), cx);
});
cx.update(|cx| {
let mut agent_settings = AgentSettings::get_global(cx).clone();
agent_settings.profiles.insert(
AgentProfileId("write_minus_mcp".into()),
AgentProfileSettings {
name: "write_minus_mcp".into(),
enable_all_context_servers: false,
..agent_settings.profiles[&AgentProfileId::default()].clone()
},
);
agent_settings.profiles.insert(
AgentProfileId("custom_mcp".into()),
AgentProfileSettings {
name: "mcp".into(),
tools: IndexMap::default(),
enable_all_context_servers: false,
context_servers: IndexMap::from_iter([("mcp".into(), context_server_preset())]),
},
);
AgentSettings::override_global(agent_settings, cx);
})
}
fn context_server_preset() -> ContextServerPreset {
ContextServerPreset {
tools: IndexMap::from_iter([
("enabled_mcp_tool".into(), true),
("disabled_mcp_tool".into(), false),
]),
}
}
fn default_tool_set(cx: &mut TestAppContext) -> Entity<ToolWorkingSet> {
cx.new(|cx| {
let mut tool_set = ToolWorkingSet::default();
tool_set.insert(Arc::new(FakeTool::new("enabled_mcp_tool", "mcp")), cx);
tool_set.insert(Arc::new(FakeTool::new("disabled_mcp_tool", "mcp")), cx);
tool_set
})
}
struct FakeTool {
name: String,
source: SharedString,
}
impl FakeTool {
fn new(name: impl Into<String>, source: impl Into<SharedString>) -> Self {
Self {
name: name.into(),
source: source.into(),
}
}
}
impl Tool for FakeTool {
fn name(&self) -> String {
self.name.clone()
}
fn source(&self) -> ToolSource {
ToolSource::ContextServer {
id: self.source.clone(),
}
}
fn description(&self) -> String {
unimplemented!()
}
fn icon(&self) -> icons::IconName {
unimplemented!()
}
fn needs_confirmation(
&self,
_input: &serde_json::Value,
_project: &Entity<Project>,
_cx: &App,
) -> bool {
unimplemented!()
}
fn ui_text(&self, _input: &serde_json::Value) -> String {
unimplemented!()
}
fn run(
self: Arc<Self>,
_input: serde_json::Value,
_request: Arc<language_model::LanguageModelRequest>,
_project: Entity<Project>,
_action_log: Entity<action_log::ActionLog>,
_model: Arc<dyn language_model::LanguageModel>,
_window: Option<gpui::AnyWindowHandle>,
_cx: &mut App,
) -> assistant_tool::ToolResult {
unimplemented!()
}
fn may_perform_edits(&self) -> bool {
unimplemented!()
}
}
}

View File

@@ -1,140 +0,0 @@
use std::sync::Arc;
use action_log::ActionLog;
use anyhow::{Result, anyhow, bail};
use assistant_tool::{Tool, ToolResult, ToolSource};
use context_server::{ContextServerId, types};
use gpui::{AnyWindowHandle, App, Entity, Task};
use icons::IconName;
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::{Project, context_server_store::ContextServerStore};
pub struct ContextServerTool {
store: Entity<ContextServerStore>,
server_id: ContextServerId,
tool: types::Tool,
}
impl ContextServerTool {
pub fn new(
store: Entity<ContextServerStore>,
server_id: ContextServerId,
tool: types::Tool,
) -> Self {
Self {
store,
server_id,
tool,
}
}
}
impl Tool for ContextServerTool {
fn name(&self) -> String {
self.tool.name.clone()
}
fn description(&self) -> String {
self.tool.description.clone().unwrap_or_default()
}
fn icon(&self) -> IconName {
IconName::ToolHammer
}
fn source(&self) -> ToolSource {
ToolSource::ContextServer {
id: self.server_id.clone().0.into(),
}
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
true
}
fn may_perform_edits(&self) -> bool {
true
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
let mut schema = self.tool.input_schema.clone();
assistant_tool::adapt_schema_to_format(&mut schema, format)?;
Ok(match schema {
serde_json::Value::Null => {
serde_json::json!({ "type": "object", "properties": [] })
}
serde_json::Value::Object(map) if map.is_empty() => {
serde_json::json!({ "type": "object", "properties": [] })
}
_ => schema,
})
}
fn ui_text(&self, _input: &serde_json::Value) -> String {
format!("Run MCP tool `{}`", self.tool.name)
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_request: Arc<LanguageModelRequest>,
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
if let Some(server) = self.store.read(cx).get_running_server(&self.server_id) {
let tool_name = self.tool.name.clone();
cx.spawn(async move |_cx| {
let Some(protocol) = server.client() else {
bail!("Context server not initialized");
};
let arguments = if let serde_json::Value::Object(map) = input {
Some(map.into_iter().collect())
} else {
None
};
log::trace!(
"Running tool: {} with arguments: {:?}",
tool_name,
arguments
);
let response = protocol
.request::<context_server::types::requests::CallTool>(
context_server::types::CallToolParams {
name: tool_name,
arguments,
meta: None,
},
)
.await?;
let mut result = String::new();
for content in response.content {
match content {
types::ToolResponseContent::Text { text } => {
result.push_str(&text);
}
types::ToolResponseContent::Image { .. } => {
log::warn!("Ignoring image content from tool response");
}
types::ToolResponseContent::Audio { .. } => {
log::warn!("Ignoring audio content from tool response");
}
types::ToolResponseContent::Resource { .. } => {
log::warn!("Ignoring resource content from tool response");
}
}
}
Ok(result.into())
})
.into()
} else {
Task::ready(Err(anyhow!("Context server not found"))).into()
}
}
}

View File

@@ -1,6 +1,5 @@
use crate::{AgentMessage, AgentMessageContent, UserMessage, UserMessageContent}; use crate::{AgentMessage, AgentMessageContent, UserMessage, UserMessageContent};
use acp_thread::UserMessageId; use acp_thread::UserMessageId;
use agent::{thread::DetailedSummaryState, thread_store};
use agent_client_protocol as acp; use agent_client_protocol as acp;
use agent_settings::{AgentProfileId, CompletionMode}; use agent_settings::{AgentProfileId, CompletionMode};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
@@ -21,8 +20,8 @@ use ui::{App, SharedString};
use zed_env_vars::ZED_STATELESS; use zed_env_vars::ZED_STATELESS;
pub type DbMessage = crate::Message; pub type DbMessage = crate::Message;
pub type DbSummary = DetailedSummaryState; pub type DbSummary = crate::legacy_thread::DetailedSummaryState;
pub type DbLanguageModel = thread_store::SerializedLanguageModel; pub type DbLanguageModel = crate::legacy_thread::SerializedLanguageModel;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DbThreadMetadata { pub struct DbThreadMetadata {
@@ -40,7 +39,7 @@ pub struct DbThread {
#[serde(default)] #[serde(default)]
pub detailed_summary: Option<SharedString>, pub detailed_summary: Option<SharedString>,
#[serde(default)] #[serde(default)]
pub initial_project_snapshot: Option<Arc<agent::thread::ProjectSnapshot>>, pub initial_project_snapshot: Option<Arc<crate::ProjectSnapshot>>,
#[serde(default)] #[serde(default)]
pub cumulative_token_usage: language_model::TokenUsage, pub cumulative_token_usage: language_model::TokenUsage,
#[serde(default)] #[serde(default)]
@@ -61,13 +60,17 @@ impl DbThread {
match saved_thread_json.get("version") { match saved_thread_json.get("version") {
Some(serde_json::Value::String(version)) => match version.as_str() { Some(serde_json::Value::String(version)) => match version.as_str() {
Self::VERSION => Ok(serde_json::from_value(saved_thread_json)?), Self::VERSION => Ok(serde_json::from_value(saved_thread_json)?),
_ => Self::upgrade_from_agent_1(agent::SerializedThread::from_json(json)?), _ => Self::upgrade_from_agent_1(crate::legacy_thread::SerializedThread::from_json(
json,
)?),
}, },
_ => Self::upgrade_from_agent_1(agent::SerializedThread::from_json(json)?), _ => {
Self::upgrade_from_agent_1(crate::legacy_thread::SerializedThread::from_json(json)?)
}
} }
} }
fn upgrade_from_agent_1(thread: agent::SerializedThread) -> Result<Self> { fn upgrade_from_agent_1(thread: crate::legacy_thread::SerializedThread) -> Result<Self> {
let mut messages = Vec::new(); let mut messages = Vec::new();
let mut request_token_usage = HashMap::default(); let mut request_token_usage = HashMap::default();
@@ -80,14 +83,19 @@ impl DbThread {
// Convert segments to content // Convert segments to content
for segment in msg.segments { for segment in msg.segments {
match segment { match segment {
thread_store::SerializedMessageSegment::Text { text } => { crate::legacy_thread::SerializedMessageSegment::Text { text } => {
content.push(UserMessageContent::Text(text)); content.push(UserMessageContent::Text(text));
} }
thread_store::SerializedMessageSegment::Thinking { text, .. } => { crate::legacy_thread::SerializedMessageSegment::Thinking {
text,
..
} => {
// User messages don't have thinking segments, but handle gracefully // User messages don't have thinking segments, but handle gracefully
content.push(UserMessageContent::Text(text)); content.push(UserMessageContent::Text(text));
} }
thread_store::SerializedMessageSegment::RedactedThinking { .. } => { crate::legacy_thread::SerializedMessageSegment::RedactedThinking {
..
} => {
// User messages don't have redacted thinking, skip. // User messages don't have redacted thinking, skip.
} }
} }
@@ -113,16 +121,18 @@ impl DbThread {
// Convert segments to content // Convert segments to content
for segment in msg.segments { for segment in msg.segments {
match segment { match segment {
thread_store::SerializedMessageSegment::Text { text } => { crate::legacy_thread::SerializedMessageSegment::Text { text } => {
content.push(AgentMessageContent::Text(text)); content.push(AgentMessageContent::Text(text));
} }
thread_store::SerializedMessageSegment::Thinking { crate::legacy_thread::SerializedMessageSegment::Thinking {
text, text,
signature, signature,
} => { } => {
content.push(AgentMessageContent::Thinking { text, signature }); content.push(AgentMessageContent::Thinking { text, signature });
} }
thread_store::SerializedMessageSegment::RedactedThinking { data } => { crate::legacy_thread::SerializedMessageSegment::RedactedThinking {
data,
} => {
content.push(AgentMessageContent::RedactedThinking(data)); content.push(AgentMessageContent::RedactedThinking(data));
} }
} }
@@ -187,10 +197,9 @@ impl DbThread {
messages, messages,
updated_at: thread.updated_at, updated_at: thread.updated_at,
detailed_summary: match thread.detailed_summary_state { detailed_summary: match thread.detailed_summary_state {
DetailedSummaryState::NotGenerated | DetailedSummaryState::Generating { .. } => { crate::legacy_thread::DetailedSummaryState::NotGenerated
None | crate::legacy_thread::DetailedSummaryState::Generating => None,
} crate::legacy_thread::DetailedSummaryState::Generated { text, .. } => Some(text),
DetailedSummaryState::Generated { text, .. } => Some(text),
}, },
initial_project_snapshot: thread.initial_project_snapshot, initial_project_snapshot: thread.initial_project_snapshot,
cumulative_token_usage: thread.cumulative_token_usage, cumulative_token_usage: thread.cumulative_token_usage,
@@ -414,84 +423,3 @@ impl ThreadsDatabase {
}) })
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use agent::MessageSegment;
use agent::context::LoadedContext;
use client::Client;
use fs::{FakeFs, Fs};
use gpui::AppContext;
use gpui::TestAppContext;
use http_client::FakeHttpClient;
use language_model::Role;
use project::Project;
use settings::SettingsStore;
fn init_test(fs: Arc<dyn Fs>, cx: &mut TestAppContext) {
env_logger::try_init().ok();
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
Project::init_settings(cx);
language::init(cx);
let http_client = FakeHttpClient::with_404_response();
let clock = Arc::new(clock::FakeSystemClock::new());
let client = Client::new(clock, http_client, cx);
agent::init(fs, cx);
agent_settings::init(cx);
language_model::init(client, cx);
});
}
#[gpui::test]
async fn test_retrieving_old_thread(cx: &mut TestAppContext) {
let fs = FakeFs::new(cx.executor());
init_test(fs.clone(), cx);
let project = Project::test(fs, [], cx).await;
// Save a thread using the old agent.
let thread_store = cx.new(|cx| agent::ThreadStore::fake(project, cx));
let thread = thread_store.update(cx, |thread_store, cx| thread_store.create_thread(cx));
thread.update(cx, |thread, cx| {
thread.insert_message(
Role::User,
vec![MessageSegment::Text("Hey!".into())],
LoadedContext::default(),
vec![],
false,
cx,
);
thread.insert_message(
Role::Assistant,
vec![MessageSegment::Text("How're you doing?".into())],
LoadedContext::default(),
vec![],
false,
cx,
)
});
thread_store
.update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
.await
.unwrap();
// Open that same thread using the new agent.
let db = cx.update(ThreadsDatabase::connect).await.unwrap();
let threads = db.list_threads().await.unwrap();
assert_eq!(threads.len(), 1);
let thread = db
.load_thread(threads[0].id.clone())
.await
.unwrap()
.unwrap();
assert_eq!(thread.messages[0].to_markdown(), "## User\n\nHey!\n");
assert_eq!(
thread.messages[1].to_markdown(),
"## Assistant\n\nHow're you doing?\n"
);
}
}

View File

@@ -1,12 +1,8 @@
use super::*; use super::*;
use crate::{ use crate::{
ReadFileToolInput, EditFileMode, EditFileToolInput, GrepToolInput, ListDirectoryToolInput, ReadFileToolInput,
edit_file_tool::{EditFileMode, EditFileToolInput},
grep_tool::GrepToolInput,
list_directory_tool::ListDirectoryToolInput,
}; };
use Role::*; use Role::*;
use assistant_tool::ToolRegistry;
use client::{Client, UserStore}; use client::{Client, UserStore};
use collections::HashMap; use collections::HashMap;
use fs::FakeFs; use fs::FakeFs;
@@ -15,11 +11,11 @@ use gpui::{AppContext, TestAppContext, Timer};
use http_client::StatusCode; use http_client::StatusCode;
use indoc::{formatdoc, indoc}; use indoc::{formatdoc, indoc};
use language_model::{ use language_model::{
LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult, LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolResultContent,
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, SelectedModel, LanguageModelToolUse, LanguageModelToolUseId, SelectedModel,
}; };
use project::Project; use project::Project;
use prompt_store::{ModelContext, ProjectContext, PromptBuilder, WorktreeContext}; use prompt_store::{ProjectContext, WorktreeContext};
use rand::prelude::*; use rand::prelude::*;
use reqwest_client::ReqwestClient; use reqwest_client::ReqwestClient;
use serde_json::json; use serde_json::json;
@@ -121,6 +117,7 @@ fn eval_delete_run_git_blame() {
// gemini-2.5-pro-06-05 | 1.0 (2025-06-16) // gemini-2.5-pro-06-05 | 1.0 (2025-06-16)
// gemini-2.5-flash | // gemini-2.5-flash |
// gpt-4.1 | // gpt-4.1 |
let input_file_path = "root/blame.rs"; let input_file_path = "root/blame.rs";
let input_file_content = include_str!("evals/fixtures/delete_run_git_blame/before.rs"); let input_file_content = include_str!("evals/fixtures/delete_run_git_blame/before.rs");
let output_file_content = include_str!("evals/fixtures/delete_run_git_blame/after.rs"); let output_file_content = include_str!("evals/fixtures/delete_run_git_blame/after.rs");
@@ -184,6 +181,7 @@ fn eval_translate_doc_comments() {
// gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22) // gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22)
// gemini-2.5-flash-preview-04-17 | // gemini-2.5-flash-preview-04-17 |
// gpt-4.1 | // gpt-4.1 |
let input_file_path = "root/canvas.rs"; let input_file_path = "root/canvas.rs";
let input_file_content = include_str!("evals/fixtures/translate_doc_comments/before.rs"); let input_file_content = include_str!("evals/fixtures/translate_doc_comments/before.rs");
let edit_description = "Translate all doc comments to Italian"; let edit_description = "Translate all doc comments to Italian";
@@ -246,6 +244,7 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
// gemini-2.5-pro-preview-latest | 0.99 (2025-06-16) // gemini-2.5-pro-preview-latest | 0.99 (2025-06-16)
// gemini-2.5-flash-preview-04-17 | // gemini-2.5-flash-preview-04-17 |
// gpt-4.1 | // gpt-4.1 |
let input_file_path = "root/lib.rs"; let input_file_path = "root/lib.rs";
let input_file_content = let input_file_content =
include_str!("evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs"); include_str!("evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs");
@@ -371,6 +370,7 @@ fn eval_disable_cursor_blinking() {
// gemini-2.5-pro | 0.95 (2025-07-14) // gemini-2.5-pro | 0.95 (2025-07-14)
// gemini-2.5-flash-preview-04-17 | 0.78 (2025-07-14) // gemini-2.5-flash-preview-04-17 | 0.78 (2025-07-14)
// gpt-4.1 | 0.00 (2025-07-14) (follows edit_description too literally) // gpt-4.1 | 0.00 (2025-07-14) (follows edit_description too literally)
let input_file_path = "root/editor.rs"; let input_file_path = "root/editor.rs";
let input_file_content = include_str!("evals/fixtures/disable_cursor_blinking/before.rs"); let input_file_content = include_str!("evals/fixtures/disable_cursor_blinking/before.rs");
let edit_description = "Comment out the call to `BlinkManager::enable`"; let edit_description = "Comment out the call to `BlinkManager::enable`";
@@ -463,6 +463,7 @@ fn eval_from_pixels_constructor() {
// claude-3.7-sonnet | 2025-06-14 | 0.88 // claude-3.7-sonnet | 2025-06-14 | 0.88
// gemini-2.5-pro-preview-06-05 | 2025-06-16 | 0.98 // gemini-2.5-pro-preview-06-05 | 2025-06-16 | 0.98
// gpt-4.1 | // gpt-4.1 |
let input_file_path = "root/canvas.rs"; let input_file_path = "root/canvas.rs";
let input_file_content = include_str!("evals/fixtures/from_pixels_constructor/before.rs"); let input_file_content = include_str!("evals/fixtures/from_pixels_constructor/before.rs");
let edit_description = "Implement from_pixels constructor and add tests."; let edit_description = "Implement from_pixels constructor and add tests.";
@@ -665,6 +666,7 @@ fn eval_zode() {
// gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22) // gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22)
// gemini-2.5-flash-preview-04-17 | 1.0 (2025-05-22) // gemini-2.5-flash-preview-04-17 | 1.0 (2025-05-22)
// gpt-4.1 | 1.0 (2025-05-22) // gpt-4.1 | 1.0 (2025-05-22)
let input_file_path = "root/zode.py"; let input_file_path = "root/zode.py";
let input_content = None; let input_content = None;
let edit_description = "Create the main Zode CLI script"; let edit_description = "Create the main Zode CLI script";
@@ -771,6 +773,7 @@ fn eval_add_overwrite_test() {
// gemini-2.5-pro-preview-03-25 | 0.35 (2025-05-22) // gemini-2.5-pro-preview-03-25 | 0.35 (2025-05-22)
// gemini-2.5-flash-preview-04-17 | // gemini-2.5-flash-preview-04-17 |
// gpt-4.1 | // gpt-4.1 |
let input_file_path = "root/action_log.rs"; let input_file_path = "root/action_log.rs";
let input_file_content = include_str!("evals/fixtures/add_overwrite_test/before.rs"); let input_file_content = include_str!("evals/fixtures/add_overwrite_test/before.rs");
let edit_description = "Add a new test for overwriting a file in action_log.rs"; let edit_description = "Add a new test for overwriting a file in action_log.rs";
@@ -1010,7 +1013,7 @@ fn eval_create_empty_file() {
// //
// TODO: gpt-4.1-mini errored 38 times: // TODO: gpt-4.1-mini errored 38 times:
// "data did not match any variant of untagged enum ResponseStreamResult" // "data did not match any variant of untagged enum ResponseStreamResult"
//
let input_file_content = None; let input_file_content = None;
let expected_output_content = String::new(); let expected_output_content = String::new();
eval( eval(
@@ -1475,19 +1478,16 @@ impl EditAgentTest {
language::init(cx); language::init(cx);
language_model::init(client.clone(), cx); language_model::init(client.clone(), cx);
language_models::init(user_store, client.clone(), cx); language_models::init(user_store, client.clone(), cx);
crate::init(client.http_client(), cx);
}); });
fs.insert_tree("/root", json!({})).await; fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let agent_model = SelectedModel::from_str( let agent_model = SelectedModel::from_str(
&std::env::var("ZED_AGENT_MODEL") &std::env::var("ZED_AGENT_MODEL").unwrap_or("anthropic/claude-4-sonnet-latest".into()),
.unwrap_or("anthropic/claude-3-7-sonnet-latest".into()),
) )
.unwrap(); .unwrap();
let judge_model = SelectedModel::from_str( let judge_model = SelectedModel::from_str(
&std::env::var("ZED_JUDGE_MODEL") &std::env::var("ZED_JUDGE_MODEL").unwrap_or("anthropic/claude-4-sonnet-latest".into()),
.unwrap_or("anthropic/claude-3-7-sonnet-latest".into()),
) )
.unwrap(); .unwrap();
let (agent_model, judge_model) = cx let (agent_model, judge_model) = cx
@@ -1553,39 +1553,27 @@ impl EditAgentTest {
.update(cx, |project, cx| project.open_buffer(path, cx)) .update(cx, |project, cx| project.open_buffer(path, cx))
.await .await
.unwrap(); .unwrap();
let tools = cx.update(|cx| {
ToolRegistry::default_global(cx) let tools = crate::built_in_tools().collect::<Vec<_>>();
.tools()
.into_iter() let system_prompt = {
.filter_map(|tool| { let worktrees = vec![WorktreeContext {
let input_schema = tool root_name: "root".to_string(),
.input_schema(self.agent.model.tool_input_format()) abs_path: Path::new("/path/to/root").into(),
.ok()?; rules_file: None,
Some(LanguageModelRequestTool { }];
name: tool.name(), let project_context = ProjectContext::new(worktrees, Vec::default());
description: tool.description(), let tool_names = tools
input_schema, .iter()
}) .map(|tool| tool.name.clone().into())
}) .collect::<Vec<_>>();
.collect::<Vec<_>>() let template = crate::SystemPromptTemplate {
}); project: &project_context,
let tool_names = tools
.iter()
.map(|tool| tool.name.clone())
.collect::<Vec<_>>();
let worktrees = vec![WorktreeContext {
root_name: "root".to_string(),
abs_path: Path::new("/path/to/root").into(),
rules_file: None,
}];
let prompt_builder = PromptBuilder::new(None)?;
let project_context = ProjectContext::new(worktrees, Vec::default());
let system_prompt = prompt_builder.generate_assistant_system_prompt(
&project_context,
&ModelContext {
available_tools: tool_names, available_tools: tool_names,
}, };
)?; let templates = Templates::new();
template.render(&templates).unwrap()
};
let has_system_prompt = eval let has_system_prompt = eval
.conversation .conversation

View File

@@ -1,4 +1,4 @@
use crate::{DbThreadMetadata, ThreadsDatabase}; use crate::{DbThread, DbThreadMetadata, ThreadsDatabase};
use acp_thread::MentionUri; use acp_thread::MentionUri;
use agent_client_protocol as acp; use agent_client_protocol as acp;
use anyhow::{Context as _, Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
@@ -8,8 +8,9 @@ use db::kvp::KEY_VALUE_STORE;
use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::*}; use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::*};
use itertools::Itertools; use itertools::Itertools;
use paths::contexts_dir; use paths::contexts_dir;
use project::Project;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{collections::VecDeque, path::Path, sync::Arc, time::Duration}; use std::{collections::VecDeque, path::Path, rc::Rc, sync::Arc, time::Duration};
use ui::ElementId; use ui::ElementId;
use util::ResultExt as _; use util::ResultExt as _;
@@ -19,6 +20,33 @@ const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50
const DEFAULT_TITLE: &SharedString = &SharedString::new_static("New Thread"); const DEFAULT_TITLE: &SharedString = &SharedString::new_static("New Thread");
//todo: We should remove this function once we support loading all acp thread
pub fn load_agent_thread(
session_id: acp::SessionId,
history_store: Entity<HistoryStore>,
project: Entity<Project>,
cx: &mut App,
) -> Task<Result<Entity<crate::Thread>>> {
use agent_servers::{AgentServer, AgentServerDelegate};
let server = Rc::new(crate::NativeAgentServer::new(
project.read(cx).fs().clone(),
history_store,
));
let delegate = AgentServerDelegate::new(
project.read(cx).agent_server_store().clone(),
project.clone(),
None,
None,
);
let connection = server.connect(None, delegate, cx);
cx.spawn(async move |cx| {
let (agent, _) = connection.await?;
let agent = agent.downcast::<crate::NativeAgentConnection>().unwrap();
cx.update(|cx| agent.load_thread(session_id, cx))?.await
})
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum HistoryEntry { pub enum HistoryEntry {
AcpThread(DbThreadMetadata), AcpThread(DbThreadMetadata),
@@ -55,8 +83,13 @@ impl HistoryEntry {
pub fn title(&self) -> &SharedString { pub fn title(&self) -> &SharedString {
match self { match self {
HistoryEntry::AcpThread(thread) if thread.title.is_empty() => DEFAULT_TITLE, HistoryEntry::AcpThread(thread) => {
HistoryEntry::AcpThread(thread) => &thread.title, if thread.title.is_empty() {
DEFAULT_TITLE
} else {
&thread.title
}
}
HistoryEntry::TextThread(context) => &context.title, HistoryEntry::TextThread(context) => &context.title,
} }
} }
@@ -87,7 +120,7 @@ enum SerializedRecentOpen {
pub struct HistoryStore { pub struct HistoryStore {
threads: Vec<DbThreadMetadata>, threads: Vec<DbThreadMetadata>,
entries: Vec<HistoryEntry>, entries: Vec<HistoryEntry>,
context_store: Entity<assistant_context::ContextStore>, text_thread_store: Entity<assistant_context::ContextStore>,
recently_opened_entries: VecDeque<HistoryEntryId>, recently_opened_entries: VecDeque<HistoryEntryId>,
_subscriptions: Vec<gpui::Subscription>, _subscriptions: Vec<gpui::Subscription>,
_save_recently_opened_entries_task: Task<()>, _save_recently_opened_entries_task: Task<()>,
@@ -95,10 +128,11 @@ pub struct HistoryStore {
impl HistoryStore { impl HistoryStore {
pub fn new( pub fn new(
context_store: Entity<assistant_context::ContextStore>, text_thread_store: Entity<assistant_context::ContextStore>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
let subscriptions = vec![cx.observe(&context_store, |this, _, cx| this.update_entries(cx))]; let subscriptions =
vec![cx.observe(&text_thread_store, |this, _, cx| this.update_entries(cx))];
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
let entries = Self::load_recently_opened_entries(cx).await; let entries = Self::load_recently_opened_entries(cx).await;
@@ -114,7 +148,7 @@ impl HistoryStore {
.detach(); .detach();
Self { Self {
context_store, text_thread_store,
recently_opened_entries: VecDeque::default(), recently_opened_entries: VecDeque::default(),
threads: Vec::default(), threads: Vec::default(),
entries: Vec::default(), entries: Vec::default(),
@@ -127,6 +161,18 @@ impl HistoryStore {
self.threads.iter().find(|thread| &thread.id == session_id) self.threads.iter().find(|thread| &thread.id == session_id)
} }
pub fn load_thread(
&mut self,
id: acp::SessionId,
cx: &mut Context<Self>,
) -> Task<Result<Option<DbThread>>> {
let database_future = ThreadsDatabase::connect(cx);
cx.background_spawn(async move {
let database = database_future.await.map_err(|err| anyhow!(err))?;
database.load_thread(id).await
})
}
pub fn delete_thread( pub fn delete_thread(
&mut self, &mut self,
id: acp::SessionId, id: acp::SessionId,
@@ -145,9 +191,8 @@ impl HistoryStore {
path: Arc<Path>, path: Arc<Path>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
self.context_store.update(cx, |context_store, cx| { self.text_thread_store
context_store.delete_local_context(path, cx) .update(cx, |store, cx| store.delete_local_context(path, cx))
})
} }
pub fn load_text_thread( pub fn load_text_thread(
@@ -155,9 +200,8 @@ impl HistoryStore {
path: Arc<Path>, path: Arc<Path>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<Entity<AssistantContext>>> { ) -> Task<Result<Entity<AssistantContext>>> {
self.context_store.update(cx, |context_store, cx| { self.text_thread_store
context_store.open_local_context(path, cx) .update(cx, |store, cx| store.open_local_context(path, cx))
})
} }
pub fn reload(&self, cx: &mut Context<Self>) { pub fn reload(&self, cx: &mut Context<Self>) {
@@ -197,7 +241,7 @@ impl HistoryStore {
let mut history_entries = Vec::new(); let mut history_entries = Vec::new();
history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread)); history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread));
history_entries.extend( history_entries.extend(
self.context_store self.text_thread_store
.read(cx) .read(cx)
.unordered_contexts() .unordered_contexts()
.cloned() .cloned()
@@ -231,21 +275,21 @@ impl HistoryStore {
}) })
}); });
let context_entries = let context_entries = self
self.context_store .text_thread_store
.read(cx) .read(cx)
.unordered_contexts() .unordered_contexts()
.flat_map(|context| { .flat_map(|context| {
self.recently_opened_entries self.recently_opened_entries
.iter() .iter()
.enumerate() .enumerate()
.flat_map(|(index, entry)| match entry { .flat_map(|(index, entry)| match entry {
HistoryEntryId::TextThread(path) if &context.path == path => { HistoryEntryId::TextThread(path) if &context.path == path => {
Some((index, HistoryEntry::TextThread(context.clone()))) Some((index, HistoryEntry::TextThread(context.clone())))
} }
_ => None, _ => None,
}) })
}); });
thread_entries thread_entries
.chain(context_entries) .chain(context_entries)

View File

@@ -0,0 +1,402 @@
use crate::ProjectSnapshot;
use agent_settings::{AgentProfileId, CompletionMode};
use anyhow::Result;
use chrono::{DateTime, Utc};
use gpui::SharedString;
use language_model::{LanguageModelToolResultContent, LanguageModelToolUseId, Role, TokenUsage};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub enum DetailedSummaryState {
#[default]
NotGenerated,
Generating,
Generated {
text: SharedString,
},
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
pub struct MessageId(pub usize);
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct SerializedThread {
pub version: String,
pub summary: SharedString,
pub updated_at: DateTime<Utc>,
pub messages: Vec<SerializedMessage>,
#[serde(default)]
pub initial_project_snapshot: Option<Arc<ProjectSnapshot>>,
#[serde(default)]
pub cumulative_token_usage: TokenUsage,
#[serde(default)]
pub request_token_usage: Vec<TokenUsage>,
#[serde(default)]
pub detailed_summary_state: DetailedSummaryState,
#[serde(default)]
pub model: Option<SerializedLanguageModel>,
#[serde(default)]
pub completion_mode: Option<CompletionMode>,
#[serde(default)]
pub tool_use_limit_reached: bool,
#[serde(default)]
pub profile: Option<AgentProfileId>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct SerializedLanguageModel {
pub provider: String,
pub model: String,
}
impl SerializedThread {
pub const VERSION: &'static str = "0.2.0";
pub fn from_json(json: &[u8]) -> Result<Self> {
let saved_thread_json = serde_json::from_slice::<serde_json::Value>(json)?;
match saved_thread_json.get("version") {
Some(serde_json::Value::String(version)) => match version.as_str() {
SerializedThreadV0_1_0::VERSION => {
let saved_thread =
serde_json::from_value::<SerializedThreadV0_1_0>(saved_thread_json)?;
Ok(saved_thread.upgrade())
}
SerializedThread::VERSION => Ok(serde_json::from_value::<SerializedThread>(
saved_thread_json,
)?),
_ => anyhow::bail!("unrecognized serialized thread version: {version:?}"),
},
None => {
let saved_thread =
serde_json::from_value::<LegacySerializedThread>(saved_thread_json)?;
Ok(saved_thread.upgrade())
}
version => anyhow::bail!("unrecognized serialized thread version: {version:?}"),
}
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SerializedThreadV0_1_0(
// The structure did not change, so we are reusing the latest SerializedThread.
// When making the next version, make sure this points to SerializedThreadV0_2_0
SerializedThread,
);
impl SerializedThreadV0_1_0 {
pub const VERSION: &'static str = "0.1.0";
pub fn upgrade(self) -> SerializedThread {
debug_assert_eq!(SerializedThread::VERSION, "0.2.0");
let mut messages: Vec<SerializedMessage> = Vec::with_capacity(self.0.messages.len());
for message in self.0.messages {
if message.role == Role::User
&& !message.tool_results.is_empty()
&& let Some(last_message) = messages.last_mut()
{
debug_assert!(last_message.role == Role::Assistant);
last_message.tool_results = message.tool_results;
continue;
}
messages.push(message);
}
SerializedThread {
messages,
version: SerializedThread::VERSION.to_string(),
..self.0
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct SerializedMessage {
pub id: MessageId,
pub role: Role,
#[serde(default)]
pub segments: Vec<SerializedMessageSegment>,
#[serde(default)]
pub tool_uses: Vec<SerializedToolUse>,
#[serde(default)]
pub tool_results: Vec<SerializedToolResult>,
#[serde(default)]
pub context: String,
#[serde(default)]
pub creases: Vec<SerializedCrease>,
#[serde(default)]
pub is_hidden: bool,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum SerializedMessageSegment {
#[serde(rename = "text")]
Text {
text: String,
},
#[serde(rename = "thinking")]
Thinking {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
signature: Option<String>,
},
RedactedThinking {
data: String,
},
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct SerializedToolUse {
pub id: LanguageModelToolUseId,
pub name: SharedString,
pub input: serde_json::Value,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct SerializedToolResult {
pub tool_use_id: LanguageModelToolUseId,
pub is_error: bool,
pub content: LanguageModelToolResultContent,
pub output: Option<serde_json::Value>,
}
#[derive(Serialize, Deserialize)]
struct LegacySerializedThread {
pub summary: SharedString,
pub updated_at: DateTime<Utc>,
pub messages: Vec<LegacySerializedMessage>,
#[serde(default)]
pub initial_project_snapshot: Option<Arc<ProjectSnapshot>>,
}
impl LegacySerializedThread {
pub fn upgrade(self) -> SerializedThread {
SerializedThread {
version: SerializedThread::VERSION.to_string(),
summary: self.summary,
updated_at: self.updated_at,
messages: self.messages.into_iter().map(|msg| msg.upgrade()).collect(),
initial_project_snapshot: self.initial_project_snapshot,
cumulative_token_usage: TokenUsage::default(),
request_token_usage: Vec::new(),
detailed_summary_state: DetailedSummaryState::default(),
model: None,
completion_mode: None,
tool_use_limit_reached: false,
profile: None,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
struct LegacySerializedMessage {
pub id: MessageId,
pub role: Role,
pub text: String,
#[serde(default)]
pub tool_uses: Vec<SerializedToolUse>,
#[serde(default)]
pub tool_results: Vec<SerializedToolResult>,
}
impl LegacySerializedMessage {
fn upgrade(self) -> SerializedMessage {
SerializedMessage {
id: self.id,
role: self.role,
segments: vec![SerializedMessageSegment::Text { text: self.text }],
tool_uses: self.tool_uses,
tool_results: self.tool_results,
context: String::new(),
creases: Vec::new(),
is_hidden: false,
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct SerializedCrease {
pub start: usize,
pub end: usize,
pub icon_path: SharedString,
pub label: SharedString,
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use language_model::{Role, TokenUsage};
use pretty_assertions::assert_eq;
#[test]
fn test_legacy_serialized_thread_upgrade() {
let updated_at = Utc::now();
let legacy_thread = LegacySerializedThread {
summary: "Test conversation".into(),
updated_at,
messages: vec![LegacySerializedMessage {
id: MessageId(1),
role: Role::User,
text: "Hello, world!".to_string(),
tool_uses: vec![],
tool_results: vec![],
}],
initial_project_snapshot: None,
};
let upgraded = legacy_thread.upgrade();
assert_eq!(
upgraded,
SerializedThread {
summary: "Test conversation".into(),
updated_at,
messages: vec![SerializedMessage {
id: MessageId(1),
role: Role::User,
segments: vec![SerializedMessageSegment::Text {
text: "Hello, world!".to_string()
}],
tool_uses: vec![],
tool_results: vec![],
context: "".to_string(),
creases: vec![],
is_hidden: false
}],
version: SerializedThread::VERSION.to_string(),
initial_project_snapshot: None,
cumulative_token_usage: TokenUsage::default(),
request_token_usage: vec![],
detailed_summary_state: DetailedSummaryState::default(),
model: None,
completion_mode: None,
tool_use_limit_reached: false,
profile: None
}
)
}
#[test]
fn test_serialized_threadv0_1_0_upgrade() {
let updated_at = Utc::now();
let thread_v0_1_0 = SerializedThreadV0_1_0(SerializedThread {
summary: "Test conversation".into(),
updated_at,
messages: vec![
SerializedMessage {
id: MessageId(1),
role: Role::User,
segments: vec![SerializedMessageSegment::Text {
text: "Use tool_1".to_string(),
}],
tool_uses: vec![],
tool_results: vec![],
context: "".to_string(),
creases: vec![],
is_hidden: false,
},
SerializedMessage {
id: MessageId(2),
role: Role::Assistant,
segments: vec![SerializedMessageSegment::Text {
text: "I want to use a tool".to_string(),
}],
tool_uses: vec![SerializedToolUse {
id: "abc".into(),
name: "tool_1".into(),
input: serde_json::Value::Null,
}],
tool_results: vec![],
context: "".to_string(),
creases: vec![],
is_hidden: false,
},
SerializedMessage {
id: MessageId(1),
role: Role::User,
segments: vec![SerializedMessageSegment::Text {
text: "Here is the tool result".to_string(),
}],
tool_uses: vec![],
tool_results: vec![SerializedToolResult {
tool_use_id: "abc".into(),
is_error: false,
content: LanguageModelToolResultContent::Text("abcdef".into()),
output: Some(serde_json::Value::Null),
}],
context: "".to_string(),
creases: vec![],
is_hidden: false,
},
],
version: SerializedThreadV0_1_0::VERSION.to_string(),
initial_project_snapshot: None,
cumulative_token_usage: TokenUsage::default(),
request_token_usage: vec![],
detailed_summary_state: DetailedSummaryState::default(),
model: None,
completion_mode: None,
tool_use_limit_reached: false,
profile: None,
});
let upgraded = thread_v0_1_0.upgrade();
assert_eq!(
upgraded,
SerializedThread {
summary: "Test conversation".into(),
updated_at,
messages: vec![
SerializedMessage {
id: MessageId(1),
role: Role::User,
segments: vec![SerializedMessageSegment::Text {
text: "Use tool_1".to_string()
}],
tool_uses: vec![],
tool_results: vec![],
context: "".to_string(),
creases: vec![],
is_hidden: false
},
SerializedMessage {
id: MessageId(2),
role: Role::Assistant,
segments: vec![SerializedMessageSegment::Text {
text: "I want to use a tool".to_string(),
}],
tool_uses: vec![SerializedToolUse {
id: "abc".into(),
name: "tool_1".into(),
input: serde_json::Value::Null,
}],
tool_results: vec![SerializedToolResult {
tool_use_id: "abc".into(),
is_error: false,
content: LanguageModelToolResultContent::Text("abcdef".into()),
output: Some(serde_json::Value::Null),
}],
context: "".to_string(),
creases: vec![],
is_hidden: false,
},
],
version: SerializedThread::VERSION.to_string(),
initial_project_snapshot: None,
cumulative_token_usage: TokenUsage::default(),
request_token_usage: vec![],
detailed_summary_state: DetailedSummaryState::default(),
model: None,
completion_mode: None,
tool_use_limit_reached: false,
profile: None
}
)
}
}

View File

@@ -1,8 +1,6 @@
use action_log::ActionLog; use anyhow::Result;
use anyhow::{Context as _, Result};
use gpui::{AsyncApp, Entity}; use gpui::{AsyncApp, Entity};
use language::{Buffer, OutlineItem, ParseStatus}; use language::{Buffer, OutlineItem, ParseStatus};
use project::Project;
use regex::Regex; use regex::Regex;
use std::fmt::Write; use std::fmt::Write;
use text::Point; use text::Point;
@@ -11,51 +9,66 @@ use text::Point;
/// we automatically provide the file's symbol outline instead, with line numbers. /// we automatically provide the file's symbol outline instead, with line numbers.
pub const AUTO_OUTLINE_SIZE: usize = 16384; pub const AUTO_OUTLINE_SIZE: usize = 16384;
pub async fn file_outline( /// Result of getting buffer content, which can be either full content or an outline.
project: Entity<Project>, pub struct BufferContent {
path: String, /// The actual content (either full text or outline)
action_log: Entity<ActionLog>, pub text: String,
regex: Option<Regex>, /// Whether this is an outline (true) or full content (false)
cx: &mut AsyncApp, pub is_outline: bool,
) -> anyhow::Result<String> {
let buffer = {
let project_path = project.read_with(cx, |project, cx| {
project
.find_project_path(&path, cx)
.with_context(|| format!("Path {path} not found in project"))
})??;
project
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
.await?
};
action_log.update(cx, |action_log, cx| {
action_log.buffer_read(buffer.clone(), cx);
})?;
// Wait until the buffer has been fully parsed, so that we can read its outline.
let mut parse_status = buffer.read_with(cx, |buffer, _| buffer.parse_status())?;
while *parse_status.borrow() != ParseStatus::Idle {
parse_status.changed().await?;
}
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
let outline = snapshot.outline(None);
render_outline(
outline
.items
.into_iter()
.map(|item| item.to_point(&snapshot)),
regex,
0,
usize::MAX,
)
.await
} }
pub async fn render_outline( /// Returns either the full content of a buffer or its outline, depending on size.
/// For files larger than AUTO_OUTLINE_SIZE, returns an outline with a header.
/// For smaller files, returns the full content.
pub async fn get_buffer_content_or_outline(
buffer: Entity<Buffer>,
path: Option<&str>,
cx: &AsyncApp,
) -> Result<BufferContent> {
let file_size = buffer.read_with(cx, |buffer, _| buffer.text().len())?;
if file_size > AUTO_OUTLINE_SIZE {
// For large files, use outline instead of full content
// Wait until the buffer has been fully parsed, so we can read its outline
let mut parse_status = buffer.read_with(cx, |buffer, _| buffer.parse_status())?;
while *parse_status.borrow() != ParseStatus::Idle {
parse_status.changed().await?;
}
let outline_items = buffer.read_with(cx, |buffer, _| {
let snapshot = buffer.snapshot();
snapshot
.outline(None)
.items
.into_iter()
.map(|item| item.to_point(&snapshot))
.collect::<Vec<_>>()
})?;
let outline_text = render_outline(outline_items, None, 0, usize::MAX).await?;
let text = if let Some(path) = path {
format!(
"# File outline for {path} (file too large to show full content)\n\n{outline_text}",
)
} else {
format!("# File outline (file too large to show full content)\n\n{outline_text}",)
};
Ok(BufferContent {
text,
is_outline: true,
})
} else {
// File is small enough, return full content
let text = buffer.read_with(cx, |buffer, _| buffer.text())?;
Ok(BufferContent {
text,
is_outline: false,
})
}
}
async fn render_outline(
items: impl IntoIterator<Item = OutlineItem<Point>>, items: impl IntoIterator<Item = OutlineItem<Point>>,
regex: Option<Regex>, regex: Option<Regex>,
offset: usize, offset: usize,
@@ -128,62 +141,3 @@ fn render_entries(
entries_rendered entries_rendered
} }
/// Result of getting buffer content, which can be either full content or an outline.
pub struct BufferContent {
/// The actual content (either full text or outline)
pub text: String,
/// Whether this is an outline (true) or full content (false)
pub is_outline: bool,
}
/// Returns either the full content of a buffer or its outline, depending on size.
/// For files larger than AUTO_OUTLINE_SIZE, returns an outline with a header.
/// For smaller files, returns the full content.
pub async fn get_buffer_content_or_outline(
buffer: Entity<Buffer>,
path: Option<&str>,
cx: &AsyncApp,
) -> Result<BufferContent> {
let file_size = buffer.read_with(cx, |buffer, _| buffer.text().len())?;
if file_size > AUTO_OUTLINE_SIZE {
// For large files, use outline instead of full content
// Wait until the buffer has been fully parsed, so we can read its outline
let mut parse_status = buffer.read_with(cx, |buffer, _| buffer.parse_status())?;
while *parse_status.borrow() != ParseStatus::Idle {
parse_status.changed().await?;
}
let outline_items = buffer.read_with(cx, |buffer, _| {
let snapshot = buffer.snapshot();
snapshot
.outline(None)
.items
.into_iter()
.map(|item| item.to_point(&snapshot))
.collect::<Vec<_>>()
})?;
let outline_text = render_outline(outline_items, None, 0, usize::MAX).await?;
let text = if let Some(path) = path {
format!(
"# File outline for {path} (file too large to show full content)\n\n{outline_text}",
)
} else {
format!("# File outline (file too large to show full content)\n\n{outline_text}",)
};
Ok(BufferContent {
text,
is_outline: true,
})
} else {
// File is small enough, return full content
let text = buffer.read_with(cx, |buffer, _| buffer.text())?;
Ok(BufferContent {
text,
is_outline: false,
})
}
}

View File

@@ -1,3 +0,0 @@
[The following is an auto-generated notification; do not reply]
These files have changed since the last read:

View File

@@ -975,9 +975,9 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
vec![context_server::types::Tool { vec![context_server::types::Tool {
name: "echo".into(), name: "echo".into(),
description: None, description: None,
input_schema: serde_json::to_value( input_schema: serde_json::to_value(EchoTool::input_schema(
EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema), LanguageModelToolSchemaFormat::JsonSchema,
) ))
.unwrap(), .unwrap(),
output_schema: None, output_schema: None,
annotations: None, annotations: None,
@@ -1149,9 +1149,9 @@ async fn test_mcp_tool_truncation(cx: &mut TestAppContext) {
context_server::types::Tool { context_server::types::Tool {
name: "echo".into(), // Conflicts with native EchoTool name: "echo".into(), // Conflicts with native EchoTool
description: None, description: None,
input_schema: serde_json::to_value( input_schema: serde_json::to_value(EchoTool::input_schema(
EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema), LanguageModelToolSchemaFormat::JsonSchema,
) ))
.unwrap(), .unwrap(),
output_schema: None, output_schema: None,
annotations: None, annotations: None,
@@ -1174,9 +1174,9 @@ async fn test_mcp_tool_truncation(cx: &mut TestAppContext) {
context_server::types::Tool { context_server::types::Tool {
name: "echo".into(), // Also conflicts with native EchoTool name: "echo".into(), // Also conflicts with native EchoTool
description: None, description: None,
input_schema: serde_json::to_value( input_schema: serde_json::to_value(EchoTool::input_schema(
EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema), LanguageModelToolSchemaFormat::JsonSchema,
) ))
.unwrap(), .unwrap(),
output_schema: None, output_schema: None,
annotations: None, annotations: None,
@@ -1864,7 +1864,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
let selector_opt = connection.model_selector(&session_id); let selector_opt = connection.model_selector(&session_id);
assert!( assert!(
selector_opt.is_some(), selector_opt.is_some(),
"agent2 should always support ModelSelector" "agent should always support ModelSelector"
); );
let selector = selector_opt.unwrap(); let selector = selector_opt.unwrap();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,48 @@
use anyhow::Result; use anyhow::Result;
use language_model::LanguageModelToolSchemaFormat;
use schemars::{
JsonSchema, Schema,
generate::SchemaSettings,
transform::{Transform, transform_subschemas},
};
use serde_json::Value; use serde_json::Value;
use crate::LanguageModelToolSchemaFormat; pub(crate) fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> Schema {
let mut generator = match format {
LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(),
LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::openapi3()
.with(|settings| {
settings.meta_schema = None;
settings.inline_subschemas = true;
})
.with_transform(ToJsonSchemaSubsetTransform)
.into_generator(),
};
generator.root_schema_for::<T>()
}
#[derive(Debug, Clone)]
struct ToJsonSchemaSubsetTransform;
impl Transform for ToJsonSchemaSubsetTransform {
fn transform(&mut self, schema: &mut Schema) {
// Ensure that the type field is not an array, this happens when we use
// Option<T>, the type will be [T, "null"].
if let Some(type_field) = schema.get_mut("type")
&& let Some(types) = type_field.as_array()
&& let Some(first_type) = types.first()
{
*type_field = first_type.clone();
}
// oneOf is not supported, use anyOf instead
if let Some(one_of) = schema.remove("oneOf") {
schema.insert("anyOf".to_string(), one_of);
}
transform_subschemas(self, schema);
}
}
/// Tries to adapt a JSON schema representation to be compatible with the specified format. /// Tries to adapt a JSON schema representation to be compatible with the specified format.
/// ///

View File

@@ -1,575 +0,0 @@
use crate::{
thread::{MessageId, PromptId, ThreadId},
thread_store::SerializedMessage,
};
use agent_settings::CompletionMode;
use anyhow::Result;
use assistant_tool::{
AnyToolCard, Tool, ToolResultContent, ToolResultOutput, ToolUseStatus, ToolWorkingSet,
};
use collections::HashMap;
use futures::{FutureExt as _, future::Shared};
use gpui::{App, Entity, SharedString, Task, Window};
use icons::IconName;
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelExt, LanguageModelRequest,
LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolUse,
LanguageModelToolUseId, Role,
};
use project::Project;
use std::sync::Arc;
use util::truncate_lines_to_byte_limit;
#[derive(Debug)]
pub struct ToolUse {
pub id: LanguageModelToolUseId,
pub name: SharedString,
pub ui_text: SharedString,
pub status: ToolUseStatus,
pub input: serde_json::Value,
pub icon: icons::IconName,
pub needs_confirmation: bool,
}
pub struct ToolUseState {
tools: Entity<ToolWorkingSet>,
tool_uses_by_assistant_message: HashMap<MessageId, Vec<LanguageModelToolUse>>,
tool_results: HashMap<LanguageModelToolUseId, LanguageModelToolResult>,
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
tool_result_cards: HashMap<LanguageModelToolUseId, AnyToolCard>,
tool_use_metadata_by_id: HashMap<LanguageModelToolUseId, ToolUseMetadata>,
}
impl ToolUseState {
pub fn new(tools: Entity<ToolWorkingSet>) -> Self {
Self {
tools,
tool_uses_by_assistant_message: HashMap::default(),
tool_results: HashMap::default(),
pending_tool_uses_by_id: HashMap::default(),
tool_result_cards: HashMap::default(),
tool_use_metadata_by_id: HashMap::default(),
}
}
/// Constructs a [`ToolUseState`] from the given list of [`SerializedMessage`]s.
///
/// Accepts a function to filter the tools that should be used to populate the state.
///
/// If `window` is `None` (e.g., when in headless mode or when running evals),
/// tool cards won't be deserialized
pub fn from_serialized_messages(
tools: Entity<ToolWorkingSet>,
messages: &[SerializedMessage],
project: Entity<Project>,
window: Option<&mut Window>, // None in headless mode
cx: &mut App,
) -> Self {
let mut this = Self::new(tools);
let mut tool_names_by_id = HashMap::default();
let mut window = window;
for message in messages {
match message.role {
Role::Assistant => {
if !message.tool_uses.is_empty() {
let tool_uses = message
.tool_uses
.iter()
.map(|tool_use| LanguageModelToolUse {
id: tool_use.id.clone(),
name: tool_use.name.clone().into(),
raw_input: tool_use.input.to_string(),
input: tool_use.input.clone(),
is_input_complete: true,
})
.collect::<Vec<_>>();
tool_names_by_id.extend(
tool_uses
.iter()
.map(|tool_use| (tool_use.id.clone(), tool_use.name.clone())),
);
this.tool_uses_by_assistant_message
.insert(message.id, tool_uses);
for tool_result in &message.tool_results {
let tool_use_id = tool_result.tool_use_id.clone();
let Some(tool_use) = tool_names_by_id.get(&tool_use_id) else {
log::warn!("no tool name found for tool use: {tool_use_id:?}");
continue;
};
this.tool_results.insert(
tool_use_id.clone(),
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
tool_name: tool_use.clone(),
is_error: tool_result.is_error,
content: tool_result.content.clone(),
output: tool_result.output.clone(),
},
);
if let Some(window) = &mut window
&& let Some(tool) = this.tools.read(cx).tool(tool_use, cx)
&& let Some(output) = tool_result.output.clone()
&& let Some(card) =
tool.deserialize_card(output, project.clone(), window, cx)
{
this.tool_result_cards.insert(tool_use_id, card);
}
}
}
}
Role::System | Role::User => {}
}
}
this
}
pub fn cancel_pending(&mut self) -> Vec<PendingToolUse> {
let mut canceled_tool_uses = Vec::new();
self.pending_tool_uses_by_id
.retain(|tool_use_id, tool_use| {
if matches!(tool_use.status, PendingToolUseStatus::Error { .. }) {
return true;
}
let content = "Tool canceled by user".into();
self.tool_results.insert(
tool_use_id.clone(),
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
tool_name: tool_use.name.clone(),
content,
output: None,
is_error: true,
},
);
canceled_tool_uses.push(tool_use.clone());
false
});
canceled_tool_uses
}
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
self.pending_tool_uses_by_id.values().collect()
}
pub fn tool_uses_for_message(
&self,
id: MessageId,
project: &Entity<Project>,
cx: &App,
) -> Vec<ToolUse> {
let Some(tool_uses_for_message) = &self.tool_uses_by_assistant_message.get(&id) else {
return Vec::new();
};
let mut tool_uses = Vec::new();
for tool_use in tool_uses_for_message.iter() {
let tool_result = self.tool_results.get(&tool_use.id);
let status = (|| {
if let Some(tool_result) = tool_result {
let content = tool_result
.content
.to_str()
.map(|str| str.to_owned().into())
.unwrap_or_default();
return if tool_result.is_error {
ToolUseStatus::Error(content)
} else {
ToolUseStatus::Finished(content)
};
}
if let Some(pending_tool_use) = self.pending_tool_uses_by_id.get(&tool_use.id) {
match pending_tool_use.status {
PendingToolUseStatus::Idle => ToolUseStatus::Pending,
PendingToolUseStatus::NeedsConfirmation { .. } => {
ToolUseStatus::NeedsConfirmation
}
PendingToolUseStatus::Running { .. } => ToolUseStatus::Running,
PendingToolUseStatus::Error(ref err) => {
ToolUseStatus::Error(err.clone().into())
}
PendingToolUseStatus::InputStillStreaming => {
ToolUseStatus::InputStillStreaming
}
}
} else {
ToolUseStatus::Pending
}
})();
let (icon, needs_confirmation) =
if let Some(tool) = self.tools.read(cx).tool(&tool_use.name, cx) {
(
tool.icon(),
tool.needs_confirmation(&tool_use.input, project, cx),
)
} else {
(IconName::Cog, false)
};
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,
tool_use.is_input_complete,
cx,
),
input: tool_use.input.clone(),
status,
icon,
needs_confirmation,
})
}
tool_uses
}
pub fn tool_ui_label(
&self,
tool_name: &str,
input: &serde_json::Value,
is_input_complete: bool,
cx: &App,
) -> SharedString {
if let Some(tool) = self.tools.read(cx).tool(tool_name, cx) {
if is_input_complete {
tool.ui_text(input).into()
} else {
tool.still_streaming_ui_text(input).into()
}
} else {
format!("Unknown tool {tool_name:?}").into()
}
}
pub fn tool_results_for_message(
&self,
assistant_message_id: MessageId,
) -> Vec<&LanguageModelToolResult> {
let Some(tool_uses) = self
.tool_uses_by_assistant_message
.get(&assistant_message_id)
else {
return Vec::new();
};
tool_uses
.iter()
.filter_map(|tool_use| self.tool_results.get(&tool_use.id))
.collect()
}
pub fn message_has_tool_results(&self, assistant_message_id: MessageId) -> bool {
self.tool_uses_by_assistant_message
.get(&assistant_message_id)
.is_some_and(|results| !results.is_empty())
}
pub fn tool_result(
&self,
tool_use_id: &LanguageModelToolUseId,
) -> Option<&LanguageModelToolResult> {
self.tool_results.get(tool_use_id)
}
pub fn tool_result_card(&self, tool_use_id: &LanguageModelToolUseId) -> Option<&AnyToolCard> {
self.tool_result_cards.get(tool_use_id)
}
pub fn insert_tool_result_card(
&mut self,
tool_use_id: LanguageModelToolUseId,
card: AnyToolCard,
) {
self.tool_result_cards.insert(tool_use_id, card);
}
pub fn request_tool_use(
&mut self,
assistant_message_id: MessageId,
tool_use: LanguageModelToolUse,
metadata: ToolUseMetadata,
cx: &App,
) -> Arc<str> {
let tool_uses = self
.tool_uses_by_assistant_message
.entry(assistant_message_id)
.or_default();
let mut existing_tool_use_found = false;
for existing_tool_use in tool_uses.iter_mut() {
if existing_tool_use.id == tool_use.id {
*existing_tool_use = tool_use.clone();
existing_tool_use_found = true;
}
}
if !existing_tool_use_found {
tool_uses.push(tool_use.clone());
}
let status = if tool_use.is_input_complete {
self.tool_use_metadata_by_id
.insert(tool_use.id.clone(), metadata);
PendingToolUseStatus::Idle
} else {
PendingToolUseStatus::InputStillStreaming
};
let ui_text: Arc<str> = self
.tool_ui_label(
&tool_use.name,
&tool_use.input,
tool_use.is_input_complete,
cx,
)
.into();
let may_perform_edits = self
.tools
.read(cx)
.tool(&tool_use.name, cx)
.is_some_and(|tool| tool.may_perform_edits());
self.pending_tool_uses_by_id.insert(
tool_use.id.clone(),
PendingToolUse {
assistant_message_id,
id: tool_use.id,
name: tool_use.name.clone(),
ui_text: ui_text.clone(),
input: tool_use.input,
may_perform_edits,
status,
},
);
ui_text
}
pub fn run_pending_tool(
&mut self,
tool_use_id: LanguageModelToolUseId,
ui_text: SharedString,
task: Task<()>,
) {
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
tool_use.ui_text = ui_text.into();
tool_use.status = PendingToolUseStatus::Running {
_task: task.shared(),
};
}
}
pub fn confirm_tool_use(
&mut self,
tool_use_id: LanguageModelToolUseId,
ui_text: impl Into<Arc<str>>,
input: serde_json::Value,
request: Arc<LanguageModelRequest>,
tool: Arc<dyn Tool>,
) {
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
let ui_text = ui_text.into();
tool_use.ui_text = ui_text.clone();
let confirmation = Confirmation {
tool_use_id,
input,
request,
tool,
ui_text,
};
tool_use.status = PendingToolUseStatus::NeedsConfirmation(Arc::new(confirmation));
}
}
pub fn insert_tool_output(
&mut self,
tool_use_id: LanguageModelToolUseId,
tool_name: Arc<str>,
output: Result<ToolResultOutput>,
configured_model: Option<&ConfiguredModel>,
completion_mode: CompletionMode,
) -> Option<PendingToolUse> {
let metadata = self.tool_use_metadata_by_id.remove(&tool_use_id);
telemetry::event!(
"Agent Tool Finished",
model = metadata
.as_ref()
.map(|metadata| metadata.model.telemetry_id()),
model_provider = metadata
.as_ref()
.map(|metadata| metadata.model.provider_id().to_string()),
thread_id = metadata.as_ref().map(|metadata| metadata.thread_id.clone()),
prompt_id = metadata.as_ref().map(|metadata| metadata.prompt_id.clone()),
tool_name,
success = output.is_ok()
);
match output {
Ok(output) => {
let tool_result = output.content;
const BYTES_PER_TOKEN_ESTIMATE: usize = 3;
let old_use = self.pending_tool_uses_by_id.remove(&tool_use_id);
// Protect from overly large output
let tool_output_limit = configured_model
.map(|model| {
model.model.max_token_count_for_mode(completion_mode.into()) as usize
* BYTES_PER_TOKEN_ESTIMATE
})
.unwrap_or(usize::MAX);
let content = match tool_result {
ToolResultContent::Text(text) => {
let text = if text.len() < tool_output_limit {
text
} else {
let truncated = truncate_lines_to_byte_limit(&text, tool_output_limit);
format!(
"Tool result too long. The first {} bytes:\n\n{}",
truncated.len(),
truncated
)
};
LanguageModelToolResultContent::Text(text.into())
}
ToolResultContent::Image(language_model_image) => {
if language_model_image.estimate_tokens() < tool_output_limit {
LanguageModelToolResultContent::Image(language_model_image)
} else {
self.tool_results.insert(
tool_use_id.clone(),
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
tool_name,
content: "Tool responded with an image that would exceeded the remaining tokens".into(),
is_error: true,
output: None,
},
);
return old_use;
}
}
};
self.tool_results.insert(
tool_use_id.clone(),
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
tool_name,
content,
is_error: false,
output: output.output,
},
);
old_use
}
Err(err) => {
self.tool_results.insert(
tool_use_id.clone(),
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
tool_name,
content: LanguageModelToolResultContent::Text(err.to_string().into()),
is_error: true,
output: None,
},
);
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
tool_use.status = PendingToolUseStatus::Error(err.to_string().into());
}
self.pending_tool_uses_by_id.get(&tool_use_id).cloned()
}
}
}
pub fn has_tool_results(&self, assistant_message_id: MessageId) -> bool {
self.tool_uses_by_assistant_message
.contains_key(&assistant_message_id)
}
pub fn tool_results(
&self,
assistant_message_id: MessageId,
) -> impl Iterator<Item = (&LanguageModelToolUse, Option<&LanguageModelToolResult>)> {
self.tool_uses_by_assistant_message
.get(&assistant_message_id)
.into_iter()
.flatten()
.map(|tool_use| (tool_use, self.tool_results.get(&tool_use.id)))
}
}
#[derive(Debug, Clone)]
pub struct PendingToolUse {
pub id: LanguageModelToolUseId,
/// The ID of the Assistant message in which the tool use was requested.
#[allow(unused)]
pub assistant_message_id: MessageId,
pub name: Arc<str>,
pub ui_text: Arc<str>,
pub input: serde_json::Value,
pub status: PendingToolUseStatus,
pub may_perform_edits: bool,
}
#[derive(Debug, Clone)]
pub struct Confirmation {
pub tool_use_id: LanguageModelToolUseId,
pub input: serde_json::Value,
pub ui_text: Arc<str>,
pub request: Arc<LanguageModelRequest>,
pub tool: Arc<dyn Tool>,
}
#[derive(Debug, Clone)]
pub enum PendingToolUseStatus {
InputStillStreaming,
Idle,
NeedsConfirmation(Arc<Confirmation>),
Running { _task: Shared<Task<()>> },
Error(#[allow(unused)] Arc<str>),
}
impl PendingToolUseStatus {
pub fn is_idle(&self) -> bool {
matches!(self, PendingToolUseStatus::Idle)
}
pub fn is_error(&self) -> bool {
matches!(self, PendingToolUseStatus::Error(_))
}
pub fn needs_confirmation(&self) -> bool {
matches!(self, PendingToolUseStatus::NeedsConfirmation { .. })
}
}
#[derive(Clone)]
pub struct ToolUseMetadata {
pub model: Arc<dyn LanguageModel>,
pub thread_id: ThreadId,
pub prompt_id: PromptId,
}

88
crates/agent/src/tools.rs Normal file
View File

@@ -0,0 +1,88 @@
mod context_server_registry;
mod copy_path_tool;
mod create_directory_tool;
mod delete_path_tool;
mod diagnostics_tool;
mod edit_file_tool;
mod fetch_tool;
mod find_path_tool;
mod grep_tool;
mod list_directory_tool;
mod move_path_tool;
mod now_tool;
mod open_tool;
mod read_file_tool;
mod terminal_tool;
mod thinking_tool;
mod web_search_tool;
use crate::AgentTool;
use language_model::{LanguageModelRequestTool, LanguageModelToolSchemaFormat};
pub use context_server_registry::*;
pub use copy_path_tool::*;
pub use create_directory_tool::*;
pub use delete_path_tool::*;
pub use diagnostics_tool::*;
pub use edit_file_tool::*;
pub use fetch_tool::*;
pub use find_path_tool::*;
pub use grep_tool::*;
pub use list_directory_tool::*;
pub use move_path_tool::*;
pub use now_tool::*;
pub use open_tool::*;
pub use read_file_tool::*;
pub use terminal_tool::*;
pub use thinking_tool::*;
pub use web_search_tool::*;
macro_rules! tools {
($($tool:ty),* $(,)?) => {
/// A list of all built-in tool names
pub fn built_in_tool_names() -> impl Iterator<Item = String> {
[
$(
<$tool>::name().to_string(),
)*
]
.into_iter()
}
/// A list of all built-in tools
pub fn built_in_tools() -> impl Iterator<Item = LanguageModelRequestTool> {
fn language_model_tool<T: AgentTool>() -> LanguageModelRequestTool {
LanguageModelRequestTool {
name: T::name().to_string(),
description: T::description().to_string(),
input_schema: T::input_schema(LanguageModelToolSchemaFormat::JsonSchema).to_value(),
}
}
[
$(
language_model_tool::<$tool>(),
)*
]
.into_iter()
}
};
}
tools! {
CopyPathTool,
CreateDirectoryTool,
DeletePathTool,
DiagnosticsTool,
EditFileTool,
FetchTool,
FindPathTool,
GrepTool,
ListDirectoryTool,
MovePathTool,
NowTool,
OpenTool,
ReadFileTool,
TerminalTool,
ThinkingTool,
WebSearchTool,
}

View File

@@ -32,6 +32,17 @@ impl ContextServerRegistry {
this this
} }
pub fn tools_for_server(
&self,
server_id: &ContextServerId,
) -> impl Iterator<Item = &Arc<dyn AnyAgentTool>> {
self.registered_servers
.get(server_id)
.map(|server| server.tools.values())
.into_iter()
.flatten()
}
pub fn servers( pub fn servers(
&self, &self,
) -> impl Iterator< ) -> impl Iterator<
@@ -154,7 +165,7 @@ impl AnyAgentTool for ContextServerTool {
format: language_model::LanguageModelToolSchemaFormat, format: language_model::LanguageModelToolSchemaFormat,
) -> Result<serde_json::Value> { ) -> Result<serde_json::Value> {
let mut schema = self.tool.input_schema.clone(); let mut schema = self.tool.input_schema.clone();
assistant_tool::adapt_schema_to_format(&mut schema, format)?; crate::tool_schema::adapt_schema_to_format(&mut schema, format)?;
Ok(match schema { Ok(match schema {
serde_json::Value::Null => { serde_json::Value::Null => {
serde_json::json!({ "type": "object", "properties": [] }) serde_json::json!({ "type": "object", "properties": [] })

View File

@@ -1,8 +1,10 @@
use crate::{AgentTool, Thread, ToolCallEventStream}; use crate::{
AgentTool, Templates, Thread, ToolCallEventStream,
edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat},
};
use acp_thread::Diff; use acp_thread::Diff;
use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields}; use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields};
use anyhow::{Context as _, Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
use assistant_tools::edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat};
use cloud_llm_client::CompletionIntent; use cloud_llm_client::CompletionIntent;
use collections::HashSet; use collections::HashSet;
use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity}; use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
@@ -34,7 +36,7 @@ const DEFAULT_UI_TEXT: &str = "Editing file";
/// ///
/// 2. Verify the directory path is correct (only applicable when creating new files): /// 2. Verify the directory path is correct (only applicable when creating new files):
/// - Use the `list_directory` tool to verify the parent directory exists and is the correct location /// - Use the `list_directory` tool to verify the parent directory exists and is the correct location
#[derive(Debug, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFileToolInput { pub struct EditFileToolInput {
/// A one-line, user-friendly markdown description of the edit. This will be shown in the UI and also passed to another model to perform the edit. /// A one-line, user-friendly markdown description of the edit. This will be shown in the UI and also passed to another model to perform the edit.
/// ///
@@ -75,7 +77,7 @@ pub struct EditFileToolInput {
pub mode: EditFileMode, pub mode: EditFileMode,
} }
#[derive(Debug, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
struct EditFileToolPartialInput { struct EditFileToolPartialInput {
#[serde(default)] #[serde(default)]
path: String, path: String,
@@ -123,6 +125,7 @@ pub struct EditFileTool {
thread: WeakEntity<Thread>, thread: WeakEntity<Thread>,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
project: Entity<Project>, project: Entity<Project>,
templates: Arc<Templates>,
} }
impl EditFileTool { impl EditFileTool {
@@ -130,11 +133,13 @@ impl EditFileTool {
project: Entity<Project>, project: Entity<Project>,
thread: WeakEntity<Thread>, thread: WeakEntity<Thread>,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
templates: Arc<Templates>,
) -> Self { ) -> Self {
Self { Self {
project, project,
thread, thread,
language_registry, language_registry,
templates,
} }
} }
@@ -294,8 +299,7 @@ impl AgentTool for EditFileTool {
model, model,
project.clone(), project.clone(),
action_log.clone(), action_log.clone(),
// TODO: move edit agent to this crate so we can use our templates self.templates.clone(),
assistant_tools::templates::Templates::new(),
edit_format, edit_format,
); );
@@ -599,6 +603,7 @@ mod tests {
project, project,
thread.downgrade(), thread.downgrade(),
language_registry, language_registry,
Templates::new(),
)) ))
.run(input, ToolCallEventStream::test().0, cx) .run(input, ToolCallEventStream::test().0, cx)
}) })
@@ -807,6 +812,7 @@ mod tests {
project.clone(), project.clone(),
thread.downgrade(), thread.downgrade(),
language_registry.clone(), language_registry.clone(),
Templates::new(),
)) ))
.run(input, ToolCallEventStream::test().0, cx) .run(input, ToolCallEventStream::test().0, cx)
}); });
@@ -865,6 +871,7 @@ mod tests {
project.clone(), project.clone(),
thread.downgrade(), thread.downgrade(),
language_registry, language_registry,
Templates::new(),
)) ))
.run(input, ToolCallEventStream::test().0, cx) .run(input, ToolCallEventStream::test().0, cx)
}); });
@@ -951,6 +958,7 @@ mod tests {
project.clone(), project.clone(),
thread.downgrade(), thread.downgrade(),
language_registry.clone(), language_registry.clone(),
Templates::new(),
)) ))
.run(input, ToolCallEventStream::test().0, cx) .run(input, ToolCallEventStream::test().0, cx)
}); });
@@ -1005,6 +1013,7 @@ mod tests {
project.clone(), project.clone(),
thread.downgrade(), thread.downgrade(),
language_registry, language_registry,
Templates::new(),
)) ))
.run(input, ToolCallEventStream::test().0, cx) .run(input, ToolCallEventStream::test().0, cx)
}); });
@@ -1057,6 +1066,7 @@ mod tests {
project.clone(), project.clone(),
thread.downgrade(), thread.downgrade(),
language_registry, language_registry,
Templates::new(),
)); ));
fs.insert_tree("/root", json!({})).await; fs.insert_tree("/root", json!({})).await;
@@ -1197,6 +1207,7 @@ mod tests {
project.clone(), project.clone(),
thread.downgrade(), thread.downgrade(),
language_registry, language_registry,
Templates::new(),
)); ));
// Test global config paths - these should require confirmation if they exist and are outside the project // Test global config paths - these should require confirmation if they exist and are outside the project
@@ -1309,6 +1320,7 @@ mod tests {
project.clone(), project.clone(),
thread.downgrade(), thread.downgrade(),
language_registry, language_registry,
Templates::new(),
)); ));
// Test files in different worktrees // Test files in different worktrees
@@ -1393,6 +1405,7 @@ mod tests {
project.clone(), project.clone(),
thread.downgrade(), thread.downgrade(),
language_registry, language_registry,
Templates::new(),
)); ));
// Test edge cases // Test edge cases
@@ -1482,6 +1495,7 @@ mod tests {
project.clone(), project.clone(),
thread.downgrade(), thread.downgrade(),
language_registry, language_registry,
Templates::new(),
)); ));
// Test different EditFileMode values // Test different EditFileMode values
@@ -1566,6 +1580,7 @@ mod tests {
project, project,
thread.downgrade(), thread.downgrade(),
language_registry, language_registry,
Templates::new(),
)); ));
cx.update(|cx| { cx.update(|cx| {
@@ -1653,6 +1668,7 @@ mod tests {
project.clone(), project.clone(),
thread.downgrade(), thread.downgrade(),
languages.clone(), languages.clone(),
Templates::new(),
)); ));
let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let edit = cx.update(|cx| { let edit = cx.update(|cx| {
@@ -1682,6 +1698,7 @@ mod tests {
project.clone(), project.clone(),
thread.downgrade(), thread.downgrade(),
languages.clone(), languages.clone(),
Templates::new(),
)); ));
let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let edit = cx.update(|cx| { let edit = cx.update(|cx| {
@@ -1709,6 +1726,7 @@ mod tests {
project.clone(), project.clone(),
thread.downgrade(), thread.downgrade(),
languages.clone(), languages.clone(),
Templates::new(),
)); ));
let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let edit = cx.update(|cx| { let edit = cx.update(|cx| {

View File

@@ -1,7 +1,6 @@
use action_log::ActionLog; use action_log::ActionLog;
use agent_client_protocol::{self as acp, ToolCallUpdateFields}; use agent_client_protocol::{self as acp, ToolCallUpdateFields};
use anyhow::{Context as _, Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
use assistant_tool::outline;
use gpui::{App, Entity, SharedString, Task}; use gpui::{App, Entity, SharedString, Task};
use indoc::formatdoc; use indoc::formatdoc;
use language::Point; use language::Point;
@@ -13,7 +12,7 @@ use settings::Settings;
use std::sync::Arc; use std::sync::Arc;
use util::markdown::MarkdownCodeBlock; use util::markdown::MarkdownCodeBlock;
use crate::{AgentTool, ToolCallEventStream}; use crate::{AgentTool, ToolCallEventStream, outline};
/// Reads the content of the given file in the project. /// Reads the content of the given file in the project.
/// ///

View File

@@ -1,102 +0,0 @@
[package]
name = "agent2"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lib]
path = "src/agent2.rs"
[features]
test-support = ["db/test-support"]
e2e = []
[lints]
workspace = true
[dependencies]
acp_thread.workspace = true
action_log.workspace = true
agent.workspace = true
agent-client-protocol.workspace = true
agent_servers.workspace = true
agent_settings.workspace = true
anyhow.workspace = true
assistant_context.workspace = true
assistant_tool.workspace = true
assistant_tools.workspace = true
chrono.workspace = true
client.workspace = true
cloud_llm_client.workspace = true
collections.workspace = true
context_server.workspace = true
db.workspace = true
fs.workspace = true
futures.workspace = true
git.workspace = true
gpui.workspace = true
handlebars = { workspace = true, features = ["rust-embed"] }
html_to_markdown.workspace = true
http_client.workspace = true
indoc.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
language_models.workspace = true
log.workspace = true
open.workspace = true
parking_lot.workspace = true
paths.workspace = true
project.workspace = true
prompt_store.workspace = true
rust-embed.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
sqlez.workspace = true
task.workspace = true
telemetry.workspace = true
terminal.workspace = true
thiserror.workspace = true
text.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true
watch.workspace = true
web_search.workspace = true
workspace-hack.workspace = true
zed_env_vars.workspace = true
zstd.workspace = true
[dev-dependencies]
agent = { workspace = true, "features" = ["test-support"] }
agent_servers = { workspace = true, "features" = ["test-support"] }
assistant_context = { workspace = true, "features" = ["test-support"] }
ctor.workspace = true
client = { workspace = true, "features" = ["test-support"] }
clock = { workspace = true, "features" = ["test-support"] }
context_server = { workspace = true, "features" = ["test-support"] }
db = { workspace = true, "features" = ["test-support"] }
editor = { workspace = true, "features" = ["test-support"] }
env_logger.workspace = true
fs = { workspace = true, "features" = ["test-support"] }
git = { workspace = true, "features" = ["test-support"] }
gpui = { workspace = true, "features" = ["test-support"] }
gpui_tokio.workspace = true
language = { workspace = true, "features" = ["test-support"] }
language_model = { workspace = true, "features" = ["test-support"] }
lsp = { workspace = true, "features" = ["test-support"] }
pretty_assertions.workspace = true
project = { workspace = true, "features" = ["test-support"] }
reqwest_client.workspace = true
settings = { workspace = true, "features" = ["test-support"] }
tempfile.workspace = true
terminal = { workspace = true, "features" = ["test-support"] }
theme = { workspace = true, "features" = ["test-support"] }
tree-sitter-rust.workspace = true
unindent = { workspace = true }
worktree = { workspace = true, "features" = ["test-support"] }
zlog.workspace = true

View File

@@ -1 +0,0 @@
../../LICENSE-GPL

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +0,0 @@
mod agent;
mod db;
mod history_store;
mod native_agent_server;
mod templates;
mod thread;
mod tool_schema;
mod tools;
#[cfg(test)]
mod tests;
pub use agent::*;
pub use db::*;
pub use history_store::*;
pub use native_agent_server::NativeAgentServer;
pub use templates::*;
pub use thread::*;
pub use tools::*;

File diff suppressed because it is too large Load Diff

View File

@@ -1,43 +0,0 @@
use language_model::LanguageModelToolSchemaFormat;
use schemars::{
JsonSchema, Schema,
generate::SchemaSettings,
transform::{Transform, transform_subschemas},
};
pub(crate) fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> Schema {
let mut generator = match format {
LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(),
LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::openapi3()
.with(|settings| {
settings.meta_schema = None;
settings.inline_subschemas = true;
})
.with_transform(ToJsonSchemaSubsetTransform)
.into_generator(),
};
generator.root_schema_for::<T>()
}
#[derive(Debug, Clone)]
struct ToJsonSchemaSubsetTransform;
impl Transform for ToJsonSchemaSubsetTransform {
fn transform(&mut self, schema: &mut Schema) {
// Ensure that the type field is not an array, this happens when we use
// Option<T>, the type will be [T, "null"].
if let Some(type_field) = schema.get_mut("type")
&& let Some(types) = type_field.as_array()
&& let Some(first_type) = types.first()
{
*type_field = first_type.clone();
}
// oneOf is not supported, use anyOf instead
if let Some(one_of) = schema.remove("oneOf") {
schema.insert("anyOf".to_string(), one_of);
}
transform_subschemas(self, schema);
}
}

View File

@@ -1,60 +0,0 @@
mod context_server_registry;
mod copy_path_tool;
mod create_directory_tool;
mod delete_path_tool;
mod diagnostics_tool;
mod edit_file_tool;
mod fetch_tool;
mod find_path_tool;
mod grep_tool;
mod list_directory_tool;
mod move_path_tool;
mod now_tool;
mod open_tool;
mod read_file_tool;
mod terminal_tool;
mod thinking_tool;
mod web_search_tool;
/// A list of all built in tool names, for use in deduplicating MCP tool names
pub fn default_tool_names() -> impl Iterator<Item = &'static str> {
[
CopyPathTool::name(),
CreateDirectoryTool::name(),
DeletePathTool::name(),
DiagnosticsTool::name(),
EditFileTool::name(),
FetchTool::name(),
FindPathTool::name(),
GrepTool::name(),
ListDirectoryTool::name(),
MovePathTool::name(),
NowTool::name(),
OpenTool::name(),
ReadFileTool::name(),
TerminalTool::name(),
ThinkingTool::name(),
WebSearchTool::name(),
]
.into_iter()
}
pub use context_server_registry::*;
pub use copy_path_tool::*;
pub use create_directory_tool::*;
pub use delete_path_tool::*;
pub use diagnostics_tool::*;
pub use edit_file_tool::*;
pub use fetch_tool::*;
pub use find_path_tool::*;
pub use grep_tool::*;
pub use list_directory_tool::*;
pub use move_path_tool::*;
pub use now_tool::*;
pub use open_tool::*;
pub use read_file_tool::*;
pub use terminal_tool::*;
pub use thinking_tool::*;
pub use web_search_tool::*;
use crate::AgentTool;

View File

@@ -15,10 +15,9 @@ use settings::{
pub use crate::agent_profile::*; pub use crate::agent_profile::*;
pub const SUMMARIZE_THREAD_PROMPT: &str = pub const SUMMARIZE_THREAD_PROMPT: &str = include_str!("prompts/summarize_thread_prompt.txt");
include_str!("../../agent/src/prompts/summarize_thread_prompt.txt");
pub const SUMMARIZE_THREAD_DETAILED_PROMPT: &str = pub const SUMMARIZE_THREAD_DETAILED_PROMPT: &str =
include_str!("../../agent/src/prompts/summarize_thread_detailed_prompt.txt"); include_str!("prompts/summarize_thread_detailed_prompt.txt");
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
AgentSettings::register(cx); AgentSettings::register(cx);

View File

@@ -20,7 +20,6 @@ acp_thread.workspace = true
action_log.workspace = true action_log.workspace = true
agent-client-protocol.workspace = true agent-client-protocol.workspace = true
agent.workspace = true agent.workspace = true
agent2.workspace = true
agent_servers.workspace = true agent_servers.workspace = true
agent_settings.workspace = true agent_settings.workspace = true
ai_onboarding.workspace = true ai_onboarding.workspace = true
@@ -29,7 +28,6 @@ arrayvec.workspace = true
assistant_context.workspace = true assistant_context.workspace = true
assistant_slash_command.workspace = true assistant_slash_command.workspace = true
assistant_slash_commands.workspace = true assistant_slash_commands.workspace = true
assistant_tool.workspace = true
audio.workspace = true audio.workspace = true
buffer_diff.workspace = true buffer_diff.workspace = true
chrono.workspace = true chrono.workspace = true
@@ -71,6 +69,7 @@ postage.workspace = true
project.workspace = true project.workspace = true
prompt_store.workspace = true prompt_store.workspace = true
proto.workspace = true proto.workspace = true
ref-cast.workspace = true
release_channel.workspace = true release_channel.workspace = true
rope.workspace = true rope.workspace = true
rules_library.workspace = true rules_library.workspace = true
@@ -104,9 +103,7 @@ zed_actions.workspace = true
[dev-dependencies] [dev-dependencies]
acp_thread = { workspace = true, features = ["test-support"] } acp_thread = { workspace = true, features = ["test-support"] }
agent = { workspace = true, features = ["test-support"] } agent = { workspace = true, features = ["test-support"] }
agent2 = { workspace = true, features = ["test-support"] }
assistant_context = { workspace = true, features = ["test-support"] } assistant_context = { workspace = true, features = ["test-support"] }
assistant_tools.workspace = true
buffer_diff = { workspace = true, features = ["test-support"] } buffer_diff = { workspace = true, features = ["test-support"] }
db = { workspace = true, features = ["test-support"] } db = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] }

View File

@@ -6,8 +6,8 @@ use std::sync::Arc;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use acp_thread::MentionUri; use acp_thread::MentionUri;
use agent::{HistoryEntry, HistoryStore};
use agent_client_protocol as acp; use agent_client_protocol as acp;
use agent2::{HistoryEntry, HistoryStore};
use anyhow::Result; use anyhow::Result;
use editor::{CompletionProvider, Editor, ExcerptId}; use editor::{CompletionProvider, Editor, ExcerptId};
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
@@ -32,6 +32,7 @@ use crate::context_picker::file_context_picker::{FileMatch, search_files};
use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules}; use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules};
use crate::context_picker::symbol_context_picker::SymbolMatch; use crate::context_picker::symbol_context_picker::SymbolMatch;
use crate::context_picker::symbol_context_picker::search_symbols; use crate::context_picker::symbol_context_picker::search_symbols;
use crate::context_picker::thread_context_picker::search_threads;
use crate::context_picker::{ use crate::context_picker::{
ContextPickerAction, ContextPickerEntry, ContextPickerMode, selection_ranges, ContextPickerAction, ContextPickerEntry, ContextPickerMode, selection_ranges,
}; };
@@ -938,42 +939,6 @@ impl CompletionProvider for ContextPickerCompletionProvider {
} }
} }
pub(crate) fn search_threads(
query: String,
cancellation_flag: Arc<AtomicBool>,
history_store: &Entity<HistoryStore>,
cx: &mut App,
) -> Task<Vec<HistoryEntry>> {
let threads = history_store.read(cx).entries().collect();
if query.is_empty() {
return Task::ready(threads);
}
let executor = cx.background_executor().clone();
cx.background_spawn(async move {
let candidates = threads
.iter()
.enumerate()
.map(|(id, thread)| StringMatchCandidate::new(id, thread.title()))
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,
&query,
false,
true,
100,
&cancellation_flag,
executor,
)
.await;
matches
.into_iter()
.map(|mat| threads[mat.candidate_id].clone())
.collect()
})
}
fn confirm_completion_callback( fn confirm_completion_callback(
crease_text: SharedString, crease_text: SharedString,
start: Anchor, start: Anchor,

View File

@@ -1,8 +1,8 @@
use std::{cell::RefCell, ops::Range, rc::Rc}; use std::{cell::RefCell, ops::Range, rc::Rc};
use acp_thread::{AcpThread, AgentThreadEntry}; use acp_thread::{AcpThread, AgentThreadEntry};
use agent::HistoryStore;
use agent_client_protocol::{self as acp, ToolCallId}; use agent_client_protocol::{self as acp, ToolCallId};
use agent2::HistoryStore;
use collections::HashMap; use collections::HashMap;
use editor::{Editor, EditorMode, MinimapVisibility}; use editor::{Editor, EditorMode, MinimapVisibility};
use gpui::{ use gpui::{
@@ -399,9 +399,9 @@ mod tests {
use std::{path::Path, rc::Rc}; use std::{path::Path, rc::Rc};
use acp_thread::{AgentConnection, StubAgentConnection}; use acp_thread::{AgentConnection, StubAgentConnection};
use agent::HistoryStore;
use agent_client_protocol as acp; use agent_client_protocol as acp;
use agent_settings::AgentSettings; use agent_settings::AgentSettings;
use agent2::HistoryStore;
use assistant_context::ContextStore; use assistant_context::ContextStore;
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
use editor::{EditorSettings, RowInfo}; use editor::{EditorSettings, RowInfo};

View File

@@ -3,12 +3,11 @@ use crate::{
context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content}, context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content},
}; };
use acp_thread::{MentionUri, selection_name}; use acp_thread::{MentionUri, selection_name};
use agent::{HistoryStore, outline};
use agent_client_protocol as acp; use agent_client_protocol as acp;
use agent_servers::{AgentServer, AgentServerDelegate}; use agent_servers::{AgentServer, AgentServerDelegate};
use agent2::HistoryStore;
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use assistant_slash_commands::codeblock_fence_for_path; use assistant_slash_commands::codeblock_fence_for_path;
use assistant_tool::outline;
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use editor::{ use editor::{
Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
@@ -230,7 +229,7 @@ impl MessageEditor {
pub fn insert_thread_summary( pub fn insert_thread_summary(
&mut self, &mut self,
thread: agent2::DbThreadMetadata, thread: agent::DbThreadMetadata,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
@@ -599,7 +598,7 @@ impl MessageEditor {
id: acp::SessionId, id: acp::SessionId,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<Mention>> { ) -> Task<Result<Mention>> {
let server = Rc::new(agent2::NativeAgentServer::new( let server = Rc::new(agent::NativeAgentServer::new(
self.project.read(cx).fs().clone(), self.project.read(cx).fs().clone(),
self.history_store.clone(), self.history_store.clone(),
)); ));
@@ -612,7 +611,7 @@ impl MessageEditor {
let connection = server.connect(None, delegate, cx); let connection = server.connect(None, delegate, cx);
cx.spawn(async move |_, cx| { cx.spawn(async move |_, cx| {
let (agent, _) = connection.await?; let (agent, _) = connection.await?;
let agent = agent.downcast::<agent2::NativeAgentConnection>().unwrap(); let agent = agent.downcast::<agent::NativeAgentConnection>().unwrap();
let summary = agent let summary = agent
.0 .0
.update(cx, |agent, cx| agent.thread_summary(id, cx))? .update(cx, |agent, cx| agent.thread_summary(id, cx))?
@@ -629,8 +628,8 @@ impl MessageEditor {
path: PathBuf, path: PathBuf,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<Mention>> { ) -> Task<Result<Mention>> {
let context = self.history_store.update(cx, |text_thread_store, cx| { let context = self.history_store.update(cx, |store, cx| {
text_thread_store.load_text_thread(path.as_path().into(), cx) store.load_text_thread(path.as_path().into(), cx)
}); });
cx.spawn(async move |_, cx| { cx.spawn(async move |_, cx| {
let context = context.await?; let context = context.await?;
@@ -1589,10 +1588,9 @@ mod tests {
use std::{cell::RefCell, ops::Range, path::Path, rc::Rc, sync::Arc}; use std::{cell::RefCell, ops::Range, path::Path, rc::Rc, sync::Arc};
use acp_thread::MentionUri; use acp_thread::MentionUri;
use agent::{HistoryStore, outline};
use agent_client_protocol as acp; use agent_client_protocol as acp;
use agent2::HistoryStore;
use assistant_context::ContextStore; use assistant_context::ContextStore;
use assistant_tool::outline;
use editor::{AnchorRangeExt as _, Editor, EditorMode}; use editor::{AnchorRangeExt as _, Editor, EditorMode};
use fs::FakeFs; use fs::FakeFs;
use futures::StreamExt as _; use futures::StreamExt as _;

View File

@@ -1,6 +1,6 @@
use crate::acp::AcpThreadView; use crate::acp::AcpThreadView;
use crate::{AgentPanel, RemoveSelectedThread}; use crate::{AgentPanel, RemoveSelectedThread};
use agent2::{HistoryEntry, HistoryStore}; use agent::{HistoryEntry, HistoryStore};
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta}; use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
use editor::{Editor, EditorEvent}; use editor::{Editor, EditorEvent};
use fuzzy::StringMatchCandidate; use fuzzy::StringMatchCandidate;
@@ -23,11 +23,8 @@ pub struct AcpThreadHistory {
hovered_index: Option<usize>, hovered_index: Option<usize>,
search_editor: Entity<Editor>, search_editor: Entity<Editor>,
search_query: SharedString, search_query: SharedString,
visible_items: Vec<ListItemType>, visible_items: Vec<ListItemType>,
local_timezone: UtcOffset, local_timezone: UtcOffset,
_update_task: Task<()>, _update_task: Task<()>,
_subscriptions: Vec<gpui::Subscription>, _subscriptions: Vec<gpui::Subscription>,
} }
@@ -62,7 +59,7 @@ impl EventEmitter<ThreadHistoryEvent> for AcpThreadHistory {}
impl AcpThreadHistory { impl AcpThreadHistory {
pub(crate) fn new( pub(crate) fn new(
history_store: Entity<agent2::HistoryStore>, history_store: Entity<agent::HistoryStore>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
@@ -642,7 +639,7 @@ impl RenderOnce for AcpHistoryEntryElement {
if let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) { if let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel panel
.open_saved_prompt_editor( .open_saved_text_thread(
context.path.clone(), context.path.clone(),
window, window,
cx, cx,

View File

@@ -5,10 +5,10 @@ use acp_thread::{
}; };
use acp_thread::{AgentConnection, Plan}; use acp_thread::{AgentConnection, Plan};
use action_log::ActionLog; use action_log::ActionLog;
use agent::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer};
use agent_client_protocol::{self as acp, PromptCapabilities}; use agent_client_protocol::{self as acp, PromptCapabilities};
use agent_servers::{AgentServer, AgentServerDelegate}; use agent_servers::{AgentServer, AgentServerDelegate};
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode}; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode};
use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer};
use anyhow::{Result, anyhow, bail}; use anyhow::{Result, anyhow, bail};
use arrayvec::ArrayVec; use arrayvec::ArrayVec;
use audio::{Audio, Sound}; use audio::{Audio, Sound};
@@ -117,7 +117,7 @@ impl ThreadError {
} }
} }
impl ProfileProvider for Entity<agent2::Thread> { impl ProfileProvider for Entity<agent::Thread> {
fn profile_id(&self, cx: &App) -> AgentProfileId { fn profile_id(&self, cx: &App) -> AgentProfileId {
self.read(cx).profile().clone() self.read(cx).profile().clone()
} }
@@ -529,7 +529,7 @@ impl AcpThreadView {
let result = if let Some(native_agent) = connection let result = if let Some(native_agent) = connection
.clone() .clone()
.downcast::<agent2::NativeAgentConnection>() .downcast::<agent::NativeAgentConnection>()
&& let Some(resume) = resume_thread.clone() && let Some(resume) = resume_thread.clone()
{ {
cx.update(|_, cx| { cx.update(|_, cx| {
@@ -3106,7 +3106,7 @@ impl AcpThreadView {
let render_history = self let render_history = self
.agent .agent
.clone() .clone()
.downcast::<agent2::NativeAgentServer>() .downcast::<agent::NativeAgentServer>()
.is_some() .is_some()
&& self && self
.history_store .history_store
@@ -4011,12 +4011,12 @@ impl AcpThreadView {
pub(crate) fn as_native_connection( pub(crate) fn as_native_connection(
&self, &self,
cx: &App, cx: &App,
) -> Option<Rc<agent2::NativeAgentConnection>> { ) -> Option<Rc<agent::NativeAgentConnection>> {
let acp_thread = self.thread()?.read(cx); let acp_thread = self.thread()?.read(cx);
acp_thread.connection().clone().downcast() acp_thread.connection().clone().downcast()
} }
pub(crate) fn as_native_thread(&self, cx: &App) -> Option<Entity<agent2::Thread>> { pub(crate) fn as_native_thread(&self, cx: &App) -> Option<Entity<agent::Thread>> {
let acp_thread = self.thread()?.read(cx); let acp_thread = self.thread()?.read(cx);
self.as_native_connection(cx)? self.as_native_connection(cx)?
.thread(acp_thread.session_id(), cx) .thread(acp_thread.session_id(), cx)
@@ -4404,7 +4404,7 @@ impl AcpThreadView {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) { if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel panel
.open_saved_prompt_editor(path.as_path().into(), window, cx) .open_saved_text_thread(path.as_path().into(), window, cx)
.detach_and_log_err(cx); .detach_and_log_err(cx);
}); });
} }
@@ -5137,7 +5137,7 @@ impl AcpThreadView {
if self if self
.agent .agent
.clone() .clone()
.downcast::<agent2::NativeAgentServer>() .downcast::<agent::NativeAgentServer>()
.is_some() .is_some()
{ {
// Native agent - use the model name // Native agent - use the model name

View File

@@ -6,8 +6,8 @@ mod tool_picker;
use std::{ops::Range, sync::Arc}; use std::{ops::Range, sync::Arc};
use agent::ContextServerRegistry;
use anyhow::Result; use anyhow::Result;
use assistant_tool::{ToolSource, ToolWorkingSet};
use cloud_llm_client::{Plan, PlanV1, PlanV2}; use cloud_llm_client::{Plan, PlanV1, PlanV2};
use collections::HashMap; use collections::HashMap;
use context_server::ContextServerId; use context_server::ContextServerId;
@@ -17,7 +17,7 @@ use extension_host::ExtensionStore;
use fs::Fs; use fs::Fs;
use gpui::{ use gpui::{
Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable, Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable,
Hsla, ScrollHandle, Subscription, Task, WeakEntity, ScrollHandle, Subscription, Task, WeakEntity,
}; };
use language::LanguageRegistry; use language::LanguageRegistry;
use language_model::{ use language_model::{
@@ -54,9 +54,8 @@ pub struct AgentConfiguration {
focus_handle: FocusHandle, focus_handle: FocusHandle,
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>, configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
context_server_store: Entity<ContextServerStore>, context_server_store: Entity<ContextServerStore>,
expanded_context_server_tools: HashMap<ContextServerId, bool>,
expanded_provider_configurations: HashMap<LanguageModelProviderId, bool>, expanded_provider_configurations: HashMap<LanguageModelProviderId, bool>,
tools: Entity<ToolWorkingSet>, context_server_registry: Entity<ContextServerRegistry>,
_registry_subscription: Subscription, _registry_subscription: Subscription,
scroll_handle: ScrollHandle, scroll_handle: ScrollHandle,
_check_for_gemini: Task<()>, _check_for_gemini: Task<()>,
@@ -67,7 +66,7 @@ impl AgentConfiguration {
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
agent_server_store: Entity<AgentServerStore>, agent_server_store: Entity<AgentServerStore>,
context_server_store: Entity<ContextServerStore>, context_server_store: Entity<ContextServerStore>,
tools: Entity<ToolWorkingSet>, context_server_registry: Entity<ContextServerRegistry>,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
window: &mut Window, window: &mut Window,
@@ -103,9 +102,8 @@ impl AgentConfiguration {
configuration_views_by_provider: HashMap::default(), configuration_views_by_provider: HashMap::default(),
agent_server_store, agent_server_store,
context_server_store, context_server_store,
expanded_context_server_tools: HashMap::default(),
expanded_provider_configurations: HashMap::default(), expanded_provider_configurations: HashMap::default(),
tools, context_server_registry,
_registry_subscription: registry_subscription, _registry_subscription: registry_subscription,
scroll_handle: ScrollHandle::new(), scroll_handle: ScrollHandle::new(),
_check_for_gemini: Task::ready(()), _check_for_gemini: Task::ready(()),
@@ -438,10 +436,6 @@ impl AgentConfiguration {
} }
} }
fn card_item_border_color(&self, cx: &mut Context<Self>) -> Hsla {
cx.theme().colors().border.opacity(0.6)
}
fn render_context_servers_section( fn render_context_servers_section(
&mut self, &mut self,
window: &mut Window, window: &mut Window,
@@ -567,7 +561,6 @@ impl AgentConfiguration {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> impl use<> + IntoElement { ) -> impl use<> + IntoElement {
let tools_by_source = self.tools.read(cx).tools_by_source(cx);
let server_status = self let server_status = self
.context_server_store .context_server_store
.read(cx) .read(cx)
@@ -596,17 +589,11 @@ impl AgentConfiguration {
None None
}; };
let are_tools_expanded = self let tool_count = self
.expanded_context_server_tools .context_server_registry
.get(&context_server_id) .read(cx)
.copied() .tools_for_server(&context_server_id)
.unwrap_or_default(); .count();
let tools = tools_by_source
.get(&ToolSource::ContextServer {
id: context_server_id.0.clone().into(),
})
.map_or([].as_slice(), |tools| tools.as_slice());
let tool_count = tools.len();
let (source_icon, source_tooltip) = if is_from_extension { let (source_icon, source_tooltip) = if is_from_extension {
( (
@@ -660,7 +647,7 @@ impl AgentConfiguration {
let language_registry = self.language_registry.clone(); let language_registry = self.language_registry.clone();
let context_server_store = self.context_server_store.clone(); let context_server_store = self.context_server_store.clone();
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();
let tools = self.tools.clone(); let context_server_registry = self.context_server_registry.clone();
move |window, cx| { move |window, cx| {
Some(ContextMenu::build(window, cx, |menu, _window, _cx| { Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
@@ -678,20 +665,16 @@ impl AgentConfiguration {
) )
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
}).when(tool_count >= 1, |this| this.entry("View Tools", None, { }).when(tool_count > 0, |this| this.entry("View Tools", None, {
let context_server_id = context_server_id.clone(); let context_server_id = context_server_id.clone();
let tools = tools.clone(); let context_server_registry = context_server_registry.clone();
let workspace = workspace.clone(); let workspace = workspace.clone();
move |window, cx| { move |window, cx| {
let context_server_id = context_server_id.clone(); let context_server_id = context_server_id.clone();
let tools = tools.clone();
let workspace = workspace.clone();
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
ConfigureContextServerToolsModal::toggle( ConfigureContextServerToolsModal::toggle(
context_server_id, context_server_id,
tools, context_server_registry.clone(),
workspace, workspace,
window, window,
cx, cx,
@@ -773,14 +756,6 @@ impl AgentConfiguration {
.child( .child(
h_flex() h_flex()
.justify_between() .justify_between()
.when(
error.is_none() && are_tools_expanded && tool_count >= 1,
|element| {
element
.border_b_1()
.border_color(self.card_item_border_color(cx))
},
)
.child( .child(
h_flex() h_flex()
.flex_1() .flex_1()
@@ -904,11 +879,6 @@ impl AgentConfiguration {
), ),
); );
} }
if !are_tools_expanded || tools.is_empty() {
return parent;
}
parent parent
}) })
} }

View File

@@ -1,4 +1,5 @@
use assistant_tool::{ToolSource, ToolWorkingSet}; use agent::ContextServerRegistry;
use collections::HashMap;
use context_server::ContextServerId; use context_server::ContextServerId;
use gpui::{ use gpui::{
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle, Window, prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle, Window, prelude::*,
@@ -8,37 +9,37 @@ use workspace::{ModalView, Workspace};
pub struct ConfigureContextServerToolsModal { pub struct ConfigureContextServerToolsModal {
context_server_id: ContextServerId, context_server_id: ContextServerId,
tools: Entity<ToolWorkingSet>, context_server_registry: Entity<ContextServerRegistry>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
expanded_tools: std::collections::HashMap<String, bool>, expanded_tools: HashMap<SharedString, bool>,
scroll_handle: ScrollHandle, scroll_handle: ScrollHandle,
} }
impl ConfigureContextServerToolsModal { impl ConfigureContextServerToolsModal {
fn new( fn new(
context_server_id: ContextServerId, context_server_id: ContextServerId,
tools: Entity<ToolWorkingSet>, context_server_registry: Entity<ContextServerRegistry>,
_window: &mut Window, _window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
Self { Self {
context_server_id, context_server_id,
tools, context_server_registry,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
expanded_tools: std::collections::HashMap::new(), expanded_tools: HashMap::default(),
scroll_handle: ScrollHandle::new(), scroll_handle: ScrollHandle::new(),
} }
} }
pub fn toggle( pub fn toggle(
context_server_id: ContextServerId, context_server_id: ContextServerId,
tools: Entity<ToolWorkingSet>, context_server_registry: Entity<ContextServerRegistry>,
workspace: &mut Workspace, workspace: &mut Workspace,
window: &mut Window, window: &mut Window,
cx: &mut Context<Workspace>, cx: &mut Context<Workspace>,
) { ) {
workspace.toggle_modal(window, cx, |window, cx| { workspace.toggle_modal(window, cx, |window, cx| {
Self::new(context_server_id, tools, window, cx) Self::new(context_server_id, context_server_registry, window, cx)
}); });
} }
@@ -51,13 +52,11 @@ impl ConfigureContextServerToolsModal {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> impl IntoElement { ) -> impl IntoElement {
let tools_by_source = self.tools.read(cx).tools_by_source(cx); let tools = self
let server_tools = tools_by_source .context_server_registry
.get(&ToolSource::ContextServer { .read(cx)
id: self.context_server_id.0.clone().into(), .tools_for_server(&self.context_server_id)
}) .collect::<Vec<_>>();
.map(|tools| tools.as_slice())
.unwrap_or(&[]);
div() div()
.size_full() .size_full()
@@ -70,11 +69,11 @@ impl ConfigureContextServerToolsModal {
.max_h_128() .max_h_128()
.overflow_y_scroll() .overflow_y_scroll()
.track_scroll(&self.scroll_handle) .track_scroll(&self.scroll_handle)
.children(server_tools.iter().enumerate().flat_map(|(index, tool)| { .children(tools.iter().enumerate().flat_map(|(index, tool)| {
let tool_name = tool.name(); let tool_name = tool.name();
let is_expanded = self let is_expanded = self
.expanded_tools .expanded_tools
.get(&tool_name) .get(tool_name.as_ref())
.copied() .copied()
.unwrap_or(false); .unwrap_or(false);
@@ -110,7 +109,7 @@ impl ConfigureContextServerToolsModal {
move |this, _event, _window, _cx| { move |this, _event, _window, _cx| {
let current = this let current = this
.expanded_tools .expanded_tools
.get(&tool_name) .get(tool_name.as_ref())
.copied() .copied()
.unwrap_or(false); .unwrap_or(false);
this.expanded_tools this.expanded_tools
@@ -127,7 +126,7 @@ impl ConfigureContextServerToolsModal {
.into_any_element(), .into_any_element(),
]; ];
if index < server_tools.len() - 1 { if index < tools.len() - 1 {
items.push( items.push(
h_flex() h_flex()
.w_full() .w_full()

View File

@@ -2,8 +2,8 @@ mod profile_modal_header;
use std::sync::Arc; use std::sync::Arc;
use agent::ContextServerRegistry;
use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, builtin_profiles}; use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, builtin_profiles};
use assistant_tool::ToolWorkingSet;
use editor::Editor; use editor::Editor;
use fs::Fs; use fs::Fs;
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, prelude::*}; use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, prelude::*};
@@ -17,8 +17,6 @@ use crate::agent_configuration::manage_profiles_modal::profile_modal_header::Pro
use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate}; use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
use crate::{AgentPanel, ManageProfiles}; use crate::{AgentPanel, ManageProfiles};
use super::tool_picker::ToolPickerMode;
enum Mode { enum Mode {
ChooseProfile(ChooseProfileMode), ChooseProfile(ChooseProfileMode),
NewProfile(NewProfileMode), NewProfile(NewProfileMode),
@@ -97,7 +95,7 @@ pub struct NewProfileMode {
pub struct ManageProfilesModal { pub struct ManageProfilesModal {
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
tools: Entity<ToolWorkingSet>, context_server_registry: Entity<ContextServerRegistry>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
mode: Mode, mode: Mode,
} }
@@ -111,10 +109,9 @@ impl ManageProfilesModal {
workspace.register_action(|workspace, action: &ManageProfiles, window, cx| { workspace.register_action(|workspace, action: &ManageProfiles, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) { if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
let fs = workspace.app_state().fs.clone(); let fs = workspace.app_state().fs.clone();
let thread_store = panel.read(cx).thread_store(); let context_server_registry = panel.read(cx).context_server_registry().clone();
let tools = thread_store.read(cx).tools();
workspace.toggle_modal(window, cx, |window, cx| { workspace.toggle_modal(window, cx, |window, cx| {
let mut this = Self::new(fs, tools, window, cx); let mut this = Self::new(fs, context_server_registry, window, cx);
if let Some(profile_id) = action.customize_tools.clone() { if let Some(profile_id) = action.customize_tools.clone() {
this.configure_builtin_tools(profile_id, window, cx); this.configure_builtin_tools(profile_id, window, cx);
@@ -128,7 +125,7 @@ impl ManageProfilesModal {
pub fn new( pub fn new(
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
tools: Entity<ToolWorkingSet>, context_server_registry: Entity<ContextServerRegistry>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
@@ -136,7 +133,7 @@ impl ManageProfilesModal {
Self { Self {
fs, fs,
tools, context_server_registry,
focus_handle, focus_handle,
mode: Mode::choose_profile(window, cx), mode: Mode::choose_profile(window, cx),
} }
@@ -193,10 +190,9 @@ impl ManageProfilesModal {
}; };
let tool_picker = cx.new(|cx| { let tool_picker = cx.new(|cx| {
let delegate = ToolPickerDelegate::new( let delegate = ToolPickerDelegate::mcp_tools(
ToolPickerMode::McpTools, &self.context_server_registry,
self.fs.clone(), self.fs.clone(),
self.tools.clone(),
profile_id.clone(), profile_id.clone(),
profile, profile,
cx, cx,
@@ -230,10 +226,12 @@ impl ManageProfilesModal {
}; };
let tool_picker = cx.new(|cx| { let tool_picker = cx.new(|cx| {
let delegate = ToolPickerDelegate::new( let delegate = ToolPickerDelegate::builtin_tools(
ToolPickerMode::BuiltinTools, //todo: This causes the web search tool to show up even it only works when using zed hosted models
agent::built_in_tool_names()
.map(|s| s.into())
.collect::<Vec<_>>(),
self.fs.clone(), self.fs.clone(),
self.tools.clone(),
profile_id.clone(), profile_id.clone(),
profile, profile,
cx, cx,

View File

@@ -1,7 +1,7 @@
use std::{collections::BTreeMap, sync::Arc}; use std::{collections::BTreeMap, sync::Arc};
use agent::ContextServerRegistry;
use agent_settings::{AgentProfileId, AgentProfileSettings}; use agent_settings::{AgentProfileId, AgentProfileSettings};
use assistant_tool::{ToolSource, ToolWorkingSet};
use fs::Fs; use fs::Fs;
use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window}; use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
@@ -14,7 +14,7 @@ pub struct ToolPicker {
} }
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub enum ToolPickerMode { enum ToolPickerMode {
BuiltinTools, BuiltinTools,
McpTools, McpTools,
} }
@@ -76,60 +76,80 @@ pub struct ToolPickerDelegate {
} }
impl ToolPickerDelegate { impl ToolPickerDelegate {
pub fn new( pub fn builtin_tools(
mode: ToolPickerMode, tool_names: Vec<Arc<str>>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
tool_set: Entity<ToolWorkingSet>,
profile_id: AgentProfileId, profile_id: AgentProfileId,
profile_settings: AgentProfileSettings, profile_settings: AgentProfileSettings,
cx: &mut Context<ToolPicker>, cx: &mut Context<ToolPicker>,
) -> Self { ) -> Self {
let items = Arc::new(Self::resolve_items(mode, &tool_set, cx)); Self::new(
Arc::new(
tool_names
.into_iter()
.map(|name| PickerItem::Tool {
name,
server_id: None,
})
.collect(),
),
ToolPickerMode::BuiltinTools,
fs,
profile_id,
profile_settings,
cx,
)
}
pub fn mcp_tools(
registry: &Entity<ContextServerRegistry>,
fs: Arc<dyn Fs>,
profile_id: AgentProfileId,
profile_settings: AgentProfileSettings,
cx: &mut Context<ToolPicker>,
) -> Self {
let mut items = Vec::new();
for (id, tools) in registry.read(cx).servers() {
let server_id = id.clone().0;
items.push(PickerItem::ContextServer {
server_id: server_id.clone(),
});
items.extend(tools.keys().map(|tool_name| PickerItem::Tool {
name: tool_name.clone().into(),
server_id: Some(server_id.clone()),
}));
}
Self::new(
Arc::new(items),
ToolPickerMode::McpTools,
fs,
profile_id,
profile_settings,
cx,
)
}
fn new(
items: Arc<Vec<PickerItem>>,
mode: ToolPickerMode,
fs: Arc<dyn Fs>,
profile_id: AgentProfileId,
profile_settings: AgentProfileSettings,
cx: &mut Context<ToolPicker>,
) -> Self {
Self { Self {
tool_picker: cx.entity().downgrade(), tool_picker: cx.entity().downgrade(),
mode,
fs, fs,
items, items,
profile_id, profile_id,
profile_settings, profile_settings,
filtered_items: Vec::new(), filtered_items: Vec::new(),
selected_index: 0, selected_index: 0,
mode,
} }
} }
fn resolve_items(
mode: ToolPickerMode,
tool_set: &Entity<ToolWorkingSet>,
cx: &mut App,
) -> Vec<PickerItem> {
let mut items = Vec::new();
for (source, tools) in tool_set.read(cx).tools_by_source(cx) {
match source {
ToolSource::Native => {
if mode == ToolPickerMode::BuiltinTools {
items.extend(tools.into_iter().map(|tool| PickerItem::Tool {
name: tool.name().into(),
server_id: None,
}));
}
}
ToolSource::ContextServer { id } => {
if mode == ToolPickerMode::McpTools && !tools.is_empty() {
let server_id: Arc<str> = id.clone().into();
items.push(PickerItem::ContextServer {
server_id: server_id.clone(),
});
items.extend(tools.into_iter().map(|tool| PickerItem::Tool {
name: tool.name().into(),
server_id: Some(server_id.clone()),
}));
}
}
}
}
items
}
} }
impl PickerDelegate for ToolPickerDelegate { impl PickerDelegate for ToolPickerDelegate {

View File

@@ -4,7 +4,7 @@ use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use acp_thread::AcpThread; use acp_thread::AcpThread;
use agent2::{DbThreadMetadata, HistoryEntry}; use agent::{ContextServerRegistry, DbThreadMetadata, HistoryEntry, HistoryStore};
use db::kvp::{Dismissable, KEY_VALUE_STORE}; use db::kvp::{Dismissable, KEY_VALUE_STORE};
use project::agent_server_store::{ use project::agent_server_store::{
AgentServerCommand, AllAgentServersSettings, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME, AgentServerCommand, AllAgentServersSettings, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME,
@@ -17,6 +17,7 @@ use zed_actions::OpenBrowser;
use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent}; use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent}; use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
use crate::context_store::ContextStore;
use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal}; use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
use crate::{ use crate::{
AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, Follow, InlineAssistant, AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, Follow, InlineAssistant,
@@ -32,16 +33,11 @@ use crate::{
use crate::{ use crate::{
ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary, placeholder_command, ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary, placeholder_command,
}; };
use agent::{
context_store::ContextStore,
thread_store::{TextThreadStore, ThreadStore},
};
use agent_settings::AgentSettings; use agent_settings::AgentSettings;
use ai_onboarding::AgentPanelOnboarding; use ai_onboarding::AgentPanelOnboarding;
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use assistant_context::{AssistantContext, ContextEvent, ContextSummary}; use assistant_context::{AssistantContext, ContextEvent, ContextSummary};
use assistant_slash_command::SlashCommandWorkingSet; use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet;
use client::{UserStore, zed_urls}; use client::{UserStore, zed_urls};
use cloud_llm_client::{Plan, PlanV1, PlanV2, UsageLimit}; use cloud_llm_client::{Plan, PlanV1, PlanV2, UsageLimit};
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer}; use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
@@ -118,7 +114,7 @@ pub fn init(cx: &mut App) {
.register_action(|workspace, _: &NewTextThread, window, cx| { .register_action(|workspace, _: &NewTextThread, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) { if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx); workspace.focus_panel::<AgentPanel>(window, cx);
panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx)); panel.update(cx, |panel, cx| panel.new_text_thread(window, cx));
} }
}) })
.register_action(|workspace, action: &NewExternalAgentThread, window, cx| { .register_action(|workspace, action: &NewExternalAgentThread, window, cx| {
@@ -281,7 +277,7 @@ impl ActiveView {
pub fn native_agent( pub fn native_agent(
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
prompt_store: Option<Entity<PromptStore>>, prompt_store: Option<Entity<PromptStore>>,
acp_history_store: Entity<agent2::HistoryStore>, history_store: Entity<agent::HistoryStore>,
project: Entity<Project>, project: Entity<Project>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
window: &mut Window, window: &mut Window,
@@ -289,12 +285,12 @@ impl ActiveView {
) -> Self { ) -> Self {
let thread_view = cx.new(|cx| { let thread_view = cx.new(|cx| {
crate::acp::AcpThreadView::new( crate::acp::AcpThreadView::new(
ExternalAgent::NativeAgent.server(fs, acp_history_store.clone()), ExternalAgent::NativeAgent.server(fs, history_store.clone()),
None, None,
None, None,
workspace, workspace,
project, project,
acp_history_store, history_store,
prompt_store, prompt_store,
window, window,
cx, cx,
@@ -304,9 +300,9 @@ impl ActiveView {
Self::ExternalAgentThread { thread_view } Self::ExternalAgentThread { thread_view }
} }
pub fn prompt_editor( pub fn text_thread(
context_editor: Entity<TextThreadEditor>, context_editor: Entity<TextThreadEditor>,
acp_history_store: Entity<agent2::HistoryStore>, acp_history_store: Entity<agent::HistoryStore>,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
@@ -379,7 +375,7 @@ impl ActiveView {
.replace_recently_opened_text_thread(old_path, new_path, cx); .replace_recently_opened_text_thread(old_path, new_path, cx);
} else { } else {
history_store.push_recently_opened_entry( history_store.push_recently_opened_entry(
agent2::HistoryEntryId::TextThread(new_path.clone()), agent::HistoryEntryId::TextThread(new_path.clone()),
cx, cx,
); );
} }
@@ -412,11 +408,11 @@ pub struct AgentPanel {
project: Entity<Project>, project: Entity<Project>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
thread_store: Entity<ThreadStore>,
acp_history: Entity<AcpThreadHistory>, acp_history: Entity<AcpThreadHistory>,
history_store: Entity<agent2::HistoryStore>, history_store: Entity<agent::HistoryStore>,
context_store: Entity<TextThreadStore>, text_thread_store: Entity<assistant_context::ContextStore>,
prompt_store: Option<Entity<PromptStore>>, prompt_store: Option<Entity<PromptStore>>,
context_server_registry: Entity<ContextServerRegistry>,
inline_assist_context_store: Entity<ContextStore>, inline_assist_context_store: Entity<ContextStore>,
configuration: Option<Entity<AgentConfiguration>>, configuration: Option<Entity<AgentConfiguration>>,
configuration_subscription: Option<Subscription>, configuration_subscription: Option<Subscription>,
@@ -424,8 +420,8 @@ pub struct AgentPanel {
previous_view: Option<ActiveView>, previous_view: Option<ActiveView>,
new_thread_menu_handle: PopoverMenuHandle<ContextMenu>, new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>, agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>, agent_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
assistant_navigation_menu: Option<Entity<ContextMenu>>, agent_navigation_menu: Option<Entity<ContextMenu>>,
width: Option<Pixels>, width: Option<Pixels>,
height: Option<Pixels>, height: Option<Pixels>,
zoomed: bool, zoomed: bool,
@@ -463,33 +459,6 @@ impl AgentPanel {
Ok(prompt_store) => prompt_store.await.ok(), Ok(prompt_store) => prompt_store.await.ok(),
Err(_) => None, Err(_) => None,
}; };
let tools = cx.new(|_| ToolWorkingSet::default())?;
let thread_store = workspace
.update(cx, |workspace, cx| {
let project = workspace.project().clone();
ThreadStore::load(
project,
tools.clone(),
prompt_store.clone(),
prompt_builder.clone(),
cx,
)
})?
.await?;
let slash_commands = Arc::new(SlashCommandWorkingSet::default());
let context_store = workspace
.update(cx, |workspace, cx| {
let project = workspace.project().clone();
assistant_context::ContextStore::new(
project,
prompt_builder.clone(),
slash_commands,
cx,
)
})?
.await?;
let serialized_panel = if let Some(panel) = cx let serialized_panel = if let Some(panel) = cx
.background_spawn(async move { KEY_VALUE_STORE.read_kvp(AGENT_PANEL_KEY) }) .background_spawn(async move { KEY_VALUE_STORE.read_kvp(AGENT_PANEL_KEY) })
.await .await
@@ -501,17 +470,22 @@ impl AgentPanel {
None None
}; };
let panel = workspace.update_in(cx, |workspace, window, cx| { let slash_commands = Arc::new(SlashCommandWorkingSet::default());
let panel = cx.new(|cx| { let text_thread_store = workspace
Self::new( .update(cx, |workspace, cx| {
workspace, let project = workspace.project().clone();
thread_store, assistant_context::ContextStore::new(
context_store, project,
prompt_store, prompt_builder,
window, slash_commands,
cx, cx,
) )
}); })?
.await?;
let panel = workspace.update_in(cx, |workspace, window, cx| {
let panel =
cx.new(|cx| Self::new(workspace, text_thread_store, prompt_store, window, cx));
panel.as_mut(cx).loading = true; panel.as_mut(cx).loading = true;
if let Some(serialized_panel) = serialized_panel { if let Some(serialized_panel) = serialized_panel {
@@ -538,8 +512,7 @@ impl AgentPanel {
fn new( fn new(
workspace: &Workspace, workspace: &Workspace,
thread_store: Entity<ThreadStore>, text_thread_store: Entity<assistant_context::ContextStore>,
context_store: Entity<TextThreadStore>,
prompt_store: Option<Entity<PromptStore>>, prompt_store: Option<Entity<PromptStore>>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
@@ -551,10 +524,11 @@ impl AgentPanel {
let client = workspace.client().clone(); let client = workspace.client().clone();
let workspace = workspace.weak_handle(); let workspace = workspace.weak_handle();
let inline_assist_context_store = let inline_assist_context_store = cx.new(|_cx| ContextStore::new(project.downgrade()));
cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade()))); let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let history_store = cx.new(|cx| agent2::HistoryStore::new(context_store.clone(), cx)); let history_store = cx.new(|cx| agent::HistoryStore::new(text_thread_store.clone(), cx));
let acp_history = cx.new(|cx| AcpThreadHistory::new(history_store.clone(), window, cx)); let acp_history = cx.new(|cx| AcpThreadHistory::new(history_store.clone(), window, cx));
cx.subscribe_in( cx.subscribe_in(
&acp_history, &acp_history,
@@ -570,7 +544,7 @@ impl AgentPanel {
); );
} }
ThreadHistoryEvent::Open(HistoryEntry::TextThread(thread)) => { ThreadHistoryEvent::Open(HistoryEntry::TextThread(thread)) => {
this.open_saved_prompt_editor(thread.path.clone(), window, cx) this.open_saved_text_thread(thread.path.clone(), window, cx)
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
}, },
@@ -589,8 +563,7 @@ impl AgentPanel {
cx, cx,
), ),
DefaultView::TextThread => { DefaultView::TextThread => {
let context = let context = text_thread_store.update(cx, |store, cx| store.create(cx));
context_store.update(cx, |context_store, cx| context_store.create(cx));
let lsp_adapter_delegate = make_lsp_adapter_delegate(&project.clone(), cx).unwrap(); let lsp_adapter_delegate = make_lsp_adapter_delegate(&project.clone(), cx).unwrap();
let context_editor = cx.new(|cx| { let context_editor = cx.new(|cx| {
let mut editor = TextThreadEditor::for_context( let mut editor = TextThreadEditor::for_context(
@@ -605,7 +578,7 @@ impl AgentPanel {
editor.insert_default_prompt(window, cx); editor.insert_default_prompt(window, cx);
editor editor
}); });
ActiveView::prompt_editor( ActiveView::text_thread(
context_editor, context_editor,
history_store.clone(), history_store.clone(),
language_registry.clone(), language_registry.clone(),
@@ -619,7 +592,7 @@ impl AgentPanel {
window.defer(cx, move |window, cx| { window.defer(cx, move |window, cx| {
let panel = weak_panel.clone(); let panel = weak_panel.clone();
let assistant_navigation_menu = let agent_navigation_menu =
ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| { ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
if let Some(panel) = panel.upgrade() { if let Some(panel) = panel.upgrade() {
menu = Self::populate_recently_opened_menu_section(menu, panel, cx); menu = Self::populate_recently_opened_menu_section(menu, panel, cx);
@@ -633,7 +606,7 @@ impl AgentPanel {
weak_panel weak_panel
.update(cx, |panel, cx| { .update(cx, |panel, cx| {
cx.subscribe_in( cx.subscribe_in(
&assistant_navigation_menu, &agent_navigation_menu,
window, window,
|_, menu, _: &DismissEvent, window, cx| { |_, menu, _: &DismissEvent, window, cx| {
menu.update(cx, |menu, _| { menu.update(cx, |menu, _| {
@@ -643,7 +616,7 @@ impl AgentPanel {
}, },
) )
.detach(); .detach();
panel.assistant_navigation_menu = Some(assistant_navigation_menu); panel.agent_navigation_menu = Some(agent_navigation_menu);
}) })
.ok(); .ok();
}); });
@@ -666,17 +639,17 @@ impl AgentPanel {
project: project.clone(), project: project.clone(),
fs: fs.clone(), fs: fs.clone(),
language_registry, language_registry,
thread_store: thread_store.clone(), text_thread_store,
context_store,
prompt_store, prompt_store,
configuration: None, configuration: None,
configuration_subscription: None, configuration_subscription: None,
context_server_registry,
inline_assist_context_store, inline_assist_context_store,
previous_view: None, previous_view: None,
new_thread_menu_handle: PopoverMenuHandle::default(), new_thread_menu_handle: PopoverMenuHandle::default(),
agent_panel_menu_handle: PopoverMenuHandle::default(), agent_panel_menu_handle: PopoverMenuHandle::default(),
assistant_navigation_menu_handle: PopoverMenuHandle::default(), agent_navigation_menu_handle: PopoverMenuHandle::default(),
assistant_navigation_menu: None, agent_navigation_menu: None,
width: None, width: None,
height: None, height: None,
zoomed: false, zoomed: false,
@@ -711,12 +684,12 @@ impl AgentPanel {
&self.inline_assist_context_store &self.inline_assist_context_store
} }
pub(crate) fn thread_store(&self) -> &Entity<ThreadStore> { pub(crate) fn thread_store(&self) -> &Entity<HistoryStore> {
&self.thread_store &self.history_store
} }
pub(crate) fn text_thread_store(&self) -> &Entity<TextThreadStore> { pub(crate) fn context_server_registry(&self) -> &Entity<ContextServerRegistry> {
&self.context_store &self.context_server_registry
} }
fn active_thread_view(&self) -> Option<&Entity<AcpThreadView>> { fn active_thread_view(&self) -> Option<&Entity<AcpThreadView>> {
@@ -753,11 +726,11 @@ impl AgentPanel {
); );
} }
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn new_text_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) {
telemetry::event!("Agent Thread Started", agent = "zed-text"); telemetry::event!("Agent Thread Started", agent = "zed-text");
let context = self let context = self
.context_store .text_thread_store
.update(cx, |context_store, cx| context_store.create(cx)); .update(cx, |context_store, cx| context_store.create(cx));
let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx) let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
.log_err() .log_err()
@@ -783,7 +756,7 @@ impl AgentPanel {
} }
self.set_active_view( self.set_active_view(
ActiveView::prompt_editor( ActiveView::text_thread(
context_editor.clone(), context_editor.clone(),
self.history_store.clone(), self.history_store.clone(),
self.language_registry.clone(), self.language_registry.clone(),
@@ -921,32 +894,29 @@ impl AgentPanel {
self.set_active_view(previous_view, window, cx); self.set_active_view(previous_view, window, cx);
} }
} else { } else {
self.thread_store
.update(cx, |thread_store, cx| thread_store.reload(cx))
.detach_and_log_err(cx);
self.set_active_view(ActiveView::History, window, cx); self.set_active_view(ActiveView::History, window, cx);
} }
cx.notify(); cx.notify();
} }
pub(crate) fn open_saved_prompt_editor( pub(crate) fn open_saved_text_thread(
&mut self, &mut self,
path: Arc<Path>, path: Arc<Path>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let context = self let context = self
.context_store .history_store
.update(cx, |store, cx| store.open_local_context(path, cx)); .update(cx, |store, cx| store.load_text_thread(path, cx));
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let context = context.await?; let context = context.await?;
this.update_in(cx, |this, window, cx| { this.update_in(cx, |this, window, cx| {
this.open_prompt_editor(context, window, cx); this.open_text_thread(context, window, cx);
}) })
}) })
} }
pub(crate) fn open_prompt_editor( pub(crate) fn open_text_thread(
&mut self, &mut self,
context: Entity<AssistantContext>, context: Entity<AssistantContext>,
window: &mut Window, window: &mut Window,
@@ -973,7 +943,7 @@ impl AgentPanel {
} }
self.set_active_view( self.set_active_view(
ActiveView::prompt_editor( ActiveView::text_thread(
editor, editor,
self.history_store.clone(), self.history_store.clone(),
self.language_registry.clone(), self.language_registry.clone(),
@@ -1013,7 +983,7 @@ impl AgentPanel {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
self.assistant_navigation_menu_handle.toggle(window, cx); self.agent_navigation_menu_handle.toggle(window, cx);
} }
pub fn toggle_options_menu( pub fn toggle_options_menu(
@@ -1106,7 +1076,6 @@ impl AgentPanel {
pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) { pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let agent_server_store = self.project.read(cx).agent_server_store().clone(); let agent_server_store = self.project.read(cx).agent_server_store().clone();
let context_server_store = self.project.read(cx).context_server_store(); let context_server_store = self.project.read(cx).context_server_store();
let tools = self.thread_store.read(cx).tools();
let fs = self.fs.clone(); let fs = self.fs.clone();
self.set_active_view(ActiveView::Configuration, window, cx); self.set_active_view(ActiveView::Configuration, window, cx);
@@ -1115,7 +1084,7 @@ impl AgentPanel {
fs, fs,
agent_server_store, agent_server_store,
context_server_store, context_server_store,
tools, self.context_server_registry.clone(),
self.language_registry.clone(), self.language_registry.clone(),
self.workspace.clone(), self.workspace.clone(),
window, window,
@@ -1183,7 +1152,7 @@ impl AgentPanel {
}); });
} }
self.new_thread(&NewThread::default(), window, cx); self.new_thread(&NewThread, window, cx);
if let Some((thread, model)) = self if let Some((thread, model)) = self
.active_native_agent_thread(cx) .active_native_agent_thread(cx)
.zip(provider.default_model(cx)) .zip(provider.default_model(cx))
@@ -1205,7 +1174,7 @@ impl AgentPanel {
} }
} }
pub(crate) fn active_native_agent_thread(&self, cx: &App) -> Option<Entity<agent2::Thread>> { pub(crate) fn active_native_agent_thread(&self, cx: &App) -> Option<Entity<agent::Thread>> {
match &self.active_view { match &self.active_view {
ActiveView::ExternalAgentThread { thread_view, .. } => { ActiveView::ExternalAgentThread { thread_view, .. } => {
thread_view.read(cx).as_native_thread(cx) thread_view.read(cx).as_native_thread(cx)
@@ -1241,7 +1210,7 @@ impl AgentPanel {
self.history_store.update(cx, |store, cx| { self.history_store.update(cx, |store, cx| {
if let Some(path) = context_editor.read(cx).context().read(cx).path() { if let Some(path) = context_editor.read(cx).context().read(cx).path() {
store.push_recently_opened_entry( store.push_recently_opened_entry(
agent2::HistoryEntryId::TextThread(path.clone()), agent::HistoryEntryId::TextThread(path.clone()),
cx, cx,
) )
} }
@@ -1295,15 +1264,15 @@ impl AgentPanel {
let entry = entry.clone(); let entry = entry.clone();
panel panel
.update(cx, move |this, cx| match &entry { .update(cx, move |this, cx| match &entry {
agent2::HistoryEntry::AcpThread(entry) => this.external_thread( agent::HistoryEntry::AcpThread(entry) => this.external_thread(
Some(ExternalAgent::NativeAgent), Some(ExternalAgent::NativeAgent),
Some(entry.clone()), Some(entry.clone()),
None, None,
window, window,
cx, cx,
), ),
agent2::HistoryEntry::TextThread(entry) => this agent::HistoryEntry::TextThread(entry) => this
.open_saved_prompt_editor(entry.path.clone(), window, cx) .open_saved_text_thread(entry.path.clone(), window, cx)
.detach_and_log_err(cx), .detach_and_log_err(cx),
}) })
.ok(); .ok();
@@ -1730,9 +1699,9 @@ impl AgentPanel {
}, },
) )
.anchor(corner) .anchor(corner)
.with_handle(self.assistant_navigation_menu_handle.clone()) .with_handle(self.agent_navigation_menu_handle.clone())
.menu({ .menu({
let menu = self.assistant_navigation_menu.clone(); let menu = self.agent_navigation_menu.clone();
move |window, cx| { move |window, cx| {
telemetry::event!("View Thread History Clicked"); telemetry::event!("View Thread History Clicked");
@@ -1832,7 +1801,7 @@ impl AgentPanel {
}) })
.item( .item(
ContextMenuEntry::new("New Thread") ContextMenuEntry::new("New Thread")
.action(NewThread::default().boxed_clone()) .action(NewThread.boxed_clone())
.icon(IconName::Thread) .icon(IconName::Thread)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.handler({ .handler({
@@ -2278,7 +2247,7 @@ impl AgentPanel {
} }
} }
fn render_prompt_editor( fn render_text_thread(
&self, &self,
context_editor: &Entity<TextThreadEditor>, context_editor: &Entity<TextThreadEditor>,
buffer_search_bar: &Entity<BufferSearchBar>, buffer_search_bar: &Entity<BufferSearchBar>,
@@ -2409,8 +2378,8 @@ impl AgentPanel {
let mut key_context = KeyContext::new_with_defaults(); let mut key_context = KeyContext::new_with_defaults();
key_context.add("AgentPanel"); key_context.add("AgentPanel");
match &self.active_view { match &self.active_view {
ActiveView::ExternalAgentThread { .. } => key_context.add("external_agent_thread"), ActiveView::ExternalAgentThread { .. } => key_context.add("acp_thread"),
ActiveView::TextThread { .. } => key_context.add("prompt_editor"), ActiveView::TextThread { .. } => key_context.add("text_thread"),
ActiveView::History | ActiveView::Configuration => {} ActiveView::History | ActiveView::Configuration => {}
} }
key_context key_context
@@ -2487,7 +2456,7 @@ impl Render for AgentPanel {
this this
} }
}) })
.child(self.render_prompt_editor( .child(self.render_text_thread(
context_editor, context_editor,
buffer_search_bar, buffer_search_bar,
window, window,
@@ -2538,8 +2507,7 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
}; };
let prompt_store = None; let prompt_store = None;
let thread_store = None; let thread_store = None;
let text_thread_store = None; let context_store = cx.new(|_| ContextStore::new(project.clone()));
let context_store = cx.new(|_| ContextStore::new(project.clone(), None));
assistant.assist( assistant.assist(
prompt_editor, prompt_editor,
self.workspace.clone(), self.workspace.clone(),
@@ -2547,7 +2515,6 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
project, project,
prompt_store, prompt_store,
thread_store, thread_store,
text_thread_store,
initial_prompt, initial_prompt,
window, window,
cx, cx,
@@ -2590,7 +2557,7 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
}; };
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel.open_saved_prompt_editor(path, window, cx) panel.open_saved_text_thread(path, window, cx)
}) })
} }

View File

@@ -4,8 +4,10 @@ mod agent_diff;
mod agent_model_selector; mod agent_model_selector;
mod agent_panel; mod agent_panel;
mod buffer_codegen; mod buffer_codegen;
mod context;
mod context_picker; mod context_picker;
mod context_server_configuration; mod context_server_configuration;
mod context_store;
mod context_strip; mod context_strip;
mod inline_assistant; mod inline_assistant;
mod inline_prompt_editor; mod inline_prompt_editor;
@@ -22,7 +24,6 @@ mod ui;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use agent::ThreadId;
use agent_settings::{AgentProfileId, AgentSettings}; use agent_settings::{AgentProfileId, AgentSettings};
use assistant_slash_command::SlashCommandRegistry; use assistant_slash_command::SlashCommandRegistry;
use client::Client; use client::Client;
@@ -139,10 +140,7 @@ pub struct QuoteSelection;
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)] #[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
#[action(namespace = agent)] #[action(namespace = agent)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct NewThread { pub struct NewThread;
#[serde(default)]
from_thread_id: Option<ThreadId>,
}
/// Creates a new external agent conversation thread. /// Creates a new external agent conversation thread.
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)] #[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
@@ -196,13 +194,13 @@ impl ExternalAgent {
pub fn server( pub fn server(
&self, &self,
fs: Arc<dyn fs::Fs>, fs: Arc<dyn fs::Fs>,
history: Entity<agent2::HistoryStore>, history: Entity<agent::HistoryStore>,
) -> Rc<dyn agent_servers::AgentServer> { ) -> Rc<dyn agent_servers::AgentServer> {
match self { match self {
Self::Gemini => Rc::new(agent_servers::Gemini), Self::Gemini => Rc::new(agent_servers::Gemini),
Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode), Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
Self::Codex => Rc::new(agent_servers::Codex), Self::Codex => Rc::new(agent_servers::Codex),
Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)), Self::NativeAgent => Rc::new(agent::NativeAgentServer::new(fs, history)),
Self::Custom { name, command: _ } => { Self::Custom { name, command: _ } => {
Rc::new(agent_servers::CustomAgentServer::new(name.clone())) Rc::new(agent_servers::CustomAgentServer::new(name.clone()))
} }
@@ -266,7 +264,6 @@ pub fn init(
init_language_model_settings(cx); init_language_model_settings(cx);
} }
assistant_slash_command::init(cx); assistant_slash_command::init(cx);
agent::init(fs.clone(), cx);
agent_panel::init(cx); agent_panel::init(cx);
context_server_configuration::init(language_registry.clone(), fs.clone(), cx); context_server_configuration::init(language_registry.clone(), fs.clone(), cx);
TextThreadEditor::init(cx); TextThreadEditor::init(cx);

View File

@@ -1,7 +1,5 @@
use crate::inline_prompt_editor::CodegenStatus; use crate::{
use agent::{ context::load_context, context_store::ContextStore, inline_prompt_editor::CodegenStatus,
ContextStore,
context::{ContextLoadResult, load_context},
}; };
use agent_settings::AgentSettings; use agent_settings::AgentSettings;
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
@@ -434,16 +432,16 @@ impl CodegenAlternative {
.generate_inline_transformation_prompt(user_prompt, language_name, buffer, range) .generate_inline_transformation_prompt(user_prompt, language_name, buffer, range)
.context("generating content prompt")?; .context("generating content prompt")?;
let context_task = self.context_store.as_ref().map(|context_store| { let context_task = self.context_store.as_ref().and_then(|context_store| {
if let Some(project) = self.project.upgrade() { if let Some(project) = self.project.upgrade() {
let context = context_store let context = context_store
.read(cx) .read(cx)
.context() .context()
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
load_context(context, &project, &self.prompt_store, cx) Some(load_context(context, &project, &self.prompt_store, cx))
} else { } else {
Task::ready(ContextLoadResult::default()) None
} }
}); });
@@ -459,7 +457,6 @@ impl CodegenAlternative {
if let Some(context_task) = context_task { if let Some(context_task) = context_task {
context_task context_task
.await .await
.loaded_context
.add_to_request_message(&mut request_message); .add_to_request_message(&mut request_message);
} }

View File

@@ -1,11 +1,8 @@
use crate::thread::Thread; use agent::outline;
use assistant_context::AssistantContext; use assistant_context::AssistantContext;
use assistant_tool::outline;
use collections::HashSet;
use futures::future; use futures::future;
use futures::{FutureExt, future::Shared}; use futures::{FutureExt, future::Shared};
use gpui::{App, AppContext as _, ElementId, Entity, SharedString, Task}; use gpui::{App, AppContext as _, ElementId, Entity, SharedString, Task};
use icons::IconName;
use language::Buffer; use language::Buffer;
use language_model::{LanguageModelImage, LanguageModelRequestMessage, MessageContent}; use language_model::{LanguageModelImage, LanguageModelRequestMessage, MessageContent};
use project::{Project, ProjectEntryId, ProjectPath, Worktree}; use project::{Project, ProjectEntryId, ProjectPath, Worktree};
@@ -17,6 +14,7 @@ use std::hash::{Hash, Hasher};
use std::path::PathBuf; use std::path::PathBuf;
use std::{ops::Range, path::Path, sync::Arc}; use std::{ops::Range, path::Path, sync::Arc};
use text::{Anchor, OffsetRangeExt as _}; use text::{Anchor, OffsetRangeExt as _};
use ui::IconName;
use util::markdown::MarkdownCodeBlock; use util::markdown::MarkdownCodeBlock;
use util::rel_path::RelPath; use util::rel_path::RelPath;
use util::{ResultExt as _, post_inc}; use util::{ResultExt as _, post_inc};
@@ -181,7 +179,7 @@ impl FileContextHandle {
}) })
} }
fn load(self, cx: &App) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> { fn load(self, cx: &App) -> Task<Option<AgentContext>> {
let buffer_ref = self.buffer.read(cx); let buffer_ref = self.buffer.read(cx);
let Some(file) = buffer_ref.file() else { let Some(file) = buffer_ref.file() else {
log::error!("file context missing path"); log::error!("file context missing path");
@@ -206,7 +204,7 @@ impl FileContextHandle {
text: buffer_content.text.into(), text: buffer_content.text.into(),
is_outline: buffer_content.is_outline, is_outline: buffer_content.is_outline,
}); });
Some((context, vec![buffer])) Some(context)
}) })
} }
} }
@@ -256,11 +254,7 @@ impl DirectoryContextHandle {
self.entry_id.hash(state) self.entry_id.hash(state)
} }
fn load( fn load(self, project: Entity<Project>, cx: &mut App) -> Task<Option<AgentContext>> {
self,
project: Entity<Project>,
cx: &mut App,
) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> {
let Some(worktree) = project.read(cx).worktree_for_entry(self.entry_id, cx) else { let Some(worktree) = project.read(cx).worktree_for_entry(self.entry_id, cx) else {
return Task::ready(None); return Task::ready(None);
}; };
@@ -307,7 +301,7 @@ impl DirectoryContextHandle {
}); });
cx.background_spawn(async move { cx.background_spawn(async move {
let (rope, buffer) = rope_task.await?; let (rope, _buffer) = rope_task.await?;
let fenced_codeblock = MarkdownCodeBlock { let fenced_codeblock = MarkdownCodeBlock {
tag: &codeblock_tag(&full_path, None), tag: &codeblock_tag(&full_path, None),
text: &rope.to_string(), text: &rope.to_string(),
@@ -318,18 +312,22 @@ impl DirectoryContextHandle {
rel_path, rel_path,
fenced_codeblock, fenced_codeblock,
}; };
Some((descendant, buffer)) Some(descendant)
}) })
})); }));
cx.background_spawn(async move { cx.background_spawn(async move {
let (descendants, buffers) = descendants_future.await.into_iter().flatten().unzip(); let descendants = descendants_future
.await
.into_iter()
.flatten()
.collect::<Vec<_>>();
let context = AgentContext::Directory(DirectoryContext { let context = AgentContext::Directory(DirectoryContext {
handle: self, handle: self,
full_path: directory_full_path, full_path: directory_full_path,
descendants, descendants,
}); });
Some((context, buffers)) Some(context)
}) })
} }
} }
@@ -397,7 +395,7 @@ impl SymbolContextHandle {
.into() .into()
} }
fn load(self, cx: &App) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> { fn load(self, cx: &App) -> Task<Option<AgentContext>> {
let buffer_ref = self.buffer.read(cx); let buffer_ref = self.buffer.read(cx);
let Some(file) = buffer_ref.file() else { let Some(file) = buffer_ref.file() else {
log::error!("symbol context's file has no path"); log::error!("symbol context's file has no path");
@@ -406,14 +404,13 @@ impl SymbolContextHandle {
let full_path = file.full_path(cx).to_string_lossy().into_owned(); let full_path = file.full_path(cx).to_string_lossy().into_owned();
let line_range = self.enclosing_range.to_point(&buffer_ref.snapshot()); let line_range = self.enclosing_range.to_point(&buffer_ref.snapshot());
let text = self.text(cx); let text = self.text(cx);
let buffer = self.buffer.clone();
let context = AgentContext::Symbol(SymbolContext { let context = AgentContext::Symbol(SymbolContext {
handle: self, handle: self,
full_path, full_path,
line_range, line_range,
text, text,
}); });
Task::ready(Some((context, vec![buffer]))) Task::ready(Some(context))
} }
} }
@@ -468,13 +465,12 @@ impl SelectionContextHandle {
.into() .into()
} }
fn load(self, cx: &App) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> { fn load(self, cx: &App) -> Task<Option<AgentContext>> {
let Some(full_path) = self.full_path(cx) else { let Some(full_path) = self.full_path(cx) else {
log::error!("selection context's file has no path"); log::error!("selection context's file has no path");
return Task::ready(None); return Task::ready(None);
}; };
let text = self.text(cx); let text = self.text(cx);
let buffer = self.buffer.clone();
let context = AgentContext::Selection(SelectionContext { let context = AgentContext::Selection(SelectionContext {
full_path: full_path.to_string_lossy().into_owned(), full_path: full_path.to_string_lossy().into_owned(),
line_range: self.line_range(cx), line_range: self.line_range(cx),
@@ -482,7 +478,7 @@ impl SelectionContextHandle {
handle: self, handle: self,
}); });
Task::ready(Some((context, vec![buffer]))) Task::ready(Some(context))
} }
} }
@@ -523,8 +519,8 @@ impl FetchedUrlContext {
})) }))
} }
pub fn load(self) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> { pub fn load(self) -> Task<Option<AgentContext>> {
Task::ready(Some((AgentContext::FetchedUrl(self), vec![]))) Task::ready(Some(AgentContext::FetchedUrl(self)))
} }
} }
@@ -537,7 +533,7 @@ impl Display for FetchedUrlContext {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ThreadContextHandle { pub struct ThreadContextHandle {
pub thread: Entity<Thread>, pub thread: Entity<agent::Thread>,
pub context_id: ContextId, pub context_id: ContextId,
} }
@@ -558,22 +554,20 @@ impl ThreadContextHandle {
} }
pub fn title(&self, cx: &App) -> SharedString { pub fn title(&self, cx: &App) -> SharedString {
self.thread.read(cx).summary().or_default() self.thread.read(cx).title()
} }
fn load(self, cx: &App) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> { fn load(self, cx: &mut App) -> Task<Option<AgentContext>> {
cx.spawn(async move |cx| { let task = self.thread.update(cx, |thread, cx| thread.summary(cx));
let text = Thread::wait_for_detailed_summary_or_text(&self.thread, cx).await?; let title = self.title(cx);
let title = self cx.background_spawn(async move {
.thread let text = task.await?;
.read_with(cx, |thread, _cx| thread.summary().or_default())
.ok()?;
let context = AgentContext::Thread(ThreadContext { let context = AgentContext::Thread(ThreadContext {
title, title,
text, text,
handle: self, handle: self,
}); });
Some((context, vec![])) Some(context)
}) })
} }
} }
@@ -612,7 +606,7 @@ impl TextThreadContextHandle {
self.context.read(cx).summary().or_default() self.context.read(cx).summary().or_default()
} }
fn load(self, cx: &App) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> { fn load(self, cx: &App) -> Task<Option<AgentContext>> {
let title = self.title(cx); let title = self.title(cx);
let text = self.context.read(cx).to_xml(cx); let text = self.context.read(cx).to_xml(cx);
let context = AgentContext::TextThread(TextThreadContext { let context = AgentContext::TextThread(TextThreadContext {
@@ -620,7 +614,7 @@ impl TextThreadContextHandle {
text: text.into(), text: text.into(),
handle: self, handle: self,
}); });
Task::ready(Some((context, vec![]))) Task::ready(Some(context))
} }
} }
@@ -666,7 +660,7 @@ impl RulesContextHandle {
self, self,
prompt_store: &Option<Entity<PromptStore>>, prompt_store: &Option<Entity<PromptStore>>,
cx: &App, cx: &App,
) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> { ) -> Task<Option<AgentContext>> {
let Some(prompt_store) = prompt_store.as_ref() else { let Some(prompt_store) = prompt_store.as_ref() else {
return Task::ready(None); return Task::ready(None);
}; };
@@ -685,7 +679,7 @@ impl RulesContextHandle {
title, title,
text, text,
}); });
Some((context, vec![])) Some(context)
}) })
} }
} }
@@ -748,32 +742,21 @@ impl ImageContext {
} }
} }
pub fn load(self, cx: &App) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> { pub fn load(self, cx: &App) -> Task<Option<AgentContext>> {
cx.background_spawn(async move { cx.background_spawn(async move {
self.image_task.clone().await; self.image_task.clone().await;
Some((AgentContext::Image(self), vec![])) Some(AgentContext::Image(self))
}) })
} }
} }
#[derive(Debug, Clone, Default)]
pub struct ContextLoadResult {
pub loaded_context: LoadedContext,
pub referenced_buffers: HashSet<Entity<Buffer>>,
}
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct LoadedContext { pub struct LoadedContext {
pub contexts: Vec<AgentContext>,
pub text: String, pub text: String,
pub images: Vec<LanguageModelImage>, pub images: Vec<LanguageModelImage>,
} }
impl LoadedContext { impl LoadedContext {
pub fn is_empty(&self) -> bool {
self.text.is_empty() && self.images.is_empty()
}
pub fn add_to_request_message(&self, request_message: &mut LanguageModelRequestMessage) { pub fn add_to_request_message(&self, request_message: &mut LanguageModelRequestMessage) {
if !self.text.is_empty() { if !self.text.is_empty() {
request_message request_message
@@ -804,7 +787,7 @@ pub fn load_context(
project: &Entity<Project>, project: &Entity<Project>,
prompt_store: &Option<Entity<PromptStore>>, prompt_store: &Option<Entity<PromptStore>>,
cx: &mut App, cx: &mut App,
) -> Task<ContextLoadResult> { ) -> Task<LoadedContext> {
let load_tasks: Vec<_> = contexts let load_tasks: Vec<_> = contexts
.into_iter() .into_iter()
.map(|context| match context { .map(|context| match context {
@@ -823,16 +806,7 @@ pub fn load_context(
cx.background_spawn(async move { cx.background_spawn(async move {
let load_results = future::join_all(load_tasks).await; let load_results = future::join_all(load_tasks).await;
let mut contexts = Vec::new();
let mut text = String::new(); let mut text = String::new();
let mut referenced_buffers = HashSet::default();
for context in load_results {
let Some((context, buffers)) = context else {
continue;
};
contexts.push(context);
referenced_buffers.extend(buffers);
}
let mut file_context = Vec::new(); let mut file_context = Vec::new();
let mut directory_context = Vec::new(); let mut directory_context = Vec::new();
@@ -843,7 +817,7 @@ pub fn load_context(
let mut text_thread_context = Vec::new(); let mut text_thread_context = Vec::new();
let mut rules_context = Vec::new(); let mut rules_context = Vec::new();
let mut images = Vec::new(); let mut images = Vec::new();
for context in &contexts { for context in load_results.into_iter().flatten() {
match context { match context {
AgentContext::File(context) => file_context.push(context), AgentContext::File(context) => file_context.push(context),
AgentContext::Directory(context) => directory_context.push(context), AgentContext::Directory(context) => directory_context.push(context),
@@ -868,14 +842,7 @@ pub fn load_context(
&& text_thread_context.is_empty() && text_thread_context.is_empty()
&& rules_context.is_empty() && rules_context.is_empty()
{ {
return ContextLoadResult { return LoadedContext { text, images };
loaded_context: LoadedContext {
contexts,
text,
images,
},
referenced_buffers,
};
} }
text.push_str( text.push_str(
@@ -961,14 +928,7 @@ pub fn load_context(
text.push_str("</context>\n"); text.push_str("</context>\n");
ContextLoadResult { LoadedContext { text, images }
loaded_context: LoadedContext {
contexts,
text,
images,
},
referenced_buffers,
}
}) })
} }
@@ -1131,11 +1091,13 @@ mod tests {
assert!(content_len > outline::AUTO_OUTLINE_SIZE); assert!(content_len > outline::AUTO_OUTLINE_SIZE);
let file_context = file_context_for(large_content, cx).await; let file_context = load_context_for("file.txt", large_content, cx).await;
assert!( assert!(
file_context.is_outline, file_context
"Large file should use outline format" .text
.contains(&format!("# File outline for {}", path!("test/file.txt"))),
"Large files should not get an outline"
); );
assert!( assert!(
@@ -1153,29 +1115,38 @@ mod tests {
assert!(content_len < outline::AUTO_OUTLINE_SIZE); assert!(content_len < outline::AUTO_OUTLINE_SIZE);
let file_context = file_context_for(small_content.to_string(), cx).await; let file_context = load_context_for("file.txt", small_content.to_string(), cx).await;
assert!( assert!(
!file_context.is_outline, !file_context
.text
.contains(&format!("# File outline for {}", path!("test/file.txt"))),
"Small files should not get an outline" "Small files should not get an outline"
); );
assert_eq!(file_context.text, small_content); assert!(
file_context.text.contains(small_content),
"Small files should use full content"
);
} }
async fn file_context_for(content: String, cx: &mut TestAppContext) -> FileContext { async fn load_context_for(
filename: &str,
content: String,
cx: &mut TestAppContext,
) -> LoadedContext {
// Create a test project with the file // Create a test project with the file
let project = create_test_project( let project = create_test_project(
cx, cx,
json!({ json!({
"file.txt": content, filename: content,
}), }),
) )
.await; .await;
// Open the buffer // Open the buffer
let buffer_path = project let buffer_path = project
.read_with(cx, |project, cx| project.find_project_path("file.txt", cx)) .read_with(cx, |project, cx| project.find_project_path(filename, cx))
.unwrap(); .unwrap();
let buffer = project let buffer = project
@@ -1190,16 +1161,5 @@ mod tests {
cx.update(|cx| load_context(vec![context_handle], &project, &None, cx)) cx.update(|cx| load_context(vec![context_handle], &project, &None, cx))
.await .await
.loaded_context
.contexts
.into_iter()
.find_map(|ctx| {
if let AgentContext::File(file_ctx) = ctx {
Some(file_ctx)
} else {
None
}
})
.expect("Should have found a file context")
} }
} }

View File

@@ -9,6 +9,8 @@ use std::ops::Range;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use agent::{HistoryEntry, HistoryEntryId, HistoryStore};
use agent_client_protocol as acp;
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use collections::HashSet; use collections::HashSet;
pub use completion_provider::ContextPickerCompletionProvider; pub use completion_provider::ContextPickerCompletionProvider;
@@ -27,9 +29,7 @@ use project::ProjectPath;
use prompt_store::PromptStore; use prompt_store::PromptStore;
use rules_context_picker::{RulesContextEntry, RulesContextPicker}; use rules_context_picker::{RulesContextEntry, RulesContextPicker};
use symbol_context_picker::SymbolContextPicker; use symbol_context_picker::SymbolContextPicker;
use thread_context_picker::{ use thread_context_picker::render_thread_context_entry;
ThreadContextEntry, ThreadContextPicker, render_thread_context_entry, unordered_thread_entries,
};
use ui::{ use ui::{
ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*, ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
}; };
@@ -37,12 +37,8 @@ use util::paths::PathStyle;
use util::rel_path::RelPath; use util::rel_path::RelPath;
use workspace::{Workspace, notifications::NotifyResultExt}; use workspace::{Workspace, notifications::NotifyResultExt};
use agent::{ use crate::context_picker::thread_context_picker::ThreadContextPicker;
ThreadId, use crate::{context::RULES_ICON, context_store::ContextStore};
context::RULES_ICON,
context_store::ContextStore,
thread_store::{TextThreadStore, ThreadStore},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ContextPickerEntry { pub(crate) enum ContextPickerEntry {
@@ -168,17 +164,16 @@ pub(super) struct ContextPicker {
mode: ContextPickerState, mode: ContextPickerState,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>, context_store: WeakEntity<ContextStore>,
thread_store: Option<WeakEntity<ThreadStore>>, thread_store: Option<WeakEntity<HistoryStore>>,
text_thread_store: Option<WeakEntity<TextThreadStore>>, prompt_store: Option<WeakEntity<PromptStore>>,
prompt_store: Option<Entity<PromptStore>>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
impl ContextPicker { impl ContextPicker {
pub fn new( pub fn new(
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<ThreadStore>>, thread_store: Option<WeakEntity<HistoryStore>>,
text_thread_store: Option<WeakEntity<TextThreadStore>>, prompt_store: Option<WeakEntity<PromptStore>>,
context_store: WeakEntity<ContextStore>, context_store: WeakEntity<ContextStore>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
@@ -199,13 +194,6 @@ impl ContextPicker {
) )
.collect::<Vec<Subscription>>(); .collect::<Vec<Subscription>>();
let prompt_store = thread_store.as_ref().and_then(|thread_store| {
thread_store
.read_with(cx, |thread_store, _cx| thread_store.prompt_store().clone())
.ok()
.flatten()
});
ContextPicker { ContextPicker {
mode: ContextPickerState::Default(ContextMenu::build( mode: ContextPickerState::Default(ContextMenu::build(
window, window,
@@ -215,7 +203,6 @@ impl ContextPicker {
workspace, workspace,
context_store, context_store,
thread_store, thread_store,
text_thread_store,
prompt_store, prompt_store,
_subscriptions: subscriptions, _subscriptions: subscriptions,
} }
@@ -355,17 +342,13 @@ impl ContextPicker {
})); }));
} }
ContextPickerMode::Thread => { ContextPickerMode::Thread => {
if let Some((thread_store, text_thread_store)) = self if let Some(thread_store) = self.thread_store.clone() {
.thread_store
.as_ref()
.zip(self.text_thread_store.as_ref())
{
self.mode = ContextPickerState::Thread(cx.new(|cx| { self.mode = ContextPickerState::Thread(cx.new(|cx| {
ThreadContextPicker::new( ThreadContextPicker::new(
thread_store.clone(), thread_store,
text_thread_store.clone(),
context_picker.clone(), context_picker.clone(),
self.context_store.clone(), self.context_store.clone(),
self.workspace.clone(),
window, window,
cx, cx,
) )
@@ -480,16 +463,23 @@ impl ContextPicker {
fn add_recent_thread( fn add_recent_thread(
&self, &self,
entry: ThreadContextEntry, entry: HistoryEntry,
window: &mut Window, _window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let Some(context_store) = self.context_store.upgrade() else { let Some(context_store) = self.context_store.upgrade() else {
return Task::ready(Err(anyhow!("context store not available"))); return Task::ready(Err(anyhow!("context store not available")));
}; };
let Some(project) = self
.workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
else {
return Task::ready(Err(anyhow!("project not available")));
};
match entry { match entry {
ThreadContextEntry::Thread { id, .. } => { HistoryEntry::AcpThread(thread) => {
let Some(thread_store) = self let Some(thread_store) = self
.thread_store .thread_store
.as_ref() .as_ref()
@@ -497,28 +487,28 @@ impl ContextPicker {
else { else {
return Task::ready(Err(anyhow!("thread store not available"))); return Task::ready(Err(anyhow!("thread store not available")));
}; };
let load_thread_task =
let open_thread_task = agent::load_agent_thread(thread.id, thread_store, project, cx);
thread_store.update(cx, |this, cx| this.open_thread(&id, window, cx));
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
let thread = open_thread_task.await?; let thread = load_thread_task.await?;
context_store.update(cx, |context_store, cx| { context_store.update(cx, |context_store, cx| {
context_store.add_thread(thread, true, cx); context_store.add_thread(thread, true, cx);
})?; })?;
this.update(cx, |_this, cx| cx.notify()) this.update(cx, |_this, cx| cx.notify())
}) })
} }
ThreadContextEntry::Context { path, .. } => { HistoryEntry::TextThread(thread) => {
let Some(text_thread_store) = self let Some(thread_store) = self
.text_thread_store .thread_store
.as_ref() .as_ref()
.and_then(|thread_store| thread_store.upgrade()) .and_then(|thread_store| thread_store.upgrade())
else { else {
return Task::ready(Err(anyhow!("text thread store not available"))); return Task::ready(Err(anyhow!("text thread store not available")));
}; };
let task = text_thread_store let task = thread_store.update(cx, |this, cx| {
.update(cx, |this, cx| this.open_local_context(path.clone(), cx)); this.load_text_thread(thread.path.clone(), cx)
});
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
let thread = task.await?; let thread = task.await?;
context_store.update(cx, |context_store, cx| { context_store.update(cx, |context_store, cx| {
@@ -542,7 +532,6 @@ impl ContextPicker {
recent_context_picker_entries_with_store( recent_context_picker_entries_with_store(
context_store, context_store,
self.thread_store.clone(), self.thread_store.clone(),
self.text_thread_store.clone(),
workspace, workspace,
None, None,
cx, cx,
@@ -599,12 +588,12 @@ pub(crate) enum RecentEntry {
project_path: ProjectPath, project_path: ProjectPath,
path_prefix: Arc<RelPath>, path_prefix: Arc<RelPath>,
}, },
Thread(ThreadContextEntry), Thread(HistoryEntry),
} }
pub(crate) fn available_context_picker_entries( pub(crate) fn available_context_picker_entries(
prompt_store: &Option<Entity<PromptStore>>, prompt_store: &Option<WeakEntity<PromptStore>>,
thread_store: &Option<WeakEntity<ThreadStore>>, thread_store: &Option<WeakEntity<HistoryStore>>,
workspace: &Entity<Workspace>, workspace: &Entity<Workspace>,
cx: &mut App, cx: &mut App,
) -> Vec<ContextPickerEntry> { ) -> Vec<ContextPickerEntry> {
@@ -639,8 +628,7 @@ pub(crate) fn available_context_picker_entries(
fn recent_context_picker_entries_with_store( fn recent_context_picker_entries_with_store(
context_store: Entity<ContextStore>, context_store: Entity<ContextStore>,
thread_store: Option<WeakEntity<ThreadStore>>, thread_store: Option<WeakEntity<HistoryStore>>,
text_thread_store: Option<WeakEntity<TextThreadStore>>,
workspace: Entity<Workspace>, workspace: Entity<Workspace>,
exclude_path: Option<ProjectPath>, exclude_path: Option<ProjectPath>,
cx: &App, cx: &App,
@@ -657,22 +645,14 @@ fn recent_context_picker_entries_with_store(
let exclude_threads = context_store.read(cx).thread_ids(); let exclude_threads = context_store.read(cx).thread_ids();
recent_context_picker_entries( recent_context_picker_entries(thread_store, workspace, &exclude_paths, exclude_threads, cx)
thread_store,
text_thread_store,
workspace,
&exclude_paths,
exclude_threads,
cx,
)
} }
pub(crate) fn recent_context_picker_entries( pub(crate) fn recent_context_picker_entries(
thread_store: Option<WeakEntity<ThreadStore>>, thread_store: Option<WeakEntity<HistoryStore>>,
text_thread_store: Option<WeakEntity<TextThreadStore>>,
workspace: Entity<Workspace>, workspace: Entity<Workspace>,
exclude_paths: &HashSet<PathBuf>, exclude_paths: &HashSet<PathBuf>,
_exclude_threads: &HashSet<ThreadId>, exclude_threads: &HashSet<acp::SessionId>,
cx: &App, cx: &App,
) -> Vec<RecentEntry> { ) -> Vec<RecentEntry> {
let mut recent = Vec::with_capacity(6); let mut recent = Vec::with_capacity(6);
@@ -698,30 +678,21 @@ pub(crate) fn recent_context_picker_entries(
}), }),
); );
if let Some((thread_store, text_thread_store)) = thread_store if let Some(thread_store) = thread_store.and_then(|store| store.upgrade()) {
.and_then(|store| store.upgrade()) const RECENT_THREADS_COUNT: usize = 2;
.zip(text_thread_store.and_then(|store| store.upgrade()))
{
let mut threads = unordered_thread_entries(thread_store, text_thread_store, cx)
.filter(|(_, thread)| match thread {
ThreadContextEntry::Thread { .. } => false,
ThreadContextEntry::Context { .. } => true,
})
.collect::<Vec<_>>();
const RECENT_COUNT: usize = 2;
if threads.len() > RECENT_COUNT {
threads.select_nth_unstable_by_key(RECENT_COUNT - 1, |(updated_at, _)| {
std::cmp::Reverse(*updated_at)
});
threads.truncate(RECENT_COUNT);
}
threads.sort_unstable_by_key(|(updated_at, _)| std::cmp::Reverse(*updated_at));
recent.extend( recent.extend(
threads thread_store
.into_iter() .read(cx)
.map(|(_, thread)| RecentEntry::Thread(thread)), .recently_opened_entries(cx)
.iter()
.filter(|e| match e.id() {
HistoryEntryId::AcpThread(session_id) => !exclude_threads.contains(&session_id),
HistoryEntryId::TextThread(path) => {
!exclude_paths.contains(&path.to_path_buf())
}
})
.take(RECENT_THREADS_COUNT)
.map(|thread| RecentEntry::Thread(thread.clone())),
); );
} }
@@ -915,17 +886,21 @@ impl MentionLink {
) )
} }
pub fn for_thread(thread: &ThreadContextEntry) -> String { pub fn for_thread(thread: &HistoryEntry) -> String {
match thread { match thread {
ThreadContextEntry::Thread { id, title } => { HistoryEntry::AcpThread(thread) => {
format!("[@{}]({}:{})", title, Self::THREAD, id) format!("[@{}]({}:{})", thread.title, Self::THREAD, thread.id)
} }
ThreadContextEntry::Context { path, title } => { HistoryEntry::TextThread(thread) => {
let filename = path.file_name().unwrap_or_default().to_string_lossy(); let filename = thread
.path
.file_name()
.unwrap_or_default()
.to_string_lossy();
let escaped_filename = urlencoding::encode(&filename); let escaped_filename = urlencoding::encode(&filename);
format!( format!(
"[@{}]({}:{}{})", "[@{}]({}:{}{})",
title, thread.title,
Self::THREAD, Self::THREAD,
Self::TEXT_THREAD_URL_PREFIX, Self::TEXT_THREAD_URL_PREFIX,
escaped_filename escaped_filename

Some files were not shown because too many files have changed in this diff Show More