1use gpui::{
2 ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
3 linear_color_stop, linear_gradient,
4};
5use project::agent_server_store::CLAUDE_AGENT_NAME;
6use ui::{TintColor, Vector, VectorName, prelude::*};
7use workspace::{ModalView, Workspace};
8
9use crate::agent_panel::{AgentPanel, AgentType};
10
11macro_rules! claude_agent_onboarding_event {
12 ($name:expr) => {
13 telemetry::event!($name, source = "ACP Claude Code Onboarding");
14 };
15 ($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
16 telemetry::event!($name, source = "ACP Claude Code Onboarding", $($key $(= $value)?),+);
17 };
18}
19
20pub struct ClaudeCodeOnboardingModal {
21 focus_handle: FocusHandle,
22 workspace: Entity<Workspace>,
23}
24
25impl ClaudeCodeOnboardingModal {
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 AgentType::Custom {
42 name: CLAUDE_AGENT_NAME.into(),
43 },
44 window,
45 cx,
46 );
47 });
48 }
49 });
50
51 cx.emit(DismissEvent);
52
53 claude_agent_onboarding_event!("Open Panel Clicked");
54 }
55
56 fn view_docs(&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 claude_agent_onboarding_event!("Documentation Link 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 ClaudeCodeOnboardingModal {}
69
70impl Focusable for ClaudeCodeOnboardingModal {
71 fn focus_handle(&self, _cx: &App) -> FocusHandle {
72 self.focus_handle.clone()
73 }
74}
75
76impl ModalView for ClaudeCodeOnboardingModal {}
77
78impl Render for ClaudeCodeOnboardingModal {
79 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
80 let illustration_element = |icon: IconName, label: Option<SharedString>, 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(icon)
92 .size(IconSize::Small)
93 .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))),
94 )
95 .map(|this| {
96 if let Some(label_text) = label {
97 this.child(
98 Label::new(label_text)
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(IconName::Stop, None, 0.15))
161 .child(illustration_element(
162 IconName::AiGemini,
163 Some("New Gemini CLI Thread".into()),
164 0.3,
165 ))
166 .child(
167 h_flex()
168 .pl_1()
169 .pr_2()
170 .py_0p5()
171 .gap_1()
172 .rounded_sm()
173 .bg(cx.theme().colors().element_active.opacity(0.2))
174 .border_1()
175 .border_color(cx.theme().colors().border)
176 .child(
177 Icon::new(IconName::AiClaude)
178 .size(IconSize::Small)
179 .color(Color::Muted),
180 )
181 .child(Label::new("New Claude Agent Thread").size(LabelSize::Small)),
182 )
183 .child(illustration_element(
184 IconName::Stop,
185 Some("Your Agent Here".into()),
186 0.3,
187 ))
188 .child(illustration_element(IconName::Stop, None, 0.15)),
189 );
190
191 let heading = v_flex()
192 .w_full()
193 .gap_1()
194 .child(
195 Label::new("Beta Release")
196 .size(LabelSize::Small)
197 .color(Color::Muted),
198 )
199 .child(Headline::new("Claude Agent: Natively in Zed").size(HeadlineSize::Large));
200
201 let copy = "Powered by the Agent Client Protocol, you can now run Claude Agent as\na first-class citizen in Zed's agent panel.";
202
203 let open_panel_button = Button::new("open-panel", "Start with Claude Agent")
204 .icon_size(IconSize::Indicator)
205 .style(ButtonStyle::Tinted(TintColor::Accent))
206 .full_width()
207 .on_click(cx.listener(Self::open_panel));
208
209 let docs_button = Button::new("add-other-agents", "Add Other Agents")
210 .icon(IconName::ArrowUpRight)
211 .icon_size(IconSize::Indicator)
212 .icon_color(Color::Muted)
213 .full_width()
214 .on_click(cx.listener(Self::view_docs));
215
216 let close_button = h_flex().absolute().top_2().right_2().child(
217 IconButton::new("cancel", IconName::Close).on_click(cx.listener(
218 |_, _: &ClickEvent, _window, cx| {
219 claude_agent_onboarding_event!("Canceled", trigger = "X click");
220 cx.emit(DismissEvent);
221 },
222 )),
223 );
224
225 v_flex()
226 .id("acp-onboarding")
227 .key_context("AcpOnboardingModal")
228 .relative()
229 .w(rems(34.))
230 .h_full()
231 .elevation_3(cx)
232 .track_focus(&self.focus_handle(cx))
233 .overflow_hidden()
234 .on_action(cx.listener(Self::cancel))
235 .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
236 claude_agent_onboarding_event!("Canceled", trigger = "Action");
237 cx.emit(DismissEvent);
238 }))
239 .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
240 this.focus_handle.focus(window, cx);
241 }))
242 .child(illustration)
243 .child(
244 v_flex()
245 .p_4()
246 .gap_2()
247 .child(heading)
248 .child(Label::new(copy).color(Color::Muted))
249 .child(
250 v_flex()
251 .w_full()
252 .mt_2()
253 .gap_1()
254 .child(open_panel_button)
255 .child(docs_button),
256 ),
257 )
258 .child(close_button)
259 }
260}