From 044508ef779775c41c2b423fdcb4196b01eb0dac Mon Sep 17 00:00:00 2001
From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Date: Fri, 28 Mar 2025 19:33:56 -0300
Subject: [PATCH] assistant2: Visually de-emphasize read-only tool calls
(#27702)
Release Notes:
- N/A
---------
Co-authored-by: Agus Zubiaga
---
crates/assistant2/src/active_thread.rs | 446 +++++++++++++++----------
crates/assistant2/src/tool_use.rs | 9 +-
2 files changed, 268 insertions(+), 187 deletions(-)
diff --git a/crates/assistant2/src/active_thread.rs b/crates/assistant2/src/active_thread.rs
index 1bdf8af3e746f3af29cd8a61e9960f2df07c0260..ab1bddb16aeffd9bff741d62791c3d1c3f29d634 100644
--- a/crates/assistant2/src/active_thread.rs
+++ b/crates/assistant2/src/active_thread.rs
@@ -1401,209 +1401,287 @@ impl ActiveThread {
.copied()
.unwrap_or_default();
- div().py_2().child(
- v_flex()
- .rounded_lg()
- .border_1()
- .border_color(self.tool_card_border_color(cx))
- .overflow_hidden()
- .child(
- h_flex()
- .group("disclosure-header")
- .relative()
- .gap_1p5()
- .justify_between()
- .py_1()
- .px_2()
- .bg(self.tool_card_header_bg(cx))
- .map(|element| {
- if is_open {
- element.border_b_1().rounded_t_md()
- } else {
- element.rounded_md()
- }
- })
+ let status_icons = div().child({
+ let (icon_name, color, animated) = match &tool_use.status {
+ ToolUseStatus::Pending | ToolUseStatus::NeedsConfirmation => {
+ (IconName::Warning, Color::Warning, false)
+ }
+ ToolUseStatus::Running => (IconName::ArrowCircle, Color::Accent, true),
+ ToolUseStatus::Finished(_) => (IconName::Check, Color::Success, false),
+ ToolUseStatus::Error(_) => (IconName::Close, Color::Error, false),
+ };
+
+ let icon = Icon::new(icon_name).color(color).size(IconSize::Small);
+
+ if animated {
+ icon.with_animation(
+ "arrow-circle",
+ Animation::new(Duration::from_secs(2)).repeat(),
+ |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
+ )
+ .into_any_element()
+ } else {
+ icon.into_any_element()
+ }
+ });
+
+ let content_container = || v_flex().py_1().gap_0p5().px_2p5();
+ let results_content = v_flex()
+ .gap_1()
+ .child(
+ content_container()
+ .child(
+ Label::new("Input")
+ .size(LabelSize::XSmall)
+ .color(Color::Muted)
+ .buffer_font(cx),
+ )
+ .child(
+ Label::new(
+ serde_json::to_string_pretty(&tool_use.input).unwrap_or_default(),
+ )
+ .size(LabelSize::Small)
+ .buffer_font(cx),
+ ),
+ )
+ .map(|container| match tool_use.status {
+ ToolUseStatus::Finished(output) => container.child(
+ content_container()
+ .border_t_1()
.border_color(self.tool_card_border_color(cx))
+ .child(
+ Label::new("Result")
+ .size(LabelSize::XSmall)
+ .color(Color::Muted)
+ .buffer_font(cx),
+ )
+ .child(Label::new(output).size(LabelSize::Small).buffer_font(cx)),
+ ),
+ ToolUseStatus::Running => container.child(
+ content_container().child(
+ h_flex()
+ .gap_1()
+ .pb_1()
+ .border_t_1()
+ .border_color(self.tool_card_border_color(cx))
+ .child(
+ Icon::new(IconName::ArrowCircle)
+ .size(IconSize::Small)
+ .color(Color::Accent)
+ .with_animation(
+ "arrow-circle",
+ Animation::new(Duration::from_secs(2)).repeat(),
+ |icon, delta| {
+ icon.transform(Transformation::rotate(percentage(
+ delta,
+ )))
+ },
+ ),
+ )
+ .child(
+ Label::new("Running…")
+ .size(LabelSize::XSmall)
+ .color(Color::Muted)
+ .buffer_font(cx),
+ ),
+ ),
+ ),
+ ToolUseStatus::Error(err) => container.child(
+ content_container()
+ .border_t_1()
+ .border_color(self.tool_card_border_color(cx))
+ .child(
+ Label::new("Error")
+ .size(LabelSize::XSmall)
+ .color(Color::Muted)
+ .buffer_font(cx),
+ )
+ .child(Label::new(err).size(LabelSize::Small).buffer_font(cx)),
+ ),
+ ToolUseStatus::Pending => container,
+ ToolUseStatus::NeedsConfirmation => container.child(
+ content_container()
+ .border_t_1()
+ .border_color(self.tool_card_border_color(cx))
+ .child(
+ Label::new("Asking Permission")
+ .size(LabelSize::Small)
+ .color(Color::Muted)
+ .buffer_font(cx),
+ ),
+ ),
+ });
+
+ fn gradient_overlay(color: Hsla) -> impl IntoElement {
+ div()
+ .h_full()
+ .absolute()
+ .w_8()
+ .bottom_0()
+ .right_12()
+ .bg(linear_gradient(
+ 90.,
+ linear_color_stop(color, 1.),
+ linear_color_stop(color.opacity(0.2), 0.),
+ ))
+ }
+
+ div().map(|this| {
+ if !tool_use.needs_confirmation {
+ this.py_2p5().child(
+ v_flex()
.child(
h_flex()
- .id("tool-label-container")
+ .group("disclosure-header")
.relative()
.gap_1p5()
- .max_w_full()
- .overflow_x_scroll()
+ .justify_between()
+ .opacity(0.8)
+ .hover(|style| style.opacity(1.))
+ .pr_2()
.child(
- Icon::new(tool_use.icon)
- .size(IconSize::XSmall)
- .color(Color::Muted),
+ h_flex()
+ .id("tool-label-container")
+ .gap_1p5()
+ .max_w_full()
+ .overflow_x_scroll()
+ .child(
+ Icon::new(tool_use.icon)
+ .size(IconSize::XSmall)
+ .color(Color::Muted),
+ )
+ .child(
+ h_flex().pr_8().text_ui_sm(cx).children(
+ self.rendered_tool_use_labels
+ .get(&tool_use.id)
+ .cloned(),
+ ),
+ ),
)
- .child(h_flex().pr_8().text_ui_sm(cx).children(
- self.rendered_tool_use_labels.get(&tool_use.id).cloned(),
- )),
- )
- .child(
- h_flex()
- .gap_1()
.child(
- div().visible_on_hover("disclosure-header").child(
- Disclosure::new("tool-use-disclosure", is_open)
- .opened_icon(IconName::ChevronUp)
- .closed_icon(IconName::ChevronDown)
- .on_click(cx.listener({
- let tool_use_id = tool_use.id.clone();
- move |this, _event, _window, _cx| {
- let is_open = this
- .expanded_tool_uses
- .entry(tool_use_id.clone())
- .or_insert(false);
-
- *is_open = !*is_open;
- }
- })),
- ),
- )
- .child({
- let (icon_name, color, animated) = match &tool_use.status {
- ToolUseStatus::Pending
- | ToolUseStatus::NeedsConfirmation => {
- (IconName::Warning, Color::Warning, false)
- }
- ToolUseStatus::Running => {
- (IconName::ArrowCircle, Color::Accent, true)
- }
- ToolUseStatus::Finished(_) => {
- (IconName::Check, Color::Success, false)
- }
- ToolUseStatus::Error(_) => {
- (IconName::Close, Color::Error, false)
- }
- };
-
- let icon =
- Icon::new(icon_name).color(color).size(IconSize::Small);
-
- if animated {
- icon.with_animation(
- "arrow-circle",
- Animation::new(Duration::from_secs(2)).repeat(),
- |icon, delta| {
- icon.transform(Transformation::rotate(percentage(
- delta,
- )))
- },
+ h_flex()
+ .gap_1()
+ .child(
+ div().visible_on_hover("disclosure-header").child(
+ Disclosure::new("tool-use-disclosure", is_open)
+ .opened_icon(IconName::ChevronUp)
+ .closed_icon(IconName::ChevronDown)
+ .on_click(cx.listener({
+ let tool_use_id = tool_use.id.clone();
+ move |this, _event, _window, _cx| {
+ let is_open = this
+ .expanded_tool_uses
+ .entry(tool_use_id.clone())
+ .or_insert(false);
+
+ *is_open = !*is_open;
+ }
+ })),
+ ),
)
- .into_any_element()
- } else {
- icon.into_any_element()
- }
- }),
+ .child(status_icons),
+ )
+ .child(gradient_overlay(cx.theme().colors().panel_background)),
)
- .child(div().h_full().absolute().w_8().bottom_0().right_12().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.),
- ),
- )),
- )
- .map(|parent| {
- if !is_open {
- return parent;
- }
-
- let content_container = || v_flex().py_1().gap_0p5().px_2p5();
+ .map(|parent| {
+ if !is_open {
+ return parent;
+ }
- parent.child(
- v_flex()
- .gap_1()
- .bg(cx.theme().colors().editor_background)
- .rounded_b_lg()
- .child(
- content_container()
- .border_b_1()
+ parent.child(
+ v_flex()
+ .mt_1()
+ .border_1()
.border_color(self.tool_card_border_color(cx))
- .child(
- Label::new("Input")
- .size(LabelSize::XSmall)
- .color(Color::Muted)
- .buffer_font(cx),
- )
- .child(
- Label::new(
- serde_json::to_string_pretty(&tool_use.input)
- .unwrap_or_default(),
- )
- .size(LabelSize::Small)
- .buffer_font(cx),
- ),
+ .bg(cx.theme().colors().editor_background)
+ .rounded_lg()
+ .child(results_content),
)
- .map(|container| match tool_use.status {
- ToolUseStatus::Finished(output) => container.child(
- content_container()
+ }),
+ )
+ } else {
+ this.py_2().child(
+ v_flex()
+ .rounded_lg()
+ .border_1()
+ .border_color(self.tool_card_border_color(cx))
+ .overflow_hidden()
+ .child(
+ h_flex()
+ .group("disclosure-header")
+ .relative()
+ .gap_1p5()
+ .justify_between()
+ .py_1()
+ .px_2()
+ .bg(self.tool_card_header_bg(cx))
+ .map(|element| {
+ if is_open {
+ element.border_b_1().rounded_t_md()
+ } else {
+ element.rounded_md()
+ }
+ })
+ .border_color(self.tool_card_border_color(cx))
+ .child(
+ h_flex()
+ .id("tool-label-container")
+ .gap_1p5()
+ .max_w_full()
+ .overflow_x_scroll()
.child(
- Label::new("Result")
- .size(LabelSize::XSmall)
- .color(Color::Muted)
- .buffer_font(cx),
+ Icon::new(tool_use.icon)
+ .size(IconSize::XSmall)
+ .color(Color::Muted),
)
.child(
- Label::new(output)
- .size(LabelSize::Small)
- .buffer_font(cx),
- ),
- ),
- ToolUseStatus::Running => container.child(
- content_container().child(
- h_flex()
- .gap_1()
- .pb_1()
- .child(
- Icon::new(IconName::ArrowCircle)
- .size(IconSize::Small)
- .color(Color::Accent)
- .with_animation(
- "arrow-circle",
- Animation::new(Duration::from_secs(2))
- .repeat(),
- |icon, delta| {
- icon.transform(Transformation::rotate(
- percentage(delta),
- ))
- },
- ),
- )
- .child(
- Label::new("Running…")
- .size(LabelSize::XSmall)
- .color(Color::Muted)
- .buffer_font(cx),
+ h_flex().pr_8().text_ui_sm(cx).children(
+ self.rendered_tool_use_labels
+ .get(&tool_use.id)
+ .cloned(),
),
- ),
- ),
- ToolUseStatus::Error(err) => container.child(
- content_container()
+ ),
+ )
+ .child(
+ h_flex()
+ .gap_1()
.child(
- Label::new("Error")
- .size(LabelSize::XSmall)
- .color(Color::Muted)
- .buffer_font(cx),
+ div().visible_on_hover("disclosure-header").child(
+ Disclosure::new("tool-use-disclosure", is_open)
+ .opened_icon(IconName::ChevronUp)
+ .closed_icon(IconName::ChevronDown)
+ .on_click(cx.listener({
+ let tool_use_id = tool_use.id.clone();
+ move |this, _event, _window, _cx| {
+ let is_open = this
+ .expanded_tool_uses
+ .entry(tool_use_id.clone())
+ .or_insert(false);
+
+ *is_open = !*is_open;
+ }
+ })),
+ ),
)
- .child(
- Label::new(err).size(LabelSize::Small).buffer_font(cx),
- ),
- ),
- ToolUseStatus::Pending => container,
- ToolUseStatus::NeedsConfirmation => container.child(
- content_container().child(
- Label::new("Asking Permission")
- .size(LabelSize::Small)
- .color(Color::Muted)
- .buffer_font(cx),
- ),
- ),
- }),
- )
- }),
- )
+ .child(status_icons),
+ )
+ .child(gradient_overlay(self.tool_card_header_bg(cx))),
+ )
+ .map(|parent| {
+ if !is_open {
+ return parent;
+ }
+
+ parent.child(
+ v_flex()
+ .bg(cx.theme().colors().editor_background)
+ .rounded_b_lg()
+ .child(results_content),
+ )
+ }),
+ )
+ }
+ })
}
fn render_rules_item(&self, cx: &Context) -> AnyElement {
diff --git a/crates/assistant2/src/tool_use.rs b/crates/assistant2/src/tool_use.rs
index cec6623aea68907fbf4db5fe9faedf5dad4783fb..a14053cff03b55cc289dffce2eb636d519419fdd 100644
--- a/crates/assistant2/src/tool_use.rs
+++ b/crates/assistant2/src/tool_use.rs
@@ -23,6 +23,7 @@ pub struct ToolUse {
pub status: ToolUseStatus,
pub input: serde_json::Value,
pub icon: ui::IconName,
+ pub needs_confirmation: bool,
}
#[derive(Debug, Clone)]
@@ -181,10 +182,11 @@ impl ToolUseState {
}
})();
- let icon = if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
- tool.icon()
+ let (icon, needs_confirmation) = if let Some(tool) = self.tools.tool(&tool_use.name, cx)
+ {
+ (tool.icon(), tool.needs_confirmation())
} else {
- IconName::Cog
+ (IconName::Cog, false)
};
tool_uses.push(ToolUse {
@@ -194,6 +196,7 @@ impl ToolUseState {
input: tool_use.input.clone(),
status,
icon,
+ needs_confirmation,
})
}