agent: Allow to see the whole command before running it (#38747)

Danilo Leal created

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:

<img width="400" height="1172" alt="Screenshot 2025-09-23 at 6  19@2x"
src="https://github.com/user-attachments/assets/71c79e45-ab66-4102-b046-950f137fa3ea"
/>

Release Notes:

- agent: Fixed terminal command not being fully displayed while in the
"waiting for confirmation" state.

Change summary

crates/agent2/src/tools/terminal_tool.rs |   2 
crates/agent_ui/src/acp/thread_view.rs   | 328 +++++++++++++++----------
2 files changed, 191 insertions(+), 139 deletions(-)

Detailed changes

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<Self>| {
-                                                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<Self>| {
+                                                        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<Self>,
+    ) -> 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,