diff --git a/assets/icons/ai_claude.svg b/assets/icons/ai_claude.svg index 423a963eba..a3e3e1f4cd 100644 --- a/assets/icons/ai_claude.svg +++ b/assets/icons/ai_claude.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/ai_gemini.svg b/assets/icons/ai_gemini.svg index 60197dc4ad..bdde44ed24 100644 --- a/assets/icons/ai_gemini.svg +++ b/assets/icons/ai_gemini.svg @@ -1 +1,3 @@ -Google Gemini + + + diff --git a/assets/icons/new_from_summary.svg b/assets/icons/new_from_summary.svg new file mode 100644 index 0000000000..3b61ca51a0 --- /dev/null +++ b/assets/icons/new_from_summary.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/new_text_thread.svg b/assets/icons/new_text_thread.svg new file mode 100644 index 0000000000..75afa934a0 --- /dev/null +++ b/assets/icons/new_text_thread.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/new_thread.svg b/assets/icons/new_thread.svg new file mode 100644 index 0000000000..8c2596a4c9 --- /dev/null +++ b/assets/icons/new_thread.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 36851e44ba..57d16d6e59 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -12,6 +12,7 @@ use serde::{Deserialize, Serialize}; use crate::NewExternalAgentThread; use crate::agent_diff::AgentDiffThread; use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES}; +use crate::ui::NewThreadButton; use crate::{ AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread, @@ -66,8 +67,8 @@ use theme::ThemeSettings; use time::UtcOffset; use ui::utils::WithRemSize; use ui::{ - Banner, Callout, ContextMenu, 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::{ @@ -1906,16 +1907,39 @@ impl AgentPanel { .when(cx.has_flag::(), |this| { this.header("Zed Agent") }) - .action("New Thread", NewThread::default().boxed_clone()) - .action("New Text Thread", NewTextThread.boxed_clone()) + .item( + ContextMenuEntry::new("New Thread") + .icon(IconName::NewThread) + .icon_color(Color::Muted) + .handler(move |window, cx| { + window.dispatch_action(NewThread::default().boxed_clone(), cx); + }), + ) + .item( + ContextMenuEntry::new("New Text Thread") + .icon(IconName::NewTextThread) + .icon_color(Color::Muted) + .handler(move |window, cx| { + window.dispatch_action(NewTextThread.boxed_clone(), cx); + }), + ) .when_some(active_thread, |this, active_thread| { let thread = active_thread.read(cx); + if !thread.is_empty() { - this.action( - "New From Summary", - Box::new(NewThread { - from_thread_id: Some(thread.id().clone()), - }), + let thread_id = thread.id().clone(); + this.item( + ContextMenuEntry::new("New From Summary") + .icon(IconName::NewFromSummary) + .icon_color(Color::Muted) + .handler(move |window, cx| { + window.dispatch_action( + Box::new(NewThread { + from_thread_id: Some(thread_id.clone()), + }), + cx, + ); + }), ) } else { this @@ -1924,19 +1948,33 @@ impl AgentPanel { .when(cx.has_flag::(), |this| { this.separator() .header("External Agents") - .action( - "New Gemini Thread", - NewExternalAgentThread { - agent: Some(crate::ExternalAgent::Gemini), - } - .boxed_clone(), + .item( + ContextMenuEntry::new("New Gemini Thread") + .icon(IconName::AiGemini) + .icon_color(Color::Muted) + .handler(move |window, cx| { + window.dispatch_action( + NewExternalAgentThread { + agent: Some(crate::ExternalAgent::Gemini), + } + .boxed_clone(), + cx, + ); + }), ) - .action( - "New Claude Code Thread", - NewExternalAgentThread { - agent: Some(crate::ExternalAgent::ClaudeCode), - } - .boxed_clone(), + .item( + ContextMenuEntry::new("New Claude Code Thread") + .icon(IconName::AiClaude) + .icon_color(Color::Muted) + .handler(move |window, cx| { + window.dispatch_action( + NewExternalAgentThread { + agent: Some(crate::ExternalAgent::ClaudeCode), + } + .boxed_clone(), + cx, + ); + }), ) }); menu @@ -2285,6 +2323,28 @@ impl AgentPanel { }))) } + fn render_empty_state_section_header( + &self, + label: impl Into, + action_slot: Option, + cx: &mut Context, + ) -> impl IntoElement { + h_flex() + .mt_2() + .pl_1p5() + .pb_1() + .w_full() + .justify_between() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .child( + Label::new(label.into()) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .children(action_slot) + } + fn render_thread_empty_state( &self, window: &mut Window, @@ -2407,19 +2467,9 @@ impl AgentPanel { .justify_end() .gap_1() .child( - h_flex() - .pl_1p5() - .pb_1() - .w_full() - .justify_between() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .child( - Label::new("Recent") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child( + self.render_empty_state_section_header( + "Recent", + Some( Button::new("view-history", "View All") .style(ButtonStyle::Subtle) .label_size(LabelSize::Small) @@ -2434,8 +2484,11 @@ impl AgentPanel { ) .on_click(move |_event, window, cx| { window.dispatch_action(OpenHistory.boxed_clone(), cx); - }), + }) + .into_any_element(), ), + cx, + ), ) .child( v_flex() @@ -2463,6 +2516,113 @@ impl AgentPanel { }, )), ) + .child(self.render_empty_state_section_header("Start", None, cx)) + .child( + v_flex() + .p_1() + .gap_2() + .child( + h_flex() + .w_full() + .gap_2() + .child( + NewThreadButton::new( + "new-thread-btn", + "New Thread", + IconName::NewThread, + ) + .keybinding(KeyBinding::for_action_in( + &NewThread::default(), + &self.focus_handle(cx), + window, + cx, + )) + .on_click( + |window, cx| { + window.dispatch_action( + NewThread::default().boxed_clone(), + cx, + ) + }, + ), + ) + .child( + NewThreadButton::new( + "new-text-thread-btn", + "New Text Thread", + IconName::NewTextThread, + ) + .keybinding(KeyBinding::for_action_in( + &NewTextThread, + &self.focus_handle(cx), + window, + cx, + )) + .on_click( + |window, cx| { + window.dispatch_action(Box::new(NewTextThread), cx) + }, + ), + ), + ) + .when(cx.has_flag::(), |this| { + this.child( + h_flex() + .w_full() + .gap_2() + .child( + NewThreadButton::new( + "new-gemini-thread-btn", + "New Gemini Thread", + IconName::AiGemini, + ) + // .keybinding(KeyBinding::for_action_in( + // &OpenHistory, + // &self.focus_handle(cx), + // window, + // cx, + // )) + .on_click( + |window, cx| { + window.dispatch_action( + Box::new(NewExternalAgentThread { + agent: Some( + crate::ExternalAgent::Gemini, + ), + }), + cx, + ) + }, + ), + ) + .child( + NewThreadButton::new( + "new-claude-thread-btn", + "New Claude Code Thread", + IconName::AiClaude, + ) + // .keybinding(KeyBinding::for_action_in( + // &OpenHistory, + // &self.focus_handle(cx), + // window, + // cx, + // )) + .on_click( + |window, cx| { + window.dispatch_action( + Box::new(NewExternalAgentThread { + agent: Some( + crate::ExternalAgent::ClaudeCode, + ), + }), + cx, + ) + }, + ), + ), + ) + }), + ) .when_some(configuration_error.as_ref(), |this, err| { this.child(self.render_configuration_error(err, &focus_handle, window, cx)) }) diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index 6398f64abb..15f2e28e58 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -2,6 +2,7 @@ mod agent_notification; mod burn_mode_tooltip; mod context_pill; mod end_trial_upsell; +mod new_thread_button; mod onboarding_modal; pub mod preview; mod upsell; @@ -10,4 +11,5 @@ pub use agent_notification::*; pub use burn_mode_tooltip::*; pub use context_pill::*; pub use end_trial_upsell::*; +pub use new_thread_button::*; pub use onboarding_modal::*; diff --git a/crates/agent_ui/src/ui/new_thread_button.rs b/crates/agent_ui/src/ui/new_thread_button.rs new file mode 100644 index 0000000000..7764144150 --- /dev/null +++ b/crates/agent_ui/src/ui/new_thread_button.rs @@ -0,0 +1,75 @@ +use gpui::{ClickEvent, ElementId, IntoElement, ParentElement, Styled}; +use ui::prelude::*; + +#[derive(IntoElement)] +pub struct NewThreadButton { + id: ElementId, + label: SharedString, + icon: IconName, + keybinding: Option, + on_click: Option>, +} + +impl NewThreadButton { + pub fn new(id: impl Into, label: impl Into, icon: IconName) -> Self { + Self { + id: id.into(), + label: label.into(), + icon, + keybinding: None, + on_click: None, + } + } + + pub fn keybinding(mut self, keybinding: Option) -> Self { + self.keybinding = keybinding; + self + } + + pub fn on_click(mut self, handler: F) -> Self + where + F: Fn(&mut Window, &mut App) + 'static, + { + self.on_click = Some(Box::new( + move |_: &ClickEvent, window: &mut Window, cx: &mut App| handler(window, cx), + )); + self + } +} + +impl RenderOnce for NewThreadButton { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + h_flex() + .id(self.id) + .w_full() + .py_1p5() + .px_2() + .gap_1() + .justify_between() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().border.opacity(0.4)) + .bg(cx.theme().colors().element_active.opacity(0.2)) + .hover(|style| { + style + .bg(cx.theme().colors().element_hover) + .border_color(cx.theme().colors().border) + }) + .child( + h_flex() + .gap_1p5() + .child( + Icon::new(self.icon) + .size(IconSize::XSmall) + .color(Color::Muted), + ) + .child(Label::new(self.label).size(LabelSize::Small)), + ) + .when_some(self.keybinding, |this, keybinding| { + this.child(keybinding.size(rems_from_px(10.))) + }) + .when_some(self.on_click, |this, on_click| { + this.on_click(move |event, window, cx| on_click(event, window, cx)) + }) + } +} diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 631ccc1af3..b85e5b517d 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -181,6 +181,9 @@ pub enum IconName { MicMute, Microscope, Minimize, + NewFromSummary, + NewTextThread, + NewThread, Option, PageDown, PageUp,