Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T08:03:21.775Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

repl_menu.rs

504 lines · 19.8 KB · rust
1use gpui::ElementId;
2use gpui::TaskExt;
3use gpui::{AnyElement, Entity};
4use picker::Picker;
5use repl::{
6    ExecutionState, JupyterSettings, Kernel, KernelSpecification, KernelStatus, Session,
7    SessionSupport,
8    components::{KernelPickerDelegate, KernelSelector},
9    worktree_id_for_editor,
10};
11use ui::{
12    ButtonLike, CommonAnimationExt, ContextMenu, IconWithIndicator, Indicator, IntoElement,
13    PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*,
14};
15use util::ResultExt;
16
17use super::QuickActionBar;
18
19const ZED_REPL_DOCUMENTATION: &str = "https://zed.dev/docs/repl";
20
21struct ReplMenuState {
22    tooltip: SharedString,
23    icon: IconName,
24    icon_color: Color,
25    icon_is_animating: bool,
26    popover_disabled: bool,
27    indicator: Option<Indicator>,
28
29    status: KernelStatus,
30    kernel_name: SharedString,
31    kernel_language: SharedString,
32}
33
34impl QuickActionBar {
35    pub fn render_repl_menu(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
36        if !JupyterSettings::enabled(cx) {
37            return None;
38        }
39
40        let editor = self.active_editor()?;
41
42        let is_valid_project = editor
43            .read(cx)
44            .workspace()
45            .map(|workspace| {
46                let project = workspace.read(cx).project().read(cx);
47                !project.is_via_collab()
48            })
49            .unwrap_or(false);
50
51        if !is_valid_project {
52            return None;
53        }
54
55        let has_nonempty_selection = {
56            editor.update(cx, |this, cx| {
57                this.selections
58                    .count()
59                    .ne(&0)
60                    .then(|| {
61                        let snapshot = this.display_snapshot(cx);
62                        let latest = this.selections.newest_display(&snapshot);
63                        !latest.is_empty()
64                    })
65                    .unwrap_or_default()
66            })
67        };
68
69        let session = repl::session(editor.downgrade(), cx);
70        let session = match session {
71            SessionSupport::ActiveSession(session) => session,
72            SessionSupport::Inactive(spec) => {
73                return self.render_repl_launch_menu(spec, cx);
74            }
75            SessionSupport::RequiresSetup(language) => {
76                return self.render_repl_setup(language.as_ref(), cx);
77            }
78            SessionSupport::Unsupported => return None,
79        };
80
81        let menu_state = session_state(session.clone(), cx);
82
83        let id = "repl-menu";
84
85        let element_id = |suffix| ElementId::Name(format!("{}-{}", id, suffix).into());
86
87        let editor = editor.downgrade();
88        let dropdown_menu = PopoverMenu::new(element_id("menu"))
89            .menu(move |window, cx| {
90                let editor = editor.clone();
91                let session = session.clone();
92                ContextMenu::build(window, cx, move |menu, _, cx| {
93                    let menu_state = session_state(session, cx);
94                    let status = menu_state.status;
95                    let editor = editor.clone();
96
97                    menu.map(|menu| {
98                        if status.is_connected() {
99                            let status = status.clone();
100                            menu.custom_row(move |_window, _cx| {
101                                h_flex()
102                                    .child(
103                                        Label::new(format!(
104                                            "kernel: {} ({})",
105                                            menu_state.kernel_name, menu_state.kernel_language
106                                        ))
107                                        .size(LabelSize::Small)
108                                        .color(Color::Muted),
109                                    )
110                                    .into_any_element()
111                            })
112                            .custom_row(move |_window, _cx| {
113                                h_flex()
114                                    .child(
115                                        Label::new(status.clone().to_string())
116                                            .size(LabelSize::Small)
117                                            .color(Color::Muted),
118                                    )
119                                    .into_any_element()
120                            })
121                        } else {
122                            let status = status.clone();
123                            menu.custom_row(move |_window, _cx| {
124                                h_flex()
125                                    .child(
126                                        Label::new(format!("{}...", status.to_string()))
127                                            .size(LabelSize::Small)
128                                            .color(Color::Muted),
129                                    )
130                                    .into_any_element()
131                            })
132                        }
133                    })
134                    .separator()
135                    .custom_entry(
136                        move |_window, _cx| {
137                            Label::new(if has_nonempty_selection {
138                                "Run Selection"
139                            } else {
140                                "Run Line"
141                            })
142                            .into_any_element()
143                        },
144                        {
145                            let editor = editor.clone();
146                            move |window, cx| {
147                                repl::run(editor.clone(), true, window, cx).log_err();
148                            }
149                        },
150                    )
151                    .custom_entry(
152                        move |_window, _cx| {
153                            Label::new("Interrupt")
154                                .size(LabelSize::Small)
155                                .color(Color::Error)
156                                .into_any_element()
157                        },
158                        {
159                            let editor = editor.clone();
160                            move |_, cx| {
161                                repl::interrupt(editor.clone(), cx);
162                            }
163                        },
164                    )
165                    .custom_entry(
166                        move |_window, _cx| {
167                            Label::new("Clear Outputs")
168                                .size(LabelSize::Small)
169                                .color(Color::Muted)
170                                .into_any_element()
171                        },
172                        {
173                            let editor = editor.clone();
174                            move |_, cx| {
175                                repl::clear_outputs(editor.clone(), cx);
176                            }
177                        },
178                    )
179                    .separator()
180                    .custom_entry(
181                        move |_window, _cx| {
182                            Label::new("Shut Down Kernel")
183                                .size(LabelSize::Small)
184                                .color(Color::Error)
185                                .into_any_element()
186                        },
187                        {
188                            let editor = editor.clone();
189                            move |window, cx| {
190                                repl::shutdown(editor.clone(), window, cx);
191                            }
192                        },
193                    )
194                    .custom_entry(
195                        move |_window, _cx| {
196                            Label::new("Restart Kernel")
197                                .size(LabelSize::Small)
198                                .color(Color::Error)
199                                .into_any_element()
200                        },
201                        {
202                            move |window, cx| {
203                                repl::restart(editor.clone(), window, cx);
204                            }
205                        },
206                    )
207                    .separator()
208                    .action("View Sessions", Box::new(repl::Sessions))
209                    // TODO: Add shut down all kernels action
210                    // .action("Shut Down all Kernels", Box::new(gpui::NoAction))
211                })
212                .into()
213            })
214            .trigger_with_tooltip(
215                ButtonLike::new_rounded_right(element_id("dropdown"))
216                    .child(
217                        Icon::new(IconName::ChevronDown)
218                            .size(IconSize::XSmall)
219                            .color(Color::Muted),
220                    )
221                    .width(rems(1.))
222                    .disabled(menu_state.popover_disabled),
223                Tooltip::text("REPL Menu"),
224            );
225
226        let button = ButtonLike::new_rounded_left("toggle_repl_icon")
227            .child(if menu_state.icon_is_animating {
228                Icon::new(menu_state.icon)
229                    .color(menu_state.icon_color)
230                    .with_rotate_animation(5)
231                    .into_any_element()
232            } else {
233                IconWithIndicator::new(
234                    Icon::new(IconName::ReplNeutral).color(menu_state.icon_color),
235                    menu_state.indicator,
236                )
237                .indicator_border_color(Some(cx.theme().colors().toolbar_background))
238                .into_any_element()
239            })
240            .size(ButtonSize::Compact)
241            .style(ButtonStyle::Subtle)
242            .tooltip(Tooltip::text(menu_state.tooltip))
243            .on_click(|_, window, cx| window.dispatch_action(Box::new(repl::Run {}), cx))
244            .into_any_element();
245
246        Some(
247            h_flex()
248                .child(self.render_kernel_selector(cx))
249                .child(button)
250                .child(dropdown_menu)
251                .into_any_element(),
252        )
253    }
254    pub fn render_repl_launch_menu(
255        &self,
256        kernel_specification: KernelSpecification,
257        cx: &mut Context<Self>,
258    ) -> Option<AnyElement> {
259        let tooltip: SharedString =
260            SharedString::from(format!("Start REPL for {}", kernel_specification.name()));
261
262        Some(
263            h_flex()
264                .child(self.render_kernel_selector(cx))
265                .child(
266                    IconButton::new("toggle_repl_icon", IconName::ReplNeutral)
267                        .size(ButtonSize::Compact)
268                        .icon_color(Color::Muted)
269                        .style(ButtonStyle::Subtle)
270                        .tooltip(Tooltip::text(tooltip))
271                        .on_click(|_, window, cx| {
272                            window.dispatch_action(Box::new(repl::Run {}), cx)
273                        }),
274                )
275                .into_any_element(),
276        )
277    }
278
279    pub fn render_kernel_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
280        let editor = if let Some(editor) = self.active_editor() {
281            editor
282        } else {
283            return div().into_any_element();
284        };
285
286        let Some(worktree_id) = worktree_id_for_editor(editor.downgrade(), cx) else {
287            return div().into_any_element();
288        };
289
290        let store = repl::ReplStore::global(cx);
291        if !store.read(cx).has_python_kernelspecs(worktree_id) {
292            if let Some(project) = editor
293                .read(cx)
294                .workspace()
295                .map(|workspace| workspace.read(cx).project().clone())
296            {
297                store
298                    .update(cx, |store, cx| {
299                        store.refresh_python_kernelspecs(worktree_id, &project, cx)
300                    })
301                    .detach_and_log_err(cx);
302            }
303        }
304
305        let session = repl::session(editor.downgrade(), cx);
306
307        let current_kernelspec = match session {
308            SessionSupport::ActiveSession(session) => {
309                Some(session.read(cx).kernel_specification.clone())
310            }
311            SessionSupport::Inactive(kernel_specification) => Some(kernel_specification),
312            SessionSupport::RequiresSetup(_language_name) => None,
313            SessionSupport::Unsupported => None,
314        };
315
316        let current_kernel_name = current_kernelspec.as_ref().map(|spec| spec.name());
317
318        let menu_handle: PopoverMenuHandle<Picker<KernelPickerDelegate>> =
319            PopoverMenuHandle::default();
320        KernelSelector::new(
321            {
322                Box::new(move |kernelspec, window, cx| {
323                    if kernelspec.has_ipykernel() {
324                        repl::assign_kernelspec(kernelspec, editor.downgrade(), window, cx).ok();
325                    } else {
326                        repl::install_ipykernel_and_assign(
327                            kernelspec,
328                            editor.downgrade(),
329                            window,
330                            cx,
331                        )
332                        .ok();
333                    }
334                })
335            },
336            worktree_id,
337            ButtonLike::new("kernel-selector")
338                .style(ButtonStyle::Subtle)
339                .size(ButtonSize::Compact)
340                .child(
341                    h_flex()
342                        .w_full()
343                        .gap_0p5()
344                        .child(
345                            div()
346                                .overflow_x_hidden()
347                                .flex_grow_1()
348                                .whitespace_nowrap()
349                                .child(
350                                    Label::new(if let Some(name) = current_kernel_name {
351                                        name
352                                    } else {
353                                        SharedString::from("Select Kernel")
354                                    })
355                                    .size(LabelSize::Small)
356                                    .color(if current_kernelspec.is_some() {
357                                        Color::Default
358                                    } else {
359                                        Color::Placeholder
360                                    })
361                                    .into_any_element(),
362                                ),
363                        )
364                        .child(
365                            Icon::new(IconName::ChevronDown)
366                                .color(Color::Muted)
367                                .size(IconSize::XSmall),
368                        ),
369                ),
370            Tooltip::text("Select Kernel"),
371        )
372        .with_handle(menu_handle)
373        .into_any_element()
374    }
375
376    pub fn render_repl_setup(&self, language: &str, cx: &mut Context<Self>) -> Option<AnyElement> {
377        let tooltip: SharedString = SharedString::from(format!("Setup Omega REPL for {}", language));
378        Some(
379            h_flex()
380                .gap(DynamicSpacing::Base06.rems(cx))
381                .child(self.render_kernel_selector(cx))
382                .child(
383                    IconButton::new("toggle_repl_icon", IconName::ReplNeutral)
384                        .style(ButtonStyle::Subtle)
385                        .shape(ui::IconButtonShape::Square)
386                        .icon_size(ui::IconSize::Small)
387                        .icon_color(Color::Muted)
388                        .tooltip(Tooltip::text(tooltip))
389                        .on_click(|_, _window, cx| {
390                            cx.open_url(&format!("{}#installation", ZED_REPL_DOCUMENTATION))
391                        }),
392                )
393                .into_any_element(),
394        )
395    }
396}
397
398fn session_state(session: Entity<Session>, cx: &mut App) -> ReplMenuState {
399    let session = session.read(cx);
400
401    let kernel_name = session.kernel_specification.name();
402    let kernel_language: SharedString = session.kernel_specification.language();
403
404    let fill_fields = || {
405        ReplMenuState {
406            tooltip: "Nothing running".into(),
407            icon: IconName::ReplNeutral,
408            icon_color: Color::Default,
409            icon_is_animating: false,
410            popover_disabled: false,
411            indicator: None,
412            kernel_name: kernel_name.clone(),
413            kernel_language: kernel_language.clone(),
414            // TODO: Technically not shutdown, but indeterminate
415            status: KernelStatus::Shutdown,
416            // current_delta: Duration::default(),
417        }
418    };
419
420    let transitional =
421        |tooltip: SharedString, animating: bool, popover_disabled: bool| ReplMenuState {
422            tooltip,
423            icon_is_animating: animating,
424            popover_disabled,
425            icon_color: Color::Muted,
426            indicator: Some(Indicator::dot().color(Color::Muted)),
427            status: session.kernel.status(),
428            ..fill_fields()
429        };
430
431    let starting = || transitional(format!("{} is starting", kernel_name).into(), true, true);
432    let restarting = || transitional(format!("Restarting {}", kernel_name).into(), true, true);
433    let shutting_down = || {
434        transitional(
435            format!("{} is shutting down", kernel_name).into(),
436            false,
437            true,
438        )
439    };
440    let auto_restarting = || {
441        transitional(
442            format!("Auto-restarting {}", kernel_name).into(),
443            true,
444            true,
445        )
446    };
447    let unknown = || transitional(format!("{} state unknown", kernel_name).into(), false, true);
448    let other = |state: &str| {
449        transitional(
450            format!("{} state: {}", kernel_name, state).into(),
451            false,
452            true,
453        )
454    };
455
456    let shutdown = || ReplMenuState {
457        tooltip: "Nothing running".into(),
458        icon: IconName::ReplNeutral,
459        icon_color: Color::Default,
460        icon_is_animating: false,
461        popover_disabled: false,
462        indicator: None,
463        status: KernelStatus::Shutdown,
464        ..fill_fields()
465    };
466
467    match &session.kernel {
468        Kernel::Restarting => restarting(),
469        Kernel::RunningKernel(kernel) => match &kernel.execution_state() {
470            ExecutionState::Idle => ReplMenuState {
471                tooltip: format!("Run code on {} ({})", kernel_name, kernel_language).into(),
472                indicator: Some(Indicator::dot().color(Color::Success)),
473                status: session.kernel.status(),
474                ..fill_fields()
475            },
476            ExecutionState::Busy => ReplMenuState {
477                tooltip: format!("Interrupt {} ({})", kernel_name, kernel_language).into(),
478                icon_is_animating: true,
479                popover_disabled: false,
480                indicator: None,
481                status: session.kernel.status(),
482                ..fill_fields()
483            },
484            ExecutionState::Unknown => unknown(),
485            ExecutionState::Starting => starting(),
486            ExecutionState::Restarting => restarting(),
487            ExecutionState::Terminating => shutting_down(),
488            ExecutionState::AutoRestarting => auto_restarting(),
489            ExecutionState::Dead => shutdown(),
490            ExecutionState::Other(state) => other(state),
491        },
492        Kernel::StartingKernel(_) => starting(),
493        Kernel::ErroredLaunch(e) => ReplMenuState {
494            tooltip: format!("Error with kernel {}: {}", kernel_name, e).into(),
495            popover_disabled: false,
496            indicator: Some(Indicator::dot().color(Color::Error)),
497            status: session.kernel.status(),
498            ..fill_fields()
499        },
500        Kernel::ShuttingDown => shutting_down(),
501        Kernel::Shutdown => shutdown(),
502    }
503}
504
Served at tenant.openagents/omega Member data and write actions are omitted.