agent: Use Markdown to render tool input and output content (#28127)

Release Notes:

- agent: Tool call's input and output content are now rendered with
Markdown, which allows them to be selected and copied.

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
This commit is contained in:
Danilo Leal 2025-04-04 18:53:21 -03:00 committed by GitHub
parent b8d05bb641
commit 288da0f072
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 161 additions and 37 deletions

View file

@ -49,7 +49,7 @@ pub struct ActiveThread {
show_scrollbar: bool, show_scrollbar: bool,
hide_scrollbar_task: Option<Task<()>>, hide_scrollbar_task: Option<Task<()>>,
rendered_messages_by_id: HashMap<MessageId, RenderedMessage>, rendered_messages_by_id: HashMap<MessageId, RenderedMessage>,
rendered_tool_use_labels: HashMap<LanguageModelToolUseId, Entity<Markdown>>, rendered_tool_uses: HashMap<LanguageModelToolUseId, RenderedToolUse>,
editing_message: Option<(MessageId, EditMessageState)>, editing_message: Option<(MessageId, EditMessageState)>,
expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>, expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
expanded_thinking_segments: HashMap<(MessageId, usize), bool>, expanded_thinking_segments: HashMap<(MessageId, usize), bool>,
@ -65,6 +65,13 @@ struct RenderedMessage {
segments: Vec<RenderedMessageSegment>, segments: Vec<RenderedMessageSegment>,
} }
#[derive(Clone)]
struct RenderedToolUse {
label: Entity<Markdown>,
input: Entity<Markdown>,
output: Entity<Markdown>,
}
impl RenderedMessage { impl RenderedMessage {
fn from_segments( fn from_segments(
segments: &[MessageSegment], segments: &[MessageSegment],
@ -235,7 +242,7 @@ fn render_markdown(
font_fallbacks: theme_settings.buffer_font.fallbacks.clone(), font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
font_features: Some(theme_settings.buffer_font.features.clone()), font_features: Some(theme_settings.buffer_font.features.clone()),
font_size: Some(buffer_font_size.into()), font_size: Some(buffer_font_size.into()),
background_color: Some(colors.editor_foreground.opacity(0.1)), background_color: Some(colors.editor_foreground.opacity(0.08)),
..Default::default() ..Default::default()
}, },
link: TextStyleRefinement { link: TextStyleRefinement {
@ -270,6 +277,74 @@ fn render_markdown(
}) })
} }
fn render_tool_use_markdown(
text: SharedString,
language_registry: Arc<LanguageRegistry>,
workspace: WeakEntity<Workspace>,
window: &Window,
cx: &mut App,
) -> Entity<Markdown> {
let theme_settings = ThemeSettings::get_global(cx);
let colors = cx.theme().colors();
let ui_font_size = TextSize::Default.rems(cx);
let buffer_font_size = TextSize::Small.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()
});
let markdown_style = MarkdownStyle {
base_text_style: text_style,
syntax: cx.theme().syntax().clone(),
selection_background_color: cx.theme().players().local().selection,
code_block_overflow_x_scroll: true,
code_block: StyleRefinement {
margin: EdgesRefinement::default(),
padding: EdgesRefinement::default(),
background: Some(colors.editor_background.into()),
border_color: None,
border_widths: EdgesRefinement::default(),
text: Some(TextStyleRefinement {
font_family: Some(theme_settings.buffer_font.family.clone()),
font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
font_features: Some(theme_settings.buffer_font.features.clone()),
font_size: Some(buffer_font_size.into()),
..Default::default()
}),
..Default::default()
},
inline_code: TextStyleRefinement {
font_family: Some(theme_settings.buffer_font.family.clone()),
font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
font_features: Some(theme_settings.buffer_font.features.clone()),
font_size: Some(TextSize::XSmall.rems(cx).into()),
..Default::default()
},
heading: StyleRefinement {
text: Some(TextStyleRefinement {
font_size: Some(ui_font_size.into()),
..Default::default()
}),
..Default::default()
},
..Default::default()
};
cx.new(|cx| {
Markdown::new(text, markdown_style, Some(language_registry), None, cx).open_url(
move |text, window, cx| {
open_markdown_link(text, workspace.clone(), window, cx);
},
)
})
}
fn open_markdown_link( fn open_markdown_link(
text: SharedString, text: SharedString,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
@ -374,7 +449,7 @@ impl ActiveThread {
save_thread_task: None, save_thread_task: None,
messages: Vec::new(), messages: Vec::new(),
rendered_messages_by_id: HashMap::default(), rendered_messages_by_id: HashMap::default(),
rendered_tool_use_labels: HashMap::default(), rendered_tool_uses: HashMap::default(),
expanded_tool_uses: HashMap::default(), expanded_tool_uses: HashMap::default(),
expanded_thinking_segments: HashMap::default(), expanded_thinking_segments: HashMap::default(),
list_state: list_state.clone(), list_state: list_state.clone(),
@ -393,9 +468,11 @@ impl ActiveThread {
this.push_message(&message.id, &message.segments, window, cx); this.push_message(&message.id, &message.segments, window, cx);
for tool_use in thread.read(cx).tool_uses_for_message(message.id, cx) { for tool_use in thread.read(cx).tool_uses_for_message(message.id, cx) {
this.render_tool_use_label_markdown( this.render_tool_use_markdown(
tool_use.id.clone(), tool_use.id.clone(),
tool_use.ui_text.clone(), tool_use.ui_text.clone(),
&tool_use.input,
tool_use.status.text(),
window, window,
cx, cx,
); );
@ -486,23 +563,44 @@ impl ActiveThread {
self.rendered_messages_by_id.remove(id); self.rendered_messages_by_id.remove(id);
} }
fn render_tool_use_label_markdown( fn render_tool_use_markdown(
&mut self, &mut self,
tool_use_id: LanguageModelToolUseId, tool_use_id: LanguageModelToolUseId,
tool_label: impl Into<SharedString>, tool_label: impl Into<SharedString>,
tool_input: &serde_json::Value,
tool_output: SharedString,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
self.rendered_tool_use_labels.insert( let rendered = RenderedToolUse {
tool_use_id, label: render_tool_use_markdown(
render_markdown(
tool_label.into(), tool_label.into(),
self.language_registry.clone(), self.language_registry.clone(),
self.workspace.clone(), self.workspace.clone(),
window, window,
cx, cx,
), ),
); input: render_tool_use_markdown(
format!(
"```json\n{}\n```",
serde_json::to_string_pretty(tool_input).unwrap_or_default()
)
.into(),
self.language_registry.clone(),
self.workspace.clone(),
window,
cx,
),
output: render_tool_use_markdown(
tool_output,
self.language_registry.clone(),
self.workspace.clone(),
window,
cx,
),
};
self.rendered_tool_uses
.insert(tool_use_id.clone(), rendered);
} }
fn handle_thread_event( fn handle_thread_event(
@ -587,9 +685,11 @@ impl ActiveThread {
.update(cx, |thread, cx| thread.use_pending_tools(cx)); .update(cx, |thread, cx| thread.use_pending_tools(cx));
for tool_use in tool_uses { for tool_use in tool_uses {
self.render_tool_use_label_markdown( self.render_tool_use_markdown(
tool_use.id.clone(), tool_use.id.clone(),
tool_use.ui_text.clone(), tool_use.ui_text.clone(),
&tool_use.input,
"".into(),
window, window,
cx, cx,
); );
@ -602,9 +702,11 @@ impl ActiveThread {
} => { } => {
let canceled = *canceled; let canceled = *canceled;
if let Some(tool_use) = pending_tool_use { if let Some(tool_use) = pending_tool_use {
self.render_tool_use_label_markdown( self.render_tool_use_markdown(
tool_use.id.clone(), tool_use.id.clone(),
SharedString::from(tool_use.ui_text.clone()), tool_use.ui_text.clone(),
&tool_use.input,
"".into(),
window, window,
cx, cx,
); );
@ -1261,7 +1363,7 @@ impl ActiveThread {
.pb_2p5() .pb_2p5()
.when(!tool_uses.is_empty(), |parent| { .when(!tool_uses.is_empty(), |parent| {
parent.child( parent.child(
v_flex().children( div().children(
tool_uses tool_uses
.into_iter() .into_iter()
.map(|tool_use| self.render_tool_use(tool_use, cx)), .map(|tool_use| self.render_tool_use(tool_use, cx)),
@ -1695,11 +1797,13 @@ impl ActiveThread {
} }
}); });
let content_container = || v_flex().py_1().gap_0p5().px_2p5(); let rendered_tool_use = self.rendered_tool_uses.get(&tool_use.id).cloned();
let results_content_container = || v_flex().p_2().gap_0p5();
let results_content = v_flex() let results_content = v_flex()
.gap_1() .gap_1()
.child( .child(
content_container() results_content_container()
.child( .child(
Label::new("Input") Label::new("Input")
.size(LabelSize::XSmall) .size(LabelSize::XSmall)
@ -1707,16 +1811,16 @@ impl ActiveThread {
.buffer_font(cx), .buffer_font(cx),
) )
.child( .child(
Label::new( div().w_full().text_ui_sm(cx).children(
serde_json::to_string_pretty(&tool_use.input).unwrap_or_default(), rendered_tool_use
) .as_ref()
.size(LabelSize::Small) .map(|rendered| rendered.input.clone()),
.buffer_font(cx), ),
), ),
) )
.map(|container| match tool_use.status { .map(|container| match tool_use.status {
ToolUseStatus::Finished(output) => container.child( ToolUseStatus::Finished(_) => container.child(
content_container() results_content_container()
.border_t_1() .border_t_1()
.border_color(self.tool_card_border_color(cx)) .border_color(self.tool_card_border_color(cx))
.child( .child(
@ -1725,10 +1829,16 @@ impl ActiveThread {
.color(Color::Muted) .color(Color::Muted)
.buffer_font(cx), .buffer_font(cx),
) )
.child(Label::new(output).size(LabelSize::Small).buffer_font(cx)), .child(
div().w_full().text_ui_sm(cx).children(
rendered_tool_use
.as_ref()
.map(|rendered| rendered.output.clone()),
),
),
), ),
ToolUseStatus::Running => container.child( ToolUseStatus::Running => container.child(
content_container().child( results_content_container().child(
h_flex() h_flex()
.gap_1() .gap_1()
.pb_1() .pb_1()
@ -1756,8 +1866,8 @@ impl ActiveThread {
), ),
), ),
), ),
ToolUseStatus::Error(err) => container.child( ToolUseStatus::Error(_) => container.child(
content_container() results_content_container()
.border_t_1() .border_t_1()
.border_color(self.tool_card_border_color(cx)) .border_color(self.tool_card_border_color(cx))
.child( .child(
@ -1766,11 +1876,17 @@ impl ActiveThread {
.color(Color::Muted) .color(Color::Muted)
.buffer_font(cx), .buffer_font(cx),
) )
.child(Label::new(err).size(LabelSize::Small).buffer_font(cx)), .child(
div().text_ui_sm(cx).children(
rendered_tool_use
.as_ref()
.map(|rendered| rendered.output.clone()),
),
),
), ),
ToolUseStatus::Pending => container, ToolUseStatus::Pending => container,
ToolUseStatus::NeedsConfirmation => container.child( ToolUseStatus::NeedsConfirmation => container.child(
content_container() results_content_container()
.border_t_1() .border_t_1()
.border_color(self.tool_card_border_color(cx)) .border_color(self.tool_card_border_color(cx))
.child( .child(
@ -1786,13 +1902,13 @@ impl ActiveThread {
div() div()
.h_full() .h_full()
.absolute() .absolute()
.w_8() .w_12()
.bottom_0() .bottom_0()
.map(|element| { .map(|element| {
if is_status_finished { if is_status_finished {
element.right_7() element.right_6()
} else { } else {
element.right(px(46.)) element.right(px(44.))
} }
}) })
.bg(linear_gradient( .bg(linear_gradient(
@ -1828,9 +1944,7 @@ impl ActiveThread {
) )
.child( .child(
h_flex().pr_8().text_ui_sm(cx).children( h_flex().pr_8().text_ui_sm(cx).children(
self.rendered_tool_use_labels rendered_tool_use.map(|rendered| rendered.label)
.get(&tool_use.id)
.cloned(),
), ),
), ),
) )
@ -1918,9 +2032,7 @@ impl ActiveThread {
) )
.child( .child(
h_flex().pr_8().text_ui_sm(cx).children( h_flex().pr_8().text_ui_sm(cx).children(
self.rendered_tool_use_labels rendered_tool_use.map(|rendered| rendered.label)
.get(&tool_use.id)
.cloned(),
), ),
), ),
) )

View file

@ -35,6 +35,18 @@ pub enum ToolUseStatus {
Error(SharedString), Error(SharedString),
} }
impl ToolUseStatus {
pub fn text(&self) -> SharedString {
match self {
ToolUseStatus::NeedsConfirmation => "".into(),
ToolUseStatus::Pending => "".into(),
ToolUseStatus::Running => "".into(),
ToolUseStatus::Finished(out) => out.clone(),
ToolUseStatus::Error(out) => out.clone(),
}
}
}
pub struct ToolUseState { pub struct ToolUseState {
tools: Arc<ToolWorkingSet>, tools: Arc<ToolWorkingSet>,
tool_uses_by_assistant_message: HashMap<MessageId, Vec<LanguageModelToolUse>>, tool_uses_by_assistant_message: HashMap<MessageId, Vec<LanguageModelToolUse>>,