Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:33:00.536Z 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

dropdown_menus.rs

334 lines · 12.8 KB · rust
1use std::rc::Rc;
2
3use collections::HashMap;
4use gpui::{Anchor, Entity, WeakEntity};
5use project::debugger::session::{ThreadId, ThreadStatus};
6use ui::{CommonAnimationExt, ContextMenu, DropdownMenu, Indicator, Tooltip, prelude::*};
7use util::{maybe, truncate_and_trailoff};
8
9use crate::{
10    debugger_panel::DebugPanel,
11    session::{DebugSession, running::RunningState},
12};
13
14struct SessionListEntry {
15    ancestors: Vec<Entity<DebugSession>>,
16    leaf: Entity<DebugSession>,
17}
18
19impl SessionListEntry {
20    pub(crate) fn label_element(&self, depth: usize, cx: &mut App) -> AnyElement {
21        const MAX_LABEL_CHARS: usize = 150;
22
23        let mut label = String::new();
24        for ancestor in &self.ancestors {
25            label.push_str(&ancestor.update(cx, |ancestor, cx| {
26                ancestor.label(cx).unwrap_or("(child)".into())
27            }));
28            label.push_str(" » ");
29        }
30        label.push_str(
31            &self
32                .leaf
33                .update(cx, |leaf, cx| leaf.label(cx).unwrap_or("(child)".into())),
34        );
35        let label = truncate_and_trailoff(&label, MAX_LABEL_CHARS);
36
37        let is_terminated = self
38            .leaf
39            .read(cx)
40            .running_state
41            .read(cx)
42            .session()
43            .read(cx)
44            .is_terminated();
45        let icon = {
46            if is_terminated {
47                Some(Indicator::dot().color(Color::Error))
48            } else {
49                match self
50                    .leaf
51                    .read(cx)
52                    .running_state
53                    .read(cx)
54                    .thread_status(cx)
55                    .unwrap_or_default()
56                {
57                    project::debugger::session::ThreadStatus::Stopped => {
58                        Some(Indicator::dot().color(Color::Conflict))
59                    }
60                    _ => Some(Indicator::dot().color(Color::Success)),
61                }
62            }
63        };
64
65        h_flex()
66            .id("session-label")
67            .ml(depth * px(16.0))
68            .gap_2()
69            .when_some(icon, |this, indicator| this.child(indicator))
70            .justify_between()
71            .child(
72                Label::new(label)
73                    .size(LabelSize::Small)
74                    .when(is_terminated, |this| this.strikethrough()),
75            )
76            .into_any_element()
77    }
78}
79
80impl DebugPanel {
81    fn dropdown_label(label: impl Into<SharedString>) -> Label {
82        const MAX_LABEL_CHARS: usize = 50;
83        let label = truncate_and_trailoff(&label.into(), MAX_LABEL_CHARS);
84        Label::new(label).size(LabelSize::Small)
85    }
86
87    pub fn render_session_menu(
88        &mut self,
89        active_session: Option<Entity<DebugSession>>,
90        running_state: Option<Entity<RunningState>>,
91        window: &mut Window,
92        cx: &mut Context<Self>,
93    ) -> Option<impl IntoElement> {
94        let running_state = running_state?;
95
96        let mut session_entries = Vec::with_capacity(self.sessions_with_children.len() * 3);
97        let mut sessions_with_children = self.sessions_with_children.iter().peekable();
98
99        while let Some((root, children)) = sessions_with_children.next() {
100            let root_entry = if let Ok([single_child]) = <&[_; 1]>::try_from(children.as_slice())
101                && let Some(single_child) = single_child.upgrade()
102                && single_child.read(cx).quirks.compact
103            {
104                sessions_with_children.next();
105                SessionListEntry {
106                    leaf: single_child.clone(),
107                    ancestors: vec![root.clone()],
108                }
109            } else {
110                SessionListEntry {
111                    leaf: root.clone(),
112                    ancestors: Vec::new(),
113                }
114            };
115            session_entries.push(root_entry);
116        }
117
118        let weak = cx.weak_entity();
119        let trigger_label = if let Some(active_session) = active_session.clone() {
120            active_session.update(cx, |active_session, cx| {
121                active_session.label(cx).unwrap_or("(child)".into())
122            })
123        } else {
124            SharedString::new_static("Unknown Session")
125        };
126        let running_state = running_state.read(cx);
127
128        let is_terminated = running_state.session().read(cx).is_terminated();
129        let is_started = active_session
130            .is_some_and(|session| session.read(cx).session(cx).read(cx).is_started());
131
132        let session_state_indicator = if is_terminated {
133            Indicator::dot().color(Color::Error).into_any_element()
134        } else if !is_started {
135            Icon::new(IconName::LoadCircle)
136                .size(IconSize::Small)
137                .color(Color::Muted)
138                .with_rotate_animation(2)
139                .into_any_element()
140        } else {
141            match running_state.thread_status(cx).unwrap_or_default() {
142                ThreadStatus::Stopped => Indicator::dot().color(Color::Conflict).into_any_element(),
143                _ => Indicator::dot().color(Color::Success).into_any_element(),
144            }
145        };
146
147        let trigger = h_flex()
148            .gap_2()
149            .child(session_state_indicator)
150            .child(
151                DebugPanel::dropdown_label(trigger_label)
152                    .when(is_terminated, |this| this.strikethrough()),
153            )
154            .into_any_element();
155
156        let menu = DropdownMenu::new_with_element(
157            "debugger-session-list",
158            trigger,
159            ContextMenu::build(window, cx, move |mut this, _, cx| {
160                let context_menu = cx.weak_entity();
161                let mut session_depths = HashMap::default();
162                for session_entry in session_entries {
163                    let session_id = session_entry.leaf.read(cx).session_id(cx);
164                    let parent_depth = session_entry
165                        .ancestors
166                        .first()
167                        .unwrap_or(&session_entry.leaf)
168                        .read(cx)
169                        .session(cx)
170                        .read(cx)
171                        .parent_id(cx)
172                        .and_then(|parent_id| session_depths.get(&parent_id).cloned());
173                    let self_depth = *session_depths
174                        .entry(session_id)
175                        .or_insert_with(|| parent_depth.map(|depth| depth + 1).unwrap_or(0usize));
176                    this = this.custom_entry(
177                        {
178                            let weak = weak.clone();
179                            let context_menu = context_menu.clone();
180                            let ancestors: Rc<[_]> = session_entry
181                                .ancestors
182                                .iter()
183                                .map(|session| session.downgrade())
184                                .collect();
185                            let leaf = session_entry.leaf.downgrade();
186                            move |window, cx| {
187                                Self::render_session_menu_entry(
188                                    weak.clone(),
189                                    context_menu.clone(),
190                                    ancestors.clone(),
191                                    leaf.clone(),
192                                    self_depth,
193                                    window,
194                                    cx,
195                                )
196                            }
197                        },
198                        {
199                            let weak = weak.clone();
200                            let leaf = session_entry.leaf.clone();
201                            move |window, cx| {
202                                weak.update(cx, |panel, cx| {
203                                    panel.activate_session(leaf.clone(), window, cx);
204                                })
205                                .ok();
206                            }
207                        },
208                    );
209                }
210                this
211            }),
212        )
213        .attach(Anchor::BottomLeft)
214        .handle(self.session_picker_menu_handle.clone())
215        .trigger_tooltip(Tooltip::text("Select a Debug Session"));
216
217        Some(menu)
218    }
219
220    fn render_session_menu_entry(
221        weak: WeakEntity<DebugPanel>,
222        context_menu: WeakEntity<ContextMenu>,
223        ancestors: Rc<[WeakEntity<DebugSession>]>,
224        leaf: WeakEntity<DebugSession>,
225        self_depth: usize,
226        _window: &mut Window,
227        cx: &mut App,
228    ) -> AnyElement {
229        let Some(session_entry) = maybe!({
230            let ancestors = ancestors
231                .iter()
232                .map(|ancestor| ancestor.upgrade())
233                .collect::<Option<Vec<_>>>()?;
234            let leaf = leaf.upgrade()?;
235            Some(SessionListEntry { ancestors, leaf })
236        }) else {
237            return div().into_any_element();
238        };
239
240        let id: SharedString = format!(
241            "debug-session-{}",
242            session_entry.leaf.read(cx).session_id(cx).0
243        )
244        .into();
245        let session_entity_id = session_entry.leaf.entity_id();
246
247        h_flex()
248            .w_full()
249            .group(id.clone())
250            .justify_between()
251            .child(session_entry.label_element(self_depth, cx))
252            .child(
253                IconButton::new("close-debug-session", IconName::Close)
254                    .visible_on_hover(id)
255                    .icon_size(IconSize::Small)
256                    .on_click({
257                        move |_, window, cx| {
258                            weak.update(cx, |panel, cx| {
259                                panel.close_session(session_entity_id, window, cx);
260                            })
261                            .ok();
262                            context_menu
263                                .update(cx, |this, cx| {
264                                    this.cancel(&Default::default(), window, cx);
265                                })
266                                .ok();
267                        }
268                    }),
269            )
270            .into_any_element()
271    }
272
273    pub(crate) fn render_thread_dropdown(
274        &self,
275        running_state: &Entity<RunningState>,
276        threads: Vec<(dap::Thread, ThreadStatus)>,
277        window: &mut Window,
278        cx: &mut Context<Self>,
279    ) -> Option<DropdownMenu> {
280        const MAX_LABEL_CHARS: usize = 150;
281
282        let running_state = running_state.clone();
283        let running_state_read = running_state.read(cx);
284        let thread_id = running_state_read.thread_id();
285        let session = running_state_read.session();
286        let session_id = session.read(cx).session_id();
287        let session_terminated = session.read(cx).is_terminated();
288        let selected_thread_name = threads
289            .iter()
290            .find(|(thread, _)| thread_id.map(|id| id.0) == Some(thread.id))
291            .map(|(thread, _)| {
292                thread
293                    .name
294                    .is_empty()
295                    .then(|| format!("Tid: {}", thread.id))
296                    .unwrap_or_else(|| thread.name.clone())
297            });
298
299        if let Some(selected_thread_name) = selected_thread_name {
300            let trigger = DebugPanel::dropdown_label(selected_thread_name).into_any_element();
301            Some(
302                DropdownMenu::new_with_element(
303                    ("thread-list", session_id.0),
304                    trigger,
305                    ContextMenu::build(window, cx, move |mut this, _, _| {
306                        for (thread, _) in threads {
307                            let running_state = running_state.clone();
308                            let thread_id = thread.id;
309                            let entry_name = thread
310                                .name
311                                .is_empty()
312                                .then(|| format!("Tid: {}", thread.id))
313                                .unwrap_or_else(|| thread.name);
314                            let entry_name = truncate_and_trailoff(&entry_name, MAX_LABEL_CHARS);
315
316                            this = this.entry(entry_name, None, move |window, cx| {
317                                running_state.update(cx, |running_state, cx| {
318                                    running_state.select_thread(ThreadId(thread_id), window, cx);
319                                });
320                            });
321                        }
322                        this
323                    }),
324                )
325                .attach(Anchor::BottomLeft)
326                .disabled(session_terminated)
327                .handle(self.thread_picker_menu_handle.clone()),
328            )
329        } else {
330            None
331        }
332    }
333}
334
Served at tenant.openagents/omega Member data and write actions are omitted.