Compare commits

...

64 Commits

Author SHA1 Message Date
Conrad Irwin
405f7cf64f Hack in authentication
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-07-02 16:49:37 -06:00
Conrad Irwin
73ac553316 Show errors from ACP when requests error
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-07-02 16:48:22 -06:00
Agus Zubiaga
136423da94 Remove some todo! 2025-07-02 18:43:17 -03:00
Agus Zubiaga
28baedd935 Show tool output diffs 2025-07-02 18:39:43 -03:00
Conrad Irwin
756358b9c7 Handle loading outside of a project 2025-07-02 14:16:27 -06:00
Conrad Irwin
54040188bb Show a bit of a better error if gemini cli exits
I considered dumping stderr to the screen, but for now it's useful to
see stderr when developing...
2025-07-02 14:06:00 -06:00
Agus Zubiaga
4755d6fa9d Display tool icons
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2025-07-02 13:48:57 -03:00
Agus Zubiaga
135143d51b Rename display_name to label
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-07-02 13:16:30 -03:00
Agus Zubiaga
450604b4a1 Add tool call with confirmation test 2025-07-02 12:13:20 -03:00
Agus Zubiaga
348bc52a3f Merge branch 'acp' of github.com:zed-industries/zed into acp 2025-07-02 11:33:22 -03:00
Agus Zubiaga
d16c595d57 Fix always allow, and update acp confirmation types 2025-07-02 11:31:51 -03:00
Antonio Scandurra
975a7e6f7f Fix clicking on tool confirmation buttons
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-07-02 14:54:24 +02:00
Antonio Scandurra
7d2f7cb70e Replace title with display_name for tool calls
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-07-02 14:40:16 +02:00
Ben Brandt
5f9afdf7ba Add buttons for more outcomes and handle tools that don't need
authorization

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-07-02 12:56:03 +02:00
Ben Brandt
7a3105b0c6 Wire up push_tool_call
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-07-02 12:03:35 +02:00
Ben Brandt
ab0b16939d Update tool call confirmation 2025-07-02 11:32:03 +02:00
Agus Zubiaga
28d992487d Better temporary title 2025-07-02 00:58:05 -03:00
Agus Zubiaga
fde15a5a68 Update tool calls via ACP 2025-07-02 00:47:28 -03:00
Agus Zubiaga
780db30e0b Handle waiting for tool confirmation in UI 2025-07-01 23:48:09 -03:00
Agus Zubiaga
7c992adfe1 Improve spacing even more 2025-07-01 23:35:29 -03:00
Agus Zubiaga
825aecfd28 Fix spacing and list scrolling 2025-07-01 23:27:12 -03:00
Agus Zubiaga
f2f32fb3bd Proper allow/reject UI 2025-07-01 23:13:56 -03:00
Agus Zubiaga
d9fd8d5eee Improve spacing 2025-07-01 21:50:14 -03:00
Agus Zubiaga
8137b3318f Remove ReadFile entry and test tool call 2025-07-01 21:37:31 -03:00
Agus Zubiaga
3ceeefe460 Tool authorization 2025-07-01 20:32:21 -03:00
Agus Zubiaga
6f768aefa2 Copy
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-07-01 17:15:57 -03:00
Agus Zubiaga
28ac84ed01 Jump to gemini thread view immediately
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-07-01 17:15:20 -03:00
Agus Zubiaga
4d803fa628 message markdown
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-07-01 16:57:22 -03:00
Agus Zubiaga
17b2dd9a93 Update list incrementally
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-07-01 16:13:16 -03:00
Mikayla Maki
7abf635e20 Use a list to render items
Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-07-01 11:48:03 -07:00
Antonio Scandurra
92adcb6e63 WIP 2025-07-01 19:01:02 +02:00
Antonio Scandurra
5ed001e0df Merge remote-tracking branch 'origin/main' into agent2
# Conflicts:
#	Cargo.lock
2025-07-01 18:30:08 +02:00
Antonio Scandurra
f12fffd1ba WIP 2025-07-01 18:23:21 +02:00
Agus Zubiaga
991ba08711 Stop button 2025-06-26 14:37:22 -03:00
Agus Zubiaga
c728731099 Merge last chunk 2025-06-26 14:30:59 -03:00
Agus Zubiaga
ddab1cbd71 Fix notify and margin
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-06-26 14:23:39 -03:00
Agus Zubiaga
f383a7626f Improve user message
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-06-26 14:16:30 -03:00
Agus Zubiaga
ee1df65569 Start displaying messages in new thread element
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-06-26 14:05:59 -03:00
Agus Zubiaga
3be45822be agent2 basic message editor
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-06-26 13:37:23 -03:00
Agus Zubiaga
3b6f30a6fd Add ThreadElement and render it when active
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-06-26 13:07:02 -03:00
Agus Zubiaga
779a68f868 Merge branch 'main' into agent2
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-06-26 12:50:36 -03:00
Agus Zubiaga
79c37284e0 Move ActiveThread into ActiveView::Thread
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-06-26 11:36:05 -03:00
Ben Brandt
0a053cf55d Merge branch 'main' into agent2 2025-06-26 14:36:39 +02:00
Ben Brandt
fc59d9cbf3 Clean up tests
Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-06-26 14:22:13 +02:00
Ben Brandt
678a42e920 Fix missing variant 2025-06-26 14:00:21 +02:00
Ben Brandt
75bcaf743c Put user messages into thread 2025-06-26 13:59:41 +02:00
Ben Brandt
47c875f6b5 Pass GEMINI_API_KEY to agent process if available 2025-06-26 12:25:23 +02:00
Max Brunsfeld
81b4d7e35a Start on using agent2 from agent_ui 2025-06-25 20:23:41 -07:00
Max Brunsfeld
33ee0c3093 Return an Arc from AcpAgent::stdio 2025-06-25 20:23:18 -07:00
Max Brunsfeld
d68f86052f Merge branch 'main' into agent2 2025-06-25 15:57:59 -07:00
Max Brunsfeld
a74ffd9ee4 In test, start gemini in the right directory
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-06-25 14:59:07 -07:00
Conrad Irwin
8b9ad1cfae passing roundtrip test
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-06-25 15:18:42 -06:00
Max Brunsfeld
adbccb1ad0 Get agent2 compiling
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-06-25 10:30:52 -07:00
Agus Zubiaga
f4e2d38c29 --wip-- 2025-06-25 13:54:31 -03:00
Ben Brandt
5f10be7791 Start implementing send
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-06-25 14:40:33 +02:00
Ben Brandt
d47a920c05 Implement ACP threads
The `create_thread` and `get_threads` methods are now implemented for
the ACP agent. A test is added to verify the file reading flow.
2025-06-25 13:10:43 +02:00
Ben Brandt
24b72be154 Add debug/clone to structs for testing 2025-06-25 10:11:50 +02:00
Max Brunsfeld
de779a45ce Get one test passing w/ gemini cli 2025-06-24 20:07:41 -07:00
Agus Zubiaga
b094a636cf Checkpoint: Wiring up acp crate
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com> Co-authored-by:
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Max <max@zed.dev>
2025-06-24 18:27:25 -03:00
Agus Zubiaga
318709b60d Fix typo
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-06-24 16:51:43 -03:00
Agus Zubiaga
f1bd531a32 Handle pending requests
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-06-24 16:30:29 -03:00
Ben Brandt
549eb4d826 wip: request / response in send loop
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-06-24 14:50:48 +02:00
Ben Brandt
c1e53b7fa5 wip: test
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-06-24 12:31:04 +02:00
Ben Brandt
ec376e0b61 Sketch out new Agent traits
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-06-24 12:26:40 +02:00
13 changed files with 2474 additions and 13 deletions

