1use client::zed_urls;
2use gpui::{
3 ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
4 linear_color_stop, linear_gradient,
5};
6use ui::{TintColor, Vector, VectorName, prelude::*};
7use workspace::{ModalView, Workspace};
8
9use crate::agent_panel::{AgentPanel, AgentType};
10
11macro_rules! acp_onboarding_event {
12 ($name:expr) => {
13 telemetry::event!($name, source = "ACP Onboarding");
14 };
15 ($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
16 telemetry::event!($name, source = "ACP Onboarding", $($key $(= $value)?),+);
17 };
18}
19
20pub struct AcpOnboardingModal {
21 focus_handle: FocusHandle,
22 workspace: Entity<Workspace>,
23}
24
25impl AcpOnboardingModal {
26 pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
27 let workspace_entity = cx.entity();
28 workspace.toggle_modal(window, cx, |_window, cx| Self {
29 workspace: workspace_entity,
30 focus_handle: cx.focus_handle(),
31 });
32 }
33
34 fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
35 self.workspace.update(cx, |workspace, cx| {
36 workspace.focus_panel::<AgentPanel>(window, cx);
37
38 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
39 panel.update(cx, |panel, cx| {
40 panel.new_agent_thread(AgentType::Gemini, window, cx);
41 });
42 }
43 });
44
45 cx.emit(DismissEvent);
46
47 acp_onboarding_event!("Open Panel Clicked");
48 }
49
50 fn view_docs(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
51 cx.open_url(&zed_urls::external_agents_docs(cx));
52 cx.notify();
53
54 acp_onboarding_event!("Documentation Link Clicked");
55 }
56
57 fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
58 cx.emit(DismissEvent);
59 }
60}
61
62impl EventEmitter<DismissEvent> for AcpOnboardingModal {}
63
64impl Focusable for AcpOnboardingModal {
65 fn focus_handle(&self, _cx: &App) -> FocusHandle {
66 self.focus_handle.clone()
67 }
68}
69
70impl ModalView for AcpOnboardingModal {}
71
72impl Render for AcpOnboardingModal {
73 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
74 let illustration_element = |label: bool, opacity: f32| {
75 h_flex()
76 .px_1()
77 .py_0p5()
78 .gap_1()
79 .rounded_sm()
80 .bg(cx.theme().colors().element_active.opacity(0.05))
81 .border_1()
82 .border_color(cx.theme().colors().border)
83 .border_dashed()
84 .child(
85 Icon::new(IconName::Stop)
86 .size(IconSize::Small)
87 .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))),
88 )
89 .map(|this| {
90 if label {
91 this.child(
92 Label::new("Your Agent Here")
93 .size(LabelSize::Small)
94 .color(Color::Muted),
95 )
96 } else {
97 this.child(
98 div().w_16().h_1().rounded_full().bg(cx
99 .theme()
100 .colors()
101 .element_active
102 .opacity(0.6)),
103 )
104 }
105 })
106 .opacity(opacity)
107 };
108
109 let illustration = h_flex()
110 .relative()
111 .h(rems_from_px(126.))
112 .bg(cx.theme().colors().editor_background)
113 .border_b_1()
114 .border_color(cx.theme().colors().border_variant)
115 .justify_center()
116 .gap_8()
117 .rounded_t_md()
118 .overflow_hidden()
119 .child(
120 div().absolute().inset_0().w(px(515.)).h(px(126.)).child(
121 Vector::new(VectorName::AcpGrid, rems_from_px(515.), rems_from_px(126.))
122 .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.02))),
123 ),
124 )
125 .child(div().absolute().inset_0().size_full().bg(linear_gradient(
126 0.,
127 linear_color_stop(
128 cx.theme().colors().elevated_surface_background.opacity(0.1),
129 0.9,
130 ),
131 linear_color_stop(
132 cx.theme().colors().elevated_surface_background.opacity(0.),
133 0.,
134 ),
135 )))
136 .child(
137 div()
138 .absolute()
139 .inset_0()
140 .size_full()
141 .bg(gpui::black().opacity(0.15)),
142 )
143 .child(
144 Vector::new(
145 VectorName::AcpLogoSerif,
146 rems_from_px(257.),
147 rems_from_px(47.),
148 )
149 .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
150 )
151 .child(
152 v_flex()
153 .gap_1p5()
154 .child(illustration_element(false, 0.15))
155 .child(illustration_element(true, 0.3))
156 .child(
157 h_flex()
158 .pl_1()
159 .pr_2()
160 .py_0p5()
161 .gap_1()
162 .rounded_sm()
163 .bg(cx.theme().colors().element_active.opacity(0.2))
164 .border_1()
165 .border_color(cx.theme().colors().border)
166 .child(
167 Icon::new(IconName::AiGemini)
168 .size(IconSize::Small)
169 .color(Color::Muted),
170 )
171 .child(Label::new("New Gemini CLI Thread").size(LabelSize::Small)),
172 )
173 .child(illustration_element(true, 0.3))
174 .child(illustration_element(false, 0.15)),
175 );
176
177 let heading = v_flex()
178 .w_full()
179 .gap_1()
180 .child(
181 Label::new("Now Available")
182 .size(LabelSize::Small)
183 .color(Color::Muted),
184 )
185 .child(Headline::new("Bring Your Own Agent to Zed").size(HeadlineSize::Large));
186
187 let copy = "Bring the agent of your choice to Zed via our new Agent Client Protocol (ACP), starting with Google's Gemini CLI integration.";
188
189 let open_panel_button = Button::new("open-panel", "Start with Gemini CLI")
190 .icon_size(IconSize::Indicator)
191 .style(ButtonStyle::Tinted(TintColor::Accent))
192 .full_width()
193 .on_click(cx.listener(Self::open_panel));
194
195 let docs_button = Button::new("add-other-agents", "Add Other Agents")
196 .icon(IconName::ArrowUpRight)
197 .icon_size(IconSize::Indicator)
198 .icon_color(Color::Muted)
199 .full_width()
200 .on_click(cx.listener(Self::view_docs));
201
202 let close_button = h_flex().absolute().top_2().right_2().child(
203 IconButton::new("cancel", IconName::Close).on_click(cx.listener(
204 |_, _: &ClickEvent, _window, cx| {
205 acp_onboarding_event!("Canceled", trigger = "X click");
206 cx.emit(DismissEvent);
207 },
208 )),
209 );
210
211 v_flex()
212 .id("acp-onboarding")
213 .key_context("AcpOnboardingModal")
214 .relative()
215 .w(rems(34.))
216 .h_full()
217 .elevation_3(cx)
218 .track_focus(&self.focus_handle(cx))
219 .overflow_hidden()
220 .on_action(cx.listener(Self::cancel))
221 .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
222 acp_onboarding_event!("Canceled", trigger = "Action");
223 cx.emit(DismissEvent);
224 }))
225 .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
226 this.focus_handle.focus(window);
227 }))
228 .child(illustration)
229 .child(
230 v_flex()
231 .p_4()
232 .gap_2()
233 .child(heading)
234 .child(Label::new(copy).color(Color::Muted))
235 .child(
236 v_flex()
237 .w_full()
238 .mt_2()
239 .gap_1()
240 .child(open_panel_button)
241 .child(docs_button),
242 ),
243 )
244 .child(close_button)
245 }
246}