From 91cbb2ec25693c901f847a9bcbbebb97146c74bc Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Wed, 3 Sep 2025 12:59:14 +0200 Subject: [PATCH] Add onboarding banner for claude code support (#37443) Release Notes: - N/A --------- Co-authored-by: Danilo Leal --- assets/images/acp_logo_serif.svg | 48 +++- crates/agent_ui/src/agent_panel.rs | 7 +- crates/agent_ui/src/ui.rs | 2 + .../agent_ui/src/ui/acp_onboarding_modal.rs | 20 +- .../src/ui/claude_code_onboarding_modal.rs | 254 ++++++++++++++++++ crates/title_bar/src/onboarding_banner.rs | 11 +- crates/title_bar/src/title_bar.rs | 10 +- crates/zed_actions/src/lib.rs | 2 + 8 files changed, 330 insertions(+), 24 deletions(-) create mode 100644 crates/agent_ui/src/ui/claude_code_onboarding_modal.rs diff --git a/assets/images/acp_logo_serif.svg b/assets/images/acp_logo_serif.svg index 6bc359cf82dde8060a66c051c8727f0e0624b938..a04d32e51c43acf358baa733f03284dbb6de1369 100644 --- a/assets/images/acp_logo_serif.svg +++ b/assets/images/acp_logo_serif.svg @@ -1,2 +1,46 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index cfa5b56358863ece6ab1f6dd024e7be365766853..305261183e92de6dbe2ad5756293e8b0bbf77849 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -10,11 +10,11 @@ use agent2::{DbThreadMetadata, HistoryEntry}; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use serde::{Deserialize, Serialize}; use zed_actions::OpenBrowser; -use zed_actions::agent::ReauthenticateAgent; +use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent}; use crate::acp::{AcpThreadHistory, ThreadHistoryEvent}; use crate::agent_diff::AgentDiffThread; -use crate::ui::AcpOnboardingModal; +use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal}; use crate::{ AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread, @@ -207,6 +207,9 @@ pub fn init(cx: &mut App) { .register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| { AcpOnboardingModal::toggle(workspace, window, cx) }) + .register_action(|workspace, _: &OpenClaudeCodeOnboardingModal, window, cx| { + ClaudeCodeOnboardingModal::toggle(workspace, window, cx) + }) .register_action(|_workspace, _: &ResetOnboarding, window, cx| { window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx); window.refresh(); diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index 600698b07e1e2bf43d78c5c225838476f04a5c76..1a3264bd77ccda1a27ffd19f3c61c3635fe78dc9 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -1,6 +1,7 @@ mod acp_onboarding_modal; mod agent_notification; mod burn_mode_tooltip; +mod claude_code_onboarding_modal; mod context_pill; mod end_trial_upsell; mod onboarding_modal; @@ -10,6 +11,7 @@ mod unavailable_editing_tooltip; pub use acp_onboarding_modal::*; pub use agent_notification::*; pub use burn_mode_tooltip::*; +pub use claude_code_onboarding_modal::*; pub use context_pill::*; pub use end_trial_upsell::*; pub use onboarding_modal::*; diff --git a/crates/agent_ui/src/ui/acp_onboarding_modal.rs b/crates/agent_ui/src/ui/acp_onboarding_modal.rs index 0ed9de7221014476f21c0406e6be8ac3592fca7c..8433904fb3b540c2d78c8634b7a6755303d6e15c 100644 --- a/crates/agent_ui/src/ui/acp_onboarding_modal.rs +++ b/crates/agent_ui/src/ui/acp_onboarding_modal.rs @@ -141,20 +141,12 @@ impl Render for AcpOnboardingModal { .bg(gpui::black().opacity(0.15)), ) .child( - h_flex() - .gap_4() - .child( - Vector::new(VectorName::AcpLogo, rems_from_px(106.), rems_from_px(40.)) - .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))), - ) - .child( - Vector::new( - VectorName::AcpLogoSerif, - rems_from_px(111.), - rems_from_px(41.), - ) - .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))), - ), + Vector::new( + VectorName::AcpLogoSerif, + rems_from_px(257.), + rems_from_px(47.), + ) + .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))), ) .child( v_flex() diff --git a/crates/agent_ui/src/ui/claude_code_onboarding_modal.rs b/crates/agent_ui/src/ui/claude_code_onboarding_modal.rs new file mode 100644 index 0000000000000000000000000000000000000000..06980f18977aefe228bb7f09962e69fe2b3a5068 --- /dev/null +++ b/crates/agent_ui/src/ui/claude_code_onboarding_modal.rs @@ -0,0 +1,254 @@ +use client::zed_urls; +use gpui::{ + ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render, + linear_color_stop, linear_gradient, +}; +use ui::{TintColor, Vector, VectorName, prelude::*}; +use workspace::{ModalView, Workspace}; + +use crate::agent_panel::{AgentPanel, AgentType}; + +macro_rules! claude_code_onboarding_event { + ($name:expr) => { + telemetry::event!($name, source = "ACP Claude Code Onboarding"); + }; + ($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => { + telemetry::event!($name, source = "ACP Claude Code Onboarding", $($key $(= $value)?),+); + }; +} + +pub struct ClaudeCodeOnboardingModal { + focus_handle: FocusHandle, + workspace: Entity, +} + +impl ClaudeCodeOnboardingModal { + pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { + 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.workspace.update(cx, |workspace, cx| { + workspace.focus_panel::(window, cx); + + if let Some(panel) = workspace.panel::(cx) { + panel.update(cx, |panel, cx| { + panel.new_agent_thread(AgentType::ClaudeCode, window, cx); + }); + } + }); + + cx.emit(DismissEvent); + + claude_code_onboarding_event!("Open Panel Clicked"); + } + + fn view_docs(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context) { + cx.open_url(&zed_urls::external_agents_docs(cx)); + cx.notify(); + + claude_code_onboarding_event!("Documentation Link Clicked"); + } + + fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { + cx.emit(DismissEvent); + } +} + +impl EventEmitter for ClaudeCodeOnboardingModal {} + +impl Focusable for ClaudeCodeOnboardingModal { + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl ModalView for ClaudeCodeOnboardingModal {} + +impl Render for ClaudeCodeOnboardingModal { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let illustration_element = |icon: IconName, label: Option, opacity: f32| { + h_flex() + .px_1() + .py_0p5() + .gap_1() + .rounded_sm() + .bg(cx.theme().colors().element_active.opacity(0.05)) + .border_1() + .border_color(cx.theme().colors().border) + .border_dashed() + .child( + Icon::new(icon) + .size(IconSize::Small) + .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))), + ) + .map(|this| { + if let Some(label_text) = label { + this.child( + Label::new(label_text) + .size(LabelSize::Small) + .color(Color::Muted), + ) + } else { + this.child( + div().w_16().h_1().rounded_full().bg(cx + .theme() + .colors() + .element_active + .opacity(0.6)), + ) + } + }) + .opacity(opacity) + }; + + let illustration = h_flex() + .relative() + .h(rems_from_px(126.)) + .bg(cx.theme().colors().editor_background) + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .justify_center() + .gap_8() + .rounded_t_md() + .overflow_hidden() + .child( + div().absolute().inset_0().w(px(515.)).h(px(126.)).child( + Vector::new(VectorName::AcpGrid, rems_from_px(515.), rems_from_px(126.)) + .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.02))), + ), + ) + .child(div().absolute().inset_0().size_full().bg(linear_gradient( + 0., + linear_color_stop( + cx.theme().colors().elevated_surface_background.opacity(0.1), + 0.9, + ), + linear_color_stop( + cx.theme().colors().elevated_surface_background.opacity(0.), + 0., + ), + ))) + .child( + div() + .absolute() + .inset_0() + .size_full() + .bg(gpui::black().opacity(0.15)), + ) + .child( + Vector::new( + VectorName::AcpLogoSerif, + rems_from_px(257.), + rems_from_px(47.), + ) + .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))), + ) + .child( + v_flex() + .gap_1p5() + .child(illustration_element(IconName::Stop, None, 0.15)) + .child(illustration_element( + IconName::AiGemini, + Some("New Gemini CLI Thread".into()), + 0.3, + )) + .child( + h_flex() + .pl_1() + .pr_2() + .py_0p5() + .gap_1() + .rounded_sm() + .bg(cx.theme().colors().element_active.opacity(0.2)) + .border_1() + .border_color(cx.theme().colors().border) + .child( + Icon::new(IconName::AiClaude) + .size(IconSize::Small) + .color(Color::Muted), + ) + .child(Label::new("New Claude Code Thread").size(LabelSize::Small)), + ) + .child(illustration_element( + IconName::Stop, + Some("Your Agent Here".into()), + 0.3, + )) + .child(illustration_element(IconName::Stop, None, 0.15)), + ); + + let heading = v_flex() + .w_full() + .gap_1() + .child( + Label::new("Beta Release") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child(Headline::new("Claude Code: Natively in Zed").size(HeadlineSize::Large)); + + let copy = "Powered by the Agent Client Protocol, you can now run Claude Code as\na first-class citizen in Zed's agent panel."; + + let open_panel_button = Button::new("open-panel", "Start with Claude Code") + .icon_size(IconSize::Indicator) + .style(ButtonStyle::Tinted(TintColor::Accent)) + .full_width() + .on_click(cx.listener(Self::open_panel)); + + let docs_button = Button::new("add-other-agents", "Add Other Agents") + .icon(IconName::ArrowUpRight) + .icon_size(IconSize::Indicator) + .icon_color(Color::Muted) + .full_width() + .on_click(cx.listener(Self::view_docs)); + + let close_button = h_flex().absolute().top_2().right_2().child( + IconButton::new("cancel", IconName::Close).on_click(cx.listener( + |_, _: &ClickEvent, _window, cx| { + claude_code_onboarding_event!("Canceled", trigger = "X click"); + cx.emit(DismissEvent); + }, + )), + ); + + v_flex() + .id("acp-onboarding") + .key_context("AcpOnboardingModal") + .relative() + .w(rems(34.)) + .h_full() + .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| { + claude_code_onboarding_event!("Canceled", trigger = "Action"); + cx.emit(DismissEvent); + })) + .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| { + this.focus_handle.focus(window); + })) + .child(illustration) + .child( + v_flex() + .p_4() + .gap_2() + .child(heading) + .child(Label::new(copy).color(Color::Muted)) + .child( + v_flex() + .w_full() + .mt_2() + .gap_1() + .child(open_panel_button) + .child(docs_button), + ), + ) + .child(close_button) + } +} diff --git a/crates/title_bar/src/onboarding_banner.rs b/crates/title_bar/src/onboarding_banner.rs index 1c2894249000861f6de14f4960205e5deffab47b..6adc5769498ee19a7139c3fd02bd586e32185778 100644 --- a/crates/title_bar/src/onboarding_banner.rs +++ b/crates/title_bar/src/onboarding_banner.rs @@ -7,6 +7,7 @@ pub struct OnboardingBanner { dismissed: bool, source: String, details: BannerDetails, + visible_when: Option bool>>, } #[derive(Clone)] @@ -42,12 +43,18 @@ impl OnboardingBanner { label: label.into(), subtitle: subtitle.or(Some(SharedString::from("Introducing:"))), }, + visible_when: None, dismissed: get_dismissed(source), } } - fn should_show(&self, _cx: &mut App) -> bool { - !self.dismissed + pub fn visible_when(mut self, predicate: impl Fn(&mut App) -> bool + 'static) -> Self { + self.visible_when = Some(Box::new(predicate)); + self + } + + fn should_show(&self, cx: &mut App) -> bool { + !self.dismissed && self.visible_when.as_ref().map_or(true, |f| f(cx)) } fn dismiss(&mut self, cx: &mut Context) { diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 2b13ef58c3a8707b81d6870590efe5337ffef048..f031b8394afc551c8077419f504104936095a0c3 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -279,13 +279,15 @@ impl TitleBar { let banner = cx.new(|cx| { OnboardingBanner::new( - "ACP Onboarding", - IconName::Sparkle, - "Bring Your Own Agent", + "ACP Claude Code Onboarding", + IconName::AiClaude, + "Claude Code", Some("Introducing:".into()), - zed_actions::agent::OpenAcpOnboardingModal.boxed_clone(), + zed_actions::agent::OpenClaudeCodeOnboardingModal.boxed_clone(), cx, ) + // When updating this to a non-AI feature release, remove this line. + .visible_when(|cx| !project::DisableAiSettings::get_global(cx).disable_ai) }); let platform_titlebar = cx.new(|cx| PlatformTitleBar::new(id, cx)); diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 8f4c42ca496e26d23765eb006d7eb0fe9db197ee..bc47b8f1e47d3a550d44af3bc852b136fa8b8bfc 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -286,6 +286,8 @@ pub mod agent { OpenOnboardingModal, /// Opens the ACP onboarding modal. OpenAcpOnboardingModal, + /// Opens the Claude Code onboarding modal. + OpenClaudeCodeOnboardingModal, /// Resets the agent onboarding state. ResetOnboarding, /// Starts a chat conversation with the agent.