Add an onboarding banner for the Agent panel (#30050)

Marshall Bowers created

This PR adds an onboarding banner for the Agent panel:

<img width="262" alt="Screenshot 2025-05-06 at 6 54 58 PM"
src="https://github.com/user-attachments/assets/52849e64-7d5d-488c-8456-4d7bd97f8ebd"
/>

Release Notes:

- N/A

Change summary

crates/agent/src/assistant_panel.rs     |  10 +
crates/agent/src/ui.rs                  |   2 
crates/agent/src/ui/onboarding_modal.rs | 157 +++++++++++++++++++++++++++
crates/title_bar/src/title_bar.rs       |   8 
crates/zed_actions/src/lib.rs           |   5 
5 files changed, 176 insertions(+), 6 deletions(-)

Detailed changes

crates/agent/src/assistant_panel.rs 🔗

@@ -46,7 +46,7 @@ use ui::{
 use util::{ResultExt as _, maybe};
 use workspace::dock::{DockPosition, Panel, PanelEvent};
 use workspace::{CollaboratorId, DraggedSelection, DraggedTab, ToolbarItemView, Workspace};
-use zed_actions::agent::OpenConfiguration;
+use zed_actions::agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding};
 use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
 use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
 use zed_llm_client::UsageLimit;
@@ -59,6 +59,7 @@ use crate::message_editor::{MessageEditor, MessageEditorEvent};
 use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
 use crate::thread_history::{EntryTimeFormat, PastContext, PastThread, ThreadHistory};
 use crate::thread_store::ThreadStore;
+use crate::ui::AgentOnboardingModal;
 use crate::{
     AddContextServer, AgentDiffPane, ContextStore, DeleteRecentlyOpenThread, ExpandMessageEditor,
     Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
@@ -145,6 +146,13 @@ pub fn init(cx: &mut App) {
                         });
                     }
                 })
+                .register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
+                    AgentOnboardingModal::toggle(workspace, window, cx)
+                })
+                .register_action(|_workspace, _: &ResetOnboarding, window, cx| {
+                    window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
+                    window.refresh();
+                })
                 .register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
                     set_trial_upsell_dismissed(false, cx);
                 });

crates/agent/src/ui.rs 🔗

@@ -2,6 +2,7 @@ mod agent_notification;
 mod animated_label;
 mod context_pill;
 mod max_mode_tooltip;
+mod onboarding_modal;
 pub mod preview;
 mod upsell;
 
@@ -9,3 +10,4 @@ pub use agent_notification::*;
 pub use animated_label::*;
 pub use context_pill::*;
 pub use max_mode_tooltip::*;
+pub use onboarding_modal::*;

crates/agent/src/ui/onboarding_modal.rs 🔗

