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! claude_code_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(AgentType::ClaudeCode, window, cx);
41 });
42 }
43 });
44
45 cx.emit(DismissEvent);
46
47 claude_code_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 claude_code_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 ClaudeCodeOnboardingModal {}
63
64impl Focusable for ClaudeCodeOnboardingModal {
65 fn focus_handle(&self, _cx: &App) -> FocusHandle {
66 self.focus_handle.clone()
67 }
68}
69
70impl ModalView for ClaudeCodeOnboardingModal {}
71
72impl Render for ClaudeCodeOnboardingModal {
73 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
74 let illustration_element = |icon: IconName, label: Option<SharedString>, 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(icon)
86 .size(IconSize::Small)
87 .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))),
88 )
89 .map(|this| {
90 if let Some(label_text) = label {
91 this.child(
92 Label::new(label_text)
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(IconName::Stop, None, 0.15))
155 .child(illustration_element(
156 IconName::AiGemini,
157 Some("New Gemini CLI Thread".into()),
158 0.3,
159 ))
160 .child(
161 h_flex()
162 .pl_1()
163 .pr_2()
164 .py_0p5()
165 .gap_1()
166 .rounded_sm()
167 .bg(cx.theme().colors().element_active.opacity(0.2))
168 .border_1()
169 .border_color(cx.theme().colors().border)
170 .child(
171 Icon::new(IconName::AiClaude)
172 .size(IconSize::Small)
173 .color(Color::Muted),
174 )
175 .child(Label::new("New Claude Code Thread").size(LabelSize::Small)),
176 )
177 .child(illustration_element(
178 IconName::Stop,
179 Some("Your Agent Here".into()),
180 0.3,
181 ))
182 .child(illustration_element(IconName::Stop, None, 0.15)),
183 );
184
185 let heading = v_flex()
186 .w_full()
187 .gap_1()
188 .child(
189 Label::new("Beta Release")
190 .size(LabelSize::Small)
191 .color(Color::Muted),
192 )
193 .child(Headline::new("Claude Code: Natively in Zed").size(HeadlineSize::Large));
194
195 let copy = "Powered by the Agent Client Protocol, you can now run Claude Code as\na first-class citizen in Zed's agent panel.";
196
197 let open_panel_button = Button::new("open-panel", "Start with Claude Code")
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 claude_code_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 claude_code_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}