49
Cargo.lock generated
View File

@@ -2,6 +2,38 @@
# It is not intended for manual editing.
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]]
name = "activity_indicator"
version = "0.1.0"
@@ -130,6 +162,7 @@ dependencies = [
name = "agent_ui"
version = "0.1.0"
dependencies = [
"acp",
"agent",
"agent_settings",
"anyhow",
@@ -212,6 +245,21 @@ dependencies = [
"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]]
name = "ahash"
version = "0.7.8"
@@ -14056,6 +14104,7 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984"
dependencies = [
"chrono",
"dyn-clone",
"indexmap",
"ref-cast",

View File

@@ -2,6 +2,7 @@
resolver = "2"
members = [
"crates/activity_indicator",
"crates/acp",
"crates/agent_ui",
"crates/agent",
"crates/agent_settings",
@@ -215,8 +216,9 @@ edition = "2024"
# Workspace member crates
#
activity_indicator = { path = "crates/activity_indicator" }
acp = { path = "crates/acp" }
agent = { path = "crates/agent" }
activity_indicator = { path = "crates/activity_indicator" }
agent_ui = { path = "crates/agent_ui" }
agent_settings = { path = "crates/agent_settings" }
ai = { path = "crates/ai" }
@@ -398,6 +400,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
agentic-coding-protocol = { path = "../agentic-coding-protocol" }
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"
@@ -480,7 +483,7 @@ json_dotpath = "1.1"
jsonschema = "0.30.0"
jsonwebtoken = "9.3"
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"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
@@ -491,7 +494,7 @@ metal = "0.29"
moka = { version = "0.12.10", features = ["sync"] }
naga = { version = "25.0", features = ["wgsl-in"] }
nanoid = "0.4"
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
nix = "0.29"
num-format = "0.4.4"
objc = "0.2"
@@ -531,7 +534,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77
"stream",
] }
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",
] }
rust-embed = { version = "8.4", features = ["include-exclude"] }

50
crates/acp/Cargo.toml Normal file
View 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
View File

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

748
crates/acp/src/acp.rs Normal file
View File

@@ -0,0 +1,748 @@
mod server;
mod thread_view;
use agentic_coding_protocol::{self as acp, Role};
use anyhow::{Context as _, Result};
use buffer_diff::BufferDiff;
use chrono::{DateTime, Utc};
use editor::MultiBuffer;
use futures::channel::oneshot;
use gpui::{AppContext, Context, Entity, EventEmitter, SharedString, Task};
use language::{Buffer, LanguageRegistry};
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 Message {
pub role: acp::Role,
pub chunks: Vec<MessageChunk>,
}
impl Message {
fn into_acp(self, cx: &App) -> acp::Message {
acp::Message {
role: self.role,
chunks: self
.chunks
.into_iter()
.map(|chunk| chunk.into_acp(cx))
.collect(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum MessageChunk {
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 MessageChunk {
pub fn from_acp(
chunk: acp::MessageChunk,
language_registry: Arc<LanguageRegistry>,
cx: &mut App,
) -> Self {
match chunk {
acp::MessageChunk::Text { chunk } => MessageChunk::Text {
chunk: cx.new(|cx| Markdown::new(chunk.into(), Some(language_registry), None, cx)),
},
}
}
pub fn into_acp(self, cx: &App) -> acp::MessageChunk {
match self {
MessageChunk::Text { chunk } => acp::MessageChunk::Text {
chunk: chunk.read(cx).source().to_string(),
},
MessageChunk::File { .. } => todo!(),
MessageChunk::Directory { .. } => todo!(),
MessageChunk::Symbol { .. } => todo!(),
MessageChunk::Fetch { .. } => todo!(),
}
}
pub fn from_str(chunk: &str, language_registry: Arc<LanguageRegistry>, cx: &mut App) -> Self {
MessageChunk::Text {
chunk: cx.new(|cx| {
Markdown::new(chunk.to_owned().into(), Some(language_registry), None, cx)
}),
}
}
}
#[derive(Debug)]
pub enum AgentThreadEntryContent {
Message(Message),
ToolCall(ToolCall),
}
#[derive(Debug)]
pub struct ToolCall {
id: ToolCallId,
label: Entity<Markdown>,
icon: IconName,
status: ToolCallStatus,
}
#[derive(Debug)]
pub enum ToolCallStatus {
WaitingForConfirmation {
confirmation: acp::ToolCallConfirmation,
respond_tx: oneshot::Sender<acp::ToolCallConfirmationOutcome>,
},
Allowed {
status: acp::ToolCallStatus,
content: Option<ToolCallContent>,
},
Rejected,
}
#[derive(Debug)]
pub enum ToolCallContent {
Markdown {
markdown: Entity<Markdown>,
},
Diff {
path: PathBuf,
diff: Entity<BufferDiff>,
buffer: Entity<MultiBuffer>,
_task: Task<Result<()>>,
},
}
/// 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::MessageChunk, cx: &mut Context<Self>) {
let entries_len = self.entries.len();
if let Some(last_entry) = self.entries.last_mut()
&& let AgentThreadEntryContent::Message(Message {
ref mut chunks,
role: Role::Assistant,
}) = last_entry.content
{
cx.emit(AcpThreadEvent::EntryUpdated(entries_len - 1));
if let (
Some(MessageChunk::Text { chunk: old_chunk }),
acp::MessageChunk::Text { chunk: new_chunk },
) = (chunks.last_mut(), &chunk)
{
old_chunk.update(cx, |old_chunk, cx| {
old_chunk.append(&new_chunk, cx);
});
} else {
chunks.push(MessageChunk::from_acp(
chunk,
self.project.read(cx).languages().clone(),
cx,
));
}
return;
}
let chunk = MessageChunk::from_acp(chunk, self.project.read(cx).languages().clone(), cx);
self.push_entry(
AgentThreadEntryContent::Message(Message {
role: Role::Assistant,
chunks: vec![chunk],
}),
cx,
);
}
pub fn request_tool_call(
&mut self,
label: String,
icon: acp::Icon,
confirmation: acp::ToolCallConfirmation,
cx: &mut Context<Self>,
) -> ToolCallRequest {
let (tx, rx) = oneshot::channel();
let status = ToolCallStatus::WaitingForConfirmation {
confirmation,
respond_tx: tx,
};
let id = self.insert_tool_call(label, status, icon, cx);
ToolCallRequest { id, outcome: rx }
}
pub fn push_tool_call(
&mut self,
label: String,
icon: acp::Icon,
cx: &mut Context<Self>,
) -> ToolCallId {
let status = ToolCallStatus::Allowed {
status: acp::ToolCallStatus::Running,
content: None,
};
self.insert_tool_call(label, status, icon, cx)
}
fn insert_tool_call(
&mut self,
label: String,
status: ToolCallStatus,
icon: acp::Icon,
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),
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,
content: None,
}
};
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) => match &mut call.status {
ToolCallStatus::Allowed { content, status } => {
*content = new_content.map(|new_content| match new_content {
acp::ToolCallContent::Markdown { markdown } => ToolCallContent::Markdown {
markdown: cx.new(|cx| {
Markdown::new(
markdown.into(),
Some(language_registry.clone()),
None,
cx,
)
}),
},
acp::ToolCallContent::Diff {
path,
old_text,
new_text,
} => {
let buffer = cx.new(|cx| Buffer::local(new_text, cx));
let text_snapshot = buffer.read(cx).text_snapshot();
let buffer_diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
let multibuffer = cx.new(|cx| {
let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx);
multibuffer.add_diff(buffer_diff.clone(), cx);
multibuffer
});
ToolCallContent::Diff {
path: path.clone(),
diff: buffer_diff.clone(),
buffer: multibuffer,
_task: cx.spawn(async move |_this, cx| {
let diff_snapshot = BufferDiff::update_diff(
buffer_diff.clone(),
text_snapshot.clone(),
old_text.map(|o| o.into()),
true,
true,
None,
Some(language_registry.clone()),
cx,
)
.await?;
buffer_diff.update(cx, |diff, cx| {
diff.set_snapshot(diff_snapshot, &text_snapshot, cx)
})?;
if let Some(language) = language_registry
.language_for_file_path(&path)
.await
.log_err()
{
buffer.update(cx, |buffer, cx| {
buffer.set_language(Some(language), cx)
})?;
}
anyhow::Ok(())
}),
}
}
});
*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::Message(_) => {
// 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 = MessageChunk::from_str(message, self.project.read(cx).languages().clone(), cx);
let message = Message {
role: Role::User,
chunks: vec![chunk],
};
self.push_entry(AgentThreadEntryContent::Message(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::{AsyncApp, 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.to_async()).unwrap();
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::Message(Message {
role: Role::User,
..
})
));
assert!(matches!(
thread.entries[1].content,
AgentThreadEntryContent::Message(Message {
role: Role::Assistant,
..
})
));
});
}
#[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.to_async()).unwrap();
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()[1].content,
AgentThreadEntryContent::ToolCall(ToolCall {
status: ToolCallStatus::Allowed { .. },
..
})
));
assert!(matches!(
thread.entries[2].content,
AgentThreadEntryContent::Message(Message {
role: Role::Assistant,
..
})
));
});
}
#[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.to_async()).unwrap();
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: acp::ToolCallConfirmation::Execute { root_command, .. },
..
},
..
}) = &thread.entries()[1].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()[1].content,
AgentThreadEntryContent::ToolCall(ToolCall {
status: ToolCallStatus::Allowed { .. },
..
})
));
});
full_turn.await.unwrap();
thread.read_with(cx, |thread, cx| {
let AgentThreadEntryContent::ToolCall(ToolCall {
status:
ToolCallStatus::Allowed {
content: Some(ToolCallContent::Markdown { markdown }),
..
},
..
}) = &thread.entries()[1].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 fn gemini_acp_server(project: Entity<Project>, mut cx: AsyncApp) -> Result<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();
Ok(AcpServer::stdio(child, project, &mut cx))
}
}

