diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index eccbef96b8..cadab3d62c 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -299,6 +299,7 @@ impl Display for ToolCallStatus { pub enum ContentBlock { Empty, Markdown { markdown: Entity }, + ResourceLink { resource_link: acp::ResourceLink }, } impl ContentBlock { @@ -330,8 +331,56 @@ impl ContentBlock { language_registry: &Arc, cx: &mut App, ) { - let new_content = match block { + if matches!(self, ContentBlock::Empty) { + if let acp::ContentBlock::ResourceLink(resource_link) = block { + *self = ContentBlock::ResourceLink { resource_link }; + return; + } + } + + let new_content = self.extract_content_from_block(block); + + match self { + ContentBlock::Empty => { + *self = Self::create_markdown_block(new_content, language_registry, cx); + } + ContentBlock::Markdown { markdown } => { + markdown.update(cx, |markdown, cx| markdown.append(&new_content, cx)); + } + ContentBlock::ResourceLink { resource_link } => { + let existing_content = Self::resource_link_to_content(&resource_link.uri); + let combined = format!("{}\n{}", existing_content, new_content); + + *self = Self::create_markdown_block(combined, language_registry, cx); + } + } + } + + fn resource_link_to_content(uri: &str) -> String { + if let Some(uri) = MentionUri::parse(&uri).log_err() { + uri.to_link() + } else { + uri.to_string().clone() + } + } + + fn create_markdown_block( + content: String, + language_registry: &Arc, + cx: &mut App, + ) -> ContentBlock { + ContentBlock::Markdown { + markdown: cx + .new(|cx| Markdown::new(content.into(), Some(language_registry.clone()), None, cx)), + } + } + + fn extract_content_from_block(&self, block: acp::ContentBlock) -> String { + match block { acp::ContentBlock::Text(text_content) => text_content.text.clone(), + acp::ContentBlock::ResourceLink(resource_link) => { + Self::resource_link_to_content(&resource_link.uri) + } acp::ContentBlock::Resource(acp::EmbeddedResource { resource: acp::EmbeddedResourceResource::TextResourceContents(acp::TextResourceContents { @@ -339,35 +388,10 @@ impl ContentBlock { .. }), .. - }) => { - if let Some(uri) = MentionUri::parse(&uri).log_err() { - uri.to_link() - } else { - uri.clone() - } - } + }) => Self::resource_link_to_content(&uri), acp::ContentBlock::Image(_) | acp::ContentBlock::Audio(_) - | acp::ContentBlock::Resource(acp::EmbeddedResource { .. }) - | acp::ContentBlock::ResourceLink(_) => String::new(), - }; - - match self { - ContentBlock::Empty => { - *self = ContentBlock::Markdown { - markdown: cx.new(|cx| { - Markdown::new( - new_content.into(), - Some(language_registry.clone()), - None, - cx, - ) - }), - }; - } - ContentBlock::Markdown { markdown } => { - markdown.update(cx, |markdown, cx| markdown.append(&new_content, cx)); - } + | acp::ContentBlock::Resource(_) => String::new(), } } @@ -375,6 +399,7 @@ impl ContentBlock { match self { ContentBlock::Empty => "", ContentBlock::Markdown { markdown } => markdown.read(cx).source(), + ContentBlock::ResourceLink { resource_link } => &resource_link.uri, } } @@ -382,6 +407,14 @@ impl ContentBlock { match self { ContentBlock::Empty => None, ContentBlock::Markdown { markdown } => Some(markdown), + ContentBlock::ResourceLink { .. } => None, + } + } + + pub fn resource_link(&self) -> Option<&acp::ResourceLink> { + match self { + ContentBlock::ResourceLink { resource_link } => Some(resource_link), + _ => None, } } } diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index 1fcd27ad4c..59c479d87b 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -19,7 +19,10 @@ impl MentionUri { if let Some(fragment) = url.fragment() { Ok(Self::Symbol(path.into(), fragment.into())) } else { - Ok(Self::File(path.into())) + let file_path = + PathBuf::from(format!("{}{}", url.host_str().unwrap_or(""), path)); + + Ok(Self::File(file_path)) } } "zed" => { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 6d8dccd18f..791542cf26 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1108,10 +1108,10 @@ impl AcpThreadView { .size(IconSize::Small) .color(Color::Muted); + let base_container = h_flex().size_4().justify_center(); + if is_collapsible { - h_flex() - .size_4() - .justify_center() + base_container .child( div() .group_hover(&group_name, |s| s.invisible().w_0()) @@ -1142,7 +1142,7 @@ impl AcpThreadView { ), ) } else { - div().child(tool_icon) + base_container.child(tool_icon) } } @@ -1205,8 +1205,10 @@ impl AcpThreadView { ToolCallContent::Diff(diff) => diff.read(cx).has_revealed_range(cx), _ => false, }); - let is_collapsible = - !tool_call.content.is_empty() && !needs_confirmation && !is_edit && !has_diff; + let use_card_layout = needs_confirmation || is_edit || has_diff; + + let is_collapsible = !tool_call.content.is_empty() && !use_card_layout; + let is_open = tool_call.content.is_empty() || needs_confirmation || has_nonempty_diff @@ -1225,9 +1227,39 @@ impl AcpThreadView { linear_color_stop(color.opacity(0.2), 0.), )) }; + let gradient_color = if use_card_layout { + self.tool_card_header_bg(cx) + } else { + cx.theme().colors().panel_background + }; + + let tool_output_display = match &tool_call.status { + ToolCallStatus::WaitingForConfirmation { options, .. } => v_flex() + .w_full() + .children(tool_call.content.iter().map(|content| { + div() + .child(self.render_tool_call_content(content, tool_call, window, cx)) + .into_any_element() + })) + .child(self.render_permission_buttons( + options, + entry_ix, + tool_call.id.clone(), + tool_call.content.is_empty(), + cx, + )), + ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => v_flex() + .w_full() + .children(tool_call.content.iter().map(|content| { + div() + .child(self.render_tool_call_content(content, tool_call, window, cx)) + .into_any_element() + })), + ToolCallStatus::Rejected => v_flex().size_0(), + }; v_flex() - .when(needs_confirmation || is_edit || has_diff, |this| { + .when(use_card_layout, |this| { this.rounded_lg() .border_1() .border_color(self.tool_card_border_color(cx)) @@ -1241,7 +1273,7 @@ impl AcpThreadView { .gap_1() .justify_between() .map(|this| { - if needs_confirmation || is_edit || has_diff { + if use_card_layout { this.pl_2() .pr_1() .py_1() @@ -1258,13 +1290,6 @@ impl AcpThreadView { .group(&card_header_id) .relative() .w_full() - .map(|this| { - if tool_call.locations.len() == 1 { - this.gap_0() - } else { - this.gap_1p5() - } - }) .text_size(self.tool_name_font_size()) .child(self.render_tool_call_icon( card_header_id, @@ -1308,6 +1333,7 @@ impl AcpThreadView { .id("non-card-label-container") .w_full() .relative() + .ml_1p5() .overflow_hidden() .child( h_flex() @@ -1324,17 +1350,7 @@ impl AcpThreadView { ), )), ) - .map(|this| { - if needs_confirmation { - this.child(gradient_overlay( - self.tool_card_header_bg(cx), - )) - } else { - this.child(gradient_overlay( - cx.theme().colors().panel_background, - )) - } - }) + .child(gradient_overlay(gradient_color)) .on_click(cx.listener({ let id = tool_call.id.clone(); move |this: &mut Self, _, _, cx: &mut Context| { @@ -1351,54 +1367,7 @@ impl AcpThreadView { ) .children(status_icon), ) - .when(is_open, |this| { - this.child( - v_flex() - .text_xs() - .when(is_collapsible, |this| { - this.mt_1() - .border_1() - .border_color(self.tool_card_border_color(cx)) - .bg(cx.theme().colors().editor_background) - .rounded_lg() - }) - .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, tool_call, window, cx, - )) - .into_any_element() - })) - .child(self.render_permission_buttons( - options, - entry_ix, - tool_call.id.clone(), - tool_call.content.is_empty(), - cx, - )), - ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => { - this.children(tool_call.content.iter().map(|content| { - div() - .py_1p5() - .child(self.render_tool_call_content( - content, tool_call, window, cx, - )) - .into_any_element() - })) - } - ToolCallStatus::Rejected => this, - } - } else { - this - } - }), - ) - }) + .when(is_open, |this| this.child(tool_output_display)) } fn render_tool_call_content( @@ -1410,16 +1379,10 @@ impl AcpThreadView { ) -> AnyElement { match content { 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() + if let Some(resource_link) = content.resource_link() { + self.render_resource_link(resource_link, cx) + } else if let Some(markdown) = content.markdown() { + self.render_markdown_output(markdown.clone(), tool_call.id.clone(), window, cx) } else { Empty.into_any_element() } @@ -1431,6 +1394,83 @@ impl AcpThreadView { } } + fn render_markdown_output( + &self, + markdown: Entity, + tool_call_id: acp::ToolCallId, + window: &Window, + cx: &Context, + ) -> AnyElement { + let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id.clone())); + + v_flex() + .mt_1p5() + .ml(px(7.)) + .px_3p5() + .gap_2() + .border_l_1() + .border_color(self.tool_card_border_color(cx)) + .text_sm() + .text_color(cx.theme().colors().text_muted) + .child(self.render_markdown(markdown, default_markdown_style(false, window, cx))) + .child( + Button::new(button_id, "Collapse Output") + .full_width() + .style(ButtonStyle::Outlined) + .label_size(LabelSize::Small) + .icon(IconName::ChevronUp) + .icon_color(Color::Muted) + .icon_position(IconPosition::Start) + .on_click(cx.listener({ + let id = tool_call_id.clone(); + move |this: &mut Self, _, _, cx: &mut Context| { + this.expanded_tool_calls.remove(&id); + cx.notify(); + } + })), + ) + .into_any_element() + } + + fn render_resource_link( + &self, + resource_link: &acp::ResourceLink, + cx: &Context, + ) -> AnyElement { + let uri: SharedString = resource_link.uri.clone().into(); + + let label: SharedString = if let Some(path) = resource_link.uri.strip_prefix("file://") { + path.to_string().into() + } else { + uri.clone() + }; + + let button_id = SharedString::from(format!("item-{}", uri.clone())); + + div() + .ml(px(7.)) + .pl_2p5() + .border_l_1() + .border_color(self.tool_card_border_color(cx)) + .overflow_hidden() + .child( + Button::new(button_id, label) + .label_size(LabelSize::Small) + .color(Color::Muted) + .icon(IconName::ArrowUpRight) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .truncate(true) + .on_click(cx.listener({ + let workspace = self.workspace.clone(); + move |_, _, window, cx: &mut Context| { + Self::open_link(uri.clone(), &workspace, window, cx); + } + })), + ) + .into_any_element() + } + fn render_permission_buttons( &self, options: &[acp::PermissionOption], @@ -1706,7 +1746,9 @@ impl AcpThreadView { .overflow_hidden() .child( v_flex() - .p_2() + .pt_1() + .pb_2() + .px_2() .gap_0p5() .bg(header_bg) .text_xs()