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
1f35c62577
commit
ca70f091c2
3 changed files with 315 additions and 212 deletions
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
.child(
|
||||||
div()
|
Icon::new(IconName::ToolThink)
|
||||||
.group_hover(&card_header_id, |s| s.invisible().w_0())
|
.size(IconSize::Small)
|
||||||
.child(
|
.color(Color::Muted),
|
||||||
Icon::new(IconName::ToolThink)
|
|
||||||
.size(IconSize::Small)
|
|
||||||
.color(Color::Muted),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
div()
|
||||||
.absolute()
|
.text_size(self.tool_name_font_size())
|
||||||
.inset_0()
|
.text_color(cx.theme().colors().text_muted)
|
||||||
.invisible()
|
.map(|this| {
|
||||||
.justify_center()
|
if pending {
|
||||||
.group_hover(&card_header_id, |s| s.visible())
|
this.child("Thinking")
|
||||||
.child(
|
} else {
|
||||||
Disclosure::new(("expand", entry_ix), is_open)
|
this.child("Thought Process")
|
||||||
.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()
|
Disclosure::new(("expand", entry_ix), is_open)
|
||||||
.text_size(self.tool_name_font_size())
|
.opened_icon(IconName::ChevronUp)
|
||||||
.text_color(cx.theme().colors().text_muted)
|
.closed_icon(IconName::ChevronDown)
|
||||||
.child("Thinking"),
|
.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()
|
.bg(editor_bg)
|
||||||
.mt_1p5()
|
.rounded_b_lg()
|
||||||
.ml(rems(0.4))
|
.child(
|
||||||
.pl_4()
|
div()
|
||||||
.border_l_1()
|
.id(("thinking-content", chunk_ix))
|
||||||
.border_color(self.tool_card_border_color(cx))
|
.when_some(scroll_handle, |this, scroll_handle| {
|
||||||
.text_ui_sm(cx)
|
this.track_scroll(&scroll_handle)
|
||||||
.child(self.render_markdown(
|
})
|
||||||
chunk,
|
.p_2()
|
||||||
default_markdown_style(false, false, window, cx),
|
.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()
|
.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| {
|
||||||
90.,
|
if use_card_layout {
|
||||||
linear_color_stop(color, 1.),
|
this.bg(linear_gradient(
|
||||||
linear_color_stop(color.opacity(0.2), 0.),
|
90.,
|
||||||
))
|
linear_color_stop(self.tool_card_header_bg(cx), 1.),
|
||||||
};
|
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 {
|
this.bg(linear_gradient(
|
||||||
cx.theme().colors().panel_background
|
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 {
|
||||||
.border_1()
|
this.my_2()
|
||||||
.border_color(self.tool_card_border_color(cx))
|
.rounded_md()
|
||||||
.bg(cx.theme().colors().editor_background)
|
.border_1()
|
||||||
.overflow_hidden()
|
.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(
|
.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,31 +1942,28 @@ 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| {
|
||||||
h_flex()
|
this.child(
|
||||||
.gap_px()
|
h_flex()
|
||||||
.when(is_collapsible, |this| {
|
.px_1()
|
||||||
this.child(
|
.gap_px()
|
||||||
|
.when(is_collapsible, |this| {
|
||||||
|
this.child(
|
||||||
Disclosure::new(("expand", entry_ix), is_open)
|
Disclosure::new(("expand", entry_ix), is_open)
|
||||||
.opened_icon(IconName::ChevronUp)
|
.opened_icon(IconName::ChevronUp)
|
||||||
.closed_icon(IconName::ChevronDown)
|
.closed_icon(IconName::ChevronDown)
|
||||||
|
@ -1920,15 +1980,16 @@ impl AcpThreadView {
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(failed_or_canceled, |this| {
|
.when(failed_or_canceled, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
Icon::new(IconName::Close)
|
Icon::new(IconName::Close)
|
||||||
.color(Color::Error)
|
.color(Color::Error)
|
||||||
.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,56 +4268,50 @@ 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(
|
|
||||||
div().visible_on_hover("thread-controls-container").child(
|
container = container
|
||||||
Label::new(
|
.child(
|
||||||
match feedback {
|
div().visible_on_hover("thread-controls-container").child(
|
||||||
|
Label::new(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 => {
|
||||||
.color(Color::Muted)
|
"Rating the thread sends all of your current conversation to the Zed team."
|
||||||
.size(LabelSize::XSmall)
|
}
|
||||||
.truncate(),
|
})
|
||||||
),
|
.color(Color::Muted)
|
||||||
).child(
|
.size(LabelSize::XSmall)
|
||||||
h_flex()
|
.truncate(),
|
||||||
.child(
|
),
|
||||||
IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
|
)
|
||||||
.shape(ui::IconButtonShape::Square)
|
.child(
|
||||||
.icon_size(IconSize::Small)
|
IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
|
||||||
.icon_color(match feedback {
|
.shape(ui::IconButtonShape::Square)
|
||||||
Some(ThreadFeedback::Positive) => Color::Accent,
|
.icon_size(IconSize::Small)
|
||||||
_ => Color::Ignored,
|
.icon_color(match feedback {
|
||||||
})
|
Some(ThreadFeedback::Positive) => Color::Accent,
|
||||||
.tooltip(Tooltip::text("Helpful Response"))
|
_ => Color::Ignored,
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
})
|
||||||
this.handle_feedback_click(
|
.tooltip(Tooltip::text("Helpful Response"))
|
||||||
ThreadFeedback::Positive,
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
window,
|
this.handle_feedback_click(ThreadFeedback::Positive, window, cx);
|
||||||
cx,
|
})),
|
||||||
);
|
)
|
||||||
})),
|
.child(
|
||||||
)
|
IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
|
||||||
.child(
|
.shape(ui::IconButtonShape::Square)
|
||||||
IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
|
.icon_size(IconSize::Small)
|
||||||
.shape(ui::IconButtonShape::Square)
|
.icon_color(match feedback {
|
||||||
.icon_size(IconSize::Small)
|
Some(ThreadFeedback::Negative) => Color::Accent,
|
||||||
.icon_color(match feedback {
|
_ => Color::Ignored,
|
||||||
Some(ThreadFeedback::Negative) => Color::Accent,
|
})
|
||||||
_ => Color::Ignored,
|
.tooltip(Tooltip::text("Not Helpful"))
|
||||||
})
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
.tooltip(Tooltip::text("Not Helpful"))
|
this.handle_feedback_click(ThreadFeedback::Negative, window, cx);
|
||||||
.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)
|
container.child(open_as_markdown).child(scroll_to_top)
|
||||||
|
|
|
@ -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| {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue