Implements an ACP client that can be used from the agent panel
This commit is contained in:
Conrad Irwin 2025-07-09 10:02:31 -06:00 committed by GitHub
parent b9b42bee99
commit 495ec7a109
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 4839 additions and 23 deletions

68
Cargo.lock generated
View file

@ -2,6 +2,33 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "acp"
version = "0.1.0"
dependencies = [
"agent_servers",
"agentic-coding-protocol",
"anyhow",
"async-pipe",
"buffer_diff",
"editor",
"env_logger 0.11.8",
"futures 0.3.31",
"gpui",
"indoc",
"itertools 0.14.0",
"language",
"markdown",
"project",
"serde_json",
"settings",
"smol",
"tempfile",
"ui",
"util",
"workspace-hack",
]
[[package]] [[package]]
name = "activity_indicator" name = "activity_indicator"
version = "0.1.0" version = "0.1.0"
@ -107,6 +134,24 @@ dependencies = [
"zstd", "zstd",
] ]
[[package]]
name = "agent_servers"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"futures 0.3.31",
"gpui",
"paths",
"project",
"schemars",
"serde",
"settings",
"util",
"which 6.0.3",
"workspace-hack",
]
[[package]] [[package]]
name = "agent_settings" name = "agent_settings"
version = "0.1.0" version = "0.1.0"
@ -130,8 +175,11 @@ dependencies = [
name = "agent_ui" name = "agent_ui"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"acp",
"agent", "agent",
"agent_servers",
"agent_settings", "agent_settings",
"agentic-coding-protocol",
"anyhow", "anyhow",
"assistant_context", "assistant_context",
"assistant_slash_command", "assistant_slash_command",
@ -191,6 +239,7 @@ dependencies = [
"settings", "settings",
"smol", "smol",
"streaming_diff", "streaming_diff",
"task",
"telemetry", "telemetry",
"telemetry_events", "telemetry_events",
"terminal", "terminal",
@ -212,6 +261,22 @@ dependencies = [
"zed_llm_client", "zed_llm_client",
] ]
[[package]]
name = "agentic-coding-protocol"
version = "0.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b962eee17ee3924870d9b9d28cc8b6dcb5421e4d4e81cd864226374a122ceed1"
dependencies = [
"anyhow",
"chrono",
"futures 0.3.31",
"log",
"parking_lot",
"schemars",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.7.8" version = "0.7.8"
@ -14078,6 +14143,7 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984" checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984"
dependencies = [ dependencies = [
"chrono",
"dyn-clone", "dyn-clone",
"indexmap", "indexmap",
"ref-cast", "ref-cast",
@ -19579,6 +19645,7 @@ dependencies = [
"rustix 1.0.7", "rustix 1.0.7",
"rustls 0.23.26", "rustls 0.23.26",
"rustls-webpki 0.103.1", "rustls-webpki 0.103.1",
"schemars",
"scopeguard", "scopeguard",
"sea-orm", "sea-orm",
"sea-query-binder", "sea-query-binder",
@ -19976,6 +20043,7 @@ version = "0.196.0"
dependencies = [ dependencies = [
"activity_indicator", "activity_indicator",
"agent", "agent",
"agent_servers",
"agent_settings", "agent_settings",
"agent_ui", "agent_ui",
"anyhow", "anyhow",

View file

@ -2,9 +2,11 @@
resolver = "2" resolver = "2"
members = [ members = [
"crates/activity_indicator", "crates/activity_indicator",
"crates/acp",
"crates/agent_ui", "crates/agent_ui",
"crates/agent", "crates/agent",
"crates/agent_settings", "crates/agent_settings",
"crates/agent_servers",
"crates/anthropic", "crates/anthropic",
"crates/askpass", "crates/askpass",
"crates/assets", "crates/assets",
@ -216,10 +218,12 @@ edition = "2024"
# Workspace member crates # Workspace member crates
# #
activity_indicator = { path = "crates/activity_indicator" } acp = { path = "crates/acp" }
agent = { path = "crates/agent" } agent = { path = "crates/agent" }
activity_indicator = { path = "crates/activity_indicator" }
agent_ui = { path = "crates/agent_ui" } agent_ui = { path = "crates/agent_ui" }
agent_settings = { path = "crates/agent_settings" } agent_settings = { path = "crates/agent_settings" }
agent_servers = { path = "crates/agent_servers" }
ai = { path = "crates/ai" } ai = { path = "crates/ai" }
anthropic = { path = "crates/anthropic" } anthropic = { path = "crates/anthropic" }
askpass = { path = "crates/askpass" } askpass = { path = "crates/askpass" }
@ -400,6 +404,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates # External crates
# #
agentic-coding-protocol = "0.0.5"
aho-corasick = "1.1" aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14" any_vec = "0.14"

View file

@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google Gemini</title><path d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81"/></svg>

After

Width:  |  Height:  |  Size: 402 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.4174 10.2159C10.5454 9.58974 10.4174 9.57261 11.3762 8.46959C11.9337 7.82822 12.335 7.09214 12.335 6.27818C12.335 5.28184 11.9309 4.32631 11.2118 3.62179C10.4926 2.91728 9.5171 2.52148 8.50001 2.52148C7.48291 2.52148 6.50748 2.91728 5.78828 3.62179C5.06909 4.32631 4.66504 5.28184 4.66504 6.27818C4.66504 6.9043 4.79288 7.65565 5.62379 8.46959C6.58253 9.59098 6.45474 9.58974 6.58257 10.2159M10.4174 10.2159L10.4174 12.2989C10.4174 12.9504 9.87836 13.4786 9.21329 13.4786H7.78674C7.12167 13.4786 6.58253 12.9504 6.58253 12.2989L6.58257 10.2159M10.4174 10.2159H8.50001H6.58257" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 776 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.4 12.5C12.6917 12.5 12.9715 12.3884 13.1778 12.1899C13.3841 11.9913 13.5 11.722 13.5 11.4412V6.14706C13.5 5.86624 13.3841 5.59693 13.1778 5.39836C12.9715 5.19979 12.6917 5.08824 12.4 5.08824H8.055C7.87103 5.08997 7.68955 5.04726 7.52717 4.96402C7.36478 4.88078 7.22668 4.75967 7.1255 4.61176L6.68 3.97647C6.57984 3.83007 6.44349 3.7099 6.28317 3.62674C6.12286 3.54358 5.94361 3.50003 5.7615 3.5H3.6C3.30826 3.5 3.02847 3.61155 2.82218 3.81012C2.61589 4.00869 2.5 4.27801 2.5 4.55882V11.4412C2.5 11.722 2.61589 11.9913 2.82218 12.1899C3.02847 12.3884 3.30826 12.5 3.6 12.5H12.4Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 778 B

View file

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 8.5L4.94864 12.6222C4.71647 12.8544 4.40157 12.9848 4.07323 12.9848C3.74488 12.9848 3.42999 12.8544 3.19781 12.6222C2.96564 12.39 2.83521 12.0751 2.83521 11.7468C2.83521 11.4185 2.96564 11.1036 3.19781 10.8714L7.5 6.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.8352 9.98474L13.8352 6.98474" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.8352 7.42495L11.7634 6.4298C11.5533 6.23484 11.4353 5.97039 11.4352 5.69462V5.08526L10.1696 3.91022C9.54495 3.33059 8.69961 3.00261 7.81649 2.99722L5.83521 2.98474L6.35041 3.41108C6.71634 3.71233 7.00935 4.08216 7.21013 4.4962C7.4109 4.91024 7.51488 5.35909 7.51521 5.81316L7.5 6.5L9 8.5L9.5 8C9.5 8 9.87337 7.79457 10.0834 7.98959L11.1552 8.98474" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 988 B

View file

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5871 5.40582C12.8514 5.14152 13 4.78304 13 4.40922C13 4.03541 12.8516 3.67688 12.5873 3.41252C12.323 3.14816 11.9645 2.99962 11.5907 2.99957C11.2169 2.99953 10.8584 3.14798 10.594 3.41227L3.92098 10.0869C3.80488 10.2027 3.71903 10.3452 3.67097 10.5019L3.01047 12.678C2.99754 12.7212 2.99657 12.7672 3.00764 12.8109C3.01872 12.8547 3.04143 12.8946 3.07337 12.9265C3.1053 12.9584 3.14528 12.981 3.18905 12.992C3.23282 13.003 3.27875 13.002 3.32197 12.989L5.49849 12.329C5.65508 12.2813 5.79758 12.196 5.91349 12.0805L12.5871 5.40582Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 5L11 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 835 B

View file

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.57132 13.7143C5.20251 13.7143 5.71418 13.2026 5.71418 12.5714C5.71418 11.9403 5.20251 11.4286 4.57132 11.4286C3.94014 11.4286 3.42847 11.9403 3.42847 12.5714C3.42847 13.2026 3.94014 13.7143 4.57132 13.7143Z" fill="black"/>
<path d="M10.2856 2.85712V5.71426M10.2856 5.71426V8.5714M10.2856 5.71426H13.1428M10.2856 5.71426H7.42847M10.2856 5.71426L12.1904 3.80949M10.2856 5.71426L8.38084 7.61906M10.2856 5.71426L12.1904 7.61906M10.2856 5.71426L8.38084 3.80949" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 631 B

View file

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 13L11 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.5 12C9.98528 12 12 9.98528 12 7.5C12 5.01472 9.98528 3 7.5 3C5.01472 3 3 5.01472 3 7.5C3 9.98528 5.01472 12 7.5 12Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 421 B

View file

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.99487 8.44023L7.32821 7.10689L5.99487 5.77356" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.33838 10.2264H10.005" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.8889 3H4.11111C3.49746 3 3 3.49746 3 4.11111V11.8889C3 12.5025 3.49746 13 4.11111 13H11.8889C12.5025 13 13 12.5025 13 11.8889V4.11111C13 3.49746 12.5025 3 11.8889 3Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 625 B

17
assets/icons/tool_web.svg Normal file
View file

@ -0,0 +1,17 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2663_433)">
<mask id="mask0_2663_433" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<path d="M16 0H0V16H16V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_2663_433)">
<path d="M8 13C10.7614 13 13 10.7614 13 7.99999C13 5.23857 10.7614 3 8 3C5.23857 3 3 5.23857 3 7.99999C3 10.7614 5.23857 13 8 13Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 3C6.71611 4.34807 6 6.13836 6 7.99999C6 9.86163 6.71611 11.6519 8 13C9.28387 11.6519 10 9.86163 10 7.99999C10 6.13836 9.28387 4.34807 8 3Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 8H13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</g>
<defs>
<clipPath id="clip0_2663_433">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1,001 B

View file

@ -306,6 +306,15 @@
"enter": "agent::AcceptSuggestedContext" "enter": "agent::AcceptSuggestedContext"
} }
}, },
{
"context": "AcpThread > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
"up": "agent::PreviousHistoryMessage",
"down": "agent::NextHistoryMessage"
}
},
{ {
"context": "ThreadHistory", "context": "ThreadHistory",
"bindings": { "bindings": {

View file

@ -357,6 +357,15 @@
"ctrl--": "pane::GoBack" "ctrl--": "pane::GoBack"
} }
}, },
{
"context": "AcpThread > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
"up": "agent::PreviousHistoryMessage",
"down": "agent::NextHistoryMessage"
}
},
{ {
"context": "ThreadHistory", "context": "ThreadHistory",
"bindings": { "bindings": {

View file

@ -1855,6 +1855,8 @@
"read_ssh_config": true, "read_ssh_config": true,
// Configures context servers for use by the agent. // Configures context servers for use by the agent.
"context_servers": {}, "context_servers": {},
// Configures agent servers available in the agent panel.
"agent_servers": {},
"debugger": { "debugger": {
"stepping_granularity": "line", "stepping_granularity": "line",
"save_breakpoints": true, "save_breakpoints": true,

46
crates/acp/Cargo.toml Normal file
View file

@ -0,0 +1,46 @@
[package]
name = "acp"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/acp.rs"
doctest = false
[features]
test-support = ["gpui/test-support", "project/test-support"]
gemini = []
[dependencies]
agent_servers.workspace = true
agentic-coding-protocol.workspace = true
anyhow.workspace = true
buffer_diff.workspace = true
editor.workspace = true
futures.workspace = true
gpui.workspace = true
itertools.workspace = true
language.workspace = true
markdown.workspace = true
project.workspace = true
settings.workspace = true
smol.workspace = true
ui.workspace = true
util.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
async-pipe.workspace = true
env_logger.workspace = true
gpui = { workspace = true, "features" = ["test-support"] }
indoc.workspace = true
project = { workspace = true, "features" = ["test-support"] }
serde_json.workspace = true
tempfile.workspace = true
util.workspace = true
settings.workspace = true

1
crates/acp/LICENSE-GPL Symbolic link
View file

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

1625
crates/acp/src/acp.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,27 @@
[package]
name = "agent_servers"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/agent_servers.rs"
doctest = false
[dependencies]
anyhow.workspace = true
collections.workspace = true
futures.workspace = true
gpui.workspace = true
paths.workspace = true
project.workspace = true
schemars.workspace = true
serde.workspace = true
settings.workspace = true
util.workspace = true
which.workspace = true
workspace-hack.workspace = true

View file

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

View file

@ -0,0 +1,231 @@
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use anyhow::{Context as _, Result};
use collections::HashMap;
use gpui::{App, AsyncApp, Entity, SharedString};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsStore};
use util::{ResultExt, paths};
pub fn init(cx: &mut App) {
AllAgentServersSettings::register(cx);
}
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug)]
pub struct AllAgentServersSettings {
gemini: Option<AgentServerSettings>,
}
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)]
pub struct AgentServerSettings {
#[serde(flatten)]
command: AgentServerCommand,
}
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
pub struct AgentServerCommand {
#[serde(rename = "command")]
pub path: PathBuf,
#[serde(default)]
pub args: Vec<String>,
pub env: Option<HashMap<String, String>>,
}
pub struct Gemini;
pub struct AgentServerVersion {
pub current_version: SharedString,
pub supported: bool,
}
pub trait AgentServer: Send {
fn command(
&self,
project: &Entity<Project>,
cx: &mut AsyncApp,
) -> impl Future<Output = Result<AgentServerCommand>>;
fn version(
&self,
command: &AgentServerCommand,
) -> impl Future<Output = Result<AgentServerVersion>> + Send;
}
const GEMINI_ACP_ARG: &str = "--acp";
impl AgentServer for Gemini {
async fn command(
&self,
project: &Entity<Project>,
cx: &mut AsyncApp,
) -> Result<AgentServerCommand> {
let custom_command = cx.read_global(|settings: &SettingsStore, _| {
let settings = settings.get::<AllAgentServersSettings>(None);
settings
.gemini
.as_ref()
.map(|gemini_settings| AgentServerCommand {
path: gemini_settings.command.path.clone(),
args: gemini_settings
.command
.args
.iter()
.cloned()
.chain(std::iter::once(GEMINI_ACP_ARG.into()))
.collect(),
env: gemini_settings.command.env.clone(),
})
})?;
if let Some(custom_command) = custom_command {
return Ok(custom_command);
}
if let Some(path) = find_bin_in_path("gemini", project, cx).await {
return Ok(AgentServerCommand {
path,
args: vec![GEMINI_ACP_ARG.into()],
env: None,
});
}
let (fs, node_runtime) = project.update(cx, |project, _| {
(project.fs().clone(), project.node_runtime().cloned())
})?;
let node_runtime = node_runtime.context("gemini not found on path")?;
let directory = ::paths::agent_servers_dir().join("gemini");
fs.create_dir(&directory).await?;
node_runtime
.npm_install_packages(&directory, &[("@google/gemini-cli", "latest")])
.await?;
let path = directory.join("node_modules/.bin/gemini");
Ok(AgentServerCommand {
path,
args: vec![GEMINI_ACP_ARG.into()],
env: None,
})
}
async fn version(&self, command: &AgentServerCommand) -> Result<AgentServerVersion> {
let version_fut = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--version")
.kill_on_drop(true)
.output();
let help_fut = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--help")
.kill_on_drop(true)
.output();
let (version_output, help_output) = futures::future::join(version_fut, help_fut).await;
let current_version = String::from_utf8(version_output?.stdout)?.into();
let supported = String::from_utf8(help_output?.stdout)?.contains(GEMINI_ACP_ARG);
Ok(AgentServerVersion {
current_version,
supported,
})
}
}
async fn find_bin_in_path(
bin_name: &'static str,
project: &Entity<Project>,
cx: &mut AsyncApp,
) -> Option<PathBuf> {
let (env_task, root_dir) = project
.update(cx, |project, cx| {
let worktree = project.visible_worktrees(cx).next();
match worktree {
Some(worktree) => {
let env_task = project.environment().update(cx, |env, cx| {
env.get_worktree_environment(worktree.clone(), cx)
});
let path = worktree.read(cx).abs_path();
(env_task, path)
}
None => {
let path: Arc<Path> = paths::home_dir().as_path().into();
let env_task = project.environment().update(cx, |env, cx| {
env.get_directory_environment(path.clone(), cx)
});
(env_task, path)
}
}
})
.log_err()?;
cx.background_executor()
.spawn(async move {
let which_result = if cfg!(windows) {
which::which(bin_name)
} else {
let env = env_task.await.unwrap_or_default();
let shell_path = env.get("PATH").cloned();
which::which_in(bin_name, shell_path.as_ref(), root_dir.as_ref())
};
if let Err(which::Error::CannotFindBinaryPath) = which_result {
return None;
}
which_result.log_err()
})
.await
}
impl std::fmt::Debug for AgentServerCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let filtered_env = self.env.as_ref().map(|env| {
env.iter()
.map(|(k, v)| {
(
k,
if util::redact::should_redact(k) {
"[REDACTED]"
} else {
v
},
)
})
.collect::<Vec<_>>()
});
f.debug_struct("AgentServerCommand")
.field("path", &self.path)
.field("args", &self.args)
.field("env", &filtered_env)
.finish()
}
}
impl settings::Settings for AllAgentServersSettings {
const KEY: Option<&'static str> = Some("agent_servers");
type FileContent = Self;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
let mut settings = AllAgentServersSettings::default();
for value in sources.defaults_and_customizations() {
if value.gemini.is_some() {
settings.gemini = value.gemini.clone();
}
}
Ok(settings)
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}

View file

@ -13,14 +13,14 @@ path = "src/agent_ui.rs"
doctest = false doctest = false
[features] [features]
test-support = [ test-support = ["gpui/test-support", "language/test-support"]
"gpui/test-support",
"language/test-support",
]
[dependencies] [dependencies]
acp.workspace = true
agent.workspace = true agent.workspace = true
agentic-coding-protocol.workspace = true
agent_settings.workspace = true agent_settings.workspace = true
agent_servers.workspace = true
anyhow.workspace = true anyhow.workspace = true
assistant_context.workspace = true assistant_context.workspace = true
assistant_slash_command.workspace = true assistant_slash_command.workspace = true
@ -76,6 +76,7 @@ serde_json_lenient.workspace = true
settings.workspace = true settings.workspace = true
smol.workspace = true smol.workspace = true
streaming_diff.workspace = true streaming_diff.workspace = true
task.workspace = true
telemetry.workspace = true telemetry.workspace = true
telemetry_events.workspace = true telemetry_events.workspace = true
terminal.workspace = true terminal.workspace = true

View file

@ -0,0 +1,5 @@
mod completion_provider;
mod message_history;
mod thread_view;
pub use thread_view::AcpThreadView;

View file

@ -0,0 +1,574 @@
use std::ops::Range;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use anyhow::Result;
use collections::HashMap;
use editor::display_map::CreaseId;
use editor::{CompletionProvider, Editor, ExcerptId};
use file_icons::FileIcons;
use gpui::{App, Entity, Task, WeakEntity};
use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext;
use parking_lot::Mutex;
use project::{Completion, CompletionIntent, CompletionResponse, ProjectPath, WorktreeId};
use rope::Point;
use text::{Anchor, ToPoint};
use ui::prelude::*;
use workspace::Workspace;
use crate::context_picker::MentionLink;
use crate::context_picker::file_context_picker::{extract_file_name_and_directory, search_files};
#[derive(Default)]
pub struct MentionSet {
paths_by_crease_id: HashMap<CreaseId, ProjectPath>,
}
impl MentionSet {
pub fn insert(&mut self, crease_id: CreaseId, path: ProjectPath) {
self.paths_by_crease_id.insert(crease_id, path);
}
pub fn path_for_crease_id(&self, crease_id: CreaseId) -> Option<ProjectPath> {
self.paths_by_crease_id.get(&crease_id).cloned()
}
pub fn drain(&mut self) -> impl Iterator<Item = CreaseId> {
self.paths_by_crease_id.drain().map(|(id, _)| id)
}
}
pub struct ContextPickerCompletionProvider {
workspace: WeakEntity<Workspace>,
editor: WeakEntity<Editor>,
mention_set: Arc<Mutex<MentionSet>>,
}
impl ContextPickerCompletionProvider {
pub fn new(
mention_set: Arc<Mutex<MentionSet>>,
workspace: WeakEntity<Workspace>,
editor: WeakEntity<Editor>,
) -> Self {
Self {
mention_set,
workspace,
editor,
}
}
fn completion_for_path(
project_path: ProjectPath,
path_prefix: &str,
is_recent: bool,
is_directory: bool,
excerpt_id: ExcerptId,
source_range: Range<Anchor>,
editor: Entity<Editor>,
mention_set: Arc<Mutex<MentionSet>>,
cx: &App,
) -> Completion {
let (file_name, directory) =
extract_file_name_and_directory(&project_path.path, path_prefix);
let label =
build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
let full_path = if let Some(directory) = directory {
format!("{}{}", directory, file_name)
} else {
file_name.to_string()
};
let crease_icon_path = if is_directory {
FileIcons::get_folder_icon(false, cx).unwrap_or_else(|| IconName::Folder.path().into())
} else {
FileIcons::get_icon(Path::new(&full_path), cx)
.unwrap_or_else(|| IconName::File.path().into())
};
let completion_icon_path = if is_recent {
IconName::HistoryRerun.path().into()
} else {
crease_icon_path.clone()
};
let new_text = format!("{} ", MentionLink::for_file(&file_name, &full_path));
let new_text_len = new_text.len();
Completion {
replace_range: source_range.clone(),
new_text,
label,
documentation: None,
source: project::CompletionSource::Custom,
icon_path: Some(completion_icon_path),
insert_text_mode: None,
confirm: Some(confirm_completion_callback(
crease_icon_path,
file_name,
project_path,
excerpt_id,
source_range.start,
new_text_len - 1,
editor,
mention_set,
)),
}
}
}
fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
let mut label = CodeLabel::default();
label.push_str(&file_name, None);
label.push_str(" ", None);
if let Some(directory) = directory {
label.push_str(&directory, comment_id);
}
label.filter_range = 0..label.text().len();
label
}
impl CompletionProvider for ContextPickerCompletionProvider {
fn completions(
&self,
excerpt_id: ExcerptId,
buffer: &Entity<Buffer>,
buffer_position: Anchor,
_trigger: CompletionContext,
_window: &mut Window,
cx: &mut Context<Editor>,
) -> Task<Result<Vec<CompletionResponse>>> {
let state = buffer.update(cx, |buffer, _cx| {
let position = buffer_position.to_point(buffer);
let line_start = Point::new(position.row, 0);
let offset_to_line = buffer.point_to_offset(line_start);
let mut lines = buffer.text_for_range(line_start..position).lines();
let line = lines.next()?;
MentionCompletion::try_parse(line, offset_to_line)
});
let Some(state) = state else {
return Task::ready(Ok(Vec::new()));
};
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(Ok(Vec::new()));
};
let snapshot = buffer.read(cx).snapshot();
let source_range = snapshot.anchor_before(state.source_range.start)
..snapshot.anchor_after(state.source_range.end);
let editor = self.editor.clone();
let mention_set = self.mention_set.clone();
let MentionCompletion { argument, .. } = state;
let query = argument.unwrap_or_else(|| "".to_string());
let search_task = search_files(query.clone(), Arc::<AtomicBool>::default(), &workspace, cx);
cx.spawn(async move |_, cx| {
let matches = search_task.await;
let Some(editor) = editor.upgrade() else {
return Ok(Vec::new());
};
let completions = cx.update(|cx| {
matches
.into_iter()
.map(|mat| {
let path_match = &mat.mat;
let project_path = ProjectPath {
worktree_id: WorktreeId::from_usize(path_match.worktree_id),
path: path_match.path.clone(),
};
Self::completion_for_path(
project_path,
&path_match.path_prefix,
mat.is_recent,
path_match.is_dir,
excerpt_id,
source_range.clone(),
editor.clone(),
mention_set.clone(),
cx,
)
})
.collect()
})?;
Ok(vec![CompletionResponse {
completions,
// Since this does its own filtering (see `filter_completions()` returns false),
// there is no benefit to computing whether this set of completions is incomplete.
is_incomplete: true,
}])
})
}
fn is_completion_trigger(
&self,
buffer: &Entity<language::Buffer>,
position: language::Anchor,
_text: &str,
_trigger_in_words: bool,
_menu_is_open: bool,
cx: &mut Context<Editor>,
) -> bool {
let buffer = buffer.read(cx);
let position = position.to_point(buffer);
let line_start = Point::new(position.row, 0);
let offset_to_line = buffer.point_to_offset(line_start);
let mut lines = buffer.text_for_range(line_start..position).lines();
if let Some(line) = lines.next() {
MentionCompletion::try_parse(line, offset_to_line)
.map(|completion| {
completion.source_range.start <= offset_to_line + position.column as usize
&& completion.source_range.end >= offset_to_line + position.column as usize
})
.unwrap_or(false)
} else {
false
}
}
fn sort_completions(&self) -> bool {
false
}
fn filter_completions(&self) -> bool {
false
}
}
fn confirm_completion_callback(
crease_icon_path: SharedString,
crease_text: SharedString,
project_path: ProjectPath,
excerpt_id: ExcerptId,
start: Anchor,
content_len: usize,
editor: Entity<Editor>,
mention_set: Arc<Mutex<MentionSet>>,
) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
Arc::new(move |_, window, cx| {
let crease_text = crease_text.clone();
let crease_icon_path = crease_icon_path.clone();
let editor = editor.clone();
let project_path = project_path.clone();
let mention_set = mention_set.clone();
window.defer(cx, move |window, cx| {
let crease_id = crate::context_picker::insert_crease_for_mention(
excerpt_id,
start,
content_len,
crease_text.clone(),
crease_icon_path,
editor.clone(),
window,
cx,
);
if let Some(crease_id) = crease_id {
mention_set.lock().insert(crease_id, project_path);
}
});
false
})
}
#[derive(Debug, Default, PartialEq)]
struct MentionCompletion {
source_range: Range<usize>,
argument: Option<String>,
}
impl MentionCompletion {
fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> {
let last_mention_start = line.rfind('@')?;
if last_mention_start >= line.len() {
return Some(Self::default());
}
if last_mention_start > 0
&& line
.chars()
.nth(last_mention_start - 1)
.map_or(false, |c| !c.is_whitespace())
{
return None;
}
let rest_of_line = &line[last_mention_start + 1..];
let mut argument = None;
let mut parts = rest_of_line.split_whitespace();
let mut end = last_mention_start + 1;
if let Some(argument_text) = parts.next() {
end += argument_text.len();
argument = Some(argument_text.to_string());
}
Some(Self {
source_range: last_mention_start + offset_to_line..end + offset_to_line,
argument,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext};
use project::{Project, ProjectPath};
use serde_json::json;
use settings::SettingsStore;
use std::{ops::Deref, rc::Rc};
use util::path;
use workspace::{AppState, Item};
#[test]
fn test_mention_completion_parse() {
assert_eq!(MentionCompletion::try_parse("Lorem Ipsum", 0), None);
assert_eq!(
MentionCompletion::try_parse("Lorem @", 0),
Some(MentionCompletion {
source_range: 6..7,
argument: None,
})
);
assert_eq!(
MentionCompletion::try_parse("Lorem @main", 0),
Some(MentionCompletion {
source_range: 6..11,
argument: Some("main".to_string()),
})
);
assert_eq!(MentionCompletion::try_parse("test@", 0), None);
}
struct AtMentionEditor(Entity<Editor>);
impl Item for AtMentionEditor {
type Event = ();
fn include_in_nav_history() -> bool {
false
}
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
"Test".into()
}
}
impl EventEmitter<()> for AtMentionEditor {}
impl Focusable for AtMentionEditor {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.0.read(cx).focus_handle(cx).clone()
}
}
impl Render for AtMentionEditor {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.0.clone().into_any_element()
}
}
#[gpui::test]
async fn test_context_completion_provider(cx: &mut TestAppContext) {
init_test(cx);
let app_state = cx.update(AppState::test);
cx.update(|cx| {
language::init(cx);
editor::init(cx);
workspace::init(app_state.clone(), cx);
Project::init_settings(cx);
});
app_state
.fs
.as_fake()
.insert_tree(
path!("/dir"),
json!({
"editor": "",
"a": {
"one.txt": "",
"two.txt": "",
"three.txt": "",
"four.txt": ""
},
"b": {
"five.txt": "",
"six.txt": "",
"seven.txt": "",
"eight.txt": "",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let workspace = window.root(cx).unwrap();
let worktree = project.update(cx, |project, cx| {
let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1);
worktrees.pop().unwrap()
});
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
let mut cx = VisualTestContext::from_window(*window.deref(), cx);
let paths = vec![
path!("a/one.txt"),
path!("a/two.txt"),
path!("a/three.txt"),
path!("a/four.txt"),
path!("b/five.txt"),
path!("b/six.txt"),
path!("b/seven.txt"),
path!("b/eight.txt"),
];
let mut opened_editors = Vec::new();
for path in paths {
let buffer = workspace
.update_in(&mut cx, |workspace, window, cx| {
workspace.open_path(
ProjectPath {
worktree_id,
path: Path::new(path).into(),
},
None,
false,
window,
cx,
)
})
.await
.unwrap();
opened_editors.push(buffer);
}
let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
let editor = cx.new(|cx| {
Editor::new(
editor::EditorMode::full(),
multi_buffer::MultiBuffer::build_simple("", cx),
None,
window,
cx,
)
});
workspace.active_pane().update(cx, |pane, cx| {
pane.add_item(
Box::new(cx.new(|_| AtMentionEditor(editor.clone()))),
true,
true,
None,
window,
cx,
);
});
editor
});
let mention_set = Arc::new(Mutex::new(MentionSet::default()));
let editor_entity = editor.downgrade();
editor.update_in(&mut cx, |editor, window, cx| {
window.focus(&editor.focus_handle(cx));
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
mention_set.clone(),
workspace.downgrade(),
editor_entity,
))));
});
cx.simulate_input("Lorem ");
editor.update(&mut cx, |editor, cx| {
assert_eq!(editor.text(cx), "Lorem ");
assert!(!editor.has_visible_completions_menu());
});
cx.simulate_input("@");
editor.update(&mut cx, |editor, cx| {
assert_eq!(editor.text(cx), "Lorem @");
assert!(editor.has_visible_completions_menu());
assert_eq!(
current_completion_labels(editor),
&[
"eight.txt dir/b/",
"seven.txt dir/b/",
"six.txt dir/b/",
"five.txt dir/b/",
"four.txt dir/a/",
"three.txt dir/a/",
"two.txt dir/a/",
"one.txt dir/a/",
"dir ",
"a dir/",
"four.txt dir/a/",
"one.txt dir/a/",
"three.txt dir/a/",
"two.txt dir/a/",
"b dir/",
"eight.txt dir/b/",
"five.txt dir/b/",
"seven.txt dir/b/",
"six.txt dir/b/",
"editor dir/"
]
);
});
// Select and confirm "File"
editor.update_in(&mut cx, |editor, window, cx| {
assert!(editor.has_visible_completions_menu());
editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
});
cx.run_until_parked();
editor.update(&mut cx, |editor, cx| {
assert_eq!(editor.text(cx), "Lorem [@four.txt](@file:dir/a/four.txt) ");
});
}
fn current_completion_labels(editor: &Editor) -> Vec<String> {
let completions = editor.current_completions().expect("Missing completions");
completions
.into_iter()
.map(|completion| completion.label.text.to_string())
.collect::<Vec<_>>()
}
pub(crate) fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let store = SettingsStore::test(cx);
cx.set_global(store);
theme::init(theme::LoadThemes::JustBase, cx);
client::init_settings(cx);
language::init(cx);
Project::init_settings(cx);
workspace::init_settings(cx);
editor::init_settings(cx);
});
}
}

