From a71dbfcb58e93de4c69414eb0b2719d3ea3c7203 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 876943f98bc6265e312cb0a664268599e6322000..1837c236704fc0a7343627c20e4afe5ee13c18dc 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 509b70125eeac53300c9a4fd18b9ff368a5f61dd..e76aa149b6a7c07c629d210973a26d8463ebb7d6 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -109,6 +109,7 @@ struct Options { pub enum CodeBlockRenderer { Default { copy_button: bool, + copy_button_on_hover: bool, border: bool, }, Custom { @@ -401,6 +402,7 @@ impl MarkdownElement { style, code_block_renderer: CodeBlockRenderer::Default { copy_button: true, + copy_button_on_hover: false, border: false, }, on_url_click: None, @@ -752,7 +754,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, ); @@ -1001,6 +1003,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(); }