Refactor to use new ACP crate (#35043)
This will prepare us for running the protocol over MCP Release Notes: - N/A --------- Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com> Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com> Co-authored-by: Richard Feldman <oss@rtfeldman.com>
This commit is contained in:
parent
45ddf32a1d
commit
2d0f10c48a
21 changed files with 1830 additions and 1748 deletions
|
@ -17,10 +17,10 @@ test-support = ["gpui/test-support", "language/test-support"]
|
|||
|
||||
[dependencies]
|
||||
acp_thread.workspace = true
|
||||
agent-client-protocol.workspace = true
|
||||
agent.workspace = true
|
||||
agentic-coding-protocol.workspace = true
|
||||
agent_settings.workspace = true
|
||||
agent_servers.workspace = true
|
||||
agent_settings.workspace = true
|
||||
ai_onboarding.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_context.workspace = true
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use acp_thread::Plan;
|
||||
use acp_thread::{AgentConnection, Plan};
|
||||
use agent_servers::AgentServer;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
|
@ -7,7 +7,7 @@ use std::rc::Rc;
|
|||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use agentic_coding_protocol::{self as acp};
|
||||
use agent_client_protocol as acp;
|
||||
use assistant_tool::ActionLog;
|
||||
use buffer_diff::BufferDiff;
|
||||
use collections::{HashMap, HashSet};
|
||||
|
@ -16,7 +16,6 @@ use editor::{
|
|||
EditorStyle, MinimapVisibility, MultiBuffer, PathKey,
|
||||
};
|
||||
use file_icons::FileIcons;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
|
||||
FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement,
|
||||
|
@ -39,8 +38,7 @@ use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
|
|||
|
||||
use ::acp_thread::{
|
||||
AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, Diff,
|
||||
LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallConfirmation, ToolCallContent,
|
||||
ToolCallId, ToolCallStatus,
|
||||
LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus,
|
||||
};
|
||||
|
||||
use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
|
||||
|
@ -64,12 +62,13 @@ pub struct AcpThreadView {
|
|||
last_error: Option<Entity<Markdown>>,
|
||||
list_state: ListState,
|
||||
auth_task: Option<Task<()>>,
|
||||
expanded_tool_calls: HashSet<ToolCallId>,
|
||||
expanded_tool_calls: HashSet<acp::ToolCallId>,
|
||||
expanded_thinking_blocks: HashSet<(usize, usize)>,
|
||||
edits_expanded: bool,
|
||||
plan_expanded: bool,
|
||||
editor_expanded: bool,
|
||||
message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
|
||||
message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
|
||||
_cancel_task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
enum ThreadState {
|
||||
|
@ -82,22 +81,16 @@ enum ThreadState {
|
|||
},
|
||||
LoadError(LoadError),
|
||||
Unauthenticated {
|
||||
thread: Entity<AcpThread>,
|
||||
connection: Rc<dyn AgentConnection>,
|
||||
},
|
||||
}
|
||||
|
||||
struct AlwaysAllowOption {
|
||||
id: &'static str,
|
||||
label: SharedString,
|
||||
outcome: acp::ToolCallConfirmationOutcome,
|
||||
}
|
||||
|
||||
impl AcpThreadView {
|
||||
pub fn new(
|
||||
agent: Rc<dyn AgentServer>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
|
||||
message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
|
||||
min_lines: usize,
|
||||
max_lines: Option<usize>,
|
||||
window: &mut Window,
|
||||
|
@ -191,6 +184,7 @@ impl AcpThreadView {
|
|||
plan_expanded: false,
|
||||
editor_expanded: false,
|
||||
message_history,
|
||||
_cancel_task: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -208,9 +202,9 @@ impl AcpThreadView {
|
|||
.map(|worktree| worktree.read(cx).abs_path())
|
||||
.unwrap_or_else(|| paths::home_dir().as_path().into());
|
||||
|
||||
let task = agent.new_thread(&root_dir, &project, cx);
|
||||
let connect_task = agent.connect(&root_dir, &project, cx);
|
||||
let load_task = cx.spawn_in(window, async move |this, cx| {
|
||||
let thread = match task.await {
|
||||
let connection = match connect_task.await {
|
||||
Ok(thread) => thread,
|
||||
Err(err) => {
|
||||
this.update(cx, |this, cx| {
|
||||
|
@ -222,48 +216,30 @@ impl AcpThreadView {
|
|||
}
|
||||
};
|
||||
|
||||
let init_response = async {
|
||||
let resp = thread
|
||||
.read_with(cx, |thread, _cx| thread.initialize())?
|
||||
.await?;
|
||||
anyhow::Ok(resp)
|
||||
};
|
||||
|
||||
let result = match init_response.await {
|
||||
let result = match connection
|
||||
.clone()
|
||||
.new_thread(project.clone(), &root_dir, cx)
|
||||
.await
|
||||
{
|
||||
Err(e) => {
|
||||
let mut cx = cx.clone();
|
||||
if e.downcast_ref::<oneshot::Canceled>().is_some() {
|
||||
let child_status = thread
|
||||
.update(&mut cx, |thread, _| thread.child_status())
|
||||
.ok()
|
||||
.flatten();
|
||||
if let Some(child_status) = child_status {
|
||||
match child_status.await {
|
||||
Ok(_) => Err(e),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
if e.downcast_ref::<acp_thread::Unauthenticated>().is_some() {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.thread_state = ThreadState::Unauthenticated { connection };
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
return;
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
Ok(response) => {
|
||||
if !response.is_authenticated {
|
||||
this.update(cx, |this, _| {
|
||||
this.thread_state = ThreadState::Unauthenticated { thread };
|
||||
})
|
||||
.ok();
|
||||
return;
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
Ok(session_id) => Ok(session_id),
|
||||
};
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
match result {
|
||||
Ok(()) => {
|
||||
Ok(thread) => {
|
||||
let thread_subscription =
|
||||
cx.subscribe_in(&thread, window, Self::handle_thread_event);
|
||||
|
||||
|
@ -305,10 +281,10 @@ impl AcpThreadView {
|
|||
|
||||
pub fn thread(&self) -> Option<&Entity<AcpThread>> {
|
||||
match &self.thread_state {
|
||||
ThreadState::Ready { thread, .. } | ThreadState::Unauthenticated { thread } => {
|
||||
Some(thread)
|
||||
}
|
||||
ThreadState::Loading { .. } | ThreadState::LoadError(..) => None,
|
||||
ThreadState::Ready { thread, .. } => Some(thread),
|
||||
ThreadState::Unauthenticated { .. }
|
||||
| ThreadState::Loading { .. }
|
||||
| ThreadState::LoadError(..) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -325,7 +301,7 @@ impl AcpThreadView {
|
|||
self.last_error.take();
|
||||
|
||||
if let Some(thread) = self.thread() {
|
||||
thread.update(cx, |thread, cx| thread.cancel(cx)).detach();
|
||||
self._cancel_task = Some(thread.update(cx, |thread, cx| thread.cancel(cx)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -362,7 +338,7 @@ impl AcpThreadView {
|
|||
self.last_error.take();
|
||||
|
||||
let mut ix = 0;
|
||||
let mut chunks: Vec<acp::UserMessageChunk> = Vec::new();
|
||||
let mut chunks: Vec<acp::ContentBlock> = Vec::new();
|
||||
let project = self.project.clone();
|
||||
self.message_editor.update(cx, |editor, cx| {
|
||||
let text = editor.text(cx);
|
||||
|
@ -374,12 +350,19 @@ impl AcpThreadView {
|
|||
{
|
||||
let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
|
||||
if crease_range.start > ix {
|
||||
chunks.push(acp::UserMessageChunk::Text {
|
||||
text: text[ix..crease_range.start].to_string(),
|
||||
});
|
||||
chunks.push(text[ix..crease_range.start].into());
|
||||
}
|
||||
if let Some(abs_path) = project.read(cx).absolute_path(&project_path, cx) {
|
||||
chunks.push(acp::UserMessageChunk::Path { path: abs_path });
|
||||
let path_str = abs_path.display().to_string();
|
||||
chunks.push(acp::ContentBlock::ResourceLink(acp::ResourceLink {
|
||||
uri: path_str.clone(),
|
||||
name: path_str,
|
||||
annotations: None,
|
||||
description: None,
|
||||
mime_type: None,
|
||||
size: None,
|
||||
title: None,
|
||||
}));
|
||||
}
|
||||
ix = crease_range.end;
|
||||
}
|
||||
|
@ -388,9 +371,7 @@ impl AcpThreadView {
|
|||
if ix < text.len() {
|
||||
let last_chunk = text[ix..].trim();
|
||||
if !last_chunk.is_empty() {
|
||||
chunks.push(acp::UserMessageChunk::Text {
|
||||
text: last_chunk.into(),
|
||||
});
|
||||
chunks.push(last_chunk.into());
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -401,8 +382,7 @@ impl AcpThreadView {
|
|||
}
|
||||
|
||||
let Some(thread) = self.thread() else { return };
|
||||
let message = acp::SendUserMessageParams { chunks };
|
||||
let task = thread.update(cx, |thread, cx| thread.send(message.clone(), cx));
|
||||
let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx));
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let result = task.await;
|
||||
|
@ -424,7 +404,7 @@ impl AcpThreadView {
|
|||
editor.remove_creases(mention_set.lock().drain(), cx)
|
||||
});
|
||||
|
||||
self.message_history.borrow_mut().push(message);
|
||||
self.message_history.borrow_mut().push(chunks);
|
||||
}
|
||||
|
||||
fn previous_history_message(
|
||||
|
@ -490,7 +470,7 @@ impl AcpThreadView {
|
|||
message_editor: Entity<Editor>,
|
||||
mention_set: Arc<Mutex<MentionSet>>,
|
||||
project: Entity<Project>,
|
||||
message: Option<&acp::SendUserMessageParams>,
|
||||
message: Option<&Vec<acp::ContentBlock>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
|
@ -503,18 +483,19 @@ impl AcpThreadView {
|
|||
let mut text = String::new();
|
||||
let mut mentions = Vec::new();
|
||||
|
||||
for chunk in &message.chunks {
|
||||
for chunk in message {
|
||||
match chunk {
|
||||
acp::UserMessageChunk::Text { text: chunk } => {
|
||||
text.push_str(&chunk);
|
||||
acp::ContentBlock::Text(text_content) => {
|
||||
text.push_str(&text_content.text);
|
||||
}
|
||||
acp::UserMessageChunk::Path { path } => {
|
||||
acp::ContentBlock::ResourceLink(resource_link) => {
|
||||
let path = Path::new(&resource_link.uri);
|
||||
let start = text.len();
|
||||
let content = MentionPath::new(path).to_string();
|
||||
let content = MentionPath::new(&path).to_string();
|
||||
text.push_str(&content);
|
||||
let end = text.len();
|
||||
if let Some(project_path) =
|
||||
project.read(cx).project_path_for_absolute_path(path, cx)
|
||||
project.read(cx).project_path_for_absolute_path(&path, cx)
|
||||
{
|
||||
let filename: SharedString = path
|
||||
.file_name()
|
||||
|
@ -525,6 +506,9 @@ impl AcpThreadView {
|
|||
mentions.push((start..end, project_path, filename));
|
||||
}
|
||||
}
|
||||
acp::ContentBlock::Image(_)
|
||||
| acp::ContentBlock::Audio(_)
|
||||
| acp::ContentBlock::Resource(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -590,71 +574,79 @@ impl AcpThreadView {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(multibuffer) = self.entry_diff_multibuffer(entry_ix, cx) else {
|
||||
let Some(multibuffers) = self.entry_diff_multibuffers(entry_ix, cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if self.diff_editors.contains_key(&multibuffer.entity_id()) {
|
||||
return;
|
||||
}
|
||||
let multibuffers = multibuffers.collect::<Vec<_>>();
|
||||
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::new(
|
||||
EditorMode::Full {
|
||||
scale_ui_elements_with_buffer_font_size: false,
|
||||
show_active_line_background: false,
|
||||
sized_by_content: true,
|
||||
},
|
||||
multibuffer.clone(),
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.disable_inline_diagnostics();
|
||||
editor.disable_expand_excerpt_buttons(cx);
|
||||
editor.set_show_vertical_scrollbar(false, cx);
|
||||
editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::None, cx);
|
||||
editor.scroll_manager.set_forbid_vertical_scroll(true);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_breakpoints(false, cx);
|
||||
editor.set_show_code_actions(false, cx);
|
||||
editor.set_show_git_diff_gutter(false, cx);
|
||||
editor.set_expand_all_diff_hunks(cx);
|
||||
editor.set_text_style_refinement(TextStyleRefinement {
|
||||
font_size: Some(
|
||||
TextSize::Small
|
||||
.rems(cx)
|
||||
.to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
|
||||
.into(),
|
||||
),
|
||||
..Default::default()
|
||||
for multibuffer in multibuffers {
|
||||
if self.diff_editors.contains_key(&multibuffer.entity_id()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::new(
|
||||
EditorMode::Full {
|
||||
scale_ui_elements_with_buffer_font_size: false,
|
||||
show_active_line_background: false,
|
||||
sized_by_content: true,
|
||||
},
|
||||
multibuffer.clone(),
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.disable_inline_diagnostics();
|
||||
editor.disable_expand_excerpt_buttons(cx);
|
||||
editor.set_show_vertical_scrollbar(false, cx);
|
||||
editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::None, cx);
|
||||
editor.scroll_manager.set_forbid_vertical_scroll(true);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_breakpoints(false, cx);
|
||||
editor.set_show_code_actions(false, cx);
|
||||
editor.set_show_git_diff_gutter(false, cx);
|
||||
editor.set_expand_all_diff_hunks(cx);
|
||||
editor.set_text_style_refinement(TextStyleRefinement {
|
||||
font_size: Some(
|
||||
TextSize::Small
|
||||
.rems(cx)
|
||||
.to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
|
||||
.into(),
|
||||
),
|
||||
..Default::default()
|
||||
});
|
||||
editor
|
||||
});
|
||||
editor
|
||||
});
|
||||
let entity_id = multibuffer.entity_id();
|
||||
cx.observe_release(&multibuffer, move |this, _, _| {
|
||||
this.diff_editors.remove(&entity_id);
|
||||
})
|
||||
.detach();
|
||||
let entity_id = multibuffer.entity_id();
|
||||
cx.observe_release(&multibuffer, move |this, _, _| {
|
||||
this.diff_editors.remove(&entity_id);
|
||||
})
|
||||
.detach();
|
||||
|
||||
self.diff_editors.insert(entity_id, editor);
|
||||
self.diff_editors.insert(entity_id, editor);
|
||||
}
|
||||
}
|
||||
|
||||
fn entry_diff_multibuffer(&self, entry_ix: usize, cx: &App) -> Option<Entity<MultiBuffer>> {
|
||||
fn entry_diff_multibuffers(
|
||||
&self,
|
||||
entry_ix: usize,
|
||||
cx: &App,
|
||||
) -> Option<impl Iterator<Item = Entity<MultiBuffer>>> {
|
||||
let entry = self.thread()?.read(cx).entries().get(entry_ix)?;
|
||||
entry.diff().map(|diff| diff.multibuffer.clone())
|
||||
Some(entry.diffs().map(|diff| diff.multibuffer.clone()))
|
||||
}
|
||||
|
||||
fn authenticate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(thread) = self.thread().cloned() else {
|
||||
let ThreadState::Unauthenticated { ref connection } = self.thread_state else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.last_error.take();
|
||||
let authenticate = thread.read(cx).authenticate();
|
||||
let authenticate = connection.authenticate(cx);
|
||||
self.auth_task = Some(cx.spawn_in(window, {
|
||||
let project = self.project.clone();
|
||||
let agent = self.agent.clone();
|
||||
|
@ -684,15 +676,16 @@ impl AcpThreadView {
|
|||
|
||||
fn authorize_tool_call(
|
||||
&mut self,
|
||||
id: ToolCallId,
|
||||
outcome: acp::ToolCallConfirmationOutcome,
|
||||
tool_call_id: acp::ToolCallId,
|
||||
option_id: acp::PermissionOptionId,
|
||||
option_kind: acp::PermissionOptionKind,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(thread) = self.thread() else {
|
||||
return;
|
||||
};
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.authorize_tool_call(id, outcome, cx);
|
||||
thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
@ -719,10 +712,12 @@ impl AcpThreadView {
|
|||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.text_xs()
|
||||
.child(self.render_markdown(
|
||||
message.content.clone(),
|
||||
user_message_markdown_style(window, cx),
|
||||
)),
|
||||
.children(message.content.markdown().map(|md| {
|
||||
self.render_markdown(
|
||||
md.clone(),
|
||||
user_message_markdown_style(window, cx),
|
||||
)
|
||||
})),
|
||||
)
|
||||
.into_any(),
|
||||
AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
|
||||
|
@ -730,20 +725,28 @@ impl AcpThreadView {
|
|||
let message_body = v_flex()
|
||||
.w_full()
|
||||
.gap_2p5()
|
||||
.children(chunks.iter().enumerate().map(|(chunk_ix, chunk)| {
|
||||
match chunk {
|
||||
AssistantMessageChunk::Text { chunk } => self
|
||||
.render_markdown(chunk.clone(), style.clone())
|
||||
.into_any_element(),
|
||||
AssistantMessageChunk::Thought { chunk } => self.render_thinking_block(
|
||||
index,
|
||||
chunk_ix,
|
||||
chunk.clone(),
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
}
|
||||
}))
|
||||
.children(chunks.iter().enumerate().filter_map(
|
||||
|(chunk_ix, chunk)| match chunk {
|
||||
AssistantMessageChunk::Message { block } => {
|
||||
block.markdown().map(|md| {
|
||||
self.render_markdown(md.clone(), style.clone())
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
AssistantMessageChunk::Thought { block } => {
|
||||
block.markdown().map(|md| {
|
||||
self.render_thinking_block(
|
||||
index,
|
||||
chunk_ix,
|
||||
md.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
},
|
||||
))
|
||||
.into_any();
|
||||
|
||||
v_flex()
|
||||
|
@ -871,7 +874,7 @@ impl AcpThreadView {
|
|||
let status_icon = match &tool_call.status {
|
||||
ToolCallStatus::WaitingForConfirmation { .. } => None,
|
||||
ToolCallStatus::Allowed {
|
||||
status: acp::ToolCallStatus::Running,
|
||||
status: acp::ToolCallStatus::InProgress,
|
||||
..
|
||||
} => Some(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
|
@ -885,13 +888,13 @@ impl AcpThreadView {
|
|||
.into_any(),
|
||||
),
|
||||
ToolCallStatus::Allowed {
|
||||
status: acp::ToolCallStatus::Finished,
|
||||
status: acp::ToolCallStatus::Completed,
|
||||
..
|
||||
} => None,
|
||||
ToolCallStatus::Rejected
|
||||
| ToolCallStatus::Canceled
|
||||
| ToolCallStatus::Allowed {
|
||||
status: acp::ToolCallStatus::Error,
|
||||
status: acp::ToolCallStatus::Failed,
|
||||
..
|
||||
} => Some(
|
||||
Icon::new(IconName::X)
|
||||
|
@ -909,34 +912,9 @@ impl AcpThreadView {
|
|||
.any(|content| matches!(content, ToolCallContent::Diff { .. })),
|
||||
};
|
||||
|
||||
let is_collapsible = tool_call.content.is_some() && !needs_confirmation;
|
||||
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
|
||||
let is_open = !is_collapsible || self.expanded_tool_calls.contains(&tool_call.id);
|
||||
|
||||
let content = if is_open {
|
||||
match &tool_call.status {
|
||||
ToolCallStatus::WaitingForConfirmation { confirmation, .. } => {
|
||||
Some(self.render_tool_call_confirmation(
|
||||
tool_call.id,
|
||||
confirmation,
|
||||
tool_call.content.as_ref(),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => {
|
||||
tool_call.content.as_ref().map(|content| {
|
||||
div()
|
||||
.py_1p5()
|
||||
.child(self.render_tool_call_content(content, window, cx))
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
ToolCallStatus::Rejected => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.when(needs_confirmation, |this| {
|
||||
this.rounded_lg()
|
||||
|
@ -976,9 +954,17 @@ impl AcpThreadView {
|
|||
})
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(tool_call.icon)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
Icon::new(match tool_call.kind {
|
||||
acp::ToolKind::Read => IconName::ToolRead,
|
||||
acp::ToolKind::Edit => IconName::ToolPencil,
|
||||
acp::ToolKind::Search => IconName::ToolSearch,
|
||||
acp::ToolKind::Execute => IconName::ToolTerminal,
|
||||
acp::ToolKind::Think => IconName::ToolBulb,
|
||||
acp::ToolKind::Fetch => IconName::ToolWeb,
|
||||
acp::ToolKind::Other => IconName::ToolHammer,
|
||||
})
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(if tool_call.locations.len() == 1 {
|
||||
let name = tool_call.locations[0]
|
||||
|
@ -1023,16 +1009,16 @@ impl AcpThreadView {
|
|||
.gap_0p5()
|
||||
.when(is_collapsible, |this| {
|
||||
this.child(
|
||||
Disclosure::new(("expand", tool_call.id.0), is_open)
|
||||
Disclosure::new(("expand", entry_ix), is_open)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.on_click(cx.listener({
|
||||
let id = tool_call.id;
|
||||
let id = tool_call.id.clone();
|
||||
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
|
||||
if is_open {
|
||||
this.expanded_tool_calls.remove(&id);
|
||||
} else {
|
||||
this.expanded_tool_calls.insert(id);
|
||||
this.expanded_tool_calls.insert(id.clone());
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
@ -1042,12 +1028,12 @@ impl AcpThreadView {
|
|||
.children(status_icon),
|
||||
)
|
||||
.on_click(cx.listener({
|
||||
let id = tool_call.id;
|
||||
let id = tool_call.id.clone();
|
||||
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
|
||||
if is_open {
|
||||
this.expanded_tool_calls.remove(&id);
|
||||
} else {
|
||||
this.expanded_tool_calls.insert(id);
|
||||
this.expanded_tool_calls.insert(id.clone());
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
@ -1055,7 +1041,7 @@ impl AcpThreadView {
|
|||
)
|
||||
.when(is_open, |this| {
|
||||
this.child(
|
||||
div()
|
||||
v_flex()
|
||||
.text_xs()
|
||||
.when(is_collapsible, |this| {
|
||||
this.mt_1()
|
||||
|
@ -1064,7 +1050,44 @@ impl AcpThreadView {
|
|||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_lg()
|
||||
})
|
||||
.children(content),
|
||||
.map(|this| {
|
||||
if is_open {
|
||||
match &tool_call.status {
|
||||
ToolCallStatus::WaitingForConfirmation { options, .. } => this
|
||||
.children(tool_call.content.iter().map(|content| {
|
||||
div()
|
||||
.py_1p5()
|
||||
.child(
|
||||
self.render_tool_call_content(
|
||||
content, window, cx,
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}))
|
||||
.child(self.render_permission_buttons(
|
||||
options,
|
||||
entry_ix,
|
||||
tool_call.id.clone(),
|
||||
cx,
|
||||
)),
|
||||
ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => {
|
||||
this.children(tool_call.content.iter().map(|content| {
|
||||
div()
|
||||
.py_1p5()
|
||||
.child(
|
||||
self.render_tool_call_content(
|
||||
content, window, cx,
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}))
|
||||
}
|
||||
ToolCallStatus::Rejected => this,
|
||||
}
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
@ -1076,14 +1099,20 @@ impl AcpThreadView {
|
|||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
match content {
|
||||
ToolCallContent::Markdown { markdown } => {
|
||||
div()
|
||||
.p_2()
|
||||
.child(self.render_markdown(
|
||||
markdown.clone(),
|
||||
default_markdown_style(false, window, cx),
|
||||
))
|
||||
.into_any_element()
|
||||
ToolCallContent::ContentBlock { content } => {
|
||||
if let Some(md) = content.markdown() {
|
||||
div()
|
||||
.p_2()
|
||||
.child(
|
||||
self.render_markdown(
|
||||
md.clone(),
|
||||
default_markdown_style(false, window, cx),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Empty.into_any_element()
|
||||
}
|
||||
}
|
||||
ToolCallContent::Diff {
|
||||
diff: Diff { multibuffer, .. },
|
||||
|
@ -1092,223 +1121,53 @@ impl AcpThreadView {
|
|||
}
|
||||
}
|
||||
|
||||
fn render_tool_call_confirmation(
|
||||
fn render_permission_buttons(
|
||||
&self,
|
||||
tool_call_id: ToolCallId,
|
||||
confirmation: &ToolCallConfirmation,
|
||||
content: Option<&ToolCallContent>,
|
||||
window: &Window,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let confirmation_container = v_flex().mt_1().py_1p5();
|
||||
|
||||
match confirmation {
|
||||
ToolCallConfirmation::Edit { description } => confirmation_container
|
||||
.child(
|
||||
div()
|
||||
.px_2()
|
||||
.children(description.clone().map(|description| {
|
||||
self.render_markdown(
|
||||
description,
|
||||
default_markdown_style(false, window, cx),
|
||||
)
|
||||
})),
|
||||
)
|
||||
.children(content.map(|content| self.render_tool_call_content(content, window, cx)))
|
||||
.child(self.render_confirmation_buttons(
|
||||
&[AlwaysAllowOption {
|
||||
id: "always_allow",
|
||||
label: "Always Allow Edits".into(),
|
||||
outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
}],
|
||||
tool_call_id,
|
||||
cx,
|
||||
))
|
||||
.into_any(),
|
||||
ToolCallConfirmation::Execute {
|
||||
command,
|
||||
root_command,
|
||||
description,
|
||||
} => confirmation_container
|
||||
.child(v_flex().px_2().pb_1p5().child(command.clone()).children(
|
||||
description.clone().map(|description| {
|
||||
self.render_markdown(description, default_markdown_style(false, window, cx))
|
||||
.on_url_click({
|
||||
let workspace = self.workspace.clone();
|
||||
move |text, window, cx| {
|
||||
Self::open_link(text, &workspace, window, cx);
|
||||
}
|
||||
})
|
||||
}),
|
||||
))
|
||||
.children(content.map(|content| self.render_tool_call_content(content, window, cx)))
|
||||
.child(self.render_confirmation_buttons(
|
||||
&[AlwaysAllowOption {
|
||||
id: "always_allow",
|
||||
label: format!("Always Allow {root_command}").into(),
|
||||
outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
}],
|
||||
tool_call_id,
|
||||
cx,
|
||||
))
|
||||
.into_any(),
|
||||
ToolCallConfirmation::Mcp {
|
||||
server_name,
|
||||
tool_name: _,
|
||||
tool_display_name,
|
||||
description,
|
||||
} => confirmation_container
|
||||
.child(
|
||||
v_flex()
|
||||
.px_2()
|
||||
.pb_1p5()
|
||||
.child(format!("{server_name} - {tool_display_name}"))
|
||||
.children(description.clone().map(|description| {
|
||||
self.render_markdown(
|
||||
description,
|
||||
default_markdown_style(false, window, cx),
|
||||
)
|
||||
})),
|
||||
)
|
||||
.children(content.map(|content| self.render_tool_call_content(content, window, cx)))
|
||||
.child(self.render_confirmation_buttons(
|
||||
&[
|
||||
AlwaysAllowOption {
|
||||
id: "always_allow_server",
|
||||
label: format!("Always Allow {server_name}").into(),
|
||||
outcome: acp::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
|
||||
},
|
||||
AlwaysAllowOption {
|
||||
id: "always_allow_tool",
|
||||
label: format!("Always Allow {tool_display_name}").into(),
|
||||
outcome: acp::ToolCallConfirmationOutcome::AlwaysAllowTool,
|
||||
},
|
||||
],
|
||||
tool_call_id,
|
||||
cx,
|
||||
))
|
||||
.into_any(),
|
||||
ToolCallConfirmation::Fetch { description, urls } => confirmation_container
|
||||
.child(
|
||||
v_flex()
|
||||
.px_2()
|
||||
.pb_1p5()
|
||||
.gap_1()
|
||||
.children(urls.iter().map(|url| {
|
||||
h_flex().child(
|
||||
Button::new(url.clone(), url)
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click({
|
||||
let url = url.clone();
|
||||
move |_, _, cx| cx.open_url(&url)
|
||||
}),
|
||||
)
|
||||
}))
|
||||
.children(description.clone().map(|description| {
|
||||
self.render_markdown(
|
||||
description,
|
||||
default_markdown_style(false, window, cx),
|
||||
)
|
||||
})),
|
||||
)
|
||||
.children(content.map(|content| self.render_tool_call_content(content, window, cx)))
|
||||
.child(self.render_confirmation_buttons(
|
||||
&[AlwaysAllowOption {
|
||||
id: "always_allow",
|
||||
label: "Always Allow".into(),
|
||||
outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
}],
|
||||
tool_call_id,
|
||||
cx,
|
||||
))
|
||||
.into_any(),
|
||||
ToolCallConfirmation::Other { description } => confirmation_container
|
||||
.child(v_flex().px_2().pb_1p5().child(self.render_markdown(
|
||||
description.clone(),
|
||||
default_markdown_style(false, window, cx),
|
||||
)))
|
||||
.children(content.map(|content| self.render_tool_call_content(content, window, cx)))
|
||||
.child(self.render_confirmation_buttons(
|
||||
&[AlwaysAllowOption {
|
||||
id: "always_allow",
|
||||
label: "Always Allow".into(),
|
||||
outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
}],
|
||||
tool_call_id,
|
||||
cx,
|
||||
))
|
||||
.into_any(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_confirmation_buttons(
|
||||
&self,
|
||||
always_allow_options: &[AlwaysAllowOption],
|
||||
tool_call_id: ToolCallId,
|
||||
options: &[acp::PermissionOption],
|
||||
entry_ix: usize,
|
||||
tool_call_id: acp::ToolCallId,
|
||||
cx: &Context<Self>,
|
||||
) -> Div {
|
||||
h_flex()
|
||||
.pt_1p5()
|
||||
.py_1p5()
|
||||
.px_1p5()
|
||||
.gap_1()
|
||||
.justify_end()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.when(self.agent.supports_always_allow(), |this| {
|
||||
this.children(always_allow_options.into_iter().map(|always_allow_option| {
|
||||
let outcome = always_allow_option.outcome;
|
||||
Button::new(
|
||||
(always_allow_option.id, tool_call_id.0),
|
||||
always_allow_option.label.clone(),
|
||||
)
|
||||
.icon(IconName::CheckDouble)
|
||||
.children(options.iter().map(|option| {
|
||||
let option_id = SharedString::from(option.id.0.clone());
|
||||
Button::new((option_id, entry_ix), option.label.clone())
|
||||
.map(|this| match option.kind {
|
||||
acp::PermissionOptionKind::AllowOnce => {
|
||||
this.icon(IconName::Check).icon_color(Color::Success)
|
||||
}
|
||||
acp::PermissionOptionKind::AllowAlways => {
|
||||
this.icon(IconName::CheckDouble).icon_color(Color::Success)
|
||||
}
|
||||
acp::PermissionOptionKind::RejectOnce => {
|
||||
this.icon(IconName::X).icon_color(Color::Error)
|
||||
}
|
||||
acp::PermissionOptionKind::RejectAlways => {
|
||||
this.icon(IconName::X).icon_color(Color::Error)
|
||||
}
|
||||
})
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Success)
|
||||
.on_click(cx.listener({
|
||||
let id = tool_call_id;
|
||||
let tool_call_id = tool_call_id.clone();
|
||||
let option_id = option.id.clone();
|
||||
let option_kind = option.kind;
|
||||
move |this, _, _, cx| {
|
||||
this.authorize_tool_call(id, outcome, cx);
|
||||
this.authorize_tool_call(
|
||||
tool_call_id.clone(),
|
||||
option_id.clone(),
|
||||
option_kind,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}))
|
||||
}))
|
||||
})
|
||||
.child(
|
||||
Button::new(("allow", tool_call_id.0), "Allow")
|
||||
.icon(IconName::Check)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Success)
|
||||
.on_click(cx.listener({
|
||||
let id = tool_call_id;
|
||||
move |this, _, _, cx| {
|
||||
this.authorize_tool_call(
|
||||
id,
|
||||
acp::ToolCallConfirmationOutcome::Allow,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new(("reject", tool_call_id.0), "Reject")
|
||||
.icon(IconName::X)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Error)
|
||||
.on_click(cx.listener({
|
||||
let id = tool_call_id;
|
||||
move |this, _, _, cx| {
|
||||
this.authorize_tool_call(
|
||||
id,
|
||||
acp::ToolCallConfirmationOutcome::Reject,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>) -> AnyElement {
|
||||
|
@ -2245,12 +2104,11 @@ impl AcpThreadView {
|
|||
.languages
|
||||
.language_for_name("Markdown");
|
||||
|
||||
let (thread_summary, markdown) = match &self.thread_state {
|
||||
ThreadState::Ready { thread, .. } | ThreadState::Unauthenticated { thread } => {
|
||||
let thread = thread.read(cx);
|
||||
(thread.title().to_string(), thread.to_markdown(cx))
|
||||
}
|
||||
ThreadState::Loading { .. } | ThreadState::LoadError(..) => return Task::ready(Ok(())),
|
||||
let (thread_summary, markdown) = if let Some(thread) = self.thread() {
|
||||
let thread = thread.read(cx);
|
||||
(thread.title().to_string(), thread.to_markdown(cx))
|
||||
} else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
|
||||
window.spawn(cx, async move |cx| {
|
||||
|
|
|
@ -1506,8 +1506,7 @@ impl AgentDiff {
|
|||
.read(cx)
|
||||
.entries()
|
||||
.last()
|
||||
.and_then(|entry| entry.diff())
|
||||
.is_some()
|
||||
.map_or(false, |entry| entry.diffs().next().is_some())
|
||||
{
|
||||
self.update_reviewing_editors(workspace, window, cx);
|
||||
}
|
||||
|
@ -1517,8 +1516,7 @@ impl AgentDiff {
|
|||
.read(cx)
|
||||
.entries()
|
||||
.get(*ix)
|
||||
.and_then(|entry| entry.diff())
|
||||
.is_some()
|
||||
.map_or(false, |entry| entry.diffs().next().is_some())
|
||||
{
|
||||
self.update_reviewing_editors(workspace, window, cx);
|
||||
}
|
||||
|
|
|
@ -440,7 +440,7 @@ pub struct AgentPanel {
|
|||
local_timezone: UtcOffset,
|
||||
active_view: ActiveView,
|
||||
acp_message_history:
|
||||
Rc<RefCell<crate::acp::MessageHistory<agentic_coding_protocol::SendUserMessageParams>>>,
|
||||
Rc<RefCell<crate::acp::MessageHistory<Vec<agent_client_protocol::ContentBlock>>>>,
|
||||
previous_view: Option<ActiveView>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
history: Entity<ThreadHistory>,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue