Compare commits
76 Commits
codex
...
acp-thinki
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab8c9b7edc | ||
|
|
fb1b761021 | ||
|
|
733e5dd8f2 | ||
|
|
fdc365ecf1 | ||
|
|
0770ae2612 | ||
|
|
a1ad858fb6 | ||
|
|
ea197d4e6d | ||
|
|
1ab78a7544 | ||
|
|
df3d956e63 | ||
|
|
4e3f66c5c2 | ||
|
|
d6c76d8d33 | ||
|
|
f1613afb22 | ||
|
|
405f7cf64f | ||
|
|
73ac553316 | ||
|
|
136423da94 | ||
|
|
28baedd935 | ||
|
|
756358b9c7 | ||
|
|
54040188bb | ||
|
|
4755d6fa9d | ||
|
|
135143d51b | ||
|
|
450604b4a1 | ||
|
|
348bc52a3f | ||
|
|
d16c595d57 | ||
|
|
975a7e6f7f | ||
|
|
7d2f7cb70e | ||
|
|
5f9afdf7ba | ||
|
|
7a3105b0c6 | ||
|
|
ab0b16939d | ||
|
|
28d992487d | ||
|
|
fde15a5a68 | ||
|
|
780db30e0b | ||
|
|
7c992adfe1 | ||
|
|
825aecfd28 | ||
|
|
f2f32fb3bd | ||
|
|
d9fd8d5eee | ||
|
|
8137b3318f | ||
|
|
3ceeefe460 | ||
|
|
6f768aefa2 | ||
|
|
28ac84ed01 | ||
|
|
4d803fa628 | ||
|
|
17b2dd9a93 | ||
|
|
7abf635e20 | ||
|
|
92adcb6e63 | ||
|
|
5ed001e0df | ||
|
|
f12fffd1ba | ||
|
|
991ba08711 | ||
|
|
c728731099 | ||
|
|
ddab1cbd71 | ||
|
|
f383a7626f | ||
|
|
ee1df65569 | ||
|
|
3be45822be | ||
|
|
3b6f30a6fd | ||
|
|
779a68f868 | ||
|
|
79c37284e0 | ||
|
|
0a053cf55d | ||
|
|
fc59d9cbf3 | ||
|
|
678a42e920 | ||
|
|
75bcaf743c | ||
|
|
47c875f6b5 | ||
|
|
81b4d7e35a | ||
|
|
33ee0c3093 | ||
|
|
d68f86052f | ||
|
|
a74ffd9ee4 | ||
|
|
8b9ad1cfae | ||
|
|
adbccb1ad0 | ||
|
|
f4e2d38c29 | ||
|
|
5f10be7791 | ||
|
|
d47a920c05 | ||
|
|
24b72be154 | ||
|
|
de779a45ce | ||
|
|
b094a636cf | ||
|
|
318709b60d | ||
|
|
f1bd531a32 | ||
|
|
549eb4d826 | ||
|
|
c1e53b7fa5 | ||
|
|
ec376e0b61 |
49
Cargo.lock
generated
49
Cargo.lock
generated
@@ -2,6 +2,38 @@
|
|||||||
# 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 = [
|
||||||
|
"agentic-coding-protocol",
|
||||||
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
|
"base64 0.22.1",
|
||||||
|
"buffer_diff",
|
||||||
|
"chrono",
|
||||||
|
"collections",
|
||||||
|
"editor",
|
||||||
|
"env_logger 0.11.8",
|
||||||
|
"futures 0.3.31",
|
||||||
|
"gpui",
|
||||||
|
"language",
|
||||||
|
"log",
|
||||||
|
"markdown",
|
||||||
|
"parking_lot",
|
||||||
|
"project",
|
||||||
|
"proto",
|
||||||
|
"serde_json",
|
||||||
|
"settings",
|
||||||
|
"smol",
|
||||||
|
"theme",
|
||||||
|
"ui",
|
||||||
|
"util",
|
||||||
|
"uuid",
|
||||||
|
"workspace-hack",
|
||||||
|
"zed_actions",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "activity_indicator"
|
name = "activity_indicator"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -130,6 +162,7 @@ dependencies = [
|
|||||||
name = "agent_ui"
|
name = "agent_ui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"acp",
|
||||||
"agent",
|
"agent",
|
||||||
"agent_settings",
|
"agent_settings",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -212,6 +245,21 @@ dependencies = [
|
|||||||
"zed_llm_client",
|
"zed_llm_client",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "agentic-coding-protocol"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
|
"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"
|
||||||
@@ -14056,6 +14104,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",
|
||||||
|
|||||||
11
Cargo.toml
11
Cargo.toml
@@ -2,6 +2,7 @@
|
|||||||
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",
|
||||||
@@ -215,8 +216,9 @@ 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" }
|
||||||
ai = { path = "crates/ai" }
|
ai = { path = "crates/ai" }
|
||||||
@@ -398,6 +400,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
|||||||
# External crates
|
# External crates
|
||||||
#
|
#
|
||||||
|
|
||||||
|
agentic-coding-protocol = { path = "../agentic-coding-protocol" }
|
||||||
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"
|
||||||
@@ -480,7 +483,7 @@ json_dotpath = "1.1"
|
|||||||
jsonschema = "0.30.0"
|
jsonschema = "0.30.0"
|
||||||
jsonwebtoken = "9.3"
|
jsonwebtoken = "9.3"
|
||||||
jupyter-protocol = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
jupyter-protocol = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||||
jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed" ,rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
|
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
|
||||||
linkify = "0.10.0"
|
linkify = "0.10.0"
|
||||||
@@ -491,7 +494,7 @@ metal = "0.29"
|
|||||||
moka = { version = "0.12.10", features = ["sync"] }
|
moka = { version = "0.12.10", features = ["sync"] }
|
||||||
naga = { version = "25.0", features = ["wgsl-in"] }
|
naga = { version = "25.0", features = ["wgsl-in"] }
|
||||||
nanoid = "0.4"
|
nanoid = "0.4"
|
||||||
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||||
nix = "0.29"
|
nix = "0.29"
|
||||||
num-format = "0.4.4"
|
num-format = "0.4.4"
|
||||||
objc = "0.2"
|
objc = "0.2"
|
||||||
@@ -531,7 +534,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77
|
|||||||
"stream",
|
"stream",
|
||||||
] }
|
] }
|
||||||
rsa = "0.9.6"
|
rsa = "0.9.6"
|
||||||
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
|
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
|
||||||
"async-dispatcher-runtime",
|
"async-dispatcher-runtime",
|
||||||
] }
|
] }
|
||||||
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||||
|
|||||||
50
crates/acp/Cargo.toml
Normal file
50
crates/acp/Cargo.toml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
[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"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
agentic-coding-protocol = { path = "../../../agentic-coding-protocol" }
|
||||||
|
anyhow.workspace = true
|
||||||
|
async-trait.workspace = true
|
||||||
|
base64.workspace = true
|
||||||
|
buffer_diff.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
collections.workspace = true
|
||||||
|
editor.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
gpui.workspace = true
|
||||||
|
language.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
markdown.workspace = true
|
||||||
|
parking_lot.workspace = true
|
||||||
|
project.workspace = true
|
||||||
|
proto.workspace = true
|
||||||
|
settings.workspace = true
|
||||||
|
smol.workspace = true
|
||||||
|
theme.workspace = true
|
||||||
|
ui.workspace = true
|
||||||
|
util.workspace = true
|
||||||
|
uuid.workspace = true
|
||||||
|
workspace-hack.workspace = true
|
||||||
|
zed_actions.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
env_logger.workspace = true
|
||||||
|
gpui = { workspace = true, "features" = ["test-support"] }
|
||||||
|
project = { workspace = true, "features" = ["test-support"] }
|
||||||
|
serde_json.workspace = true
|
||||||
|
util.workspace = true
|
||||||
|
settings.workspace = true
|
||||||
1
crates/acp/LICENSE-GPL
Symbolic link
1
crates/acp/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../LICENSE-GPL
|
||||||
893
crates/acp/src/acp.rs
Normal file
893
crates/acp/src/acp.rs
Normal file
@@ -0,0 +1,893 @@
|
|||||||
|
mod server;
|
||||||
|
mod thread_view;
|
||||||
|
|
||||||
|
use agentic_coding_protocol::{self as acp};
|
||||||
|
use anyhow::{Context as _, Result};
|
||||||
|
use buffer_diff::BufferDiff;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use editor::{MultiBuffer, PathKey};
|
||||||
|
use futures::channel::oneshot;
|
||||||
|
use gpui::{AppContext, Context, Entity, EventEmitter, SharedString, Task};
|
||||||
|
use language::{Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _};
|
||||||
|
use markdown::Markdown;
|
||||||
|
use project::Project;
|
||||||
|
use std::{mem, ops::Range, path::PathBuf, sync::Arc};
|
||||||
|
use ui::{App, IconName};
|
||||||
|
use util::{ResultExt, debug_panic};
|
||||||
|
|
||||||
|
pub use server::AcpServer;
|
||||||
|
pub use thread_view::AcpThreadView;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ThreadId(SharedString);
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct FileVersion(u64);
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AgentThreadSummary {
|
||||||
|
pub id: ThreadId,
|
||||||
|
pub title: String,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct FileContent {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub version: FileVersion,
|
||||||
|
pub content: SharedString,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct UserMessage {
|
||||||
|
pub chunks: Vec<UserMessageChunk>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserMessage {
|
||||||
|
fn into_acp(self, cx: &App) -> acp::UserMessage {
|
||||||
|
acp::UserMessage {
|
||||||
|
chunks: self
|
||||||
|
.chunks
|
||||||
|
.into_iter()
|
||||||
|
.map(|chunk| chunk.into_acp(cx))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum UserMessageChunk {
|
||||||
|
Text {
|
||||||
|
chunk: Entity<Markdown>,
|
||||||
|
},
|
||||||
|
File {
|
||||||
|
content: FileContent,
|
||||||
|
},
|
||||||
|
Directory {
|
||||||
|
path: PathBuf,
|
||||||
|
contents: Vec<FileContent>,
|
||||||
|
},
|
||||||
|
Symbol {
|
||||||
|
path: PathBuf,
|
||||||
|
range: Range<u64>,
|
||||||
|
version: FileVersion,
|
||||||
|
name: SharedString,
|
||||||
|
content: SharedString,
|
||||||
|
},
|
||||||
|
Fetch {
|
||||||
|
url: SharedString,
|
||||||
|
content: SharedString,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserMessageChunk {
|
||||||
|
pub fn into_acp(self, cx: &App) -> acp::UserMessageChunk {
|
||||||
|
match self {
|
||||||
|
Self::Text { chunk } => acp::UserMessageChunk::Text {
|
||||||
|
chunk: chunk.read(cx).source().to_string(),
|
||||||
|
},
|
||||||
|
Self::File { .. } => todo!(),
|
||||||
|
Self::Directory { .. } => todo!(),
|
||||||
|
Self::Symbol { .. } => todo!(),
|
||||||
|
Self::Fetch { .. } => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_str(chunk: &str, language_registry: Arc<LanguageRegistry>, cx: &mut App) -> Self {
|
||||||
|
Self::Text {
|
||||||
|
chunk: cx.new(|cx| {
|
||||||
|
Markdown::new(chunk.to_owned().into(), Some(language_registry), None, cx)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct AssistantMessage {
|
||||||
|
pub chunks: Vec<AssistantMessageChunk>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum AssistantMessageChunk {
|
||||||
|
Text { chunk: Entity<Markdown> },
|
||||||
|
Thought { chunk: Entity<Markdown> },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssistantMessageChunk {
|
||||||
|
pub fn from_acp(
|
||||||
|
chunk: acp::AssistantMessageChunk,
|
||||||
|
language_registry: Arc<LanguageRegistry>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Self {
|
||||||
|
match chunk {
|
||||||
|
acp::AssistantMessageChunk::Text { chunk } => Self::Text {
|
||||||
|
chunk: cx.new(|cx| Markdown::new(chunk.into(), Some(language_registry), None, cx)),
|
||||||
|
},
|
||||||
|
acp::AssistantMessageChunk::Thought { chunk } => Self::Thought {
|
||||||
|
chunk: cx.new(|cx| Markdown::new(chunk.into(), Some(language_registry), None, cx)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_str(chunk: &str, language_registry: Arc<LanguageRegistry>, cx: &mut App) -> Self {
|
||||||
|
Self::Text {
|
||||||
|
chunk: cx.new(|cx| {
|
||||||
|
Markdown::new(chunk.to_owned().into(), Some(language_registry), None, cx)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum AgentThreadEntryContent {
|
||||||
|
UserMessage(UserMessage),
|
||||||
|
AssistantMessage(AssistantMessage),
|
||||||
|
ToolCall(ToolCall),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ToolCall {
|
||||||
|
id: ToolCallId,
|
||||||
|
label: Entity<Markdown>,
|
||||||
|
icon: IconName,
|
||||||
|
content: Option<ToolCallContent>,
|
||||||
|
status: ToolCallStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ToolCallStatus {
|
||||||
|
WaitingForConfirmation {
|
||||||
|
confirmation: ToolCallConfirmation,
|
||||||
|
respond_tx: oneshot::Sender<acp::ToolCallConfirmationOutcome>,
|
||||||
|
},
|
||||||
|
Allowed {
|
||||||
|
status: acp::ToolCallStatus,
|
||||||
|
},
|
||||||
|
Rejected,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ToolCallConfirmation {
|
||||||
|
Edit {
|
||||||
|
description: Option<Entity<Markdown>>,
|
||||||
|
},
|
||||||
|
Execute {
|
||||||
|
command: String,
|
||||||
|
root_command: String,
|
||||||
|
description: Option<Entity<Markdown>>,
|
||||||
|
},
|
||||||
|
Mcp {
|
||||||
|
server_name: String,
|
||||||
|
tool_name: String,
|
||||||
|
tool_display_name: String,
|
||||||
|
description: Option<Entity<Markdown>>,
|
||||||
|
},
|
||||||
|
Fetch {
|
||||||
|
urls: Vec<String>,
|
||||||
|
description: Option<Entity<Markdown>>,
|
||||||
|
},
|
||||||
|
Other {
|
||||||
|
description: Entity<Markdown>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToolCallConfirmation {
|
||||||
|
pub fn from_acp(
|
||||||
|
confirmation: acp::ToolCallConfirmation,
|
||||||
|
language_registry: Arc<LanguageRegistry>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Self {
|
||||||
|
let to_md = |description: String, cx: &mut App| -> Entity<Markdown> {
|
||||||
|
cx.new(|cx| {
|
||||||
|
Markdown::new(
|
||||||
|
description.into(),
|
||||||
|
Some(language_registry.clone()),
|
||||||
|
None,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
match confirmation {
|
||||||
|
acp::ToolCallConfirmation::Edit { description } => Self::Edit {
|
||||||
|
description: description.map(|description| to_md(description, cx)),
|
||||||
|
},
|
||||||
|
acp::ToolCallConfirmation::Execute {
|
||||||
|
command,
|
||||||
|
root_command,
|
||||||
|
description,
|
||||||
|
} => Self::Execute {
|
||||||
|
command,
|
||||||
|
root_command,
|
||||||
|
description: description.map(|description| to_md(description, cx)),
|
||||||
|
},
|
||||||
|
acp::ToolCallConfirmation::Mcp {
|
||||||
|
server_name,
|
||||||
|
tool_name,
|
||||||
|
tool_display_name,
|
||||||
|
description,
|
||||||
|
} => Self::Mcp {
|
||||||
|
server_name,
|
||||||
|
tool_name,
|
||||||
|
tool_display_name,
|
||||||
|
description: description.map(|description| to_md(description, cx)),
|
||||||
|
},
|
||||||
|
acp::ToolCallConfirmation::Fetch { urls, description } => Self::Fetch {
|
||||||
|
urls,
|
||||||
|
description: description.map(|description| to_md(description, cx)),
|
||||||
|
},
|
||||||
|
acp::ToolCallConfirmation::Other { description } => Self::Other {
|
||||||
|
description: to_md(description, cx),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ToolCallContent {
|
||||||
|
Markdown { markdown: Entity<Markdown> },
|
||||||
|
Diff { diff: Diff },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToolCallContent {
|
||||||
|
pub fn from_acp(
|
||||||
|
content: acp::ToolCallContent,
|
||||||
|
language_registry: Arc<LanguageRegistry>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Self {
|
||||||
|
match content {
|
||||||
|
acp::ToolCallContent::Markdown { markdown } => Self::Markdown {
|
||||||
|
markdown: cx.new(|cx| Markdown::new_text(markdown.into(), cx)),
|
||||||
|
},
|
||||||
|
acp::ToolCallContent::Diff { diff } => Self::Diff {
|
||||||
|
diff: Diff::from_acp(diff, language_registry, cx),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Diff {
|
||||||
|
multibuffer: Entity<MultiBuffer>,
|
||||||
|
path: PathBuf,
|
||||||
|
_task: Task<Result<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Diff {
|
||||||
|
pub fn from_acp(
|
||||||
|
diff: acp::Diff,
|
||||||
|
language_registry: Arc<LanguageRegistry>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Self {
|
||||||
|
let acp::Diff {
|
||||||
|
path,
|
||||||
|
old_text,
|
||||||
|
new_text,
|
||||||
|
} = diff;
|
||||||
|
|
||||||
|
let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly));
|
||||||
|
|
||||||
|
let new_buffer = cx.new(|cx| Buffer::local(new_text, cx));
|
||||||
|
let old_buffer = cx.new(|cx| Buffer::local(old_text.unwrap_or("".into()), cx));
|
||||||
|
let new_buffer_snapshot = new_buffer.read(cx).text_snapshot();
|
||||||
|
let old_buffer_snapshot = old_buffer.read(cx).snapshot();
|
||||||
|
let buffer_diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot, cx));
|
||||||
|
let diff_task = buffer_diff.update(cx, |diff, cx| {
|
||||||
|
diff.set_base_text(
|
||||||
|
old_buffer_snapshot,
|
||||||
|
Some(language_registry.clone()),
|
||||||
|
new_buffer_snapshot,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let task = cx.spawn({
|
||||||
|
let multibuffer = multibuffer.clone();
|
||||||
|
let path = path.clone();
|
||||||
|
async move |cx| {
|
||||||
|
diff_task.await?;
|
||||||
|
|
||||||
|
multibuffer
|
||||||
|
.update(cx, |multibuffer, cx| {
|
||||||
|
let hunk_ranges = {
|
||||||
|
let buffer = new_buffer.read(cx);
|
||||||
|
let diff = buffer_diff.read(cx);
|
||||||
|
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, cx)
|
||||||
|
.map(|diff_hunk| diff_hunk.buffer_range.to_point(&buffer))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
multibuffer.set_excerpts_for_path(
|
||||||
|
PathKey::for_buffer(&new_buffer, cx),
|
||||||
|
new_buffer.clone(),
|
||||||
|
hunk_ranges,
|
||||||
|
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
multibuffer.add_diff(buffer_diff.clone(), cx);
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
|
||||||
|
if let Some(language) = language_registry
|
||||||
|
.language_for_file_path(&path)
|
||||||
|
.await
|
||||||
|
.log_err()
|
||||||
|
{
|
||||||
|
new_buffer.update(cx, |buffer, cx| buffer.set_language(Some(language), cx))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
multibuffer,
|
||||||
|
path,
|
||||||
|
_task: task,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `ThreadEntryId` that is known to be a ToolCall
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
pub struct ToolCallId(ThreadEntryId);
|
||||||
|
|
||||||
|
impl ToolCallId {
|
||||||
|
pub fn as_u64(&self) -> u64 {
|
||||||
|
self.0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
pub struct ThreadEntryId(pub u64);
|
||||||
|
|
||||||
|
impl ThreadEntryId {
|
||||||
|
pub fn post_inc(&mut self) -> Self {
|
||||||
|
let id = *self;
|
||||||
|
self.0 += 1;
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ThreadEntry {
|
||||||
|
pub id: ThreadEntryId,
|
||||||
|
pub content: AgentThreadEntryContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AcpThread {
|
||||||
|
id: ThreadId,
|
||||||
|
next_entry_id: ThreadEntryId,
|
||||||
|
entries: Vec<ThreadEntry>,
|
||||||
|
server: Arc<AcpServer>,
|
||||||
|
title: SharedString,
|
||||||
|
project: Entity<Project>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AcpThreadEvent {
|
||||||
|
NewEntry,
|
||||||
|
EntryUpdated(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<AcpThreadEvent> for AcpThread {}
|
||||||
|
|
||||||
|
impl AcpThread {
|
||||||
|
pub fn new(
|
||||||
|
server: Arc<AcpServer>,
|
||||||
|
thread_id: ThreadId,
|
||||||
|
entries: Vec<AgentThreadEntryContent>,
|
||||||
|
project: Entity<Project>,
|
||||||
|
_: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let mut next_entry_id = ThreadEntryId(0);
|
||||||
|
Self {
|
||||||
|
title: "A new agent2 thread".into(),
|
||||||
|
entries: entries
|
||||||
|
.into_iter()
|
||||||
|
.map(|entry| ThreadEntry {
|
||||||
|
id: next_entry_id.post_inc(),
|
||||||
|
content: entry,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
server,
|
||||||
|
id: thread_id,
|
||||||
|
next_entry_id,
|
||||||
|
project,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title(&self) -> SharedString {
|
||||||
|
self.title.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn entries(&self) -> &[ThreadEntry] {
|
||||||
|
&self.entries
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_entry(
|
||||||
|
&mut self,
|
||||||
|
entry: AgentThreadEntryContent,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> ThreadEntryId {
|
||||||
|
let id = self.next_entry_id.post_inc();
|
||||||
|
self.entries.push(ThreadEntry { id, content: entry });
|
||||||
|
cx.emit(AcpThreadEvent::NewEntry);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_assistant_chunk(
|
||||||
|
&mut self,
|
||||||
|
chunk: acp::AssistantMessageChunk,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let entries_len = self.entries.len();
|
||||||
|
if let Some(last_entry) = self.entries.last_mut()
|
||||||
|
&& let AgentThreadEntryContent::AssistantMessage(AssistantMessage { ref mut chunks }) =
|
||||||
|
last_entry.content
|
||||||
|
{
|
||||||
|
cx.emit(AcpThreadEvent::EntryUpdated(entries_len - 1));
|
||||||
|
|
||||||
|
match (chunks.last_mut(), &chunk) {
|
||||||
|
(
|
||||||
|
Some(AssistantMessageChunk::Text { chunk: old_chunk }),
|
||||||
|
acp::AssistantMessageChunk::Text { chunk: new_chunk },
|
||||||
|
)
|
||||||
|
| (
|
||||||
|
Some(AssistantMessageChunk::Thought { chunk: old_chunk }),
|
||||||
|
acp::AssistantMessageChunk::Thought { chunk: new_chunk },
|
||||||
|
) => {
|
||||||
|
old_chunk.update(cx, |old_chunk, cx| {
|
||||||
|
old_chunk.append(&new_chunk, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
chunks.push(AssistantMessageChunk::from_acp(
|
||||||
|
chunk,
|
||||||
|
self.project.read(cx).languages().clone(),
|
||||||
|
cx,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let chunk = AssistantMessageChunk::from_acp(
|
||||||
|
chunk,
|
||||||
|
self.project.read(cx).languages().clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.push_entry(
|
||||||
|
AgentThreadEntryContent::AssistantMessage(AssistantMessage {
|
||||||
|
chunks: vec![chunk],
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_tool_call(
|
||||||
|
&mut self,
|
||||||
|
label: String,
|
||||||
|
icon: acp::Icon,
|
||||||
|
content: Option<acp::ToolCallContent>,
|
||||||
|
confirmation: acp::ToolCallConfirmation,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> ToolCallRequest {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
|
||||||
|
let status = ToolCallStatus::WaitingForConfirmation {
|
||||||
|
confirmation: ToolCallConfirmation::from_acp(
|
||||||
|
confirmation,
|
||||||
|
self.project.read(cx).languages().clone(),
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
respond_tx: tx,
|
||||||
|
};
|
||||||
|
|
||||||
|
let id = self.insert_tool_call(label, status, icon, content, cx);
|
||||||
|
ToolCallRequest { id, outcome: rx }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_tool_call(
|
||||||
|
&mut self,
|
||||||
|
label: String,
|
||||||
|
icon: acp::Icon,
|
||||||
|
content: Option<acp::ToolCallContent>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> ToolCallId {
|
||||||
|
let status = ToolCallStatus::Allowed {
|
||||||
|
status: acp::ToolCallStatus::Running,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.insert_tool_call(label, status, icon, content, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_tool_call(
|
||||||
|
&mut self,
|
||||||
|
label: String,
|
||||||
|
status: ToolCallStatus,
|
||||||
|
icon: acp::Icon,
|
||||||
|
content: Option<acp::ToolCallContent>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> ToolCallId {
|
||||||
|
let language_registry = self.project.read(cx).languages().clone();
|
||||||
|
|
||||||
|
let entry_id = self.push_entry(
|
||||||
|
AgentThreadEntryContent::ToolCall(ToolCall {
|
||||||
|
// todo! clean up id creation
|
||||||
|
id: ToolCallId(ThreadEntryId(self.entries.len() as u64)),
|
||||||
|
label: cx.new(|cx| {
|
||||||
|
Markdown::new(label.into(), Some(language_registry.clone()), None, cx)
|
||||||
|
}),
|
||||||
|
icon: acp_icon_to_ui_icon(icon),
|
||||||
|
content: content
|
||||||
|
.map(|content| ToolCallContent::from_acp(content, language_registry, cx)),
|
||||||
|
status,
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
ToolCallId(entry_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn authorize_tool_call(
|
||||||
|
&mut self,
|
||||||
|
id: ToolCallId,
|
||||||
|
outcome: acp::ToolCallConfirmationOutcome,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let Some(entry) = self.entry_mut(id.0) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let AgentThreadEntryContent::ToolCall(call) = &mut entry.content else {
|
||||||
|
debug_panic!("expected ToolCall");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_status = if outcome == acp::ToolCallConfirmationOutcome::Reject {
|
||||||
|
ToolCallStatus::Rejected
|
||||||
|
} else {
|
||||||
|
ToolCallStatus::Allowed {
|
||||||
|
status: acp::ToolCallStatus::Running,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let curr_status = mem::replace(&mut call.status, new_status);
|
||||||
|
|
||||||
|
if let ToolCallStatus::WaitingForConfirmation { respond_tx, .. } = curr_status {
|
||||||
|
respond_tx.send(outcome).log_err();
|
||||||
|
} else {
|
||||||
|
debug_panic!("tried to authorize an already authorized tool call");
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.emit(AcpThreadEvent::EntryUpdated(id.as_u64() as usize));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_tool_call(
|
||||||
|
&mut self,
|
||||||
|
id: ToolCallId,
|
||||||
|
new_status: acp::ToolCallStatus,
|
||||||
|
new_content: Option<acp::ToolCallContent>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let language_registry = self.project.read(cx).languages().clone();
|
||||||
|
let entry = self.entry_mut(id.0).context("Entry not found")?;
|
||||||
|
|
||||||
|
match &mut entry.content {
|
||||||
|
AgentThreadEntryContent::ToolCall(call) => {
|
||||||
|
call.content = new_content.map(|new_content| {
|
||||||
|
ToolCallContent::from_acp(new_content, language_registry, cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
match &mut call.status {
|
||||||
|
ToolCallStatus::Allowed { status } => {
|
||||||
|
*status = new_status;
|
||||||
|
}
|
||||||
|
ToolCallStatus::WaitingForConfirmation { .. } => {
|
||||||
|
anyhow::bail!("Tool call hasn't been authorized yet")
|
||||||
|
}
|
||||||
|
ToolCallStatus::Rejected => {
|
||||||
|
anyhow::bail!("Tool call was rejected and therefore can't be updated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => anyhow::bail!("Entry is not a tool call"),
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.emit(AcpThreadEvent::EntryUpdated(id.as_u64() as usize));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entry_mut(&mut self, id: ThreadEntryId) -> Option<&mut ThreadEntry> {
|
||||||
|
let entry = self.entries.get_mut(id.0 as usize);
|
||||||
|
debug_assert!(
|
||||||
|
entry.is_some(),
|
||||||
|
"We shouldn't give out ids to entries that don't exist"
|
||||||
|
);
|
||||||
|
entry
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the last turn is awaiting tool authorization
|
||||||
|
pub fn waiting_for_tool_confirmation(&self) -> bool {
|
||||||
|
for entry in self.entries.iter().rev() {
|
||||||
|
match &entry.content {
|
||||||
|
AgentThreadEntryContent::ToolCall(call) => match call.status {
|
||||||
|
ToolCallStatus::WaitingForConfirmation { .. } => return true,
|
||||||
|
ToolCallStatus::Allowed { .. } | ToolCallStatus::Rejected => continue,
|
||||||
|
},
|
||||||
|
AgentThreadEntryContent::UserMessage(_)
|
||||||
|
| AgentThreadEntryContent::AssistantMessage(_) => {
|
||||||
|
// Reached the beginning of the turn
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(&mut self, message: &str, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||||
|
let agent = self.server.clone();
|
||||||
|
let id = self.id.clone();
|
||||||
|
let chunk =
|
||||||
|
UserMessageChunk::from_str(message, self.project.read(cx).languages().clone(), cx);
|
||||||
|
let message = UserMessage {
|
||||||
|
chunks: vec![chunk],
|
||||||
|
};
|
||||||
|
self.push_entry(AgentThreadEntryContent::UserMessage(message.clone()), cx);
|
||||||
|
let acp_message = message.into_acp(cx);
|
||||||
|
cx.spawn(async move |_, cx| {
|
||||||
|
agent.send_message(id, acp_message, cx).await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn acp_icon_to_ui_icon(icon: acp::Icon) -> IconName {
|
||||||
|
match icon {
|
||||||
|
acp::Icon::FileSearch => IconName::FileSearch,
|
||||||
|
acp::Icon::Folder => IconName::Folder,
|
||||||
|
acp::Icon::Globe => IconName::Globe,
|
||||||
|
acp::Icon::Hammer => IconName::Hammer,
|
||||||
|
acp::Icon::LightBulb => IconName::LightBulb,
|
||||||
|
acp::Icon::Pencil => IconName::Pencil,
|
||||||
|
acp::Icon::Regex => IconName::Regex,
|
||||||
|
acp::Icon::Terminal => IconName::Terminal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ToolCallRequest {
|
||||||
|
pub id: ToolCallId,
|
||||||
|
pub outcome: oneshot::Receiver<acp::ToolCallConfirmationOutcome>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use futures::{FutureExt as _, channel::mpsc, select};
|
||||||
|
use gpui::TestAppContext;
|
||||||
|
use project::FakeFs;
|
||||||
|
use serde_json::json;
|
||||||
|
use settings::SettingsStore;
|
||||||
|
use smol::stream::StreamExt as _;
|
||||||
|
use std::{env, path::Path, process::Stdio, time::Duration};
|
||||||
|
use util::path;
|
||||||
|
|
||||||
|
fn init_test(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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_gemini_basic(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
cx.executor().allow_parking();
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
let project = Project::test(fs, [], cx).await;
|
||||||
|
let server = gemini_acp_server(project.clone(), cx).await;
|
||||||
|
let thread = server.create_thread(&mut cx.to_async()).await.unwrap();
|
||||||
|
thread
|
||||||
|
.update(cx, |thread, cx| thread.send("Hello from Zed!", cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
thread.read_with(cx, |thread, _| {
|
||||||
|
assert_eq!(thread.entries.len(), 2);
|
||||||
|
assert!(matches!(
|
||||||
|
thread.entries[0].content,
|
||||||
|
AgentThreadEntryContent::UserMessage(_)
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
thread.entries[1].content,
|
||||||
|
AgentThreadEntryContent::AssistantMessage(_)
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_gemini_tool_call(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
cx.executor().allow_parking();
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
path!("/private/tmp"),
|
||||||
|
json!({"foo": "Lorem ipsum dolor", "bar": "bar", "baz": "baz"}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
|
||||||
|
let server = gemini_acp_server(project.clone(), cx).await;
|
||||||
|
let thread = server.create_thread(&mut cx.to_async()).await.unwrap();
|
||||||
|
thread
|
||||||
|
.update(cx, |thread, cx| {
|
||||||
|
thread.send(
|
||||||
|
"Read the '/private/tmp/foo' file and tell me what you see.",
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
thread.read_with(cx, |thread, _cx| {
|
||||||
|
assert!(matches!(
|
||||||
|
&thread.entries()[2].content,
|
||||||
|
AgentThreadEntryContent::ToolCall(ToolCall {
|
||||||
|
status: ToolCallStatus::Allowed { .. },
|
||||||
|
..
|
||||||
|
})
|
||||||
|
));
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
thread.entries[3].content,
|
||||||
|
AgentThreadEntryContent::AssistantMessage(_)
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_gemini_tool_call_with_confirmation(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
cx.executor().allow_parking();
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
|
||||||
|
let server = gemini_acp_server(project.clone(), cx).await;
|
||||||
|
let thread = server.create_thread(&mut cx.to_async()).await.unwrap();
|
||||||
|
let full_turn = thread.update(cx, |thread, cx| {
|
||||||
|
thread.send(r#"Run `echo "Hello, world!"`"#, cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
run_until_tool_call(&thread, cx).await;
|
||||||
|
|
||||||
|
let tool_call_id = thread.read_with(cx, |thread, _cx| {
|
||||||
|
let AgentThreadEntryContent::ToolCall(ToolCall {
|
||||||
|
id,
|
||||||
|
status:
|
||||||
|
ToolCallStatus::WaitingForConfirmation {
|
||||||
|
confirmation: ToolCallConfirmation::Execute { root_command, .. },
|
||||||
|
..
|
||||||
|
},
|
||||||
|
..
|
||||||
|
}) = &thread.entries()[2].content
|
||||||
|
else {
|
||||||
|
panic!();
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(root_command, "echo");
|
||||||
|
|
||||||
|
*id
|
||||||
|
});
|
||||||
|
|
||||||
|
thread.update(cx, |thread, cx| {
|
||||||
|
thread.authorize_tool_call(tool_call_id, acp::ToolCallConfirmationOutcome::Allow, cx);
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
&thread.entries()[2].content,
|
||||||
|
AgentThreadEntryContent::ToolCall(ToolCall {
|
||||||
|
status: ToolCallStatus::Allowed { .. },
|
||||||
|
..
|
||||||
|
})
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
full_turn.await.unwrap();
|
||||||
|
|
||||||
|
thread.read_with(cx, |thread, cx| {
|
||||||
|
let AgentThreadEntryContent::ToolCall(ToolCall {
|
||||||
|
content: Some(ToolCallContent::Markdown { markdown }),
|
||||||
|
status: ToolCallStatus::Allowed { .. },
|
||||||
|
..
|
||||||
|
}) = &thread.entries()[2].content
|
||||||
|
else {
|
||||||
|
panic!();
|
||||||
|
};
|
||||||
|
|
||||||
|
markdown.read_with(cx, |md, _cx| {
|
||||||
|
assert!(
|
||||||
|
md.source().contains("Hello, world!"),
|
||||||
|
r#"Expected '{}' to contain "Hello, world!""#,
|
||||||
|
md.source()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_until_tool_call(thread: &Entity<AcpThread>, cx: &mut TestAppContext) {
|
||||||
|
let (mut tx, mut rx) = mpsc::channel::<()>(1);
|
||||||
|
|
||||||
|
let subscription = cx.update(|cx| {
|
||||||
|
cx.subscribe(thread, move |thread, _, cx| {
|
||||||
|
if thread
|
||||||
|
.read(cx)
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.any(|e| matches!(e.content, AgentThreadEntryContent::ToolCall(_)))
|
||||||
|
{
|
||||||
|
tx.try_send(()).unwrap();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
select! {
|
||||||
|
_ = futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(10))) => {
|
||||||
|
panic!("Timeout waiting for tool call")
|
||||||
|
}
|
||||||
|
_ = rx.next().fuse() => {
|
||||||
|
drop(subscription);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn gemini_acp_server(
|
||||||
|
project: Entity<Project>,
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) -> Arc<AcpServer> {
|
||||||
|
let cli_path =
|
||||||
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../gemini-cli/packages/cli");
|
||||||
|
let mut command = util::command::new_smol_command("node");
|
||||||
|
command
|
||||||
|
.arg(cli_path)
|
||||||
|
.arg("--acp")
|
||||||
|
.current_dir("/private/tmp")
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::inherit())
|
||||||
|
.kill_on_drop(true);
|
||||||
|
|
||||||
|
if let Ok(gemini_key) = std::env::var("GEMINI_API_KEY") {
|
||||||
|
command.env("GEMINI_API_KEY", gemini_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
let child = command.spawn().unwrap();
|
||||||
|
let server = cx.update(|cx| AcpServer::stdio(child, project, cx));
|
||||||
|
server.initialize().await.unwrap();
|
||||||
|
server
|
||||||
|
}
|
||||||
|
}
|
||||||
262
crates/acp/src/server.rs
Normal file
262
crates/acp/src/server.rs
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
use crate::{AcpThread, ThreadEntryId, ThreadId, ToolCallId, ToolCallRequest};
|
||||||
|
use agentic_coding_protocol as acp;
|
||||||
|
use anyhow::{Context as _, Result};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use collections::HashMap;
|
||||||
|
use gpui::{App, AppContext, AsyncApp, Context, Entity, Task, WeakEntity};
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use project::Project;
|
||||||
|
use smol::process::Child;
|
||||||
|
use std::{process::ExitStatus, sync::Arc};
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
|
pub struct AcpServer {
|
||||||
|
connection: Arc<acp::AgentConnection>,
|
||||||
|
threads: Arc<Mutex<HashMap<ThreadId, WeakEntity<AcpThread>>>>,
|
||||||
|
project: Entity<Project>,
|
||||||
|
exit_status: Arc<Mutex<Option<ExitStatus>>>,
|
||||||
|
_handler_task: Task<()>,
|
||||||
|
_io_task: Task<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AcpClientDelegate {
|
||||||
|
project: Entity<Project>,
|
||||||
|
threads: Arc<Mutex<HashMap<ThreadId, WeakEntity<AcpThread>>>>,
|
||||||
|
cx: AsyncApp,
|
||||||
|
// sent_buffer_versions: HashMap<Entity<Buffer>, HashMap<u64, BufferSnapshot>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AcpClientDelegate {
|
||||||
|
fn new(
|
||||||
|
project: Entity<Project>,
|
||||||
|
threads: Arc<Mutex<HashMap<ThreadId, WeakEntity<AcpThread>>>>,
|
||||||
|
cx: AsyncApp,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
project,
|
||||||
|
threads,
|
||||||
|
cx: cx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_thread<R>(
|
||||||
|
&self,
|
||||||
|
thread_id: &ThreadId,
|
||||||
|
cx: &mut App,
|
||||||
|
callback: impl FnOnce(&mut AcpThread, &mut Context<AcpThread>) -> R,
|
||||||
|
) -> Option<R> {
|
||||||
|
let thread = self.threads.lock().get(&thread_id)?.clone();
|
||||||
|
let Some(thread) = thread.upgrade() else {
|
||||||
|
self.threads.lock().remove(&thread_id);
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(thread.update(cx, callback))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait(?Send)]
|
||||||
|
impl acp::Client for AcpClientDelegate {
|
||||||
|
async fn stream_assistant_message_chunk(
|
||||||
|
&self,
|
||||||
|
params: acp::StreamAssistantMessageChunkParams,
|
||||||
|
) -> Result<acp::StreamAssistantMessageChunkResponse> {
|
||||||
|
let cx = &mut self.cx.clone();
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
self.update_thread(¶ms.thread_id.into(), cx, |thread, cx| {
|
||||||
|
thread.push_assistant_chunk(params.chunk, cx)
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(acp::StreamAssistantMessageChunkResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn request_tool_call_confirmation(
|
||||||
|
&self,
|
||||||
|
request: acp::RequestToolCallConfirmationParams,
|
||||||
|
) -> Result<acp::RequestToolCallConfirmationResponse> {
|
||||||
|
let cx = &mut self.cx.clone();
|
||||||
|
let ToolCallRequest { id, outcome } = cx
|
||||||
|
.update(|cx| {
|
||||||
|
self.update_thread(&request.thread_id.into(), cx, |thread, cx| {
|
||||||
|
thread.request_tool_call(
|
||||||
|
request.label,
|
||||||
|
request.icon,
|
||||||
|
request.content,
|
||||||
|
request.confirmation,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.context("Failed to update thread")?;
|
||||||
|
|
||||||
|
Ok(acp::RequestToolCallConfirmationResponse {
|
||||||
|
id: id.into(),
|
||||||
|
outcome: outcome.await?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn push_tool_call(
|
||||||
|
&self,
|
||||||
|
request: acp::PushToolCallParams,
|
||||||
|
) -> Result<acp::PushToolCallResponse> {
|
||||||
|
let cx = &mut self.cx.clone();
|
||||||
|
let entry_id = cx
|
||||||
|
.update(|cx| {
|
||||||
|
self.update_thread(&request.thread_id.into(), cx, |thread, cx| {
|
||||||
|
thread.push_tool_call(request.label, request.icon, request.content, cx)
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.context("Failed to update thread")?;
|
||||||
|
|
||||||
|
Ok(acp::PushToolCallResponse {
|
||||||
|
id: entry_id.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_tool_call(
|
||||||
|
&self,
|
||||||
|
request: acp::UpdateToolCallParams,
|
||||||
|
) -> Result<acp::UpdateToolCallResponse> {
|
||||||
|
let cx = &mut self.cx.clone();
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
self.update_thread(&request.thread_id.into(), cx, |thread, cx| {
|
||||||
|
thread.update_tool_call(
|
||||||
|
request.tool_call_id.into(),
|
||||||
|
request.status,
|
||||||
|
request.content,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.context("Failed to update thread")??;
|
||||||
|
|
||||||
|
Ok(acp::UpdateToolCallResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AcpServer {
|
||||||
|
pub fn stdio(mut process: Child, project: Entity<Project>, cx: &mut App) -> Arc<Self> {
|
||||||
|
let stdin = process.stdin.take().expect("process didn't have stdin");
|
||||||
|
let stdout = process.stdout.take().expect("process didn't have stdout");
|
||||||
|
|
||||||
|
let threads: Arc<Mutex<HashMap<ThreadId, WeakEntity<AcpThread>>>> = Default::default();
|
||||||
|
let (connection, handler_fut, io_fut) = acp::AgentConnection::connect_to_agent(
|
||||||
|
AcpClientDelegate::new(project.clone(), threads.clone(), cx.to_async()),
|
||||||
|
stdin,
|
||||||
|
stdout,
|
||||||
|
);
|
||||||
|
|
||||||
|
let exit_status: Arc<Mutex<Option<ExitStatus>>> = Default::default();
|
||||||
|
let io_task = cx.background_spawn({
|
||||||
|
let exit_status = exit_status.clone();
|
||||||
|
async move {
|
||||||
|
io_fut.await.log_err();
|
||||||
|
let result = process.status().await.log_err();
|
||||||
|
*exit_status.lock() = result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Arc::new(Self {
|
||||||
|
project,
|
||||||
|
connection: Arc::new(connection),
|
||||||
|
threads,
|
||||||
|
exit_status,
|
||||||
|
_handler_task: cx.foreground_executor().spawn(handler_fut),
|
||||||
|
_io_task: io_task,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn initialize(&self) -> Result<acp::InitializeResponse> {
|
||||||
|
self.connection
|
||||||
|
.request(acp::InitializeParams)
|
||||||
|
.await
|
||||||
|
.map_err(to_anyhow)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn authenticate(&self) -> Result<()> {
|
||||||
|
self.connection
|
||||||
|
.request(acp::AuthenticateParams)
|
||||||
|
.await
|
||||||
|
.map_err(to_anyhow)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_thread(self: Arc<Self>, cx: &mut AsyncApp) -> Result<Entity<AcpThread>> {
|
||||||
|
let response = self
|
||||||
|
.connection
|
||||||
|
.request(acp::CreateThreadParams)
|
||||||
|
.await
|
||||||
|
.map_err(to_anyhow)?;
|
||||||
|
|
||||||
|
let thread_id: ThreadId = response.thread_id.into();
|
||||||
|
let server = self.clone();
|
||||||
|
let thread = cx.new(|_| AcpThread {
|
||||||
|
// todo!
|
||||||
|
title: "ACP Thread".into(),
|
||||||
|
id: thread_id.clone(), // Either<ErrorState, Id>
|
||||||
|
next_entry_id: ThreadEntryId(0),
|
||||||
|
entries: Vec::default(),
|
||||||
|
project: self.project.clone(),
|
||||||
|
server,
|
||||||
|
})?;
|
||||||
|
self.threads.lock().insert(thread_id, thread.downgrade());
|
||||||
|
Ok(thread)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_message(
|
||||||
|
&self,
|
||||||
|
thread_id: ThreadId,
|
||||||
|
message: acp::UserMessage,
|
||||||
|
_cx: &mut AsyncApp,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.connection
|
||||||
|
.request(acp::SendUserMessageParams {
|
||||||
|
thread_id: thread_id.clone().into(),
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(to_anyhow)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exit_status(&self) -> Option<ExitStatus> {
|
||||||
|
*self.exit_status.lock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn to_anyhow(e: acp::Error) -> anyhow::Error {
|
||||||
|
log::error!(
|
||||||
|
"failed to send message: {code}: {message}",
|
||||||
|
code = e.code,
|
||||||
|
message = e.message
|
||||||
|
);
|
||||||
|
anyhow::anyhow!(e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<acp::ThreadId> for ThreadId {
|
||||||
|
fn from(thread_id: acp::ThreadId) -> Self {
|
||||||
|
Self(thread_id.0.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ThreadId> for acp::ThreadId {
|
||||||
|
fn from(thread_id: ThreadId) -> Self {
|
||||||
|
acp::ThreadId(thread_id.0.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<acp::ToolCallId> for ToolCallId {
|
||||||
|
fn from(tool_call_id: acp::ToolCallId) -> Self {
|
||||||
|
Self(ThreadEntryId(tool_call_id.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ToolCallId> for acp::ToolCallId {
|
||||||
|
fn from(tool_call_id: ToolCallId) -> Self {
|
||||||
|
acp::ToolCallId(tool_call_id.as_u64())
|
||||||
|
}
|
||||||
|
}
|
||||||
1252
crates/acp/src/thread_view.rs
Normal file
1252
crates/acp/src/thread_view.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,12 +13,10 @@ 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
|
||||||
agent_settings.workspace = true
|
agent_settings.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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,
|
||||||
@@ -109,6 +110,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,6 +132,7 @@ 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::AcpThread { .. } => todo!(),
|
||||||
ActiveView::TextThread { .. }
|
ActiveView::TextThread { .. }
|
||||||
| ActiveView::History
|
| ActiveView::History
|
||||||
| ActiveView::Configuration => {}
|
| ActiveView::Configuration => {}
|
||||||
@@ -188,6 +196,9 @@ enum ActiveView {
|
|||||||
message_editor: Entity<MessageEditor>,
|
message_editor: Entity<MessageEditor>,
|
||||||
_subscriptions: Vec<gpui::Subscription>,
|
_subscriptions: Vec<gpui::Subscription>,
|
||||||
},
|
},
|
||||||
|
AcpThread {
|
||||||
|
thread_view: Entity<acp::AcpThreadView>,
|
||||||
|
},
|
||||||
TextThread {
|
TextThread {
|
||||||
context_editor: Entity<TextThreadEditor>,
|
context_editor: Entity<TextThreadEditor>,
|
||||||
title_editor: Entity<Editor>,
|
title_editor: Entity<Editor>,
|
||||||
@@ -207,7 +218,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 +251,9 @@ impl ActiveView {
|
|||||||
thread.scroll_to_bottom(cx);
|
thread.scroll_to_bottom(cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
ActiveView::AcpThread { .. } => {
|
||||||
|
// todo!
|
||||||
|
}
|
||||||
ActiveView::TextThread { .. }
|
ActiveView::TextThread { .. }
|
||||||
| ActiveView::History
|
| ActiveView::History
|
||||||
| ActiveView::Configuration => {}
|
| ActiveView::Configuration => {}
|
||||||
@@ -653,6 +669,9 @@ 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::AcpThread { .. } => {
|
||||||
|
// todo!
|
||||||
|
}
|
||||||
ActiveView::TextThread { .. }
|
ActiveView::TextThread { .. }
|
||||||
| ActiveView::History
|
| ActiveView::History
|
||||||
| ActiveView::Configuration => {}
|
| ActiveView::Configuration => {}
|
||||||
@@ -733,6 +752,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());
|
||||||
|
}
|
||||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
|
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -740,6 +762,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::AcpThread { .. } => {
|
||||||
|
// todo!
|
||||||
|
None
|
||||||
|
}
|
||||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None,
|
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -862,6 +888,19 @@ 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 project = self.project.clone();
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let thread_view =
|
||||||
|
cx.new_window_entity(|window, cx| acp::AcpThreadView::new(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 +1033,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(),
|
||||||
@@ -1018,6 +1058,7 @@ impl AgentPanel {
|
|||||||
pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
match self.active_view {
|
match self.active_view {
|
||||||
ActiveView::Configuration | ActiveView::History => {
|
ActiveView::Configuration | ActiveView::History => {
|
||||||
|
// todo! check go back works correctly
|
||||||
if let Some(previous_view) = self.previous_view.take() {
|
if let Some(previous_view) = self.previous_view.take() {
|
||||||
self.active_view = previous_view;
|
self.active_view = previous_view;
|
||||||
|
|
||||||
@@ -1025,6 +1066,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 { .. } => {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
ActiveView::TextThread { context_editor, .. } => {
|
ActiveView::TextThread { context_editor, .. } => {
|
||||||
context_editor.focus_handle(cx).focus(window);
|
context_editor.focus_handle(cx).focus(window);
|
||||||
}
|
}
|
||||||
@@ -1144,6 +1188,7 @@ impl AgentPanel {
|
|||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
|
ActiveView::AcpThread { .. } => todo!(),
|
||||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
|
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1197,6 +1242,9 @@ impl AgentPanel {
|
|||||||
)
|
)
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
|
ActiveView::AcpThread { .. } => {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
|
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1231,6 +1279,10 @@ impl AgentPanel {
|
|||||||
pub(crate) fn active_thread(&self, cx: &App) -> Option<Entity<Thread>> {
|
pub(crate) fn active_thread(&self, cx: &App) -> Option<Entity<Thread>> {
|
||||||
match &self.active_view {
|
match &self.active_view {
|
||||||
ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
|
ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
|
||||||
|
ActiveView::AcpThread { .. } => {
|
||||||
|
// todo!
|
||||||
|
None
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1336,6 +1388,9 @@ impl AgentPanel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ActiveView::AcpThread { .. } => {
|
||||||
|
// todo!
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1351,6 +1406,9 @@ impl AgentPanel {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
ActiveView::AcpThread { .. } => {
|
||||||
|
// todo! push history entry
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1437,6 +1495,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 +1652,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,6 +1789,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::AcpThread { .. } => {
|
||||||
|
// todo!
|
||||||
|
None
|
||||||
|
}
|
||||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None,
|
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1755,6 +1821,7 @@ 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())
|
||||||
|
.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 +1960,10 @@ impl AgentPanel {
|
|||||||
message_editor,
|
message_editor,
|
||||||
..
|
..
|
||||||
} => (thread.read(cx), message_editor.read(cx)),
|
} => (thread.read(cx), message_editor.read(cx)),
|
||||||
|
ActiveView::AcpThread { .. } => {
|
||||||
|
// todo!
|
||||||
|
return None;
|
||||||
|
}
|
||||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
|
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -2031,6 +2102,10 @@ impl AgentPanel {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ActiveView::AcpThread { .. } => {
|
||||||
|
// todo!
|
||||||
|
return false;
|
||||||
|
}
|
||||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
|
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -2615,6 +2690,10 @@ 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 { .. } => {
|
||||||
|
// todo!
|
||||||
|
return None;
|
||||||
|
}
|
||||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
|
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -2961,6 +3040,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 +3116,9 @@ impl Render for AgentPanel {
|
|||||||
});
|
});
|
||||||
this.continue_conversation(window, cx);
|
this.continue_conversation(window, cx);
|
||||||
}
|
}
|
||||||
|
ActiveView::AcpThread { .. } => {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
ActiveView::TextThread { .. }
|
ActiveView::TextThread { .. }
|
||||||
| ActiveView::History
|
| ActiveView::History
|
||||||
| ActiveView::Configuration => {}
|
| ActiveView::Configuration => {}
|
||||||
@@ -3075,6 +3160,12 @@ 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())
|
||||||
|
// todo!
|
||||||
|
// .child(h_flex().child(self.message_editor.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,
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ actions!(
|
|||||||
agent,
|
agent,
|
||||||
[
|
[
|
||||||
NewTextThread,
|
NewTextThread,
|
||||||
|
NewGeminiThread,
|
||||||
ToggleContextPicker,
|
ToggleContextPicker,
|
||||||
ToggleNavigationMenu,
|
ToggleNavigationMenu,
|
||||||
ToggleOptionsMenu,
|
ToggleOptionsMenu,
|
||||||
@@ -65,7 +66,6 @@ actions!(
|
|||||||
OpenHistory,
|
OpenHistory,
|
||||||
AddContextServer,
|
AddContextServer,
|
||||||
RemoveSelectedThread,
|
RemoveSelectedThread,
|
||||||
Chat,
|
|
||||||
ChatWithFollow,
|
ChatWithFollow,
|
||||||
CycleNextInlineAssist,
|
CycleNextInlineAssist,
|
||||||
CyclePreviousInlineAssist,
|
CyclePreviousInlineAssist,
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -205,7 +205,12 @@ pub mod agent {
|
|||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
agent,
|
agent,
|
||||||
[OpenConfiguration, OpenOnboardingModal, ResetOnboarding]
|
[
|
||||||
|
OpenConfiguration,
|
||||||
|
OpenOnboardingModal,
|
||||||
|
ResetOnboarding,
|
||||||
|
Chat
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user