thread view: Refine tool call UI (#36937)

Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
This commit is contained in:
Danilo Leal 2025-08-26 12:55:40 -03:00 committed by GitHub
parent 858ab9cc23
commit 65c6c709fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 315 additions and 212 deletions

View file

@ -6,7 +6,7 @@ use agent2::HistoryStore;
use collections::HashMap; use collections::HashMap;
use editor::{Editor, EditorMode, MinimapVisibility}; use editor::{Editor, EditorMode, MinimapVisibility};
use gpui::{ use gpui::{
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, ScrollHandle,
TextStyleRefinement, WeakEntity, Window, TextStyleRefinement, WeakEntity, Window,
}; };
use language::language_settings::SoftWrap; use language::language_settings::SoftWrap;
@ -154,10 +154,22 @@ impl EntryViewState {
}); });
} }
} }
AgentThreadEntry::AssistantMessage(_) => { AgentThreadEntry::AssistantMessage(message) => {
if index == self.entries.len() { let entry = if let Some(Entry::AssistantMessage(entry)) =
self.entries.push(Entry::empty()) self.entries.get_mut(index)
} {
entry
} else {
self.set_entry(
index,
Entry::AssistantMessage(AssistantMessageEntry::default()),
);
let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else {
unreachable!()
};
entry
};
entry.sync(message);
} }
}; };
} }
@ -177,7 +189,7 @@ impl EntryViewState {
pub fn settings_changed(&mut self, cx: &mut App) { pub fn settings_changed(&mut self, cx: &mut App) {
for entry in self.entries.iter() { for entry in self.entries.iter() {
match entry { match entry {
Entry::UserMessage { .. } => {} Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
Entry::Content(response_views) => { Entry::Content(response_views) => {
for view in response_views.values() { for view in response_views.values() {
if let Ok(diff_editor) = view.clone().downcast::<Editor>() { if let Ok(diff_editor) = view.clone().downcast::<Editor>() {
@ -208,9 +220,29 @@ pub enum ViewEvent {
MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent), MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent),
} }
#[derive(Default, Debug)]
pub struct AssistantMessageEntry {
scroll_handles_by_chunk_index: HashMap<usize, ScrollHandle>,
}
impl AssistantMessageEntry {
pub fn scroll_handle_for_chunk(&self, ix: usize) -> Option<ScrollHandle> {
self.scroll_handles_by_chunk_index.get(&ix).cloned()
}
pub fn sync(&mut self, message: &acp_thread::AssistantMessage) {
if let Some(acp_thread::AssistantMessageChunk::Thought { .. }) = message.chunks.last() {
let ix = message.chunks.len() - 1;
let handle = self.scroll_handles_by_chunk_index.entry(ix).or_default();
handle.scroll_to_bottom();
}
}
}
#[derive(Debug)] #[derive(Debug)]
pub enum Entry { pub enum Entry {
UserMessage(Entity<MessageEditor>), UserMessage(Entity<MessageEditor>),
AssistantMessage(AssistantMessageEntry),
Content(HashMap<EntityId, AnyEntity>), Content(HashMap<EntityId, AnyEntity>),
} }
@ -218,7 +250,7 @@ impl Entry {
pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> { pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> {
match self { match self {
Self::UserMessage(editor) => Some(editor), Self::UserMessage(editor) => Some(editor),
Entry::Content(_) => None, Self::AssistantMessage(_) | Self::Content(_) => None,
} }
} }
@ -239,6 +271,16 @@ impl Entry {
.map(|entity| entity.downcast::<TerminalView>().unwrap()) .map(|entity| entity.downcast::<TerminalView>().unwrap())
} }
pub fn scroll_handle_for_assistant_message_chunk(
&self,
chunk_ix: usize,
) -> Option<ScrollHandle> {
match self {
Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix),
Self::UserMessage(_) | Self::Content(_) => None,
}
}
fn content_map(&self) -> Option<&HashMap<EntityId, AnyEntity>> { fn content_map(&self) -> Option<&HashMap<EntityId, AnyEntity>> {
match self { match self {
Self::Content(map) => Some(map), Self::Content(map) => Some(map),
@ -254,7 +296,7 @@ impl Entry {
pub fn has_content(&self) -> bool { pub fn has_content(&self) -> bool {
match self { match self {
Self::Content(map) => !map.is_empty(), Self::Content(map) => !map.is_empty(),
Self::UserMessage(_) => false, Self::UserMessage(_) | Self::AssistantMessage(_) => false,
} }
} }
} }

View file

@ -20,11 +20,11 @@ use file_icons::FileIcons;
use fs::Fs; use fs::Fs;
use gpui::{ use gpui::{
Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem, Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem,
EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length,
ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement,
Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, point, Window, WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage,
prelude::*, pulsating_between, point, prelude::*, pulsating_between,
}; };
use language::Buffer; use language::Buffer;
@ -66,7 +66,6 @@ use crate::{
KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector, KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector,
}; };
const RESPONSE_PADDING_X: Pixels = px(19.);
pub const MIN_EDITOR_LINES: usize = 4; pub const MIN_EDITOR_LINES: usize = 4;
pub const MAX_EDITOR_LINES: usize = 8; pub const MAX_EDITOR_LINES: usize = 8;
@ -1334,6 +1333,10 @@ impl AcpThreadView {
window: &mut Window, window: &mut Window,
cx: &Context<Self>, cx: &Context<Self>,
) -> AnyElement { ) -> AnyElement {
let is_generating = self
.thread()
.is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle);
let primary = match &entry { let primary = match &entry {
AgentThreadEntry::UserMessage(message) => { AgentThreadEntry::UserMessage(message) => {
let Some(editor) = self let Some(editor) = self
@ -1493,6 +1496,20 @@ impl AcpThreadView {
.into_any() .into_any()
} }
AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => { AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
let is_last = entry_ix + 1 == total_entries;
let pending_thinking_chunk_ix = if is_generating && is_last {
chunks
.iter()
.enumerate()
.next_back()
.filter(|(_, segment)| {
matches!(segment, AssistantMessageChunk::Thought { .. })
})
.map(|(index, _)| index)
} else {
None
};
let style = default_markdown_style(false, false, window, cx); let style = default_markdown_style(false, false, window, cx);
let message_body = v_flex() let message_body = v_flex()
.w_full() .w_full()
@ -1511,6 +1528,7 @@ impl AcpThreadView {
entry_ix, entry_ix,
chunk_ix, chunk_ix,
md.clone(), md.clone(),
Some(chunk_ix) == pending_thinking_chunk_ix,
window, window,
cx, cx,
) )
@ -1524,7 +1542,7 @@ impl AcpThreadView {
v_flex() v_flex()
.px_5() .px_5()
.py_1() .py_1()
.when(entry_ix + 1 == total_entries, |this| this.pb_4()) .when(is_last, |this| this.pb_4())
.w_full() .w_full()
.text_ui(cx) .text_ui(cx)
.child(message_body) .child(message_body)
@ -1533,7 +1551,7 @@ impl AcpThreadView {
AgentThreadEntry::ToolCall(tool_call) => { AgentThreadEntry::ToolCall(tool_call) => {
let has_terminals = tool_call.terminals().next().is_some(); let has_terminals = tool_call.terminals().next().is_some();
div().w_full().py_1().px_5().map(|this| { div().w_full().map(|this| {
if has_terminals { if has_terminals {
this.children(tool_call.terminals().map(|terminal| { this.children(tool_call.terminals().map(|terminal| {
self.render_terminal_tool_call( self.render_terminal_tool_call(
@ -1609,64 +1627,90 @@ impl AcpThreadView {
entry_ix: usize, entry_ix: usize,
chunk_ix: usize, chunk_ix: usize,
chunk: Entity<Markdown>, chunk: Entity<Markdown>,
pending: bool,
window: &Window, window: &Window,
cx: &Context<Self>, cx: &Context<Self>,
) -> AnyElement { ) -> AnyElement {
let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix)); let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
let card_header_id = SharedString::from("inner-card-header"); let card_header_id = SharedString::from("inner-card-header");
let key = (entry_ix, chunk_ix); let key = (entry_ix, chunk_ix);
let is_open = self.expanded_thinking_blocks.contains(&key); let is_open = self.expanded_thinking_blocks.contains(&key);
let editor_bg = cx.theme().colors().editor_background;
let gradient_overlay = div()
.rounded_b_lg()
.h_full()
.absolute()
.w_full()
.bottom_0()
.left_0()
.bg(linear_gradient(
180.,
linear_color_stop(editor_bg, 1.),
linear_color_stop(editor_bg.opacity(0.2), 0.),
));
let scroll_handle = self
.entry_view_state
.read(cx)
.entry(entry_ix)
.and_then(|entry| entry.scroll_handle_for_assistant_message_chunk(chunk_ix));
v_flex() v_flex()
.rounded_md()
.border_1()
.border_color(self.tool_card_border_color(cx))
.child( .child(
h_flex() h_flex()
.id(header_id) .id(header_id)
.group(&card_header_id) .group(&card_header_id)
.relative() .relative()
.w_full() .w_full()
.gap_1p5() .py_0p5()
.px_1p5()
.rounded_t_md()
.bg(self.tool_card_header_bg(cx))
.justify_between()
.border_b_1()
.border_color(self.tool_card_border_color(cx))
.child( .child(
h_flex() h_flex()
.size_4() .h(window.line_height())
.justify_center() .gap_1p5()
.child(
div()
.group_hover(&card_header_id, |s| s.invisible().w_0())
.child( .child(
Icon::new(IconName::ToolThink) Icon::new(IconName::ToolThink)
.size(IconSize::Small) .size(IconSize::Small)
.color(Color::Muted), .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( .child(
div() div()
.text_size(self.tool_name_font_size()) .text_size(self.tool_name_font_size())
.text_color(cx.theme().colors().text_muted) .text_color(cx.theme().colors().text_muted)
.child("Thinking"), .map(|this| {
if pending {
this.child("Thinking")
} else {
this.child("Thought Process")
}
}),
),
)
.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({
move |this, _event, _window, cx| {
if is_open {
this.expanded_thinking_blocks.remove(&key);
} else {
this.expanded_thinking_blocks.insert(key);
}
cx.notify();
}
})),
) )
.on_click(cx.listener({ .on_click(cx.listener({
move |this, _event, _window, cx| { move |this, _event, _window, cx| {
@ -1679,22 +1723,28 @@ impl AcpThreadView {
} }
})), })),
) )
.when(is_open, |this| { .child(
this.child(
div() div()
.relative() .relative()
.mt_1p5() .bg(editor_bg)
.ml(rems(0.4)) .rounded_b_lg()
.pl_4() .child(
.border_l_1() div()
.border_color(self.tool_card_border_color(cx)) .id(("thinking-content", chunk_ix))
.when_some(scroll_handle, |this, scroll_handle| {
this.track_scroll(&scroll_handle)
})
.p_2()
.when(!is_open, |this| this.max_h_20())
.text_ui_sm(cx) .text_ui_sm(cx)
.overflow_hidden()
.child(self.render_markdown( .child(self.render_markdown(
chunk, chunk,
default_markdown_style(false, false, window, cx), default_markdown_style(false, false, window, cx),
)), )),
) )
}) .when(!is_open && pending, |this| this.child(gradient_overlay)),
)
.into_any_element() .into_any_element()
} }
@ -1705,7 +1755,6 @@ impl AcpThreadView {
window: &Window, window: &Window,
cx: &Context<Self>, cx: &Context<Self>,
) -> Div { ) -> Div {
let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix));
let card_header_id = SharedString::from("inner-tool-call-header"); let card_header_id = SharedString::from("inner-tool-call-header");
let tool_icon = let tool_icon =
@ -1734,11 +1783,7 @@ impl AcpThreadView {
_ => false, _ => false,
}; };
let failed_tool_call = matches!( let has_location = tool_call.locations.len() == 1;
tool_call.status,
ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed
);
let needs_confirmation = matches!( let needs_confirmation = matches!(
tool_call.status, tool_call.status,
ToolCallStatus::WaitingForConfirmation { .. } ToolCallStatus::WaitingForConfirmation { .. }
@ -1751,23 +1796,31 @@ impl AcpThreadView {
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id); let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
let gradient_overlay = |color: Hsla| { let gradient_overlay = {
div() div()
.absolute() .absolute()
.top_0() .top_0()
.right_0() .right_0()
.w_12() .w_12()
.h_full() .h_full()
.bg(linear_gradient( .map(|this| {
if use_card_layout {
this.bg(linear_gradient(
90., 90.,
linear_color_stop(color, 1.), linear_color_stop(self.tool_card_header_bg(cx), 1.),
linear_color_stop(color.opacity(0.2), 0.), linear_color_stop(self.tool_card_header_bg(cx).opacity(0.2), 0.),
)) ))
};
let gradient_color = if use_card_layout {
self.tool_card_header_bg(cx)
} else { } else {
cx.theme().colors().panel_background 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 { let tool_output_display = if is_open {
@ -1818,41 +1871,58 @@ impl AcpThreadView {
}; };
v_flex() v_flex()
.when(use_card_layout, |this| { .map(|this| {
this.rounded_md() if use_card_layout {
this.my_2()
.rounded_md()
.border_1() .border_1()
.border_color(self.tool_card_border_color(cx)) .border_color(self.tool_card_border_color(cx))
.bg(cx.theme().colors().editor_background) .bg(cx.theme().colors().editor_background)
.overflow_hidden() .overflow_hidden()
} else {
this.my_1()
}
}) })
.map(|this| {
if has_location && !use_card_layout {
this.ml_4()
} else {
this.ml_5()
}
})
.mr_5()
.child( .child(
h_flex() h_flex()
.id(header_id)
.group(&card_header_id) .group(&card_header_id)
.relative() .relative()
.w_full() .w_full()
.max_w_full()
.gap_1() .gap_1()
.justify_between()
.when(use_card_layout, |this| { .when(use_card_layout, |this| {
this.pl_1p5() this.p_0p5()
.pr_1()
.py_0p5()
.rounded_t_md() .rounded_t_md()
.when(is_open && !failed_tool_call, |this| { .bg(self.tool_card_header_bg(cx))
.when(is_open && !failed_or_canceled, |this| {
this.border_b_1() this.border_b_1()
.border_color(self.tool_card_border_color(cx)) .border_color(self.tool_card_border_color(cx))
}) })
.bg(self.tool_card_header_bg(cx))
}) })
.child( .child(
h_flex() h_flex()
.relative() .relative()
.w_full() .w_full()
.h(window.line_height() - px(2.)) .h(window.line_height())
.text_size(self.tool_name_font_size()) .text_size(self.tool_name_font_size())
.gap_0p5() .gap_1p5()
.when(has_location || use_card_layout, |this| this.px_1())
.when(has_location, |this| {
this.cursor(CursorStyle::PointingHand)
.rounded_sm()
.hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.5)))
})
.overflow_hidden()
.child(tool_icon) .child(tool_icon)
.child(if tool_call.locations.len() == 1 { .child(if has_location {
let name = tool_call.locations[0] let name = tool_call.locations[0]
.path .path
.file_name() .file_name()
@ -1863,13 +1933,6 @@ impl AcpThreadView {
h_flex() h_flex()
.id(("open-tool-call-location", entry_ix)) .id(("open-tool-call-location", entry_ix))
.w_full() .w_full()
.max_w_full()
.px_1p5()
.rounded_sm()
.overflow_x_scroll()
.hover(|label| {
label.bg(cx.theme().colors().element_hover.opacity(0.5))
})
.map(|this| { .map(|this| {
if use_card_layout { if use_card_layout {
this.text_color(cx.theme().colors().text) this.text_color(cx.theme().colors().text)
@ -1879,28 +1942,25 @@ impl AcpThreadView {
}) })
.child(name) .child(name)
.tooltip(Tooltip::text("Jump to File")) .tooltip(Tooltip::text("Jump to File"))
.cursor(gpui::CursorStyle::PointingHand)
.on_click(cx.listener(move |this, _, window, cx| { .on_click(cx.listener(move |this, _, window, cx| {
this.open_tool_call_location(entry_ix, 0, window, cx); this.open_tool_call_location(entry_ix, 0, window, cx);
})) }))
.into_any_element() .into_any_element()
} else { } else {
h_flex() h_flex()
.relative()
.w_full() .w_full()
.max_w_full() .child(self.render_markdown(
.ml_1p5()
.overflow_hidden()
.child(h_flex().pr_8().child(self.render_markdown(
tool_call.label.clone(), tool_call.label.clone(),
default_markdown_style(false, true, window, cx), default_markdown_style(false, true, window, cx),
))) ))
.child(gradient_overlay(gradient_color))
.into_any() .into_any()
}), })
.when(!has_location, |this| this.child(gradient_overlay)),
) )
.child( .when(is_collapsible || failed_or_canceled, |this| {
this.child(
h_flex() h_flex()
.px_1()
.gap_px() .gap_px()
.when(is_collapsible, |this| { .when(is_collapsible, |this| {
this.child( this.child(
@ -1928,7 +1988,8 @@ impl AcpThreadView {
.size(IconSize::Small), .size(IconSize::Small),
) )
}), }),
), )
}),
) )
.children(tool_output_display) .children(tool_output_display)
} }
@ -2214,6 +2275,12 @@ impl AcpThreadView {
started_at.elapsed() started_at.elapsed()
}; };
let header_id =
SharedString::from(format!("terminal-tool-header-{}", terminal.entity_id()));
let header_group = SharedString::from(format!(
"terminal-tool-header-group-{}",
terminal.entity_id()
));
let header_bg = cx let header_bg = cx
.theme() .theme()
.colors() .colors()
@ -2229,10 +2296,7 @@ impl AcpThreadView {
let is_expanded = self.expanded_tool_calls.contains(&tool_call.id); let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
let header = h_flex() let header = h_flex()
.id(SharedString::from(format!( .id(header_id)
"terminal-tool-header-{}",
terminal.entity_id()
)))
.flex_none() .flex_none()
.gap_1() .gap_1()
.justify_between() .justify_between()
@ -2296,23 +2360,6 @@ impl AcpThreadView {
), ),
) )
}) })
.when(tool_failed || command_failed, |header| {
header.child(
div()
.id(("terminal-tool-error-code-indicator", terminal.entity_id()))
.child(
Icon::new(IconName::Close)
.size(IconSize::Small)
.color(Color::Error),
)
.when_some(output.and_then(|o| o.exit_status), |this, status| {
this.tooltip(Tooltip::text(format!(
"Exited with code {}",
status.code().unwrap_or(-1),
)))
}),
)
})
.when(truncated_output, |header| { .when(truncated_output, |header| {
let tooltip = if let Some(output) = output { let tooltip = if let Some(output) = output {
if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES { if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
@ -2365,6 +2412,7 @@ impl AcpThreadView {
) )
.opened_icon(IconName::ChevronUp) .opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown) .closed_icon(IconName::ChevronDown)
.visible_on_hover(&header_group)
.on_click(cx.listener({ .on_click(cx.listener({
let id = tool_call.id.clone(); let id = tool_call.id.clone();
move |this, _event, _window, _cx| { move |this, _event, _window, _cx| {
@ -2373,8 +2421,26 @@ impl AcpThreadView {
} else { } else {
this.expanded_tool_calls.insert(id.clone()); this.expanded_tool_calls.insert(id.clone());
} }
}})), }
); })),
)
.when(tool_failed || command_failed, |header| {
header.child(
div()
.id(("terminal-tool-error-code-indicator", terminal.entity_id()))
.child(
Icon::new(IconName::Close)
.size(IconSize::Small)
.color(Color::Error),
)
.when_some(output.and_then(|o| o.exit_status), |this, status| {
this.tooltip(Tooltip::text(format!(
"Exited with code {}",
status.code().unwrap_or(-1),
)))
}),
)
});
let terminal_view = self let terminal_view = self
.entry_view_state .entry_view_state
@ -2384,7 +2450,8 @@ impl AcpThreadView {
let show_output = is_expanded && terminal_view.is_some(); let show_output = is_expanded && terminal_view.is_some();
v_flex() v_flex()
.mb_2() .my_2()
.mx_5()
.border_1() .border_1()
.when(tool_failed || command_failed, |card| card.border_dashed()) .when(tool_failed || command_failed, |card| card.border_dashed())
.border_color(border_color) .border_color(border_color)
@ -2392,9 +2459,10 @@ impl AcpThreadView {
.overflow_hidden() .overflow_hidden()
.child( .child(
v_flex() v_flex()
.group(&header_group)
.py_1p5() .py_1p5()
.pl_2()
.pr_1p5() .pr_1p5()
.pl_2()
.gap_0p5() .gap_0p5()
.bg(header_bg) .bg(header_bg)
.text_xs() .text_xs()
@ -4153,13 +4221,14 @@ impl AcpThreadView {
) -> impl IntoElement { ) -> impl IntoElement {
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating); let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
if is_generating { if is_generating {
return h_flex().id("thread-controls-container").ml_1().child( return h_flex().id("thread-controls-container").child(
div() div()
.py_2() .py_2()
.px(rems_from_px(22.)) .px_5()
.child(SpinnerLabel::new().size(LabelSize::Small)), .child(SpinnerLabel::new().size(LabelSize::Small)),
); );
} }
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown) let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
.shape(ui::IconButtonShape::Square) .shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
@ -4185,12 +4254,10 @@ impl AcpThreadView {
.id("thread-controls-container") .id("thread-controls-container")
.group("thread-controls-container") .group("thread-controls-container")
.w_full() .w_full()
.mr_1() .py_2()
.pt_1() .px_5()
.pb_2()
.px(RESPONSE_PADDING_X)
.gap_px() .gap_px()
.opacity(0.4) .opacity(0.6)
.hover(|style| style.opacity(1.)) .hover(|style| style.opacity(1.))
.flex_wrap() .flex_wrap()
.justify_end(); .justify_end();
@ -4201,21 +4268,24 @@ impl AcpThreadView {
.is_some_and(|thread| thread.read(cx).connection().telemetry().is_some()) .is_some_and(|thread| thread.read(cx).connection().telemetry().is_some())
{ {
let feedback = self.thread_feedback.feedback; let feedback = self.thread_feedback.feedback;
container = container.child(
container = container
.child(
div().visible_on_hover("thread-controls-container").child( div().visible_on_hover("thread-controls-container").child(
Label::new( Label::new(match feedback {
match feedback {
Some(ThreadFeedback::Positive) => "Thanks for your feedback!", Some(ThreadFeedback::Positive) => "Thanks for your feedback!",
Some(ThreadFeedback::Negative) => "We appreciate your feedback and will use it to improve.", Some(ThreadFeedback::Negative) => {
None => "Rating the thread sends all of your current conversation to the Zed team.", "We appreciate your feedback and will use it to improve."
} }
) None => {
"Rating the thread sends all of your current conversation to the Zed team."
}
})
.color(Color::Muted) .color(Color::Muted)
.size(LabelSize::XSmall) .size(LabelSize::XSmall)
.truncate(), .truncate(),
), ),
).child( )
h_flex()
.child( .child(
IconButton::new("feedback-thumbs-up", IconName::ThumbsUp) IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
.shape(ui::IconButtonShape::Square) .shape(ui::IconButtonShape::Square)
@ -4226,11 +4296,7 @@ impl AcpThreadView {
}) })
.tooltip(Tooltip::text("Helpful Response")) .tooltip(Tooltip::text("Helpful Response"))
.on_click(cx.listener(move |this, _, window, cx| { .on_click(cx.listener(move |this, _, window, cx| {
this.handle_feedback_click( this.handle_feedback_click(ThreadFeedback::Positive, window, cx);
ThreadFeedback::Positive,
window,
cx,
);
})), })),
) )
.child( .child(
@ -4243,14 +4309,9 @@ impl AcpThreadView {
}) })
.tooltip(Tooltip::text("Not Helpful")) .tooltip(Tooltip::text("Not Helpful"))
.on_click(cx.listener(move |this, _, window, cx| { .on_click(cx.listener(move |this, _, window, cx| {
this.handle_feedback_click( this.handle_feedback_click(ThreadFeedback::Negative, window, cx);
ThreadFeedback::Negative,
window,
cx,
);
})), })),
) );
)
} }
container.child(open_as_markdown).child(scroll_to_top) container.child(open_as_markdown).child(scroll_to_top)

View file

@ -1323,7 +1323,7 @@ fn render_copy_code_block_button(
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.style(ButtonStyle::Filled) .style(ButtonStyle::Filled)
.shape(ui::IconButtonShape::Square) .shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text("Copy Code")) .tooltip(Tooltip::text("Copy"))
.on_click({ .on_click({
let markdown = markdown; let markdown = markdown;
move |_event, _window, cx| { move |_event, _window, cx| {