agent: Allow to see the whole command before running it (#38747)
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.
This commit is contained in:
@@ -82,7 +82,7 @@ impl AgentTool for TerminalTool {
|
||||
.into(),
|
||||
}
|
||||
} else {
|
||||
"Run terminal command".into()
|
||||
"".into()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)))
|
||||
.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))
|
||||
})
|
||||
.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(!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,
|
||||
|
||||
Reference in New Issue
Block a user