Skip to repository content

tenant.openagents/omega

No repository description is available.

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

attach_modal.rs

443 lines · 14.0 KB · rust
1use dap::{DapRegistry, DebugRequest};
2use futures::channel::oneshot;
3use fuzzy::{StringMatch, StringMatchCandidate};
4use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render, Task, TaskExt};
5use gpui::{Subscription, WeakEntity};
6use picker::{Picker, PickerDelegate};
7use project::Project;
8use rpc::proto;
9use task::ZedDebugConfig;
10use util::debug_panic;
11
12use std::sync::Arc;
13
14use sysinfo::{ProcessRefreshKind, RefreshKind, System, UpdateKind};
15use ui::{Context, Tooltip, prelude::*};
16use ui::{ListItem, ListItemSpacing};
17use workspace::{ModalView, Workspace};
18
19use crate::debugger_panel::DebugPanel;
20
21#[derive(Debug, Clone)]
22pub(super) struct Candidate {
23    pub(super) pid: u32,
24    pub(super) name: SharedString,
25    pub(super) command: Vec<String>,
26}
27
28pub(crate) enum ModalIntent {
29    ResolveProcessId(Option<oneshot::Sender<Option<i32>>>),
30    AttachToProcess(ZedDebugConfig),
31}
32
33pub(crate) struct AttachModalDelegate {
34    selected_index: usize,
35    matches: Vec<StringMatch>,
36    placeholder_text: Arc<str>,
37    pub(crate) intent: ModalIntent,
38    workspace: WeakEntity<Workspace>,
39    candidates: Arc<[Candidate]>,
40}
41
42impl AttachModalDelegate {
43    fn new(
44        workspace: WeakEntity<Workspace>,
45        intent: ModalIntent,
46        candidates: Arc<[Candidate]>,
47    ) -> Self {
48        Self {
49            workspace,
50            candidates,
51            intent,
52            selected_index: 0,
53            matches: Vec::default(),
54            placeholder_text: Arc::from("Select the process you want to attach the debugger to"),
55        }
56    }
57}
58
59pub struct AttachModal {
60    _subscription: Subscription,
61    pub(crate) picker: Entity<Picker<AttachModalDelegate>>,
62}
63
64impl AttachModal {
65    pub(crate) fn new(
66        intent: ModalIntent,
67        workspace: WeakEntity<Workspace>,
68        project: Entity<Project>,
69        modal: bool,
70        window: &mut Window,
71        cx: &mut Context<Self>,
72    ) -> Self {
73        let processes_task = get_processes_for_project(&project, cx);
74
75        let modal = Self::with_processes(workspace, Arc::new([]), modal, intent, window, cx);
76
77        cx.spawn_in(window, async move |this, cx| {
78            let processes = processes_task.await;
79            this.update_in(cx, |modal, window, cx| {
80                modal.picker.update(cx, |picker, cx| {
81                    picker.delegate.candidates = processes;
82                    picker.refresh(window, cx);
83                });
84            })?;
85            anyhow::Ok(())
86        })
87        .detach_and_log_err(cx);
88
89        modal
90    }
91
92    pub(super) fn with_processes(
93        workspace: WeakEntity<Workspace>,
94        processes: Arc<[Candidate]>,
95        modal: bool,
96        intent: ModalIntent,
97        window: &mut Window,
98        cx: &mut Context<Self>,
99    ) -> Self {
100        let picker = cx.new(|cx| {
101            Picker::uniform_list(
102                AttachModalDelegate::new(workspace, intent, processes),
103                window,
104                cx,
105            )
106            .when(!modal, |picker| picker.embedded())
107        });
108        Self {
109            _subscription: cx.subscribe(&picker, |_, _, _, cx| {
110                cx.emit(DismissEvent);
111            }),
112            picker,
113        }
114    }
115}
116
117impl Render for AttachModal {
118    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
119        v_flex()
120            .key_context("AttachModal")
121            .track_focus(&self.focus_handle(cx))
122            .child(self.picker.clone())
123    }
124}
125
126impl EventEmitter<DismissEvent> for AttachModal {}
127
128impl Focusable for AttachModal {
129    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
130        self.picker.read(cx).focus_handle(cx)
131    }
132}
133
134impl ModalView for AttachModal {}
135
136impl PickerDelegate for AttachModalDelegate {
137    type ListItem = ListItem;
138
139    fn name() -> &'static str {
140        "attach modal"
141    }
142
143    fn match_count(&self) -> usize {
144        self.matches.len()
145    }
146
147    fn selected_index(&self) -> usize {
148        self.selected_index
149    }
150
151    fn set_selected_index(
152        &mut self,
153        ix: usize,
154        _window: &mut Window,
155        _: &mut Context<Picker<Self>>,
156    ) {
157        self.selected_index = ix;
158    }
159
160    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
161        self.placeholder_text.clone()
162    }
163
164    fn update_matches(
165        &mut self,
166        query: String,
167        _window: &mut Window,
168        cx: &mut Context<Picker<Self>>,
169    ) -> gpui::Task<()> {
170        cx.spawn(async move |this, cx| {
171            let Some(processes) = this
172                .read_with(cx, |this, _| this.delegate.candidates.clone())
173                .ok()
174            else {
175                return;
176            };
177
178            let matches = fuzzy::match_strings(
179                &processes
180                    .iter()
181                    .enumerate()
182                    .map(|(id, candidate)| {
183                        StringMatchCandidate::new(
184                            id,
185                            format!(
186                                "{} {} {}",
187                                candidate.command.join(" "),
188                                candidate.pid,
189                                candidate.name
190                            )
191                            .as_str(),
192                        )
193                    })
194                    .collect::<Vec<_>>(),
195                &query,
196                true,
197                true,
198                100,
199                &Default::default(),
200                cx.background_executor().clone(),
201            )
202            .await;
203
204            this.update(cx, |this, _| {
205                let delegate = &mut this.delegate;
206
207                delegate.matches = matches;
208
209                if delegate.matches.is_empty() {
210                    delegate.selected_index = 0;
211                } else {
212                    delegate.selected_index =
213                        delegate.selected_index.min(delegate.matches.len() - 1);
214                }
215            })
216            .ok();
217        })
218    }
219
220    fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
221        let candidate = self
222            .matches
223            .get(self.selected_index())
224            .and_then(|current_match| {
225                let ix = current_match.candidate_id;
226                self.candidates.get(ix)
227            });
228
229        match &mut self.intent {
230            ModalIntent::ResolveProcessId(sender) => {
231                cx.emit(DismissEvent);
232
233                if let Some(sender) = sender.take() {
234                    sender
235                        .send(candidate.map(|candidate| candidate.pid as i32))
236                        .ok();
237                }
238            }
239            ModalIntent::AttachToProcess(definition) => {
240                let Some(candidate) = candidate else {
241                    return cx.emit(DismissEvent);
242                };
243
244                match &mut definition.request {
245                    DebugRequest::Attach(config) => {
246                        config.process_id = Some(candidate.pid);
247                    }
248                    DebugRequest::Launch(_) => {
249                        debug_panic!("Debugger attach modal used on launch debug config");
250                        return;
251                    }
252                }
253
254                let workspace = self.workspace.clone();
255                let Some(panel) = workspace
256                    .update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
257                    .ok()
258                    .flatten()
259                else {
260                    return;
261                };
262
263                let Some(adapter) = cx.read_global::<DapRegistry, _>(|registry, _| {
264                    registry.adapter(&definition.adapter)
265                }) else {
266                    return;
267                };
268
269                let definition = definition.clone();
270                cx.spawn_in(window, async move |this, cx| {
271                    let Ok(scenario) = adapter.config_from_zed_format(definition).await else {
272                        return;
273                    };
274
275                    panel
276                        .update_in(cx, |panel, window, cx| {
277                            panel.start_session(
278                                scenario,
279                                Default::default(),
280                                None,
281                                None,
282                                window,
283                                cx,
284                            );
285                        })
286                        .ok();
287                    this.update(cx, |_, cx| {
288                        cx.emit(DismissEvent);
289                    })
290                    .ok();
291                })
292                .detach();
293            }
294        }
295    }
296
297    fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
298        self.selected_index = 0;
299
300        match &mut self.intent {
301            ModalIntent::ResolveProcessId(sender) => {
302                if let Some(sender) = sender.take() {
303                    sender.send(None).ok();
304                }
305            }
306            ModalIntent::AttachToProcess(_) => {}
307        }
308
309        cx.emit(DismissEvent);
310    }
311
312    fn render_match(
313        &self,
314        ix: usize,
315        selected: bool,
316        _window: &mut Window,
317        _: &mut Context<Picker<Self>>,
318    ) -> Option<Self::ListItem> {
319        let hit = &self.matches.get(ix)?;
320        let candidate = self.candidates.get(hit.candidate_id)?;
321
322        Some(
323            ListItem::new(format!("process-entry-{ix}"))
324                .inset(true)
325                .spacing(ListItemSpacing::Sparse)
326                .toggle_state(selected)
327                .child(
328                    v_flex()
329                        .items_start()
330                        .child(Label::new(format!("{} {}", candidate.name, candidate.pid)))
331                        .child(
332                            div()
333                                .id(format!("process-entry-{ix}-command"))
334                                .tooltip(Tooltip::text(
335                                    candidate
336                                        .command
337                                        .clone()
338                                        .into_iter()
339                                        .collect::<Vec<_>>()
340                                        .join(" "),
341                                ))
342                                .child(
343                                    Label::new(format!(
344                                        "{} {}",
345                                        candidate.name,
346                                        candidate
347                                            .command
348                                            .clone()
349                                            .into_iter()
350                                            .skip(1)
351                                            .collect::<Vec<_>>()
352                                            .join(" ")
353                                    ))
354                                    .size(LabelSize::Small)
355                                    .color(Color::Muted),
356                                ),
357                        ),
358                ),
359        )
360    }
361}
362
363fn get_processes_for_project(project: &Entity<Project>, cx: &mut App) -> Task<Arc<[Candidate]>> {
364    let project = project.read(cx);
365
366    if let Some(remote_client) = project.remote_client() {
367        let proto_client = remote_client.read(cx).proto_client();
368        cx.background_spawn(async move {
369            let response = proto_client
370                .request(proto::GetProcesses {
371                    project_id: proto::REMOTE_SERVER_PROJECT_ID,
372                })
373                .await
374                .unwrap_or_else(|_| proto::GetProcessesResponse {
375                    processes: Vec::new(),
376                });
377
378            let mut processes: Vec<Candidate> = response
379                .processes
380                .into_iter()
381                .map(|p| Candidate {
382                    pid: p.pid,
383                    name: p.name.into(),
384                    command: p.command,
385                })
386                .collect();
387
388            processes.sort_by_key(|k| k.name.clone());
389            Arc::from(processes.into_boxed_slice())
390        })
391    } else {
392        let refresh_kind = RefreshKind::nothing().with_processes(
393            ProcessRefreshKind::nothing()
394                .without_tasks()
395                .with_cmd(UpdateKind::Always),
396        );
397        let mut processes: Box<[_]> = System::new_with_specifics(refresh_kind)
398            .processes()
399            .values()
400            .map(|process| {
401                let name = process.name().to_string_lossy().into_owned();
402                Candidate {
403                    name: name.into(),
404                    pid: process.pid().as_u32(),
405                    command: process
406                        .cmd()
407                        .iter()
408                        .map(|s| s.to_string_lossy().into_owned())
409                        .collect::<Vec<_>>(),
410                }
411            })
412            .collect();
413        processes.sort_by_key(|k| k.name.clone());
414        let processes = processes.into_iter().collect();
415        Task::ready(processes)
416    }
417}
418
419#[cfg(test)]
420pub(crate) fn set_candidates(
421    modal: &AttachModal,
422    candidates: Arc<[Candidate]>,
423    window: &mut Window,
424    cx: &mut Context<AttachModal>,
425) {
426    modal.picker.update(cx, |picker, cx| {
427        picker.delegate.candidates = candidates;
428        picker.refresh(window, cx);
429    });
430}
431
432#[cfg(test)]
433pub(crate) fn process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
434    modal.picker.read_with(cx, |picker, _| {
435        picker
436            .delegate
437            .matches
438            .iter()
439            .map(|hit| hit.string.clone())
440            .collect::<Vec<_>>()
441    })
442}
443
Served at tenant.openagents/omega Member data and write actions are omitted.