From 288da0f072a16c9e6040e14183110ef4469a73da Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 4 Apr 2025 18:53:21 -0300 Subject: [PATCH] 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 --- crates/agent/src/active_thread.rs | 186 ++++++++++++++++++++++++------ crates/agent/src/tool_use.rs | 12 ++ 2 files changed, 161 insertions(+), 37 deletions(-) diff --git a/crates/agent/src/active_thread.rs b/crates/agent/src/active_thread.rs index 496fd6735270f1ade6a62921f52a08019dbbe392..fed075bc440e5cd8824da32e5023e269692acf6f 100644 --- a/crates/agent/src/active_thread.rs +++ b/crates/agent/src/active_thread.rs @@ -49,7 +49,7 @@ pub struct ActiveThread { show_scrollbar: bool, hide_scrollbar_task: Option>, rendered_messages_by_id: HashMap, - rendered_tool_use_labels: HashMap>, + rendered_tool_uses: HashMap, editing_message: Option<(MessageId, EditMessageState)>, expanded_tool_uses: HashMap, expanded_thinking_segments: HashMap<(MessageId, usize), bool>, @@ -65,6 +65,13 @@ struct RenderedMessage { segments: Vec, } +#[derive(Clone)] +struct RenderedToolUse { + label: Entity, + input: Entity, + output: Entity, +} + impl RenderedMessage { fn from_segments( segments: &[MessageSegment], @@ -235,7 +242,7 @@ fn render_markdown( font_fallbacks: theme_settings.buffer_font.fallbacks.clone(), font_features: Some(theme_settings.buffer_font.features.clone()), 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() }, link: TextStyleRefinement { @@ -270,6 +277,74 @@ fn render_markdown( }) } +fn render_tool_use_markdown( + text: SharedString, + language_registry: Arc, + workspace: WeakEntity, + window: &Window, + cx: &mut App, +) -> Entity { + 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( text: SharedString, workspace: WeakEntity, @@ -374,7 +449,7 @@ impl ActiveThread { save_thread_task: None, messages: Vec::new(), rendered_messages_by_id: HashMap::default(), - rendered_tool_use_labels: HashMap::default(), + rendered_tool_uses: HashMap::default(), expanded_tool_uses: HashMap::default(), expanded_thinking_segments: HashMap::default(), list_state: list_state.clone(), @@ -393,9 +468,11 @@ impl ActiveThread { this.push_message(&message.id, &message.segments, window, 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.ui_text.clone(), + &tool_use.input, + tool_use.status.text(), window, cx, ); @@ -486,23 +563,44 @@ impl ActiveThread { self.rendered_messages_by_id.remove(id); } - fn render_tool_use_label_markdown( + fn render_tool_use_markdown( &mut self, tool_use_id: LanguageModelToolUseId, tool_label: impl Into, + tool_input: &serde_json::Value, + tool_output: SharedString, window: &mut Window, cx: &mut Context, ) { - self.rendered_tool_use_labels.insert( - tool_use_id, - render_markdown( + let rendered = RenderedToolUse { + label: render_tool_use_markdown( tool_label.into(), self.language_registry.clone(), self.workspace.clone(), window, 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( @@ -587,9 +685,11 @@ impl ActiveThread { .update(cx, |thread, cx| thread.use_pending_tools(cx)); for tool_use in tool_uses { - self.render_tool_use_label_markdown( + self.render_tool_use_markdown( tool_use.id.clone(), tool_use.ui_text.clone(), + &tool_use.input, + "".into(), window, cx, ); @@ -602,9 +702,11 @@ impl ActiveThread { } => { let canceled = *canceled; if let Some(tool_use) = pending_tool_use { - self.render_tool_use_label_markdown( + self.render_tool_use_markdown( tool_use.id.clone(), - SharedString::from(tool_use.ui_text.clone()), + tool_use.ui_text.clone(), + &tool_use.input, + "".into(), window, cx, ); @@ -1261,7 +1363,7 @@ impl ActiveThread { .pb_2p5() .when(!tool_uses.is_empty(), |parent| { parent.child( - v_flex().children( + div().children( tool_uses .into_iter() .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() .gap_1() .child( - content_container() + results_content_container() .child( Label::new("Input") .size(LabelSize::XSmall) @@ -1707,16 +1811,16 @@ impl ActiveThread { .buffer_font(cx), ) .child( - Label::new( - serde_json::to_string_pretty(&tool_use.input).unwrap_or_default(), - ) - .size(LabelSize::Small) - .buffer_font(cx), + div().w_full().text_ui_sm(cx).children( + rendered_tool_use + .as_ref() + .map(|rendered| rendered.input.clone()), + ), ), ) .map(|container| match tool_use.status { - ToolUseStatus::Finished(output) => container.child( - content_container() + ToolUseStatus::Finished(_) => container.child( + results_content_container() .border_t_1() .border_color(self.tool_card_border_color(cx)) .child( @@ -1725,10 +1829,16 @@ impl ActiveThread { .color(Color::Muted) .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( - content_container().child( + results_content_container().child( h_flex() .gap_1() .pb_1() @@ -1756,8 +1866,8 @@ impl ActiveThread { ), ), ), - ToolUseStatus::Error(err) => container.child( - content_container() + ToolUseStatus::Error(_) => container.child( + results_content_container() .border_t_1() .border_color(self.tool_card_border_color(cx)) .child( @@ -1766,11 +1876,17 @@ impl ActiveThread { .color(Color::Muted) .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::NeedsConfirmation => container.child( - content_container() + results_content_container() .border_t_1() .border_color(self.tool_card_border_color(cx)) .child( @@ -1786,13 +1902,13 @@ impl ActiveThread { div() .h_full() .absolute() - .w_8() + .w_12() .bottom_0() .map(|element| { if is_status_finished { - element.right_7() + element.right_6() } else { - element.right(px(46.)) + element.right(px(44.)) } }) .bg(linear_gradient( @@ -1828,9 +1944,7 @@ impl ActiveThread { ) .child( h_flex().pr_8().text_ui_sm(cx).children( - self.rendered_tool_use_labels - .get(&tool_use.id) - .cloned(), + rendered_tool_use.map(|rendered| rendered.label) ), ), ) @@ -1918,9 +2032,7 @@ impl ActiveThread { ) .child( h_flex().pr_8().text_ui_sm(cx).children( - self.rendered_tool_use_labels - .get(&tool_use.id) - .cloned(), + rendered_tool_use.map(|rendered| rendered.label) ), ), ) diff --git a/crates/agent/src/tool_use.rs b/crates/agent/src/tool_use.rs index 03cabb0870cf3693fd5d2598722c88c9c62f5552..5dd9e119c3830e12aeac9dca605c603b8db533ec 100644 --- a/crates/agent/src/tool_use.rs +++ b/crates/agent/src/tool_use.rs @@ -35,6 +35,18 @@ pub enum ToolUseStatus { 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 { tools: Arc, tool_uses_by_assistant_message: HashMap>,