agent2: Add custom UI for resource link content blocks (#36005)

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>
This commit is contained in:
Danilo Leal 2025-08-12 12:39:27 -03:00 committed by GitHub
parent d2162446d0
commit b105028c05
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 192 additions and 114 deletions

View file

@ -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()