View file

@ -0,0 +1,81 @@
pub struct MessageHistory<T> {
items: Vec<T>,
current: Option<usize>,
}
impl<T> MessageHistory<T> {
pub fn new() -> Self {
MessageHistory {
items: Vec::new(),
current: None,
}
}
pub fn push(&mut self, message: T) {
self.current.take();
self.items.push(message);
}
pub fn prev(&mut self) -> Option<&T> {
if self.items.is_empty() {
return None;
}
let new_ix = self
.current
.get_or_insert(self.items.len())
.saturating_sub(1);
self.current = Some(new_ix);
self.items.get(new_ix)
}
pub fn next(&mut self) -> Option<&T> {
let current = self.current.as_mut()?;
*current += 1;
self.items.get(*current).or_else(|| {
self.current.take();
None
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prev_next() {
let mut history = MessageHistory::new();
// Test empty history
assert_eq!(history.prev(), None);
assert_eq!(history.next(), None);
// Add some messages
history.push("first");
history.push("second");
history.push("third");
// Test prev navigation
assert_eq!(history.prev(), Some(&"third"));
assert_eq!(history.prev(), Some(&"second"));
assert_eq!(history.prev(), Some(&"first"));
assert_eq!(history.prev(), Some(&"first"));
assert_eq!(history.next(), Some(&"second"));
// Test mixed navigation
history.push("fourth");
assert_eq!(history.prev(), Some(&"fourth"));
assert_eq!(history.prev(), Some(&"third"));
assert_eq!(history.next(), Some(&"fourth"));
assert_eq!(history.next(), None);
// Test that push resets navigation
history.prev();
history.prev();
history.push("fifth");
assert_eq!(history.prev(), Some(&"fifth"));
}
}

File diff suppressed because it is too large Load diff

View file

@ -7,12 +7,14 @@ use std::time::Duration;
use db::kvp::{Dismissable, KEY_VALUE_STORE}; use db::kvp::{Dismissable, KEY_VALUE_STORE};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::NewGeminiThread;
use crate::language_model_selector::ToggleModelSelector; use crate::language_model_selector::ToggleModelSelector;
use crate::{ use crate::{
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode, AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell,
ResetTrialUpsell, ToggleBurnMode, ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu, ResetTrialUpsell, ToggleBurnMode, ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu,
acp::AcpThreadView,
active_thread::{self, ActiveThread, ActiveThreadEvent}, active_thread::{self, ActiveThread, ActiveThreadEvent},
agent_configuration::{AgentConfiguration, AssistantConfigurationEvent}, agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
agent_diff::AgentDiff, agent_diff::AgentDiff,
@ -38,6 +40,7 @@ use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet; use assistant_tool::ToolWorkingSet;
use client::{UserStore, zed_urls}; use client::{UserStore, zed_urls};
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer}; use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use feature_flags::{self, FeatureFlagAppExt};
use fs::Fs; use fs::Fs;
use gpui::{ use gpui::{
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem, Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
@ -109,6 +112,12 @@ pub fn init(cx: &mut App) {
panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx)); panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
} }
}) })
.register_action(|workspace, _: &NewGeminiThread, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
panel.update(cx, |panel, cx| panel.new_gemini_thread(window, cx));
}
})
.register_action(|workspace, action: &OpenRulesLibrary, window, cx| { .register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) { if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx); workspace.focus_panel::<AgentPanel>(window, cx);
@ -125,7 +134,8 @@ pub fn init(cx: &mut App) {
let thread = thread.read(cx).thread().clone(); let thread = thread.read(cx).thread().clone();
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx); AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
} }
ActiveView::TextThread { .. } ActiveView::AcpThread { .. }
| ActiveView::TextThread { .. }
| ActiveView::History | ActiveView::History
| ActiveView::Configuration => {} | ActiveView::Configuration => {}
} }
@ -188,6 +198,9 @@ enum ActiveView {
message_editor: Entity<MessageEditor>, message_editor: Entity<MessageEditor>,
_subscriptions: Vec<gpui::Subscription>, _subscriptions: Vec<gpui::Subscription>,
}, },
AcpThread {
thread_view: Entity<AcpThreadView>,
},
TextThread { TextThread {
context_editor: Entity<TextThreadEditor>, context_editor: Entity<TextThreadEditor>,
title_editor: Entity<Editor>, title_editor: Entity<Editor>,
@ -207,7 +220,9 @@ enum WhichFontSize {
impl ActiveView { impl ActiveView {
pub fn which_font_size_used(&self) -> WhichFontSize { pub fn which_font_size_used(&self) -> WhichFontSize {
match self { match self {
ActiveView::Thread { .. } | ActiveView::History => WhichFontSize::AgentFont, ActiveView::Thread { .. } | ActiveView::AcpThread { .. } | ActiveView::History => {
WhichFontSize::AgentFont
}
ActiveView::TextThread { .. } => WhichFontSize::BufferFont, ActiveView::TextThread { .. } => WhichFontSize::BufferFont,
ActiveView::Configuration => WhichFontSize::None, ActiveView::Configuration => WhichFontSize::None,
} }
@ -238,6 +253,7 @@ impl ActiveView {
thread.scroll_to_bottom(cx); thread.scroll_to_bottom(cx);
}); });
} }
ActiveView::AcpThread { .. } => {}
ActiveView::TextThread { .. } ActiveView::TextThread { .. }
| ActiveView::History | ActiveView::History
| ActiveView::Configuration => {} | ActiveView::Configuration => {}
@ -653,7 +669,8 @@ impl AgentPanel {
.clone() .clone()
.update(cx, |thread, cx| thread.get_or_init_configured_model(cx)); .update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
} }
ActiveView::TextThread { .. } ActiveView::AcpThread { .. }
| ActiveView::TextThread { .. }
| ActiveView::History | ActiveView::History
| ActiveView::Configuration => {} | ActiveView::Configuration => {}
}, },
@ -733,6 +750,9 @@ impl AgentPanel {
ActiveView::Thread { thread, .. } => { ActiveView::Thread { thread, .. } => {
thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx)); thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
} }
ActiveView::AcpThread { thread_view, .. } => {
thread_view.update(cx, |thread_element, cx| thread_element.cancel(cx));
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {} ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
} }
} }
@ -740,7 +760,10 @@ impl AgentPanel {
fn active_message_editor(&self) -> Option<&Entity<MessageEditor>> { fn active_message_editor(&self) -> Option<&Entity<MessageEditor>> {
match &self.active_view { match &self.active_view {
ActiveView::Thread { message_editor, .. } => Some(message_editor), ActiveView::Thread { message_editor, .. } => Some(message_editor),
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None, ActiveView::AcpThread { .. }
| ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => None,
} }
} }
@ -862,6 +885,21 @@ impl AgentPanel {
context_editor.focus_handle(cx).focus(window); context_editor.focus_handle(cx).focus(window);
} }
fn new_gemini_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let workspace = self.workspace.clone();
let project = self.project.clone();
cx.spawn_in(window, async move |this, cx| {
let thread_view = cx.new_window_entity(|window, cx| {
crate::acp::AcpThreadView::new(workspace, project, window, cx)
})?;
this.update_in(cx, |this, window, cx| {
this.set_active_view(ActiveView::AcpThread { thread_view }, window, cx);
})
})
.detach();
}
fn deploy_rules_library( fn deploy_rules_library(
&mut self, &mut self,
action: &OpenRulesLibrary, action: &OpenRulesLibrary,
@ -994,6 +1032,7 @@ impl AgentPanel {
cx, cx,
) )
}); });
let message_editor = cx.new(|cx| { let message_editor = cx.new(|cx| {
MessageEditor::new( MessageEditor::new(
self.fs.clone(), self.fs.clone(),
@ -1025,6 +1064,9 @@ impl AgentPanel {
ActiveView::Thread { message_editor, .. } => { ActiveView::Thread { message_editor, .. } => {
message_editor.focus_handle(cx).focus(window); message_editor.focus_handle(cx).focus(window);
} }
ActiveView::AcpThread { thread_view } => {
thread_view.focus_handle(cx).focus(window);
}
ActiveView::TextThread { context_editor, .. } => { ActiveView::TextThread { context_editor, .. } => {
context_editor.focus_handle(cx).focus(window); context_editor.focus_handle(cx).focus(window);
} }
@ -1144,7 +1186,10 @@ impl AgentPanel {
}) })
.log_err(); .log_err();
} }
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {} ActiveView::AcpThread { .. }
| ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => {}
} }
} }
@ -1197,6 +1242,13 @@ impl AgentPanel {
) )
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
ActiveView::AcpThread { thread_view } => {
thread_view
.update(cx, |thread_view, cx| {
thread_view.open_thread_as_markdown(workspace, window, cx)
})
.detach_and_log_err(cx);
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {} ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
} }
} }
@ -1351,7 +1403,8 @@ impl AgentPanel {
} }
}) })
} }
_ => {} ActiveView::AcpThread { .. } => {}
ActiveView::History | ActiveView::Configuration => {}
} }
if current_is_special && !new_is_special { if current_is_special && !new_is_special {
@ -1437,6 +1490,7 @@ impl Focusable for AgentPanel {
fn focus_handle(&self, cx: &App) -> FocusHandle { fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.active_view { match &self.active_view {
ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx), ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx),
ActiveView::AcpThread { thread_view, .. } => thread_view.focus_handle(cx),
ActiveView::History => self.history.focus_handle(cx), ActiveView::History => self.history.focus_handle(cx),
ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx), ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx),
ActiveView::Configuration => { ActiveView::Configuration => {
@ -1593,6 +1647,9 @@ impl AgentPanel {
.into_any_element(), .into_any_element(),
} }
} }
ActiveView::AcpThread { thread_view } => Label::new(thread_view.read(cx).title(cx))
.truncate()
.into_any_element(),
ActiveView::TextThread { ActiveView::TextThread {
title_editor, title_editor,
context_editor, context_editor,
@ -1727,7 +1784,10 @@ impl AgentPanel {
let active_thread = match &self.active_view { let active_thread = match &self.active_view {
ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()), ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None, ActiveView::AcpThread { .. }
| ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => None,
}; };
let agent_extra_menu = PopoverMenu::new("agent-options-menu") let agent_extra_menu = PopoverMenu::new("agent-options-menu")
@ -1755,6 +1815,9 @@ impl AgentPanel {
menu = menu menu = menu
.action("New Thread", NewThread::default().boxed_clone()) .action("New Thread", NewThread::default().boxed_clone())
.action("New Text Thread", NewTextThread.boxed_clone()) .action("New Text Thread", NewTextThread.boxed_clone())
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
this.action("New Gemini Thread", NewGeminiThread.boxed_clone())
})
.when_some(active_thread, |this, active_thread| { .when_some(active_thread, |this, active_thread| {
let thread = active_thread.read(cx); let thread = active_thread.read(cx);
if !thread.is_empty() { if !thread.is_empty() {
@ -1893,6 +1956,9 @@ impl AgentPanel {
message_editor, message_editor,
.. ..
} => (thread.read(cx), message_editor.read(cx)), } => (thread.read(cx), message_editor.read(cx)),
ActiveView::AcpThread { .. } => {
return None;
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => { ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
return None; return None;
} }
@ -2031,6 +2097,9 @@ impl AgentPanel {
return false; return false;
} }
} }
ActiveView::AcpThread { .. } => {
return false;
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => { ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
return false; return false;
} }
@ -2615,6 +2684,9 @@ impl AgentPanel {
) -> Option<AnyElement> { ) -> Option<AnyElement> {
let active_thread = match &self.active_view { let active_thread = match &self.active_view {
ActiveView::Thread { thread, .. } => thread, ActiveView::Thread { thread, .. } => thread,
ActiveView::AcpThread { .. } => {
return None;
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => { ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
return None; return None;
} }
@ -2961,6 +3033,9 @@ impl AgentPanel {
.detach(); .detach();
}); });
} }
ActiveView::AcpThread { .. } => {
unimplemented!()
}
ActiveView::TextThread { context_editor, .. } => { ActiveView::TextThread { context_editor, .. } => {
context_editor.update(cx, |context_editor, cx| { context_editor.update(cx, |context_editor, cx| {
TextThreadEditor::insert_dragged_files( TextThreadEditor::insert_dragged_files(
@ -3034,6 +3109,7 @@ impl Render for AgentPanel {
}); });
this.continue_conversation(window, cx); this.continue_conversation(window, cx);
} }
ActiveView::AcpThread { .. } => {}
ActiveView::TextThread { .. } ActiveView::TextThread { .. }
| ActiveView::History | ActiveView::History
| ActiveView::Configuration => {} | ActiveView::Configuration => {}
@ -3075,6 +3151,10 @@ impl Render for AgentPanel {
}) })
.child(h_flex().child(message_editor.clone())) .child(h_flex().child(message_editor.clone()))
.child(self.render_drag_target(cx)), .child(self.render_drag_target(cx)),
ActiveView::AcpThread { thread_view, .. } => parent
.relative()
.child(thread_view.clone())
.child(self.render_drag_target(cx)),
ActiveView::History => parent.child(self.history.clone()), ActiveView::History => parent.child(self.history.clone()),
ActiveView::TextThread { ActiveView::TextThread {
context_editor, context_editor,

View file

@ -1,3 +1,4 @@
mod acp;
mod active_thread; mod active_thread;
mod agent_configuration; mod agent_configuration;
mod agent_diff; mod agent_diff;
@ -56,6 +57,8 @@ actions!(
[ [
/// Creates a new text-based conversation thread. /// Creates a new text-based conversation thread.
NewTextThread, NewTextThread,
/// Creates a new Gemini CLI-based conversation thread.
NewGeminiThread,
/// Toggles the context picker interface for adding files, symbols, or other context. /// Toggles the context picker interface for adding files, symbols, or other context.
ToggleContextPicker, ToggleContextPicker,
/// Toggles the navigation menu for switching between threads and views. /// Toggles the navigation menu for switching between threads and views.
@ -76,8 +79,6 @@ actions!(
AddContextServer, AddContextServer,
/// Removes the currently selected thread. /// Removes the currently selected thread.
RemoveSelectedThread, RemoveSelectedThread,
/// Starts a chat conversation with the agent.
Chat,
/// Starts a chat conversation with follow-up enabled. /// Starts a chat conversation with follow-up enabled.
ChatWithFollow, ChatWithFollow,
/// Cycles to the next inline assist suggestion. /// Cycles to the next inline assist suggestion.

View file

@ -1,6 +1,6 @@
mod completion_provider; mod completion_provider;
mod fetch_context_picker; mod fetch_context_picker;
mod file_context_picker; pub(crate) mod file_context_picker;
mod rules_context_picker; mod rules_context_picker;
mod symbol_context_picker; mod symbol_context_picker;
mod thread_context_picker; mod thread_context_picker;

View file

@ -47,13 +47,14 @@ use ui::{
}; };
use util::ResultExt as _; use util::ResultExt as _;
use workspace::{CollaboratorId, Workspace}; use workspace::{CollaboratorId, Workspace};
use zed_actions::agent::Chat;
use zed_llm_client::CompletionIntent; use zed_llm_client::CompletionIntent;
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention}; use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind}; use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::profile_selector::ProfileSelector; use crate::profile_selector::ProfileSelector;
use crate::{ use crate::{
ActiveThread, AgentDiffPane, Chat, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll, ActiveThread, AgentDiffPane, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll,
ModelUsageContext, NewThread, OpenAgentDiff, RejectAll, RemoveAllContext, ToggleBurnMode, ModelUsageContext, NewThread, OpenAgentDiff, RejectAll, RemoveAllContext, ToggleBurnMode,
ToggleContextPicker, ToggleProfileSelector, register_agent_preview, ToggleContextPicker, ToggleProfileSelector, register_agent_preview,
}; };

View file

@ -92,6 +92,12 @@ impl FeatureFlag for JjUiFeatureFlag {
const NAME: &'static str = "jj-ui"; const NAME: &'static str = "jj-ui";
} }
pub struct AcpFeatureFlag;
impl FeatureFlag for AcpFeatureFlag {
const NAME: &'static str = "acp";
}
pub struct ZedCloudFeatureFlag {} pub struct ZedCloudFeatureFlag {}
impl FeatureFlag for ZedCloudFeatureFlag { impl FeatureFlag for ZedCloudFeatureFlag {

View file

@ -13,6 +13,7 @@ pub enum IconName {
AiBedrock, AiBedrock,
AiDeepSeek, AiDeepSeek,
AiEdit, AiEdit,
AiGemini,
AiGoogle, AiGoogle,
AiLmStudio, AiLmStudio,
AiMistral, AiMistral,
@ -252,6 +253,14 @@ pub enum IconName {
TextSnippet, TextSnippet,
ThumbsDown, ThumbsDown,
ThumbsUp, ThumbsUp,
ToolBulb,
ToolFolder,
ToolHammer,
ToolPencil,
ToolRegex,
ToolSearch,
ToolTerminal,
ToolWeb,
Trash, Trash,
TrashAlt, TrashAlt,
Triangle, Triangle,

View file

@ -352,6 +352,14 @@ pub fn debug_adapters_dir() -> &'static PathBuf {
DEBUG_ADAPTERS_DIR.get_or_init(|| data_dir().join("debug_adapters")) DEBUG_ADAPTERS_DIR.get_or_init(|| data_dir().join("debug_adapters"))
} }
/// Returns the path to the agent servers directory
///
/// This is where agent servers are downloaded to
pub fn agent_servers_dir() -> &'static PathBuf {
static AGENT_SERVERS_DIR: OnceLock<PathBuf> = OnceLock::new();
AGENT_SERVERS_DIR.get_or_init(|| data_dir().join("agent_servers"))
}
/// Returns the path to the Copilot directory. /// Returns the path to the Copilot directory.
pub fn copilot_dir() -> &'static PathBuf { pub fn copilot_dir() -> &'static PathBuf {
static COPILOT_DIR: OnceLock<PathBuf> = OnceLock::new(); static COPILOT_DIR: OnceLock<PathBuf> = OnceLock::new();

View file

@ -84,7 +84,7 @@ impl ProjectEnvironment {
self.get_worktree_environment(worktree, cx) self.get_worktree_environment(worktree, cx)
} }
pub(crate) fn get_worktree_environment( pub fn get_worktree_environment(
&mut self, &mut self,
worktree: Entity<Worktree>, worktree: Entity<Worktree>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
@ -118,7 +118,7 @@ impl ProjectEnvironment {
/// If the project was opened from the CLI, then the inherited CLI environment is returned. /// If the project was opened from the CLI, then the inherited CLI environment is returned.
/// If it wasn't opened from the CLI, and an absolute path is given, then a shell is spawned in /// If it wasn't opened from the CLI, and an absolute path is given, then a shell is spawned in
/// that directory, to get environment variables as if the user has `cd`'d there. /// that directory, to get environment variables as if the user has `cd`'d there.
pub(crate) fn get_directory_environment( pub fn get_directory_environment(
&mut self, &mut self,
abs_path: Arc<Path>, abs_path: Arc<Path>,
cx: &mut Context<Self>, cx: &mut Context<Self>,

View file

@ -39,7 +39,7 @@ pub trait StyledExt: Styled + Sized {
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
/// ///
/// Example Elements: Title Bar, Panel, Tab Bar, Editor /// Example Elements: Title Bar, Panel, Tab Bar, Editor
fn elevation_1(self, cx: &mut App) -> Self { fn elevation_1(self, cx: &App) -> Self {
elevated(self, cx, ElevationIndex::Surface) elevated(self, cx, ElevationIndex::Surface)
} }

View file

@ -1,13 +1,11 @@
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use editor::test::editor_lsp_test_context::EditorLspTestContext; use editor::test::editor_lsp_test_context::EditorLspTestContext;
use gpui::{Context, Entity, SemanticVersion, UpdateGlobal, actions}; use gpui::{Context, Entity, SemanticVersion, UpdateGlobal};
use search::{BufferSearchBar, project_search::ProjectSearchBar}; use search::{BufferSearchBar, project_search::ProjectSearchBar};
use crate::{state::Operator, *}; use crate::{state::Operator, *};
actions!(agent, [Chat]);
pub struct VimTestContext { pub struct VimTestContext {
cx: EditorLspTestContext, cx: EditorLspTestContext,
} }

View file

@ -23,6 +23,7 @@ activity_indicator.workspace = true
agent.workspace = true agent.workspace = true
agent_ui.workspace = true agent_ui.workspace = true
agent_settings.workspace = true agent_settings.workspace = true
agent_servers.workspace = true
anyhow.workspace = true anyhow.workspace = true
askpass.workspace = true askpass.workspace = true
assets.workspace = true assets.workspace = true

View file

@ -520,6 +520,7 @@ pub fn main() {
supermaven::init(app_state.client.clone(), cx); supermaven::init(app_state.client.clone(), cx);
language_model::init(app_state.client.clone(), cx); language_model::init(app_state.client.clone(), cx);
language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx); language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);
agent_servers::init(cx);
web_search::init(cx); web_search::init(cx);
web_search_providers::init(app_state.client.clone(), cx); web_search_providers::init(app_state.client.clone(), cx);
snippet_provider::init(cx); snippet_provider::init(cx);

View file

@ -268,7 +268,13 @@ pub mod agent {
/// Opens the agent onboarding modal. /// Opens the agent onboarding modal.
OpenOnboardingModal, OpenOnboardingModal,
/// Resets the agent onboarding state. /// Resets the agent onboarding state.
ResetOnboarding ResetOnboarding,
/// Starts a chat conversation with the agent.
Chat,
/// Displays the previous message in the history.
PreviousHistoryMessage,
/// Displays the next message in the history.
NextHistoryMessage
] ]
); );
} }

View file

@ -107,6 +107,7 @@ rustc-hash = { version = "1" }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["fs", "net", "std"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["fs", "net", "std"] }
rustls = { version = "0.23", features = ["ring"] } rustls = { version = "0.23", features = ["ring"] }
rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] } rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] }
schemars = { version = "1", features = ["chrono04", "indexmap2"] }
sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] } sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] }
sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] } sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] }
semver = { version = "1", features = ["serde"] } semver = { version = "1", features = ["serde"] }
@ -239,6 +240,7 @@ rustc-hash = { version = "1" }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["fs", "net", "std"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["fs", "net", "std"] }
rustls = { version = "0.23", features = ["ring"] } rustls = { version = "0.23", features = ["ring"] }
rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] } rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] }
schemars = { version = "1", features = ["chrono04", "indexmap2"] }
sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] } sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] }
sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] } sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] }
semver = { version = "1", features = ["serde"] } semver = { version = "1", features = ["serde"] }