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 h_flex()
145 .gap_4()
146 .child(
147 Vector::new(VectorName::AcpLogo, rems_from_px(106.), rems_from_px(40.))
148 .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
149 )
150 .child(
151 Vector::new(
152 VectorName::AcpLogoSerif,
153 rems_from_px(111.),
154 rems_from_px(41.),
155 )
156 .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
157 ),
158 )
159 .child(
160 v_flex()
161 .gap_1p5()
162 .child(illustration_element(false, 0.15))
163 .child(illustration_element(true, 0.3))
164 .child(
165 h_flex()
166 .pl_1()
167 .pr_2()
168 .py_0p5()
169 .gap_1()
170 .rounded_sm()
171 .bg(cx.theme().colors().element_active.opacity(0.2))
172 .border_1()
173 .border_color(cx.theme().colors().border)
174 .child(
175 Icon::new(IconName::AiGemini)
176 .size(IconSize::Small)
177 .color(Color::Muted),
178 )
179 .child(Label::new("New Gemini CLI Thread").size(LabelSize::Small)),
180 )
181 .child(illustration_element(true, 0.3))
182 .child(illustration_element(false, 0.15)),
183 );
184
185 let heading = v_flex()
186 .w_full()
187 .gap_1()
188 .child(
189 Label::new("Now Available")
190 .size(LabelSize::Small)
191 .color(Color::Muted),
192 )
193 .child(Headline::new("Bring Your Own Agent to Zed").size(HeadlineSize::Large));
194
195 let copy = "Bring the agent of your choice to Zed via our new Agent Client Protocol (ACP), starting with Google's Gemini CLI integration.";
196
197 let open_panel_button = Button::new("open-panel", "Start with Gemini CLI")
198 .icon_size(IconSize::Indicator)
199 .style(ButtonStyle::Tinted(TintColor::Accent))
200 .full_width()
201 .on_click(cx.listener(Self::open_panel));
202
203 let docs_button = Button::new("add-other-agents", "Add Other Agents")
204 .icon(IconName::ArrowUpRight)
205 .icon_size(IconSize::Indicator)
206 .icon_color(Color::Muted)
207 .full_width()
208 .on_click(cx.listener(Self::view_docs));
209
210 let close_button = h_flex().absolute().top_2().right_2().child(
211 IconButton::new("cancel", IconName::Close).on_click(cx.listener(
212 |_, _: &ClickEvent, _window, cx| {
213 acp_onboarding_event!("Canceled", trigger = "X click");
214 cx.emit(DismissEvent);
215 },
216 )),
217 );
218
219 v_flex()
220 .id("acp-onboarding")
221 .key_context("AcpOnboardingModal")
222 .relative()
223 .w(rems(34.))
224 .h_full()
225 .elevation_3(cx)
226 .track_focus(&self.focus_handle(cx))
227 .overflow_hidden()
228 .on_action(cx.listener(Self::cancel))
229 .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
230 acp_onboarding_event!("Canceled", trigger = "Action");
231 cx.emit(DismissEvent);
232 }))
233 .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
234 this.focus_handle.focus(window);
235 }))
236 .child(illustration)
237 .child(
238 v_flex()
239 .p_4()
240 .gap_2()
241 .child(heading)
242 .child(Label::new(copy).color(Color::Muted))
243 .child(
244 v_flex()
245 .w_full()
246 .mt_2()
247 .gap_1()
248 .child(open_panel_button)
249 .child(docs_button),
250 ),
251 )
252 .child(close_button)
253 }
254}