assistant2: Visually de-emphasize read-only tool calls (#27702)

<img
src="https://github.com/user-attachments/assets/03961518-ae40-47d8-b84c-974c9b897eb3"
width="500"/>

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
This commit is contained in:
Danilo Leal 2025-03-28 19:33:56 -03:00 committed by GitHub
parent d63658cee2
commit 044508ef77
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 272 additions and 191 deletions

View file

@ -1401,209 +1401,287 @@ impl ActiveThread {
.copied() .copied()
.unwrap_or_default(); .unwrap_or_default();
div().py_2().child( let status_icons = div().child({
v_flex() let (icon_name, color, animated) = match &tool_use.status {
.rounded_lg() ToolUseStatus::Pending | ToolUseStatus::NeedsConfirmation => {
.border_1() (IconName::Warning, Color::Warning, false)
.border_color(self.tool_card_border_color(cx)) }
.overflow_hidden() ToolUseStatus::Running => (IconName::ArrowCircle, Color::Accent, true),
.child( ToolUseStatus::Finished(_) => (IconName::Check, Color::Success, false),
h_flex() ToolUseStatus::Error(_) => (IconName::Close, Color::Error, false),
.group("disclosure-header") };
.relative()
.gap_1p5() let icon = Icon::new(icon_name).color(color).size(IconSize::Small);
.justify_between()
.py_1() if animated {
.px_2() icon.with_animation(
.bg(self.tool_card_header_bg(cx)) "arrow-circle",
.map(|element| { Animation::new(Duration::from_secs(2)).repeat(),
if is_open { |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
element.border_b_1().rounded_t_md() )
} else { .into_any_element()
element.rounded_md() } 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)) .border_color(self.tool_card_border_color(cx))
.child( .child(
h_flex() Label::new("Result")
.id("tool-label-container") .size(LabelSize::XSmall)
.relative() .color(Color::Muted)
.gap_1p5() .buffer_font(cx),
.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( .child(Label::new(output).size(LabelSize::Small).buffer_font(cx)),
h_flex() ),
.gap_1() ToolUseStatus::Running => container.child(
.child( content_container().child(
div().visible_on_hover("disclosure-header").child( h_flex()
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,
)))
},
)
.into_any_element()
} else {
icon.into_any_element()
}
}),
)
.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();
parent.child(
v_flex()
.gap_1() .gap_1()
.bg(cx.theme().colors().editor_background) .pb_1()
.rounded_b_lg() .border_t_1()
.border_color(self.tool_card_border_color(cx))
.child( .child(
content_container() Icon::new(IconName::ArrowCircle)
.border_b_1() .size(IconSize::Small)
.border_color(self.tool_card_border_color(cx)) .color(Color::Accent)
.child( .with_animation(
Label::new("Input") "arrow-circle",
.size(LabelSize::XSmall) Animation::new(Duration::from_secs(2)).repeat(),
.color(Color::Muted) |icon, delta| {
.buffer_font(cx), icon.transform(Transformation::rotate(percentage(
) delta,
.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 { .child(
ToolUseStatus::Finished(output) => container.child( Label::new("Running…")
content_container() .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()
.group("disclosure-header")
.relative()
.gap_1p5()
.justify_between()
.opacity(0.8)
.hover(|style| style.opacity(1.))
.pr_2()
.child(
h_flex()
.id("tool-label-container")
.gap_1p5()
.max_w_full()
.overflow_x_scroll()
.child( .child(
Label::new("Result") Icon::new(tool_use.icon)
.size(LabelSize::XSmall) .size(IconSize::XSmall)
.color(Color::Muted) .color(Color::Muted),
.buffer_font(cx),
) )
.child( .child(
Label::new(output) h_flex().pr_8().text_ui_sm(cx).children(
.size(LabelSize::Small) self.rendered_tool_use_labels
.buffer_font(cx), .get(&tool_use.id)
), .cloned(),
),
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),
), ),
), ),
), )
ToolUseStatus::Error(err) => container.child( .child(
content_container() h_flex()
.gap_1()
.child( .child(
Label::new("Error") div().visible_on_hover("disclosure-header").child(
.size(LabelSize::XSmall) Disclosure::new("tool-use-disclosure", is_open)
.color(Color::Muted) .opened_icon(IconName::ChevronUp)
.buffer_font(cx), .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(status_icons),
)
.child(gradient_overlay(cx.theme().colors().panel_background)),
)
.map(|parent| {
if !is_open {
return parent;
}
parent.child(
v_flex()
.mt_1()
.border_1()
.border_color(self.tool_card_border_color(cx))
.bg(cx.theme().colors().editor_background)
.rounded_lg()
.child(results_content),
)
}),
)
} 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(
Icon::new(tool_use.icon)
.size(IconSize::XSmall)
.color(Color::Muted),
) )
.child( .child(
Label::new(err).size(LabelSize::Small).buffer_font(cx), h_flex().pr_8().text_ui_sm(cx).children(
self.rendered_tool_use_labels
.get(&tool_use.id)
.cloned(),
),
), ),
), )
ToolUseStatus::Pending => container, .child(
ToolUseStatus::NeedsConfirmation => container.child( h_flex()
content_container().child( .gap_1()
Label::new("Asking Permission") .child(
.size(LabelSize::Small) div().visible_on_hover("disclosure-header").child(
.color(Color::Muted) Disclosure::new("tool-use-disclosure", is_open)
.buffer_font(cx), .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(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<Self>) -> AnyElement { fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {

View file

@ -23,6 +23,7 @@ pub struct ToolUse {
pub status: ToolUseStatus, pub status: ToolUseStatus,
pub input: serde_json::Value, pub input: serde_json::Value,
pub icon: ui::IconName, pub icon: ui::IconName,
pub needs_confirmation: bool,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -181,10 +182,11 @@ impl ToolUseState {
} }
})(); })();
let icon = if let Some(tool) = self.tools.tool(&tool_use.name, cx) { let (icon, needs_confirmation) = if let Some(tool) = self.tools.tool(&tool_use.name, cx)
tool.icon() {
(tool.icon(), tool.needs_confirmation())
} else { } else {
IconName::Cog (IconName::Cog, false)
}; };
tool_uses.push(ToolUse { tool_uses.push(ToolUse {
@ -194,6 +196,7 @@ impl ToolUseState {
input: tool_use.input.clone(), input: tool_use.input.clone(),
status, status,
icon, icon,
needs_confirmation,
}) })
} }