Skip to repository content443 lines · 14.0 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:33:05.720Z 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
attach_modal.rs
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