From a0d0195ca9130b43409704898d27a9770b0d3edb Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:05:43 -0300 Subject: [PATCH] Add onboarding for parallel agents (#52940) Release Notes: - N/A --------- Co-authored-by: Eric Holk Co-authored-by: Bennet Bo Fenner Co-authored-by: Katie Geer --- Cargo.lock | 7 + assets/images/ai_grid.svg | 334 -------------- assets/images/business_stamp.svg | 1 + assets/images/pro_trial_stamp.svg | 2 +- assets/images/pro_user_stamp.svg | 2 +- assets/images/student_stamp.svg | 1 + crates/agent_ui/src/agent_panel.rs | 157 ++++++- .../src/agent_panel_onboarding_card.rs | 96 ++--- .../src/agent_panel_onboarding_content.rs | 25 +- crates/ai_onboarding/src/ai_onboarding.rs | 225 ++++++++-- crates/ai_onboarding/src/ai_upsell_card.rs | 407 ------------------ crates/ai_onboarding/src/plan_definitions.rs | 14 +- crates/auto_update_ui/Cargo.toml | 5 + crates/auto_update_ui/src/auto_update_ui.rs | 97 +++-- .../edit_prediction/src/onboarding_modal.rs | 41 +- crates/gpui/src/window.rs | 2 +- crates/onboarding/Cargo.toml | 2 + crates/onboarding/src/basics_page.rs | 191 +++++++- crates/onboarding/src/onboarding.rs | 18 +- crates/onboarding/src/theme_preview.rs | 40 +- crates/ui/src/components/ai.rs | 4 + .../src/components/ai/agent_setup_button.rs | 110 +++++ .../ai/parallel_agents_illustration.rs | 149 +++++++ crates/ui/src/components/image.rs | 3 +- .../notification/announcement_toast.rs | 40 +- crates/workspace/src/welcome.rs | 181 +++++--- 26 files changed, 1112 insertions(+), 1042 deletions(-) delete mode 100644 assets/images/ai_grid.svg create mode 100644 assets/images/business_stamp.svg create mode 100644 assets/images/student_stamp.svg delete mode 100644 crates/ai_onboarding/src/ai_upsell_card.rs create mode 100644 crates/ui/src/components/ai/agent_setup_button.rs create mode 100644 crates/ui/src/components/ai/parallel_agents_illustration.rs diff --git a/Cargo.lock b/Cargo.lock index d296c6c57e12bf2a569e8eed42a2bc0ad129d24f..c6225699f1c882839624cc493e5c130a2cf4c647 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1233,10 +1233,13 @@ dependencies = [ name = "auto_update_ui" version = "0.1.0" dependencies = [ + "agent_settings", "anyhow", "auto_update", "client", + "db", "editor", + "fs", "gpui", "markdown_preview", "release_channel", @@ -1244,9 +1247,11 @@ dependencies = [ "serde", "serde_json", "smol", + "telemetry", "ui", "util", "workspace", + "zed_actions", ] [[package]] @@ -11584,6 +11589,8 @@ version = "0.1.0" dependencies = [ "anyhow", "client", + "cloud_api_types", + "collections", "component", "db", "documented", diff --git a/assets/images/ai_grid.svg b/assets/images/ai_grid.svg deleted file mode 100644 index 49e8c4139efb985277f812f4a1cd6656a713ba6a..0000000000000000000000000000000000000000 --- a/assets/images/ai_grid.svg +++ /dev/null @@ -1,334 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/business_stamp.svg b/assets/images/business_stamp.svg new file mode 100644 index 0000000000000000000000000000000000000000..a7bbf6914b3e4f1486bfa652afa3b85b75a7e6bd --- /dev/null +++ b/assets/images/business_stamp.svg @@ -0,0 +1 @@ + diff --git a/assets/images/pro_trial_stamp.svg b/assets/images/pro_trial_stamp.svg index a3f9095120876949c51f1cd03f8fb8499bf4ea3e..101ff361b311f72446b200f1ef838d73a8261ee9 100644 --- a/assets/images/pro_trial_stamp.svg +++ b/assets/images/pro_trial_stamp.svg @@ -1 +1 @@ - + diff --git a/assets/images/pro_user_stamp.svg b/assets/images/pro_user_stamp.svg index d037a9e8335d31f4b515a674f3bfa9495bf8a6a3..7b560d0b2534fd45aac709922f12eb889326ad6c 100644 --- a/assets/images/pro_user_stamp.svg +++ b/assets/images/pro_user_stamp.svg @@ -1 +1 @@ - + diff --git a/assets/images/student_stamp.svg b/assets/images/student_stamp.svg new file mode 100644 index 0000000000000000000000000000000000000000..947f4bb78557d23a3604776895a10953a8a56ae7 --- /dev/null +++ b/assets/images/student_stamp.svg @@ -0,0 +1 @@ + diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 5efa51739ccccce23bb9835b8ee99d644af60e9e..f522f05fdeb93ea0a2063f03f6da8827133e8648 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -44,7 +44,7 @@ use crate::{DEFAULT_THREAD_TITLE, ui::AcpOnboardingModal}; use crate::{ExpandMessageEditor, ThreadHistoryView}; use crate::{ManageProfiles, ThreadHistoryViewEvent}; use crate::{ThreadHistory, agent_connection_store::AgentConnectionStore}; -use agent_settings::AgentSettings; +use agent_settings::{AgentSettings, WindowLayout}; use ai_onboarding::AgentPanelOnboarding; use anyhow::{Context as _, Result, anyhow}; use client::UserStore; @@ -279,7 +279,7 @@ pub fn init(cx: &mut App) { if let Some(panel) = workspace.panel::(cx) { panel.update(cx, |panel, _| { panel - .on_boarding_upsell_dismissed + .new_user_onboarding_upsell_dismissed .store(false, Ordering::Release); }); } @@ -797,7 +797,10 @@ pub struct AgentPanel { _project_subscription: Subscription, zoomed: bool, pending_serialization: Option>>, - onboarding: Entity, + new_user_onboarding: Entity, + new_user_onboarding_upsell_dismissed: AtomicBool, + agent_layout_onboarding: Entity, + agent_layout_onboarding_dismissed: AtomicBool, selected_agent: Agent, start_thread_in: StartThreadIn, worktree_creation_status: Option, @@ -805,7 +808,6 @@ pub struct AgentPanel { _active_thread_focus_subscription: Option, _worktree_creation_task: Option>, show_trust_workspace_message: bool, - on_boarding_upsell_dismissed: AtomicBool, _active_view_observation: Option, } @@ -1039,18 +1041,55 @@ impl AgentPanel { client, move |_window, cx| { weak_panel - .update(cx, |panel, _| { - panel - .on_boarding_upsell_dismissed - .store(true, Ordering::Release); + .update(cx, |panel, cx| { + panel.dismiss_ai_onboarding(cx); }) .ok(); - OnboardingUpsell::set_dismissed(true, cx); }, cx, ) }); + let weak_panel = cx.entity().downgrade(); + + let layout = AgentSettings::get_layout(cx); + let is_agent_layout = matches!(layout, WindowLayout::Agent(_)); + + let agent_layout_onboarding = cx.new(|_cx| ai_onboarding::AgentLayoutOnboarding { + use_agent_layout: Arc::new({ + let fs = fs.clone(); + let weak_panel = weak_panel.clone(); + move |_window, cx| { + AgentSettings::set_layout(WindowLayout::Agent(None), fs.clone(), cx); + weak_panel + .update(cx, |panel, cx| { + panel.dismiss_agent_layout_onboarding(cx); + }) + .ok(); + } + }), + revert_to_editor_layout: Arc::new({ + let fs = fs.clone(); + let weak_panel = weak_panel.clone(); + move |_window, cx| { + AgentSettings::set_layout(WindowLayout::Editor(None), fs.clone(), cx); + weak_panel + .update(cx, |panel, cx| { + panel.dismiss_agent_layout_onboarding(cx); + }) + .ok(); + } + }), + dismissed: Arc::new(move |_window, cx| { + weak_panel + .update(cx, |panel, cx| { + panel.dismiss_agent_layout_onboarding(cx); + }) + .ok(); + }), + is_agent_layout, + }); + // Subscribe to extension events to sync agent servers when extensions change let extension_subscription = if let Some(extension_events) = ExtensionEvents::try_global(cx) { @@ -1115,7 +1154,8 @@ impl AgentPanel { _project_subscription, zoomed: false, pending_serialization: None, - onboarding, + new_user_onboarding: onboarding, + agent_layout_onboarding, thread_store, selected_agent: Agent::default(), start_thread_in: StartThreadIn::default(), @@ -1124,7 +1164,10 @@ impl AgentPanel { _active_thread_focus_subscription: None, _worktree_creation_task: None, show_trust_workspace_message: false, - on_boarding_upsell_dismissed: AtomicBool::new(OnboardingUpsell::dismissed(cx)), + new_user_onboarding_upsell_dismissed: AtomicBool::new(OnboardingUpsell::dismissed(cx)), + agent_layout_onboarding_dismissed: AtomicBool::new(AgentLayoutOnboarding::dismissed( + cx, + )), _active_view_observation: None, }; @@ -3973,8 +4016,66 @@ impl AgentPanel { plan.is_some_and(|plan| plan == Plan::ZedFree) && has_previous_trial } - fn should_render_onboarding(&self, cx: &mut Context) -> bool { - if self.on_boarding_upsell_dismissed.load(Ordering::Acquire) { + fn should_render_agent_layout_onboarding(&self, cx: &mut Context) -> bool { + // We only want to show this for existing users: those who + // have used the agent panel before the sidebar was introduced. + // We can infer that state by users having seen the onboarding + // at one point, but not the agent layout onboarding. + + let has_messages = self.active_thread_has_messages(cx); + let is_dismissed = self + .agent_layout_onboarding_dismissed + .load(Ordering::Acquire); + + if is_dismissed || has_messages { + return false; + } + + match &self.active_view { + ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => { + false + } + ActiveView::AgentThread { .. } => { + let existing_user = self + .new_user_onboarding_upsell_dismissed + .load(Ordering::Acquire); + existing_user + } + } + } + + fn render_agent_layout_onboarding( + &self, + _window: &mut Window, + cx: &mut Context, + ) -> Option { + if !self.should_render_agent_layout_onboarding(cx) { + return None; + } + + Some(div().child(self.agent_layout_onboarding.clone())) + } + + fn dismiss_agent_layout_onboarding(&mut self, cx: &mut Context) { + self.agent_layout_onboarding_dismissed + .store(true, Ordering::Release); + AgentLayoutOnboarding::set_dismissed(true, cx); + cx.notify(); + } + + fn dismiss_ai_onboarding(&mut self, cx: &mut Context) { + self.new_user_onboarding_upsell_dismissed + .store(true, Ordering::Release); + OnboardingUpsell::set_dismissed(true, cx); + self.dismiss_agent_layout_onboarding(cx); + cx.notify(); + } + + fn should_render_new_user_onboarding(&mut self, cx: &mut Context) -> bool { + if self + .new_user_onboarding_upsell_dismissed + .load(Ordering::Acquire) + { return false; } @@ -3986,9 +4087,12 @@ impl AgentPanel { .and_then(|period| period.0.checked_add_days(chrono::Days::new(1))) .is_some_and(|date| date < chrono::Utc::now()) { - OnboardingUpsell::set_dismissed(true, cx); - self.on_boarding_upsell_dismissed - .store(true, Ordering::Release); + if !self + .new_user_onboarding_upsell_dismissed + .load(Ordering::Acquire) + { + self.dismiss_ai_onboarding(cx); + } return false; } @@ -4017,16 +4121,20 @@ impl AgentPanel { } } - fn render_onboarding( - &self, + fn render_new_user_onboarding( + &mut self, _window: &mut Window, cx: &mut Context, ) -> Option { - if !self.should_render_onboarding(cx) { + if !self.should_render_new_user_onboarding(cx) { return None; } - Some(div().child(self.onboarding.clone())) + Some( + div() + .bg(cx.theme().colors().editor_background) + .child(self.new_user_onboarding.clone()), + ) } fn render_trial_end_upsell( @@ -4220,7 +4328,8 @@ impl Render for AgentPanel { })) .child(self.render_toolbar(window, cx)) .children(self.render_workspace_trust_message(cx)) - .children(self.render_onboarding(window, cx)) + .children(self.render_new_user_onboarding(window, cx)) + .children(self.render_agent_layout_onboarding(window, cx)) .map(|parent| match &self.active_view { ActiveView::Uninitialized => parent, ActiveView::AgentThread { @@ -4311,6 +4420,12 @@ impl Dismissable for OnboardingUpsell { const KEY: &'static str = "dismissed-trial-upsell"; } +struct AgentLayoutOnboarding; + +impl Dismissable for AgentLayoutOnboarding { + const KEY: &'static str = "dismissed-agent-layout-onboarding"; +} + struct TrialEndUpsell; impl Dismissable for TrialEndUpsell { diff --git a/crates/ai_onboarding/src/agent_panel_onboarding_card.rs b/crates/ai_onboarding/src/agent_panel_onboarding_card.rs index c63c5926428ab47f80afd2e157f90f8852dbf4ee..d4070b701ecc1a648a9e48f3c61a3289f3c38bc9 100644 --- a/crates/ai_onboarding/src/agent_panel_onboarding_card.rs +++ b/crates/ai_onboarding/src/agent_panel_onboarding_card.rs @@ -1,6 +1,6 @@ use gpui::{AnyElement, IntoElement, ParentElement, linear_color_stop, linear_gradient}; use smallvec::SmallVec; -use ui::{Vector, VectorName, prelude::*}; +use ui::prelude::*; #[derive(IntoElement)] pub struct AgentPanelOnboardingCard { @@ -23,61 +23,43 @@ impl ParentElement for AgentPanelOnboardingCard { impl RenderOnce for AgentPanelOnboardingCard { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - div() - .m_2p5() - .p(px(3.)) - .elevation_2(cx) - .rounded_lg() - .bg(cx.theme().colors().background.alpha(0.5)) - .child( - v_flex() - .relative() - .size_full() - .px_4() - .py_3() - .gap_2() - .border_1() - .rounded(px(5.)) - .border_color(cx.theme().colors().text.alpha(0.1)) - .overflow_hidden() - .bg(cx.theme().colors().panel_background) - .child( - div() - .opacity(0.5) - .absolute() - .top(px(-8.0)) - .right_0() - .w(px(400.)) - .h(px(92.)) - .rounded_md() - .child( - Vector::new( - VectorName::AiGrid, - rems_from_px(400.), - rems_from_px(92.), - ) - .color(Color::Custom(cx.theme().colors().text.alpha(0.32))), - ), - ) - .child( - div() - .absolute() - .top_0p5() - .right_0p5() - .w(px(660.)) - .h(px(401.)) - .overflow_hidden() - .rounded_md() - .bg(linear_gradient( - 75., - linear_color_stop( - cx.theme().colors().panel_background.alpha(0.01), - 1.0, - ), - linear_color_stop(cx.theme().colors().panel_background, 0.45), - )), - ) - .children(self.children), - ) + let color = cx.theme().colors(); + + div().min_w_0().p_2p5().bg(color.editor_background).child( + div() + .min_w_0() + .p(px(3.)) + .rounded_lg() + .elevation_2(cx) + .bg(color.background.opacity(0.5)) + .child( + v_flex() + .relative() + .size_full() + .min_w_0() + .px_4() + .py_3() + .gap_2() + .border_1() + .rounded(px(5.)) + .border_color(color.text.opacity(0.1)) + .bg(color.panel_background) + .overflow_hidden() + .child( + div() + .absolute() + .inset_0() + .size_full() + .rounded_md() + .overflow_hidden() + .bg(linear_gradient( + 360., + linear_color_stop(color.panel_background, 1.0), + linear_color_stop(color.editor_background, 0.45), + )), + ) + .children(self.children), + ), + ) } } diff --git a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs index cc60a35e501329b0ca089e2f218ab1551ca35d93..494177da3c5f1fecee1784dfac9ce88026544f5a 100644 --- a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs +++ b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs @@ -59,25 +59,26 @@ impl Render for AgentPanelOnboarding { .read(cx) .plan() .is_some_and(|plan| plan == Plan::ZedProTrial); + let is_pro_user = self .user_store .read(cx) .plan() .is_some_and(|plan| plan == Plan::ZedPro); + let onboarding = ZedAiOnboarding::new( + self.client.clone(), + &self.user_store, + self.continue_with_zed_ai.clone(), + cx, + ) + .with_dismiss({ + let callback = self.continue_with_zed_ai.clone(); + move |window, cx| callback(window, cx) + }); + AgentPanelOnboardingCard::new() - .child( - ZedAiOnboarding::new( - self.client.clone(), - &self.user_store, - self.continue_with_zed_ai.clone(), - cx, - ) - .with_dismiss({ - let callback = self.continue_with_zed_ai.clone(); - move |window, cx| callback(window, cx) - }), - ) + .child(onboarding) .map(|this| { if enrolled_in_trial || is_pro_user || self.has_configured_providers { this diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index e05853fa167267c505d4424365c29844e0ce08db..f51389b8ca25b22798fc1ae9dc2ceee81d60110a 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -1,7 +1,6 @@ mod agent_api_keys_onboarding; mod agent_panel_onboarding_card; mod agent_panel_onboarding_content; -mod ai_upsell_card; mod edit_prediction_onboarding_content; mod plan_definitions; mod young_account_banner; @@ -9,7 +8,6 @@ mod young_account_banner; pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProviders}; pub use agent_panel_onboarding_card::AgentPanelOnboardingCard; pub use agent_panel_onboarding_content::AgentPanelOnboarding; -pub use ai_upsell_card::AiUpsellCard; use cloud_api_types::Plan; pub use edit_prediction_onboarding_content::EditPredictionOnboarding; pub use plan_definitions::PlanDefinitions; @@ -19,7 +17,9 @@ use std::sync::Arc; use client::{Client, UserStore, zed_urls}; use gpui::{AnyElement, Entity, IntoElement, ParentElement}; -use ui::{Divider, RegisterComponent, Tooltip, prelude::*}; +use ui::{ + Divider, List, ListBulletItem, RegisterComponent, Tooltip, Vector, VectorName, prelude::*, +}; #[derive(PartialEq)] pub enum SignInStatus { @@ -84,6 +84,50 @@ impl ZedAiOnboarding { self } + fn certified_user_stamp(cx: &App) -> impl IntoElement { + div().absolute().bottom_1().right_1().child( + Vector::new( + VectorName::ProUserStamp, + rems_from_px(156.), + rems_from_px(60.), + ) + .color(Color::Custom(cx.theme().colors().text_accent.alpha(0.8))), + ) + } + + fn pro_trial_stamp(cx: &App) -> impl IntoElement { + div().absolute().bottom_1().right_1().child( + Vector::new( + VectorName::ProTrialStamp, + rems_from_px(156.), + rems_from_px(60.), + ) + .color(Color::Custom(cx.theme().colors().text.alpha(0.8))), + ) + } + + fn business_stamp(cx: &App) -> impl IntoElement { + div().absolute().bottom_1().right_1().child( + Vector::new( + VectorName::BusinessStamp, + rems_from_px(156.), + rems_from_px(60.), + ) + .color(Color::Custom(cx.theme().colors().text_accent.alpha(0.8))), + ) + } + + fn student_stamp(cx: &App) -> impl IntoElement { + div().absolute().bottom_1().right_1().child( + Vector::new( + VectorName::StudentStamp, + rems_from_px(156.), + rems_from_px(60.), + ) + .color(Color::Custom(cx.theme().colors().text.alpha(0.8))), + ) + } + fn render_dismiss_button(&self) -> Option { self.dismiss_onboarding.as_ref().map(|dismiss_callback| { let callback = dismiss_callback.clone(); @@ -109,6 +153,7 @@ impl ZedAiOnboarding { let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn); v_flex() + .w_full() .relative() .gap_1() .child(Headline::new("Welcome to Zed AI")) @@ -139,7 +184,7 @@ impl ZedAiOnboarding { if self.account_too_young { v_flex() .relative() - .max_w_full() + .min_w_0() .gap_1() .child(Headline::new("Welcome to Zed AI")) .child(YoungAccountBanner) @@ -175,6 +220,7 @@ impl ZedAiOnboarding { .into_any_element() } else { v_flex() + .w_full() .relative() .gap_1() .child(Headline::new("Welcome to Zed AI")) @@ -237,10 +283,12 @@ impl ZedAiOnboarding { } } - fn render_trial_state(&self, _cx: &mut App) -> AnyElement { + fn render_trial_state(&self, cx: &mut App) -> AnyElement { v_flex() + .w_full() .relative() .gap_1() + .child(Self::pro_trial_stamp(cx)) .child(Headline::new("Welcome to the Zed Pro Trial")) .child( Label::new("Here's what you get for the next 14 days:") @@ -252,9 +300,12 @@ impl ZedAiOnboarding { .into_any_element() } - fn render_pro_plan_state(&self, _cx: &mut App) -> AnyElement { + fn render_pro_plan_state(&self, cx: &mut App) -> AnyElement { v_flex() + .w_full() + .relative() .gap_1() + .child(Self::certified_user_stamp(cx)) .child(Headline::new("Welcome to Zed Pro")) .child( Label::new("Here's what you get:") @@ -266,9 +317,12 @@ impl ZedAiOnboarding { .into_any_element() } - fn render_business_plan_state(&self, _cx: &mut App) -> AnyElement { + fn render_business_plan_state(&self, cx: &mut App) -> AnyElement { v_flex() + .w_full() + .relative() .gap_1() + .child(Self::business_stamp(cx)) .child(Headline::new("Welcome to Zed Business")) .child( Label::new("Here's what you get:") @@ -280,9 +334,12 @@ impl ZedAiOnboarding { .into_any_element() } - fn render_student_plan_state(&self, _cx: &mut App) -> AnyElement { + fn render_student_plan_state(&self, cx: &mut App) -> AnyElement { v_flex() + .w_full() + .relative() .gap_1() + .child(Self::student_stamp(cx)) .child(Headline::new("Welcome to Zed Student")) .child( Label::new("Here's what you get:") @@ -318,11 +375,7 @@ impl Component for ZedAiOnboarding { } fn name() -> &'static str { - "Agent Panel Banners" - } - - fn sort_name() -> &'static str { - "Agent Panel Banners" + "Agent New User Onboarding" } fn preview(_window: &mut Window, _cx: &mut App) -> Option { @@ -331,22 +384,30 @@ impl Component for ZedAiOnboarding { plan: Option, account_too_young: bool, ) -> AnyElement { - ZedAiOnboarding { - sign_in_status, - plan, - account_too_young, - continue_with_zed_ai: Arc::new(|_, _| {}), - sign_in: Arc::new(|_, _| {}), - dismiss_onboarding: None, - } - .into_any_element() + div() + .w_full() + .min_w_40() + .max_w(px(1100.)) + .child( + AgentPanelOnboardingCard::new().child( + ZedAiOnboarding { + sign_in_status, + plan, + account_too_young, + continue_with_zed_ai: Arc::new(|_, _| {}), + sign_in: Arc::new(|_, _| {}), + dismiss_onboarding: None, + } + .into_any_element(), + ), + ) + .into_any_element() } Some( v_flex() + .min_w_0() .gap_4() - .items_center() - .max_w_4_5() .children(vec![ single_example( "Not Signed-in", @@ -381,3 +442,119 @@ impl Component for ZedAiOnboarding { ) } } + +#[derive(RegisterComponent)] +pub struct AgentLayoutOnboarding { + pub use_agent_layout: Arc, + pub revert_to_editor_layout: Arc, + pub dismissed: Arc, + pub is_agent_layout: bool, +} + +impl Render for AgentLayoutOnboarding { + fn render(&mut self, _window: &mut ui::Window, _cx: &mut Context) -> impl IntoElement { + let description = "The new threads sidebar, positioned in the far left of your workspace, allows you to manage agents across many projects. Your agent thread lives alongside it, and all other panels live on the right."; + + let dismiss_button = div().absolute().top_1().right_1().child( + IconButton::new("dismiss", IconName::Close) + .icon_size(IconSize::Small) + .on_click({ + let dismiss = self.dismissed.clone(); + move |_, window, cx| { + telemetry::event!("Agentic Layout Onboarding Dismissed"); + dismiss(window, cx) + } + }), + ); + + let primary_button = if self.is_agent_layout { + Button::new("revert", "Use Previous Layout") + .label_size(LabelSize::Small) + .style(ButtonStyle::Outlined) + .on_click({ + let revert = self.revert_to_editor_layout.clone(); + let dismiss = self.dismissed.clone(); + move |_, window, cx| { + telemetry::event!("Clicked to Use Previous Layout"); + revert(window, cx); + dismiss(window, cx); + } + }) + } else { + Button::new("start", "Use New Layout") + .label_size(LabelSize::Small) + .style(ButtonStyle::Outlined) + .on_click({ + let use_layout = self.use_agent_layout.clone(); + let dismiss = self.dismissed.clone(); + move |_, window, cx| { + telemetry::event!("Clicked to Use New Layout"); + use_layout(window, cx); + dismiss(window, cx); + } + }) + }; + + let content = v_flex() + .min_w_0() + .w_full() + .relative() + .gap_1() + .child(Label::new("A new workspace layout for agentic work")) + .child(Label::new(description).color(Color::Muted).mb_2()) + .child( + List::new() + .child(ListBulletItem::new("Use your favorite agents in parallel")) + .child(ListBulletItem::new("Isolate agents using worktrees")) + .child(ListBulletItem::new( + "Combine multiple projects in one window", + )), + ) + .child( + h_flex() + .w_full() + .gap_1() + .flex_wrap() + .justify_end() + .child(primary_button), + ) + .child(dismiss_button); + + AgentPanelOnboardingCard::new().child(content) + } +} + +impl Component for AgentLayoutOnboarding { + fn scope() -> ComponentScope { + ComponentScope::Onboarding + } + + fn name() -> &'static str { + "Agent Layout Onboarding" + } + + fn preview(_window: &mut Window, cx: &mut App) -> Option { + let onboarding = cx.new(|_cx| AgentLayoutOnboarding { + use_agent_layout: Arc::new(|_, _| {}), + revert_to_editor_layout: Arc::new(|_, _| {}), + dismissed: Arc::new(|_, _| {}), + is_agent_layout: false, + }); + + Some( + v_flex() + .min_w_0() + .gap_4() + .child(single_example( + "Agent Layout Onboarding", + div() + .w_full() + .min_w_40() + .max_w(px(1100.)) + .child(onboarding) + .into_any_element(), + )) + .into_any_element(), + ) + } +} diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs deleted file mode 100644 index cbaa9785db9e5471dd76a3add2cb9f19ca1b7ae1..0000000000000000000000000000000000000000 --- a/crates/ai_onboarding/src/ai_upsell_card.rs +++ /dev/null @@ -1,407 +0,0 @@ -use std::sync::Arc; - -use client::{Client, UserStore, zed_urls}; -use cloud_api_types::Plan; -use gpui::{AnyElement, App, Entity, IntoElement, RenderOnce, Window}; -use ui::{CommonAnimationExt, Divider, Vector, VectorName, prelude::*}; - -use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions}; - -#[derive(IntoElement, RegisterComponent)] -pub struct AiUpsellCard { - sign_in_status: SignInStatus, - sign_in: Arc, - account_too_young: bool, - user_plan: Option, - tab_index: Option, -} - -impl AiUpsellCard { - pub fn new( - client: Arc, - user_store: &Entity, - user_plan: Option, - cx: &mut App, - ) -> Self { - let status = *client.status().borrow(); - let store = user_store.read(cx); - - Self { - user_plan, - sign_in_status: status.into(), - sign_in: Arc::new(move |_window, cx| { - cx.spawn({ - let client = client.clone(); - async move |cx| client.sign_in_with_optional_connect(true, cx).await - }) - .detach_and_log_err(cx); - }), - account_too_young: store.account_too_young(), - tab_index: None, - } - } - - pub fn tab_index(mut self, tab_index: Option) -> Self { - self.tab_index = tab_index; - self - } -} - -impl RenderOnce for AiUpsellCard { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let pro_section = v_flex() - .flex_grow() - .w_full() - .gap_1() - .child( - h_flex() - .gap_2() - .child( - Label::new("Pro") - .size(LabelSize::Small) - .color(Color::Accent) - .buffer_font(cx), - ) - .child(Divider::horizontal()), - ) - .child(PlanDefinitions.pro_plan()); - - let free_section = v_flex() - .flex_grow() - .w_full() - .gap_1() - .child( - h_flex() - .gap_2() - .child( - Label::new("Free") - .size(LabelSize::Small) - .color(Color::Muted) - .buffer_font(cx), - ) - .child(Divider::horizontal()), - ) - .child(PlanDefinitions.free_plan()); - - let grid_bg = h_flex() - .absolute() - .inset_0() - .w_full() - .h(px(240.)) - .bg(gpui::pattern_slash( - cx.theme().colors().border.opacity(0.1), - 2., - 25., - )); - - let gradient_bg = div() - .absolute() - .inset_0() - .size_full() - .bg(gpui::linear_gradient( - 180., - gpui::linear_color_stop( - cx.theme().colors().elevated_surface_background.opacity(0.8), - 0., - ), - gpui::linear_color_stop( - cx.theme().colors().elevated_surface_background.opacity(0.), - 0.8, - ), - )); - - let description = PlanDefinitions::AI_DESCRIPTION; - - let card = v_flex() - .relative() - .flex_grow() - .p_4() - .pt_3() - .border_1() - .border_color(cx.theme().colors().border) - .rounded_lg() - .overflow_hidden() - .child(grid_bg) - .child(gradient_bg); - - let plans_section = h_flex() - .w_full() - .mt_1p5() - .mb_2p5() - .items_start() - .gap_6() - .child(free_section) - .child(pro_section); - - let footer_container = v_flex().items_center().gap_1(); - - let certified_user_stamp = div() - .absolute() - .top_2() - .right_2() - .size(rems_from_px(72.)) - .child( - Vector::new( - VectorName::ProUserStamp, - rems_from_px(72.), - rems_from_px(72.), - ) - .color(Color::Custom(cx.theme().colors().text_accent.alpha(0.3))) - .with_rotate_animation(10), - ); - - let pro_trial_stamp = div() - .absolute() - .top_2() - .right_2() - .size(rems_from_px(72.)) - .child( - Vector::new( - VectorName::ProTrialStamp, - rems_from_px(72.), - rems_from_px(72.), - ) - .color(Color::Custom(cx.theme().colors().text.alpha(0.2))), - ); - - match self.sign_in_status { - SignInStatus::SignedIn => match self.user_plan { - None | Some(Plan::ZedFree) => card - .child(Label::new("Try Zed AI").size(LabelSize::Large)) - .map(|this| { - if self.account_too_young { - this.child(YoungAccountBanner).child( - v_flex() - .mt_2() - .gap_1() - .child( - h_flex() - .gap_2() - .child( - Label::new("Pro") - .size(LabelSize::Small) - .color(Color::Accent) - .buffer_font(cx), - ) - .child(Divider::horizontal()), - ) - .child(PlanDefinitions.pro_plan()) - .child( - Button::new("pro", "Get Started") - .full_width() - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .on_click(move |_, _window, cx| { - telemetry::event!( - "Upgrade To Pro Clicked", - state = "young-account" - ); - cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)) - }), - ), - ) - } else { - this.child( - div() - .max_w_3_4() - .mb_2() - .child(Label::new(description).color(Color::Muted)), - ) - .child(plans_section) - .child( - footer_container - .child( - Button::new("start_trial", "Start Pro Trial") - .full_width() - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .when_some(self.tab_index, |this, tab_index| { - this.tab_index(tab_index) - }) - .on_click(move |_, _window, cx| { - telemetry::event!( - "Start Trial Clicked", - state = "post-sign-in" - ); - cx.open_url(&zed_urls::start_trial_url(cx)) - }), - ) - .child( - Label::new("14 days, no credit card required") - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) - } - }), - Some(Plan::ZedProTrial) => card - .child(pro_trial_stamp) - .child(Label::new("You're in the Zed Pro Trial").size(LabelSize::Large)) - .child( - Label::new("Here's what you get for the next 14 days:") - .color(Color::Muted) - .mb_2(), - ) - .child(PlanDefinitions.pro_trial(false)), - Some(Plan::ZedPro) => card - .child(certified_user_stamp) - .child(Label::new("You're in the Zed Pro plan").size(LabelSize::Large)) - .child( - Label::new("Here's what you get:") - .color(Color::Muted) - .mb_2(), - ) - .child(PlanDefinitions.pro_plan()), - Some(Plan::ZedBusiness) => card - .child(certified_user_stamp) - .child(Label::new("You're in the Zed Business plan").size(LabelSize::Large)) - .child( - Label::new("Here's what you get:") - .color(Color::Muted) - .mb_2(), - ) - .child(PlanDefinitions.business_plan()), - Some(Plan::ZedStudent) => card - .child(certified_user_stamp) - .child(Label::new("You're in the Zed Student plan").size(LabelSize::Large)) - .child( - Label::new("Here's what you get:") - .color(Color::Muted) - .mb_2(), - ) - .child(PlanDefinitions.student_plan()), - }, - // Signed Out State - _ => card - .child(Label::new("Try Zed AI").size(LabelSize::Large)) - .child( - div() - .max_w_3_4() - .mb_2() - .child(Label::new(description).color(Color::Muted)), - ) - .child(plans_section) - .child( - Button::new("sign_in", "Sign In") - .full_width() - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)) - .on_click({ - let callback = self.sign_in.clone(); - move |_, window, cx| { - telemetry::event!("Start Trial Clicked", state = "pre-sign-in"); - callback(window, cx) - } - }), - ), - } - } -} - -impl Component for AiUpsellCard { - fn scope() -> ComponentScope { - ComponentScope::Onboarding - } - - fn name() -> &'static str { - "AI Upsell Card" - } - - fn sort_name() -> &'static str { - "AI Upsell Card" - } - - fn description() -> Option<&'static str> { - Some("A card presenting the Zed AI product during user's first-open onboarding flow.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_4() - .items_center() - .max_w_4_5() - .child(single_example( - "Signed Out State", - AiUpsellCard { - sign_in_status: SignInStatus::SignedOut, - sign_in: Arc::new(|_, _| {}), - account_too_young: false, - user_plan: None, - tab_index: Some(0), - } - .into_any_element(), - )) - .child(example_group_with_title( - "Signed In States", - vec![ - single_example( - "Free Plan", - AiUpsellCard { - sign_in_status: SignInStatus::SignedIn, - sign_in: Arc::new(|_, _| {}), - account_too_young: false, - user_plan: Some(Plan::ZedFree), - tab_index: Some(1), - } - .into_any_element(), - ), - single_example( - "Free Plan but Young Account", - AiUpsellCard { - sign_in_status: SignInStatus::SignedIn, - sign_in: Arc::new(|_, _| {}), - account_too_young: true, - user_plan: Some(Plan::ZedFree), - tab_index: Some(1), - } - .into_any_element(), - ), - single_example( - "Pro Trial", - AiUpsellCard { - sign_in_status: SignInStatus::SignedIn, - sign_in: Arc::new(|_, _| {}), - account_too_young: false, - user_plan: Some(Plan::ZedProTrial), - tab_index: Some(1), - } - .into_any_element(), - ), - single_example( - "Pro Plan", - AiUpsellCard { - sign_in_status: SignInStatus::SignedIn, - sign_in: Arc::new(|_, _| {}), - account_too_young: false, - user_plan: Some(Plan::ZedPro), - tab_index: Some(1), - } - .into_any_element(), - ), - single_example( - "Business Plan", - AiUpsellCard { - sign_in_status: SignInStatus::SignedIn, - sign_in: Arc::new(|_, _| {}), - account_too_young: false, - user_plan: Some(Plan::ZedBusiness), - tab_index: Some(1), - } - .into_any_element(), - ), - single_example( - "Student Plan", - AiUpsellCard { - sign_in_status: SignInStatus::SignedIn, - sign_in: Arc::new(|_, _| {}), - account_too_young: false, - user_plan: Some(Plan::ZedStudent), - tab_index: Some(1), - } - .into_any_element(), - ), - ], - )) - .into_any_element(), - ) - } -} diff --git a/crates/ai_onboarding/src/plan_definitions.rs b/crates/ai_onboarding/src/plan_definitions.rs index 184815bcad9babb1892335c6207a79e1fe193c04..cc80b5ccf6d3d6ad06e7b3cf693356dbad3ce541 100644 --- a/crates/ai_onboarding/src/plan_definitions.rs +++ b/crates/ai_onboarding/src/plan_definitions.rs @@ -5,23 +5,19 @@ use ui::{List, ListBulletItem, prelude::*}; pub struct PlanDefinitions; impl PlanDefinitions { - pub const AI_DESCRIPTION: &'static str = "Zed offers a complete agentic experience, with robust editing and reviewing features to collaborate with AI."; - pub fn free_plan(&self) -> impl IntoElement { List::new() .child(ListBulletItem::new("2,000 accepted edit predictions")) .child(ListBulletItem::new( "Unlimited prompts with your AI API keys", )) - .child(ListBulletItem::new( - "Unlimited use of external agents like Claude Agent", - )) + .child(ListBulletItem::new("Unlimited use of external agents")) } pub fn pro_trial(&self, period: bool) -> impl IntoElement { List::new() + .child(ListBulletItem::new("$20 of tokens in Zed agent")) .child(ListBulletItem::new("Unlimited edit predictions")) - .child(ListBulletItem::new("$20 of tokens")) .when(period, |this| { this.child(ListBulletItem::new( "Try it out for 14 days, no credit card required", @@ -31,9 +27,9 @@ impl PlanDefinitions { pub fn pro_plan(&self) -> impl IntoElement { List::new() - .child(ListBulletItem::new("Unlimited edit predictions")) - .child(ListBulletItem::new("$5 of tokens")) + .child(ListBulletItem::new("$5 of tokens in Zed agent")) .child(ListBulletItem::new("Usage-based billing beyond $5")) + .child(ListBulletItem::new("Unlimited edit predictions")) } pub fn business_plan(&self) -> impl IntoElement { @@ -45,7 +41,7 @@ impl PlanDefinitions { pub fn student_plan(&self) -> impl IntoElement { List::new() .child(ListBulletItem::new("Unlimited edit predictions")) - .child(ListBulletItem::new("$10 of tokens")) + .child(ListBulletItem::new("$10 of tokens in Zed agent")) .child(ListBulletItem::new( "Optional credit packs for additional usage", )) diff --git a/crates/auto_update_ui/Cargo.toml b/crates/auto_update_ui/Cargo.toml index 76d2c7210f68711646758e809825bca73dccdc1d..b7b51c4a28448434ec4483f898e2d67b3301533e 100644 --- a/crates/auto_update_ui/Cargo.toml +++ b/crates/auto_update_ui/Cargo.toml @@ -12,9 +12,12 @@ workspace = true path = "src/auto_update_ui.rs" [dependencies] +agent_settings.workspace = true anyhow.workspace = true auto_update.workspace = true client.workspace = true +db.workspace = true +fs.workspace = true editor.workspace = true gpui.workspace = true markdown_preview.workspace = true @@ -23,6 +26,8 @@ semver.workspace = true serde.workspace = true serde_json.workspace = true smol.workspace = true +telemetry.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true +zed_actions.workspace = true diff --git a/crates/auto_update_ui/src/auto_update_ui.rs b/crates/auto_update_ui/src/auto_update_ui.rs index e613d3af68875267f6a678505b83d605b9f8425c..4ff5df72bab2539218f546444be015d63fa97712 100644 --- a/crates/auto_update_ui/src/auto_update_ui.rs +++ b/crates/auto_update_ui/src/auto_update_ui.rs @@ -1,5 +1,10 @@ +use std::sync::Arc; + +use agent_settings::{AgentSettings, WindowLayout}; use auto_update::{AutoUpdater, release_notes_url}; +use db::kvp::Dismissable; use editor::{Editor, MultiBuffer}; +use fs::Fs; use gpui::{ App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Window, actions, prelude::*, }; @@ -8,10 +13,10 @@ use release_channel::{AppVersion, ReleaseChannel}; use semver::Version; use serde::Deserialize; use smol::io::AsyncReadExt; -use ui::{AnnouncementToast, ListBulletItem, prelude::*}; +use ui::{AnnouncementToast, ListBulletItem, ParallelAgentsIllustration, prelude::*}; use util::{ResultExt as _, maybe}; use workspace::{ - Workspace, + ToggleWorkspaceSidebar, Workspace, notifications::{ ErrorMessagePrompt, Notification, NotificationId, SuppressEvent, show_app_notification, simple_message_notification::MessageNotification, @@ -169,23 +174,52 @@ struct AnnouncementContent { bullet_items: Vec, primary_action_label: SharedString, primary_action_url: Option, + primary_action_callback: Option>, + secondary_action_url: Option, + on_dismiss: Option>, +} + +struct ParallelAgentAnnouncement; + +impl Dismissable for ParallelAgentAnnouncement { + const KEY: &'static str = "parallel-agent-announcement"; } -fn announcement_for_version(version: &Version) -> Option { - #[allow(clippy::match_single_binding)] +fn announcement_for_version(version: &Version, cx: &App) -> Option { match (version.major, version.minor, version.patch) { - // TODO: Add real version when we have it - // (0, 225, 0) => Some(AnnouncementContent { - // heading: "What's new in Zed 0.225".into(), - // description: "This release includes some exciting improvements.".into(), - // bullet_items: vec![ - // "Improved agent performance".into(), - // "New agentic features".into(), - // "Better agent capabilities".into(), - // ], - // primary_action_label: "Learn More".into(), - // primary_action_url: Some("https://zed.dev/".into()), - // }), + (0, 232, _) => { + if ParallelAgentAnnouncement::dismissed(cx) { + None + } else { + let fs = ::global(cx); + let already_agent_layout = + matches!(AgentSettings::get_layout(cx), WindowLayout::Agent(_)); + + Some(AnnouncementContent { + heading: "Introducing Parallel Agents".into(), + description: "Run multiple agent threads simultaneously across projects." + .into(), + bullet_items: vec![ + "Mix and match Zed's agent with any ACP-compatible agent".into(), + "Optional worktree isolation keeps agents from conflicting".into(), + "Updated workspace layout designed for agentic workflows".into(), + ], + primary_action_label: "Try Now".into(), + primary_action_url: None, + primary_action_callback: Some(Arc::new(move |window, cx| { + if !already_agent_layout { + AgentSettings::set_layout(WindowLayout::Agent(None), fs.clone(), cx); + } + window.dispatch_action(Box::new(ToggleWorkspaceSidebar), cx); + window.dispatch_action(Box::new(zed_actions::assistant::ToggleFocus), cx); + })), + on_dismiss: Some(Arc::new(|cx| { + ParallelAgentAnnouncement::set_dismissed(true, cx) + })), + secondary_action_url: Some("https://zed.dev/blog/".into()), + }) + } + } _ => None, } } @@ -202,6 +236,13 @@ impl AnnouncementToastNotification { content, } } + + fn dismiss(&mut self, cx: &mut Context) { + cx.emit(DismissEvent); + if let Some(on_dismiss) = &self.content.on_dismiss { + on_dismiss(cx); + } + } } impl Focusable for AnnouncementToastNotification { @@ -217,6 +258,7 @@ impl Notification for AnnouncementToastNotification {} impl Render for AnnouncementToastNotification { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { AnnouncementToast::new() + .illustration(ParallelAgentsIllustration::new()) .heading(self.content.heading.clone()) .description(self.content.description.clone()) .bullet_items( @@ -228,24 +270,31 @@ impl Render for AnnouncementToastNotification { .primary_action_label(self.content.primary_action_label.clone()) .primary_on_click(cx.listener({ let url = self.content.primary_action_url.clone(); - move |_, _, _window, cx| { + let callback = self.content.primary_action_callback.clone(); + move |this, _, window, cx| { + telemetry::event!("Parallel Agent Announcement Main Click"); + if let Some(callback) = &callback { + callback(window, cx); + } if let Some(url) = &url { cx.open_url(url); } - cx.emit(DismissEvent); + this.dismiss(cx); } })) .secondary_on_click(cx.listener({ - let url = self.content.primary_action_url.clone(); - move |_, _, _window, cx| { + let url = self.content.secondary_action_url.clone(); + move |this, _, _window, cx| { + telemetry::event!("Parallel Agent Announcement Secondary Click"); if let Some(url) = &url { cx.open_url(url); } - cx.emit(DismissEvent); + this.dismiss(cx); } })) - .dismiss_on_click(cx.listener(|_, _, _window, cx| { - cx.emit(DismissEvent); + .dismiss_on_click(cx.listener(|this, _, _window, cx| { + telemetry::event!("Parallel Agent Announcement Dismiss"); + this.dismiss(cx); })) } } @@ -274,7 +323,7 @@ pub fn notify_if_app_was_updated(cx: &mut App) { version.build = semver::BuildMetadata::EMPTY; let app_name = ReleaseChannel::global(cx).display_name(); - if let Some(content) = announcement_for_version(&version) { + if let Some(content) = announcement_for_version(&version, cx) { show_app_notification( NotificationId::unique::(), cx, diff --git a/crates/edit_prediction/src/onboarding_modal.rs b/crates/edit_prediction/src/onboarding_modal.rs index a09310d3df20b40cbd1b3ce6a52b2cbf376f7216..0147fb75bdd5e621ed7b84ba1f08481e42287864 100644 --- a/crates/edit_prediction/src/onboarding_modal.rs +++ b/crates/edit_prediction/src/onboarding_modal.rs @@ -11,7 +11,7 @@ use gpui::{ }; use language::language_settings::EditPredictionProvider; use settings::update_settings_file; -use ui::{Vector, VectorName, prelude::*}; +use ui::prelude::*; use workspace::{ModalView, Workspace}; #[macro_export] @@ -119,6 +119,7 @@ impl Render for ZedPredictModal { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let window_height = window.viewport_size().height; let max_height = window_height - px(200.); + let color = cx.theme().colors(); v_flex() .id("edit-prediction-onboarding") @@ -127,7 +128,7 @@ impl Render for ZedPredictModal { .w(px(550.)) .h_full() .max_h(max_height) - .p_4() + .p_1() .gap_2() .elevation_3(cx) .track_focus(&self.focus_handle(cx)) @@ -142,32 +143,19 @@ impl Render for ZedPredictModal { })) .child( div() - .opacity(0.5) - .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(Color::Custom(cx.theme().colors().text.alpha(0.32))), - ), - ) - .child( - div() - .absolute() - .top_0() - .right_0() - .w(px(660.)) - .h(px(401.)) - .overflow_hidden() + .p_3() + .size_full() + .border_1() + .border_color(cx.theme().colors().border) + .rounded(px(5.)) .bg(linear_gradient( - 75., - linear_color_stop(cx.theme().colors().panel_background.alpha(0.01), 1.0), - linear_color_stop(cx.theme().colors().panel_background, 0.45), - )), + 360., + linear_color_stop(color.panel_background, 1.0), + linear_color_stop(color.editor_background, 0.45), + )) + .child(self.onboarding.clone()), ) - .child(h_flex().absolute().top_2().right_2().child( + .child(h_flex().absolute().top_3().right_3().child( IconButton::new("cancel", IconName::Close).on_click(cx.listener( |_, _: &ClickEvent, _window, cx| { onboarding_event!("Cancelled", trigger = "X click"); @@ -175,6 +163,5 @@ impl Render for ZedPredictModal { }, )), )) - .child(self.onboarding.clone()) } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index f9885f634d962b167bcf32cc459d5bf6e0d5661e..5d39998700ff41433b3f8d2281d3f00892165794 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -61,7 +61,7 @@ use crate::util::atomic_incr_if_not_zero; pub use prompts::*; /// Default window size used when no explicit size is provided. -pub const DEFAULT_WINDOW_SIZE: Size = size(px(1536.), px(864.)); +pub const DEFAULT_WINDOW_SIZE: Size = size(px(1536.), px(1095.)); /// A 6:5 aspect ratio minimum window size to be used for functional, /// additional-to-main-Zed windows, like the settings and rules library windows. diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index 545a4b614160054186d4acf7bce17e36ac1cd4f1..0092b440a263a0b1cfb098225c822ade16a3dfd3 100644 --- a/crates/onboarding/Cargo.toml +++ b/crates/onboarding/Cargo.toml @@ -17,6 +17,8 @@ default = [] [dependencies] anyhow.workspace = true client.workspace = true +cloud_api_types.workspace = true +collections.workspace = true component.workspace = true db.workspace = true documented.workspace = true diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index b2e595b28a33ed4ee7f066c4d969baffdb2a081b..4817e897f7715de09f096619957675850e311b25 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -1,15 +1,23 @@ use std::sync::Arc; +use std::time::Duration; -use client::TelemetrySettings; +use client::{Client, TelemetrySettings, UserStore, zed_urls}; +use cloud_api_types::Plan; +use collections::HashMap; use fs::Fs; -use gpui::{Action, App, IntoElement}; +use gpui::{Action, Animation, AnimationExt, App, Entity, IntoElement, pulsating_between}; +use project::agent_server_store::AllAgentServersSettings; use project::project_settings::ProjectSettings; -use settings::{BaseKeymap, Settings, update_settings_file}; +use project::{AgentRegistryStore, RegistryAgent}; +use settings::{ + BaseKeymap, CustomAgentServerSettings, Settings, SettingsStore, update_settings_file, +}; use theme::{Appearance, SystemAppearance, ThemeRegistry}; use theme_settings::{ThemeAppearanceMode, ThemeName, ThemeSelection, ThemeSettings}; use ui::{ - Divider, StatefulInteractiveElement, SwitchField, TintColor, ToggleButtonGroup, - ToggleButtonGroupSize, ToggleButtonSimple, ToggleButtonWithIcon, Tooltip, prelude::*, + AgentSetupButton, Divider, StatefulInteractiveElement, SwitchField, TintColor, + ToggleButtonGroup, ToggleButtonGroupSize, ToggleButtonSimple, ToggleButtonWithIcon, Tooltip, + prelude::*, }; use vim_mode_setting::VimModeSetting; @@ -86,7 +94,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement ) .child( h_flex() - .gap_4() + .gap_2() .justify_between() .children(render_theme_previews(tab_index, &theme_selection, cx)), ); @@ -520,13 +528,182 @@ fn render_import_settings_section(tab_index: &mut isize, cx: &mut App) -> impl I .child(h_flex().gap_1().child(vscode).child(cursor)) } -pub(crate) fn render_basics_page(cx: &mut App) -> impl IntoElement { +const FEATURED_AGENT_IDS: &[&str] = &["claude-acp", "codex-acp", "github-copilot-cli", "cursor"]; + +fn render_registry_agent_button( + agent: &RegistryAgent, + installed: bool, + cx: &mut App, +) -> impl IntoElement { + let agent_id = agent.id().to_string(); + let element_id = format!("{}-onboarding", agent_id); + + let icon = match agent.icon_path() { + Some(icon_path) => Icon::from_external_svg(icon_path.clone()), + None => Icon::new(IconName::Sparkle), + } + .size(IconSize::XSmall) + .color(Color::Muted); + + let fs = ::global(cx); + + let state_element = if installed { + Icon::new(IconName::Check) + .size(IconSize::Small) + .color(Color::Success) + .into_any_element() + } else { + Label::new("Install") + .size(LabelSize::XSmall) + .color(Color::Muted) + .into_any_element() + }; + + AgentSetupButton::new(element_id) + .icon(icon) + .name(agent.name().clone()) + .state(state_element) + .disabled(installed) + .on_click(move |_, _, cx| { + let agent_id = agent_id.clone(); + update_settings_file(fs.clone(), cx, move |settings, _| { + let agent_servers = settings.agent_servers.get_or_insert_default(); + agent_servers.entry(agent_id).or_insert_with(|| { + CustomAgentServerSettings::Registry { + env: Default::default(), + default_mode: None, + default_model: None, + favorite_models: Vec::new(), + default_config_options: HashMap::default(), + favorite_config_option_values: HashMap::default(), + } + }); + }); + }) +} + +fn render_zed_agent_button(user_store: &Entity, cx: &mut App) -> impl IntoElement { + let client = Client::global(cx); + let status = *client.status().borrow(); + + let plan = user_store.read(cx).plan(); + let is_free = matches!(plan, Some(Plan::ZedFree) | None); + let is_pro = matches!(plan, Some(Plan::ZedPro)); + let is_trial = matches!(plan, Some(Plan::ZedProTrial)); + + let is_signed_out = status.is_signed_out() + || matches!( + status, + client::Status::AuthenticationError | client::Status::ConnectionError + ); + let is_signing_in = status.is_signing_in(); + let is_signed_in = !is_signed_out; + + let state_element = if is_signed_out { + Label::new("Sign In") + .size(LabelSize::XSmall) + .color(Color::Muted) + .into_any_element() + } else if is_signing_in { + Label::new("Signing In…") + .size(LabelSize::XSmall) + .color(Color::Muted) + .with_animation( + "signing-in", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.4, 0.8)), + |label, delta| label.alpha(delta), + ) + .into_any_element() + } else if is_signed_in && is_free { + Label::new("Start Free Trial") + .size(LabelSize::XSmall) + .color(Color::Muted) + .into_any_element() + } else { + Icon::new(IconName::Check) + .size(IconSize::Small) + .color(Color::Success) + .into_any_element() + }; + + AgentSetupButton::new("zed-agent-onboarding") + .icon( + Icon::new(IconName::ZedAgent) + .size(IconSize::XSmall) + .color(Color::Muted), + ) + .name("Zed Agent") + .state(state_element) + .disabled(is_trial || is_pro) + .map(|this| { + if is_signed_in && is_free { + this.on_click(move |_, _window, cx| { + telemetry::event!("Start Trial Clicked", state = "post-sign-in"); + cx.open_url(&zed_urls::start_trial_url(cx)) + }) + } else { + this.on_click(move |_, _, cx| { + let client = Client::global(cx); + cx.spawn(async move |cx| client.sign_in_with_optional_connect(true, cx).await) + .detach_and_log_err(cx); + }) + } + }) +} + +fn render_ai_section(user_store: &Entity, cx: &mut App) -> impl IntoElement { + let registry_agents = AgentRegistryStore::try_global(cx) + .map(|store| store.read(cx).agents().to_vec()) + .unwrap_or_default(); + + let installed_agents = cx + .global::() + .get::(None) + .clone(); + + let column_count = 1 + FEATURED_AGENT_IDS.len() as u16; + + let grid = FEATURED_AGENT_IDS.iter().fold( + div() + .w_full() + .mt_1p5() + .grid() + .grid_cols(column_count) + .gap_2() + .child(render_zed_agent_button(user_store, cx)), + |grid, agent_id| { + let Some(agent) = registry_agents + .iter() + .find(|a| a.id().as_ref() == *agent_id) + else { + return grid; + }; + let is_installed = installed_agents.contains_key(*agent_id); + grid.child(render_registry_agent_button(agent, is_installed, cx)) + }, + ); + + v_flex() + .gap_0p5() + .child(Label::new("Agent Setup")) + .child( + Label::new("Install your favorite agents and start your first thread.") + .color(Color::Muted), + ) + .child(grid) +} + +pub(crate) fn render_basics_page(user_store: &Entity, cx: &mut App) -> impl IntoElement { let mut tab_index = 0; + v_flex() .id("basics-page") .gap_6() .child(render_theme_section(&mut tab_index, cx)) .child(render_base_keymap_section(&mut tab_index, cx)) + .child(render_ai_section(user_store, cx)) .child(render_import_settings_section(&mut tab_index, cx)) .child(render_vim_mode_switch(&mut tab_index, cx)) .child(render_worktree_auto_trust_switch(&mut tab_index, cx)) diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 808cba456406f915bdd9f593a6647ea3e90c696d..caa1a5458f66f77a46665627731f720a47a0cdbd 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -16,6 +16,7 @@ use ui::{ Divider, KeyBinding, ParentElement as _, StatefulInteractiveElement, Vector, VectorName, WithScrollbar as _, prelude::*, rems_from_px, }; + pub use workspace::welcome::ShowWelcome; use workspace::welcome::WelcomePage; use workspace::{ @@ -259,7 +260,7 @@ impl Onboarding { } fn render_page(&mut self, cx: &mut Context) -> AnyElement { - crate::basics_page::render_basics_page(cx).into_any_element() + crate::basics_page::render_basics_page(&self.user_store, cx).into_any_element() } } @@ -329,15 +330,12 @@ impl Render for Onboarding { Button::new("finish_setup", "Finish Setup") .style(ButtonStyle::Filled) .size(ButtonSize::Medium) - .width(Rems(12.0)) - .key_binding( - KeyBinding::for_action_in( - &Finish, - &self.focus_handle, - cx, - ) - .size(rems_from_px(12.)), - ) + .width(rems_from_px(200.)) + .key_binding(KeyBinding::for_action_in( + &Finish, + &self.focus_handle, + cx, + )) .on_click(|_, window, cx| { window.dispatch_action(Finish.boxed_clone(), cx); }) diff --git a/crates/onboarding/src/theme_preview.rs b/crates/onboarding/src/theme_preview.rs index 602695cca6a643d4eb4d3476286bba7fcfe74c40..99990d1273f0c03ddeece7a376820ddf5e1b4e50 100644 --- a/crates/onboarding/src/theme_preview.rs +++ b/crates/onboarding/src/theme_preview.rs @@ -129,7 +129,7 @@ impl ThemePreviewTile { syntax_colors[idx].unwrap_or(colors.text) }; - let line_count = 13; + let line_count = 10; let lines = (0..line_count) .map(|line_idx| { @@ -147,7 +147,7 @@ impl ThemePreviewTile { }) .collect::>(); - h_flex().gap(px(2.)).ml(relative(indent)).children(blocks) + h_flex().gap_0p5().ml(relative(indent)).children(blocks) }) .collect::>(); @@ -160,14 +160,16 @@ impl ThemePreviewTile { width: impl Into + Clone, skeleton_height: impl Into, ) -> impl IntoElement { - div() + v_flex() .h_full() .w(width) - .border_r(px(1.)) - .border_color(colors.border_transparent) + .p_2() + .gap_1() .bg(colors.panel_background) - .child(v_flex().p_2().size_full().gap_1().children( - Self::render_sidebar_skeleton_items(seed, colors, skeleton_height.into()), + .children(Self::render_sidebar_skeleton_items( + seed, + colors, + skeleton_height.into(), )) } @@ -176,18 +178,16 @@ impl ThemePreviewTile { theme: Arc, skeleton_height: impl Into, ) -> impl IntoElement { - v_flex().h_full().flex_grow().child( - div() - .size_full() - .overflow_hidden() - .bg(theme.colors().editor_background) - .p_2() - .child(Self::render_pseudo_code_skeleton( - seed, - theme, - skeleton_height.into(), - )), - ) + div() + .p_2() + .size_full() + .overflow_hidden() + .bg(theme.colors().editor_background) + .child(Self::render_pseudo_code_skeleton( + seed, + theme, + skeleton_height.into(), + )) } pub fn render_editor( @@ -197,8 +197,8 @@ impl ThemePreviewTile { skeleton_height: impl Into + Clone, ) -> impl IntoElement { div() - .size_full() .flex() + .size_full() .bg(theme.colors().background.alpha(1.00)) .child(Self::render_sidebar( seed, diff --git a/crates/ui/src/components/ai.rs b/crates/ui/src/components/ai.rs index e3ad1db794902ae28b28274a60e3593efb3be392..91b5385c45b770cafbeb13ab1bef08f45cd0e33c 100644 --- a/crates/ui/src/components/ai.rs +++ b/crates/ui/src/components/ai.rs @@ -1,7 +1,11 @@ +mod agent_setup_button; mod ai_setting_item; mod configured_api_card; +mod parallel_agents_illustration; mod thread_item; +pub use agent_setup_button::*; pub use ai_setting_item::*; pub use configured_api_card::*; +pub use parallel_agents_illustration::*; pub use thread_item::*; diff --git a/crates/ui/src/components/ai/agent_setup_button.rs b/crates/ui/src/components/ai/agent_setup_button.rs new file mode 100644 index 0000000000000000000000000000000000000000..d56baf91e1361d033cfb032af8cae0e3079192ab --- /dev/null +++ b/crates/ui/src/components/ai/agent_setup_button.rs @@ -0,0 +1,110 @@ +use crate::prelude::*; +use gpui::{ClickEvent, SharedString}; + +#[derive(IntoElement, RegisterComponent)] +pub struct AgentSetupButton { + id: ElementId, + icon: Option, + name: Option, + state: Option, + disabled: bool, + on_click: Option>, +} + +impl AgentSetupButton { + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + icon: None, + name: None, + state: None, + disabled: false, + on_click: None, + } + } + + pub fn icon(mut self, icon: Icon) -> Self { + self.icon = Some(icon); + self + } + + pub fn name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + self + } + + pub fn state(mut self, element: impl IntoElement) -> Self { + self.state = Some(element.into_any_element()); + self + } + + pub fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } + + pub fn on_click( + mut self, + handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_click = Some(Box::new(handler)); + self + } +} + +impl Component for AgentSetupButton { + fn scope() -> ComponentScope { + ComponentScope::Agent + } + + fn preview(_window: &mut Window, _cx: &mut App) -> Option { + None + } +} + +impl RenderOnce for AgentSetupButton { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let is_clickable = !self.disabled && self.on_click.is_some(); + + let has_top_section = self.icon.is_some() || self.name.is_some(); + let top_section = has_top_section.then(|| { + h_flex() + .p_1p5() + .gap_1() + .justify_center() + .when_some(self.icon, |this, icon| this.child(icon)) + .when_some(self.name, |this, name| { + this.child(Label::new(name).size(LabelSize::Small)) + }) + }); + + let bottom_section = self.state.map(|state_element| { + h_flex() + .p_0p5() + .h_full() + .justify_center() + .border_t_1() + .border_color(cx.theme().colors().border_variant) + .bg(cx.theme().colors().element_background.opacity(0.5)) + .child(state_element) + }); + + v_flex() + .id(self.id) + .border_1() + .border_color(cx.theme().colors().border_variant) + .rounded_sm() + .when(is_clickable, |this| { + this.cursor_pointer().hover(|style| { + style + .bg(cx.theme().colors().element_hover) + .border_color(cx.theme().colors().border) + }) + }) + .when_some(top_section, |this, section| this.child(section)) + .when_some(bottom_section, |this, section| this.child(section)) + .when_some(self.on_click.filter(|_| is_clickable), |this, on_click| { + this.on_click(on_click) + }) + } +} diff --git a/crates/ui/src/components/ai/parallel_agents_illustration.rs b/crates/ui/src/components/ai/parallel_agents_illustration.rs new file mode 100644 index 0000000000000000000000000000000000000000..3640f71c075b3ba8f8cfb24fbde0b583c3763ab8 --- /dev/null +++ b/crates/ui/src/components/ai/parallel_agents_illustration.rs @@ -0,0 +1,149 @@ +use crate::{DiffStat, Divider, prelude::*}; +use gpui::{Animation, AnimationExt, pulsating_between}; +use std::time::Duration; + +#[derive(IntoElement)] +pub struct ParallelAgentsIllustration; + +impl ParallelAgentsIllustration { + pub fn new() -> Self { + Self + } +} + +impl RenderOnce for ParallelAgentsIllustration { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let icon_container = || h_flex().size_4().flex_shrink_0().justify_center(); + + let title_bar = |id: &'static str, width: DefiniteLength, duration_ms: u64| { + div() + .h_2() + .w(width) + .rounded_full() + .debug_bg_blue() + .bg(cx.theme().colors().element_selected) + .with_animation( + id, + Animation::new(Duration::from_millis(duration_ms)) + .repeat() + .with_easing(pulsating_between(0.4, 0.8)), + |label, delta| label.opacity(delta), + ) + }; + + let time = + |time: SharedString| Label::new(time).size(LabelSize::XSmall).color(Color::Muted); + + let worktree = |worktree: SharedString| { + h_flex() + .gap_1() + .child( + Icon::new(IconName::GitWorktree) + .color(Color::Muted) + .size(IconSize::XSmall), + ) + .child( + Label::new(worktree) + .size(LabelSize::XSmall) + .color(Color::Muted), + ) + }; + + let dot_separator = || { + Label::new("•") + .size(LabelSize::Small) + .color(Color::Muted) + .alpha(0.5) + }; + + let agent = |id: &'static str, + icon: IconName, + width: DefiniteLength, + duration_ms: u64, + data: Vec| { + v_flex() + .p_2() + .child( + h_flex() + .w_full() + .gap_2() + .child( + icon_container() + .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted)), + ) + .child(title_bar(id, width, duration_ms)), + ) + .child( + h_flex() + .opacity(0.8) + .w_full() + .gap_2() + .child(icon_container()) + .children(data), + ) + }; + + let agents = v_flex() + .absolute() + .w(rems_from_px(380.)) + .top_8() + .rounded_t_sm() + .border_1() + .border_color(cx.theme().colors().border.opacity(0.5)) + .bg(cx.theme().colors().elevated_surface_background) + .shadow_md() + .child(agent( + "zed-agent-bar", + IconName::ZedAgent, + relative(0.7), + 1800, + vec![ + worktree("happy-tree".into()).into_any_element(), + dot_separator().into_any_element(), + DiffStat::new("ds", 23, 13) + .label_size(LabelSize::XSmall) + .into_any_element(), + dot_separator().into_any_element(), + time("2m".into()).into_any_element(), + ], + )) + .child(Divider::horizontal()) + .child(agent( + "claude-bar", + IconName::AiClaude, + relative(0.85), + 2400, + vec![ + DiffStat::new("ds", 120, 84) + .label_size(LabelSize::XSmall) + .into_any_element(), + dot_separator().into_any_element(), + time("16m".into()).into_any_element(), + ], + )) + .child(Divider::horizontal()) + .child(agent( + "openai-bar", + IconName::AiOpenAi, + relative(0.4), + 3100, + vec![ + worktree("silent-forest".into()).into_any_element(), + dot_separator().into_any_element(), + time("37m".into()).into_any_element(), + ], + )) + .child(Divider::horizontal()); + + h_flex() + .relative() + .h(rems_from_px(180.)) + .bg(cx.theme().colors().editor_background) + .justify_center() + .items_end() + .rounded_t_md() + .overflow_hidden() + .bg(gpui::black().opacity(0.2)) + .child(agents) + } +} diff --git a/crates/ui/src/components/image.rs b/crates/ui/src/components/image.rs index 14127464dfc8b20dd6ad51ea9f39dc28c633fe6b..ceb1273bb06466bae4beb6310e04aed013e95d75 100644 --- a/crates/ui/src/components/image.rs +++ b/crates/ui/src/components/image.rs @@ -16,10 +16,11 @@ pub enum VectorName { AcpGrid, AcpLogo, AcpLogoSerif, - AiGrid, + BusinessStamp, Grid, ProTrialStamp, ProUserStamp, + StudentStamp, ZedLogo, ZedXCopilot, } diff --git a/crates/ui/src/components/notification/announcement_toast.rs b/crates/ui/src/components/notification/announcement_toast.rs index ec8495851658f8d1e8b1ac1219c90f645506535f..8879f6fcb6051f399dfb551ea688b9e11a1219d3 100644 --- a/crates/ui/src/components/notification/announcement_toast.rs +++ b/crates/ui/src/components/notification/announcement_toast.rs @@ -26,9 +26,9 @@ impl AnnouncementToast { heading: None, description: None, bullet_items: SmallVec::new(), - primary_action_label: "Learn More".into(), + primary_action_label: "Try Now".into(), primary_on_click: Box::new(|_, _, _| {}), - secondary_action_label: "View Release Notes".into(), + secondary_action_label: "Learn More".into(), secondary_on_click: Box::new(|_, _, _| {}), dismiss_on_click: Box::new(|_, _, _| {}), } @@ -134,12 +134,13 @@ impl RenderOnce for AnnouncementToast { .gap_1() .child( Button::new("try-now", self.primary_action_label) - .style(ButtonStyle::Outlined) + .style(ButtonStyle::Tinted(crate::TintColor::Accent)) .full_width() .on_click(self.primary_on_click), ) .child( Button::new("release-notes", self.secondary_action_label) + .style(ButtonStyle::OutlinedGhost) .full_width() .on_click(self.secondary_on_click), ), @@ -208,19 +209,26 @@ impl Component for AnnouncementToast { let examples = vec![single_example( "Basic", - div().w_80().child( - AnnouncementToast::new() - .illustration(illustration) - .heading("What's new in Zed") - .description( - "This version comes in with some changes to the workspace for a better experience.", - ) - .bullet_item(ListBulletItem::new("Improved agent performance")) - .bullet_item(ListBulletItem::new("New agentic features")) - .bullet_item(ListBulletItem::new("Better agent capabilities")) - - ) - .into_any_element(), + div() + .w_80() + .child( + AnnouncementToast::new() + .illustration(illustration) + .heading("Introducing Parallel Agents") + .description("Run multiple agent threads simultaneously across projects.") + .bullet_item(ListBulletItem::new( + "Mix and match Zed's agent with any ACP-compatible agent", + )) + .bullet_item(ListBulletItem::new( + "Optional worktree isolation keeps agents from conflicting", + )) + .bullet_item(ListBulletItem::new( + "Updated workspace layout designed for agentic workflows", + )) + .primary_action_label("Try Now") + .secondary_action_label("Learn More"), + ) + .into_any_element(), )]; Some( diff --git a/crates/workspace/src/welcome.rs b/crates/workspace/src/welcome.rs index dceca3e85f4308952563e689c608c92e9f77144f..c7f5d1c726c96c352a6f1ddb6ee8611b861d81f1 100644 --- a/crates/workspace/src/welcome.rs +++ b/crates/workspace/src/welcome.rs @@ -1,23 +1,27 @@ use crate::{ - NewFile, Open, OpenMode, PathList, SerializedWorkspaceLocation, Workspace, WorkspaceId, + NewFile, Open, OpenMode, PathList, SerializedWorkspaceLocation, ToggleWorkspaceSidebar, + Workspace, WorkspaceId, item::{Item, ItemEvent}, persistence::WorkspaceDb, }; +use agent_settings::AgentSettings; use chrono::{DateTime, Utc}; use git::Clone as GitClone; -use gpui::WeakEntity; use gpui::{ Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, ParentElement, Render, Styled, Task, Window, actions, }; +use gpui::{WeakEntity, linear_color_stop, linear_gradient}; use menu::{SelectNext, SelectPrevious}; -use project::DisableAiSettings; + use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; use ui::{ButtonLike, Divider, DividerColor, KeyBinding, Vector, VectorName, prelude::*}; use util::ResultExt; -use zed_actions::{Extensions, OpenOnboarding, OpenSettings, agent, command_palette}; +use zed_actions::{ + Extensions, OpenKeymap, OpenOnboarding, OpenSettings, assistant::ToggleFocus, command_palette, +}; #[derive(PartialEq, Clone, Debug, Deserialize, Serialize, JsonSchema, Action)] #[action(namespace = welcome)] @@ -126,14 +130,12 @@ impl RenderOnce for SectionButton { enum SectionVisibility { Always, - Conditional(fn(&App) -> bool), } impl SectionVisibility { - fn is_visible(&self, cx: &App) -> bool { + fn is_visible(&self) -> bool { match self { SectionVisibility::Always => true, - SectionVisibility::Conditional(f) => f(cx), } } } @@ -146,13 +148,8 @@ struct SectionEntry { } impl SectionEntry { - fn render( - &self, - button_index: usize, - focus: &FocusHandle, - cx: &App, - ) -> Option { - self.visibility_guard.is_visible(cx).then(|| { + fn render(&self, button_index: usize, focus: &FocusHandle) -> Option { + self.visibility_guard.is_visible().then(|| { SectionButton::new( self.title, self.icon, @@ -204,12 +201,10 @@ const CONTENT: (Section<4>, Section<3>) = ( visibility_guard: SectionVisibility::Always, }, SectionEntry { - icon: IconName::ZedAssistant, - title: "View AI Settings", - action: &agent::OpenSettings, - visibility_guard: SectionVisibility::Conditional(|cx| { - !DisableAiSettings::get_global(cx).disable_ai - }), + icon: IconName::Keyboard, + title: "Customize Keymaps", + action: &OpenKeymap, + visibility_guard: SectionVisibility::Always, }, SectionEntry { icon: IconName::Blocks, @@ -230,7 +225,7 @@ struct Section { } impl Section { - fn render(self, index_offset: usize, focus: &FocusHandle, cx: &App) -> impl IntoElement { + fn render(self, index_offset: usize, focus: &FocusHandle) -> impl IntoElement { v_flex() .min_w_full() .child(SectionHeader::new(self.title)) @@ -238,7 +233,7 @@ impl Section { self.entries .iter() .enumerate() - .filter_map(|(index, entry)| entry.render(index_offset + index, focus, cx)), + .filter_map(|(index, entry)| entry.render(index_offset + index, focus)), ) } } @@ -338,6 +333,55 @@ impl WelcomePage { } } + fn render_agent_card(&self, tab_index: usize, cx: &mut Context) -> impl IntoElement { + let focus = self.focus_handle.clone(); + let color = cx.theme().colors(); + + let description = "Run multiple threads at once, mix and match any ACP-compatible agent, and keep work conflict-free with worktrees."; + + v_flex() + .w_full() + .p_2() + .rounded_md() + .border_1() + .border_color(color.border_variant) + .bg(linear_gradient( + 360., + linear_color_stop(color.panel_background, 1.0), + linear_color_stop(color.editor_background, 0.45), + )) + .child( + h_flex() + .gap_1p5() + .child( + Icon::new(IconName::ZedAssistant) + .color(Color::Muted) + .size(IconSize::Small), + ) + .child(Label::new("Collaborate with Agents")), + ) + .child( + Label::new(description) + .size(LabelSize::Small) + .color(Color::Muted) + .mb_2(), + ) + .child( + Button::new("open-agent", "Open Agent Panel") + .full_width() + .tab_index(tab_index as isize) + .style(ButtonStyle::Outlined) + .key_binding( + KeyBinding::for_action_in(&ToggleFocus, &self.focus_handle, cx) + .size(rems_from_px(12.)), + ) + .on_click(move |_, window, cx| { + focus.dispatch_action(&ToggleWorkspaceSidebar, window, cx); + focus.dispatch_action(&ToggleFocus, window, cx); + }), + ) + } + fn render_recent_project_section( &self, recent_projects: Vec, @@ -385,7 +429,9 @@ impl Render for WelcomePage { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let (first_section, second_section) = CONTENT; let first_section_entries = first_section.entries.len(); - let last_index = first_section_entries + second_section.entries.len(); + let mut next_tab_index = first_section_entries + second_section.entries.len(); + + let ai_enabled = AgentSettings::get_global(cx).enabled(cx); let recent_projects = self .recent_workspaces @@ -404,7 +450,7 @@ impl Render for WelcomePage { .into_any_element() } else { second_section - .render(first_section_entries, &self.focus_handle, cx) + .render(first_section_entries, &self.focus_handle) .into_any_element() }; @@ -421,58 +467,53 @@ impl Render for WelcomePage { .on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::open_recent_project)) .size_full() - .justify_center() - .overflow_hidden() .bg(cx.theme().colors().editor_background) + .justify_center() .child( - h_flex() - .relative() + v_flex() + .id("welcome-content") + .p_8() + .max_w_128() .size_full() - .px_12() - .max_w(px(1100.)) + .gap_6() + .justify_center() + .overflow_y_scroll() .child( - v_flex() - .flex_1() + h_flex() + .w_full() .justify_center() - .max_w_128() - .mx_auto() - .gap_6() - .overflow_x_hidden() + .mb_4() + .gap_4() + .child(Vector::square(VectorName::ZedLogo, rems_from_px(45.))) .child( - h_flex() - .w_full() - .justify_center() - .mb_4() - .gap_4() - .child(Vector::square(VectorName::ZedLogo, rems_from_px(45.))) - .child( - v_flex().child(Headline::new(welcome_label)).child( - Label::new("The editor for what's next") - .size(LabelSize::Small) - .color(Color::Muted) - .italic(), - ), - ), - ) - .child(first_section.render(Default::default(), &self.focus_handle, cx)) - .child(second_section) - .when(!self.fallback_to_recent_projects, |this| { - this.child( - v_flex().gap_1().child(Divider::horizontal()).child( - Button::new("welcome-exit", "Return to Onboarding") - .tab_index(last_index as isize) - .full_width() - .label_size(LabelSize::XSmall) - .on_click(|_, window, cx| { - window.dispatch_action( - OpenOnboarding.boxed_clone(), - cx, - ); - }), - ), - ) - }), - ), + v_flex().child(Headline::new(welcome_label)).child( + Label::new("The editor for what's next") + .size(LabelSize::Small) + .color(Color::Muted) + .italic(), + ), + ), + ) + .child(first_section.render(Default::default(), &self.focus_handle)) + .child(second_section) + .when(ai_enabled, |this| { + let agent_tab_index = next_tab_index; + next_tab_index += 1; + this.child(self.render_agent_card(agent_tab_index, cx)) + }) + .when(!self.fallback_to_recent_projects, |this| { + this.child( + v_flex().gap_4().child(Divider::horizontal()).child( + Button::new("welcome-exit", "Return to Onboarding") + .tab_index(next_tab_index as isize) + .full_width() + .label_size(LabelSize::XSmall) + .on_click(|_, window, cx| { + window.dispatch_action(OpenOnboarding.boxed_clone(), cx); + }), + ), + ) + }), ) } }