claude_agent_onboarding_modal.rs

  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}