thread view: Add UI refinements (#35754)
More notably around how we render tool calls. Nothing too drastic, though. Release Notes: - N/A
This commit is contained in:
parent
58392b9c13
commit
8e290b446e
8 changed files with 188 additions and 103 deletions
Before Width: | Height: | Size: 776 B After Width: | Height: | Size: 776 B |
|
@ -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()
|
||||
.child(
|
||||
Icon::new(IconName::ToolBulb)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.size_4()
|
||||
.justify_center()
|
||||
.child(
|
||||
div()
|
||||
.text_size(self.tool_name_font_size())
|
||||
.child("Thinking"),
|
||||
.group_hover(&card_header_id, |s| s.invisible().w_0())
|
||||
.child(
|
||||
Icon::new(IconName::ToolThink)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
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()
|
||||
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());
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}))
|
||||
.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();
|
||||
}
|
||||
})),
|
||||
)
|
||||
})
|
||||
.children(status_icon),
|
||||
)
|
||||
.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> {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue