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",
|
"agent_servers",
|
||||||
"agentic-coding-protocol",
|
"agentic-coding-protocol",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"assistant_tool",
|
||||||
"async-pipe",
|
"async-pipe",
|
||||||
"buffer_diff",
|
"buffer_diff",
|
||||||
"editor",
|
"editor",
|
||||||
|
@ -263,9 +264,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agentic-coding-protocol"
|
name = "agentic-coding-protocol"
|
||||||
version = "0.0.6"
|
version = "0.0.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d1ac0351749af7bf53c65042ef69fefb9351aa8b7efa0a813d6281377605c37d"
|
checksum = "a75f520bcc049ebe40c8c99427aa61b48ad78a01bcc96a13b350b903dcfb9438"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
|
@ -404,7 +404,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||||
# External crates
|
# External crates
|
||||||
#
|
#
|
||||||
|
|
||||||
agentic-coding-protocol = "0.0.6"
|
agentic-coding-protocol = "0.0.7"
|
||||||
aho-corasick = "1.1"
|
aho-corasick = "1.1"
|
||||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
||||||
any_vec = "0.14"
|
any_vec = "0.14"
|
||||||
|
|
|
@ -320,7 +320,8 @@
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"enter": "agent::Chat",
|
"enter": "agent::Chat",
|
||||||
"up": "agent::PreviousHistoryMessage",
|
"up": "agent::PreviousHistoryMessage",
|
||||||
"down": "agent::NextHistoryMessage"
|
"down": "agent::NextHistoryMessage",
|
||||||
|
"shift-ctrl-r": "agent::OpenAgentDiff"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -371,7 +371,8 @@
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"enter": "agent::Chat",
|
"enter": "agent::Chat",
|
||||||
"up": "agent::PreviousHistoryMessage",
|
"up": "agent::PreviousHistoryMessage",
|
||||||
"down": "agent::NextHistoryMessage"
|
"down": "agent::NextHistoryMessage",
|
||||||
|
"shift-ctrl-r": "agent::OpenAgentDiff"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -20,6 +20,7 @@ gemini = []
|
||||||
agent_servers.workspace = true
|
agent_servers.workspace = true
|
||||||
agentic-coding-protocol.workspace = true
|
agentic-coding-protocol.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
assistant_tool.workspace = true
|
||||||
buffer_diff.workspace = true
|
buffer_diff.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
|
|
|
@ -2,14 +2,19 @@ pub use acp::ToolCallId;
|
||||||
use agent_servers::AgentServer;
|
use agent_servers::AgentServer;
|
||||||
use agentic_coding_protocol::{self as acp, UserMessageChunk};
|
use agentic_coding_protocol::{self as acp, UserMessageChunk};
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
|
use assistant_tool::ActionLog;
|
||||||
use buffer_diff::BufferDiff;
|
use buffer_diff::BufferDiff;
|
||||||
use editor::{MultiBuffer, PathKey};
|
use editor::{MultiBuffer, PathKey};
|
||||||
use futures::{FutureExt, channel::oneshot, future::BoxFuture};
|
use futures::{FutureExt, channel::oneshot, future::BoxFuture};
|
||||||
use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
|
use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
|
||||||
use itertools::Itertools;
|
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 markdown::Markdown;
|
||||||
use project::Project;
|
use project::{AgentLocation, Project};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fmt::{Formatter, Write};
|
use std::fmt::{Formatter, Write};
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -159,6 +164,18 @@ impl AgentThreadEntry {
|
||||||
Self::ToolCall(too_call) => too_call.to_markdown(cx),
|
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)]
|
#[derive(Debug)]
|
||||||
|
@ -168,6 +185,7 @@ pub struct ToolCall {
|
||||||
pub icon: IconName,
|
pub icon: IconName,
|
||||||
pub content: Option<ToolCallContent>,
|
pub content: Option<ToolCallContent>,
|
||||||
pub status: ToolCallStatus,
|
pub status: ToolCallStatus,
|
||||||
|
pub locations: Vec<acp::ToolCallLocation>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToolCall {
|
impl ToolCall {
|
||||||
|
@ -328,6 +346,8 @@ impl ToolCallContent {
|
||||||
pub struct Diff {
|
pub struct Diff {
|
||||||
pub multibuffer: Entity<MultiBuffer>,
|
pub multibuffer: Entity<MultiBuffer>,
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
pub new_buffer: Entity<Buffer>,
|
||||||
|
pub old_buffer: Entity<Buffer>,
|
||||||
_task: Task<Result<()>>,
|
_task: Task<Result<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -362,6 +382,7 @@ impl Diff {
|
||||||
let task = cx.spawn({
|
let task = cx.spawn({
|
||||||
let multibuffer = multibuffer.clone();
|
let multibuffer = multibuffer.clone();
|
||||||
let path = path.clone();
|
let path = path.clone();
|
||||||
|
let new_buffer = new_buffer.clone();
|
||||||
async move |cx| {
|
async move |cx| {
|
||||||
diff_task.await?;
|
diff_task.await?;
|
||||||
|
|
||||||
|
@ -401,6 +422,8 @@ impl Diff {
|
||||||
Self {
|
Self {
|
||||||
multibuffer,
|
multibuffer,
|
||||||
path,
|
path,
|
||||||
|
new_buffer,
|
||||||
|
old_buffer,
|
||||||
_task: task,
|
_task: task,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -421,6 +444,8 @@ pub struct AcpThread {
|
||||||
entries: Vec<AgentThreadEntry>,
|
entries: Vec<AgentThreadEntry>,
|
||||||
title: SharedString,
|
title: SharedString,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
|
action_log: Entity<ActionLog>,
|
||||||
|
shared_buffers: HashMap<Entity<Buffer>, BufferSnapshot>,
|
||||||
send_task: Option<Task<()>>,
|
send_task: Option<Task<()>>,
|
||||||
connection: Arc<acp::AgentConnection>,
|
connection: Arc<acp::AgentConnection>,
|
||||||
child_status: Option<Task<Result<()>>>,
|
child_status: Option<Task<Result<()>>>,
|
||||||
|
@ -522,7 +547,11 @@ impl AcpThread {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
action_log,
|
||||||
|
shared_buffers: Default::default(),
|
||||||
entries: Default::default(),
|
entries: Default::default(),
|
||||||
title: "ACP Thread".into(),
|
title: "ACP Thread".into(),
|
||||||
project,
|
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)]
|
#[cfg(test)]
|
||||||
pub fn fake(
|
pub fn fake(
|
||||||
stdin: async_pipe::PipeWriter,
|
stdin: async_pipe::PipeWriter,
|
||||||
|
@ -558,7 +595,11 @@ impl AcpThread {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
action_log,
|
||||||
|
shared_buffers: Default::default(),
|
||||||
entries: Default::default(),
|
entries: Default::default(),
|
||||||
title: "ACP Thread".into(),
|
title: "ACP Thread".into(),
|
||||||
project,
|
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>) {
|
pub fn push_entry(&mut self, entry: AgentThreadEntry, cx: &mut Context<Self>) {
|
||||||
self.entries.push(entry);
|
self.entries.push(entry);
|
||||||
cx.emit(AcpThreadEvent::NewEntry);
|
cx.emit(AcpThreadEvent::NewEntry);
|
||||||
|
@ -644,65 +705,63 @@ impl AcpThread {
|
||||||
|
|
||||||
pub fn request_tool_call(
|
pub fn request_tool_call(
|
||||||
&mut self,
|
&mut self,
|
||||||
label: String,
|
tool_call: acp::RequestToolCallConfirmationParams,
|
||||||
icon: acp::Icon,
|
|
||||||
content: Option<acp::ToolCallContent>,
|
|
||||||
confirmation: acp::ToolCallConfirmation,
|
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> ToolCallRequest {
|
) -> ToolCallRequest {
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
|
|
||||||
let status = ToolCallStatus::WaitingForConfirmation {
|
let status = ToolCallStatus::WaitingForConfirmation {
|
||||||
confirmation: ToolCallConfirmation::from_acp(
|
confirmation: ToolCallConfirmation::from_acp(
|
||||||
confirmation,
|
tool_call.confirmation,
|
||||||
self.project.read(cx).languages().clone(),
|
self.project.read(cx).languages().clone(),
|
||||||
cx,
|
cx,
|
||||||
),
|
),
|
||||||
respond_tx: tx,
|
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 }
|
ToolCallRequest { id, outcome: rx }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_tool_call(
|
pub fn push_tool_call(
|
||||||
&mut self,
|
&mut self,
|
||||||
label: String,
|
request: acp::PushToolCallParams,
|
||||||
icon: acp::Icon,
|
|
||||||
content: Option<acp::ToolCallContent>,
|
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> acp::ToolCallId {
|
) -> acp::ToolCallId {
|
||||||
let status = ToolCallStatus::Allowed {
|
let status = ToolCallStatus::Allowed {
|
||||||
status: acp::ToolCallStatus::Running,
|
status: acp::ToolCallStatus::Running,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.insert_tool_call(label, status, icon, content, cx)
|
self.insert_tool_call(request, status, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_tool_call(
|
fn insert_tool_call(
|
||||||
&mut self,
|
&mut self,
|
||||||
label: String,
|
tool_call: acp::PushToolCallParams,
|
||||||
status: ToolCallStatus,
|
status: ToolCallStatus,
|
||||||
icon: acp::Icon,
|
|
||||||
content: Option<acp::ToolCallContent>,
|
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> acp::ToolCallId {
|
) -> acp::ToolCallId {
|
||||||
let language_registry = self.project.read(cx).languages().clone();
|
let language_registry = self.project.read(cx).languages().clone();
|
||||||
let id = acp::ToolCallId(self.entries.len() as u64);
|
let id = acp::ToolCallId(self.entries.len() as u64);
|
||||||
|
let call = ToolCall {
|
||||||
self.push_entry(
|
id,
|
||||||
AgentThreadEntry::ToolCall(ToolCall {
|
label: cx.new(|cx| {
|
||||||
id,
|
Markdown::new(
|
||||||
label: cx.new(|cx| {
|
tool_call.label.into(),
|
||||||
Markdown::new(label.into(), Some(language_registry.clone()), None, cx)
|
Some(language_registry.clone()),
|
||||||
}),
|
None,
|
||||||
icon: acp_icon_to_ui_icon(icon),
|
cx,
|
||||||
content: content
|
)
|
||||||
.map(|content| ToolCallContent::from_acp(content, language_registry, cx)),
|
|
||||||
status,
|
|
||||||
}),
|
}),
|
||||||
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
|
id
|
||||||
}
|
}
|
||||||
|
@ -804,14 +863,16 @@ impl AcpThread {
|
||||||
false
|
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();
|
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();
|
let connection = self.connection.clone();
|
||||||
async move { Ok(connection.request(acp::AuthenticateParams).await?) }
|
async move { connection.request(acp::AuthenticateParams).await }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -819,7 +880,7 @@ impl AcpThread {
|
||||||
&mut self,
|
&mut self,
|
||||||
message: &str,
|
message: &str,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> BoxFuture<'static, Result<()>> {
|
) -> BoxFuture<'static, Result<(), acp::Error>> {
|
||||||
self.send(
|
self.send(
|
||||||
acp::SendUserMessageParams {
|
acp::SendUserMessageParams {
|
||||||
chunks: vec![acp::UserMessageChunk::Text {
|
chunks: vec![acp::UserMessageChunk::Text {
|
||||||
|
@ -834,7 +895,7 @@ impl AcpThread {
|
||||||
&mut self,
|
&mut self,
|
||||||
message: acp::SendUserMessageParams,
|
message: acp::SendUserMessageParams,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> BoxFuture<'static, Result<()>> {
|
) -> BoxFuture<'static, Result<(), acp::Error>> {
|
||||||
let agent = self.connection.clone();
|
let agent = self.connection.clone();
|
||||||
self.push_entry(
|
self.push_entry(
|
||||||
AgentThreadEntry::UserMessage(UserMessage::from_acp(
|
AgentThreadEntry::UserMessage(UserMessage::from_acp(
|
||||||
|
@ -865,7 +926,7 @@ impl AcpThread {
|
||||||
.boxed()
|
.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();
|
let agent = self.connection.clone();
|
||||||
|
|
||||||
if self.send_task.take().is_some() {
|
if self.send_task.take().is_some() {
|
||||||
|
@ -898,13 +959,123 @@ impl AcpThread {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})?;
|
||||||
|
Ok(())
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Task::ready(Ok(()))
|
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<()>>> {
|
pub fn child_status(&mut self) -> Option<Task<Result<()>>> {
|
||||||
self.child_status.take()
|
self.child_status.take()
|
||||||
}
|
}
|
||||||
|
@ -930,7 +1101,7 @@ impl acp::Client for AcpClientDelegate {
|
||||||
async fn stream_assistant_message_chunk(
|
async fn stream_assistant_message_chunk(
|
||||||
&self,
|
&self,
|
||||||
params: acp::StreamAssistantMessageChunkParams,
|
params: acp::StreamAssistantMessageChunkParams,
|
||||||
) -> Result<()> {
|
) -> Result<(), acp::Error> {
|
||||||
let cx = &mut self.cx.clone();
|
let cx = &mut self.cx.clone();
|
||||||
|
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
|
@ -947,45 +1118,37 @@ impl acp::Client for AcpClientDelegate {
|
||||||
async fn request_tool_call_confirmation(
|
async fn request_tool_call_confirmation(
|
||||||
&self,
|
&self,
|
||||||
request: acp::RequestToolCallConfirmationParams,
|
request: acp::RequestToolCallConfirmationParams,
|
||||||
) -> Result<acp::RequestToolCallConfirmationResponse> {
|
) -> Result<acp::RequestToolCallConfirmationResponse, acp::Error> {
|
||||||
let cx = &mut self.cx.clone();
|
let cx = &mut self.cx.clone();
|
||||||
let ToolCallRequest { id, outcome } = cx
|
let ToolCallRequest { id, outcome } = cx
|
||||||
.update(|cx| {
|
.update(|cx| {
|
||||||
self.thread.update(cx, |thread, cx| {
|
self.thread
|
||||||
thread.request_tool_call(
|
.update(cx, |thread, cx| thread.request_tool_call(request, cx))
|
||||||
request.label,
|
|
||||||
request.icon,
|
|
||||||
request.content,
|
|
||||||
request.confirmation,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})?
|
})?
|
||||||
.context("Failed to update thread")?;
|
.context("Failed to update thread")?;
|
||||||
|
|
||||||
Ok(acp::RequestToolCallConfirmationResponse {
|
Ok(acp::RequestToolCallConfirmationResponse {
|
||||||
id,
|
id,
|
||||||
outcome: outcome.await?,
|
outcome: outcome.await.map_err(acp::Error::into_internal_error)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn push_tool_call(
|
async fn push_tool_call(
|
||||||
&self,
|
&self,
|
||||||
request: acp::PushToolCallParams,
|
request: acp::PushToolCallParams,
|
||||||
) -> Result<acp::PushToolCallResponse> {
|
) -> Result<acp::PushToolCallResponse, acp::Error> {
|
||||||
let cx = &mut self.cx.clone();
|
let cx = &mut self.cx.clone();
|
||||||
let id = cx
|
let id = cx
|
||||||
.update(|cx| {
|
.update(|cx| {
|
||||||
self.thread.update(cx, |thread, cx| {
|
self.thread
|
||||||
thread.push_tool_call(request.label, request.icon, request.content, cx)
|
.update(cx, |thread, cx| thread.push_tool_call(request, cx))
|
||||||
})
|
|
||||||
})?
|
})?
|
||||||
.context("Failed to update thread")?;
|
.context("Failed to update thread")?;
|
||||||
|
|
||||||
Ok(acp::PushToolCallResponse { id })
|
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();
|
let cx = &mut self.cx.clone();
|
||||||
|
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
|
@ -997,6 +1160,34 @@ impl acp::Client for AcpClientDelegate {
|
||||||
|
|
||||||
Ok(())
|
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 {
|
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]
|
#[gpui::test]
|
||||||
async fn test_succeeding_canceled_toolcall(cx: &mut TestAppContext) {
|
async fn test_succeeding_canceled_toolcall(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
@ -1124,6 +1389,7 @@ mod tests {
|
||||||
label: "Fetch".to_string(),
|
label: "Fetch".to_string(),
|
||||||
icon: acp::Icon::Globe,
|
icon: acp::Icon::Globe,
|
||||||
content: None,
|
content: None,
|
||||||
|
locations: vec![],
|
||||||
})
|
})
|
||||||
})?
|
})?
|
||||||
.await
|
.await
|
||||||
|
@ -1553,7 +1819,7 @@ mod tests {
|
||||||
acp::SendUserMessageParams,
|
acp::SendUserMessageParams,
|
||||||
Entity<FakeAcpServer>,
|
Entity<FakeAcpServer>,
|
||||||
AsyncApp,
|
AsyncApp,
|
||||||
) -> LocalBoxFuture<'static, Result<()>>,
|
) -> LocalBoxFuture<'static, Result<(), acp::Error>>,
|
||||||
>,
|
>,
|
||||||
>,
|
>,
|
||||||
}
|
}
|
||||||
|
@ -1565,21 +1831,24 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl acp::Agent for FakeAgent {
|
impl acp::Agent for FakeAgent {
|
||||||
async fn initialize(&self) -> Result<acp::InitializeResponse> {
|
async fn initialize(&self) -> Result<acp::InitializeResponse, acp::Error> {
|
||||||
Ok(acp::InitializeResponse {
|
Ok(acp::InitializeResponse {
|
||||||
is_authenticated: true,
|
is_authenticated: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn authenticate(&self) -> Result<()> {
|
async fn authenticate(&self) -> Result<(), acp::Error> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn cancel_send_message(&self) -> Result<()> {
|
async fn cancel_send_message(&self) -> Result<(), acp::Error> {
|
||||||
Ok(())
|
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 mut cx = self.cx.clone();
|
||||||
let handler = self
|
let handler = self
|
||||||
.server
|
.server
|
||||||
|
@ -1589,7 +1858,7 @@ mod tests {
|
||||||
if let Some(handler) = handler {
|
if let Some(handler) = handler {
|
||||||
handler(request, self.server.clone(), self.cx.clone()).await
|
handler(request, self.server.clone(), self.cx.clone()).await
|
||||||
} else {
|
} 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
|
handler: impl for<'a> Fn(acp::SendUserMessageParams, Entity<FakeAcpServer>, AsyncApp) -> F
|
||||||
+ 'static,
|
+ 'static,
|
||||||
) where
|
) where
|
||||||
F: Future<Output = Result<()>> + 'static,
|
F: Future<Output = Result<(), acp::Error>> + 'static,
|
||||||
{
|
{
|
||||||
self.on_user_message
|
self.on_user_message
|
||||||
.replace(Rc::new(move |request, server, cx| {
|
.replace(Rc::new(move |request, server, cx| {
|
||||||
|
|
|
@ -1,33 +1,37 @@
|
||||||
|
use std::collections::BTreeMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use agentic_coding_protocol::{self as acp};
|
use agentic_coding_protocol::{self as acp};
|
||||||
|
use assistant_tool::ActionLog;
|
||||||
|
use buffer_diff::BufferDiff;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use editor::{
|
use editor::{
|
||||||
AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode,
|
AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode,
|
||||||
EditorStyle, MinimapVisibility, MultiBuffer,
|
EditorStyle, MinimapVisibility, MultiBuffer, PathKey,
|
||||||
};
|
};
|
||||||
use file_icons::FileIcons;
|
use file_icons::FileIcons;
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId, Focusable,
|
Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
|
||||||
Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement, Subscription, TextStyle,
|
FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement,
|
||||||
TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, div, list, percentage,
|
Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
|
||||||
prelude::*, pulsating_between,
|
Window, div, linear_color_stop, linear_gradient, list, percentage, point, prelude::*,
|
||||||
|
pulsating_between,
|
||||||
};
|
};
|
||||||
use gpui::{FocusHandle, Task};
|
|
||||||
use language::language_settings::SoftWrap;
|
use language::language_settings::SoftWrap;
|
||||||
use language::{Buffer, Language};
|
use language::{Buffer, Language};
|
||||||
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use settings::Settings as _;
|
use settings::Settings as _;
|
||||||
|
use text::Anchor;
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::{Disclosure, Tooltip, prelude::*};
|
use ui::{Disclosure, Divider, DividerColor, KeyBinding, Tooltip, prelude::*};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::Workspace;
|
use workspace::{CollaboratorId, Workspace};
|
||||||
use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
|
use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
|
||||||
|
|
||||||
use ::acp::{
|
use ::acp::{
|
||||||
|
@ -38,6 +42,8 @@ use ::acp::{
|
||||||
|
|
||||||
use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
|
use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
|
||||||
use crate::acp::message_history::MessageHistory;
|
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.);
|
const RESPONSE_PADDING_X: Pixels = px(19.);
|
||||||
|
|
||||||
|
@ -53,6 +59,7 @@ pub struct AcpThreadView {
|
||||||
auth_task: Option<Task<()>>,
|
auth_task: Option<Task<()>>,
|
||||||
expanded_tool_calls: HashSet<ToolCallId>,
|
expanded_tool_calls: HashSet<ToolCallId>,
|
||||||
expanded_thinking_blocks: HashSet<(usize, usize)>,
|
expanded_thinking_blocks: HashSet<(usize, usize)>,
|
||||||
|
edits_expanded: bool,
|
||||||
message_history: MessageHistory<acp::SendUserMessageParams>,
|
message_history: MessageHistory<acp::SendUserMessageParams>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,7 +69,7 @@ enum ThreadState {
|
||||||
},
|
},
|
||||||
Ready {
|
Ready {
|
||||||
thread: Entity<AcpThread>,
|
thread: Entity<AcpThread>,
|
||||||
_subscription: Subscription,
|
_subscription: [Subscription; 2],
|
||||||
},
|
},
|
||||||
LoadError(LoadError),
|
LoadError(LoadError),
|
||||||
Unauthenticated {
|
Unauthenticated {
|
||||||
|
@ -136,9 +143,9 @@ impl AcpThreadView {
|
||||||
);
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
workspace,
|
workspace: workspace.clone(),
|
||||||
project: project.clone(),
|
project: project.clone(),
|
||||||
thread_state: Self::initial_state(project, window, cx),
|
thread_state: Self::initial_state(workspace, project, window, cx),
|
||||||
message_editor,
|
message_editor,
|
||||||
mention_set,
|
mention_set,
|
||||||
diff_editors: Default::default(),
|
diff_editors: Default::default(),
|
||||||
|
@ -147,11 +154,13 @@ impl AcpThreadView {
|
||||||
auth_task: None,
|
auth_task: None,
|
||||||
expanded_tool_calls: HashSet::default(),
|
expanded_tool_calls: HashSet::default(),
|
||||||
expanded_thinking_blocks: HashSet::default(),
|
expanded_thinking_blocks: HashSet::default(),
|
||||||
|
edits_expanded: false,
|
||||||
message_history: MessageHistory::new(),
|
message_history: MessageHistory::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initial_state(
|
fn initial_state(
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
|
@ -219,15 +228,23 @@ impl AcpThreadView {
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let subscription =
|
let thread_subscription =
|
||||||
cx.subscribe_in(&thread, window, Self::handle_thread_event);
|
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
|
this.list_state
|
||||||
.splice(0..0, thread.read(cx).entries().len());
|
.splice(0..0, thread.read(cx).entries().len());
|
||||||
|
|
||||||
|
AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
|
||||||
|
|
||||||
this.thread_state = ThreadState::Ready {
|
this.thread_state = ThreadState::Ready {
|
||||||
thread,
|
thread,
|
||||||
_subscription: subscription,
|
_subscription: [thread_subscription, action_log_subscription],
|
||||||
};
|
};
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
@ -250,7 +267,7 @@ impl AcpThreadView {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn thread(&self) -> Option<&Entity<AcpThread>> {
|
pub fn thread(&self) -> Option<&Entity<AcpThread>> {
|
||||||
match &self.thread_state {
|
match &self.thread_state {
|
||||||
ThreadState::Ready { thread, .. } | ThreadState::Unauthenticated { thread } => {
|
ThreadState::Ready { thread, .. } | ThreadState::Unauthenticated { thread } => {
|
||||||
Some(thread)
|
Some(thread)
|
||||||
|
@ -281,7 +298,6 @@ impl AcpThreadView {
|
||||||
|
|
||||||
let mut ix = 0;
|
let mut ix = 0;
|
||||||
let mut chunks: Vec<acp::UserMessageChunk> = Vec::new();
|
let mut chunks: Vec<acp::UserMessageChunk> = Vec::new();
|
||||||
|
|
||||||
let project = self.project.clone();
|
let project = self.project.clone();
|
||||||
self.message_editor.update(cx, |editor, cx| {
|
self.message_editor.update(cx, |editor, cx| {
|
||||||
let text = editor.text(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(
|
fn set_draft_message(
|
||||||
message_editor: Entity<Editor>,
|
message_editor: Entity<Editor>,
|
||||||
mention_set: Arc<Mutex<MentionSet>>,
|
mention_set: Arc<Mutex<MentionSet>>,
|
||||||
|
@ -464,7 +507,8 @@ impl AcpThreadView {
|
||||||
let count = self.list_state.item_count();
|
let count = self.list_state.item_count();
|
||||||
match event {
|
match event {
|
||||||
AcpThreadEvent::NewEntry => {
|
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);
|
self.list_state.splice(count..count, 1);
|
||||||
}
|
}
|
||||||
AcpThreadEvent::EntryUpdated(index) => {
|
AcpThreadEvent::EntryUpdated(index) => {
|
||||||
|
@ -537,15 +581,7 @@ impl AcpThreadView {
|
||||||
|
|
||||||
fn entry_diff_multibuffer(&self, entry_ix: usize, cx: &App) -> Option<Entity<MultiBuffer>> {
|
fn entry_diff_multibuffer(&self, entry_ix: usize, cx: &App) -> Option<Entity<MultiBuffer>> {
|
||||||
let entry = self.thread()?.read(cx).entries().get(entry_ix)?;
|
let entry = self.thread()?.read(cx).entries().get(entry_ix)?;
|
||||||
if let AgentThreadEntry::ToolCall(ToolCall {
|
entry.diff().map(|diff| diff.multibuffer.clone())
|
||||||
content: Some(ToolCallContent::Diff { diff }),
|
|
||||||
..
|
|
||||||
}) = &entry
|
|
||||||
{
|
|
||||||
Some(diff.multibuffer.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn authenticate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
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)
|
Markdown::new(format!("Error: {err}").into(), None, None, cx)
|
||||||
}))
|
}))
|
||||||
} else {
|
} 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()
|
this.auth_task.take()
|
||||||
})
|
})
|
||||||
|
@ -1529,6 +1566,357 @@ impl AcpThreadView {
|
||||||
container.into_any()
|
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 {
|
fn render_message_editor(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let settings = ThemeSettings::get_global(cx);
|
let settings = ThemeSettings::get_global(cx);
|
||||||
let font_size = TextSize::Small
|
let font_size = TextSize::Small
|
||||||
|
@ -1559,6 +1947,76 @@ impl AcpThreadView {
|
||||||
.into_any()
|
.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 {
|
fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
|
||||||
let workspace = self.workspace.clone();
|
let workspace = self.workspace.clone();
|
||||||
MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
|
MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
|
||||||
|
@ -1673,10 +2131,6 @@ impl Focusable for AcpThreadView {
|
||||||
|
|
||||||
impl Render for AcpThreadView {
|
impl Render for AcpThreadView {
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
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)
|
let open_as_markdown = IconButton::new("open-as-markdown", IconName::DocumentText)
|
||||||
.icon_size(IconSize::XSmall)
|
.icon_size(IconSize::XSmall)
|
||||||
.icon_color(Color::Ignored)
|
.icon_color(Color::Ignored)
|
||||||
|
@ -1702,6 +2156,7 @@ impl Render for AcpThreadView {
|
||||||
.on_action(cx.listener(Self::chat))
|
.on_action(cx.listener(Self::chat))
|
||||||
.on_action(cx.listener(Self::previous_history_message))
|
.on_action(cx.listener(Self::previous_history_message))
|
||||||
.on_action(cx.listener(Self::next_history_message))
|
.on_action(cx.listener(Self::next_history_message))
|
||||||
|
.on_action(cx.listener(Self::open_agent_diff))
|
||||||
.child(match &self.thread_state {
|
.child(match &self.thread_state {
|
||||||
ThreadState::Unauthenticated { .. } => v_flex()
|
ThreadState::Unauthenticated { .. } => v_flex()
|
||||||
.p_2()
|
.p_2()
|
||||||
|
@ -1755,6 +2210,7 @@ impl Render for AcpThreadView {
|
||||||
.child(LoadingLabel::new("").size(LabelSize::Small))
|
.child(LoadingLabel::new("").size(LabelSize::Small))
|
||||||
.into(),
|
.into(),
|
||||||
})
|
})
|
||||||
|
.children(self.render_edits_bar(&thread, window, cx))
|
||||||
} else {
|
} else {
|
||||||
this.child(self.render_empty_state(false, cx))
|
this.child(self.render_empty_state(false, cx))
|
||||||
}
|
}
|
||||||
|
@ -1782,47 +2238,12 @@ impl Render for AcpThreadView {
|
||||||
.border_t_1()
|
.border_t_1()
|
||||||
.border_color(cx.theme().colors().border)
|
.border_color(cx.theme().colors().border)
|
||||||
.child(self.render_message_editor(cx))
|
.child(self.render_message_editor(cx))
|
||||||
.child({
|
.child(
|
||||||
let thread = self.thread();
|
h_flex()
|
||||||
|
.justify_between()
|
||||||
h_flex().justify_end().child(
|
.child(self.render_follow_toggle(cx))
|
||||||
if thread.map_or(true, |thread| {
|
.child(self.render_send_button(cx)),
|
||||||
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)))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll};
|
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 agent_settings::AgentSettings;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use assistant_tool::ActionLog;
|
||||||
use buffer_diff::DiffHunkStatus;
|
use buffer_diff::DiffHunkStatus;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use editor::{
|
use editor::{
|
||||||
|
@ -41,16 +43,108 @@ use zed_actions::assistant::ToggleFocus;
|
||||||
pub struct AgentDiffPane {
|
pub struct AgentDiffPane {
|
||||||
multibuffer: Entity<MultiBuffer>,
|
multibuffer: Entity<MultiBuffer>,
|
||||||
editor: Entity<Editor>,
|
editor: Entity<Editor>,
|
||||||
thread: Entity<Thread>,
|
thread: AgentDiffThread,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
title: SharedString,
|
title: SharedString,
|
||||||
_subscriptions: Vec<Subscription>,
|
_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 {
|
impl AgentDiffPane {
|
||||||
pub fn deploy(
|
pub fn deploy(
|
||||||
thread: Entity<Thread>,
|
thread: impl Into<AgentDiffThread>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
|
@ -61,14 +155,16 @@ impl AgentDiffPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deploy_in_workspace(
|
pub fn deploy_in_workspace(
|
||||||
thread: Entity<Thread>,
|
thread: impl Into<AgentDiffThread>,
|
||||||
workspace: &mut Workspace,
|
workspace: &mut Workspace,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Workspace>,
|
cx: &mut Context<Workspace>,
|
||||||
) -> Entity<Self> {
|
) -> Entity<Self> {
|
||||||
|
let thread = thread.into();
|
||||||
let existing_diff = workspace
|
let existing_diff = workspace
|
||||||
.items_of_type::<AgentDiffPane>(cx)
|
.items_of_type::<AgentDiffPane>(cx)
|
||||||
.find(|diff| diff.read(cx).thread == thread);
|
.find(|diff| diff.read(cx).thread == thread);
|
||||||
|
|
||||||
if let Some(existing_diff) = existing_diff {
|
if let Some(existing_diff) = existing_diff {
|
||||||
workspace.activate_item(&existing_diff, true, true, window, cx);
|
workspace.activate_item(&existing_diff, true, true, window, cx);
|
||||||
existing_diff
|
existing_diff
|
||||||
|
@ -81,7 +177,7 @@ impl AgentDiffPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
thread: Entity<Thread>,
|
thread: AgentDiffThread,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
|
@ -89,7 +185,7 @@ impl AgentDiffPane {
|
||||||
let focus_handle = cx.focus_handle();
|
let focus_handle = cx.focus_handle();
|
||||||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
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 editor = cx.new(|cx| {
|
||||||
let mut editor =
|
let mut editor =
|
||||||
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
|
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
|
||||||
|
@ -100,16 +196,27 @@ impl AgentDiffPane {
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
|
|
||||||
let action_log = thread.read(cx).action_log().clone();
|
let action_log = thread.action_log(cx).clone();
|
||||||
|
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
_subscriptions: vec![
|
_subscriptions: [
|
||||||
cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
|
Some(
|
||||||
this.update_excerpts(window, cx)
|
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)
|
),
|
||||||
}),
|
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(),
|
title: SharedString::default(),
|
||||||
multibuffer,
|
multibuffer,
|
||||||
editor,
|
editor,
|
||||||
|
@ -123,8 +230,7 @@ impl AgentDiffPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let thread = self.thread.read(cx);
|
let changed_buffers = self.thread.action_log(cx).read(cx).changed_buffers(cx);
|
||||||
let changed_buffers = thread.action_log().read(cx).changed_buffers(cx);
|
|
||||||
let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
|
let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
|
||||||
|
|
||||||
for (buffer, diff_handle) in changed_buffers {
|
for (buffer, diff_handle) in changed_buffers {
|
||||||
|
@ -211,7 +317,7 @@ impl AgentDiffPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_title(&mut self, cx: &mut Context<Self>) {
|
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 {
|
if new_title != self.title {
|
||||||
self.title = new_title;
|
self.title = new_title;
|
||||||
cx.emit(EditorEvent::TitleChanged);
|
cx.emit(EditorEvent::TitleChanged);
|
||||||
|
@ -275,14 +381,15 @@ impl AgentDiffPane {
|
||||||
|
|
||||||
fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
|
fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.thread
|
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(
|
fn keep_edits_in_selection(
|
||||||
editor: &mut Editor,
|
editor: &mut Editor,
|
||||||
buffer_snapshot: &MultiBufferSnapshot,
|
buffer_snapshot: &MultiBufferSnapshot,
|
||||||
thread: &Entity<Thread>,
|
thread: &AgentDiffThread,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
) {
|
) {
|
||||||
|
@ -297,7 +404,7 @@ fn keep_edits_in_selection(
|
||||||
fn reject_edits_in_selection(
|
fn reject_edits_in_selection(
|
||||||
editor: &mut Editor,
|
editor: &mut Editor,
|
||||||
buffer_snapshot: &MultiBufferSnapshot,
|
buffer_snapshot: &MultiBufferSnapshot,
|
||||||
thread: &Entity<Thread>,
|
thread: &AgentDiffThread,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
) {
|
) {
|
||||||
|
@ -311,7 +418,7 @@ fn reject_edits_in_selection(
|
||||||
fn keep_edits_in_ranges(
|
fn keep_edits_in_ranges(
|
||||||
editor: &mut Editor,
|
editor: &mut Editor,
|
||||||
buffer_snapshot: &MultiBufferSnapshot,
|
buffer_snapshot: &MultiBufferSnapshot,
|
||||||
thread: &Entity<Thread>,
|
thread: &AgentDiffThread,
|
||||||
ranges: Vec<Range<editor::Anchor>>,
|
ranges: Vec<Range<editor::Anchor>>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
|
@ -326,8 +433,8 @@ fn keep_edits_in_ranges(
|
||||||
for hunk in &diff_hunks_in_ranges {
|
for hunk in &diff_hunks_in_ranges {
|
||||||
let buffer = multibuffer.read(cx).buffer(hunk.buffer_id);
|
let buffer = multibuffer.read(cx).buffer(hunk.buffer_id);
|
||||||
if let Some(buffer) = buffer {
|
if let Some(buffer) = buffer {
|
||||||
thread.update(cx, |thread, cx| {
|
thread.action_log(cx).update(cx, |action_log, cx| {
|
||||||
thread.keep_edits_in_range(buffer, hunk.buffer_range.clone(), 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(
|
fn reject_edits_in_ranges(
|
||||||
editor: &mut Editor,
|
editor: &mut Editor,
|
||||||
buffer_snapshot: &MultiBufferSnapshot,
|
buffer_snapshot: &MultiBufferSnapshot,
|
||||||
thread: &Entity<Thread>,
|
thread: &AgentDiffThread,
|
||||||
ranges: Vec<Range<editor::Anchor>>,
|
ranges: Vec<Range<editor::Anchor>>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
|
@ -362,8 +469,9 @@ fn reject_edits_in_ranges(
|
||||||
|
|
||||||
for (buffer, ranges) in ranges_by_buffer {
|
for (buffer, ranges) in ranges_by_buffer {
|
||||||
thread
|
thread
|
||||||
.update(cx, |thread, cx| {
|
.action_log(cx)
|
||||||
thread.reject_edits_in_ranges(buffer, ranges, cx)
|
.update(cx, |action_log, cx| {
|
||||||
|
action_log.reject_edits_in_ranges(buffer, ranges, cx)
|
||||||
})
|
})
|
||||||
.detach_and_log_err(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 {
|
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))
|
Label::new(format!("Review: {}", summary))
|
||||||
.color(if params.selected {
|
.color(if params.selected {
|
||||||
Color::Default
|
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();
|
let thread = thread.clone();
|
||||||
|
|
||||||
Arc::new(
|
Arc::new(
|
||||||
|
@ -676,7 +784,7 @@ fn render_diff_hunk_controls(
|
||||||
hunk_range: Range<editor::Anchor>,
|
hunk_range: Range<editor::Anchor>,
|
||||||
is_created_file: bool,
|
is_created_file: bool,
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
thread: &Entity<Thread>,
|
thread: &AgentDiffThread,
|
||||||
editor: &Entity<Editor>,
|
editor: &Entity<Editor>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
|
@ -1112,11 +1220,8 @@ impl Render for AgentDiffToolbar {
|
||||||
return Empty.into_any();
|
return Empty.into_any();
|
||||||
};
|
};
|
||||||
|
|
||||||
let has_pending_edit_tool_use = agent_diff
|
let has_pending_edit_tool_use =
|
||||||
.read(cx)
|
agent_diff.read(cx).thread.has_pending_edit_tool_uses(cx);
|
||||||
.thread
|
|
||||||
.read(cx)
|
|
||||||
.has_pending_edit_tool_uses();
|
|
||||||
|
|
||||||
if has_pending_edit_tool_use {
|
if has_pending_edit_tool_use {
|
||||||
return div().px_2().child(spinner_icon).into_any();
|
return div().px_2().child(spinner_icon).into_any();
|
||||||
|
@ -1187,8 +1292,8 @@ pub enum EditorState {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WorkspaceThread {
|
struct WorkspaceThread {
|
||||||
thread: WeakEntity<Thread>,
|
thread: WeakAgentDiffThread,
|
||||||
_thread_subscriptions: [Subscription; 2],
|
_thread_subscriptions: (Subscription, Subscription),
|
||||||
singleton_editors: HashMap<WeakEntity<Buffer>, HashMap<WeakEntity<Editor>, Subscription>>,
|
singleton_editors: HashMap<WeakEntity<Buffer>, HashMap<WeakEntity<Editor>, Subscription>>,
|
||||||
_settings_subscription: Subscription,
|
_settings_subscription: Subscription,
|
||||||
_workspace_subscription: Option<Subscription>,
|
_workspace_subscription: Option<Subscription>,
|
||||||
|
@ -1212,23 +1317,23 @@ impl AgentDiff {
|
||||||
|
|
||||||
pub fn set_active_thread(
|
pub fn set_active_thread(
|
||||||
workspace: &WeakEntity<Workspace>,
|
workspace: &WeakEntity<Workspace>,
|
||||||
thread: &Entity<Thread>,
|
thread: impl Into<AgentDiffThread>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) {
|
) {
|
||||||
Self::global(cx).update(cx, |this, cx| {
|
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(
|
fn register_active_thread_impl(
|
||||||
&mut self,
|
&mut self,
|
||||||
workspace: &WeakEntity<Workspace>,
|
workspace: &WeakEntity<Workspace>,
|
||||||
thread: &Entity<Thread>,
|
thread: AgentDiffThread,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
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 action_log_subscription = cx.observe_in(&action_log, window, {
|
||||||
let workspace = workspace.clone();
|
let workspace = workspace.clone();
|
||||||
|
@ -1237,17 +1342,25 @@ impl AgentDiff {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let thread_subscription = cx.subscribe_in(&thread, window, {
|
let thread_subscription = match &thread {
|
||||||
let workspace = workspace.clone();
|
AgentDiffThread::Native(thread) => cx.subscribe_in(&thread, window, {
|
||||||
move |this, _thread, event, window, cx| {
|
let workspace = workspace.clone();
|
||||||
this.handle_thread_event(&workspace, event, window, cx)
|
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) {
|
if let Some(workspace_thread) = self.workspace_threads.get_mut(&workspace) {
|
||||||
// replace thread and action log subscription, but keep editors
|
// replace thread and action log subscription, but keep editors
|
||||||
workspace_thread.thread = thread.downgrade();
|
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);
|
self.update_reviewing_editors(&workspace, window, cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1272,7 +1385,7 @@ impl AgentDiff {
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
WorkspaceThread {
|
WorkspaceThread {
|
||||||
thread: thread.downgrade(),
|
thread: thread.downgrade(),
|
||||||
_thread_subscriptions: [action_log_subscription, thread_subscription],
|
_thread_subscriptions: (action_log_subscription, thread_subscription),
|
||||||
singleton_editors: HashMap::default(),
|
singleton_editors: HashMap::default(),
|
||||||
_settings_subscription: settings_subscription,
|
_settings_subscription: settings_subscription,
|
||||||
_workspace_subscription: workspace_subscription,
|
_workspace_subscription: workspace_subscription,
|
||||||
|
@ -1319,7 +1432,7 @@ impl AgentDiff {
|
||||||
|
|
||||||
fn register_review_action<T: Action>(
|
fn register_review_action<T: Action>(
|
||||||
workspace: &mut Workspace,
|
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,
|
+ 'static,
|
||||||
this: &Entity<AgentDiff>,
|
this: &Entity<AgentDiff>,
|
||||||
) {
|
) {
|
||||||
|
@ -1338,7 +1451,7 @@ impl AgentDiff {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_thread_event(
|
fn handle_native_thread_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
workspace: &WeakEntity<Workspace>,
|
workspace: &WeakEntity<Workspace>,
|
||||||
event: &ThreadEvent,
|
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(
|
fn handle_workspace_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
workspace: &Entity<Workspace>,
|
workspace: &Entity<Workspace>,
|
||||||
|
@ -1485,7 +1632,7 @@ impl AgentDiff {
|
||||||
return;
|
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 changed_buffers = action_log.read(cx).changed_buffers(cx);
|
||||||
|
|
||||||
let mut unaffected = self.reviewing_editors.clone();
|
let mut unaffected = self.reviewing_editors.clone();
|
||||||
|
@ -1510,7 +1657,7 @@ impl AgentDiff {
|
||||||
multibuffer.add_diff(diff_handle.clone(), cx);
|
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
|
EditorState::Generating
|
||||||
} else {
|
} else {
|
||||||
EditorState::Reviewing
|
EditorState::Reviewing
|
||||||
|
@ -1606,7 +1753,7 @@ impl AgentDiff {
|
||||||
|
|
||||||
fn keep_all(
|
fn keep_all(
|
||||||
editor: &Entity<Editor>,
|
editor: &Entity<Editor>,
|
||||||
thread: &Entity<Thread>,
|
thread: &AgentDiffThread,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> PostReviewState {
|
) -> PostReviewState {
|
||||||
|
@ -1626,7 +1773,7 @@ impl AgentDiff {
|
||||||
|
|
||||||
fn reject_all(
|
fn reject_all(
|
||||||
editor: &Entity<Editor>,
|
editor: &Entity<Editor>,
|
||||||
thread: &Entity<Thread>,
|
thread: &AgentDiffThread,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> PostReviewState {
|
) -> PostReviewState {
|
||||||
|
@ -1646,7 +1793,7 @@ impl AgentDiff {
|
||||||
|
|
||||||
fn keep(
|
fn keep(
|
||||||
editor: &Entity<Editor>,
|
editor: &Entity<Editor>,
|
||||||
thread: &Entity<Thread>,
|
thread: &AgentDiffThread,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> PostReviewState {
|
) -> PostReviewState {
|
||||||
|
@ -1659,7 +1806,7 @@ impl AgentDiff {
|
||||||
|
|
||||||
fn reject(
|
fn reject(
|
||||||
editor: &Entity<Editor>,
|
editor: &Entity<Editor>,
|
||||||
thread: &Entity<Thread>,
|
thread: &AgentDiffThread,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> PostReviewState {
|
) -> PostReviewState {
|
||||||
|
@ -1682,7 +1829,7 @@ impl AgentDiff {
|
||||||
fn review_in_active_editor(
|
fn review_in_active_editor(
|
||||||
&mut self,
|
&mut self,
|
||||||
workspace: &mut Workspace,
|
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,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Option<Task<Result<()>>> {
|
) -> Option<Task<Result<()>>> {
|
||||||
|
@ -1703,7 +1850,7 @@ impl AgentDiff {
|
||||||
|
|
||||||
if let PostReviewState::AllReviewed = review(&editor, &thread, window, cx) {
|
if let PostReviewState::AllReviewed = review(&editor, &thread, window, cx) {
|
||||||
if let Some(curr_buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
|
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();
|
let mut keys = changed_buffers.keys().cycle();
|
||||||
keys.find(|k| *k == &curr_buffer);
|
keys.find(|k| *k == &curr_buffer);
|
||||||
|
@ -1801,8 +1948,9 @@ mod tests {
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
let thread =
|
||||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
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) =
|
let (workspace, cx) =
|
||||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||||
|
@ -1988,8 +2136,9 @@ mod tests {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set the active thread
|
// Set the active thread
|
||||||
|
let thread = AgentDiffThread::Native(thread);
|
||||||
cx.update(|window, cx| {
|
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
|
let buffer1 = project
|
||||||
|
|
|
@ -8,6 +8,7 @@ use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::NewAcpThread;
|
use crate::NewAcpThread;
|
||||||
|
use crate::agent_diff::AgentDiffThread;
|
||||||
use crate::language_model_selector::ToggleModelSelector;
|
use crate::language_model_selector::ToggleModelSelector;
|
||||||
use crate::{
|
use crate::{
|
||||||
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
|
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();
|
let weak_panel = weak_self.clone();
|
||||||
|
|
||||||
|
@ -845,7 +846,7 @@ impl AgentPanel {
|
||||||
let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
|
let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
|
||||||
self.set_active_view(thread_view, 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>) {
|
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| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
let thread_view = cx.new_window_entity(|window, 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.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();
|
.detach();
|
||||||
}
|
}
|
||||||
|
@ -1050,7 +1060,7 @@ impl AgentPanel {
|
||||||
|
|
||||||
let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
|
let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
|
||||||
self.set_active_view(thread_view, 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>) {
|
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();
|
let thread = thread.read(cx).thread().clone();
|
||||||
self.workspace
|
self.workspace
|
||||||
.update(cx, |workspace, cx| {
|
.update(cx, |workspace, cx| {
|
||||||
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx)
|
AgentDiffPane::deploy_in_workspace(
|
||||||
|
AgentDiffThread::Native(thread),
|
||||||
|
workspace,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ use std::collections::BTreeMap;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::agent_diff::AgentDiffThread;
|
||||||
use crate::agent_model_selector::AgentModelSelector;
|
use crate::agent_model_selector::AgentModelSelector;
|
||||||
use crate::language_model_selector::ToggleModelSelector;
|
use crate::language_model_selector::ToggleModelSelector;
|
||||||
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
|
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
|
||||||
|
@ -475,9 +476,12 @@ impl MessageEditor {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
if let Ok(diff) =
|
if let Ok(diff) = AgentDiffPane::deploy(
|
||||||
AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx)
|
AgentDiffThread::Native(self.thread.clone()),
|
||||||
{
|
self.workspace.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
) {
|
||||||
let path_key = multi_buffer::PathKey::for_buffer(&buffer, cx);
|
let path_key = multi_buffer::PathKey::for_buffer(&buffer, cx);
|
||||||
diff.update(cx, |diff, cx| diff.move_to_path(path_key, window, 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 project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
|
||||||
use std::{cmp, ops::Range, sync::Arc};
|
use std::{cmp, ops::Range, sync::Arc};
|
||||||
use text::{Edit, Patch, Rope};
|
use text::{Edit, Patch, Rope};
|
||||||
use util::RangeExt;
|
use util::{RangeExt, ResultExt as _};
|
||||||
|
|
||||||
/// Tracks actions performed by tools in a thread
|
/// Tracks actions performed by tools in a thread
|
||||||
pub struct ActionLog {
|
pub struct ActionLog {
|
||||||
|
@ -47,6 +47,10 @@ impl ActionLog {
|
||||||
self.edited_since_project_diagnostics_check
|
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(
|
fn track_buffer_internal(
|
||||||
&mut self,
|
&mut self,
|
||||||
buffer: Entity<Buffer>,
|
buffer: Entity<Buffer>,
|
||||||
|
@ -715,6 +719,22 @@ impl ActionLog {
|
||||||
cx.notify();
|
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.
|
/// 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>> {
|
pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, Entity<BufferDiff>> {
|
||||||
self.tracked_buffers
|
self.tracked_buffers
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue