@@ -48,7 +48,7 @@
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
"ensure_final_newline_on_save": true,
|
||||
"file_scan_exclusions": [
|
||||
"crates/assistant_tools/src/edit_agent/evals/fixtures",
|
||||
"crates/agent/src/edit_agent/evals/fixtures",
|
||||
"crates/eval/worktrees/",
|
||||
"crates/eval/repos/",
|
||||
"**/.git",
|
||||
|
||||
266
Cargo.lock
generated
266
Cargo.lock
generated
@@ -139,90 +139,14 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "agent"
|
||||
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 = [
|
||||
"acp_thread",
|
||||
"action_log",
|
||||
"agent",
|
||||
"agent-client-protocol",
|
||||
"agent_servers",
|
||||
"agent_settings",
|
||||
"anyhow",
|
||||
"assistant_context",
|
||||
"assistant_tool",
|
||||
"assistant_tools",
|
||||
"chrono",
|
||||
"client",
|
||||
"clock",
|
||||
@@ -231,6 +155,7 @@ dependencies = [
|
||||
"context_server",
|
||||
"ctor",
|
||||
"db",
|
||||
"derive_more",
|
||||
"editor",
|
||||
"env_logger 0.11.8",
|
||||
"fs",
|
||||
@@ -254,14 +179,19 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"reqwest_client",
|
||||
"rust-embed",
|
||||
"schemars 1.0.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"sqlez",
|
||||
"streaming_diff",
|
||||
"strsim",
|
||||
"task",
|
||||
"telemetry",
|
||||
"tempfile",
|
||||
@@ -283,6 +213,23 @@ dependencies = [
|
||||
"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 = "agent_servers"
|
||||
version = "0.1.0"
|
||||
@@ -356,7 +303,6 @@ dependencies = [
|
||||
"action_log",
|
||||
"agent",
|
||||
"agent-client-protocol",
|
||||
"agent2",
|
||||
"agent_servers",
|
||||
"agent_settings",
|
||||
"ai_onboarding",
|
||||
@@ -365,8 +311,6 @@ dependencies = [
|
||||
"assistant_context",
|
||||
"assistant_slash_command",
|
||||
"assistant_slash_commands",
|
||||
"assistant_tool",
|
||||
"assistant_tools",
|
||||
"audio",
|
||||
"buffer_diff",
|
||||
"chrono",
|
||||
@@ -411,6 +355,7 @@ dependencies = [
|
||||
"prompt_store",
|
||||
"proto",
|
||||
"rand 0.9.1",
|
||||
"ref-cast",
|
||||
"release_channel",
|
||||
"rope",
|
||||
"rules_library",
|
||||
@@ -965,106 +910,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "async-attributes"
|
||||
version = "1.1.2"
|
||||
@@ -5819,63 +5664,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "event-listener"
|
||||
version = "2.5.3"
|
||||
@@ -8987,7 +8775,6 @@ dependencies = [
|
||||
"open_router",
|
||||
"parking_lot",
|
||||
"proto",
|
||||
"schemars 1.0.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
@@ -14006,10 +13793,9 @@ name = "remote_server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"action_log",
|
||||
"agent",
|
||||
"anyhow",
|
||||
"askpass",
|
||||
"assistant_tool",
|
||||
"assistant_tools",
|
||||
"cargo_toml",
|
||||
"clap",
|
||||
"client",
|
||||
@@ -21242,14 +21028,12 @@ version = "0.210.0"
|
||||
dependencies = [
|
||||
"acp_tools",
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
"agent_settings",
|
||||
"agent_ui",
|
||||
"anyhow",
|
||||
"ashpd 0.11.0",
|
||||
"askpass",
|
||||
"assets",
|
||||
"assistant_tools",
|
||||
"audio",
|
||||
"auto_update",
|
||||
"auto_update_ui",
|
||||
|
||||
@@ -6,7 +6,6 @@ members = [
|
||||
"crates/action_log",
|
||||
"crates/activity_indicator",
|
||||
"crates/agent",
|
||||
"crates/agent2",
|
||||
"crates/agent_servers",
|
||||
"crates/agent_settings",
|
||||
"crates/agent_ui",
|
||||
@@ -17,8 +16,6 @@ members = [
|
||||
"crates/assistant_context",
|
||||
"crates/assistant_slash_command",
|
||||
"crates/assistant_slash_commands",
|
||||
"crates/assistant_tool",
|
||||
"crates/assistant_tools",
|
||||
"crates/audio",
|
||||
"crates/auto_update",
|
||||
"crates/auto_update_helper",
|
||||
@@ -61,7 +58,7 @@ members = [
|
||||
"crates/edit_prediction_context",
|
||||
"crates/zeta2_tools",
|
||||
"crates/editor",
|
||||
"crates/eval",
|
||||
# "crates/eval",
|
||||
"crates/explorer_command_injector",
|
||||
"crates/extension",
|
||||
"crates/extension_api",
|
||||
@@ -240,7 +237,6 @@ acp_tools = { path = "crates/acp_tools" }
|
||||
acp_thread = { path = "crates/acp_thread" }
|
||||
action_log = { path = "crates/action_log" }
|
||||
agent = { path = "crates/agent" }
|
||||
agent2 = { path = "crates/agent2" }
|
||||
activity_indicator = { path = "crates/activity_indicator" }
|
||||
agent_ui = { path = "crates/agent_ui" }
|
||||
agent_settings = { path = "crates/agent_settings" }
|
||||
@@ -253,8 +249,6 @@ assets = { path = "crates/assets" }
|
||||
assistant_context = { path = "crates/assistant_context" }
|
||||
assistant_slash_command = { path = "crates/assistant_slash_command" }
|
||||
assistant_slash_commands = { path = "crates/assistant_slash_commands" }
|
||||
assistant_tool = { path = "crates/assistant_tool" }
|
||||
assistant_tools = { path = "crates/assistant_tools" }
|
||||
audio = { path = "crates/audio" }
|
||||
auto_update = { path = "crates/auto_update" }
|
||||
auto_update_helper = { path = "crates/auto_update_helper" }
|
||||
|
||||
@@ -269,14 +269,14 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && prompt_editor",
|
||||
"context": "AgentPanel && text_thread",
|
||||
"bindings": {
|
||||
"ctrl-n": "agent::NewTextThread",
|
||||
"ctrl-alt-t": "agent::NewThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && external_agent_thread",
|
||||
"context": "AgentPanel && acp_thread",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-n": "agent::NewExternalAgentThread",
|
||||
|
||||
@@ -307,7 +307,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && prompt_editor",
|
||||
"context": "AgentPanel && text_thread",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "agent::NewTextThread",
|
||||
@@ -315,7 +315,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && external_agent_thread",
|
||||
"context": "AgentPanel && acp_thread",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "agent::NewExternalAgentThread",
|
||||
|
||||
@@ -270,7 +270,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && prompt_editor",
|
||||
"context": "AgentPanel && text_thread",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-n": "agent::NewTextThread",
|
||||
@@ -278,7 +278,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && external_agent_thread",
|
||||
"context": "AgentPanel && acp_thread",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-n": "agent::NewExternalAgentThread",
|
||||
|
||||
@@ -3,7 +3,7 @@ avoid-breaking-exported-api = false
|
||||
ignore-interior-mutability = [
|
||||
# Suppresses clippy::mutable_key_type, which is a false positive as the Eq
|
||||
# and Hash impls do not use fields with interior mutability.
|
||||
"agent::context::AgentContextKey"
|
||||
"agent_ui::context::AgentContextKey"
|
||||
]
|
||||
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" },
|
||||
|
||||
@@ -5,74 +5,100 @@ edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
path = "src/agent.rs"
|
||||
|
||||
[features]
|
||||
test-support = ["db/test-support"]
|
||||
e2e = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/agent.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"gpui/test-support",
|
||||
"language/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
acp_thread.workspace = true
|
||||
action_log.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
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
context_server.workspace = true
|
||||
convert_case.workspace = true
|
||||
db.workspace = true
|
||||
derive_more.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
git.workspace = true
|
||||
gpui.workspace = true
|
||||
heed.workspace = true
|
||||
handlebars = { workspace = true, features = ["rust-embed"] }
|
||||
html_to_markdown.workspace = true
|
||||
http_client.workspace = true
|
||||
icons.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
|
||||
postage.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
ref-cast.workspace = true
|
||||
rope.workspace = true
|
||||
regex.workspace = true
|
||||
rust-embed.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
sqlez.workspace = true
|
||||
streaming_diff.workspace = true
|
||||
strsim.workspace = true
|
||||
task.workspace = true
|
||||
telemetry.workspace = true
|
||||
terminal.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.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]
|
||||
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"] }
|
||||
indoc.workspace = true
|
||||
gpui_tokio.workspace = true
|
||||
language = { 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
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, "features" = ["test-support"] }
|
||||
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
@@ -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!()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::{AgentMessage, AgentMessageContent, UserMessage, UserMessageContent};
|
||||
use acp_thread::UserMessageId;
|
||||
use agent::{thread::DetailedSummaryState, thread_store};
|
||||
use agent_client_protocol as acp;
|
||||
use agent_settings::{AgentProfileId, CompletionMode};
|
||||
use anyhow::{Result, anyhow};
|
||||
@@ -21,8 +20,8 @@ use ui::{App, SharedString};
|
||||
use zed_env_vars::ZED_STATELESS;
|
||||
|
||||
pub type DbMessage = crate::Message;
|
||||
pub type DbSummary = DetailedSummaryState;
|
||||
pub type DbLanguageModel = thread_store::SerializedLanguageModel;
|
||||
pub type DbSummary = crate::legacy_thread::DetailedSummaryState;
|
||||
pub type DbLanguageModel = crate::legacy_thread::SerializedLanguageModel;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DbThreadMetadata {
|
||||
@@ -40,7 +39,7 @@ pub struct DbThread {
|
||||
#[serde(default)]
|
||||
pub detailed_summary: Option<SharedString>,
|
||||
#[serde(default)]
|
||||
pub initial_project_snapshot: Option<Arc<agent::thread::ProjectSnapshot>>,
|
||||
pub initial_project_snapshot: Option<Arc<crate::ProjectSnapshot>>,
|
||||
#[serde(default)]
|
||||
pub cumulative_token_usage: language_model::TokenUsage,
|
||||
#[serde(default)]
|
||||
@@ -61,13 +60,17 @@ impl DbThread {
|
||||
match saved_thread_json.get("version") {
|
||||
Some(serde_json::Value::String(version)) => match version.as_str() {
|
||||
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 request_token_usage = HashMap::default();
|
||||
|
||||
@@ -80,14 +83,19 @@ impl DbThread {
|
||||
// Convert segments to content
|
||||
for segment in msg.segments {
|
||||
match segment {
|
||||
thread_store::SerializedMessageSegment::Text { text } => {
|
||||
crate::legacy_thread::SerializedMessageSegment::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
|
||||
content.push(UserMessageContent::Text(text));
|
||||
}
|
||||
thread_store::SerializedMessageSegment::RedactedThinking { .. } => {
|
||||
crate::legacy_thread::SerializedMessageSegment::RedactedThinking {
|
||||
..
|
||||
} => {
|
||||
// User messages don't have redacted thinking, skip.
|
||||
}
|
||||
}
|
||||
@@ -113,16 +121,18 @@ impl DbThread {
|
||||
// Convert segments to content
|
||||
for segment in msg.segments {
|
||||
match segment {
|
||||
thread_store::SerializedMessageSegment::Text { text } => {
|
||||
crate::legacy_thread::SerializedMessageSegment::Text { text } => {
|
||||
content.push(AgentMessageContent::Text(text));
|
||||
}
|
||||
thread_store::SerializedMessageSegment::Thinking {
|
||||
crate::legacy_thread::SerializedMessageSegment::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));
|
||||
}
|
||||
}
|
||||
@@ -187,10 +197,9 @@ impl DbThread {
|
||||
messages,
|
||||
updated_at: thread.updated_at,
|
||||
detailed_summary: match thread.detailed_summary_state {
|
||||
DetailedSummaryState::NotGenerated | DetailedSummaryState::Generating { .. } => {
|
||||
None
|
||||
}
|
||||
DetailedSummaryState::Generated { text, .. } => Some(text),
|
||||
crate::legacy_thread::DetailedSummaryState::NotGenerated
|
||||
| crate::legacy_thread::DetailedSummaryState::Generating => None,
|
||||
crate::legacy_thread::DetailedSummaryState::Generated { text, .. } => Some(text),
|
||||
},
|
||||
initial_project_snapshot: thread.initial_project_snapshot,
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
use super::*;
|
||||
use crate::{
|
||||
ReadFileToolInput,
|
||||
edit_file_tool::{EditFileMode, EditFileToolInput},
|
||||
grep_tool::GrepToolInput,
|
||||
list_directory_tool::ListDirectoryToolInput,
|
||||
EditFileMode, EditFileToolInput, GrepToolInput, ListDirectoryToolInput, ReadFileToolInput,
|
||||
};
|
||||
use Role::*;
|
||||
use assistant_tool::ToolRegistry;
|
||||
use client::{Client, UserStore};
|
||||
use collections::HashMap;
|
||||
use fs::FakeFs;
|
||||
@@ -15,11 +11,11 @@ use gpui::{AppContext, TestAppContext, Timer};
|
||||
use http_client::StatusCode;
|
||||
use indoc::{formatdoc, indoc};
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, SelectedModel,
|
||||
LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolResultContent,
|
||||
LanguageModelToolUse, LanguageModelToolUseId, SelectedModel,
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_store::{ModelContext, ProjectContext, PromptBuilder, WorktreeContext};
|
||||
use prompt_store::{ProjectContext, WorktreeContext};
|
||||
use rand::prelude::*;
|
||||
use reqwest_client::ReqwestClient;
|
||||
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-flash |
|
||||
// gpt-4.1 |
|
||||
|
||||
let input_file_path = "root/blame.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");
|
||||
@@ -184,6 +181,7 @@ fn eval_translate_doc_comments() {
|
||||
// gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22)
|
||||
// gemini-2.5-flash-preview-04-17 |
|
||||
// gpt-4.1 |
|
||||
|
||||
let input_file_path = "root/canvas.rs";
|
||||
let input_file_content = include_str!("evals/fixtures/translate_doc_comments/before.rs");
|
||||
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-flash-preview-04-17 |
|
||||
// gpt-4.1 |
|
||||
|
||||
let input_file_path = "root/lib.rs";
|
||||
let input_file_content =
|
||||
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-flash-preview-04-17 | 0.78 (2025-07-14)
|
||||
// gpt-4.1 | 0.00 (2025-07-14) (follows edit_description too literally)
|
||||
|
||||
let input_file_path = "root/editor.rs";
|
||||
let input_file_content = include_str!("evals/fixtures/disable_cursor_blinking/before.rs");
|
||||
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
|
||||
// gemini-2.5-pro-preview-06-05 | 2025-06-16 | 0.98
|
||||
// gpt-4.1 |
|
||||
|
||||
let input_file_path = "root/canvas.rs";
|
||||
let input_file_content = include_str!("evals/fixtures/from_pixels_constructor/before.rs");
|
||||
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-flash-preview-04-17 | 1.0 (2025-05-22)
|
||||
// gpt-4.1 | 1.0 (2025-05-22)
|
||||
|
||||
let input_file_path = "root/zode.py";
|
||||
let input_content = None;
|
||||
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-flash-preview-04-17 |
|
||||
// gpt-4.1 |
|
||||
|
||||
let input_file_path = "root/action_log.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";
|
||||
@@ -1010,7 +1013,7 @@ fn eval_create_empty_file() {
|
||||
//
|
||||
// TODO: gpt-4.1-mini errored 38 times:
|
||||
// "data did not match any variant of untagged enum ResponseStreamResult"
|
||||
//
|
||||
|
||||
let input_file_content = None;
|
||||
let expected_output_content = String::new();
|
||||
eval(
|
||||
@@ -1475,19 +1478,16 @@ impl EditAgentTest {
|
||||
language::init(cx);
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store, client.clone(), cx);
|
||||
crate::init(client.http_client(), cx);
|
||||
});
|
||||
|
||||
fs.insert_tree("/root", json!({})).await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let agent_model = SelectedModel::from_str(
|
||||
&std::env::var("ZED_AGENT_MODEL")
|
||||
.unwrap_or("anthropic/claude-3-7-sonnet-latest".into()),
|
||||
&std::env::var("ZED_AGENT_MODEL").unwrap_or("anthropic/claude-4-sonnet-latest".into()),
|
||||
)
|
||||
.unwrap();
|
||||
let judge_model = SelectedModel::from_str(
|
||||
&std::env::var("ZED_JUDGE_MODEL")
|
||||
.unwrap_or("anthropic/claude-3-7-sonnet-latest".into()),
|
||||
&std::env::var("ZED_JUDGE_MODEL").unwrap_or("anthropic/claude-4-sonnet-latest".into()),
|
||||
)
|
||||
.unwrap();
|
||||
let (agent_model, judge_model) = cx
|
||||
@@ -1553,39 +1553,27 @@ impl EditAgentTest {
|
||||
.update(cx, |project, cx| project.open_buffer(path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let tools = cx.update(|cx| {
|
||||
ToolRegistry::default_global(cx)
|
||||
.tools()
|
||||
.into_iter()
|
||||
.filter_map(|tool| {
|
||||
let input_schema = tool
|
||||
.input_schema(self.agent.model.tool_input_format())
|
||||
.ok()?;
|
||||
Some(LanguageModelRequestTool {
|
||||
name: tool.name(),
|
||||
description: tool.description(),
|
||||
input_schema,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
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 {
|
||||
|
||||
let tools = crate::built_in_tools().collect::<Vec<_>>();
|
||||
|
||||
let system_prompt = {
|
||||
let worktrees = vec![WorktreeContext {
|
||||
root_name: "root".to_string(),
|
||||
abs_path: Path::new("/path/to/root").into(),
|
||||
rules_file: None,
|
||||
}];
|
||||
let project_context = ProjectContext::new(worktrees, Vec::default());
|
||||
let tool_names = tools
|
||||
.iter()
|
||||
.map(|tool| tool.name.clone().into())
|
||||
.collect::<Vec<_>>();
|
||||
let template = crate::SystemPromptTemplate {
|
||||
project: &project_context,
|
||||
available_tools: tool_names,
|
||||
},
|
||||
)?;
|
||||
};
|
||||
let templates = Templates::new();
|
||||
template.render(&templates).unwrap()
|
||||
};
|
||||
|
||||
let has_system_prompt = eval
|
||||
.conversation
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{DbThreadMetadata, ThreadsDatabase};
|
||||
use crate::{DbThread, DbThreadMetadata, ThreadsDatabase};
|
||||
use acp_thread::MentionUri;
|
||||
use agent_client_protocol as acp;
|
||||
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 itertools::Itertools;
|
||||
use paths::contexts_dir;
|
||||
use project::Project;
|
||||
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 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");
|
||||
|
||||
//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)]
|
||||
pub enum HistoryEntry {
|
||||
AcpThread(DbThreadMetadata),
|
||||
@@ -55,8 +83,13 @@ impl HistoryEntry {
|
||||
|
||||
pub fn title(&self) -> &SharedString {
|
||||
match self {
|
||||
HistoryEntry::AcpThread(thread) if thread.title.is_empty() => DEFAULT_TITLE,
|
||||
HistoryEntry::AcpThread(thread) => &thread.title,
|
||||
HistoryEntry::AcpThread(thread) => {
|
||||
if thread.title.is_empty() {
|
||||
DEFAULT_TITLE
|
||||
} else {
|
||||
&thread.title
|
||||
}
|
||||
}
|
||||
HistoryEntry::TextThread(context) => &context.title,
|
||||
}
|
||||
}
|
||||
@@ -87,7 +120,7 @@ enum SerializedRecentOpen {
|
||||
pub struct HistoryStore {
|
||||
threads: Vec<DbThreadMetadata>,
|
||||
entries: Vec<HistoryEntry>,
|
||||
context_store: Entity<assistant_context::ContextStore>,
|
||||
text_thread_store: Entity<assistant_context::ContextStore>,
|
||||
recently_opened_entries: VecDeque<HistoryEntryId>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
_save_recently_opened_entries_task: Task<()>,
|
||||
@@ -95,10 +128,11 @@ pub struct HistoryStore {
|
||||
|
||||
impl HistoryStore {
|
||||
pub fn new(
|
||||
context_store: Entity<assistant_context::ContextStore>,
|
||||
text_thread_store: Entity<assistant_context::ContextStore>,
|
||||
cx: &mut Context<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| {
|
||||
let entries = Self::load_recently_opened_entries(cx).await;
|
||||
@@ -114,7 +148,7 @@ impl HistoryStore {
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
context_store,
|
||||
text_thread_store,
|
||||
recently_opened_entries: VecDeque::default(),
|
||||
threads: Vec::default(),
|
||||
entries: Vec::default(),
|
||||
@@ -127,6 +161,18 @@ impl HistoryStore {
|
||||
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(
|
||||
&mut self,
|
||||
id: acp::SessionId,
|
||||
@@ -145,9 +191,8 @@ impl HistoryStore {
|
||||
path: Arc<Path>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.context_store.update(cx, |context_store, cx| {
|
||||
context_store.delete_local_context(path, cx)
|
||||
})
|
||||
self.text_thread_store
|
||||
.update(cx, |store, cx| store.delete_local_context(path, cx))
|
||||
}
|
||||
|
||||
pub fn load_text_thread(
|
||||
@@ -155,9 +200,8 @@ impl HistoryStore {
|
||||
path: Arc<Path>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Entity<AssistantContext>>> {
|
||||
self.context_store.update(cx, |context_store, cx| {
|
||||
context_store.open_local_context(path, cx)
|
||||
})
|
||||
self.text_thread_store
|
||||
.update(cx, |store, cx| store.open_local_context(path, cx))
|
||||
}
|
||||
|
||||
pub fn reload(&self, cx: &mut Context<Self>) {
|
||||
@@ -197,7 +241,7 @@ impl HistoryStore {
|
||||
let mut history_entries = Vec::new();
|
||||
history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread));
|
||||
history_entries.extend(
|
||||
self.context_store
|
||||
self.text_thread_store
|
||||
.read(cx)
|
||||
.unordered_contexts()
|
||||
.cloned()
|
||||
@@ -231,21 +275,21 @@ impl HistoryStore {
|
||||
})
|
||||
});
|
||||
|
||||
let context_entries =
|
||||
self.context_store
|
||||
.read(cx)
|
||||
.unordered_contexts()
|
||||
.flat_map(|context| {
|
||||
self.recently_opened_entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(index, entry)| match entry {
|
||||
HistoryEntryId::TextThread(path) if &context.path == path => {
|
||||
Some((index, HistoryEntry::TextThread(context.clone())))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
let context_entries = self
|
||||
.text_thread_store
|
||||
.read(cx)
|
||||
.unordered_contexts()
|
||||
.flat_map(|context| {
|
||||
self.recently_opened_entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(index, entry)| match entry {
|
||||
HistoryEntryId::TextThread(path) if &context.path == path => {
|
||||
Some((index, HistoryEntry::TextThread(context.clone())))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
|
||||
thread_entries
|
||||
.chain(context_entries)
|
||||
402
crates/agent/src/legacy_thread.rs
Normal file
402
crates/agent/src/legacy_thread.rs
Normal 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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
use action_log::ActionLog;
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::Result;
|
||||
use gpui::{AsyncApp, Entity};
|
||||
use language::{Buffer, OutlineItem, ParseStatus};
|
||||
use project::Project;
|
||||
use regex::Regex;
|
||||
use std::fmt::Write;
|
||||
use text::Point;
|
||||
@@ -11,51 +9,66 @@ use text::Point;
|
||||
/// we automatically provide the file's symbol outline instead, with line numbers.
|
||||
pub const AUTO_OUTLINE_SIZE: usize = 16384;
|
||||
|
||||
pub async fn file_outline(
|
||||
project: Entity<Project>,
|
||||
path: String,
|
||||
action_log: Entity<ActionLog>,
|
||||
regex: Option<Regex>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> 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
|
||||
/// 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,
|
||||
}
|
||||
|
||||
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>>,
|
||||
regex: Option<Regex>,
|
||||
offset: usize,
|
||||
@@ -128,62 +141,3 @@ fn render_entries(
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
[The following is an auto-generated notification; do not reply]
|
||||
|
||||
These files have changed since the last read:
|
||||
@@ -975,9 +975,9 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
|
||||
vec![context_server::types::Tool {
|
||||
name: "echo".into(),
|
||||
description: None,
|
||||
input_schema: serde_json::to_value(
|
||||
EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema),
|
||||
)
|
||||
input_schema: serde_json::to_value(EchoTool::input_schema(
|
||||
LanguageModelToolSchemaFormat::JsonSchema,
|
||||
))
|
||||
.unwrap(),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
@@ -1149,9 +1149,9 @@ async fn test_mcp_tool_truncation(cx: &mut TestAppContext) {
|
||||
context_server::types::Tool {
|
||||
name: "echo".into(), // Conflicts with native EchoTool
|
||||
description: None,
|
||||
input_schema: serde_json::to_value(
|
||||
EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema),
|
||||
)
|
||||
input_schema: serde_json::to_value(EchoTool::input_schema(
|
||||
LanguageModelToolSchemaFormat::JsonSchema,
|
||||
))
|
||||
.unwrap(),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
@@ -1174,9 +1174,9 @@ async fn test_mcp_tool_truncation(cx: &mut TestAppContext) {
|
||||
context_server::types::Tool {
|
||||
name: "echo".into(), // Also conflicts with native EchoTool
|
||||
description: None,
|
||||
input_schema: serde_json::to_value(
|
||||
EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema),
|
||||
)
|
||||
input_schema: serde_json::to_value(EchoTool::input_schema(
|
||||
LanguageModelToolSchemaFormat::JsonSchema,
|
||||
))
|
||||
.unwrap(),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
@@ -1864,7 +1864,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
|
||||
let selector_opt = connection.model_selector(&session_id);
|
||||
assert!(
|
||||
selector_opt.is_some(),
|
||||
"agent2 should always support ModelSelector"
|
||||
"agent should always support ModelSelector"
|
||||
);
|
||||
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
@@ -1,7 +1,48 @@
|
||||
use anyhow::Result;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
use schemars::{
|
||||
JsonSchema, Schema,
|
||||
generate::SchemaSettings,
|
||||
transform::{Transform, transform_subschemas},
|
||||
};
|
||||
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.
|
||||
///
|
||||
@@ -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
88
crates/agent/src/tools.rs
Normal 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,
|
||||
}
|
||||
@@ -32,6 +32,17 @@ impl ContextServerRegistry {
|
||||
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(
|
||||
&self,
|
||||
) -> impl Iterator<
|
||||
@@ -154,7 +165,7 @@ impl AnyAgentTool for ContextServerTool {
|
||||
format: language_model::LanguageModelToolSchemaFormat,
|
||||
) -> Result<serde_json::Value> {
|
||||
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 {
|
||||
serde_json::Value::Null => {
|
||||
serde_json::json!({ "type": "object", "properties": [] })
|
||||
@@ -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 agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tools::edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat};
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::HashSet;
|
||||
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):
|
||||
/// - 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 {
|
||||
/// 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,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
struct EditFileToolPartialInput {
|
||||
#[serde(default)]
|
||||
path: String,
|
||||
@@ -123,6 +125,7 @@ pub struct EditFileTool {
|
||||
thread: WeakEntity<Thread>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
project: Entity<Project>,
|
||||
templates: Arc<Templates>,
|
||||
}
|
||||
|
||||
impl EditFileTool {
|
||||
@@ -130,11 +133,13 @@ impl EditFileTool {
|
||||
project: Entity<Project>,
|
||||
thread: WeakEntity<Thread>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
templates: Arc<Templates>,
|
||||
) -> Self {
|
||||
Self {
|
||||
project,
|
||||
thread,
|
||||
language_registry,
|
||||
templates,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,8 +299,7 @@ impl AgentTool for EditFileTool {
|
||||
model,
|
||||
project.clone(),
|
||||
action_log.clone(),
|
||||
// TODO: move edit agent to this crate so we can use our templates
|
||||
assistant_tools::templates::Templates::new(),
|
||||
self.templates.clone(),
|
||||
edit_format,
|
||||
);
|
||||
|
||||
@@ -599,6 +603,7 @@ mod tests {
|
||||
project,
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
Templates::new(),
|
||||
))
|
||||
.run(input, ToolCallEventStream::test().0, cx)
|
||||
})
|
||||
@@ -807,6 +812,7 @@ mod tests {
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry.clone(),
|
||||
Templates::new(),
|
||||
))
|
||||
.run(input, ToolCallEventStream::test().0, cx)
|
||||
});
|
||||
@@ -865,6 +871,7 @@ mod tests {
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
Templates::new(),
|
||||
))
|
||||
.run(input, ToolCallEventStream::test().0, cx)
|
||||
});
|
||||
@@ -951,6 +958,7 @@ mod tests {
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry.clone(),
|
||||
Templates::new(),
|
||||
))
|
||||
.run(input, ToolCallEventStream::test().0, cx)
|
||||
});
|
||||
@@ -1005,6 +1013,7 @@ mod tests {
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
Templates::new(),
|
||||
))
|
||||
.run(input, ToolCallEventStream::test().0, cx)
|
||||
});
|
||||
@@ -1057,6 +1066,7 @@ mod tests {
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
Templates::new(),
|
||||
));
|
||||
fs.insert_tree("/root", json!({})).await;
|
||||
|
||||
@@ -1197,6 +1207,7 @@ mod tests {
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
Templates::new(),
|
||||
));
|
||||
|
||||
// Test global config paths - these should require confirmation if they exist and are outside the project
|
||||
@@ -1309,6 +1320,7 @@ mod tests {
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
Templates::new(),
|
||||
));
|
||||
|
||||
// Test files in different worktrees
|
||||
@@ -1393,6 +1405,7 @@ mod tests {
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
Templates::new(),
|
||||
));
|
||||
|
||||
// Test edge cases
|
||||
@@ -1482,6 +1495,7 @@ mod tests {
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
Templates::new(),
|
||||
));
|
||||
|
||||
// Test different EditFileMode values
|
||||
@@ -1566,6 +1580,7 @@ mod tests {
|
||||
project,
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
Templates::new(),
|
||||
));
|
||||
|
||||
cx.update(|cx| {
|
||||
@@ -1653,6 +1668,7 @@ mod tests {
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
languages.clone(),
|
||||
Templates::new(),
|
||||
));
|
||||
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
|
||||
let edit = cx.update(|cx| {
|
||||
@@ -1682,6 +1698,7 @@ mod tests {
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
languages.clone(),
|
||||
Templates::new(),
|
||||
));
|
||||
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
|
||||
let edit = cx.update(|cx| {
|
||||
@@ -1709,6 +1726,7 @@ mod tests {
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
languages.clone(),
|
||||
Templates::new(),
|
||||
));
|
||||
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
|
||||
let edit = cx.update(|cx| {
|
||||
@@ -1,7 +1,6 @@
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol::{self as acp, ToolCallUpdateFields};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::outline;
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use indoc::formatdoc;
|
||||
use language::Point;
|
||||
@@ -13,7 +12,7 @@ use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use util::markdown::MarkdownCodeBlock;
|
||||
|
||||
use crate::{AgentTool, ToolCallEventStream};
|
||||
use crate::{AgentTool, ToolCallEventStream, outline};
|
||||
|
||||
/// Reads the content of the given file in the project.
|
||||
///
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-GPL
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -15,10 +15,9 @@ use settings::{
|
||||
|
||||
pub use crate::agent_profile::*;
|
||||
|
||||
pub const SUMMARIZE_THREAD_PROMPT: &str =
|
||||
include_str!("../../agent/src/prompts/summarize_thread_prompt.txt");
|
||||
pub const SUMMARIZE_THREAD_PROMPT: &str = include_str!("prompts/summarize_thread_prompt.txt");
|
||||
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) {
|
||||
AgentSettings::register(cx);
|
||||
|
||||
@@ -20,7 +20,6 @@ acp_thread.workspace = true
|
||||
action_log.workspace = true
|
||||
agent-client-protocol.workspace = true
|
||||
agent.workspace = true
|
||||
agent2.workspace = true
|
||||
agent_servers.workspace = true
|
||||
agent_settings.workspace = true
|
||||
ai_onboarding.workspace = true
|
||||
@@ -29,7 +28,6 @@ arrayvec.workspace = true
|
||||
assistant_context.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
assistant_slash_commands.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
audio.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
@@ -71,6 +69,7 @@ postage.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
proto.workspace = true
|
||||
ref-cast.workspace = true
|
||||
release_channel.workspace = true
|
||||
rope.workspace = true
|
||||
rules_library.workspace = true
|
||||
@@ -104,9 +103,7 @@ zed_actions.workspace = true
|
||||
[dev-dependencies]
|
||||
acp_thread = { 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_tools.workspace = true
|
||||
buffer_diff = { workspace = true, features = ["test-support"] }
|
||||
db = { workspace = true, features = ["test-support"] }
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -6,8 +6,8 @@ use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use acp_thread::MentionUri;
|
||||
use agent::{HistoryEntry, HistoryStore};
|
||||
use agent_client_protocol as acp;
|
||||
use agent2::{HistoryEntry, HistoryStore};
|
||||
use anyhow::Result;
|
||||
use editor::{CompletionProvider, Editor, ExcerptId};
|
||||
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::symbol_context_picker::SymbolMatch;
|
||||
use crate::context_picker::symbol_context_picker::search_symbols;
|
||||
use crate::context_picker::thread_context_picker::search_threads;
|
||||
use crate::context_picker::{
|
||||
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(
|
||||
crease_text: SharedString,
|
||||
start: Anchor,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::{cell::RefCell, ops::Range, rc::Rc};
|
||||
|
||||
use acp_thread::{AcpThread, AgentThreadEntry};
|
||||
use agent::HistoryStore;
|
||||
use agent_client_protocol::{self as acp, ToolCallId};
|
||||
use agent2::HistoryStore;
|
||||
use collections::HashMap;
|
||||
use editor::{Editor, EditorMode, MinimapVisibility};
|
||||
use gpui::{
|
||||
@@ -399,9 +399,9 @@ mod tests {
|
||||
use std::{path::Path, rc::Rc};
|
||||
|
||||
use acp_thread::{AgentConnection, StubAgentConnection};
|
||||
use agent::HistoryStore;
|
||||
use agent_client_protocol as acp;
|
||||
use agent_settings::AgentSettings;
|
||||
use agent2::HistoryStore;
|
||||
use assistant_context::ContextStore;
|
||||
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
||||
use editor::{EditorSettings, RowInfo};
|
||||
|
||||
@@ -3,12 +3,11 @@ use crate::{
|
||||
context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content},
|
||||
};
|
||||
use acp_thread::{MentionUri, selection_name};
|
||||
use agent::{HistoryStore, outline};
|
||||
use agent_client_protocol as acp;
|
||||
use agent_servers::{AgentServer, AgentServerDelegate};
|
||||
use agent2::HistoryStore;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_slash_commands::codeblock_fence_for_path;
|
||||
use assistant_tool::outline;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
|
||||
@@ -230,7 +229,7 @@ impl MessageEditor {
|
||||
|
||||
pub fn insert_thread_summary(
|
||||
&mut self,
|
||||
thread: agent2::DbThreadMetadata,
|
||||
thread: agent::DbThreadMetadata,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -599,7 +598,7 @@ impl MessageEditor {
|
||||
id: acp::SessionId,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Mention>> {
|
||||
let server = Rc::new(agent2::NativeAgentServer::new(
|
||||
let server = Rc::new(agent::NativeAgentServer::new(
|
||||
self.project.read(cx).fs().clone(),
|
||||
self.history_store.clone(),
|
||||
));
|
||||
@@ -612,7 +611,7 @@ impl MessageEditor {
|
||||
let connection = server.connect(None, delegate, cx);
|
||||
cx.spawn(async move |_, cx| {
|
||||
let (agent, _) = connection.await?;
|
||||
let agent = agent.downcast::<agent2::NativeAgentConnection>().unwrap();
|
||||
let agent = agent.downcast::<agent::NativeAgentConnection>().unwrap();
|
||||
let summary = agent
|
||||
.0
|
||||
.update(cx, |agent, cx| agent.thread_summary(id, cx))?
|
||||
@@ -629,8 +628,8 @@ impl MessageEditor {
|
||||
path: PathBuf,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Mention>> {
|
||||
let context = self.history_store.update(cx, |text_thread_store, cx| {
|
||||
text_thread_store.load_text_thread(path.as_path().into(), cx)
|
||||
let context = self.history_store.update(cx, |store, cx| {
|
||||
store.load_text_thread(path.as_path().into(), cx)
|
||||
});
|
||||
cx.spawn(async move |_, cx| {
|
||||
let context = context.await?;
|
||||
@@ -1589,10 +1588,9 @@ mod tests {
|
||||
use std::{cell::RefCell, ops::Range, path::Path, rc::Rc, sync::Arc};
|
||||
|
||||
use acp_thread::MentionUri;
|
||||
use agent::{HistoryStore, outline};
|
||||
use agent_client_protocol as acp;
|
||||
use agent2::HistoryStore;
|
||||
use assistant_context::ContextStore;
|
||||
use assistant_tool::outline;
|
||||
use editor::{AnchorRangeExt as _, Editor, EditorMode};
|
||||
use fs::FakeFs;
|
||||
use futures::StreamExt as _;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::acp::AcpThreadView;
|
||||
use crate::{AgentPanel, RemoveSelectedThread};
|
||||
use agent2::{HistoryEntry, HistoryStore};
|
||||
use agent::{HistoryEntry, HistoryStore};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
@@ -23,11 +23,8 @@ pub struct AcpThreadHistory {
|
||||
hovered_index: Option<usize>,
|
||||
search_editor: Entity<Editor>,
|
||||
search_query: SharedString,
|
||||
|
||||
visible_items: Vec<ListItemType>,
|
||||
|
||||
local_timezone: UtcOffset,
|
||||
|
||||
_update_task: Task<()>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
}
|
||||
@@ -62,7 +59,7 @@ impl EventEmitter<ThreadHistoryEvent> for AcpThreadHistory {}
|
||||
|
||||
impl AcpThreadHistory {
|
||||
pub(crate) fn new(
|
||||
history_store: Entity<agent2::HistoryStore>,
|
||||
history_store: Entity<agent::HistoryStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -642,7 +639,7 @@ impl RenderOnce for AcpHistoryEntryElement {
|
||||
if let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel
|
||||
.open_saved_prompt_editor(
|
||||
.open_saved_text_thread(
|
||||
context.path.clone(),
|
||||
window,
|
||||
cx,
|
||||
|
||||
@@ -5,10 +5,10 @@ use acp_thread::{
|
||||
};
|
||||
use acp_thread::{AgentConnection, Plan};
|
||||
use action_log::ActionLog;
|
||||
use agent::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer};
|
||||
use agent_client_protocol::{self as acp, PromptCapabilities};
|
||||
use agent_servers::{AgentServer, AgentServerDelegate};
|
||||
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode};
|
||||
use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer};
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use arrayvec::ArrayVec;
|
||||
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 {
|
||||
self.read(cx).profile().clone()
|
||||
}
|
||||
@@ -529,7 +529,7 @@ impl AcpThreadView {
|
||||
|
||||
let result = if let Some(native_agent) = connection
|
||||
.clone()
|
||||
.downcast::<agent2::NativeAgentConnection>()
|
||||
.downcast::<agent::NativeAgentConnection>()
|
||||
&& let Some(resume) = resume_thread.clone()
|
||||
{
|
||||
cx.update(|_, cx| {
|
||||
@@ -3106,7 +3106,7 @@ impl AcpThreadView {
|
||||
let render_history = self
|
||||
.agent
|
||||
.clone()
|
||||
.downcast::<agent2::NativeAgentServer>()
|
||||
.downcast::<agent::NativeAgentServer>()
|
||||
.is_some()
|
||||
&& self
|
||||
.history_store
|
||||
@@ -4011,12 +4011,12 @@ impl AcpThreadView {
|
||||
pub(crate) fn as_native_connection(
|
||||
&self,
|
||||
cx: &App,
|
||||
) -> Option<Rc<agent2::NativeAgentConnection>> {
|
||||
) -> Option<Rc<agent::NativeAgentConnection>> {
|
||||
let acp_thread = self.thread()?.read(cx);
|
||||
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);
|
||||
self.as_native_connection(cx)?
|
||||
.thread(acp_thread.session_id(), cx)
|
||||
@@ -4404,7 +4404,7 @@ impl AcpThreadView {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -5137,7 +5137,7 @@ impl AcpThreadView {
|
||||
if self
|
||||
.agent
|
||||
.clone()
|
||||
.downcast::<agent2::NativeAgentServer>()
|
||||
.downcast::<agent::NativeAgentServer>()
|
||||
.is_some()
|
||||
{
|
||||
// Native agent - use the model name
|
||||
|
||||
@@ -6,8 +6,8 @@ mod tool_picker;
|
||||
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use agent::ContextServerRegistry;
|
||||
use anyhow::Result;
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use cloud_llm_client::{Plan, PlanV1, PlanV2};
|
||||
use collections::HashMap;
|
||||
use context_server::ContextServerId;
|
||||
@@ -17,7 +17,7 @@ use extension_host::ExtensionStore;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
Hsla, ScrollHandle, Subscription, Task, WeakEntity,
|
||||
ScrollHandle, Subscription, Task, WeakEntity,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
@@ -54,9 +54,8 @@ pub struct AgentConfiguration {
|
||||
focus_handle: FocusHandle,
|
||||
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
|
||||
context_server_store: Entity<ContextServerStore>,
|
||||
expanded_context_server_tools: HashMap<ContextServerId, bool>,
|
||||
expanded_provider_configurations: HashMap<LanguageModelProviderId, bool>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
context_server_registry: Entity<ContextServerRegistry>,
|
||||
_registry_subscription: Subscription,
|
||||
scroll_handle: ScrollHandle,
|
||||
_check_for_gemini: Task<()>,
|
||||
@@ -67,7 +66,7 @@ impl AgentConfiguration {
|
||||
fs: Arc<dyn Fs>,
|
||||
agent_server_store: Entity<AgentServerStore>,
|
||||
context_server_store: Entity<ContextServerStore>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
context_server_registry: Entity<ContextServerRegistry>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
@@ -103,9 +102,8 @@ impl AgentConfiguration {
|
||||
configuration_views_by_provider: HashMap::default(),
|
||||
agent_server_store,
|
||||
context_server_store,
|
||||
expanded_context_server_tools: HashMap::default(),
|
||||
expanded_provider_configurations: HashMap::default(),
|
||||
tools,
|
||||
context_server_registry,
|
||||
_registry_subscription: registry_subscription,
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
_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(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
@@ -567,7 +561,6 @@ impl AgentConfiguration {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl use<> + IntoElement {
|
||||
let tools_by_source = self.tools.read(cx).tools_by_source(cx);
|
||||
let server_status = self
|
||||
.context_server_store
|
||||
.read(cx)
|
||||
@@ -596,17 +589,11 @@ impl AgentConfiguration {
|
||||
None
|
||||
};
|
||||
|
||||
let are_tools_expanded = self
|
||||
.expanded_context_server_tools
|
||||
.get(&context_server_id)
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
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 tool_count = self
|
||||
.context_server_registry
|
||||
.read(cx)
|
||||
.tools_for_server(&context_server_id)
|
||||
.count();
|
||||
|
||||
let (source_icon, source_tooltip) = if is_from_extension {
|
||||
(
|
||||
@@ -660,7 +647,7 @@ impl AgentConfiguration {
|
||||
let language_registry = self.language_registry.clone();
|
||||
let context_server_store = self.context_server_store.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
let tools = self.tools.clone();
|
||||
let context_server_registry = self.context_server_registry.clone();
|
||||
|
||||
move |window, cx| {
|
||||
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
|
||||
@@ -678,20 +665,16 @@ impl AgentConfiguration {
|
||||
)
|
||||
.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 tools = tools.clone();
|
||||
let context_server_registry = context_server_registry.clone();
|
||||
let workspace = workspace.clone();
|
||||
|
||||
move |window, cx| {
|
||||
let context_server_id = context_server_id.clone();
|
||||
let tools = tools.clone();
|
||||
let workspace = workspace.clone();
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
ConfigureContextServerToolsModal::toggle(
|
||||
context_server_id,
|
||||
tools,
|
||||
context_server_registry.clone(),
|
||||
workspace,
|
||||
window,
|
||||
cx,
|
||||
@@ -773,14 +756,6 @@ impl AgentConfiguration {
|
||||
.child(
|
||||
h_flex()
|
||||
.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(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
@@ -904,11 +879,6 @@ impl AgentConfiguration {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if !are_tools_expanded || tools.is_empty() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
parent
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use agent::ContextServerRegistry;
|
||||
use collections::HashMap;
|
||||
use context_server::ContextServerId;
|
||||
use gpui::{
|
||||
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle, Window, prelude::*,
|
||||
@@ -8,37 +9,37 @@ use workspace::{ModalView, Workspace};
|
||||
|
||||
pub struct ConfigureContextServerToolsModal {
|
||||
context_server_id: ContextServerId,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
context_server_registry: Entity<ContextServerRegistry>,
|
||||
focus_handle: FocusHandle,
|
||||
expanded_tools: std::collections::HashMap<String, bool>,
|
||||
expanded_tools: HashMap<SharedString, bool>,
|
||||
scroll_handle: ScrollHandle,
|
||||
}
|
||||
|
||||
impl ConfigureContextServerToolsModal {
|
||||
fn new(
|
||||
context_server_id: ContextServerId,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
context_server_registry: Entity<ContextServerRegistry>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
context_server_id,
|
||||
tools,
|
||||
context_server_registry,
|
||||
focus_handle: cx.focus_handle(),
|
||||
expanded_tools: std::collections::HashMap::new(),
|
||||
expanded_tools: HashMap::default(),
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(
|
||||
context_server_id: ContextServerId,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
context_server_registry: Entity<ContextServerRegistry>,
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
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,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let tools_by_source = self.tools.read(cx).tools_by_source(cx);
|
||||
let server_tools = tools_by_source
|
||||
.get(&ToolSource::ContextServer {
|
||||
id: self.context_server_id.0.clone().into(),
|
||||
})
|
||||
.map(|tools| tools.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
let tools = self
|
||||
.context_server_registry
|
||||
.read(cx)
|
||||
.tools_for_server(&self.context_server_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
@@ -70,11 +69,11 @@ impl ConfigureContextServerToolsModal {
|
||||
.max_h_128()
|
||||
.overflow_y_scroll()
|
||||
.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 is_expanded = self
|
||||
.expanded_tools
|
||||
.get(&tool_name)
|
||||
.get(tool_name.as_ref())
|
||||
.copied()
|
||||
.unwrap_or(false);
|
||||
|
||||
@@ -110,7 +109,7 @@ impl ConfigureContextServerToolsModal {
|
||||
move |this, _event, _window, _cx| {
|
||||
let current = this
|
||||
.expanded_tools
|
||||
.get(&tool_name)
|
||||
.get(tool_name.as_ref())
|
||||
.copied()
|
||||
.unwrap_or(false);
|
||||
this.expanded_tools
|
||||
@@ -127,7 +126,7 @@ impl ConfigureContextServerToolsModal {
|
||||
.into_any_element(),
|
||||
];
|
||||
|
||||
if index < server_tools.len() - 1 {
|
||||
if index < tools.len() - 1 {
|
||||
items.push(
|
||||
h_flex()
|
||||
.w_full()
|
||||
|
||||
@@ -2,8 +2,8 @@ mod profile_modal_header;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent::ContextServerRegistry;
|
||||
use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, builtin_profiles};
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use editor::Editor;
|
||||
use fs::Fs;
|
||||
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::{AgentPanel, ManageProfiles};
|
||||
|
||||
use super::tool_picker::ToolPickerMode;
|
||||
|
||||
enum Mode {
|
||||
ChooseProfile(ChooseProfileMode),
|
||||
NewProfile(NewProfileMode),
|
||||
@@ -97,7 +95,7 @@ pub struct NewProfileMode {
|
||||
|
||||
pub struct ManageProfilesModal {
|
||||
fs: Arc<dyn Fs>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
context_server_registry: Entity<ContextServerRegistry>,
|
||||
focus_handle: FocusHandle,
|
||||
mode: Mode,
|
||||
}
|
||||
@@ -111,10 +109,9 @@ impl ManageProfilesModal {
|
||||
workspace.register_action(|workspace, action: &ManageProfiles, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let thread_store = panel.read(cx).thread_store();
|
||||
let tools = thread_store.read(cx).tools();
|
||||
let context_server_registry = panel.read(cx).context_server_registry().clone();
|
||||
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() {
|
||||
this.configure_builtin_tools(profile_id, window, cx);
|
||||
@@ -128,7 +125,7 @@ impl ManageProfilesModal {
|
||||
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
context_server_registry: Entity<ContextServerRegistry>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -136,7 +133,7 @@ impl ManageProfilesModal {
|
||||
|
||||
Self {
|
||||
fs,
|
||||
tools,
|
||||
context_server_registry,
|
||||
focus_handle,
|
||||
mode: Mode::choose_profile(window, cx),
|
||||
}
|
||||
@@ -193,10 +190,9 @@ impl ManageProfilesModal {
|
||||
};
|
||||
|
||||
let tool_picker = cx.new(|cx| {
|
||||
let delegate = ToolPickerDelegate::new(
|
||||
ToolPickerMode::McpTools,
|
||||
let delegate = ToolPickerDelegate::mcp_tools(
|
||||
&self.context_server_registry,
|
||||
self.fs.clone(),
|
||||
self.tools.clone(),
|
||||
profile_id.clone(),
|
||||
profile,
|
||||
cx,
|
||||
@@ -230,10 +226,12 @@ impl ManageProfilesModal {
|
||||
};
|
||||
|
||||
let tool_picker = cx.new(|cx| {
|
||||
let delegate = ToolPickerDelegate::new(
|
||||
ToolPickerMode::BuiltinTools,
|
||||
let delegate = ToolPickerDelegate::builtin_tools(
|
||||
//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.tools.clone(),
|
||||
profile_id.clone(),
|
||||
profile,
|
||||
cx,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use agent::ContextServerRegistry;
|
||||
use agent_settings::{AgentProfileId, AgentProfileSettings};
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use fs::Fs;
|
||||
use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
@@ -14,7 +14,7 @@ pub struct ToolPicker {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ToolPickerMode {
|
||||
enum ToolPickerMode {
|
||||
BuiltinTools,
|
||||
McpTools,
|
||||
}
|
||||
@@ -76,60 +76,80 @@ pub struct ToolPickerDelegate {
|
||||
}
|
||||
|
||||
impl ToolPickerDelegate {
|
||||
pub fn new(
|
||||
mode: ToolPickerMode,
|
||||
pub fn builtin_tools(
|
||||
tool_names: Vec<Arc<str>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
tool_set: Entity<ToolWorkingSet>,
|
||||
profile_id: AgentProfileId,
|
||||
profile_settings: AgentProfileSettings,
|
||||
cx: &mut Context<ToolPicker>,
|
||||
) -> 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 {
|
||||
tool_picker: cx.entity().downgrade(),
|
||||
mode,
|
||||
fs,
|
||||
items,
|
||||
profile_id,
|
||||
profile_settings,
|
||||
filtered_items: Vec::new(),
|
||||
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 {
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use acp_thread::AcpThread;
|
||||
use agent2::{DbThreadMetadata, HistoryEntry};
|
||||
use agent::{ContextServerRegistry, DbThreadMetadata, HistoryEntry, HistoryStore};
|
||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||
use project::agent_server_store::{
|
||||
AgentServerCommand, AllAgentServersSettings, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME,
|
||||
@@ -17,6 +17,7 @@ use zed_actions::OpenBrowser;
|
||||
use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
|
||||
|
||||
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
|
||||
use crate::{
|
||||
AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, Follow, InlineAssistant,
|
||||
@@ -32,16 +33,11 @@ use crate::{
|
||||
use crate::{
|
||||
ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary, placeholder_command,
|
||||
};
|
||||
use agent::{
|
||||
context_store::ContextStore,
|
||||
thread_store::{TextThreadStore, ThreadStore},
|
||||
};
|
||||
use agent_settings::AgentSettings;
|
||||
use ai_onboarding::AgentPanelOnboarding;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_context::{AssistantContext, ContextEvent, ContextSummary};
|
||||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use client::{UserStore, zed_urls};
|
||||
use cloud_llm_client::{Plan, PlanV1, PlanV2, UsageLimit};
|
||||
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
||||
@@ -118,7 +114,7 @@ pub fn init(cx: &mut App) {
|
||||
.register_action(|workspace, _: &NewTextThread, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(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| {
|
||||
@@ -281,7 +277,7 @@ impl ActiveView {
|
||||
pub fn native_agent(
|
||||
fs: Arc<dyn Fs>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
acp_history_store: Entity<agent2::HistoryStore>,
|
||||
history_store: Entity<agent::HistoryStore>,
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
@@ -289,12 +285,12 @@ impl ActiveView {
|
||||
) -> Self {
|
||||
let thread_view = cx.new(|cx| {
|
||||
crate::acp::AcpThreadView::new(
|
||||
ExternalAgent::NativeAgent.server(fs, acp_history_store.clone()),
|
||||
ExternalAgent::NativeAgent.server(fs, history_store.clone()),
|
||||
None,
|
||||
None,
|
||||
workspace,
|
||||
project,
|
||||
acp_history_store,
|
||||
history_store,
|
||||
prompt_store,
|
||||
window,
|
||||
cx,
|
||||
@@ -304,9 +300,9 @@ impl ActiveView {
|
||||
Self::ExternalAgentThread { thread_view }
|
||||
}
|
||||
|
||||
pub fn prompt_editor(
|
||||
pub fn text_thread(
|
||||
context_editor: Entity<TextThreadEditor>,
|
||||
acp_history_store: Entity<agent2::HistoryStore>,
|
||||
acp_history_store: Entity<agent::HistoryStore>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -379,7 +375,7 @@ impl ActiveView {
|
||||
.replace_recently_opened_text_thread(old_path, new_path, cx);
|
||||
} else {
|
||||
history_store.push_recently_opened_entry(
|
||||
agent2::HistoryEntryId::TextThread(new_path.clone()),
|
||||
agent::HistoryEntryId::TextThread(new_path.clone()),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -412,11 +408,11 @@ pub struct AgentPanel {
|
||||
project: Entity<Project>,
|
||||
fs: Arc<dyn Fs>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
acp_history: Entity<AcpThreadHistory>,
|
||||
history_store: Entity<agent2::HistoryStore>,
|
||||
context_store: Entity<TextThreadStore>,
|
||||
history_store: Entity<agent::HistoryStore>,
|
||||
text_thread_store: Entity<assistant_context::ContextStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
context_server_registry: Entity<ContextServerRegistry>,
|
||||
inline_assist_context_store: Entity<ContextStore>,
|
||||
configuration: Option<Entity<AgentConfiguration>>,
|
||||
configuration_subscription: Option<Subscription>,
|
||||
@@ -424,8 +420,8 @@ pub struct AgentPanel {
|
||||
previous_view: Option<ActiveView>,
|
||||
new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
assistant_navigation_menu: Option<Entity<ContextMenu>>,
|
||||
agent_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
agent_navigation_menu: Option<Entity<ContextMenu>>,
|
||||
width: Option<Pixels>,
|
||||
height: Option<Pixels>,
|
||||
zoomed: bool,
|
||||
@@ -463,33 +459,6 @@ impl AgentPanel {
|
||||
Ok(prompt_store) => prompt_store.await.ok(),
|
||||
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
|
||||
.background_spawn(async move { KEY_VALUE_STORE.read_kvp(AGENT_PANEL_KEY) })
|
||||
.await
|
||||
@@ -501,17 +470,22 @@ impl AgentPanel {
|
||||
None
|
||||
};
|
||||
|
||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||
let panel = cx.new(|cx| {
|
||||
Self::new(
|
||||
workspace,
|
||||
thread_store,
|
||||
context_store,
|
||||
prompt_store,
|
||||
window,
|
||||
let slash_commands = Arc::new(SlashCommandWorkingSet::default());
|
||||
let text_thread_store = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
assistant_context::ContextStore::new(
|
||||
project,
|
||||
prompt_builder,
|
||||
slash_commands,
|
||||
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;
|
||||
if let Some(serialized_panel) = serialized_panel {
|
||||
@@ -538,8 +512,7 @@ impl AgentPanel {
|
||||
|
||||
fn new(
|
||||
workspace: &Workspace,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
context_store: Entity<TextThreadStore>,
|
||||
text_thread_store: Entity<assistant_context::ContextStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -551,10 +524,11 @@ impl AgentPanel {
|
||||
let client = workspace.client().clone();
|
||||
let workspace = workspace.weak_handle();
|
||||
|
||||
let inline_assist_context_store =
|
||||
cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
|
||||
let inline_assist_context_store = cx.new(|_cx| ContextStore::new(project.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));
|
||||
cx.subscribe_in(
|
||||
&acp_history,
|
||||
@@ -570,7 +544,7 @@ impl AgentPanel {
|
||||
);
|
||||
}
|
||||
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);
|
||||
}
|
||||
},
|
||||
@@ -589,8 +563,7 @@ impl AgentPanel {
|
||||
cx,
|
||||
),
|
||||
DefaultView::TextThread => {
|
||||
let context =
|
||||
context_store.update(cx, |context_store, cx| context_store.create(cx));
|
||||
let context = text_thread_store.update(cx, |store, cx| store.create(cx));
|
||||
let lsp_adapter_delegate = make_lsp_adapter_delegate(&project.clone(), cx).unwrap();
|
||||
let context_editor = cx.new(|cx| {
|
||||
let mut editor = TextThreadEditor::for_context(
|
||||
@@ -605,7 +578,7 @@ impl AgentPanel {
|
||||
editor.insert_default_prompt(window, cx);
|
||||
editor
|
||||
});
|
||||
ActiveView::prompt_editor(
|
||||
ActiveView::text_thread(
|
||||
context_editor,
|
||||
history_store.clone(),
|
||||
language_registry.clone(),
|
||||
@@ -619,7 +592,7 @@ impl AgentPanel {
|
||||
|
||||
window.defer(cx, move |window, cx| {
|
||||
let panel = weak_panel.clone();
|
||||
let assistant_navigation_menu =
|
||||
let agent_navigation_menu =
|
||||
ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
|
||||
if let Some(panel) = panel.upgrade() {
|
||||
menu = Self::populate_recently_opened_menu_section(menu, panel, cx);
|
||||
@@ -633,7 +606,7 @@ impl AgentPanel {
|
||||
weak_panel
|
||||
.update(cx, |panel, cx| {
|
||||
cx.subscribe_in(
|
||||
&assistant_navigation_menu,
|
||||
&agent_navigation_menu,
|
||||
window,
|
||||
|_, menu, _: &DismissEvent, window, cx| {
|
||||
menu.update(cx, |menu, _| {
|
||||
@@ -643,7 +616,7 @@ impl AgentPanel {
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
panel.assistant_navigation_menu = Some(assistant_navigation_menu);
|
||||
panel.agent_navigation_menu = Some(agent_navigation_menu);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
@@ -666,17 +639,17 @@ impl AgentPanel {
|
||||
project: project.clone(),
|
||||
fs: fs.clone(),
|
||||
language_registry,
|
||||
thread_store: thread_store.clone(),
|
||||
context_store,
|
||||
text_thread_store,
|
||||
prompt_store,
|
||||
configuration: None,
|
||||
configuration_subscription: None,
|
||||
context_server_registry,
|
||||
inline_assist_context_store,
|
||||
previous_view: None,
|
||||
new_thread_menu_handle: PopoverMenuHandle::default(),
|
||||
agent_panel_menu_handle: PopoverMenuHandle::default(),
|
||||
assistant_navigation_menu_handle: PopoverMenuHandle::default(),
|
||||
assistant_navigation_menu: None,
|
||||
agent_navigation_menu_handle: PopoverMenuHandle::default(),
|
||||
agent_navigation_menu: None,
|
||||
width: None,
|
||||
height: None,
|
||||
zoomed: false,
|
||||
@@ -711,12 +684,12 @@ impl AgentPanel {
|
||||
&self.inline_assist_context_store
|
||||
}
|
||||
|
||||
pub(crate) fn thread_store(&self) -> &Entity<ThreadStore> {
|
||||
&self.thread_store
|
||||
pub(crate) fn thread_store(&self) -> &Entity<HistoryStore> {
|
||||
&self.history_store
|
||||
}
|
||||
|
||||
pub(crate) fn text_thread_store(&self) -> &Entity<TextThreadStore> {
|
||||
&self.context_store
|
||||
pub(crate) fn context_server_registry(&self) -> &Entity<ContextServerRegistry> {
|
||||
&self.context_server_registry
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
let context = self
|
||||
.context_store
|
||||
.text_thread_store
|
||||
.update(cx, |context_store, cx| context_store.create(cx));
|
||||
let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
|
||||
.log_err()
|
||||
@@ -783,7 +756,7 @@ impl AgentPanel {
|
||||
}
|
||||
|
||||
self.set_active_view(
|
||||
ActiveView::prompt_editor(
|
||||
ActiveView::text_thread(
|
||||
context_editor.clone(),
|
||||
self.history_store.clone(),
|
||||
self.language_registry.clone(),
|
||||
@@ -921,32 +894,29 @@ impl AgentPanel {
|
||||
self.set_active_view(previous_view, window, cx);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn open_saved_prompt_editor(
|
||||
pub(crate) fn open_saved_text_thread(
|
||||
&mut self,
|
||||
path: Arc<Path>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let context = self
|
||||
.context_store
|
||||
.update(cx, |store, cx| store.open_local_context(path, cx));
|
||||
.history_store
|
||||
.update(cx, |store, cx| store.load_text_thread(path, cx));
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let context = context.await?;
|
||||
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,
|
||||
context: Entity<AssistantContext>,
|
||||
window: &mut Window,
|
||||
@@ -973,7 +943,7 @@ impl AgentPanel {
|
||||
}
|
||||
|
||||
self.set_active_view(
|
||||
ActiveView::prompt_editor(
|
||||
ActiveView::text_thread(
|
||||
editor,
|
||||
self.history_store.clone(),
|
||||
self.language_registry.clone(),
|
||||
@@ -1013,7 +983,7 @@ impl AgentPanel {
|
||||
window: &mut Window,
|
||||
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(
|
||||
@@ -1106,7 +1076,6 @@ impl AgentPanel {
|
||||
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 context_server_store = self.project.read(cx).context_server_store();
|
||||
let tools = self.thread_store.read(cx).tools();
|
||||
let fs = self.fs.clone();
|
||||
|
||||
self.set_active_view(ActiveView::Configuration, window, cx);
|
||||
@@ -1115,7 +1084,7 @@ impl AgentPanel {
|
||||
fs,
|
||||
agent_server_store,
|
||||
context_server_store,
|
||||
tools,
|
||||
self.context_server_registry.clone(),
|
||||
self.language_registry.clone(),
|
||||
self.workspace.clone(),
|
||||
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
|
||||
.active_native_agent_thread(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 {
|
||||
ActiveView::ExternalAgentThread { thread_view, .. } => {
|
||||
thread_view.read(cx).as_native_thread(cx)
|
||||
@@ -1241,7 +1210,7 @@ impl AgentPanel {
|
||||
self.history_store.update(cx, |store, cx| {
|
||||
if let Some(path) = context_editor.read(cx).context().read(cx).path() {
|
||||
store.push_recently_opened_entry(
|
||||
agent2::HistoryEntryId::TextThread(path.clone()),
|
||||
agent::HistoryEntryId::TextThread(path.clone()),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
@@ -1295,15 +1264,15 @@ impl AgentPanel {
|
||||
let entry = entry.clone();
|
||||
panel
|
||||
.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(entry.clone()),
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
agent2::HistoryEntry::TextThread(entry) => this
|
||||
.open_saved_prompt_editor(entry.path.clone(), window, cx)
|
||||
agent::HistoryEntry::TextThread(entry) => this
|
||||
.open_saved_text_thread(entry.path.clone(), window, cx)
|
||||
.detach_and_log_err(cx),
|
||||
})
|
||||
.ok();
|
||||
@@ -1730,9 +1699,9 @@ impl AgentPanel {
|
||||
},
|
||||
)
|
||||
.anchor(corner)
|
||||
.with_handle(self.assistant_navigation_menu_handle.clone())
|
||||
.with_handle(self.agent_navigation_menu_handle.clone())
|
||||
.menu({
|
||||
let menu = self.assistant_navigation_menu.clone();
|
||||
let menu = self.agent_navigation_menu.clone();
|
||||
move |window, cx| {
|
||||
telemetry::event!("View Thread History Clicked");
|
||||
|
||||
@@ -1832,7 +1801,7 @@ impl AgentPanel {
|
||||
})
|
||||
.item(
|
||||
ContextMenuEntry::new("New Thread")
|
||||
.action(NewThread::default().boxed_clone())
|
||||
.action(NewThread.boxed_clone())
|
||||
.icon(IconName::Thread)
|
||||
.icon_color(Color::Muted)
|
||||
.handler({
|
||||
@@ -2278,7 +2247,7 @@ impl AgentPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_prompt_editor(
|
||||
fn render_text_thread(
|
||||
&self,
|
||||
context_editor: &Entity<TextThreadEditor>,
|
||||
buffer_search_bar: &Entity<BufferSearchBar>,
|
||||
@@ -2409,8 +2378,8 @@ impl AgentPanel {
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
key_context.add("AgentPanel");
|
||||
match &self.active_view {
|
||||
ActiveView::ExternalAgentThread { .. } => key_context.add("external_agent_thread"),
|
||||
ActiveView::TextThread { .. } => key_context.add("prompt_editor"),
|
||||
ActiveView::ExternalAgentThread { .. } => key_context.add("acp_thread"),
|
||||
ActiveView::TextThread { .. } => key_context.add("text_thread"),
|
||||
ActiveView::History | ActiveView::Configuration => {}
|
||||
}
|
||||
key_context
|
||||
@@ -2487,7 +2456,7 @@ impl Render for AgentPanel {
|
||||
this
|
||||
}
|
||||
})
|
||||
.child(self.render_prompt_editor(
|
||||
.child(self.render_text_thread(
|
||||
context_editor,
|
||||
buffer_search_bar,
|
||||
window,
|
||||
@@ -2538,8 +2507,7 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
|
||||
};
|
||||
let prompt_store = None;
|
||||
let thread_store = None;
|
||||
let text_thread_store = None;
|
||||
let context_store = cx.new(|_| ContextStore::new(project.clone(), None));
|
||||
let context_store = cx.new(|_| ContextStore::new(project.clone()));
|
||||
assistant.assist(
|
||||
prompt_editor,
|
||||
self.workspace.clone(),
|
||||
@@ -2547,7 +2515,6 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
|
||||
project,
|
||||
prompt_store,
|
||||
thread_store,
|
||||
text_thread_store,
|
||||
initial_prompt,
|
||||
window,
|
||||
cx,
|
||||
@@ -2590,7 +2557,7 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
};
|
||||
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.open_saved_prompt_editor(path, window, cx)
|
||||
panel.open_saved_text_thread(path, window, cx)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ mod agent_diff;
|
||||
mod agent_model_selector;
|
||||
mod agent_panel;
|
||||
mod buffer_codegen;
|
||||
mod context;
|
||||
mod context_picker;
|
||||
mod context_server_configuration;
|
||||
mod context_store;
|
||||
mod context_strip;
|
||||
mod inline_assistant;
|
||||
mod inline_prompt_editor;
|
||||
@@ -22,7 +24,6 @@ mod ui;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent::ThreadId;
|
||||
use agent_settings::{AgentProfileId, AgentSettings};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::Client;
|
||||
@@ -139,10 +140,7 @@ pub struct QuoteSelection;
|
||||
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = agent)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct NewThread {
|
||||
#[serde(default)]
|
||||
from_thread_id: Option<ThreadId>,
|
||||
}
|
||||
pub struct NewThread;
|
||||
|
||||
/// Creates a new external agent conversation thread.
|
||||
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
|
||||
@@ -196,13 +194,13 @@ impl ExternalAgent {
|
||||
pub fn server(
|
||||
&self,
|
||||
fs: Arc<dyn fs::Fs>,
|
||||
history: Entity<agent2::HistoryStore>,
|
||||
history: Entity<agent::HistoryStore>,
|
||||
) -> Rc<dyn agent_servers::AgentServer> {
|
||||
match self {
|
||||
Self::Gemini => Rc::new(agent_servers::Gemini),
|
||||
Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
|
||||
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: _ } => {
|
||||
Rc::new(agent_servers::CustomAgentServer::new(name.clone()))
|
||||
}
|
||||
@@ -266,7 +264,6 @@ pub fn init(
|
||||
init_language_model_settings(cx);
|
||||
}
|
||||
assistant_slash_command::init(cx);
|
||||
agent::init(fs.clone(), cx);
|
||||
agent_panel::init(cx);
|
||||
context_server_configuration::init(language_registry.clone(), fs.clone(), cx);
|
||||
TextThreadEditor::init(cx);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use crate::inline_prompt_editor::CodegenStatus;
|
||||
use agent::{
|
||||
ContextStore,
|
||||
context::{ContextLoadResult, load_context},
|
||||
use crate::{
|
||||
context::load_context, context_store::ContextStore, inline_prompt_editor::CodegenStatus,
|
||||
};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result};
|
||||
@@ -434,16 +432,16 @@ impl CodegenAlternative {
|
||||
.generate_inline_transformation_prompt(user_prompt, language_name, buffer, range)
|
||||
.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() {
|
||||
let context = context_store
|
||||
.read(cx)
|
||||
.context()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
load_context(context, &project, &self.prompt_store, cx)
|
||||
Some(load_context(context, &project, &self.prompt_store, cx))
|
||||
} else {
|
||||
Task::ready(ContextLoadResult::default())
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
@@ -459,7 +457,6 @@ impl CodegenAlternative {
|
||||
if let Some(context_task) = context_task {
|
||||
context_task
|
||||
.await
|
||||
.loaded_context
|
||||
.add_to_request_message(&mut request_message);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
use crate::thread::Thread;
|
||||
use agent::outline;
|
||||
use assistant_context::AssistantContext;
|
||||
use assistant_tool::outline;
|
||||
use collections::HashSet;
|
||||
use futures::future;
|
||||
use futures::{FutureExt, future::Shared};
|
||||
use gpui::{App, AppContext as _, ElementId, Entity, SharedString, Task};
|
||||
use icons::IconName;
|
||||
use language::Buffer;
|
||||
use language_model::{LanguageModelImage, LanguageModelRequestMessage, MessageContent};
|
||||
use project::{Project, ProjectEntryId, ProjectPath, Worktree};
|
||||
@@ -17,6 +14,7 @@ use std::hash::{Hash, Hasher};
|
||||
use std::path::PathBuf;
|
||||
use std::{ops::Range, path::Path, sync::Arc};
|
||||
use text::{Anchor, OffsetRangeExt as _};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownCodeBlock;
|
||||
use util::rel_path::RelPath;
|
||||
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 Some(file) = buffer_ref.file() else {
|
||||
log::error!("file context missing path");
|
||||
@@ -206,7 +204,7 @@ impl FileContextHandle {
|
||||
text: buffer_content.text.into(),
|
||||
is_outline: buffer_content.is_outline,
|
||||
});
|
||||
Some((context, vec![buffer]))
|
||||
Some(context)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -256,11 +254,7 @@ impl DirectoryContextHandle {
|
||||
self.entry_id.hash(state)
|
||||
}
|
||||
|
||||
fn load(
|
||||
self,
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> {
|
||||
fn load(self, project: Entity<Project>, cx: &mut App) -> Task<Option<AgentContext>> {
|
||||
let Some(worktree) = project.read(cx).worktree_for_entry(self.entry_id, cx) else {
|
||||
return Task::ready(None);
|
||||
};
|
||||
@@ -307,7 +301,7 @@ impl DirectoryContextHandle {
|
||||
});
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let (rope, buffer) = rope_task.await?;
|
||||
let (rope, _buffer) = rope_task.await?;
|
||||
let fenced_codeblock = MarkdownCodeBlock {
|
||||
tag: &codeblock_tag(&full_path, None),
|
||||
text: &rope.to_string(),
|
||||
@@ -318,18 +312,22 @@ impl DirectoryContextHandle {
|
||||
rel_path,
|
||||
fenced_codeblock,
|
||||
};
|
||||
Some((descendant, buffer))
|
||||
Some(descendant)
|
||||
})
|
||||
}));
|
||||
|
||||
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 {
|
||||
handle: self,
|
||||
full_path: directory_full_path,
|
||||
descendants,
|
||||
});
|
||||
Some((context, buffers))
|
||||
Some(context)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -397,7 +395,7 @@ impl SymbolContextHandle {
|
||||
.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 Some(file) = buffer_ref.file() else {
|
||||
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 line_range = self.enclosing_range.to_point(&buffer_ref.snapshot());
|
||||
let text = self.text(cx);
|
||||
let buffer = self.buffer.clone();
|
||||
let context = AgentContext::Symbol(SymbolContext {
|
||||
handle: self,
|
||||
full_path,
|
||||
line_range,
|
||||
text,
|
||||
});
|
||||
Task::ready(Some((context, vec![buffer])))
|
||||
Task::ready(Some(context))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,13 +465,12 @@ impl SelectionContextHandle {
|
||||
.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 {
|
||||
log::error!("selection context's file has no path");
|
||||
return Task::ready(None);
|
||||
};
|
||||
let text = self.text(cx);
|
||||
let buffer = self.buffer.clone();
|
||||
let context = AgentContext::Selection(SelectionContext {
|
||||
full_path: full_path.to_string_lossy().into_owned(),
|
||||
line_range: self.line_range(cx),
|
||||
@@ -482,7 +478,7 @@ impl SelectionContextHandle {
|
||||
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>>)>> {
|
||||
Task::ready(Some((AgentContext::FetchedUrl(self), vec![])))
|
||||
pub fn load(self) -> Task<Option<AgentContext>> {
|
||||
Task::ready(Some(AgentContext::FetchedUrl(self)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,7 +533,7 @@ impl Display for FetchedUrlContext {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ThreadContextHandle {
|
||||
pub thread: Entity<Thread>,
|
||||
pub thread: Entity<agent::Thread>,
|
||||
pub context_id: ContextId,
|
||||
}
|
||||
|
||||
@@ -558,22 +554,20 @@ impl ThreadContextHandle {
|
||||
}
|
||||
|
||||
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>>)>> {
|
||||
cx.spawn(async move |cx| {
|
||||
let text = Thread::wait_for_detailed_summary_or_text(&self.thread, cx).await?;
|
||||
let title = self
|
||||
.thread
|
||||
.read_with(cx, |thread, _cx| thread.summary().or_default())
|
||||
.ok()?;
|
||||
fn load(self, cx: &mut App) -> Task<Option<AgentContext>> {
|
||||
let task = self.thread.update(cx, |thread, cx| thread.summary(cx));
|
||||
let title = self.title(cx);
|
||||
cx.background_spawn(async move {
|
||||
let text = task.await?;
|
||||
let context = AgentContext::Thread(ThreadContext {
|
||||
title,
|
||||
text,
|
||||
handle: self,
|
||||
});
|
||||
Some((context, vec![]))
|
||||
Some(context)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -612,7 +606,7 @@ impl TextThreadContextHandle {
|
||||
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 text = self.context.read(cx).to_xml(cx);
|
||||
let context = AgentContext::TextThread(TextThreadContext {
|
||||
@@ -620,7 +614,7 @@ impl TextThreadContextHandle {
|
||||
text: text.into(),
|
||||
handle: self,
|
||||
});
|
||||
Task::ready(Some((context, vec![])))
|
||||
Task::ready(Some(context))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,7 +660,7 @@ impl RulesContextHandle {
|
||||
self,
|
||||
prompt_store: &Option<Entity<PromptStore>>,
|
||||
cx: &App,
|
||||
) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> {
|
||||
) -> Task<Option<AgentContext>> {
|
||||
let Some(prompt_store) = prompt_store.as_ref() else {
|
||||
return Task::ready(None);
|
||||
};
|
||||
@@ -685,7 +679,7 @@ impl RulesContextHandle {
|
||||
title,
|
||||
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 {
|
||||
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)]
|
||||
pub struct LoadedContext {
|
||||
pub contexts: Vec<AgentContext>,
|
||||
pub text: String,
|
||||
pub images: Vec<LanguageModelImage>,
|
||||
}
|
||||
|
||||
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) {
|
||||
if !self.text.is_empty() {
|
||||
request_message
|
||||
@@ -804,7 +787,7 @@ pub fn load_context(
|
||||
project: &Entity<Project>,
|
||||
prompt_store: &Option<Entity<PromptStore>>,
|
||||
cx: &mut App,
|
||||
) -> Task<ContextLoadResult> {
|
||||
) -> Task<LoadedContext> {
|
||||
let load_tasks: Vec<_> = contexts
|
||||
.into_iter()
|
||||
.map(|context| match context {
|
||||
@@ -823,16 +806,7 @@ pub fn load_context(
|
||||
cx.background_spawn(async move {
|
||||
let load_results = future::join_all(load_tasks).await;
|
||||
|
||||
let mut contexts = Vec::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 directory_context = Vec::new();
|
||||
@@ -843,7 +817,7 @@ pub fn load_context(
|
||||
let mut text_thread_context = Vec::new();
|
||||
let mut rules_context = Vec::new();
|
||||
let mut images = Vec::new();
|
||||
for context in &contexts {
|
||||
for context in load_results.into_iter().flatten() {
|
||||
match context {
|
||||
AgentContext::File(context) => file_context.push(context),
|
||||
AgentContext::Directory(context) => directory_context.push(context),
|
||||
@@ -868,14 +842,7 @@ pub fn load_context(
|
||||
&& text_thread_context.is_empty()
|
||||
&& rules_context.is_empty()
|
||||
{
|
||||
return ContextLoadResult {
|
||||
loaded_context: LoadedContext {
|
||||
contexts,
|
||||
text,
|
||||
images,
|
||||
},
|
||||
referenced_buffers,
|
||||
};
|
||||
return LoadedContext { text, images };
|
||||
}
|
||||
|
||||
text.push_str(
|
||||
@@ -961,14 +928,7 @@ pub fn load_context(
|
||||
|
||||
text.push_str("</context>\n");
|
||||
|
||||
ContextLoadResult {
|
||||
loaded_context: LoadedContext {
|
||||
contexts,
|
||||
text,
|
||||
images,
|
||||
},
|
||||
referenced_buffers,
|
||||
}
|
||||
LoadedContext { text, images }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1131,11 +1091,13 @@ mod tests {
|
||||
|
||||
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!(
|
||||
file_context.is_outline,
|
||||
"Large file should use outline format"
|
||||
file_context
|
||||
.text
|
||||
.contains(&format!("# File outline for {}", path!("test/file.txt"))),
|
||||
"Large files should not get an outline"
|
||||
);
|
||||
|
||||
assert!(
|
||||
@@ -1153,29 +1115,38 @@ mod tests {
|
||||
|
||||
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!(
|
||||
!file_context.is_outline,
|
||||
!file_context
|
||||
.text
|
||||
.contains(&format!("# File outline for {}", path!("test/file.txt"))),
|
||||
"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
|
||||
let project = create_test_project(
|
||||
cx,
|
||||
json!({
|
||||
"file.txt": content,
|
||||
filename: content,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Open the buffer
|
||||
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();
|
||||
|
||||
let buffer = project
|
||||
@@ -1190,16 +1161,5 @@ mod tests {
|
||||
|
||||
cx.update(|cx| load_context(vec![context_handle], &project, &None, cx))
|
||||
.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")
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ use std::ops::Range;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent::{HistoryEntry, HistoryEntryId, HistoryStore};
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::{Result, anyhow};
|
||||
use collections::HashSet;
|
||||
pub use completion_provider::ContextPickerCompletionProvider;
|
||||
@@ -27,9 +29,7 @@ use project::ProjectPath;
|
||||
use prompt_store::PromptStore;
|
||||
use rules_context_picker::{RulesContextEntry, RulesContextPicker};
|
||||
use symbol_context_picker::SymbolContextPicker;
|
||||
use thread_context_picker::{
|
||||
ThreadContextEntry, ThreadContextPicker, render_thread_context_entry, unordered_thread_entries,
|
||||
};
|
||||
use thread_context_picker::render_thread_context_entry;
|
||||
use ui::{
|
||||
ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
|
||||
};
|
||||
@@ -37,12 +37,8 @@ use util::paths::PathStyle;
|
||||
use util::rel_path::RelPath;
|
||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
|
||||
use agent::{
|
||||
ThreadId,
|
||||
context::RULES_ICON,
|
||||
context_store::ContextStore,
|
||||
thread_store::{TextThreadStore, ThreadStore},
|
||||
};
|
||||
use crate::context_picker::thread_context_picker::ThreadContextPicker;
|
||||
use crate::{context::RULES_ICON, context_store::ContextStore};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum ContextPickerEntry {
|
||||
@@ -168,17 +164,16 @@ pub(super) struct ContextPicker {
|
||||
mode: ContextPickerState,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
text_thread_store: Option<WeakEntity<TextThreadStore>>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
thread_store: Option<WeakEntity<HistoryStore>>,
|
||||
prompt_store: Option<WeakEntity<PromptStore>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl ContextPicker {
|
||||
pub fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
text_thread_store: Option<WeakEntity<TextThreadStore>>,
|
||||
thread_store: Option<WeakEntity<HistoryStore>>,
|
||||
prompt_store: Option<WeakEntity<PromptStore>>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -199,13 +194,6 @@ impl ContextPicker {
|
||||
)
|
||||
.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 {
|
||||
mode: ContextPickerState::Default(ContextMenu::build(
|
||||
window,
|
||||
@@ -215,7 +203,6 @@ impl ContextPicker {
|
||||
workspace,
|
||||
context_store,
|
||||
thread_store,
|
||||
text_thread_store,
|
||||
prompt_store,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
@@ -355,17 +342,13 @@ impl ContextPicker {
|
||||
}));
|
||||
}
|
||||
ContextPickerMode::Thread => {
|
||||
if let Some((thread_store, text_thread_store)) = self
|
||||
.thread_store
|
||||
.as_ref()
|
||||
.zip(self.text_thread_store.as_ref())
|
||||
{
|
||||
if let Some(thread_store) = self.thread_store.clone() {
|
||||
self.mode = ContextPickerState::Thread(cx.new(|cx| {
|
||||
ThreadContextPicker::new(
|
||||
thread_store.clone(),
|
||||
text_thread_store.clone(),
|
||||
thread_store,
|
||||
context_picker.clone(),
|
||||
self.context_store.clone(),
|
||||
self.workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -480,16 +463,23 @@ impl ContextPicker {
|
||||
|
||||
fn add_recent_thread(
|
||||
&self,
|
||||
entry: ThreadContextEntry,
|
||||
window: &mut Window,
|
||||
entry: HistoryEntry,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let Some(context_store) = self.context_store.upgrade() else {
|
||||
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 {
|
||||
ThreadContextEntry::Thread { id, .. } => {
|
||||
HistoryEntry::AcpThread(thread) => {
|
||||
let Some(thread_store) = self
|
||||
.thread_store
|
||||
.as_ref()
|
||||
@@ -497,28 +487,28 @@ impl ContextPicker {
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("thread store not available")));
|
||||
};
|
||||
|
||||
let open_thread_task =
|
||||
thread_store.update(cx, |this, cx| this.open_thread(&id, window, cx));
|
||||
let load_thread_task =
|
||||
agent::load_agent_thread(thread.id, thread_store, project, 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.add_thread(thread, true, cx);
|
||||
})?;
|
||||
this.update(cx, |_this, cx| cx.notify())
|
||||
})
|
||||
}
|
||||
ThreadContextEntry::Context { path, .. } => {
|
||||
let Some(text_thread_store) = self
|
||||
.text_thread_store
|
||||
HistoryEntry::TextThread(thread) => {
|
||||
let Some(thread_store) = self
|
||||
.thread_store
|
||||
.as_ref()
|
||||
.and_then(|thread_store| thread_store.upgrade())
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("text thread store not available")));
|
||||
};
|
||||
|
||||
let task = text_thread_store
|
||||
.update(cx, |this, cx| this.open_local_context(path.clone(), cx));
|
||||
let task = thread_store.update(cx, |this, cx| {
|
||||
this.load_text_thread(thread.path.clone(), cx)
|
||||
});
|
||||
cx.spawn(async move |this, cx| {
|
||||
let thread = task.await?;
|
||||
context_store.update(cx, |context_store, cx| {
|
||||
@@ -542,7 +532,6 @@ impl ContextPicker {
|
||||
recent_context_picker_entries_with_store(
|
||||
context_store,
|
||||
self.thread_store.clone(),
|
||||
self.text_thread_store.clone(),
|
||||
workspace,
|
||||
None,
|
||||
cx,
|
||||
@@ -599,12 +588,12 @@ pub(crate) enum RecentEntry {
|
||||
project_path: ProjectPath,
|
||||
path_prefix: Arc<RelPath>,
|
||||
},
|
||||
Thread(ThreadContextEntry),
|
||||
Thread(HistoryEntry),
|
||||
}
|
||||
|
||||
pub(crate) fn available_context_picker_entries(
|
||||
prompt_store: &Option<Entity<PromptStore>>,
|
||||
thread_store: &Option<WeakEntity<ThreadStore>>,
|
||||
prompt_store: &Option<WeakEntity<PromptStore>>,
|
||||
thread_store: &Option<WeakEntity<HistoryStore>>,
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> Vec<ContextPickerEntry> {
|
||||
@@ -639,8 +628,7 @@ pub(crate) fn available_context_picker_entries(
|
||||
|
||||
fn recent_context_picker_entries_with_store(
|
||||
context_store: Entity<ContextStore>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
text_thread_store: Option<WeakEntity<TextThreadStore>>,
|
||||
thread_store: Option<WeakEntity<HistoryStore>>,
|
||||
workspace: Entity<Workspace>,
|
||||
exclude_path: Option<ProjectPath>,
|
||||
cx: &App,
|
||||
@@ -657,22 +645,14 @@ fn recent_context_picker_entries_with_store(
|
||||
|
||||
let exclude_threads = context_store.read(cx).thread_ids();
|
||||
|
||||
recent_context_picker_entries(
|
||||
thread_store,
|
||||
text_thread_store,
|
||||
workspace,
|
||||
&exclude_paths,
|
||||
exclude_threads,
|
||||
cx,
|
||||
)
|
||||
recent_context_picker_entries(thread_store, workspace, &exclude_paths, exclude_threads, cx)
|
||||
}
|
||||
|
||||
pub(crate) fn recent_context_picker_entries(
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
text_thread_store: Option<WeakEntity<TextThreadStore>>,
|
||||
thread_store: Option<WeakEntity<HistoryStore>>,
|
||||
workspace: Entity<Workspace>,
|
||||
exclude_paths: &HashSet<PathBuf>,
|
||||
_exclude_threads: &HashSet<ThreadId>,
|
||||
exclude_threads: &HashSet<acp::SessionId>,
|
||||
cx: &App,
|
||||
) -> Vec<RecentEntry> {
|
||||
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
|
||||
.and_then(|store| store.upgrade())
|
||||
.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));
|
||||
|
||||
if let Some(thread_store) = thread_store.and_then(|store| store.upgrade()) {
|
||||
const RECENT_THREADS_COUNT: usize = 2;
|
||||
recent.extend(
|
||||
threads
|
||||
.into_iter()
|
||||
.map(|(_, thread)| RecentEntry::Thread(thread)),
|
||||
thread_store
|
||||
.read(cx)
|
||||
.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 {
|
||||
ThreadContextEntry::Thread { id, title } => {
|
||||
format!("[@{}]({}:{})", title, Self::THREAD, id)
|
||||
HistoryEntry::AcpThread(thread) => {
|
||||
format!("[@{}]({}:{})", thread.title, Self::THREAD, thread.id)
|
||||
}
|
||||
ThreadContextEntry::Context { path, title } => {
|
||||
let filename = path.file_name().unwrap_or_default().to_string_lossy();
|
||||
HistoryEntry::TextThread(thread) => {
|
||||
let filename = thread
|
||||
.path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy();
|
||||
let escaped_filename = urlencoding::encode(&filename);
|
||||
format!(
|
||||
"[@{}]({}:{}{})",
|
||||
title,
|
||||
thread.title,
|
||||
Self::THREAD,
|
||||
Self::TEXT_THREAD_URL_PREFIX,
|
||||
escaped_filename
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user