agent: Render edit tool error as markdown (#30325)

Release Notes:

- agent: Render edit tool error as markdown and allow selecting it
This commit is contained in:
Agus Zubiaga 2025-05-08 22:18:52 -03:00 committed by GitHub
parent 05a6c31ad8
commit c512d43e8c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 98 additions and 68 deletions

View file

@ -20,6 +20,7 @@ use language::{
language_settings::SoftWrap,
};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@ -335,7 +336,7 @@ pub struct EditFileToolCard {
project: Entity<Project>,
diff_task: Option<Task<Result<()>>>,
preview_expanded: bool,
error_expanded: bool,
error_expanded: Option<Entity<Markdown>>,
full_height_expanded: bool,
total_lines: Option<u32>,
editor_unique_id: EntityId,
@ -378,7 +379,7 @@ impl EditFileToolCard {
multibuffer,
diff_task: None,
preview_expanded: true,
error_expanded: false,
error_expanded: None,
full_height_expanded: false,
total_lines: None,
}
@ -435,9 +436,9 @@ impl ToolCard for EditFileToolCard {
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> impl IntoElement {
let (failed, error_message) = match status {
ToolUseStatus::Error(err) => (true, Some(err.to_string())),
_ => (false, None),
let error_message = match status {
ToolUseStatus::Error(err) => Some(err),
_ => None,
};
let path_label_button = h_flex()
@ -525,9 +526,11 @@ impl ToolCard for EditFileToolCard {
.gap_1()
.justify_between()
.rounded_t_md()
.when(!failed, |header| header.bg(codeblock_header_bg))
.when(error_message.is_none(), |header| {
header.bg(codeblock_header_bg)
})
.child(path_label_button)
.when(failed, |header| {
.when_some(error_message, |header, error_message| {
header.child(
h_flex()
.gap_1()
@ -539,19 +542,28 @@ impl ToolCard for EditFileToolCard {
.child(
Disclosure::new(
("edit-file-error-disclosure", self.editor_unique_id),
self.error_expanded,
self.error_expanded.is_some(),
)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener(
move |this, _event, _window, _cx| {
this.error_expanded = !this.error_expanded;
},
)),
.on_click(cx.listener({
let error_message = error_message.clone();
move |this, _event, _window, cx| {
if this.error_expanded.is_some() {
this.error_expanded.take();
} else {
this.error_expanded = Some(cx.new(|cx| {
Markdown::new(error_message.clone(), None, None, cx)
}))
}
cx.notify();
}
})),
),
)
})
.when(!failed && self.has_diff(), |header| {
.when(error_message.is_none() && self.has_diff(), |header| {
header.child(
Disclosure::new(
("edit-file-disclosure", self.editor_unique_id),
@ -658,12 +670,12 @@ impl ToolCard for EditFileToolCard {
v_flex()
.mb_2()
.border_1()
.when(failed, |card| card.border_dashed())
.when(error_message.is_some(), |card| card.border_dashed())
.border_color(border_color)
.rounded_md()
.overflow_hidden()
.child(codeblock_header)
.when(failed && self.error_expanded, |card| {
.when_some(self.error_expanded.as_ref(), |card, error_markdown| {
card.child(
v_flex()
.p_2()
@ -683,65 +695,81 @@ impl ToolCard for EditFileToolCard {
.rounded_md()
.text_ui_sm(cx)
.bg(cx.theme().colors().editor_background)
.children(
error_message
.map(|error| div().child(error).into_any_element()),
),
.child(MarkdownElement::new(
error_markdown.clone(),
markdown_style(window, cx),
)),
),
)
})
.when(!self.has_diff() && !failed, |card| {
.when(!self.has_diff() && error_message.is_none(), |card| {
card.child(waiting_for_diff)
})
.when(
!failed && self.preview_expanded && self.has_diff(),
|card| {
.when(self.preview_expanded && self.has_diff(), |card| {
card.child(
v_flex()
.relative()
.h_full()
.when(!self.full_height_expanded, |editor_container| {
editor_container
.max_h(DEFAULT_COLLAPSED_LINES as f32 * editor_line_height)
})
.overflow_hidden()
.border_t_1()
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.child(editor)
.when(
!self.full_height_expanded && is_collapsible,
|editor_container| editor_container.child(gradient_overlay),
),
)
.when(is_collapsible, |card| {
card.child(
v_flex()
.relative()
.h_full()
.when(!self.full_height_expanded, |editor_container| {
editor_container
.max_h(DEFAULT_COLLAPSED_LINES as f32 * editor_line_height)
})
.overflow_hidden()
h_flex()
.id(("expand-button", self.editor_unique_id))
.flex_none()
.cursor_pointer()
.h_5()
.justify_center()
.border_t_1()
.rounded_b_md()
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.child(editor)
.when(
!self.full_height_expanded && is_collapsible,
|editor_container| editor_container.child(gradient_overlay),
),
.hover(|style| style.bg(cx.theme().colors().element_hover.opacity(0.1)))
.child(
Icon::new(full_height_icon)
.size(IconSize::Small)
.color(Color::Muted),
)
.tooltip(Tooltip::text(full_height_tooltip_label))
.on_click(cx.listener(move |this, _event, _window, _cx| {
this.full_height_expanded = !this.full_height_expanded;
})),
)
.when(is_collapsible, |card| {
card.child(
h_flex()
.id(("expand-button", self.editor_unique_id))
.flex_none()
.cursor_pointer()
.h_5()
.justify_center()
.border_t_1()
.rounded_b_md()
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.hover(|style| {
style.bg(cx.theme().colors().element_hover.opacity(0.1))
})
.child(
Icon::new(full_height_icon)
.size(IconSize::Small)
.color(Color::Muted),
)
.tooltip(Tooltip::text(full_height_tooltip_label))
.on_click(cx.listener(move |this, _event, _window, _cx| {
this.full_height_expanded = !this.full_height_expanded;
})),
)
})
},
)
})
})
}
}
fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
let theme_settings = ThemeSettings::get_global(cx);
let ui_font_size = TextSize::Default.rems(cx);
let mut text_style = window.text_style();
text_style.refine(&TextStyleRefinement {
font_family: Some(theme_settings.ui_font.family.clone()),
font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
font_features: Some(theme_settings.ui_font.features.clone()),
font_size: Some(ui_font_size.into()),
color: Some(cx.theme().colors().text),
..Default::default()
});
MarkdownStyle {
base_text_style: text_style.clone(),
selection_background_color: cx.theme().players().local().selection,
..Default::default()
}
}