diff --git a/assets/images/acp_grid.svg b/assets/images/acp_grid.svg
new file mode 100644
index 0000000000..8ebff8e1bc
--- /dev/null
+++ b/assets/images/acp_grid.svg
@@ -0,0 +1,1257 @@
+
diff --git a/assets/images/acp_logo.svg b/assets/images/acp_logo.svg
new file mode 100644
index 0000000000..efaa46707b
--- /dev/null
+++ b/assets/images/acp_logo.svg
@@ -0,0 +1 @@
+
diff --git a/assets/images/acp_logo_serif.svg b/assets/images/acp_logo_serif.svg
new file mode 100644
index 0000000000..6bc359cf82
--- /dev/null
+++ b/assets/images/acp_logo_serif.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs
index c279115880..224f49cc3e 100644
--- a/crates/agent_ui/src/agent_configuration.rs
+++ b/crates/agent_ui/src/agent_configuration.rs
@@ -1093,7 +1093,7 @@ impl AgentConfiguration {
)
.child(
Label::new(
- "Use the full power of Zed's UI with your favorite agent, connected via the Agent Client Protocol.",
+ "Bring the agent of your choice to Zed via our new Agent Client Protocol.",
)
.color(Color::Muted),
),
diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs
index 267c76d73f..d1cf748733 100644
--- a/crates/agent_ui/src/agent_panel.rs
+++ b/crates/agent_ui/src/agent_panel.rs
@@ -14,6 +14,7 @@ use zed_actions::agent::ReauthenticateAgent;
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
use crate::agent_diff::AgentDiffThread;
+use crate::ui::AcpOnboardingModal;
use crate::{
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
@@ -77,7 +78,10 @@ use workspace::{
};
use zed_actions::{
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
- agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector},
+ agent::{
+ OpenAcpOnboardingModal, OpenOnboardingModal, OpenSettings, ResetOnboarding,
+ ToggleModelSelector,
+ },
assistant::{OpenRulesLibrary, ToggleFocus},
};
@@ -201,6 +205,9 @@ pub fn init(cx: &mut App) {
.register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
AgentOnboardingModal::toggle(workspace, window, cx)
})
+ .register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| {
+ AcpOnboardingModal::toggle(workspace, window, cx)
+ })
.register_action(|_workspace, _: &ResetOnboarding, window, cx| {
window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
window.refresh();
@@ -1841,19 +1848,6 @@ impl AgentPanel {
menu
}
- pub fn set_selected_agent(
- &mut self,
- agent: AgentType,
- window: &mut Window,
- cx: &mut Context,
- ) {
- if self.selected_agent != agent {
- self.selected_agent = agent.clone();
- self.serialize(cx);
- }
- self.new_agent_thread(agent, window, cx);
- }
-
pub fn selected_agent(&self) -> AgentType {
self.selected_agent.clone()
}
@@ -1864,6 +1858,11 @@ impl AgentPanel {
window: &mut Window,
cx: &mut Context,
) {
+ if self.selected_agent != agent {
+ self.selected_agent = agent.clone();
+ self.serialize(cx);
+ }
+
match agent {
AgentType::Zed => {
window.dispatch_action(
@@ -2544,7 +2543,7 @@ impl AgentPanel {
workspace.panel::(cx)
{
panel.update(cx, |panel, cx| {
- panel.set_selected_agent(
+ panel.new_agent_thread(
AgentType::NativeAgent,
window,
cx,
@@ -2570,7 +2569,7 @@ impl AgentPanel {
workspace.panel::(cx)
{
panel.update(cx, |panel, cx| {
- panel.set_selected_agent(
+ panel.new_agent_thread(
AgentType::TextThread,
window,
cx,
@@ -2598,7 +2597,7 @@ impl AgentPanel {
workspace.panel::(cx)
{
panel.update(cx, |panel, cx| {
- panel.set_selected_agent(
+ panel.new_agent_thread(
AgentType::Gemini,
window,
cx,
@@ -2625,7 +2624,7 @@ impl AgentPanel {
workspace.panel::(cx)
{
panel.update(cx, |panel, cx| {
- panel.set_selected_agent(
+ panel.new_agent_thread(
AgentType::ClaudeCode,
window,
cx,
@@ -2658,7 +2657,7 @@ impl AgentPanel {
workspace.panel::(cx)
{
panel.update(cx, |panel, cx| {
- panel.set_selected_agent(
+ panel.new_agent_thread(
AgentType::Custom {
name: agent_name
.clone(),
@@ -2682,9 +2681,9 @@ impl AgentPanel {
})
.when(cx.has_flag::(), |menu| {
menu.separator().link(
- "Add Your Own Agent",
+ "Add Other Agents",
OpenBrowser {
- url: "https://agentclientprotocol.com/".into(),
+ url: zed_urls::external_agents_docs(cx),
}
.boxed_clone(),
)
diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs
index ada973cddf..600698b07e 100644
--- a/crates/agent_ui/src/ui.rs
+++ b/crates/agent_ui/src/ui.rs
@@ -1,3 +1,4 @@
+mod acp_onboarding_modal;
mod agent_notification;
mod burn_mode_tooltip;
mod context_pill;
@@ -6,6 +7,7 @@ mod onboarding_modal;
pub mod preview;
mod unavailable_editing_tooltip;
+pub use acp_onboarding_modal::*;
pub use agent_notification::*;
pub use burn_mode_tooltip::*;
pub use context_pill::*;
diff --git a/crates/agent_ui/src/ui/acp_onboarding_modal.rs b/crates/agent_ui/src/ui/acp_onboarding_modal.rs
new file mode 100644
index 0000000000..0ed9de7221
--- /dev/null
+++ b/crates/agent_ui/src/ui/acp_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! acp_onboarding_event {
+ ($name:expr) => {
+ telemetry::event!($name, source = "ACP Onboarding");
+ };
+ ($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
+ telemetry::event!($name, source = "ACP Onboarding", $($key $(= $value)?),+);
+ };
+}
+
+pub struct AcpOnboardingModal {
+ focus_handle: FocusHandle,
+ workspace: Entity,
+}
+
+impl AcpOnboardingModal {
+ 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::Gemini, window, cx);
+ });
+ }
+ });
+
+ cx.emit(DismissEvent);
+
+ acp_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();
+
+ acp_onboarding_event!("Documentation Link Clicked");
+ }
+
+ fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) {
+ cx.emit(DismissEvent);
+ }
+}
+
+impl EventEmitter for AcpOnboardingModal {}
+
+impl Focusable for AcpOnboardingModal {
+ fn focus_handle(&self, _cx: &App) -> FocusHandle {
+ self.focus_handle.clone()
+ }
+}
+
+impl ModalView for AcpOnboardingModal {}
+
+impl Render for AcpOnboardingModal {
+ fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement {
+ let illustration_element = |label: bool, 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(IconName::Stop)
+ .size(IconSize::Small)
+ .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))),
+ )
+ .map(|this| {
+ if label {
+ this.child(
+ Label::new("Your Agent Here")
+ .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(
+ 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))),
+ ),
+ )
+ .child(
+ v_flex()
+ .gap_1p5()
+ .child(illustration_element(false, 0.15))
+ .child(illustration_element(true, 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::AiGemini)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
+ .child(Label::new("New Gemini CLI Thread").size(LabelSize::Small)),
+ )
+ .child(illustration_element(true, 0.3))
+ .child(illustration_element(false, 0.15)),
+ );
+
+ let heading = v_flex()
+ .w_full()
+ .gap_1()
+ .child(
+ Label::new("Now Available")
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ .child(Headline::new("Bring Your Own Agent to Zed").size(HeadlineSize::Large));
+
+ let copy = "Bring the agent of your choice to Zed via our new Agent Client Protocol (ACP), starting with Google's Gemini CLI integration.";
+
+ let open_panel_button = Button::new("open-panel", "Start with Gemini CLI")
+ .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| {
+ acp_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| {
+ acp_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/client/src/zed_urls.rs b/crates/client/src/zed_urls.rs
index 9df41906d7..7193c09947 100644
--- a/crates/client/src/zed_urls.rs
+++ b/crates/client/src/zed_urls.rs
@@ -43,3 +43,11 @@ pub fn ai_privacy_and_security(cx: &App) -> String {
server_url = server_url(cx)
)
}
+
+/// Returns the URL to Zed AI's external agents documentation.
+pub fn external_agents_docs(cx: &App) -> String {
+ format!(
+ "{server_url}/docs/ai/external-agents",
+ server_url = server_url(cx)
+ )
+}
diff --git a/crates/title_bar/src/onboarding_banner.rs b/crates/title_bar/src/onboarding_banner.rs
index ed43c5277a..1c28942490 100644
--- a/crates/title_bar/src/onboarding_banner.rs
+++ b/crates/title_bar/src/onboarding_banner.rs
@@ -119,7 +119,7 @@ impl Render for OnboardingBanner {
h_flex()
.h_full()
.gap_1()
- .child(Icon::new(self.details.icon_name).size(IconSize::Small))
+ .child(Icon::new(self.details.icon_name).size(IconSize::XSmall))
.child(
h_flex()
.gap_0p5()
diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs
index b84a2800b6..ad64dac9c6 100644
--- a/crates/title_bar/src/title_bar.rs
+++ b/crates/title_bar/src/title_bar.rs
@@ -275,11 +275,11 @@ impl TitleBar {
let banner = cx.new(|cx| {
OnboardingBanner::new(
- "Debugger Onboarding",
- IconName::Debug,
- "The Debugger",
- None,
- zed_actions::debugger::OpenOnboardingModal.boxed_clone(),
+ "ACP Onboarding",
+ IconName::Sparkle,
+ "Bring Your Own Agent",
+ Some("Introducing:".into()),
+ zed_actions::agent::OpenAcpOnboardingModal.boxed_clone(),
cx,
)
});
diff --git a/crates/ui/src/components/image.rs b/crates/ui/src/components/image.rs
index 09c3bbeb94..6e552ddcee 100644
--- a/crates/ui/src/components/image.rs
+++ b/crates/ui/src/components/image.rs
@@ -13,6 +13,9 @@ use crate::prelude::*;
)]
#[strum(serialize_all = "snake_case")]
pub enum VectorName {
+ AcpGrid,
+ AcpLogo,
+ AcpLogoSerif,
AiGrid,
DebuggerGrid,
Grid,
diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs
index a5223a2cdf..8f4c42ca49 100644
--- a/crates/zed_actions/src/lib.rs
+++ b/crates/zed_actions/src/lib.rs
@@ -284,6 +284,8 @@ pub mod agent {
OpenSettings,
/// Opens the agent onboarding modal.
OpenOnboardingModal,
+ /// Opens the ACP onboarding modal.
+ OpenAcpOnboardingModal,
/// Resets the agent onboarding state.
ResetOnboarding,
/// Starts a chat conversation with the agent.