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