agent: Add new thread start buttons to the empty state (#34829)

Danilo Leal created

Release Notes:

- N/A

Change summary

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(-)

Detailed changes

assets/icons/ai_gemini.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>

assets/icons/new_from_summary.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>

assets/icons/new_text_thread.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>

assets/icons/new_thread.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>

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::<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))
                     })

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::*;

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<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))
+            })
+    }
+}

crates/icons/src/icons.rs 🔗

@@ -181,6 +181,9 @@ pub enum IconName {
     MicMute,
     Microscope,
     Minimize,
+    NewFromSummary,
+    NewTextThread,
+    NewThread,
     Option,
     PageDown,
     PageUp,