363
crates/acp/src/server.rs Normal file
View File

@@ -0,0 +1,363 @@
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::{io::Write as _, path::Path, 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 stat(&self, params: acp::StatParams) -> Result<acp::StatResponse> {
let cx = &mut self.cx.clone();
self.project.update(cx, |project, cx| {
let path = project
.project_path_for_absolute_path(Path::new(&params.path), cx)
.context("Failed to get project path")?;
match project.entry_for_path(&path, cx) {
// todo! refresh entry?
None => Ok(acp::StatResponse {
exists: false,
is_directory: false,
}),
Some(entry) => Ok(acp::StatResponse {
exists: entry.is_created(),
is_directory: entry.is_dir(),
}),
}
})?
}
async fn stream_message_chunk(
&self,
params: acp::StreamMessageChunkParams,
) -> Result<acp::StreamMessageChunkResponse> {
let cx = &mut self.cx.clone();
cx.update(|cx| {
self.update_thread(&params.thread_id.into(), cx, |thread, cx| {
thread.push_assistant_chunk(params.chunk, cx)
});
})?;
Ok(acp::StreamMessageChunkResponse)
}
async fn read_text_file(
&self,
request: acp::ReadTextFileParams,
) -> Result<acp::ReadTextFileResponse> {
let cx = &mut self.cx.clone();
let buffer = self
.project
.update(cx, |project, cx| {
let path = project
.project_path_for_absolute_path(Path::new(&request.path), cx)
.context("Failed to get project path")?;
anyhow::Ok(project.open_buffer(path, cx))
})??
.await?;
buffer.update(cx, |buffer, _cx| {
let start = language::Point::new(request.line_offset.unwrap_or(0), 0);
let end = match request.line_limit {
None => buffer.max_point(),
Some(limit) => start + language::Point::new(limit + 1, 0),
};
let content: String = buffer.text_for_range(start..end).collect();
acp::ReadTextFileResponse {
content,
version: acp::FileVersion(0),
}
})
}
async fn read_binary_file(
&self,
request: acp::ReadBinaryFileParams,
) -> Result<acp::ReadBinaryFileResponse> {
let cx = &mut self.cx.clone();
let file = self
.project
.update(cx, |project, cx| {
let (worktree, path) = project
.find_worktree(Path::new(&request.path), cx)
.context("Failed to get project path")?;
let task = worktree.update(cx, |worktree, cx| worktree.load_binary_file(&path, cx));
anyhow::Ok(task)
})??
.await?;
// todo! test
let content = cx
.background_spawn(async move {
let start = request.byte_offset.unwrap_or(0) as usize;
let end = request
.byte_limit
.map(|limit| (start + limit as usize).min(file.content.len()))
.unwrap_or(file.content.len());
let range_content = &file.content[start..end];
let mut base64_content = Vec::new();
let mut base64_encoder = base64::write::EncoderWriter::new(
std::io::Cursor::new(&mut base64_content),
&base64::engine::general_purpose::STANDARD,
);
base64_encoder.write_all(range_content)?;
drop(base64_encoder);
// SAFETY: The base64 encoder should not produce non-UTF8.
unsafe { anyhow::Ok(String::from_utf8_unchecked(base64_content)) }
})
.await?;
Ok(acp::ReadBinaryFileResponse {
content,
// todo!
version: acp::FileVersion(0),
})
}
async fn glob_search(
&self,
_request: acp::GlobSearchParams,
) -> Result<acp::GlobSearchResponse> {
todo!()
}
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.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, 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::Message,
_cx: &mut AsyncApp,
) -> Result<()> {
self.connection
.request(acp::SendMessageParams {
thread_id: thread_id.clone().into(),
message,
})
.await
.map_err(to_anyhow)?;
Ok(())
}
pub fn exit_status(&self) -> Option<ExitStatus> {
self.exit_status.lock().clone()
}
}
#[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())
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -13,12 +13,10 @@ path = "src/agent_ui.rs"
doctest = false
[features]
test-support = [
"gpui/test-support",
"language/test-support",
]
test-support = ["gpui/test-support", "language/test-support"]
[dependencies]
acp.workspace = true
agent.workspace = true
agent_settings.workspace = true
anyhow.workspace = true

