assets/icons/ai_claude.svg 🔗
@@ -1,3 +1,3 @@
-<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
Danilo Leal created
Release Notes:
- N/A
assets/icons/ai_claude.svg | 1
assets/icons/ai_gemini.svg | 4
assets/icons/new_from_summary.svg | 7
assets/icons/new_text_thread.svg | 7
assets/icons/new_thread.svg | 3
crates/agent_ui/src/agent_panel.rs | 230 +++++++++++++++++++---
crates/agent_ui/src/ui.rs | 2
crates/agent_ui/src/ui/new_thread_button.rs | 75 +++++++
crates/icons/src/icons.rs | 3
9 files changed, 295 insertions(+), 37 deletions(-)
@@ -1,3 +1,3 @@
-<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -1 +1,3 @@
-<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google Gemini</title><path d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7.44 12.27C7.81333 13.1217 8 14.0317 8 15C8 14.0317 8.18083 13.1217 8.5425 12.27C8.91583 11.4183 9.4175 10.6775 10.0475 10.0475C10.6775 9.4175 11.4183 8.92167 12.27 8.56C13.1217 8.18667 14.0317 8 15 8C14.0317 8 13.1217 7.81917 12.27 7.4575C11.4411 7.1001 10.6871 6.5895 10.0475 5.9525C9.4105 5.31293 8.8999 4.55891 8.5425 3.73C8.18083 2.87833 8 1.96833 8 1C8 1.96833 7.81333 2.87833 7.44 3.73C7.07833 4.58167 6.5825 5.3225 5.9525 5.9525C5.31293 6.5895 4.55891 7.1001 3.73 7.4575C2.87833 7.81917 1.96833 8 1 8C1.96833 8 2.87833 8.18667 3.73 8.56C4.58167 8.92167 5.3225 9.4175 5.9525 10.0475C6.5825 10.6775 7.07833 11.4183 7.44 12.27Z" fill="black"/>
+</svg>
@@ -0,0 +1,7 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M14 4H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.66667 8H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.66667 12H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.00016 12C8.41993 12.5597 9.00515 12.9731 9.67294 13.1817C10.3407 13.3903 11.0572 13.3835 11.7209 13.1623C12.3846 12.941 12.9619 12.5166 13.371 11.949C13.78 11.3815 14.0002 10.6996 14.0002 10C14.0002 9.20435 13.6841 8.44129 13.1215 7.87868C12.5589 7.31607 11.7958 7 11.0002 7C10.1135 7 9.30683 7.36 8.72683 7.94L7.3335 9.33333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.3335 6.66669V9.33335H10.0002" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -0,0 +1,7 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7.33333 8H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.6667 5H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 11H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12 7V11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M14 9H10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6.31254 12.549C7.3841 13.0987 8.61676 13.2476 9.78839 12.9688C10.96 12.6901 11.9936 12.0021 12.7028 11.0287C13.412 10.0554 13.7503 8.8607 13.6566 7.66002C13.5629 6.45934 13.0435 5.33159 12.1919 4.48C11.3403 3.62841 10.2126 3.10898 9.01188 3.01531C7.8112 2.92164 6.61655 3.2599 5.64319 3.96912C4.66984 4.67834 3.9818 5.71188 3.70306 6.88351C3.42432 8.05514 3.5732 9.2878 4.12289 10.3594L3 13.6719L6.31254 12.549Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -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::<feature_flags::AcpFeatureFlag>(), |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::<feature_flags::AcpFeatureFlag>(), |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<SharedString>,
+ action_slot: Option<AnyElement>,
+ cx: &mut Context<Self>,
+ ) -> 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::<feature_flags::AcpFeatureFlag>(), |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))
})
@@ -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::*;
@@ -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<ui::KeyBinding>,
+ on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
+}
+
+impl NewThreadButton {
+ pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>, icon: IconName) -> Self {
+ Self {
+ id: id.into(),
+ label: label.into(),
+ icon,
+ keybinding: None,
+ on_click: None,
+ }
+ }
+
+ pub fn keybinding(mut self, keybinding: Option<ui::KeyBinding>) -> Self {
+ self.keybinding = keybinding;
+ self
+ }
+
+ pub fn on_click<F>(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))
+ })
+ }
+}
@@ -181,6 +181,9 @@ pub enum IconName {
MicMute,
Microscope,
Minimize,
+ NewFromSummary,
+ NewTextThread,
+ NewThread,
Option,
PageDown,
PageUp,