From b60f19f71e1378e35214f533d589e8366f541536 Mon Sep 17 00:00:00 2001
From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Date: Tue, 23 Sep 2025 18:57:28 -0300
Subject: [PATCH] agent: Allow to see the whole command before running it
(#38747)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Closes https://github.com/zed-industries/zed/issues/38528
In the agent panel's `thread_view.rs` file, we have a `render_tool_call`
function that controls what we show in the UI for most types of tools.
However, for some of them—for example, terminal/execute and edit
tools—we have a special rendering so we can tailor the UI for their
specific needs. But... before the specific rendering function is called,
all tools still go through the `render_tool_call`.
Problem is that, in the case of the terminal tool, you couldn't see the
full command the agent wants to run when the tool is still in its
`render_tool_call` state. That's mostly because of the treatment we give
to labels while in that state. A particularly bad scenario because
well... seeing the _full_ command _before_ you choose to accept or
reject is rather important.
This PR fixes that by essentially special-casing the terminal tool
display when in the `render_tool_call` rendering state, so to speak.
There's still a slight UI misalignment I want to fix but it shouldn't
block this fix to go out.
Here's our final result:
Release Notes:
- agent: Fixed terminal command not being fully displayed while in the
"waiting for confirmation" state.
---
crates/agent2/src/tools/terminal_tool.rs | 2 +-
crates/agent_ui/src/acp/thread_view.rs | 328 +++++++++++++----------
2 files changed, 191 insertions(+), 139 deletions(-)
diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs
index 7acfc2455093eac0f3d15e840abce47f38a6c8b0..6d30c19152001deaef5deeacbdf266e28ac03d08 100644
--- a/crates/agent2/src/tools/terminal_tool.rs
+++ b/crates/agent2/src/tools/terminal_tool.rs
@@ -82,7 +82,7 @@ impl AgentTool for TerminalTool {
.into(),
}
} else {
- "Run terminal command".into()
+ "".into()
}
}
diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs
index cf6f563ecdeb5a60e2b3faa4a5ee36d3282e1bdb..4a91a9fff7b9902bc51301752a578114159bf680 100644
--- a/crates/agent_ui/src/acp/thread_view.rs
+++ b/crates/agent_ui/src/acp/thread_view.rs
@@ -2079,27 +2079,6 @@ impl AcpThreadView {
let has_location = tool_call.locations.len() == 1;
let card_header_id = SharedString::from("inner-tool-call-header");
- let tool_icon = if tool_call.kind == acp::ToolKind::Edit && has_location {
- FileIcons::get_icon(&tool_call.locations[0].path, cx)
- .map(Icon::from_path)
- .unwrap_or(Icon::new(IconName::ToolPencil))
- } else {
- Icon::new(match tool_call.kind {
- acp::ToolKind::Read => IconName::ToolSearch,
- acp::ToolKind::Edit => IconName::ToolPencil,
- acp::ToolKind::Delete => IconName::ToolDeleteFile,
- acp::ToolKind::Move => IconName::ArrowRightLeft,
- acp::ToolKind::Search => IconName::ToolSearch,
- acp::ToolKind::Execute => IconName::ToolTerminal,
- acp::ToolKind::Think => IconName::ToolThink,
- acp::ToolKind::Fetch => IconName::ToolWeb,
- acp::ToolKind::SwitchMode => IconName::ArrowRightLeft,
- acp::ToolKind::Other => IconName::ToolHammer,
- })
- }
- .size(IconSize::Small)
- .color(Color::Muted);
-
let failed_or_canceled = match &tool_call.status {
ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true,
_ => false,
@@ -2109,41 +2088,16 @@ impl AcpThreadView {
tool_call.status,
ToolCallStatus::WaitingForConfirmation { .. }
);
+ let is_terminal_tool = matches!(tool_call.kind, acp::ToolKind::Execute);
let is_edit =
matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some();
- let use_card_layout = needs_confirmation || is_edit;
+
+ let use_card_layout = needs_confirmation || is_edit || is_terminal_tool;
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
- let gradient_overlay = {
- div()
- .absolute()
- .top_0()
- .right_0()
- .w_12()
- .h_full()
- .map(|this| {
- if use_card_layout {
- this.bg(linear_gradient(
- 90.,
- linear_color_stop(self.tool_card_header_bg(cx), 1.),
- linear_color_stop(self.tool_card_header_bg(cx).opacity(0.2), 0.),
- ))
- } else {
- this.bg(linear_gradient(
- 90.,
- linear_color_stop(cx.theme().colors().panel_background, 1.),
- linear_color_stop(
- cx.theme().colors().panel_background.opacity(0.2),
- 0.,
- ),
- ))
- }
- })
- };
-
let tool_output_display =
if is_open {
match &tool_call.status {
@@ -2228,104 +2182,202 @@ impl AcpThreadView {
}
})
.mr_5()
- .child(
- h_flex()
- .group(&card_header_id)
- .relative()
- .w_full()
- .gap_1()
- .justify_between()
- .when(use_card_layout, |this| {
- this.p_0p5()
- .rounded_t(rems_from_px(5.))
+ .map(|this| {
+ if is_terminal_tool {
+ this.child(
+ v_flex()
+ .p_1p5()
+ .gap_0p5()
+ .text_ui_sm(cx)
.bg(self.tool_card_header_bg(cx))
- })
- .child(
+ .child(
+ Label::new("Run Command")
+ .buffer_font(cx)
+ .size(LabelSize::XSmall)
+ .color(Color::Muted),
+ )
+ .child(
+ MarkdownElement::new(
+ tool_call.label.clone(),
+ terminal_command_markdown_style(window, cx),
+ )
+ .code_block_renderer(
+ markdown::CodeBlockRenderer::Default {
+ copy_button: false,
+ copy_button_on_hover: false,
+ border: false,
+ },
+ )
+ ),
+ )
+ } else {
+ this.child(
h_flex()
+ .group(&card_header_id)
.relative()
.w_full()
- .h(window.line_height() - px(2.))
- .text_size(self.tool_name_font_size())
- .gap_1p5()
- .when(has_location || use_card_layout, |this| this.px_1())
- .when(has_location, |this| {
- this.cursor(CursorStyle::PointingHand)
- .rounded(rems_from_px(3.)) // Concentric border radius
- .hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.5)))
- })
- .overflow_hidden()
- .child(tool_icon)
- .child(if has_location {
- h_flex()
- .id(("open-tool-call-location", entry_ix))
- .w_full()
- .map(|this| {
- if use_card_layout {
- this.text_color(cx.theme().colors().text)
- } else {
- this.text_color(cx.theme().colors().text_muted)
- }
- })
- .child(self.render_markdown(
- tool_call.label.clone(),
- MarkdownStyle {
- prevent_mouse_interaction: true,
- ..default_markdown_style(false, true, window, cx)
- },
- ))
- .tooltip(Tooltip::text("Jump to File"))
- .on_click(cx.listener(move |this, _, window, cx| {
- this.open_tool_call_location(entry_ix, 0, window, cx);
- }))
- .into_any_element()
- } else {
- h_flex()
- .w_full()
- .child(self.render_markdown(
- tool_call.label.clone(),
- default_markdown_style(false, true, window, cx),
- ))
- .into_any()
+ .gap_1()
+ .justify_between()
+ .when(use_card_layout, |this| {
+ this.p_0p5()
+ .rounded_t(rems_from_px(5.))
+ .bg(self.tool_card_header_bg(cx))
})
- .when(!has_location, |this| this.child(gradient_overlay)),
- )
- .when(is_collapsible || failed_or_canceled, |this| {
- this.child(
- h_flex()
- .px_1()
- .gap_px()
- .when(is_collapsible, |this| {
- this.child(
- Disclosure::new(("expand", entry_ix), is_open)
- .opened_icon(IconName::ChevronUp)
- .closed_icon(IconName::ChevronDown)
- .visible_on_hover(&card_header_id)
- .on_click(cx.listener({
- let id = tool_call.id.clone();
- move |this: &mut Self, _, _, cx: &mut Context| {
- if is_open {
- this.expanded_tool_calls.remove(&id);
- } else {
- this.expanded_tool_calls.insert(id.clone());
- }
- cx.notify();
- }
- })),
+ .child(self.render_tool_call_label(
+ entry_ix,
+ tool_call,
+ is_edit,
+ use_card_layout,
+ window,
+ cx,
+ ))
+ .when(is_collapsible || failed_or_canceled, |this| {
+ this.child(
+ h_flex()
+ .px_1()
+ .gap_px()
+ .when(is_collapsible, |this| {
+ this.child(
+ Disclosure::new(("expand", entry_ix), is_open)
+ .opened_icon(IconName::ChevronUp)
+ .closed_icon(IconName::ChevronDown)
+ .visible_on_hover(&card_header_id)
+ .on_click(cx.listener({
+ let id = tool_call.id.clone();
+ move |this: &mut Self, _, _, cx: &mut Context| {
+ if is_open {
+ this.expanded_tool_calls.remove(&id);
+ } else {
+ this.expanded_tool_calls.insert(id.clone());
+ }
+ cx.notify();
+ }
+ })),
+ )
+ })
+ .when(failed_or_canceled, |this| {
+ this.child(
+ Icon::new(IconName::Close)
+ .color(Color::Error)
+ .size(IconSize::Small),
+ )
+ }),
)
- })
- .when(failed_or_canceled, |this| {
- this.child(
- Icon::new(IconName::Close)
- .color(Color::Error)
- .size(IconSize::Small),
- )
- }),
- )
- }),
- )
+ }),
+ )
+ }
+ })
.children(tool_output_display)
}
+ fn render_tool_call_label(
+ &self,
+ entry_ix: usize,
+ tool_call: &ToolCall,
+ is_edit: bool,
+ use_card_layout: bool,
+ window: &Window,
+ cx: &Context,
+ ) -> Div {
+ let has_location = tool_call.locations.len() == 1;
+
+ let tool_icon = if tool_call.kind == acp::ToolKind::Edit && has_location {
+ FileIcons::get_icon(&tool_call.locations[0].path, cx)
+ .map(Icon::from_path)
+ .unwrap_or(Icon::new(IconName::ToolPencil))
+ } else {
+ Icon::new(match tool_call.kind {
+ acp::ToolKind::Read => IconName::ToolSearch,
+ acp::ToolKind::Edit => IconName::ToolPencil,
+ acp::ToolKind::Delete => IconName::ToolDeleteFile,
+ acp::ToolKind::Move => IconName::ArrowRightLeft,
+ acp::ToolKind::Search => IconName::ToolSearch,
+ acp::ToolKind::Execute => IconName::ToolTerminal,
+ acp::ToolKind::Think => IconName::ToolThink,
+ acp::ToolKind::Fetch => IconName::ToolWeb,
+ acp::ToolKind::SwitchMode => IconName::ArrowRightLeft,
+ acp::ToolKind::Other => IconName::ToolHammer,
+ })
+ }
+ .size(IconSize::Small)
+ .color(Color::Muted);
+
+ let gradient_overlay = {
+ div()
+ .absolute()
+ .top_0()
+ .right_0()
+ .w_12()
+ .h_full()
+ .map(|this| {
+ if use_card_layout {
+ this.bg(linear_gradient(
+ 90.,
+ linear_color_stop(self.tool_card_header_bg(cx), 1.),
+ linear_color_stop(self.tool_card_header_bg(cx).opacity(0.2), 0.),
+ ))
+ } else {
+ this.bg(linear_gradient(
+ 90.,
+ linear_color_stop(cx.theme().colors().panel_background, 1.),
+ linear_color_stop(
+ cx.theme().colors().panel_background.opacity(0.2),
+ 0.,
+ ),
+ ))
+ }
+ })
+ };
+
+ h_flex()
+ .relative()
+ .w_full()
+ .h(window.line_height() - px(2.))
+ .text_size(self.tool_name_font_size())
+ .gap_1p5()
+ .when(has_location || use_card_layout, |this| this.px_1())
+ .when(has_location, |this| {
+ this.cursor(CursorStyle::PointingHand)
+ .rounded(rems_from_px(3.)) // Concentric border radius
+ .hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.5)))
+ })
+ .overflow_hidden()
+ .child(tool_icon)
+ .child(if has_location {
+ h_flex()
+ .id(("open-tool-call-location", entry_ix))
+ .w_full()
+ .map(|this| {
+ if use_card_layout {
+ this.text_color(cx.theme().colors().text)
+ } else {
+ this.text_color(cx.theme().colors().text_muted)
+ }
+ })
+ .child(self.render_markdown(
+ tool_call.label.clone(),
+ MarkdownStyle {
+ prevent_mouse_interaction: true,
+ ..default_markdown_style(false, true, window, cx)
+ },
+ ))
+ .tooltip(Tooltip::text("Jump to File"))
+ .on_click(cx.listener(move |this, _, window, cx| {
+ this.open_tool_call_location(entry_ix, 0, window, cx);
+ }))
+ .into_any_element()
+ } else {
+ h_flex()
+ .w_full()
+ .child(self.render_markdown(
+ tool_call.label.clone(),
+ default_markdown_style(false, true, window, cx),
+ ))
+ .into_any()
+ })
+ .when(!is_edit, |this| this.child(gradient_overlay))
+ }
+
fn render_tool_call_content(
&self,
entry_ix: usize,