@@ -299,6 +299,7 @@ impl Display for ToolCallStatus {
pub enum ContentBlock {
Empty,
Markdown { markdown: Entity<Markdown> },
+ ResourceLink { resource_link: acp::ResourceLink },
}
impl ContentBlock {
@@ -330,8 +331,56 @@ impl ContentBlock {
language_registry: &Arc<LanguageRegistry>,
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<LanguageRegistry>,
+ 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,
}
}
}
@@ -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<Self>| {
@@ -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<Markdown>,
+ tool_call_id: acp::ToolCallId,
+ window: &Window,
+ cx: &Context<Self>,
+ ) -> 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<Self>| {
+ this.expanded_tool_calls.remove(&id);
+ cx.notify();
+ }
+ })),
+ )
+ .into_any_element()
+ }
+
+ fn render_resource_link(
+ &self,
+ resource_link: &acp::ResourceLink,
+ cx: &Context<Self>,
+ ) -> 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>| {
+ 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()