View File

@@ -7,6 +7,7 @@ use std::time::Duration;
use db::kvp::{Dismissable, KEY_VALUE_STORE};
use serde::{Deserialize, Serialize};
use crate::NewGeminiThread;
use crate::language_model_selector::ToggleModelSelector;
use crate::{
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));
}
})
.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| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
@@ -125,6 +132,7 @@ pub fn init(cx: &mut App) {
let thread = thread.read(cx).thread().clone();
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
}
ActiveView::AcpThread { .. } => todo!(),
ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => {}
@@ -188,6 +196,9 @@ enum ActiveView {
message_editor: Entity<MessageEditor>,
_subscriptions: Vec<gpui::Subscription>,
},
AcpThread {
thread_view: Entity<acp::AcpThreadView>,
},
TextThread {
context_editor: Entity<TextThreadEditor>,
title_editor: Entity<Editor>,
@@ -207,7 +218,9 @@ enum WhichFontSize {
impl ActiveView {
pub fn which_font_size_used(&self) -> WhichFontSize {
match self {
ActiveView::Thread { .. } | ActiveView::History => WhichFontSize::AgentFont,
ActiveView::Thread { .. } | ActiveView::AcpThread { .. } | ActiveView::History => {
WhichFontSize::AgentFont
}
ActiveView::TextThread { .. } => WhichFontSize::BufferFont,
ActiveView::Configuration => WhichFontSize::None,
}
@@ -238,6 +251,9 @@ impl ActiveView {
thread.scroll_to_bottom(cx);
});
}
ActiveView::AcpThread { .. } => {
// todo!
}
ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => {}
@@ -653,6 +669,9 @@ impl AgentPanel {
.clone()
.update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
}
ActiveView::AcpThread { .. } => {
// todo!
}
ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => {}
@@ -733,6 +752,9 @@ impl AgentPanel {
ActiveView::Thread { thread, .. } => {
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 => {}
}
}
@@ -740,6 +762,10 @@ impl AgentPanel {
fn active_message_editor(&self) -> Option<&Entity<MessageEditor>> {
match &self.active_view {
ActiveView::Thread { message_editor, .. } => Some(message_editor),
ActiveView::AcpThread { .. } => {
// todo!
None
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None,
}
}
@@ -862,6 +888,19 @@ impl AgentPanel {
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(
&mut self,
action: &OpenRulesLibrary,
@@ -994,6 +1033,7 @@ impl AgentPanel {
cx,
)
});
let message_editor = cx.new(|cx| {
MessageEditor::new(
self.fs.clone(),
@@ -1018,6 +1058,7 @@ impl AgentPanel {
pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
match self.active_view {
ActiveView::Configuration | ActiveView::History => {
// todo! check go back works correctly
if let Some(previous_view) = self.previous_view.take() {
self.active_view = previous_view;
@@ -1025,6 +1066,9 @@ impl AgentPanel {
ActiveView::Thread { message_editor, .. } => {
message_editor.focus_handle(cx).focus(window);
}
ActiveView::AcpThread { .. } => {
todo!()
}
ActiveView::TextThread { context_editor, .. } => {
context_editor.focus_handle(cx).focus(window);
}
@@ -1144,6 +1188,7 @@ impl AgentPanel {
})
.log_err();
}
ActiveView::AcpThread { .. } => todo!(),
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
}
}
@@ -1197,6 +1242,9 @@ impl AgentPanel {
)
.detach_and_log_err(cx);
}
ActiveView::AcpThread { .. } => {
todo!()
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
}
}
@@ -1231,6 +1279,10 @@ impl AgentPanel {
pub(crate) fn active_thread(&self, cx: &App) -> Option<Entity<Thread>> {
match &self.active_view {
ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
ActiveView::AcpThread { .. } => {
// todo!
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 {
match &self.active_view {
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::TextThread { context_editor, .. } => context_editor.focus_handle(cx),
ActiveView::Configuration => {
@@ -1593,6 +1652,9 @@ impl AgentPanel {
.into_any_element(),
}
}
ActiveView::AcpThread { thread_view } => Label::new(thread_view.read(cx).title(cx))
.truncate()
.into_any_element(),
ActiveView::TextThread {
title_editor,
context_editor,
@@ -1727,6 +1789,10 @@ impl AgentPanel {
let active_thread = match &self.active_view {
ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
ActiveView::AcpThread { .. } => {
// todo!
None
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None,
};
@@ -1755,6 +1821,7 @@ impl AgentPanel {
menu = menu
.action("New Thread", NewThread::default().boxed_clone())
.action("New Text Thread", NewTextThread.boxed_clone())
.action("New Gemini Thread", NewGeminiThread.boxed_clone())
.when_some(active_thread, |this, active_thread| {
let thread = active_thread.read(cx);
if !thread.is_empty() {
@@ -1893,6 +1960,10 @@ impl AgentPanel {
message_editor,
..
} => (thread.read(cx), message_editor.read(cx)),
ActiveView::AcpThread { .. } => {
// todo!
return None;
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
return None;
}
@@ -2031,6 +2102,10 @@ impl AgentPanel {
return false;
}
}
ActiveView::AcpThread { .. } => {
// todo!
return false;
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
return false;
}
@@ -2615,6 +2690,10 @@ impl AgentPanel {
) -> Option<AnyElement> {
let active_thread = match &self.active_view {
ActiveView::Thread { thread, .. } => thread,
ActiveView::AcpThread { .. } => {
// todo!
return None;
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
return None;
}
@@ -2961,6 +3040,9 @@ impl AgentPanel {
.detach();
});
}
ActiveView::AcpThread { .. } => {
unimplemented!()
}
ActiveView::TextThread { context_editor, .. } => {
context_editor.update(cx, |context_editor, cx| {
TextThreadEditor::insert_dragged_files(
@@ -3034,6 +3116,9 @@ impl Render for AgentPanel {
});
this.continue_conversation(window, cx);
}
ActiveView::AcpThread { .. } => {
todo!()
}
ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => {}
@@ -3075,6 +3160,12 @@ impl Render for AgentPanel {
})
.child(h_flex().child(message_editor.clone()))
.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::TextThread {
context_editor,

View File

@@ -55,6 +55,7 @@ actions!(
agent,
[
NewTextThread,
NewGeminiThread,
ToggleContextPicker,
ToggleNavigationMenu,
ToggleOptionsMenu,
@@ -65,7 +66,6 @@ actions!(
OpenHistory,
AddContextServer,
RemoveSelectedThread,
Chat,
ChatWithFollow,
CycleNextInlineAssist,
CyclePreviousInlineAssist,

View File

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

View File

@@ -39,7 +39,7 @@ pub trait StyledExt: Styled + Sized {
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
///
/// 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)
}

View File

@@ -205,7 +205,12 @@ pub mod agent {
actions!(
agent,
[OpenConfiguration, OpenOnboardingModal, ResetOnboarding]
[
OpenConfiguration,
OpenOnboardingModal,
ResetOnboarding,
Chat
]
);
}