claude_code_onboarding_modal.rs

  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}