diff --git a/assets/icons/menu_alt.svg b/assets/icons/menu_alt.svg index f73102e286..87add13216 100644 --- a/assets/icons/menu_alt.svg +++ b/assets/icons/menu_alt.svg @@ -1 +1,3 @@ - + + + diff --git a/assets/icons/menu_alt_temp.svg b/assets/icons/menu_alt_temp.svg new file mode 100644 index 0000000000..87add13216 --- /dev/null +++ b/assets/icons/menu_alt_temp.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/x_circle_filled.svg b/assets/icons/x_circle_filled.svg new file mode 100644 index 0000000000..52215acda8 --- /dev/null +++ b/assets/icons/x_circle_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/zed_agent.svg b/assets/icons/zed_agent.svg new file mode 100644 index 0000000000..b6e120a0b6 --- /dev/null +++ b/assets/icons/zed_agent.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index f8cf3dd602..74d24efb13 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -27,16 +27,15 @@ impl AgentServer for NativeAgentServer { } fn empty_state_headline(&self) -> &'static str { - "Native Agent" + "" } fn empty_state_message(&self) -> &'static str { - "How can I help you today?" + "" } fn logo(&self) -> ui::IconName { - // Using the ZedAssistant icon as it's the native built-in agent - ui::IconName::ZedAssistant + ui::IconName::ZedAgent } fn connect( diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index e1ecaf0bb5..813f8b1fe0 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -26,7 +26,7 @@ impl AgentServer for Gemini { } fn empty_state_message(&self) -> &'static str { - "Ask questions, edit files, run commands.\nBe specific for the best results." + "Ask questions, edit files, run commands" } fn logo(&self) -> ui::IconName { diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index 0b0b8471a7..98af9bf838 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -189,6 +189,7 @@ pub enum ViewEvent { MessageEditorEvent(Entity, MessageEditorEvent), } +#[derive(Debug)] pub enum Entry { UserMessage(Entity), Content(HashMap), diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 05f626d48e..4862bb0aa6 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -21,11 +21,11 @@ use file_icons::FileIcons; use fs::Fs; use gpui::{ Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem, - EdgesRefinement, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, - MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, Task, - TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, - WindowHandle, div, linear_color_stop, linear_gradient, list, percentage, point, prelude::*, - pulsating_between, + 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; @@ -170,7 +170,7 @@ impl AcpThreadView { project.clone(), thread_store.clone(), text_thread_store.clone(), - "Message the agent - @ to include context", + "Message the agent — @ to include context", prevent_slash_commands, editor::EditorMode::AutoHeight { min_lines: MIN_EDITOR_LINES, @@ -928,29 +928,41 @@ impl AcpThreadView { None }; - div() + v_flex() .id(("user_message", entry_ix)) - .py_4() + .pt_2() + .pb_4() .px_2() + .gap_1p5() + .w_full() + .children(rules_item) .children(message.id.clone().and_then(|message_id| { message.checkpoint.as_ref()?.show.then(|| { - Button::new("restore-checkpoint", "Restore Checkpoint") - .icon(IconName::Undo) - .icon_size(IconSize::XSmall) - .icon_position(IconPosition::Start) - .label_size(LabelSize::XSmall) - .on_click(cx.listener(move |this, _, _window, cx| { - this.rewind(&message_id, cx); - })) + h_flex() + .gap_2() + .child(Divider::horizontal()) + .child( + Button::new("restore-checkpoint", "Restore Checkpoint") + .icon(IconName::Undo) + .icon_size(IconSize::XSmall) + .icon_position(IconPosition::Start) + .label_size(LabelSize::XSmall) + .icon_color(Color::Muted) + .color(Color::Muted) + .on_click(cx.listener(move |this, _, _window, cx| { + this.rewind(&message_id, cx); + })) + ) + .child(Divider::horizontal()) }) })) - .children(rules_item) .child( div() .relative() .child( div() - .p_3() + .py_3() + .px_2() .rounded_lg() .shadow_md() .bg(cx.theme().colors().editor_background) @@ -1080,12 +1092,20 @@ impl AcpThreadView { if let Some(editing_index) = self.editing_message.as_ref() && *editing_index < entry_ix { - div() - .child(primary) - .opacity(0.2) + let backdrop = div() + .id(("backdrop", entry_ix)) + .size_full() + .absolute() + .inset_0() + .bg(cx.theme().colors().panel_background) + .opacity(0.8) .block_mouse_except_scroll() - .id("overlay") - .on_click(cx.listener(Self::cancel_editing)) + .on_click(cx.listener(Self::cancel_editing)); + + div() + .relative() + .child(primary) + .child(backdrop) .into_any_element() } else { primary @@ -1100,7 +1120,7 @@ impl AcpThreadView { } fn tool_card_border_color(&self, cx: &Context) -> Hsla { - cx.theme().colors().border.opacity(0.6) + cx.theme().colors().border.opacity(0.8) } fn tool_name_font_size(&self) -> Rems { @@ -1299,23 +1319,14 @@ impl AcpThreadView { tool_call.status, ToolCallStatus::WaitingForConfirmation { .. } ); - let is_edit = matches!(tool_call.kind, acp::ToolKind::Edit); - let has_diff = tool_call - .content - .iter() - .any(|content| matches!(content, ToolCallContent::Diff { .. })); - let has_nonempty_diff = tool_call.content.iter().any(|content| match content { - ToolCallContent::Diff(diff) => diff.read(cx).has_revealed_range(cx), - _ => false, - }); - let use_card_layout = needs_confirmation || is_edit || has_diff; + let is_edit = + matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some(); + let use_card_layout = needs_confirmation || is_edit; let is_collapsible = !tool_call.content.is_empty() && !use_card_layout; - let is_open = tool_call.content.is_empty() - || needs_confirmation - || has_nonempty_diff - || self.expanded_tool_calls.contains(&tool_call.id); + let is_open = + needs_confirmation || is_edit || self.expanded_tool_calls.contains(&tool_call.id); let gradient_overlay = |color: Hsla| { div() @@ -1336,41 +1347,49 @@ impl AcpThreadView { cx.theme().colors().panel_background }; - let tool_output_display = match &tool_call.status { - ToolCallStatus::WaitingForConfirmation { options, .. } => v_flex() - .w_full() - .children(tool_call.content.iter().map(|content| { - div() - .child( - self.render_tool_call_content(entry_ix, content, tool_call, window, cx), - ) - .into_any_element() - })) - .child(self.render_permission_buttons( - options, - entry_ix, - tool_call.id.clone(), - tool_call.content.is_empty(), - cx, - )), - ToolCallStatus::Pending - | ToolCallStatus::InProgress - | ToolCallStatus::Completed - | ToolCallStatus::Failed - | ToolCallStatus::Canceled => { - v_flex() + let tool_output_display = if is_open { + match &tool_call.status { + ToolCallStatus::WaitingForConfirmation { options, .. } => { + v_flex() + .w_full() + .children(tool_call.content.iter().map(|content| { + div() + .child(self.render_tool_call_content( + entry_ix, content, tool_call, window, cx, + )) + .into_any_element() + })) + .child(self.render_permission_buttons( + options, + entry_ix, + tool_call.id.clone(), + tool_call.content.is_empty(), + cx, + )) + .into_any() + } + ToolCallStatus::Pending | ToolCallStatus::InProgress + if is_edit && tool_call.content.is_empty() => + { + self.render_diff_loading(cx).into_any() + } + ToolCallStatus::Pending + | ToolCallStatus::InProgress + | ToolCallStatus::Completed + | ToolCallStatus::Failed + | ToolCallStatus::Canceled => v_flex() .w_full() .children(tool_call.content.iter().map(|content| { - div() - .child( - self.render_tool_call_content( - entry_ix, content, tool_call, window, cx, - ), - ) - .into_any_element() + div().child( + self.render_tool_call_content(entry_ix, content, tool_call, window, cx), + ) })) + .into_any(), + ToolCallStatus::Rejected => Empty.into_any(), } - ToolCallStatus::Rejected => v_flex().size_0(), + .into() + } else { + None }; v_flex() @@ -1390,9 +1409,13 @@ impl AcpThreadView { .map(|this| { if use_card_layout { this.pl_2() - .pr_1() + .pr_1p5() .py_1() .rounded_t_md() + .when(is_open, |this| { + this.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.)) @@ -1403,6 +1426,7 @@ impl AcpThreadView { .group(&card_header_id) .relative() .w_full() + .min_h_6() .text_size(self.tool_name_font_size()) .child(self.render_tool_call_icon( card_header_id, @@ -1456,11 +1480,7 @@ impl AcpThreadView { .overflow_x_scroll() .child(self.render_markdown( tool_call.label.clone(), - default_markdown_style( - needs_confirmation || is_edit || has_diff, - window, - cx, - ), + default_markdown_style(false, window, cx), )), ) .child(gradient_overlay(gradient_color)) @@ -1480,7 +1500,7 @@ impl AcpThreadView { ) .children(status_icon), ) - .when(is_open, |this| this.child(tool_output_display)) + .children(tool_output_display) } fn render_tool_call_content( @@ -1501,7 +1521,7 @@ impl AcpThreadView { Empty.into_any_element() } } - ToolCallContent::Diff(diff) => self.render_diff_editor(entry_ix, diff, cx), + ToolCallContent::Diff(diff) => self.render_diff_editor(entry_ix, diff, tool_call, cx), ToolCallContent::Terminal(terminal) => { self.render_terminal_tool_call(entry_ix, terminal, tool_call, window, cx) } @@ -1645,21 +1665,69 @@ impl AcpThreadView { }))) } + fn render_diff_loading(&self, cx: &Context) -> AnyElement { + let bar = |n: u64, width_class: &str| { + let bg_color = cx.theme().colors().element_active; + let base = h_flex().h_1().rounded_full(); + + let modified = match width_class { + "w_4_5" => base.w_3_4(), + "w_1_4" => base.w_1_4(), + "w_2_4" => base.w_2_4(), + "w_3_5" => base.w_3_5(), + "w_2_5" => base.w_2_5(), + _ => base.w_1_2(), + }; + + modified.with_animation( + ElementId::Integer(n), + Animation::new(Duration::from_secs(2)).repeat(), + move |tab, delta| { + let delta = (delta - 0.15 * n as f32) / 0.7; + let delta = 1.0 - (0.5 - delta).abs() * 2.; + let delta = ease_in_out(delta.clamp(0., 1.)); + let delta = 0.1 + 0.9 * delta; + + tab.bg(bg_color.opacity(delta)) + }, + ) + }; + + v_flex() + .p_3() + .gap_1() + .rounded_b_md() + .bg(cx.theme().colors().editor_background) + .child(bar(0, "w_4_5")) + .child(bar(1, "w_1_4")) + .child(bar(2, "w_2_4")) + .child(bar(3, "w_3_5")) + .child(bar(4, "w_2_5")) + .into_any_element() + } + fn render_diff_editor( &self, entry_ix: usize, diff: &Entity, + tool_call: &ToolCall, cx: &Context, ) -> AnyElement { + let tool_progress = matches!( + &tool_call.status, + ToolCallStatus::InProgress | ToolCallStatus::Pending + ); + v_flex() .h_full() - .border_t_1() - .border_color(self.tool_card_border_color(cx)) .child( if let Some(entry) = self.entry_view_state.read(cx).entry(entry_ix) && let Some(editor) = entry.editor_for_diff(diff) + && diff.read(cx).has_revealed_range(cx) { editor.clone().into_any_element() + } else if tool_progress { + self.render_diff_loading(cx) } else { Empty.into_any() }, @@ -1924,11 +1992,11 @@ impl AcpThreadView { .justify_center() .child(div().opacity(0.3).child(logo)) .child( - h_flex().absolute().right_1().bottom_0().child( - Icon::new(IconName::XCircle) - .color(Color::Error) - .size(IconSize::Small), - ), + h_flex() + .absolute() + .right_1() + .bottom_0() + .child(Icon::new(IconName::XCircleFilled).color(Color::Error)), ) .into_any_element() } @@ -1982,12 +2050,12 @@ impl AcpThreadView { Some( v_flex() - .pt_2() .px_2p5() .gap_1() .when_some(user_rules_text, |parent, user_rules_text| { parent.child( h_flex() + .group("user-rules") .w_full() .child( Icon::new(IconName::Reader) @@ -2008,6 +2076,7 @@ impl AcpThreadView { .shape(ui::IconButtonShape::Square) .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) + .visible_on_hover("user-rules") // TODO: Figure out a way to pass focus handle here so we can display the `OpenRulesLibrary` keybinding .tooltip(Tooltip::text("View User Rules")) .on_click(move |_event, window, cx| { @@ -2024,6 +2093,7 @@ impl AcpThreadView { .when_some(rules_file_text, |parent, rules_file_text| { parent.child( h_flex() + .group("project-rules") .w_full() .child( Icon::new(IconName::File) @@ -2044,7 +2114,8 @@ impl AcpThreadView { .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) .on_click(cx.listener(Self::handle_open_rules)) - .tooltip(Tooltip::text("View Rules")), + .visible_on_hover("project-rules") + .tooltip(Tooltip::text("View Project Rules")), ), ) }) @@ -2119,11 +2190,9 @@ impl AcpThreadView { .items_center() .justify_center() .child(self.render_error_agent_logo()) - .child( - h_flex().mt_4().mb_1().justify_center().child( - Headline::new("Authentication Required").size(HeadlineSize::Medium), - ), - ) + .child(h_flex().mt_4().mb_1().justify_center().child( + Headline::new(self.agent.empty_state_headline()).size(HeadlineSize::Medium), + )) .into_any(), ) .children(description.map(|desc| { @@ -2838,10 +2907,10 @@ impl AcpThreadView { .child( h_flex() .flex_none() + .flex_wrap() .justify_between() .child( h_flex() - .gap_1() .child(self.render_follow_toggle(cx)) .children(self.render_burn_mode_toggle(cx)), ) @@ -2883,7 +2952,7 @@ impl AcpThreadView { h_flex() .flex_shrink_0() .gap_0p5() - .mr_1() + .mr_1p5() .child( Label::new(used) .size(LabelSize::Small) @@ -2904,7 +2973,11 @@ impl AcpThreadView { } }), ) - .child(Label::new("/").size(LabelSize::Small).color(Color::Muted)) + .child( + Label::new("/") + .size(LabelSize::Small) + .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.5))), + ) .child(Label::new(max).size(LabelSize::Small).color(Color::Muted)), ) } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 297bb5f3e8..c89dc56795 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -65,8 +65,8 @@ use theme::ThemeSettings; use time::UtcOffset; use ui::utils::WithRemSize; use ui::{ - Banner, Callout, ContextMenu, ContextMenuEntry, Divider, ElevationIndex, KeyBinding, - PopoverMenu, PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*, + Banner, Callout, ContextMenu, ContextMenuEntry, ElevationIndex, KeyBinding, PopoverMenu, + PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*, }; use util::ResultExt as _; use workspace::{ @@ -245,17 +245,16 @@ impl AgentType { match self { Self::Zed | Self::TextThread => "Zed Agent", Self::NativeAgent => "Agent 2", - Self::Gemini => "Google Gemini", + Self::Gemini => "Gemini CLI", Self::ClaudeCode => "Claude Code", } } - fn icon(self) -> IconName { + fn icon(self) -> Option { match self { - Self::Zed | Self::TextThread => IconName::AiZed, - Self::NativeAgent => IconName::ZedAssistant, - Self::Gemini => IconName::AiGemini, - Self::ClaudeCode => IconName::AiClaude, + Self::Zed | Self::NativeAgent | Self::TextThread => None, + Self::Gemini => Some(IconName::AiGemini), + Self::ClaudeCode => Some(IconName::AiClaude), } } } @@ -2158,12 +2157,17 @@ impl AgentPanel { }) } - fn render_recent_entries_menu(&self, cx: &mut Context) -> impl IntoElement { + fn render_recent_entries_menu( + &self, + icon: IconName, + corner: Corner, + cx: &mut Context, + ) -> impl IntoElement { let focus_handle = self.focus_handle(cx); PopoverMenu::new("agent-nav-menu") .trigger_with_tooltip( - IconButton::new("agent-nav-menu", IconName::MenuAlt).icon_size(IconSize::Small), + IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small), { let focus_handle = focus_handle.clone(); move |window, cx| { @@ -2177,7 +2181,7 @@ impl AgentPanel { } }, ) - .anchor(Corner::TopLeft) + .anchor(corner) .with_handle(self.assistant_navigation_menu_handle.clone()) .menu({ let menu = self.assistant_navigation_menu.clone(); @@ -2304,7 +2308,9 @@ impl AgentPanel { .pl(DynamicSpacing::Base04.rems(cx)) .child(self.render_toolbar_back_button(cx)) .into_any_element(), - _ => self.render_recent_entries_menu(cx).into_any_element(), + _ => self + .render_recent_entries_menu(IconName::MenuAlt, Corner::TopLeft, cx) + .into_any_element(), }) .child(self.render_title_view(window, cx)), ) @@ -2390,7 +2396,7 @@ impl AgentPanel { .item( ContextMenuEntry::new("New Thread") .action(NewThread::default().boxed_clone()) - .icon(IconName::ZedAssistant) + .icon(IconName::Thread) .icon_color(Color::Muted) .handler({ let workspace = workspace.clone(); @@ -2443,7 +2449,7 @@ impl AgentPanel { .header("External Agents") .when(cx.has_flag::(), |menu| { menu.item( - ContextMenuEntry::new("New Gemini Thread") + ContextMenuEntry::new("New Gemini CLI Thread") .icon(IconName::AiGemini) .icon_color(Color::Muted) .handler({ @@ -2503,16 +2509,18 @@ impl AgentPanel { let selected_agent_label = self.selected_agent.label().into(); let selected_agent = div() .id("selected_agent_icon") - .px(DynamicSpacing::Base02.rems(cx)) - .child(Icon::new(self.selected_agent.icon()).color(Color::Muted)) - .tooltip(move |window, cx| { - Tooltip::with_meta( - selected_agent_label.clone(), - None, - "Selected Agent", - window, - cx, - ) + .when_some(self.selected_agent.icon(), |this, icon| { + this.px(DynamicSpacing::Base02.rems(cx)) + .child(Icon::new(icon).color(Color::Muted)) + .tooltip(move |window, cx| { + Tooltip::with_meta( + selected_agent_label.clone(), + None, + "Selected Agent", + window, + cx, + ) + }) }) .into_any_element(); @@ -2535,31 +2543,23 @@ impl AgentPanel { ActiveView::History | ActiveView::Configuration => { self.render_toolbar_back_button(cx).into_any_element() } - _ => h_flex() - .gap_1() - .child(self.render_recent_entries_menu(cx)) - .child(Divider::vertical()) - .child(selected_agent) - .into_any_element(), + _ => selected_agent.into_any_element(), }) .child(self.render_title_view(window, cx)), ) .child( h_flex() - .h_full() - .gap_2() - .children(self.render_token_count(cx)) - .child( - h_flex() - .h_full() - .gap(DynamicSpacing::Base02.rems(cx)) - .pl(DynamicSpacing::Base04.rems(cx)) - .pr(DynamicSpacing::Base06.rems(cx)) - .border_l_1() - .border_color(cx.theme().colors().border) - .child(new_thread_menu) - .child(self.render_panel_options_menu(window, cx)), - ), + .flex_none() + .gap(DynamicSpacing::Base02.rems(cx)) + .pl(DynamicSpacing::Base04.rems(cx)) + .pr(DynamicSpacing::Base06.rems(cx)) + .child(new_thread_menu) + .child(self.render_recent_entries_menu( + IconName::MenuAltTemp, + Corner::TopRight, + cx, + )) + .child(self.render_panel_options_menu(window, cx)), ) } diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 8bd76cbecf..38f02c2206 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -155,6 +155,7 @@ pub enum IconName { Maximize, Menu, MenuAlt, + MenuAltTemp, Mic, MicMute, Minimize, @@ -245,6 +246,8 @@ pub enum IconName { Warning, WholeWord, XCircle, + XCircleFilled, + ZedAgent, ZedAssistant, ZedBurnMode, ZedBurnModeOn, diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 7939e97e48..a161ddd074 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -1084,7 +1084,13 @@ impl Element for MarkdownElement { cx, ); el.child( - div().absolute().top_1().right_0p5().w_5().child(codeblock), + h_flex() + .w_5() + .absolute() + .top_1() + .right_1() + .justify_center() + .child(codeblock), ) }); }