Skip to repository content504 lines · 19.8 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T06:55:49.354Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
repl_menu.rs
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