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:
parent
858ab9cc23
commit
65c6c709fd
3 changed files with 315 additions and 212 deletions
|
@ -6,7 +6,7 @@ use agent2::HistoryStore;
|
|||
use collections::HashMap;
|
||||
use editor::{Editor, EditorMode, MinimapVisibility};
|
||||
use gpui::{
|
||||
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable,
|
||||
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, ScrollHandle,
|
||||
TextStyleRefinement, WeakEntity, Window,
|
||||
};
|
||||
use language::language_settings::SoftWrap;
|
||||
|
@ -154,10 +154,22 @@ impl EntryViewState {
|
|||
});
|
||||
}
|
||||
}
|
||||
AgentThreadEntry::AssistantMessage(_) => {
|
||||
if index == self.entries.len() {
|
||||
self.entries.push(Entry::empty())
|
||||
}
|
||||
AgentThreadEntry::AssistantMessage(message) => {
|
||||
let entry = if let Some(Entry::AssistantMessage(entry)) =
|
||||
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) {
|
||||
for entry in self.entries.iter() {
|
||||
match entry {
|
||||
Entry::UserMessage { .. } => {}
|
||||
Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
|
||||
Entry::Content(response_views) => {
|
||||
for view in response_views.values() {
|
||||
if let Ok(diff_editor) = view.clone().downcast::<Editor>() {
|
||||
|
@ -208,9 +220,29 @@ pub enum ViewEvent {
|
|||
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)]
|
||||
pub enum Entry {
|
||||
UserMessage(Entity<MessageEditor>),
|
||||
AssistantMessage(AssistantMessageEntry),
|
||||
Content(HashMap<EntityId, AnyEntity>),
|
||||
}
|
||||
|
||||
|
@ -218,7 +250,7 @@ impl Entry {
|
|||
pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> {
|
||||
match self {
|
||||
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())
|
||||
}
|
||||
|
||||
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>> {
|
||||
match self {
|
||||
Self::Content(map) => Some(map),
|
||||
|
@ -254,7 +296,7 @@ impl Entry {
|
|||
pub fn has_content(&self) -> bool {
|
||||
match self {
|
||||
Self::Content(map) => !map.is_empty(),
|
||||
Self::UserMessage(_) => false,
|
||||
Self::UserMessage(_) | Self::AssistantMessage(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,11 +20,11 @@ use file_icons::FileIcons;
|
|||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem,
|
||||
EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset,
|
||||
ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription,
|
||||
Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window,
|
||||
WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, point,
|
||||
prelude::*, pulsating_between,
|
||||
CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length,
|
||||
ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement,
|
||||
Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
|
||||
Window, WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage,
|
||||
point, prelude::*, pulsating_between,
|
||||
};
|
||||
use language::Buffer;
|
||||
|
||||
|
@ -66,7 +66,6 @@ use crate::{
|
|||
KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector,
|
||||
};
|
||||
|
||||
const RESPONSE_PADDING_X: Pixels = px(19.);
|
||||
pub const MIN_EDITOR_LINES: usize = 4;
|
||||
pub const MAX_EDITOR_LINES: usize = 8;
|
||||
|
||||
|
@ -1334,6 +1333,10 @@ impl AcpThreadView {
|
|||
window: &mut Window,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let is_generating = self
|
||||
.thread()
|
||||
.is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle);
|
||||
|
||||
let primary = match &entry {
|
||||
AgentThreadEntry::UserMessage(message) => {
|
||||
let Some(editor) = self
|
||||
|
@ -1493,6 +1496,20 @@ impl AcpThreadView {
|
|||
.into_any()
|
||||
}
|
||||
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 message_body = v_flex()
|
||||
.w_full()
|
||||
|
@ -1511,6 +1528,7 @@ impl AcpThreadView {
|
|||
entry_ix,
|
||||
chunk_ix,
|
||||
md.clone(),
|
||||
Some(chunk_ix) == pending_thinking_chunk_ix,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
@ -1524,7 +1542,7 @@ impl AcpThreadView {
|
|||
v_flex()
|
||||
.px_5()
|
||||
.py_1()
|
||||
.when(entry_ix + 1 == total_entries, |this| this.pb_4())
|
||||
.when(is_last, |this| this.pb_4())
|
||||
.w_full()
|
||||
.text_ui(cx)
|
||||
.child(message_body)
|
||||
|
@ -1533,7 +1551,7 @@ impl AcpThreadView {
|
|||
AgentThreadEntry::ToolCall(tool_call) => {
|
||||
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 {
|
||||
this.children(tool_call.terminals().map(|terminal| {
|
||||
self.render_terminal_tool_call(
|
||||
|
@ -1609,64 +1627,90 @@ impl AcpThreadView {
|
|||
entry_ix: usize,
|
||||
chunk_ix: usize,
|
||||
chunk: Entity<Markdown>,
|
||||
pending: bool,
|
||||
window: &Window,
|
||||
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);
|
||||
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()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.id(header_id)
|
||||
.group(&card_header_id)
|
||||
.relative()
|
||||
.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(
|
||||
h_flex()
|
||||
.size_4()
|
||||
.justify_center()
|
||||
.h(window.line_height())
|
||||
.gap_1p5()
|
||||
.child(
|
||||
div()
|
||||
.group_hover(&card_header_id, |s| s.invisible().w_0())
|
||||
.child(
|
||||
Icon::new(IconName::ToolThink)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
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();
|
||||
}
|
||||
})),
|
||||
),
|
||||
div()
|
||||
.text_size(self.tool_name_font_size())
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.map(|this| {
|
||||
if pending {
|
||||
this.child("Thinking")
|
||||
} else {
|
||||
this.child("Thought Process")
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(self.tool_name_font_size())
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child("Thinking"),
|
||||
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({
|
||||
move |this, _event, _window, cx| {
|
||||
|
@ -1679,22 +1723,28 @@ impl AcpThreadView {
|
|||
}
|
||||
})),
|
||||
)
|
||||
.when(is_open, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.relative()
|
||||
.mt_1p5()
|
||||
.ml(rems(0.4))
|
||||
.pl_4()
|
||||
.border_l_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.text_ui_sm(cx)
|
||||
.child(self.render_markdown(
|
||||
chunk,
|
||||
default_markdown_style(false, false, window, cx),
|
||||
)),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.relative()
|
||||
.bg(editor_bg)
|
||||
.rounded_b_lg()
|
||||
.child(
|
||||
div()
|
||||
.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)
|
||||
.overflow_hidden()
|
||||
.child(self.render_markdown(
|
||||
chunk,
|
||||
default_markdown_style(false, false, window, cx),
|
||||
)),
|
||||
)
|
||||
.when(!is_open && pending, |this| this.child(gradient_overlay)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
|
@ -1705,7 +1755,6 @@ impl AcpThreadView {
|
|||
window: &Window,
|
||||
cx: &Context<Self>,
|
||||
) -> Div {
|
||||
let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix));
|
||||
let card_header_id = SharedString::from("inner-tool-call-header");
|
||||
|
||||
let tool_icon =
|
||||
|
@ -1734,11 +1783,7 @@ impl AcpThreadView {
|
|||
_ => false,
|
||||
};
|
||||
|
||||
let failed_tool_call = matches!(
|
||||
tool_call.status,
|
||||
ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed
|
||||
);
|
||||
|
||||
let has_location = tool_call.locations.len() == 1;
|
||||
let needs_confirmation = matches!(
|
||||
tool_call.status,
|
||||
ToolCallStatus::WaitingForConfirmation { .. }
|
||||
|
@ -1751,23 +1796,31 @@ impl AcpThreadView {
|
|||
|
||||
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
|
||||
|
||||
let gradient_overlay = |color: Hsla| {
|
||||
let gradient_overlay = {
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.right_0()
|
||||
.w_12()
|
||||
.h_full()
|
||||
.bg(linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(color, 1.),
|
||||
linear_color_stop(color.opacity(0.2), 0.),
|
||||
))
|
||||
};
|
||||
let gradient_color = if use_card_layout {
|
||||
self.tool_card_header_bg(cx)
|
||||
} else {
|
||||
cx.theme().colors().panel_background
|
||||
.map(|this| {
|
||||
if use_card_layout {
|
||||
this.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.),
|
||||
))
|
||||
} else {
|
||||
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 {
|
||||
|
@ -1818,41 +1871,58 @@ impl AcpThreadView {
|
|||
};
|
||||
|
||||
v_flex()
|
||||
.when(use_card_layout, |this| {
|
||||
this.rounded_md()
|
||||
.border_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.overflow_hidden()
|
||||
.map(|this| {
|
||||
if use_card_layout {
|
||||
this.my_2()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.overflow_hidden()
|
||||
} else {
|
||||
this.my_1()
|
||||
}
|
||||
})
|
||||
.map(|this| {
|
||||
if has_location && !use_card_layout {
|
||||
this.ml_4()
|
||||
} else {
|
||||
this.ml_5()
|
||||
}
|
||||
})
|
||||
.mr_5()
|
||||
.child(
|
||||
h_flex()
|
||||
.id(header_id)
|
||||
.group(&card_header_id)
|
||||
.relative()
|
||||
.w_full()
|
||||
.max_w_full()
|
||||
.gap_1()
|
||||
.justify_between()
|
||||
.when(use_card_layout, |this| {
|
||||
this.pl_1p5()
|
||||
.pr_1()
|
||||
.py_0p5()
|
||||
this.p_0p5()
|
||||
.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()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
})
|
||||
.bg(self.tool_card_header_bg(cx))
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.relative()
|
||||
.w_full()
|
||||
.h(window.line_height() - px(2.))
|
||||
.h(window.line_height())
|
||||
.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(if tool_call.locations.len() == 1 {
|
||||
.child(if has_location {
|
||||
let name = tool_call.locations[0]
|
||||
.path
|
||||
.file_name()
|
||||
|
@ -1863,13 +1933,6 @@ impl AcpThreadView {
|
|||
h_flex()
|
||||
.id(("open-tool-call-location", entry_ix))
|
||||
.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| {
|
||||
if use_card_layout {
|
||||
this.text_color(cx.theme().colors().text)
|
||||
|
@ -1879,31 +1942,28 @@ impl AcpThreadView {
|
|||
})
|
||||
.child(name)
|
||||
.tooltip(Tooltip::text("Jump to File"))
|
||||
.cursor(gpui::CursorStyle::PointingHand)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.open_tool_call_location(entry_ix, 0, window, cx);
|
||||
}))
|
||||
.into_any_element()
|
||||
} else {
|
||||
h_flex()
|
||||
.relative()
|
||||
.w_full()
|
||||
.max_w_full()
|
||||
.ml_1p5()
|
||||
.overflow_hidden()
|
||||
.child(h_flex().pr_8().child(self.render_markdown(
|
||||
.child(self.render_markdown(
|
||||
tool_call.label.clone(),
|
||||
default_markdown_style(false, true, window, cx),
|
||||
)))
|
||||
.child(gradient_overlay(gradient_color))
|
||||
))
|
||||
.into_any()
|
||||
}),
|
||||
})
|
||||
.when(!has_location, |this| this.child(gradient_overlay)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_px()
|
||||
.when(is_collapsible, |this| {
|
||||
this.child(
|
||||
.when(is_collapsible || failed_or_canceled, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.px_1()
|
||||
.gap_px()
|
||||
.when(is_collapsible, |this| {
|
||||
this.child(
|
||||
Disclosure::new(("expand", entry_ix), is_open)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
|
@ -1920,15 +1980,16 @@ impl AcpThreadView {
|
|||
}
|
||||
})),
|
||||
)
|
||||
})
|
||||
.when(failed_or_canceled, |this| {
|
||||
this.child(
|
||||
Icon::new(IconName::Close)
|
||||
.color(Color::Error)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
}),
|
||||
),
|
||||
})
|
||||
.when(failed_or_canceled, |this| {
|
||||
this.child(
|
||||
Icon::new(IconName::Close)
|
||||
.color(Color::Error)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.children(tool_output_display)
|
||||
}
|
||||
|
@ -2214,6 +2275,12 @@ impl AcpThreadView {
|
|||
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
|
||||
.theme()
|
||||
.colors()
|
||||
|
@ -2229,10 +2296,7 @@ impl AcpThreadView {
|
|||
let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
|
||||
|
||||
let header = h_flex()
|
||||
.id(SharedString::from(format!(
|
||||
"terminal-tool-header-{}",
|
||||
terminal.entity_id()
|
||||
)))
|
||||
.id(header_id)
|
||||
.flex_none()
|
||||
.gap_1()
|
||||
.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| {
|
||||
let tooltip = if let Some(output) = output {
|
||||
if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
|
||||
|
@ -2365,6 +2412,7 @@ impl AcpThreadView {
|
|||
)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.visible_on_hover(&header_group)
|
||||
.on_click(cx.listener({
|
||||
let id = tool_call.id.clone();
|
||||
move |this, _event, _window, _cx| {
|
||||
|
@ -2373,8 +2421,26 @@ impl AcpThreadView {
|
|||
} else {
|
||||
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
|
||||
.entry_view_state
|
||||
|
@ -2384,7 +2450,8 @@ impl AcpThreadView {
|
|||
let show_output = is_expanded && terminal_view.is_some();
|
||||
|
||||
v_flex()
|
||||
.mb_2()
|
||||
.my_2()
|
||||
.mx_5()
|
||||
.border_1()
|
||||
.when(tool_failed || command_failed, |card| card.border_dashed())
|
||||
.border_color(border_color)
|
||||
|
@ -2392,9 +2459,10 @@ impl AcpThreadView {
|
|||
.overflow_hidden()
|
||||
.child(
|
||||
v_flex()
|
||||
.group(&header_group)
|
||||
.py_1p5()
|
||||
.pl_2()
|
||||
.pr_1p5()
|
||||
.pl_2()
|
||||
.gap_0p5()
|
||||
.bg(header_bg)
|
||||
.text_xs()
|
||||
|
@ -4153,13 +4221,14 @@ impl AcpThreadView {
|
|||
) -> impl IntoElement {
|
||||
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
|
||||
if is_generating {
|
||||
return h_flex().id("thread-controls-container").ml_1().child(
|
||||
return h_flex().id("thread-controls-container").child(
|
||||
div()
|
||||
.py_2()
|
||||
.px(rems_from_px(22.))
|
||||
.px_5()
|
||||
.child(SpinnerLabel::new().size(LabelSize::Small)),
|
||||
);
|
||||
}
|
||||
|
||||
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
|
@ -4185,12 +4254,10 @@ impl AcpThreadView {
|
|||
.id("thread-controls-container")
|
||||
.group("thread-controls-container")
|
||||
.w_full()
|
||||
.mr_1()
|
||||
.pt_1()
|
||||
.pb_2()
|
||||
.px(RESPONSE_PADDING_X)
|
||||
.py_2()
|
||||
.px_5()
|
||||
.gap_px()
|
||||
.opacity(0.4)
|
||||
.opacity(0.6)
|
||||
.hover(|style| style.opacity(1.))
|
||||
.flex_wrap()
|
||||
.justify_end();
|
||||
|
@ -4201,56 +4268,50 @@ impl AcpThreadView {
|
|||
.is_some_and(|thread| thread.read(cx).connection().telemetry().is_some())
|
||||
{
|
||||
let feedback = self.thread_feedback.feedback;
|
||||
container = container.child(
|
||||
div().visible_on_hover("thread-controls-container").child(
|
||||
Label::new(
|
||||
match feedback {
|
||||
|
||||
container = container
|
||||
.child(
|
||||
div().visible_on_hover("thread-controls-container").child(
|
||||
Label::new(match feedback {
|
||||
Some(ThreadFeedback::Positive) => "Thanks for your feedback!",
|
||||
Some(ThreadFeedback::Negative) => "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)
|
||||
.size(LabelSize::XSmall)
|
||||
.truncate(),
|
||||
),
|
||||
).child(
|
||||
h_flex()
|
||||
.child(
|
||||
IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(match feedback {
|
||||
Some(ThreadFeedback::Positive) => Color::Accent,
|
||||
_ => Color::Ignored,
|
||||
})
|
||||
.tooltip(Tooltip::text("Helpful Response"))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.handle_feedback_click(
|
||||
ThreadFeedback::Positive,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(match feedback {
|
||||
Some(ThreadFeedback::Negative) => Color::Accent,
|
||||
_ => Color::Ignored,
|
||||
})
|
||||
.tooltip(Tooltip::text("Not Helpful"))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.handle_feedback_click(
|
||||
ThreadFeedback::Negative,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
)
|
||||
)
|
||||
Some(ThreadFeedback::Negative) => {
|
||||
"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)
|
||||
.size(LabelSize::XSmall)
|
||||
.truncate(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(match feedback {
|
||||
Some(ThreadFeedback::Positive) => Color::Accent,
|
||||
_ => Color::Ignored,
|
||||
})
|
||||
.tooltip(Tooltip::text("Helpful Response"))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.handle_feedback_click(ThreadFeedback::Positive, window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(match feedback {
|
||||
Some(ThreadFeedback::Negative) => Color::Accent,
|
||||
_ => Color::Ignored,
|
||||
})
|
||||
.tooltip(Tooltip::text("Not Helpful"))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.handle_feedback_click(ThreadFeedback::Negative, window, cx);
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
container.child(open_as_markdown).child(scroll_to_top)
|
||||
|
|
|
@ -1323,7 +1323,7 @@ fn render_copy_code_block_button(
|
|||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Filled)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::text("Copy Code"))
|
||||
.tooltip(Tooltip::text("Copy"))
|
||||
.on_click({
|
||||
let markdown = markdown;
|
||||
move |_event, _window, cx| {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue