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

1
Cargo.lock generated
View file

@ -676,6 +676,7 @@ dependencies = [
"language_models", "language_models",
"linkme", "linkme",
"log", "log",
"markdown",
"open", "open",
"paths", "paths",
"portable-pty", "portable-pty",

View file

@ -17,14 +17,14 @@ eval = []
[dependencies] [dependencies]
aho-corasick.workspace = true aho-corasick.workspace = true
anyhow.workspace = true anyhow.workspace = true
assistant_tool.workspace = true
assistant_settings.workspace = true assistant_settings.workspace = true
assistant_tool.workspace = true
buffer_diff.workspace = true buffer_diff.workspace = true
chrono.workspace = true chrono.workspace = true
collections.workspace = true collections.workspace = true
component.workspace = true component.workspace = true
editor.workspace = true
derive_more.workspace = true derive_more.workspace = true
editor.workspace = true
feature_flags.workspace = true feature_flags.workspace = true
futures.workspace = true futures.workspace = true
gpui.workspace = true gpui.workspace = true
@ -35,8 +35,9 @@ indoc.workspace = true
itertools.workspace = true itertools.workspace = true
language.workspace = true language.workspace = true
language_model.workspace = true language_model.workspace = true
log.workspace = true
linkme.workspace = true linkme.workspace = true
log.workspace = true
markdown.workspace = true
open.workspace = true open.workspace = true
paths.workspace = true paths.workspace = true
portable-pty.workspace = true portable-pty.workspace = true

View file

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