assets/icons/tool_bulb.svg → assets/icons/tool_think.svg 🔗
Danilo Leal created
More notably around how we render tool calls. Nothing too drastic,
though.
Release Notes:
- N/A
assets/icons/tool_think.svg | 0
assets/keymaps/default-linux.json | 4
assets/keymaps/default-macos.json | 4
crates/agent_ui/src/acp/thread_view.rs | 273 ++++++++++++++--------
crates/agent_ui/src/active_thread.rs | 2
crates/assistant_tools/src/thinking_tool.rs | 2
crates/icons/src/icons.rs | 2
crates/ui/src/components/disclosure.rs | 2
8 files changed, 187 insertions(+), 102 deletions(-)
@@ -332,7 +332,9 @@
"enter": "agent::Chat",
"up": "agent::PreviousHistoryMessage",
"down": "agent::NextHistoryMessage",
- "shift-ctrl-r": "agent::OpenAgentDiff"
+ "shift-ctrl-r": "agent::OpenAgentDiff",
+ "ctrl-shift-y": "agent::KeepAll",
+ "ctrl-shift-n": "agent::RejectAll"
}
},
{
@@ -384,7 +384,9 @@
"enter": "agent::Chat",
"up": "agent::PreviousHistoryMessage",
"down": "agent::NextHistoryMessage",
- "shift-ctrl-r": "agent::OpenAgentDiff"
+ "shift-ctrl-r": "agent::OpenAgentDiff",
+ "cmd-shift-y": "agent::KeepAll",
+ "cmd-shift-n": "agent::RejectAll"
}
},
{
@@ -858,6 +858,7 @@ impl AcpThreadView {
.into_any()
}
AgentThreadEntry::ToolCall(tool_call) => div()
+ .w_full()
.py_1p5()
.px_5()
.child(self.render_tool_call(index, tool_call, window, cx))
@@ -903,6 +904,7 @@ impl AcpThreadView {
cx: &Context<Self>,
) -> AnyElement {
let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
+ let card_header_id = SharedString::from("inner-card-header");
let key = (entry_ix, chunk_ix);
let is_open = self.expanded_thinking_blocks.contains(&key);
@@ -910,41 +912,53 @@ impl AcpThreadView {
.child(
h_flex()
.id(header_id)
- .group("disclosure-header")
+ .group(&card_header_id)
+ .relative()
.w_full()
- .justify_between()
+ .gap_1p5()
.opacity(0.8)
.hover(|style| style.opacity(1.))
.child(
h_flex()
- .gap_1p5()
+ .size_4()
+ .justify_center()
.child(
- Icon::new(IconName::ToolBulb)
- .size(IconSize::Small)
- .color(Color::Muted),
+ div()
+ .group_hover(&card_header_id, |s| s.invisible().w_0())
+ .child(
+ Icon::new(IconName::ToolThink)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ ),
)
.child(
- div()
- .text_size(self.tool_name_font_size())
- .child("Thinking"),
+ h_flex()
+ .absolute()
+ .inset_0()
+ .invisible()
+ .justify_center()
+ .group_hover(&card_header_id, |s| s.visible())
+ .child(
+ Disclosure::new(("expand", entry_ix), is_open)
+ .opened_icon(IconName::ChevronUp)
+ .closed_icon(IconName::ChevronRight)
+ .on_click(cx.listener({
+ move |this, _event, _window, cx| {
+ if is_open {
+ this.expanded_thinking_blocks.remove(&key);
+ } else {
+ this.expanded_thinking_blocks.insert(key);
+ }
+ cx.notify();
+ }
+ })),
+ ),
),
)
.child(
- div().visible_on_hover("disclosure-header").child(
- Disclosure::new("thinking-disclosure", is_open)
- .opened_icon(IconName::ChevronUp)
- .closed_icon(IconName::ChevronDown)
- .on_click(cx.listener({
- move |this, _event, _window, cx| {
- if is_open {
- this.expanded_thinking_blocks.remove(&key);
- } else {
- this.expanded_thinking_blocks.insert(key);
- }
- cx.notify();
- }
- })),
- ),
+ div()
+ .text_size(self.tool_name_font_size())
+ .child("Thinking"),
)
.on_click(cx.listener({
move |this, _event, _window, cx| {
@@ -975,6 +989,67 @@ impl AcpThreadView {
.into_any_element()
}
+ fn render_tool_call_icon(
+ &self,
+ group_name: SharedString,
+ entry_ix: usize,
+ is_collapsible: bool,
+ is_open: bool,
+ tool_call: &ToolCall,
+ cx: &Context<Self>,
+ ) -> Div {
+ let tool_icon = Icon::new(match tool_call.kind {
+ acp::ToolKind::Read => IconName::ToolRead,
+ 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::Other => IconName::ToolHammer,
+ })
+ .size(IconSize::Small)
+ .color(Color::Muted);
+
+ if is_collapsible {
+ h_flex()
+ .size_4()
+ .justify_center()
+ .child(
+ div()
+ .group_hover(&group_name, |s| s.invisible().w_0())
+ .child(tool_icon),
+ )
+ .child(
+ h_flex()
+ .absolute()
+ .inset_0()
+ .invisible()
+ .justify_center()
+ .group_hover(&group_name, |s| s.visible())
+ .child(
+ Disclosure::new(("expand", entry_ix), is_open)
+ .opened_icon(IconName::ChevronUp)
+ .closed_icon(IconName::ChevronRight)
+ .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();
+ }
+ })),
+ ),
+ )
+ } else {
+ div().child(tool_icon)
+ }
+ }
+
fn render_tool_call(
&self,
entry_ix: usize,
@@ -982,7 +1057,8 @@ impl AcpThreadView {
window: &Window,
cx: &Context<Self>,
) -> Div {
- let header_id = SharedString::from(format!("tool-call-header-{}", entry_ix));
+ let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix));
+ let card_header_id = SharedString::from("inner-tool-call-header");
let status_icon = match &tool_call.status {
ToolCallStatus::Allowed {
@@ -1031,6 +1107,21 @@ impl AcpThreadView {
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
let is_open = !is_collapsible || self.expanded_tool_calls.contains(&tool_call.id);
+ let gradient_color = cx.theme().colors().panel_background;
+ let gradient_overlay = {
+ div()
+ .absolute()
+ .top_0()
+ .right_0()
+ .w_12()
+ .h_full()
+ .bg(linear_gradient(
+ 90.,
+ linear_color_stop(gradient_color, 1.),
+ linear_color_stop(gradient_color.opacity(0.2), 0.),
+ ))
+ };
+
v_flex()
.when(needs_confirmation, |this| {
this.rounded_lg()
@@ -1047,43 +1138,38 @@ impl AcpThreadView {
.justify_between()
.map(|this| {
if needs_confirmation {
- this.px_2()
+ this.pl_2()
+ .pr_1()
.py_1()
.rounded_t_md()
- .bg(self.tool_card_header_bg(cx))
.border_b_1()
.border_color(self.tool_card_border_color(cx))
+ .bg(self.tool_card_header_bg(cx))
} else {
this.opacity(0.8).hover(|style| style.opacity(1.))
}
})
.child(
h_flex()
- .id("tool-call-header")
- .overflow_x_scroll()
+ .group(&card_header_id)
+ .relative()
+ .w_full()
.map(|this| {
- if needs_confirmation {
- this.text_xs()
+ if tool_call.locations.len() == 1 {
+ this.gap_0()
} else {
- this.text_size(self.tool_name_font_size())
+ this.gap_1p5()
}
})
- .gap_1p5()
- .child(
- Icon::new(match tool_call.kind {
- acp::ToolKind::Read => IconName::ToolRead,
- 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::ToolBulb,
- acp::ToolKind::Fetch => IconName::ToolWeb,
- acp::ToolKind::Other => IconName::ToolHammer,
- })
- .size(IconSize::Small)
- .color(Color::Muted),
- )
+ .text_size(self.tool_name_font_size())
+ .child(self.render_tool_call_icon(
+ card_header_id,
+ entry_ix,
+ is_collapsible,
+ is_open,
+ tool_call,
+ cx,
+ ))
.child(if tool_call.locations.len() == 1 {
let name = tool_call.locations[0]
.path
@@ -1094,13 +1180,11 @@ impl AcpThreadView {
h_flex()
.id(("open-tool-call-location", entry_ix))
- .child(name)
.w_full()
.max_w_full()
- .pr_1()
- .gap_0p5()
- .cursor_pointer()
+ .px_1p5()
.rounded_sm()
+ .overflow_x_scroll()
.opacity(0.8)
.hover(|label| {
label.opacity(1.).bg(cx
@@ -1109,53 +1193,49 @@ impl AcpThreadView {
.element_hover
.opacity(0.5))
})
+ .child(name)
.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 {
- self.render_markdown(
- tool_call.label.clone(),
- default_markdown_style(needs_confirmation, window, cx),
- )
- .into_any()
- }),
- )
- .child(
- h_flex()
- .gap_0p5()
- .when(is_collapsible, |this| {
- this.child(
- Disclosure::new(("expand", entry_ix), is_open)
- .opened_icon(IconName::ChevronUp)
- .closed_icon(IconName::ChevronDown)
- .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();
+ h_flex()
+ .id("non-card-label-container")
+ .w_full()
+ .relative()
+ .overflow_hidden()
+ .child(
+ h_flex()
+ .id("non-card-label")
+ .pr_8()
+ .w_full()
+ .overflow_x_scroll()
+ .child(self.render_markdown(
+ tool_call.label.clone(),
+ default_markdown_style(
+ needs_confirmation,
+ window,
+ cx,
+ ),
+ )),
+ )
+ .child(gradient_overlay)
+ .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());
}
- })),
- )
- })
- .children(status_icon),
+ cx.notify();
+ }
+ }))
+ .into_any()
+ }),
)
- .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();
- }
- })),
+ .children(status_icon),
)
.when(is_open, |this| {
this.child(
@@ -1249,8 +1329,7 @@ impl AcpThreadView {
cx: &Context<Self>,
) -> Div {
h_flex()
- .py_1p5()
- .px_1p5()
+ .p_1p5()
.gap_1()
.justify_end()
.when(!empty_content, |this| {
@@ -1276,6 +1355,7 @@ impl AcpThreadView {
})
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
+ .label_size(LabelSize::Small)
.on_click(cx.listener({
let tool_call_id = tool_call_id.clone();
let option_id = option.id.clone();
@@ -1525,7 +1605,7 @@ impl AcpThreadView {
})
})
.when(!changed_buffers.is_empty(), |this| {
- this.child(Divider::horizontal())
+ this.child(Divider::horizontal().color(DividerColor::Border))
.child(self.render_edits_summary(
action_log,
&changed_buffers,
@@ -1555,6 +1635,7 @@ impl AcpThreadView {
{
h_flex()
.w_full()
+ .cursor_default()
.gap_1()
.text_xs()
.text_color(cx.theme().colors().text_muted)
@@ -1584,7 +1665,7 @@ impl AcpThreadView {
let status_label = if stats.pending == 0 {
"All Done".to_string()
} else if stats.completed == 0 {
- format!("{}", plan.entries.len())
+ format!("{} Tasks", plan.entries.len())
} else {
format!("{}/{}", stats.completed, plan.entries.len())
};
@@ -1698,7 +1779,6 @@ impl AcpThreadView {
.child(
h_flex()
.id("edits-container")
- .cursor_pointer()
.w_full()
.gap_1()
.child(Disclosure::new("edits-disclosure", expanded))
@@ -2473,6 +2553,7 @@ impl AcpThreadView {
}));
h_flex()
+ .w_full()
.mr_1()
.pb_2()
.px(RESPONSE_PADDING_X)
@@ -2624,7 +2624,7 @@ impl ActiveThread {
h_flex()
.gap_1p5()
.child(
- Icon::new(IconName::ToolBulb)
+ Icon::new(IconName::ToolThink)
.size(IconSize::Small)
.color(Color::Muted),
)
@@ -37,7 +37,7 @@ impl Tool for ThinkingTool {
}
fn icon(&self) -> IconName {
- IconName::ToolBulb
+ IconName::ToolThink
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
@@ -261,7 +261,6 @@ pub enum IconName {
TodoComplete,
TodoPending,
TodoProgress,
- ToolBulb,
ToolCopy,
ToolDeleteFile,
ToolDiagnostics,
@@ -273,6 +272,7 @@ pub enum IconName {
ToolRegex,
ToolSearch,
ToolTerminal,
+ ToolThink,
ToolWeb,
Trash,
Triangle,
@@ -95,7 +95,7 @@ impl RenderOnce for Disclosure {
impl Component for Disclosure {
fn scope() -> ComponentScope {
- ComponentScope::Navigation
+ ComponentScope::Input
}
fn description() -> Option<&'static str> {