@@ -0,0 +1,157 @@
+use gpui::{
+    ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
+};
+use ui::{TintColor, Vector, VectorName, prelude::*};
+use workspace::{ModalView, Workspace};
+
+use crate::assistant_panel::AssistantPanel;
+
+macro_rules! agent_onboarding_event {
+    ($name:expr) => {
+        telemetry::event!($name, source = "Agent Onboarding");
+    };
+    ($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
+        telemetry::event!($name, source = "Agent Onboarding", $($key $(= $value)?),+);
+    };
+}
+
+pub struct AgentOnboardingModal {
+    focus_handle: FocusHandle,
+    workspace: Entity<Workspace>,
+}
+
+impl AgentOnboardingModal {
+    pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
+        let workspace_entity = cx.entity();
+        workspace.toggle_modal(window, cx, |_window, cx| Self {
+            workspace: workspace_entity,
+            focus_handle: cx.focus_handle(),
+        });
+    }
+
+    fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
+        self.workspace.update(cx, |workspace, cx| {
+            workspace.focus_panel::<AssistantPanel>(window, cx);
+        });
+
+        cx.emit(DismissEvent);
+
+        agent_onboarding_event!("Open Panel Clicked");
+    }
+
+    fn view_blog(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
+        cx.open_url("http://zed.dev/blog/fastest-ai-code-editor");
+        cx.notify();
+
+        agent_onboarding_event!("Blog Link Clicked");
+    }
+
+    fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
+        cx.emit(DismissEvent);
+    }
+}
+
+impl EventEmitter<DismissEvent> for AgentOnboardingModal {}
+
+impl Focusable for AgentOnboardingModal {
+    fn focus_handle(&self, _cx: &App) -> FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl ModalView for AgentOnboardingModal {}
+
+impl Render for AgentOnboardingModal {
+    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let window_height = window.viewport_size().height;
+        let max_height = window_height - px(200.);
+
+        let base = v_flex()
+            .id("agent-onboarding")
+            .key_context("AgentOnboardingModal")
+            .relative()
+            .w(px(450.))
+            .h_full()
+            .max_h(max_height)
+            .p_4()
+            .gap_2()
+            .elevation_3(cx)
+            .track_focus(&self.focus_handle(cx))
+            .overflow_hidden()
+            .on_action(cx.listener(Self::cancel))
+            .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
+                agent_onboarding_event!("Canceled", trigger = "Action");
+                cx.emit(DismissEvent);
+            }))
+            .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
+                this.focus_handle.focus(window);
+            }))
+            .child(
+                div()
+                    .absolute()
+                    .top_0()
+                    .right(px(-1.0))
+                    .w(px(441.))
+                    .h(px(167.))
+                    .child(
+                        Vector::new(VectorName::Grid, rems_from_px(441.), rems_from_px(167.))
+                            .color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1))),
+                    ),
+            )
+            .child(
+                div()
+                    .absolute()
+                    .top(px(-8.0))
+                    .right_0()
+                    .w(px(400.))
+                    .h(px(92.))
+                    .child(
+                        Vector::new(VectorName::AiGrid, rems_from_px(400.), rems_from_px(92.))
+                            .color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32))),
+                    ),
+            )
+            .child(
+                v_flex()
+                    .w_full()
+                    .gap_1()
+                    .child(
+                        Label::new("Introducing")
+                            .size(LabelSize::Small)
+                            .color(Color::Muted),
+                    )
+                    .child(Headline::new("Agentic Editing in Zed").size(HeadlineSize::Large)),
+            )
+            .child(h_flex().absolute().top_2().right_2().child(
+                IconButton::new("cancel", IconName::X).on_click(cx.listener(
+                    |_, _: &ClickEvent, _window, cx| {
+                        agent_onboarding_event!("Cancelled", trigger = "X click");
+                        cx.emit(DismissEvent);
+                    },
+                )),
+            ));
+
+        let open_panel_button = Button::new("open-panel", "Get Started with the Agent Panel")
+            .icon_size(IconSize::Indicator)
+            .style(ButtonStyle::Tinted(TintColor::Accent))
+            .full_width()
+            .on_click(cx.listener(Self::open_panel));
+
+        let blog_post_button = Button::new("view-blog", "Check out the Blog Post")
+            .icon(IconName::ArrowUpRight)
+            .icon_size(IconSize::Indicator)
+            .icon_color(Color::Muted)
+            .full_width()
+            .on_click(cx.listener(Self::view_blog));
+
+        let copy = "Zed natively supports agentic editing, enabling seamless collaboration between humans and AI.";
+
+        base.child(Label::new(copy).color(Color::Muted)).child(
+            v_flex()
+                .w_full()
+                .mt_2()
+                .gap_2()
+                .child(open_panel_button)
+                .child(blog_post_button),
+        )
+    }
+}

crates/title_bar/src/title_bar.rs 🔗

@@ -309,11 +309,11 @@ impl TitleBar {
 
         let banner = cx.new(|cx| {
             OnboardingBanner::new(
-                "Git Onboarding",
-                IconName::GitBranchSmall,
-                "Git Support",
+                "Agentic Onboarding",
+                IconName::ZedAssistant,
+                "Agentic Editing",
                 None,
-                zed_actions::OpenGitIntegrationOnboarding.boxed_clone(),
+                zed_actions::agent::OpenOnboardingModal.boxed_clone(),
                 cx,
             )
         });

crates/zed_actions/src/lib.rs 🔗

@@ -186,7 +186,10 @@ pub mod icon_theme_selector {
 pub mod agent {
     use gpui::actions;
 
-    actions!(agent, [OpenConfiguration]);
+    actions!(
+        agent,
+        [OpenConfiguration, OpenOnboardingModal, ResetOnboarding]
+    );
 }
 
 pub mod assistant {