From 39da72161f6bd9472b11725f587ee989072618f4 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 9 May 2025 21:53:11 -0300 Subject: [PATCH] agent: Make terminal command render with Markdown in the tool card (#30430) Closes https://github.com/zed-industries/zed/issues/30411 Rendering as markdown gives us text selection and copying for free. In the future, we may want to explore having these commands be actual editors, allowing you to step in, change the command, and re-run it right from there. Release Notes: - agent: Made the terminal command in the tool card selectable and copyable. --- crates/agent/src/active_thread.rs | 3 + crates/assistant_tools/src/edit_file_tool.rs | 2 +- crates/assistant_tools/src/terminal_tool.rs | 78 +++++++++++++++++--- crates/editor/src/code_context_menus.rs | 1 + crates/editor/src/hover_popover.rs | 1 + crates/markdown/src/markdown.rs | 35 ++++++++- 6 files changed, 108 insertions(+), 12 deletions(-) diff --git a/crates/agent/src/active_thread.rs b/crates/agent/src/active_thread.rs index 9f466883cc26ebe57a4b6c8119c5d768c0d0a23b..841482e4823f4ba308999ad4ad79258907f0ac46 100644 --- a/crates/agent/src/active_thread.rs +++ b/crates/agent/src/active_thread.rs @@ -2400,6 +2400,7 @@ impl ActiveThread { markdown_element.code_block_renderer( markdown::CodeBlockRenderer::Default { copy_button: false, + copy_button_on_hover: false, border: true, }, ) @@ -2719,6 +2720,7 @@ impl ActiveThread { ) .code_block_renderer(markdown::CodeBlockRenderer::Default { copy_button: false, + copy_button_on_hover: false, border: false, }) .on_url_click({ @@ -2749,6 +2751,7 @@ impl ActiveThread { ) .code_block_renderer(markdown::CodeBlockRenderer::Default { copy_button: false, + copy_button_on_hover: false, border: false, }) .on_url_click({ diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index bbc92ea73513d1e0868cc2459a7a3e9d01c83fe5..8c60f980da1ee51f0b3f37aa71fb98a52cb18829 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -637,7 +637,7 @@ impl ToolCard for EditFileToolCard { .p_3() .gap_1() .border_t_1() - .rounded_md() + .rounded_b_md() .border_color(border_color) .bg(cx.theme().colors().editor_background); diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index d8415415e91e814b85125ba7cd2ac148ccb73823..5ca65741acefbd54f11ea8f8a5ca41a8d60cba76 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -2,13 +2,18 @@ use crate::schema::json_schema_for; use anyhow::{Context as _, Result, anyhow, bail}; use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus}; use futures::{FutureExt as _, future::Shared}; -use gpui::{AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, WeakEntity, Window}; +use gpui::{ + AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, TextStyleRefinement, + WeakEntity, Window, +}; use language::LineEnding; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; +use markdown::{Markdown, MarkdownElement, MarkdownStyle}; use portable_pty::{CommandBuilder, PtySize, native_pty_system}; use project::{Project, terminals::TerminalKind}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use settings::Settings; use std::{ env, path::{Path, PathBuf}, @@ -17,6 +22,7 @@ use std::{ time::{Duration, Instant}, }; use terminal_view::TerminalView; +use theme::ThemeSettings; use ui::{Disclosure, Tooltip, prelude::*}; use util::{ get_system_shell, markdown::MarkdownInlineCode, size::format_file_size, @@ -211,8 +217,21 @@ impl Tool for TerminalTool { } }); + let command_markdown = cx.new(|cx| { + Markdown::new( + format!("```bash\n{}\n```", input.command).into(), + None, + None, + cx, + ) + }); + let card = cx.new(|cx| { - TerminalToolCard::new(input.command.clone(), working_dir.clone(), cx.entity_id()) + TerminalToolCard::new( + command_markdown.clone(), + working_dir.clone(), + cx.entity_id(), + ) }); let output = cx.spawn({ @@ -388,7 +407,7 @@ fn working_dir( } struct TerminalToolCard { - input_command: String, + input_command: Entity, working_dir: Option, entity_id: EntityId, exit_status: Option, @@ -404,7 +423,11 @@ struct TerminalToolCard { } impl TerminalToolCard { - pub fn new(input_command: String, working_dir: Option, entity_id: EntityId) -> Self { + pub fn new( + input_command: Entity, + working_dir: Option, + entity_id: EntityId, + ) -> Self { Self { input_command, working_dir, @@ -427,7 +450,7 @@ impl ToolCard for TerminalToolCard { fn render( &mut self, status: &ToolUseStatus, - _window: &mut Window, + window: &mut Window, _workspace: WeakEntity, cx: &mut Context, ) -> impl IntoElement { @@ -571,11 +594,25 @@ impl ToolCard for TerminalToolCard { .rounded_lg() .overflow_hidden() .child( - v_flex().p_2().gap_0p5().bg(header_bg).child(header).child( - Label::new(self.input_command.clone()) - .buffer_font(cx) - .size(LabelSize::Small), - ), + v_flex() + .p_2() + .gap_0p5() + .bg(header_bg) + .text_xs() + .child(header) + .child( + MarkdownElement::new( + self.input_command.clone(), + markdown_style(window, cx), + ) + .code_block_renderer( + markdown::CodeBlockRenderer::Default { + copy_button: false, + copy_button_on_hover: true, + border: false, + }, + ), + ), ) .when(self.preview_expanded && !should_hide_terminal, |this| { this.child( @@ -594,6 +631,27 @@ impl ToolCard for TerminalToolCard { } } +fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle { + let theme_settings = ThemeSettings::get_global(cx); + let buffer_font_size = TextSize::Default.rems(cx); + let mut text_style = window.text_style(); + + text_style.refine(&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()), + 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() + } +} + #[cfg(test)] mod tests { use editor::EditorSettings; diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 84c8042d74447e79e43e48cef5dc2bba560bede9..4f97ec04ef9c4bdac05403e9af18f41a617e1927 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -640,6 +640,7 @@ impl CompletionsMenu { MarkdownElement::new(markdown.clone(), hover_markdown_style(window, cx)) .code_block_renderer(markdown::CodeBlockRenderer::Default { copy_button: false, + copy_button_on_hover: false, border: false, }) .on_url_click(open_markdown_url), diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 297abd5c371478885fac98c58f8117001f5924fd..d741a980c00831eac1cb1ef5b385ca2ef2d3982f 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -897,6 +897,7 @@ impl InfoPopover { MarkdownElement::new(markdown, hover_markdown_style(window, cx)) .code_block_renderer(markdown::CodeBlockRenderer::Default { copy_button: false, + copy_button_on_hover: false, border: false, }) .on_url_click(open_markdown_url), diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index f9d9c8871c798e33f619caa0b780b3c3bb8e1ec6..a3a8e7c4564e8d5f440be4c6fe2b4fe98b48b124 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -113,6 +113,7 @@ struct Options { pub enum CodeBlockRenderer { Default { copy_button: bool, + copy_button_on_hover: bool, border: bool, }, Custom { @@ -444,6 +445,7 @@ impl MarkdownElement { style, code_block_renderer: CodeBlockRenderer::Default { copy_button: true, + copy_button_on_hover: false, border: false, }, on_url_click: None, @@ -815,7 +817,7 @@ impl Element for MarkdownElement { (CodeBlockRenderer::Default { .. }, _) | (_, true) => { // This is a parent container that we can position the copy button inside. builder.push_div( - div().relative().w_full(), + div().group("code_block").relative().w_full(), range, markdown_end, ); @@ -1066,6 +1068,37 @@ impl Element for MarkdownElement { }); } + if let CodeBlockRenderer::Default { + copy_button_on_hover: true, + .. + } = &self.code_block_renderer + { + builder.modify_current_div(|el| { + let content_range = parser::extract_code_block_content_range( + parsed_markdown.source()[range.clone()].trim(), + ); + let content_range = content_range.start + range.start + ..content_range.end + range.start; + + let code = parsed_markdown.source()[content_range].to_string(); + let codeblock = render_copy_code_block_button( + range.end, + code, + self.markdown.clone(), + cx, + ); + el.child( + div() + .absolute() + .top_0() + .right_0() + .w_5() + .visible_on_hover("code_block") + .child(codeblock), + ) + }); + } + // Pop the parent container. builder.pop_div(); }