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:
parent
d2162446d0
commit
b105028c05
3 changed files with 192 additions and 114 deletions
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" => {
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue