ACP follow (#34235)
Closes #ISSUE Release Notes: - N/A --------- Co-authored-by: Agus Zubiaga <agus@zed.dev> Co-authored-by: Anthony Eid <hello@anthonyeid.me> Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
This commit is contained in:
parent
496bf0ec43
commit
993e0f55ec
11 changed files with 1090 additions and 208 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
@ -9,6 +9,7 @@ dependencies = [
|
|||
"agent_servers",
|
||||
"agentic-coding-protocol",
|
||||
"anyhow",
|
||||
"assistant_tool",
|
||||
"async-pipe",
|
||||
"buffer_diff",
|
||||
"editor",
|
||||
|
@ -263,9 +264,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "agentic-coding-protocol"
|
||||
version = "0.0.6"
|
||||
version = "0.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1ac0351749af7bf53c65042ef69fefb9351aa8b7efa0a813d6281377605c37d"
|
||||
checksum = "a75f520bcc049ebe40c8c99427aa61b48ad78a01bcc96a13b350b903dcfb9438"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
|
|
|
@ -404,7 +404,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
|||
# External crates
|
||||
#
|
||||
|
||||
agentic-coding-protocol = "0.0.6"
|
||||
agentic-coding-protocol = "0.0.7"
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
||||
any_vec = "0.14"
|
||||
|
|
|
@ -320,7 +320,8 @@
|
|||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"up": "agent::PreviousHistoryMessage",
|
||||
"down": "agent::NextHistoryMessage"
|
||||
"down": "agent::NextHistoryMessage",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -371,7 +371,8 @@
|
|||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"up": "agent::PreviousHistoryMessage",
|
||||
"down": "agent::NextHistoryMessage"
|
||||
"down": "agent::NextHistoryMessage",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -20,6 +20,7 @@ gemini = []
|
|||
agent_servers.workspace = true
|
||||
agentic-coding-protocol.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
|
|
|
@ -2,14 +2,19 @@ pub use acp::ToolCallId;
|
|||
use agent_servers::AgentServer;
|
||||
use agentic_coding_protocol::{self as acp, UserMessageChunk};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::ActionLog;
|
||||
use buffer_diff::BufferDiff;
|
||||
use editor::{MultiBuffer, PathKey};
|
||||
use futures::{FutureExt, channel::oneshot, future::BoxFuture};
|
||||
use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
|
||||
use itertools::Itertools;
|
||||
use language::{Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _};
|
||||
use language::{
|
||||
Anchor, Buffer, BufferSnapshot, Capability, LanguageRegistry, OffsetRangeExt as _, Point,
|
||||
text_diff,
|
||||
};
|
||||
use markdown::Markdown;
|
||||
use project::Project;
|
||||
use project::{AgentLocation, Project};
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Formatter, Write};
|
||||
use std::{
|
||||
|
@ -159,6 +164,18 @@ impl AgentThreadEntry {
|
|||
Self::ToolCall(too_call) => too_call.to_markdown(cx),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn diff(&self) -> Option<&Diff> {
|
||||
if let AgentThreadEntry::ToolCall(ToolCall {
|
||||
content: Some(ToolCallContent::Diff { diff }),
|
||||
..
|
||||
}) = self
|
||||
{
|
||||
Some(&diff)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -168,6 +185,7 @@ pub struct ToolCall {
|
|||
pub icon: IconName,
|
||||
pub content: Option<ToolCallContent>,
|
||||
pub status: ToolCallStatus,
|
||||
pub locations: Vec<acp::ToolCallLocation>,
|
||||
}
|
||||
|
||||
impl ToolCall {
|
||||
|
@ -328,6 +346,8 @@ impl ToolCallContent {
|
|||
pub struct Diff {
|
||||
pub multibuffer: Entity<MultiBuffer>,
|
||||
pub path: PathBuf,
|
||||
pub new_buffer: Entity<Buffer>,
|
||||
pub old_buffer: Entity<Buffer>,
|
||||
_task: Task<Result<()>>,
|
||||
}
|
||||
|
||||
|
@ -362,6 +382,7 @@ impl Diff {
|
|||
let task = cx.spawn({
|
||||
let multibuffer = multibuffer.clone();
|
||||
let path = path.clone();
|
||||
let new_buffer = new_buffer.clone();
|
||||
async move |cx| {
|
||||
diff_task.await?;
|
||||
|
||||
|
@ -401,6 +422,8 @@ impl Diff {
|
|||
Self {
|
||||
multibuffer,
|
||||
path,
|
||||
new_buffer,
|
||||
old_buffer,
|
||||
_task: task,
|
||||
}
|
||||
}
|
||||
|
@ -421,6 +444,8 @@ pub struct AcpThread {
|
|||
entries: Vec<AgentThreadEntry>,
|
||||
title: SharedString,
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
shared_buffers: HashMap<Entity<Buffer>, BufferSnapshot>,
|
||||
send_task: Option<Task<()>>,
|
||||
connection: Arc<acp::AgentConnection>,
|
||||
child_status: Option<Task<Result<()>>>,
|
||||
|
@ -522,7 +547,11 @@ impl AcpThread {
|
|||
}
|
||||
});
|
||||
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
|
||||
Self {
|
||||
action_log,
|
||||
shared_buffers: Default::default(),
|
||||
entries: Default::default(),
|
||||
title: "ACP Thread".into(),
|
||||
project,
|
||||
|
@ -534,6 +563,14 @@ impl AcpThread {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn action_log(&self) -> &Entity<ActionLog> {
|
||||
&self.action_log
|
||||
}
|
||||
|
||||
pub fn project(&self) -> &Entity<Project> {
|
||||
&self.project
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn fake(
|
||||
stdin: async_pipe::PipeWriter,
|
||||
|
@ -558,7 +595,11 @@ impl AcpThread {
|
|||
}
|
||||
});
|
||||
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
|
||||
Self {
|
||||
action_log,
|
||||
shared_buffers: Default::default(),
|
||||
entries: Default::default(),
|
||||
title: "ACP Thread".into(),
|
||||
project,
|
||||
|
@ -589,6 +630,26 @@ impl AcpThread {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn has_pending_edit_tool_calls(&self) -> bool {
|
||||
for entry in self.entries.iter().rev() {
|
||||
match entry {
|
||||
AgentThreadEntry::UserMessage(_) => return false,
|
||||
AgentThreadEntry::ToolCall(ToolCall {
|
||||
status:
|
||||
ToolCallStatus::Allowed {
|
||||
status: acp::ToolCallStatus::Running,
|
||||
..
|
||||
},
|
||||
content: Some(ToolCallContent::Diff { .. }),
|
||||
..
|
||||
}) => return true,
|
||||
AgentThreadEntry::ToolCall(_) | AgentThreadEntry::AssistantMessage(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn push_entry(&mut self, entry: AgentThreadEntry, cx: &mut Context<Self>) {
|
||||
self.entries.push(entry);
|
||||
cx.emit(AcpThreadEvent::NewEntry);
|
||||
|
@ -644,65 +705,63 @@ impl AcpThread {
|
|||
|
||||
pub fn request_tool_call(
|
||||
&mut self,
|
||||
label: String,
|
||||
icon: acp::Icon,
|
||||
content: Option<acp::ToolCallContent>,
|
||||
confirmation: acp::ToolCallConfirmation,
|
||||
tool_call: acp::RequestToolCallConfirmationParams,
|
||||
cx: &mut Context<Self>,
|
||||
) -> ToolCallRequest {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
let status = ToolCallStatus::WaitingForConfirmation {
|
||||
confirmation: ToolCallConfirmation::from_acp(
|
||||
confirmation,
|
||||
tool_call.confirmation,
|
||||
self.project.read(cx).languages().clone(),
|
||||
cx,
|
||||
),
|
||||
respond_tx: tx,
|
||||
};
|
||||
|
||||
let id = self.insert_tool_call(label, status, icon, content, cx);
|
||||
let id = self.insert_tool_call(tool_call.tool_call, status, cx);
|
||||
ToolCallRequest { id, outcome: rx }
|
||||
}
|
||||
|
||||
pub fn push_tool_call(
|
||||
&mut self,
|
||||
label: String,
|
||||
icon: acp::Icon,
|
||||
content: Option<acp::ToolCallContent>,
|
||||
request: acp::PushToolCallParams,
|
||||
cx: &mut Context<Self>,
|
||||
) -> acp::ToolCallId {
|
||||
let status = ToolCallStatus::Allowed {
|
||||
status: acp::ToolCallStatus::Running,
|
||||
};
|
||||
|
||||
self.insert_tool_call(label, status, icon, content, cx)
|
||||
self.insert_tool_call(request, status, cx)
|
||||
}
|
||||
|
||||
fn insert_tool_call(
|
||||
&mut self,
|
||||
label: String,
|
||||
tool_call: acp::PushToolCallParams,
|
||||
status: ToolCallStatus,
|
||||
icon: acp::Icon,
|
||||
content: Option<acp::ToolCallContent>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> acp::ToolCallId {
|
||||
let language_registry = self.project.read(cx).languages().clone();
|
||||
let id = acp::ToolCallId(self.entries.len() as u64);
|
||||
|
||||
self.push_entry(
|
||||
AgentThreadEntry::ToolCall(ToolCall {
|
||||
id,
|
||||
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,
|
||||
let call = ToolCall {
|
||||
id,
|
||||
label: cx.new(|cx| {
|
||||
Markdown::new(
|
||||
tool_call.label.into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
icon: acp_icon_to_ui_icon(tool_call.icon),
|
||||
content: tool_call
|
||||
.content
|
||||
.map(|content| ToolCallContent::from_acp(content, language_registry, cx)),
|
||||
locations: tool_call.locations,
|
||||
status,
|
||||
};
|
||||
|
||||
self.push_entry(AgentThreadEntry::ToolCall(call), cx);
|
||||
|
||||
id
|
||||
}
|
||||
|
@ -804,14 +863,16 @@ impl AcpThread {
|
|||
false
|
||||
}
|
||||
|
||||
pub fn initialize(&self) -> impl use<> + Future<Output = Result<acp::InitializeResponse>> {
|
||||
pub fn initialize(
|
||||
&self,
|
||||
) -> impl use<> + Future<Output = Result<acp::InitializeResponse, acp::Error>> {
|
||||
let connection = self.connection.clone();
|
||||
async move { Ok(connection.request(acp::InitializeParams).await?) }
|
||||
async move { connection.request(acp::InitializeParams).await }
|
||||
}
|
||||
|
||||
pub fn authenticate(&self) -> impl use<> + Future<Output = Result<()>> {
|
||||
pub fn authenticate(&self) -> impl use<> + Future<Output = Result<(), acp::Error>> {
|
||||
let connection = self.connection.clone();
|
||||
async move { Ok(connection.request(acp::AuthenticateParams).await?) }
|
||||
async move { connection.request(acp::AuthenticateParams).await }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -819,7 +880,7 @@ impl AcpThread {
|
|||
&mut self,
|
||||
message: &str,
|
||||
cx: &mut Context<Self>,
|
||||
) -> BoxFuture<'static, Result<()>> {
|
||||
) -> BoxFuture<'static, Result<(), acp::Error>> {
|
||||
self.send(
|
||||
acp::SendUserMessageParams {
|
||||
chunks: vec![acp::UserMessageChunk::Text {
|
||||
|
@ -834,7 +895,7 @@ impl AcpThread {
|
|||
&mut self,
|
||||
message: acp::SendUserMessageParams,
|
||||
cx: &mut Context<Self>,
|
||||
) -> BoxFuture<'static, Result<()>> {
|
||||
) -> BoxFuture<'static, Result<(), acp::Error>> {
|
||||
let agent = self.connection.clone();
|
||||
self.push_entry(
|
||||
AgentThreadEntry::UserMessage(UserMessage::from_acp(
|
||||
|
@ -865,7 +926,7 @@ impl AcpThread {
|
|||
.boxed()
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
pub fn cancel(&mut self, cx: &mut Context<Self>) -> Task<Result<(), acp::Error>> {
|
||||
let agent = self.connection.clone();
|
||||
|
||||
if self.send_task.take().is_some() {
|
||||
|
@ -898,13 +959,123 @@ impl AcpThread {
|
|||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_text_file(
|
||||
&self,
|
||||
request: acp::ReadTextFileParams,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<String>> {
|
||||
let project = self.project.clone();
|
||||
let action_log = self.action_log.clone();
|
||||
cx.spawn(async move |this, cx| {
|
||||
let load = project.update(cx, |project, cx| {
|
||||
let path = project
|
||||
.project_path_for_absolute_path(&request.path, cx)
|
||||
.context("invalid path")?;
|
||||
anyhow::Ok(project.open_buffer(path, cx))
|
||||
});
|
||||
let buffer = load??.await?;
|
||||
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.buffer_read(buffer.clone(), cx);
|
||||
})?;
|
||||
project.update(cx, |project, cx| {
|
||||
let position = buffer
|
||||
.read(cx)
|
||||
.snapshot()
|
||||
.anchor_before(Point::new(request.line.unwrap_or_default(), 0));
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
|
||||
this.update(cx, |this, _| {
|
||||
let text = snapshot.text();
|
||||
this.shared_buffers.insert(buffer.clone(), snapshot);
|
||||
text
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write_text_file(
|
||||
&self,
|
||||
path: PathBuf,
|
||||
content: String,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let project = self.project.clone();
|
||||
let action_log = self.action_log.clone();
|
||||
cx.spawn(async move |this, cx| {
|
||||
let load = project.update(cx, |project, cx| {
|
||||
let path = project
|
||||
.project_path_for_absolute_path(&path, cx)
|
||||
.context("invalid path")?;
|
||||
anyhow::Ok(project.open_buffer(path, cx))
|
||||
});
|
||||
let buffer = load??.await?;
|
||||
let snapshot = this.update(cx, |this, cx| {
|
||||
this.shared_buffers
|
||||
.get(&buffer)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| buffer.read(cx).snapshot())
|
||||
})?;
|
||||
let edits = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
let old_text = snapshot.text();
|
||||
text_diff(old_text.as_str(), &content)
|
||||
.into_iter()
|
||||
.map(|(range, replacement)| {
|
||||
(
|
||||
snapshot.anchor_after(range.start)
|
||||
..snapshot.anchor_before(range.end),
|
||||
replacement,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.await;
|
||||
cx.update(|cx| {
|
||||
project.update(cx, |project, cx| {
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: edits
|
||||
.last()
|
||||
.map(|(range, _)| range.end)
|
||||
.unwrap_or(Anchor::MIN),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.buffer_read(buffer.clone(), cx);
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(edits, None, cx);
|
||||
});
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.buffer_edited(buffer.clone(), cx);
|
||||
});
|
||||
})?;
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer, cx))?
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
pub fn child_status(&mut self) -> Option<Task<Result<()>>> {
|
||||
self.child_status.take()
|
||||
}
|
||||
|
@ -930,7 +1101,7 @@ impl acp::Client for AcpClientDelegate {
|
|||
async fn stream_assistant_message_chunk(
|
||||
&self,
|
||||
params: acp::StreamAssistantMessageChunkParams,
|
||||
) -> Result<()> {
|
||||
) -> Result<(), acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
|
@ -947,45 +1118,37 @@ impl acp::Client for AcpClientDelegate {
|
|||
async fn request_tool_call_confirmation(
|
||||
&self,
|
||||
request: acp::RequestToolCallConfirmationParams,
|
||||
) -> Result<acp::RequestToolCallConfirmationResponse> {
|
||||
) -> Result<acp::RequestToolCallConfirmationResponse, acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let ToolCallRequest { id, outcome } = cx
|
||||
.update(|cx| {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.request_tool_call(
|
||||
request.label,
|
||||
request.icon,
|
||||
request.content,
|
||||
request.confirmation,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
self.thread
|
||||
.update(cx, |thread, cx| thread.request_tool_call(request, cx))
|
||||
})?
|
||||
.context("Failed to update thread")?;
|
||||
|
||||
Ok(acp::RequestToolCallConfirmationResponse {
|
||||
id,
|
||||
outcome: outcome.await?,
|
||||
outcome: outcome.await.map_err(acp::Error::into_internal_error)?,
|
||||
})
|
||||
}
|
||||
|
||||
async fn push_tool_call(
|
||||
&self,
|
||||
request: acp::PushToolCallParams,
|
||||
) -> Result<acp::PushToolCallResponse> {
|
||||
) -> Result<acp::PushToolCallResponse, acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let id = cx
|
||||
.update(|cx| {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.push_tool_call(request.label, request.icon, request.content, cx)
|
||||
})
|
||||
self.thread
|
||||
.update(cx, |thread, cx| thread.push_tool_call(request, cx))
|
||||
})?
|
||||
.context("Failed to update thread")?;
|
||||
|
||||
Ok(acp::PushToolCallResponse { id })
|
||||
}
|
||||
|
||||
async fn update_tool_call(&self, request: acp::UpdateToolCallParams) -> Result<()> {
|
||||
async fn update_tool_call(&self, request: acp::UpdateToolCallParams) -> Result<(), acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
|
@ -997,6 +1160,34 @@ impl acp::Client for AcpClientDelegate {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_text_file(
|
||||
&self,
|
||||
request: acp::ReadTextFileParams,
|
||||
) -> Result<acp::ReadTextFileResponse, acp::Error> {
|
||||
let content = self
|
||||
.cx
|
||||
.update(|cx| {
|
||||
self.thread
|
||||
.update(cx, |thread, cx| thread.read_text_file(request, cx))
|
||||
})?
|
||||
.context("Failed to update thread")?
|
||||
.await?;
|
||||
Ok(acp::ReadTextFileResponse { content })
|
||||
}
|
||||
|
||||
async fn write_text_file(&self, request: acp::WriteTextFileParams) -> Result<(), acp::Error> {
|
||||
self.cx
|
||||
.update(|cx| {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.write_text_file(request.path, request.content, cx)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")?
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn acp_icon_to_ui_icon(icon: acp::Icon) -> IconName {
|
||||
|
@ -1100,6 +1291,80 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_edits_concurrently_to_user(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/tmp"), json!({"foo": "one\ntwo\nthree\n"}))
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [], cx).await;
|
||||
let (thread, fake_server) = fake_acp_thread(project.clone(), cx);
|
||||
let (worktree, pathbuf) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_worktree(path!("/tmp/foo"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer((worktree.read(cx).id(), pathbuf), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (read_file_tx, read_file_rx) = oneshot::channel::<()>();
|
||||
let read_file_tx = Rc::new(RefCell::new(Some(read_file_tx)));
|
||||
|
||||
fake_server.update(cx, |fake_server, _| {
|
||||
fake_server.on_user_message(move |_, server, mut cx| {
|
||||
let read_file_tx = read_file_tx.clone();
|
||||
async move {
|
||||
let content = server
|
||||
.update(&mut cx, |server, _| {
|
||||
server.send_to_zed(acp::ReadTextFileParams {
|
||||
path: path!("/tmp/foo").into(),
|
||||
line: None,
|
||||
limit: None,
|
||||
})
|
||||
})?
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(content.content, "one\ntwo\nthree\n");
|
||||
read_file_tx.take().unwrap().send(()).unwrap();
|
||||
server
|
||||
.update(&mut cx, |server, _| {
|
||||
server.send_to_zed(acp::WriteTextFileParams {
|
||||
path: path!("/tmp/foo").into(),
|
||||
content: "one\ntwo\nthree\nfour\nfive\n".to_string(),
|
||||
})
|
||||
})?
|
||||
.await
|
||||
.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.send_raw("Extend the count in /tmp/foo", cx)
|
||||
});
|
||||
read_file_rx.await.ok();
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "zero\n".to_string())], None, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.text()),
|
||||
"zero\none\ntwo\nthree\nfour\nfive\n"
|
||||
);
|
||||
assert_eq!(
|
||||
String::from_utf8(fs.read_file_sync(path!("/tmp/foo")).unwrap()).unwrap(),
|
||||
"zero\none\ntwo\nthree\nfour\nfive\n"
|
||||
);
|
||||
request.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_succeeding_canceled_toolcall(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
@ -1124,6 +1389,7 @@ mod tests {
|
|||
label: "Fetch".to_string(),
|
||||
icon: acp::Icon::Globe,
|
||||
content: None,
|
||||
locations: vec![],
|
||||
})
|
||||
})?
|
||||
.await
|
||||
|
@ -1553,7 +1819,7 @@ mod tests {
|
|||
acp::SendUserMessageParams,
|
||||
Entity<FakeAcpServer>,
|
||||
AsyncApp,
|
||||
) -> LocalBoxFuture<'static, Result<()>>,
|
||||
) -> LocalBoxFuture<'static, Result<(), acp::Error>>,
|
||||
>,
|
||||
>,
|
||||
}
|
||||
|
@ -1565,21 +1831,24 @@ mod tests {
|
|||
}
|
||||
|
||||
impl acp::Agent for FakeAgent {
|
||||
async fn initialize(&self) -> Result<acp::InitializeResponse> {
|
||||
async fn initialize(&self) -> Result<acp::InitializeResponse, acp::Error> {
|
||||
Ok(acp::InitializeResponse {
|
||||
is_authenticated: true,
|
||||
})
|
||||
}
|
||||
|
||||
async fn authenticate(&self) -> Result<()> {
|
||||
async fn authenticate(&self) -> Result<(), acp::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cancel_send_message(&self) -> Result<()> {
|
||||
async fn cancel_send_message(&self) -> Result<(), acp::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_user_message(&self, request: acp::SendUserMessageParams) -> Result<()> {
|
||||
async fn send_user_message(
|
||||
&self,
|
||||
request: acp::SendUserMessageParams,
|
||||
) -> Result<(), acp::Error> {
|
||||
let mut cx = self.cx.clone();
|
||||
let handler = self
|
||||
.server
|
||||
|
@ -1589,7 +1858,7 @@ mod tests {
|
|||
if let Some(handler) = handler {
|
||||
handler(request, self.server.clone(), self.cx.clone()).await
|
||||
} else {
|
||||
anyhow::bail!("No handler for on_user_message")
|
||||
Err(anyhow::anyhow!("No handler for on_user_message").into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1624,7 +1893,7 @@ mod tests {
|
|||
handler: impl for<'a> Fn(acp::SendUserMessageParams, Entity<FakeAcpServer>, AsyncApp) -> F
|
||||
+ 'static,
|
||||
) where
|
||||
F: Future<Output = Result<()>> + 'static,
|
||||
F: Future<Output = Result<(), acp::Error>> + 'static,
|
||||
{
|
||||
self.on_user_message
|
||||
.replace(Rc::new(move |request, server, cx| {
|
||||
|
|
|
@ -1,33 +1,37 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use agentic_coding_protocol::{self as acp};
|
||||
use assistant_tool::ActionLog;
|
||||
use buffer_diff::BufferDiff;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode,
|
||||
EditorStyle, MinimapVisibility, MultiBuffer,
|
||||
EditorStyle, MinimapVisibility, MultiBuffer, PathKey,
|
||||
};
|
||||
use file_icons::FileIcons;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId, Focusable,
|
||||
Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement, Subscription, TextStyle,
|
||||
TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, div, list, percentage,
|
||||
prelude::*, pulsating_between,
|
||||
Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
|
||||
FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement,
|
||||
Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
|
||||
Window, div, linear_color_stop, linear_gradient, list, percentage, point, prelude::*,
|
||||
pulsating_between,
|
||||
};
|
||||
use gpui::{FocusHandle, Task};
|
||||
use language::language_settings::SoftWrap;
|
||||
use language::{Buffer, Language};
|
||||
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
||||
use parking_lot::Mutex;
|
||||
use project::Project;
|
||||
use settings::Settings as _;
|
||||
use text::Anchor;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{Disclosure, Tooltip, prelude::*};
|
||||
use ui::{Disclosure, Divider, DividerColor, KeyBinding, Tooltip, prelude::*};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
|
||||
|
||||
use ::acp::{
|
||||
|
@ -38,6 +42,8 @@ use ::acp::{
|
|||
|
||||
use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
|
||||
use crate::acp::message_history::MessageHistory;
|
||||
use crate::agent_diff::AgentDiff;
|
||||
use crate::{AgentDiffPane, Follow, KeepAll, OpenAgentDiff, RejectAll};
|
||||
|
||||
const RESPONSE_PADDING_X: Pixels = px(19.);
|
||||
|
||||
|
@ -53,6 +59,7 @@ pub struct AcpThreadView {
|
|||
auth_task: Option<Task<()>>,
|
||||
expanded_tool_calls: HashSet<ToolCallId>,
|
||||
expanded_thinking_blocks: HashSet<(usize, usize)>,
|
||||
edits_expanded: bool,
|
||||
message_history: MessageHistory<acp::SendUserMessageParams>,
|
||||
}
|
||||
|
||||
|
@ -62,7 +69,7 @@ enum ThreadState {
|
|||
},
|
||||
Ready {
|
||||
thread: Entity<AcpThread>,
|
||||
_subscription: Subscription,
|
||||
_subscription: [Subscription; 2],
|
||||
},
|
||||
LoadError(LoadError),
|
||||
Unauthenticated {
|
||||
|
@ -136,9 +143,9 @@ impl AcpThreadView {
|
|||
);
|
||||
|
||||
Self {
|
||||
workspace,
|
||||
workspace: workspace.clone(),
|
||||
project: project.clone(),
|
||||
thread_state: Self::initial_state(project, window, cx),
|
||||
thread_state: Self::initial_state(workspace, project, window, cx),
|
||||
message_editor,
|
||||
mention_set,
|
||||
diff_editors: Default::default(),
|
||||
|
@ -147,11 +154,13 @@ impl AcpThreadView {
|
|||
auth_task: None,
|
||||
expanded_tool_calls: HashSet::default(),
|
||||
expanded_thinking_blocks: HashSet::default(),
|
||||
edits_expanded: false,
|
||||
message_history: MessageHistory::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn initial_state(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
|
@ -219,15 +228,23 @@ impl AcpThreadView {
|
|||
this.update_in(cx, |this, window, cx| {
|
||||
match result {
|
||||
Ok(()) => {
|
||||
let subscription =
|
||||
let thread_subscription =
|
||||
cx.subscribe_in(&thread, window, Self::handle_thread_event);
|
||||
|
||||
let action_log = thread.read(cx).action_log().clone();
|
||||
let action_log_subscription =
|
||||
cx.observe(&action_log, |_, _, cx| cx.notify());
|
||||
|
||||
this.list_state
|
||||
.splice(0..0, thread.read(cx).entries().len());
|
||||
|
||||
AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
|
||||
|
||||
this.thread_state = ThreadState::Ready {
|
||||
thread,
|
||||
_subscription: subscription,
|
||||
_subscription: [thread_subscription, action_log_subscription],
|
||||
};
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
Err(err) => {
|
||||
|
@ -250,7 +267,7 @@ impl AcpThreadView {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
fn thread(&self) -> Option<&Entity<AcpThread>> {
|
||||
pub fn thread(&self) -> Option<&Entity<AcpThread>> {
|
||||
match &self.thread_state {
|
||||
ThreadState::Ready { thread, .. } | ThreadState::Unauthenticated { thread } => {
|
||||
Some(thread)
|
||||
|
@ -281,7 +298,6 @@ impl AcpThreadView {
|
|||
|
||||
let mut ix = 0;
|
||||
let mut chunks: Vec<acp::UserMessageChunk> = Vec::new();
|
||||
|
||||
let project = self.project.clone();
|
||||
self.message_editor.update(cx, |editor, cx| {
|
||||
let text = editor.text(cx);
|
||||
|
@ -377,6 +393,33 @@ impl AcpThreadView {
|
|||
);
|
||||
}
|
||||
|
||||
fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(thread) = self.thread() {
|
||||
AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err();
|
||||
}
|
||||
}
|
||||
|
||||
fn open_edited_buffer(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(thread) = self.thread() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(diff) =
|
||||
AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.move_to_path(PathKey::for_buffer(&buffer, cx), window, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn set_draft_message(
|
||||
message_editor: Entity<Editor>,
|
||||
mention_set: Arc<Mutex<MentionSet>>,
|
||||
|
@ -464,7 +507,8 @@ impl AcpThreadView {
|
|||
let count = self.list_state.item_count();
|
||||
match event {
|
||||
AcpThreadEvent::NewEntry => {
|
||||
self.sync_thread_entry_view(thread.read(cx).entries().len() - 1, window, cx);
|
||||
let index = thread.read(cx).entries().len() - 1;
|
||||
self.sync_thread_entry_view(index, window, cx);
|
||||
self.list_state.splice(count..count, 1);
|
||||
}
|
||||
AcpThreadEvent::EntryUpdated(index) => {
|
||||
|
@ -537,15 +581,7 @@ impl AcpThreadView {
|
|||
|
||||
fn entry_diff_multibuffer(&self, entry_ix: usize, cx: &App) -> Option<Entity<MultiBuffer>> {
|
||||
let entry = self.thread()?.read(cx).entries().get(entry_ix)?;
|
||||
if let AgentThreadEntry::ToolCall(ToolCall {
|
||||
content: Some(ToolCallContent::Diff { diff }),
|
||||
..
|
||||
}) = &entry
|
||||
{
|
||||
Some(diff.multibuffer.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
entry.diff().map(|diff| diff.multibuffer.clone())
|
||||
}
|
||||
|
||||
fn authenticate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
|
@ -566,7 +602,8 @@ impl AcpThreadView {
|
|||
Markdown::new(format!("Error: {err}").into(), None, None, cx)
|
||||
}))
|
||||
} else {
|
||||
this.thread_state = Self::initial_state(project.clone(), window, cx)
|
||||
this.thread_state =
|
||||
Self::initial_state(this.workspace.clone(), project.clone(), window, cx)
|
||||
}
|
||||
this.auth_task.take()
|
||||
})
|
||||
|
@ -1529,6 +1566,357 @@ impl AcpThreadView {
|
|||
container.into_any()
|
||||
}
|
||||
|
||||
fn render_edits_bar(
|
||||
&self,
|
||||
thread_entity: &Entity<AcpThread>,
|
||||
window: &mut Window,
|
||||
cx: &Context<Self>,
|
||||
) -> Option<AnyElement> {
|
||||
let thread = thread_entity.read(cx);
|
||||
let action_log = thread.action_log();
|
||||
let changed_buffers = action_log.read(cx).changed_buffers(cx);
|
||||
|
||||
if changed_buffers.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let editor_bg_color = cx.theme().colors().editor_background;
|
||||
let active_color = cx.theme().colors().element_selected;
|
||||
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
|
||||
|
||||
let pending_edits = thread.has_pending_edit_tool_calls();
|
||||
let expanded = self.edits_expanded;
|
||||
|
||||
v_flex()
|
||||
.mt_1()
|
||||
.mx_2()
|
||||
.bg(bg_edit_files_disclosure)
|
||||
.border_1()
|
||||
.border_b_0()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_t_md()
|
||||
.shadow(vec![gpui::BoxShadow {
|
||||
color: gpui::black().opacity(0.15),
|
||||
offset: point(px(1.), px(-1.)),
|
||||
blur_radius: px(3.),
|
||||
spread_radius: px(0.),
|
||||
}])
|
||||
.child(self.render_edits_bar_summary(
|
||||
action_log,
|
||||
&changed_buffers,
|
||||
expanded,
|
||||
pending_edits,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.when(expanded, |parent| {
|
||||
parent.child(self.render_edits_bar_files(
|
||||
action_log,
|
||||
&changed_buffers,
|
||||
pending_edits,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
.into_any()
|
||||
.into()
|
||||
}
|
||||
|
||||
fn render_edits_bar_summary(
|
||||
&self,
|
||||
action_log: &Entity<ActionLog>,
|
||||
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
|
||||
expanded: bool,
|
||||
pending_edits: bool,
|
||||
window: &mut Window,
|
||||
cx: &Context<Self>,
|
||||
) -> Div {
|
||||
const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
|
||||
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
h_flex()
|
||||
.p_1()
|
||||
.justify_between()
|
||||
.when(expanded, |this| {
|
||||
this.border_b_1().border_color(cx.theme().colors().border)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.id("edits-container")
|
||||
.cursor_pointer()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(Disclosure::new("edits-disclosure", expanded))
|
||||
.map(|this| {
|
||||
if pending_edits {
|
||||
this.child(
|
||||
Label::new(format!(
|
||||
"Editing {} {}…",
|
||||
changed_buffers.len(),
|
||||
if changed_buffers.len() == 1 {
|
||||
"file"
|
||||
} else {
|
||||
"files"
|
||||
}
|
||||
))
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.with_animation(
|
||||
"edit-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.3, 0.7)),
|
||||
|label, delta| label.alpha(delta),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
Label::new("Edits")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new("•").size(LabelSize::XSmall).color(Color::Muted))
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"{} {}",
|
||||
changed_buffers.len(),
|
||||
if changed_buffers.len() == 1 {
|
||||
"file"
|
||||
} else {
|
||||
"files"
|
||||
}
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.edits_expanded = !this.edits_expanded;
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconButton::new("review-changes", IconName::ListTodo)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Review Changes",
|
||||
&OpenAgentDiff,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(OpenAgentDiff.boxed_clone(), cx);
|
||||
})),
|
||||
)
|
||||
.child(Divider::vertical().color(DividerColor::Border))
|
||||
.child(
|
||||
Button::new("reject-all-changes", "Reject All")
|
||||
.label_size(LabelSize::Small)
|
||||
.disabled(pending_edits)
|
||||
.when(pending_edits, |this| {
|
||||
this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
|
||||
})
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&RejectAll,
|
||||
&focus_handle.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.))),
|
||||
)
|
||||
.on_click({
|
||||
let action_log = action_log.clone();
|
||||
cx.listener(move |_, _, _, cx| {
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.reject_all_edits(cx).detach();
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("keep-all-changes", "Keep All")
|
||||
.label_size(LabelSize::Small)
|
||||
.disabled(pending_edits)
|
||||
.when(pending_edits, |this| {
|
||||
this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
|
||||
})
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(&KeepAll, &focus_handle, window, cx)
|
||||
.map(|kb| kb.size(rems_from_px(10.))),
|
||||
)
|
||||
.on_click({
|
||||
let action_log = action_log.clone();
|
||||
cx.listener(move |_, _, _, cx| {
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.keep_all_edits(cx);
|
||||
})
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_edits_bar_files(
|
||||
&self,
|
||||
action_log: &Entity<ActionLog>,
|
||||
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
|
||||
pending_edits: bool,
|
||||
cx: &Context<Self>,
|
||||
) -> Div {
|
||||
let editor_bg_color = cx.theme().colors().editor_background;
|
||||
|
||||
v_flex().children(changed_buffers.into_iter().enumerate().flat_map(
|
||||
|(index, (buffer, _diff))| {
|
||||
let file = buffer.read(cx).file()?;
|
||||
let path = file.path();
|
||||
|
||||
let file_path = path.parent().and_then(|parent| {
|
||||
let parent_str = parent.to_string_lossy();
|
||||
|
||||
if parent_str.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Label::new(format!("/{}{}", parent_str, std::path::MAIN_SEPARATOR_STR))
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
let file_name = path.file_name().map(|name| {
|
||||
Label::new(name.to_string_lossy().to_string())
|
||||
.size(LabelSize::XSmall)
|
||||
.buffer_font(cx)
|
||||
});
|
||||
|
||||
let file_icon = FileIcons::get_icon(&path, cx)
|
||||
.map(Icon::from_path)
|
||||
.map(|icon| icon.color(Color::Muted).size(IconSize::Small))
|
||||
.unwrap_or_else(|| {
|
||||
Icon::new(IconName::File)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small)
|
||||
});
|
||||
|
||||
let overlay_gradient = linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(editor_bg_color, 1.),
|
||||
linear_color_stop(editor_bg_color.opacity(0.2), 0.),
|
||||
);
|
||||
|
||||
let element = h_flex()
|
||||
.group("edited-code")
|
||||
.id(("file-container", index))
|
||||
.relative()
|
||||
.py_1()
|
||||
.pl_2()
|
||||
.pr_1()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.bg(editor_bg_color)
|
||||
.when(index < changed_buffers.len() - 1, |parent| {
|
||||
parent.border_color(cx.theme().colors().border).border_b_1()
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.id(("file-name", index))
|
||||
.pr_8()
|
||||
.gap_1p5()
|
||||
.max_w_full()
|
||||
.overflow_x_scroll()
|
||||
.child(file_icon)
|
||||
.child(h_flex().gap_0p5().children(file_name).children(file_path))
|
||||
.on_click({
|
||||
let buffer = buffer.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.open_edited_buffer(&buffer, window, cx);
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.visible_on_hover("edited-code")
|
||||
.child(
|
||||
Button::new("review", "Review")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let buffer = buffer.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.open_edited_buffer(&buffer, window, cx);
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(Divider::vertical().color(DividerColor::BorderVariant))
|
||||
.child(
|
||||
Button::new("reject-file", "Reject")
|
||||
.label_size(LabelSize::Small)
|
||||
.disabled(pending_edits)
|
||||
.on_click({
|
||||
let buffer = buffer.clone();
|
||||
let action_log = action_log.clone();
|
||||
move |_, _, cx| {
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log
|
||||
.reject_edits_in_ranges(
|
||||
buffer.clone(),
|
||||
vec![Anchor::MIN..Anchor::MAX],
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("keep-file", "Keep")
|
||||
.label_size(LabelSize::Small)
|
||||
.disabled(pending_edits)
|
||||
.on_click({
|
||||
let buffer = buffer.clone();
|
||||
let action_log = action_log.clone();
|
||||
move |_, _, cx| {
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.keep_edits_in_range(
|
||||
buffer.clone(),
|
||||
Anchor::MIN..Anchor::MAX,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("gradient-overlay")
|
||||
.absolute()
|
||||
.h_full()
|
||||
.w_12()
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.right(px(152.))
|
||||
.bg(overlay_gradient),
|
||||
);
|
||||
|
||||
Some(element)
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn render_message_editor(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let font_size = TextSize::Small
|
||||
|
@ -1559,6 +1947,76 @@ impl AcpThreadView {
|
|||
.into_any()
|
||||
}
|
||||
|
||||
fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
if self.thread().map_or(true, |thread| {
|
||||
thread.read(cx).status() == ThreadStatus::Idle
|
||||
}) {
|
||||
let is_editor_empty = self.message_editor.read(cx).is_empty(cx);
|
||||
IconButton::new("send-message", IconName::Send)
|
||||
.icon_color(Color::Accent)
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(self.thread().is_none() || is_editor_empty)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.chat(&Chat, window, cx);
|
||||
}))
|
||||
.when(!is_editor_empty, |button| {
|
||||
button.tooltip(move |window, cx| Tooltip::for_action("Send", &Chat, window, cx))
|
||||
})
|
||||
.when(is_editor_empty, |button| {
|
||||
button.tooltip(Tooltip::text("Type a message to submit"))
|
||||
})
|
||||
.into_any_element()
|
||||
} else {
|
||||
IconButton::new("stop-generation", IconName::StopFilled)
|
||||
.icon_color(Color::Error)
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Error))
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action("Stop Generation", &editor::actions::Cancel, window, cx)
|
||||
})
|
||||
.on_click(cx.listener(|this, _event, _, cx| this.cancel(cx)))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let following = self
|
||||
.workspace
|
||||
.read_with(cx, |workspace, _| {
|
||||
workspace.is_being_followed(CollaboratorId::Agent)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
IconButton::new("follow-agent", IconName::Crosshair)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.toggle_state(following)
|
||||
.selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
|
||||
.tooltip(move |window, cx| {
|
||||
if following {
|
||||
Tooltip::for_action("Stop Following Agent", &Follow, window, cx)
|
||||
} else {
|
||||
Tooltip::with_meta(
|
||||
"Follow Agent",
|
||||
Some(&Follow),
|
||||
"Track the agent's location as it reads and edits files.",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
if following {
|
||||
workspace.unfollow(CollaboratorId::Agent, window, cx);
|
||||
} else {
|
||||
workspace.follow(CollaboratorId::Agent, window, cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
|
||||
let workspace = self.workspace.clone();
|
||||
MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
|
||||
|
@ -1673,10 +2131,6 @@ impl Focusable for AcpThreadView {
|
|||
|
||||
impl Render for AcpThreadView {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let text = self.message_editor.read(cx).text(cx);
|
||||
let is_editor_empty = text.is_empty();
|
||||
let focus_handle = self.message_editor.focus_handle(cx);
|
||||
|
||||
let open_as_markdown = IconButton::new("open-as-markdown", IconName::DocumentText)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Ignored)
|
||||
|
@ -1702,6 +2156,7 @@ impl Render for AcpThreadView {
|
|||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(Self::previous_history_message))
|
||||
.on_action(cx.listener(Self::next_history_message))
|
||||
.on_action(cx.listener(Self::open_agent_diff))
|
||||
.child(match &self.thread_state {
|
||||
ThreadState::Unauthenticated { .. } => v_flex()
|
||||
.p_2()
|
||||
|
@ -1755,6 +2210,7 @@ impl Render for AcpThreadView {
|
|||
.child(LoadingLabel::new("").size(LabelSize::Small))
|
||||
.into(),
|
||||
})
|
||||
.children(self.render_edits_bar(&thread, window, cx))
|
||||
} else {
|
||||
this.child(self.render_empty_state(false, cx))
|
||||
}
|
||||
|
@ -1782,47 +2238,12 @@ impl Render for AcpThreadView {
|
|||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(self.render_message_editor(cx))
|
||||
.child({
|
||||
let thread = self.thread();
|
||||
|
||||
h_flex().justify_end().child(
|
||||
if thread.map_or(true, |thread| {
|
||||
thread.read(cx).status() == ThreadStatus::Idle
|
||||
}) {
|
||||
IconButton::new("send-message", IconName::Send)
|
||||
.icon_color(Color::Accent)
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(thread.is_none() || is_editor_empty)
|
||||
.on_click({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(&Chat, window, cx);
|
||||
}
|
||||
})
|
||||
.when(!is_editor_empty, |button| {
|
||||
button.tooltip(move |window, cx| {
|
||||
Tooltip::for_action("Send", &Chat, window, cx)
|
||||
})
|
||||
})
|
||||
.when(is_editor_empty, |button| {
|
||||
button.tooltip(Tooltip::text("Type a message to submit"))
|
||||
})
|
||||
} else {
|
||||
IconButton::new("stop-generation", IconName::StopFilled)
|
||||
.icon_color(Color::Error)
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Error))
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Stop Generation",
|
||||
&editor::actions::Cancel,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|this, _event, _, cx| this.cancel(cx)))
|
||||
},
|
||||
)
|
||||
}),
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(self.render_follow_toggle(cx))
|
||||
.child(self.render_send_button(cx)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll};
|
||||
use agent::{Thread, ThreadEvent};
|
||||
use acp::{AcpThread, AcpThreadEvent};
|
||||
use agent::{Thread, ThreadEvent, ThreadSummary};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use assistant_tool::ActionLog;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
|
@ -41,16 +43,108 @@ use zed_actions::assistant::ToggleFocus;
|
|||
pub struct AgentDiffPane {
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
editor: Entity<Editor>,
|
||||
thread: Entity<Thread>,
|
||||
thread: AgentDiffThread,
|
||||
focus_handle: FocusHandle,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
title: SharedString,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone)]
|
||||
pub enum AgentDiffThread {
|
||||
Native(Entity<Thread>),
|
||||
AcpThread(Entity<AcpThread>),
|
||||
}
|
||||
|
||||
impl AgentDiffThread {
|
||||
fn project(&self, cx: &App) -> Entity<Project> {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).project().clone(),
|
||||
AgentDiffThread::AcpThread(thread) => thread.read(cx).project().clone(),
|
||||
}
|
||||
}
|
||||
fn action_log(&self, cx: &App) -> Entity<ActionLog> {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).action_log().clone(),
|
||||
AgentDiffThread::AcpThread(thread) => thread.read(cx).action_log().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn summary(&self, cx: &App) -> ThreadSummary {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).summary().clone(),
|
||||
AgentDiffThread::AcpThread(thread) => ThreadSummary::Ready(thread.read(cx).title()),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_generating(&self, cx: &App) -> bool {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).is_generating(),
|
||||
AgentDiffThread::AcpThread(thread) => {
|
||||
thread.read(cx).status() == acp::ThreadStatus::Generating
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn has_pending_edit_tool_uses(&self, cx: &App) -> bool {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).has_pending_edit_tool_uses(),
|
||||
AgentDiffThread::AcpThread(thread) => thread.read(cx).has_pending_edit_tool_calls(),
|
||||
}
|
||||
}
|
||||
|
||||
fn downgrade(&self) -> WeakAgentDiffThread {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => WeakAgentDiffThread::Native(thread.downgrade()),
|
||||
AgentDiffThread::AcpThread(thread) => {
|
||||
WeakAgentDiffThread::AcpThread(thread.downgrade())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Entity<Thread>> for AgentDiffThread {
|
||||
fn from(entity: Entity<Thread>) -> Self {
|
||||
AgentDiffThread::Native(entity)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Entity<AcpThread>> for AgentDiffThread {
|
||||
fn from(entity: Entity<AcpThread>) -> Self {
|
||||
AgentDiffThread::AcpThread(entity)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone)]
|
||||
pub enum WeakAgentDiffThread {
|
||||
Native(WeakEntity<Thread>),
|
||||
AcpThread(WeakEntity<AcpThread>),
|
||||
}
|
||||
|
||||
impl WeakAgentDiffThread {
|
||||
pub fn upgrade(&self) -> Option<AgentDiffThread> {
|
||||
match self {
|
||||
WeakAgentDiffThread::Native(weak) => weak.upgrade().map(AgentDiffThread::Native),
|
||||
WeakAgentDiffThread::AcpThread(weak) => weak.upgrade().map(AgentDiffThread::AcpThread),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WeakEntity<Thread>> for WeakAgentDiffThread {
|
||||
fn from(entity: WeakEntity<Thread>) -> Self {
|
||||
WeakAgentDiffThread::Native(entity)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WeakEntity<AcpThread>> for WeakAgentDiffThread {
|
||||
fn from(entity: WeakEntity<AcpThread>) -> Self {
|
||||
WeakAgentDiffThread::AcpThread(entity)
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentDiffPane {
|
||||
pub fn deploy(
|
||||
thread: Entity<Thread>,
|
||||
thread: impl Into<AgentDiffThread>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
|
@ -61,14 +155,16 @@ impl AgentDiffPane {
|
|||
}
|
||||
|
||||
pub fn deploy_in_workspace(
|
||||
thread: Entity<Thread>,
|
||||
thread: impl Into<AgentDiffThread>,
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Entity<Self> {
|
||||
let thread = thread.into();
|
||||
let existing_diff = workspace
|
||||
.items_of_type::<AgentDiffPane>(cx)
|
||||
.find(|diff| diff.read(cx).thread == thread);
|
||||
|
||||
if let Some(existing_diff) = existing_diff {
|
||||
workspace.activate_item(&existing_diff, true, true, window, cx);
|
||||
existing_diff
|
||||
|
@ -81,7 +177,7 @@ impl AgentDiffPane {
|
|||
}
|
||||
|
||||
pub fn new(
|
||||
thread: Entity<Thread>,
|
||||
thread: AgentDiffThread,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
|
@ -89,7 +185,7 @@ impl AgentDiffPane {
|
|||
let focus_handle = cx.focus_handle();
|
||||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||||
|
||||
let project = thread.read(cx).project().clone();
|
||||
let project = thread.project(cx).clone();
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
|
||||
|
@ -100,16 +196,27 @@ impl AgentDiffPane {
|
|||
editor
|
||||
});
|
||||
|
||||
let action_log = thread.read(cx).action_log().clone();
|
||||
let action_log = thread.action_log(cx).clone();
|
||||
|
||||
let mut this = Self {
|
||||
_subscriptions: vec![
|
||||
cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
|
||||
this.update_excerpts(window, cx)
|
||||
}),
|
||||
cx.subscribe(&thread, |this, _thread, event, cx| {
|
||||
this.handle_thread_event(event, cx)
|
||||
}),
|
||||
],
|
||||
_subscriptions: [
|
||||
Some(
|
||||
cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
|
||||
this.update_excerpts(window, cx)
|
||||
}),
|
||||
),
|
||||
match &thread {
|
||||
AgentDiffThread::Native(thread) => {
|
||||
Some(cx.subscribe(&thread, |this, _thread, event, cx| {
|
||||
this.handle_thread_event(event, cx)
|
||||
}))
|
||||
}
|
||||
AgentDiffThread::AcpThread(_) => None,
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect(),
|
||||
title: SharedString::default(),
|
||||
multibuffer,
|
||||
editor,
|
||||
|
@ -123,8 +230,7 @@ impl AgentDiffPane {
|
|||
}
|
||||
|
||||
fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let thread = self.thread.read(cx);
|
||||
let changed_buffers = thread.action_log().read(cx).changed_buffers(cx);
|
||||
let changed_buffers = self.thread.action_log(cx).read(cx).changed_buffers(cx);
|
||||
let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
|
||||
|
||||
for (buffer, diff_handle) in changed_buffers {
|
||||
|
@ -211,7 +317,7 @@ impl AgentDiffPane {
|
|||
}
|
||||
|
||||
fn update_title(&mut self, cx: &mut Context<Self>) {
|
||||
let new_title = self.thread.read(cx).summary().unwrap_or("Agent Changes");
|
||||
let new_title = self.thread.summary(cx).unwrap_or("Agent Changes");
|
||||
if new_title != self.title {
|
||||
self.title = new_title;
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
|
@ -275,14 +381,15 @@ impl AgentDiffPane {
|
|||
|
||||
fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.thread
|
||||
.update(cx, |thread, cx| thread.keep_all_edits(cx));
|
||||
.action_log(cx)
|
||||
.update(cx, |action_log, cx| action_log.keep_all_edits(cx))
|
||||
}
|
||||
}
|
||||
|
||||
fn keep_edits_in_selection(
|
||||
editor: &mut Editor,
|
||||
buffer_snapshot: &MultiBufferSnapshot,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
|
@ -297,7 +404,7 @@ fn keep_edits_in_selection(
|
|||
fn reject_edits_in_selection(
|
||||
editor: &mut Editor,
|
||||
buffer_snapshot: &MultiBufferSnapshot,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
|
@ -311,7 +418,7 @@ fn reject_edits_in_selection(
|
|||
fn keep_edits_in_ranges(
|
||||
editor: &mut Editor,
|
||||
buffer_snapshot: &MultiBufferSnapshot,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
ranges: Vec<Range<editor::Anchor>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
|
@ -326,8 +433,8 @@ fn keep_edits_in_ranges(
|
|||
for hunk in &diff_hunks_in_ranges {
|
||||
let buffer = multibuffer.read(cx).buffer(hunk.buffer_id);
|
||||
if let Some(buffer) = buffer {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.keep_edits_in_range(buffer, hunk.buffer_range.clone(), cx)
|
||||
thread.action_log(cx).update(cx, |action_log, cx| {
|
||||
action_log.keep_edits_in_range(buffer, hunk.buffer_range.clone(), cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -336,7 +443,7 @@ fn keep_edits_in_ranges(
|
|||
fn reject_edits_in_ranges(
|
||||
editor: &mut Editor,
|
||||
buffer_snapshot: &MultiBufferSnapshot,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
ranges: Vec<Range<editor::Anchor>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
|
@ -362,8 +469,9 @@ fn reject_edits_in_ranges(
|
|||
|
||||
for (buffer, ranges) in ranges_by_buffer {
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.reject_edits_in_ranges(buffer, ranges, cx)
|
||||
.action_log(cx)
|
||||
.update(cx, |action_log, cx| {
|
||||
action_log.reject_edits_in_ranges(buffer, ranges, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
@ -461,7 +569,7 @@ impl Item for AgentDiffPane {
|
|||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
|
||||
let summary = self.thread.read(cx).summary().unwrap_or("Agent Changes");
|
||||
let summary = self.thread.summary(cx).unwrap_or("Agent Changes");
|
||||
Label::new(format!("Review: {}", summary))
|
||||
.color(if params.selected {
|
||||
Color::Default
|
||||
|
@ -641,7 +749,7 @@ impl Render for AgentDiffPane {
|
|||
}
|
||||
}
|
||||
|
||||
fn diff_hunk_controls(thread: &Entity<Thread>) -> editor::RenderDiffHunkControlsFn {
|
||||
fn diff_hunk_controls(thread: &AgentDiffThread) -> editor::RenderDiffHunkControlsFn {
|
||||
let thread = thread.clone();
|
||||
|
||||
Arc::new(
|
||||
|
@ -676,7 +784,7 @@ fn render_diff_hunk_controls(
|
|||
hunk_range: Range<editor::Anchor>,
|
||||
is_created_file: bool,
|
||||
line_height: Pixels,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
editor: &Entity<Editor>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
|
@ -1112,11 +1220,8 @@ impl Render for AgentDiffToolbar {
|
|||
return Empty.into_any();
|
||||
};
|
||||
|
||||
let has_pending_edit_tool_use = agent_diff
|
||||
.read(cx)
|
||||
.thread
|
||||
.read(cx)
|
||||
.has_pending_edit_tool_uses();
|
||||
let has_pending_edit_tool_use =
|
||||
agent_diff.read(cx).thread.has_pending_edit_tool_uses(cx);
|
||||
|
||||
if has_pending_edit_tool_use {
|
||||
return div().px_2().child(spinner_icon).into_any();
|
||||
|
@ -1187,8 +1292,8 @@ pub enum EditorState {
|
|||
}
|
||||
|
||||
struct WorkspaceThread {
|
||||
thread: WeakEntity<Thread>,
|
||||
_thread_subscriptions: [Subscription; 2],
|
||||
thread: WeakAgentDiffThread,
|
||||
_thread_subscriptions: (Subscription, Subscription),
|
||||
singleton_editors: HashMap<WeakEntity<Buffer>, HashMap<WeakEntity<Editor>, Subscription>>,
|
||||
_settings_subscription: Subscription,
|
||||
_workspace_subscription: Option<Subscription>,
|
||||
|
@ -1212,23 +1317,23 @@ impl AgentDiff {
|
|||
|
||||
pub fn set_active_thread(
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
thread: &Entity<Thread>,
|
||||
thread: impl Into<AgentDiffThread>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
Self::global(cx).update(cx, |this, cx| {
|
||||
this.register_active_thread_impl(workspace, thread, window, cx);
|
||||
this.register_active_thread_impl(workspace, thread.into(), window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn register_active_thread_impl(
|
||||
&mut self,
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
thread: &Entity<Thread>,
|
||||
thread: AgentDiffThread,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let action_log = thread.read(cx).action_log().clone();
|
||||
let action_log = thread.action_log(cx).clone();
|
||||
|
||||
let action_log_subscription = cx.observe_in(&action_log, window, {
|
||||
let workspace = workspace.clone();
|
||||
|
@ -1237,17 +1342,25 @@ impl AgentDiff {
|
|||
}
|
||||
});
|
||||
|
||||
let thread_subscription = cx.subscribe_in(&thread, window, {
|
||||
let workspace = workspace.clone();
|
||||
move |this, _thread, event, window, cx| {
|
||||
this.handle_thread_event(&workspace, event, window, cx)
|
||||
}
|
||||
});
|
||||
let thread_subscription = match &thread {
|
||||
AgentDiffThread::Native(thread) => cx.subscribe_in(&thread, window, {
|
||||
let workspace = workspace.clone();
|
||||
move |this, _thread, event, window, cx| {
|
||||
this.handle_native_thread_event(&workspace, event, window, cx)
|
||||
}
|
||||
}),
|
||||
AgentDiffThread::AcpThread(thread) => cx.subscribe_in(&thread, window, {
|
||||
let workspace = workspace.clone();
|
||||
move |this, thread, event, window, cx| {
|
||||
this.handle_acp_thread_event(&workspace, thread, event, window, cx)
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
if let Some(workspace_thread) = self.workspace_threads.get_mut(&workspace) {
|
||||
// replace thread and action log subscription, but keep editors
|
||||
workspace_thread.thread = thread.downgrade();
|
||||
workspace_thread._thread_subscriptions = [action_log_subscription, thread_subscription];
|
||||
workspace_thread._thread_subscriptions = (action_log_subscription, thread_subscription);
|
||||
self.update_reviewing_editors(&workspace, window, cx);
|
||||
return;
|
||||
}
|
||||
|
@ -1272,7 +1385,7 @@ impl AgentDiff {
|
|||
workspace.clone(),
|
||||
WorkspaceThread {
|
||||
thread: thread.downgrade(),
|
||||
_thread_subscriptions: [action_log_subscription, thread_subscription],
|
||||
_thread_subscriptions: (action_log_subscription, thread_subscription),
|
||||
singleton_editors: HashMap::default(),
|
||||
_settings_subscription: settings_subscription,
|
||||
_workspace_subscription: workspace_subscription,
|
||||
|
@ -1319,7 +1432,7 @@ impl AgentDiff {
|
|||
|
||||
fn register_review_action<T: Action>(
|
||||
workspace: &mut Workspace,
|
||||
review: impl Fn(&Entity<Editor>, &Entity<Thread>, &mut Window, &mut App) -> PostReviewState
|
||||
review: impl Fn(&Entity<Editor>, &AgentDiffThread, &mut Window, &mut App) -> PostReviewState
|
||||
+ 'static,
|
||||
this: &Entity<AgentDiff>,
|
||||
) {
|
||||
|
@ -1338,7 +1451,7 @@ impl AgentDiff {
|
|||
});
|
||||
}
|
||||
|
||||
fn handle_thread_event(
|
||||
fn handle_native_thread_event(
|
||||
&mut self,
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
event: &ThreadEvent,
|
||||
|
@ -1380,6 +1493,40 @@ impl AgentDiff {
|
|||
}
|
||||
}
|
||||
|
||||
fn handle_acp_thread_event(
|
||||
&mut self,
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
thread: &Entity<AcpThread>,
|
||||
event: &AcpThreadEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
AcpThreadEvent::NewEntry => {
|
||||
if thread
|
||||
.read(cx)
|
||||
.entries()
|
||||
.last()
|
||||
.and_then(|entry| entry.diff())
|
||||
.is_some()
|
||||
{
|
||||
self.update_reviewing_editors(workspace, window, cx);
|
||||
}
|
||||
}
|
||||
AcpThreadEvent::EntryUpdated(ix) => {
|
||||
if thread
|
||||
.read(cx)
|
||||
.entries()
|
||||
.get(*ix)
|
||||
.and_then(|entry| entry.diff())
|
||||
.is_some()
|
||||
{
|
||||
self.update_reviewing_editors(workspace, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_workspace_event(
|
||||
&mut self,
|
||||
workspace: &Entity<Workspace>,
|
||||
|
@ -1485,7 +1632,7 @@ impl AgentDiff {
|
|||
return;
|
||||
};
|
||||
|
||||
let action_log = thread.read(cx).action_log();
|
||||
let action_log = thread.action_log(cx);
|
||||
let changed_buffers = action_log.read(cx).changed_buffers(cx);
|
||||
|
||||
let mut unaffected = self.reviewing_editors.clone();
|
||||
|
@ -1510,7 +1657,7 @@ impl AgentDiff {
|
|||
multibuffer.add_diff(diff_handle.clone(), cx);
|
||||
});
|
||||
|
||||
let new_state = if thread.read(cx).is_generating() {
|
||||
let new_state = if thread.is_generating(cx) {
|
||||
EditorState::Generating
|
||||
} else {
|
||||
EditorState::Reviewing
|
||||
|
@ -1606,7 +1753,7 @@ impl AgentDiff {
|
|||
|
||||
fn keep_all(
|
||||
editor: &Entity<Editor>,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> PostReviewState {
|
||||
|
@ -1626,7 +1773,7 @@ impl AgentDiff {
|
|||
|
||||
fn reject_all(
|
||||
editor: &Entity<Editor>,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> PostReviewState {
|
||||
|
@ -1646,7 +1793,7 @@ impl AgentDiff {
|
|||
|
||||
fn keep(
|
||||
editor: &Entity<Editor>,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> PostReviewState {
|
||||
|
@ -1659,7 +1806,7 @@ impl AgentDiff {
|
|||
|
||||
fn reject(
|
||||
editor: &Entity<Editor>,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> PostReviewState {
|
||||
|
@ -1682,7 +1829,7 @@ impl AgentDiff {
|
|||
fn review_in_active_editor(
|
||||
&mut self,
|
||||
workspace: &mut Workspace,
|
||||
review: impl Fn(&Entity<Editor>, &Entity<Thread>, &mut Window, &mut App) -> PostReviewState,
|
||||
review: impl Fn(&Entity<Editor>, &AgentDiffThread, &mut Window, &mut App) -> PostReviewState,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
|
@ -1703,7 +1850,7 @@ impl AgentDiff {
|
|||
|
||||
if let PostReviewState::AllReviewed = review(&editor, &thread, window, cx) {
|
||||
if let Some(curr_buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
|
||||
let changed_buffers = thread.read(cx).action_log().read(cx).changed_buffers(cx);
|
||||
let changed_buffers = thread.action_log(cx).read(cx).changed_buffers(cx);
|
||||
|
||||
let mut keys = changed_buffers.keys().cycle();
|
||||
keys.find(|k| *k == &curr_buffer);
|
||||
|
@ -1801,8 +1948,9 @@ mod tests {
|
|||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
let thread =
|
||||
AgentDiffThread::Native(thread_store.update(cx, |store, cx| store.create_thread(cx)));
|
||||
let action_log = cx.read(|cx| thread.action_log(cx));
|
||||
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
|
@ -1988,8 +2136,9 @@ mod tests {
|
|||
});
|
||||
|
||||
// Set the active thread
|
||||
let thread = AgentDiffThread::Native(thread);
|
||||
cx.update(|window, cx| {
|
||||
AgentDiff::set_active_thread(&workspace.downgrade(), &thread, window, cx)
|
||||
AgentDiff::set_active_thread(&workspace.downgrade(), thread.clone(), window, cx)
|
||||
});
|
||||
|
||||
let buffer1 = project
|
||||
|
|
|
@ -8,6 +8,7 @@ use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::NewAcpThread;
|
||||
use crate::agent_diff::AgentDiffThread;
|
||||
use crate::language_model_selector::ToggleModelSelector;
|
||||
use crate::{
|
||||
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
|
||||
|
@ -624,7 +625,7 @@ impl AgentPanel {
|
|||
}
|
||||
};
|
||||
|
||||
AgentDiff::set_active_thread(&workspace, &thread, window, cx);
|
||||
AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
|
||||
|
||||
let weak_panel = weak_self.clone();
|
||||
|
||||
|
@ -845,7 +846,7 @@ impl AgentPanel {
|
|||
let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
|
||||
self.set_active_view(thread_view, window, cx);
|
||||
|
||||
AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
|
||||
AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
|
||||
}
|
||||
|
||||
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
|
@ -890,11 +891,20 @@ impl AgentPanel {
|
|||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let thread_view = cx.new_window_entity(|window, cx| {
|
||||
crate::acp::AcpThreadView::new(workspace, project, window, cx)
|
||||
crate::acp::AcpThreadView::new(workspace.clone(), project, window, cx)
|
||||
})?;
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.set_active_view(ActiveView::AcpThread { thread_view }, window, cx);
|
||||
this.set_active_view(
|
||||
ActiveView::AcpThread {
|
||||
thread_view: thread_view.clone(),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
@ -1050,7 +1060,7 @@ impl AgentPanel {
|
|||
|
||||
let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
|
||||
self.set_active_view(thread_view, window, cx);
|
||||
AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
|
||||
AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
|
||||
}
|
||||
|
||||
pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
|
||||
|
@ -1181,7 +1191,12 @@ impl AgentPanel {
|
|||
let thread = thread.read(cx).thread().clone();
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx)
|
||||
AgentDiffPane::deploy_in_workspace(
|
||||
AgentDiffThread::Native(thread),
|
||||
workspace,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ use std::collections::BTreeMap;
|
|||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::agent_diff::AgentDiffThread;
|
||||
use crate::agent_model_selector::AgentModelSelector;
|
||||
use crate::language_model_selector::ToggleModelSelector;
|
||||
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
|
||||
|
@ -475,9 +476,12 @@ impl MessageEditor {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Ok(diff) =
|
||||
AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx)
|
||||
{
|
||||
if let Ok(diff) = AgentDiffPane::deploy(
|
||||
AgentDiffThread::Native(self.thread.clone()),
|
||||
self.workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
let path_key = multi_buffer::PathKey::for_buffer(&buffer, cx);
|
||||
diff.update(cx, |diff, cx| diff.move_to_path(path_key, window, cx));
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
|
|||
use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
|
||||
use std::{cmp, ops::Range, sync::Arc};
|
||||
use text::{Edit, Patch, Rope};
|
||||
use util::RangeExt;
|
||||
use util::{RangeExt, ResultExt as _};
|
||||
|
||||
/// Tracks actions performed by tools in a thread
|
||||
pub struct ActionLog {
|
||||
|
@ -47,6 +47,10 @@ impl ActionLog {
|
|||
self.edited_since_project_diagnostics_check
|
||||
}
|
||||
|
||||
pub fn latest_snapshot(&self, buffer: &Entity<Buffer>) -> Option<text::BufferSnapshot> {
|
||||
Some(self.tracked_buffers.get(buffer)?.snapshot.clone())
|
||||
}
|
||||
|
||||
fn track_buffer_internal(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
|
@ -715,6 +719,22 @@ impl ActionLog {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn reject_all_edits(&mut self, cx: &mut Context<Self>) -> Task<()> {
|
||||
let futures = self.changed_buffers(cx).into_keys().map(|buffer| {
|
||||
let reject = self.reject_edits_in_ranges(buffer, vec![Anchor::MIN..Anchor::MAX], cx);
|
||||
|
||||
async move {
|
||||
reject.await.log_err();
|
||||
}
|
||||
});
|
||||
|
||||
let task = futures::future::join_all(futures);
|
||||
|
||||
cx.spawn(async move |_, _| {
|
||||
task.await;
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the set of buffers that contain edits that haven't been reviewed by the user.
|
||||
pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, Entity<BufferDiff>> {
|
||||
self.tracked_buffers
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue