agent_ui: Allow selection of commands from tool calls (#50545)

Closes #50427.

Before you mark this PR as ready for review, make sure that you have:
- [x] Added a solid test coverage and/or screenshots from doing manual
testing
- [x] Done a self-review taking into account security and performance
aspects
- [x] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)

The changes respect your current theme + mimic the existing UI when it
comes to font size+weight.

<img width="437" height="131" alt="image"
src="https://github.com/user-attachments/assets/89f6c5d0-d1a3-478a-88e9-7d203240416d"
/>


https://github.com/user-attachments/assets/a5fb6c82-fffd-494f-a374-9296d1690736

Release Notes:

- Allow selection of commands from tool calls
This commit is contained in:
Kunall Banerjee 2026-04-24 09:54:28 -04:00 committed by GitHub
parent 1b37531a2e
commit 280f8b1048
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 68 additions and 46 deletions

View file

@ -38,7 +38,9 @@ use gpui::{
};
use language::Buffer;
use language_model::{LanguageModelCompletionError, LanguageModelRegistry};
use markdown::{Markdown, MarkdownElement, MarkdownFont, MarkdownStyle};
use markdown::{
CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownFont, MarkdownStyle,
};
use parking_lot::RwLock;
use project::{AgentId, AgentServerStore, Project, ProjectEntryId};
use prompt_store::{PromptId, PromptStore};

View file

@ -5910,42 +5910,58 @@ impl ThreadView {
&self,
group: SharedString,
is_preview: bool,
command_source: &str,
command: Entity<Markdown>,
window: &Window,
cx: &Context<Self>,
) -> Div {
v_flex()
.group(group.clone())
.p_1p5()
.bg(self.tool_card_header_bg(cx))
.when(is_preview, |this| {
this.pt_1().child(
// Wrapping this label on a container with 24px height to avoid
// layout shift when it changes from being a preview label
// to the actual path where the command will run in
h_flex().h_6().child(
Label::new("Run Command")
.buffer_font(cx)
.size(LabelSize::XSmall)
.color(Color::Muted),
),
)
})
.children(command_source.lines().map(|line| {
let text: SharedString = if line.is_empty() {
" ".into()
} else {
line.to_string().into()
};
// The label's markdown source is a fenced code block (```\n...\n```);
// strip the fences so the copy button yields just the command text.
let command_source = command.read(cx).source();
let command_text = command_source
.strip_prefix("```\n")
.and_then(|s| s.strip_suffix("\n```"))
.unwrap_or(&command_source)
.to_string();
Label::new(text).buffer_font(cx).size(LabelSize::Small)
}))
.child(
div().absolute().top_1().right_1().child(
CopyButton::new("copy-command", command_source.to_string())
.tooltip_label("Copy Command")
.visible_on_hover(group),
let mut style = MarkdownStyle::themed(MarkdownFont::Agent, window, cx).with_buffer_font(cx);
style.container_style.text.font_size = Some(rems_from_px(12.).into());
style.container_style.text.line_height = Some(rems_from_px(17.).into());
style.height_is_multiple_of_line_height = true;
let header_bg = self.tool_card_header_bg(cx);
let run_command_label = if is_preview {
Some(
h_flex().h_6().child(
Label::new("Run Command")
.buffer_font(cx)
.size(LabelSize::XSmall)
.color(Color::Muted),
),
)
} else {
None
};
// Suppress the code block's built-in copy button so we don't stack two
// copy buttons on top of each other; the outer button below is the one
// we want, because it copies the unfenced command text.
let markdown_element =
self.render_markdown(command, style)
.code_block_renderer(CodeBlockRenderer::Default {
copy_button_visibility: CopyButtonVisibility::Hidden,
border: false,
});
let copy_button = CopyButton::new("copy-command", command_text)
.tooltip_label("Copy Command")
.visible_on_hover(group.clone());
v_flex()
.group(group)
.relative()
.p_1p5()
.bg(header_bg)
.when(is_preview, |this| this.pt_1().children(run_command_label))
.child(markdown_element)
.child(div().absolute().top_1().right_1().child(copy_button))
}
fn render_terminal_tool_call(
@ -5961,7 +5977,6 @@ impl ThreadView {
) -> AnyElement {
let terminal_data = terminal.read(cx);
let working_dir = terminal_data.working_dir();
let command = terminal_data.command();
let started_at = terminal_data.started_at();
let tool_failed = matches!(
@ -6012,17 +6027,13 @@ impl ThreadView {
.map(|path| path.display().to_string())
.unwrap_or_else(|| "current directory".to_string());
// Since the command's source is wrapped in a markdown code block
// (```\n...\n```), we need to strip that so we're left with only the
// command's content.
let command_source = command.read(cx).source();
let command_content = command_source
.strip_prefix("```\n")
.and_then(|s| s.strip_suffix("\n```"))
.unwrap_or(&command_source);
let command_element =
self.render_collapsible_command(header_group.clone(), false, command_content, cx);
let command_element = self.render_collapsible_command(
header_group.clone(),
false,
tool_call.label.clone(),
window,
cx,
);
let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
@ -6565,11 +6576,11 @@ impl ThreadView {
})
.map(|this| {
if is_terminal_tool {
let label_source = tool_call.label.read(cx).source();
this.child(self.render_collapsible_command(
card_header_id.clone(),
true,
label_source,
tool_call.label.clone(),
window,
cx,
))
} else {

View file

@ -297,6 +297,15 @@ impl MarkdownStyle {
}
}
pub fn with_buffer_font(mut self, cx: &App) -> Self {
let theme_settings = ThemeSettings::get_global(cx);
self.base_text_style.font_family = theme_settings.buffer_font.family.clone();
self.base_text_style.font_fallbacks = theme_settings.buffer_font.fallbacks.clone();
self.base_text_style.font_features = theme_settings.buffer_font.features.clone();
self.base_text_style.font_weight = theme_settings.buffer_font.weight;
self
}
pub fn with_muted_text(mut self, cx: &App) -> Self {
let colors = cx.theme().colors();
self.base_text_style.color = colors.text_muted;