Skip to repository content

tenant.openagents/omega

No repository description is available.

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

dap_store.rs

1038 lines · 35.6 KB · rust
1use super::{
2    breakpoint_store::BreakpointStore,
3    dap_command::EvaluateCommand,
4    locators,
5    session::{self, Session, SessionStateEvent},
6};
7use remote::Interactive;
8
9use crate::{
10    InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState,
11    debugger::session::SessionQuirks,
12    project_settings::{DapBinary, ProjectSettings},
13    worktree_store::WorktreeStore,
14};
15use anyhow::{Context as _, Result, anyhow};
16use async_trait::async_trait;
17use collections::HashMap;
18use dap::{
19    Capabilities, DapRegistry, DebugRequest, EvaluateArgumentsContext, StackFrameId,
20    adapters::{
21        DapDelegate, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, TcpArguments,
22    },
23    client::SessionId,
24    inline_value::VariableLookupKind,
25    messages::Message,
26};
27use fs::{Fs, RemoveOptions};
28use futures::{
29    StreamExt, TryStreamExt as _,
30    channel::mpsc::{self, UnboundedSender},
31    future::{Shared, join_all},
32};
33use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, TaskExt};
34use http_client::HttpClient;
35use language::{Buffer, LanguageToolchainStore};
36use node_runtime::NodeRuntime;
37use settings::InlayHintKind;
38
39use remote::RemoteClient;
40use rpc::{
41    AnyProtoClient, TypedEnvelope,
42    proto::{self},
43};
44use serde::{Deserialize, Serialize};
45use settings::{Settings, SettingsLocation, WorktreeId};
46use std::{
47    borrow::Borrow,
48    collections::BTreeMap,
49    ffi::OsStr,
50    net::{IpAddr, Ipv4Addr},
51    path::{Path, PathBuf},
52    sync::{Arc, Once},
53};
54use task::{DebugScenario, SharedTaskContext, SpawnInTerminal, TaskTemplate};
55use util::{ResultExt as _, rel_path::RelPath};
56use worktree::Worktree;
57
58#[derive(Debug)]
59pub enum DapStoreEvent {
60    DebugClientStarted(SessionId),
61    DebugSessionInitialized(SessionId),
62    DebugClientShutdown(SessionId),
63    DebugClientEvent {
64        session_id: SessionId,
65        message: Message,
66    },
67    Notification(String),
68    RemoteHasInitialized,
69}
70
71enum DapStoreMode {
72    Local(LocalDapStore),
73    Remote(RemoteDapStore),
74    Collab,
75}
76
77pub struct LocalDapStore {
78    fs: Arc<dyn Fs>,
79    node_runtime: NodeRuntime,
80    http_client: Arc<dyn HttpClient>,
81    environment: Entity<ProjectEnvironment>,
82    toolchain_store: Arc<dyn LanguageToolchainStore>,
83    is_headless: bool,
84}
85
86pub struct RemoteDapStore {
87    remote_client: Entity<RemoteClient>,
88    upstream_client: AnyProtoClient,
89    upstream_project_id: u64,
90    node_runtime: NodeRuntime,
91    http_client: Arc<dyn HttpClient>,
92}
93
94pub struct DapStore {
95    mode: DapStoreMode,
96    downstream_client: Option<(AnyProtoClient, u64)>,
97    breakpoint_store: Entity<BreakpointStore>,
98    worktree_store: Entity<WorktreeStore>,
99    sessions: BTreeMap<SessionId, Entity<Session>>,
100    next_session_id: u32,
101    adapter_options: BTreeMap<DebugAdapterName, Arc<PersistedAdapterOptions>>,
102}
103
104impl EventEmitter<DapStoreEvent> for DapStore {}
105
106#[derive(Clone, Serialize, Deserialize)]
107pub struct PersistedExceptionBreakpoint {
108    pub enabled: bool,
109}
110
111/// Represents best-effort serialization of adapter state during last session (e.g. watches)
112#[derive(Clone, Default, Serialize, Deserialize)]
113pub struct PersistedAdapterOptions {
114    /// Which exception breakpoints were enabled during the last session with this adapter?
115    pub exception_breakpoints: BTreeMap<String, PersistedExceptionBreakpoint>,
116}
117
118impl DapStore {
119    pub fn init(client: &AnyProtoClient, cx: &mut App) {
120        static ADD_LOCATORS: Once = Once::new();
121        ADD_LOCATORS.call_once(|| {
122            let registry = DapRegistry::global(cx);
123            registry.add_locator(Arc::new(locators::cargo::CargoLocator {}));
124            registry.add_locator(Arc::new(locators::go::GoLocator {}));
125            registry.add_locator(Arc::new(locators::node::NodeLocator));
126            registry.add_locator(Arc::new(locators::python::PythonLocator));
127        });
128        client.add_entity_request_handler(Self::handle_run_debug_locator);
129        client.add_entity_request_handler(Self::handle_get_debug_adapter_binary);
130        client.add_entity_message_handler(Self::handle_log_to_debug_console);
131    }
132
133    #[expect(clippy::too_many_arguments)]
134    pub fn new_local(
135        http_client: Arc<dyn HttpClient>,
136        node_runtime: NodeRuntime,
137        fs: Arc<dyn Fs>,
138        environment: Entity<ProjectEnvironment>,
139        toolchain_store: Arc<dyn LanguageToolchainStore>,
140        worktree_store: Entity<WorktreeStore>,
141        breakpoint_store: Entity<BreakpointStore>,
142        is_headless: bool,
143        cx: &mut Context<Self>,
144    ) -> Self {
145        let mode = DapStoreMode::Local(LocalDapStore {
146            fs: fs.clone(),
147            environment,
148            http_client,
149            node_runtime,
150            toolchain_store,
151            is_headless,
152        });
153
154        Self::new(mode, breakpoint_store, worktree_store, fs, cx)
155    }
156
157    pub fn new_remote(
158        project_id: u64,
159        remote_client: Entity<RemoteClient>,
160        breakpoint_store: Entity<BreakpointStore>,
161        worktree_store: Entity<WorktreeStore>,
162        node_runtime: NodeRuntime,
163        http_client: Arc<dyn HttpClient>,
164        fs: Arc<dyn Fs>,
165        cx: &mut Context<Self>,
166    ) -> Self {
167        let mode = DapStoreMode::Remote(RemoteDapStore {
168            upstream_client: remote_client.read(cx).proto_client(),
169            remote_client,
170            upstream_project_id: project_id,
171            node_runtime,
172            http_client,
173        });
174
175        Self::new(mode, breakpoint_store, worktree_store, fs, cx)
176    }
177
178    pub fn new_collab(
179        _project_id: u64,
180        _upstream_client: AnyProtoClient,
181        breakpoint_store: Entity<BreakpointStore>,
182        worktree_store: Entity<WorktreeStore>,
183        fs: Arc<dyn Fs>,
184        cx: &mut Context<Self>,
185    ) -> Self {
186        Self::new(
187            DapStoreMode::Collab,
188            breakpoint_store,
189            worktree_store,
190            fs,
191            cx,
192        )
193    }
194
195    fn new(
196        mode: DapStoreMode,
197        breakpoint_store: Entity<BreakpointStore>,
198        worktree_store: Entity<WorktreeStore>,
199        fs: Arc<dyn Fs>,
200        cx: &mut Context<Self>,
201    ) -> Self {
202        cx.background_spawn(async move {
203            let dir = paths::debug_adapters_dir().join("js-debug-companion");
204
205            let mut children = fs.read_dir(&dir).await?.try_collect::<Vec<_>>().await?;
206            children.sort_by_key(|child| semver::Version::parse(child.file_name()?.to_str()?).ok());
207
208            if let Some(child) = children.last()
209                && let Some(name) = child.file_name()
210                && let Some(name) = name.to_str()
211                && semver::Version::parse(name).is_ok()
212            {
213                children.pop();
214            }
215
216            for child in children {
217                fs.remove_dir(
218                    &child,
219                    RemoveOptions {
220                        recursive: true,
221                        ignore_if_not_exists: true,
222                    },
223                )
224                .await
225                .ok();
226            }
227
228            anyhow::Ok(())
229        })
230        .detach();
231
232        Self {
233            mode,
234            next_session_id: 0,
235            downstream_client: None,
236            breakpoint_store,
237            worktree_store,
238            sessions: Default::default(),
239            adapter_options: Default::default(),
240        }
241    }
242
243    pub fn get_debug_adapter_binary(
244        &mut self,
245        definition: DebugTaskDefinition,
246        session_id: SessionId,
247        worktree: &Entity<Worktree>,
248        console: UnboundedSender<String>,
249        cx: &mut Context<Self>,
250    ) -> Task<Result<DebugAdapterBinary>> {
251        match &self.mode {
252            DapStoreMode::Local(_) => {
253                let Some(adapter) = DapRegistry::global(cx).adapter(&definition.adapter) else {
254                    return Task::ready(Err(anyhow!("Failed to find a debug adapter")));
255                };
256
257                let settings_location = SettingsLocation {
258                    worktree_id: worktree.read(cx).id(),
259                    path: RelPath::empty(),
260                };
261                let dap_settings = ProjectSettings::get(Some(settings_location), cx)
262                    .dap
263                    .get(&adapter.name());
264                let user_installed_path = dap_settings.and_then(|s| match &s.binary {
265                    DapBinary::Default => None,
266                    DapBinary::Custom(binary) => {
267                        let path = PathBuf::from(binary);
268                        Some(worktree.read(cx).resolve_relative_path(path))
269                    }
270                });
271                let user_args = dap_settings.and_then(|s| s.args.clone());
272                let user_env = dap_settings.and_then(|s| s.env.clone());
273
274                let delegate = self.delegate(worktree, console, cx);
275
276                let worktree = worktree.clone();
277                cx.spawn(async move |this, cx| {
278                    let mut binary = adapter
279                        .get_binary(
280                            &delegate,
281                            &definition,
282                            user_installed_path,
283                            user_args,
284                            user_env,
285                            cx,
286                        )
287                        .await?;
288
289                    let env = this
290                        .update(cx, |this, cx| {
291                            this.as_local()
292                                .unwrap()
293                                .environment
294                                .update(cx, |environment, cx| {
295                                    environment.worktree_environment(worktree, cx)
296                                })
297                        })?
298                        .await;
299
300                    if let Some(mut env) = env {
301                        env.extend(std::mem::take(&mut binary.envs));
302                        binary.envs = env;
303                    }
304
305                    Ok(binary)
306                })
307            }
308            DapStoreMode::Remote(remote) => {
309                let request = remote
310                    .upstream_client
311                    .request(proto::GetDebugAdapterBinary {
312                        session_id: session_id.to_proto(),
313                        project_id: remote.upstream_project_id,
314                        worktree_id: worktree.read(cx).id().to_proto(),
315                        definition: Some(definition.to_proto()),
316                    });
317                let remote = remote.remote_client.clone();
318
319                cx.spawn(async move |_, cx| {
320                    let response = request.await?;
321                    let binary = DebugAdapterBinary::from_proto(response)?;
322
323                    let port_forwarding;
324                    let connection;
325                    if let Some(c) = binary.connection {
326                        let host = IpAddr::V4(Ipv4Addr::LOCALHOST);
327                        let port;
328                        if remote.read_with(cx, |remote, _cx| remote.shares_network_interface()) {
329                            port = c.port;
330                            port_forwarding = None;
331                        } else {
332                            port = dap::transport::TcpTransport::unused_port(host).await?;
333                            port_forwarding = Some((port, c.host.to_string(), c.port));
334                        }
335                        connection = Some(TcpArguments {
336                            port,
337                            host,
338                            timeout: c.timeout,
339                        })
340                    } else {
341                        port_forwarding = None;
342                        connection = None;
343                    }
344
345                    let command = remote.read_with(cx, |remote, _cx| {
346                        remote.build_command(
347                            binary.command,
348                            &binary.arguments,
349                            &binary.envs,
350                            binary.cwd.map(|path| path.display().to_string()),
351                            port_forwarding,
352                            Interactive::No,
353                        )
354                    })?;
355
356                    Ok(DebugAdapterBinary {
357                        command: Some(command.program),
358                        arguments: command.args,
359                        envs: command.env,
360                        cwd: None,
361                        connection,
362                        request_args: binary.request_args,
363                    })
364                })
365            }
366            DapStoreMode::Collab => {
367                Task::ready(Err(anyhow!("Debugging is not yet supported via collab")))
368            }
369        }
370    }
371
372    pub fn debug_scenario_for_build_task(
373        &self,
374        build: TaskTemplate,
375        adapter: DebugAdapterName,
376        label: SharedString,
377        cx: &mut App,
378    ) -> Task<Option<DebugScenario>> {
379        let locators = DapRegistry::global(cx).locators();
380
381        cx.background_spawn(async move {
382            for locator in locators.values() {
383                if let Some(scenario) = locator.create_scenario(&build, &label, &adapter).await {
384                    return Some(scenario);
385                }
386            }
387            None
388        })
389    }
390
391    pub fn run_debug_locator(
392        &mut self,
393        locator_name: &str,
394        build_command: SpawnInTerminal,
395        cx: &mut Context<Self>,
396    ) -> Task<Result<DebugRequest>> {
397        match &self.mode {
398            DapStoreMode::Local(_) => {
399                // Pre-resolve args with existing environment.
400                let locators = DapRegistry::global(cx).locators();
401                let locator = locators.get(locator_name);
402                let executor = cx.background_executor().clone();
403
404                if let Some(locator) = locator.cloned() {
405                    cx.background_spawn(async move {
406                        let result = locator
407                            .run(build_command.clone(), executor)
408                            .await
409                            .log_with_level(log::Level::Error);
410                        if let Some(result) = result {
411                            return Ok(result);
412                        }
413
414                        anyhow::bail!(
415                            "None of the locators for task `{}` completed successfully",
416                            build_command.label
417                        )
418                    })
419                } else {
420                    Task::ready(Err(anyhow!(
421                        "Couldn't find any locator for task `{}`. Specify the `attach` or `launch` arguments in your debug scenario definition",
422                        build_command.label
423                    )))
424                }
425            }
426            DapStoreMode::Remote(remote) => {
427                let request = remote.upstream_client.request(proto::RunDebugLocators {
428                    project_id: remote.upstream_project_id,
429                    build_command: Some(build_command.to_proto()),
430                    locator: locator_name.to_owned(),
431                });
432                cx.background_spawn(async move {
433                    let response = request.await?;
434                    DebugRequest::from_proto(response)
435                })
436            }
437            DapStoreMode::Collab => {
438                Task::ready(Err(anyhow!("Debugging is not yet supported via collab")))
439            }
440        }
441    }
442
443    fn as_local(&self) -> Option<&LocalDapStore> {
444        match &self.mode {
445            DapStoreMode::Local(local_dap_store) => Some(local_dap_store),
446            _ => None,
447        }
448    }
449
450    pub fn new_session(
451        &mut self,
452        label: Option<SharedString>,
453        adapter: DebugAdapterName,
454        task_context: SharedTaskContext,
455        parent_session: Option<Entity<Session>>,
456        quirks: SessionQuirks,
457        cx: &mut Context<Self>,
458    ) -> Entity<Session> {
459        let session_id = SessionId(util::post_inc(&mut self.next_session_id));
460
461        if let Some(session) = &parent_session {
462            session.update(cx, |session, _| {
463                session.add_child_session_id(session_id);
464            });
465        }
466
467        let (remote_client, node_runtime, http_client) = match &self.mode {
468            DapStoreMode::Local(_) => (None, None, None),
469            DapStoreMode::Remote(remote_dap_store) => (
470                Some(remote_dap_store.remote_client.clone()),
471                Some(remote_dap_store.node_runtime.clone()),
472                Some(remote_dap_store.http_client.clone()),
473            ),
474            DapStoreMode::Collab => (None, None, None),
475        };
476        let session = Session::new(
477            self.breakpoint_store.clone(),
478            session_id,
479            parent_session,
480            label,
481            adapter,
482            task_context,
483            quirks,
484            remote_client,
485            node_runtime,
486            http_client,
487            cx,
488        );
489
490        self.sessions.insert(session_id, session.clone());
491        cx.notify();
492
493        cx.subscribe(&session, {
494            move |this: &mut DapStore, _, event: &SessionStateEvent, cx| match event {
495                SessionStateEvent::Shutdown => {
496                    this.shutdown_session(session_id, cx).detach_and_log_err(cx);
497                }
498                SessionStateEvent::Restart | SessionStateEvent::SpawnChildSession { .. } => {}
499                SessionStateEvent::Running => {
500                    cx.emit(DapStoreEvent::DebugClientStarted(session_id));
501                }
502            }
503        })
504        .detach();
505
506        session
507    }
508
509    pub fn boot_session(
510        &self,
511        session: Entity<Session>,
512        definition: DebugTaskDefinition,
513        worktree: Entity<Worktree>,
514        cx: &mut Context<Self>,
515    ) -> Task<Result<()>> {
516        let dap_store = cx.weak_entity();
517        let console = session.update(cx, |session, cx| session.console_output(cx));
518        let session_id = session.read(cx).session_id();
519
520        cx.spawn({
521            let session = session.clone();
522            async move |this, cx| {
523                let binary = this
524                    .update(cx, |this, cx| {
525                        this.get_debug_adapter_binary(
526                            definition.clone(),
527                            session_id,
528                            &worktree,
529                            console,
530                            cx,
531                        )
532                    })?
533                    .await?;
534                session
535                    .update(cx, |session, cx| {
536                        session.boot(binary, worktree, dap_store, cx)
537                    })
538                    .await
539            }
540        })
541    }
542
543    pub fn session_by_id(
544        &self,
545        session_id: impl Borrow<SessionId>,
546    ) -> Option<Entity<session::Session>> {
547        let session_id = session_id.borrow();
548
549        self.sessions.get(session_id).cloned()
550    }
551    pub fn sessions(&self) -> impl Iterator<Item = &Entity<Session>> {
552        self.sessions.values()
553    }
554
555    pub fn capabilities_by_id(
556        &self,
557        session_id: impl Borrow<SessionId>,
558        cx: &App,
559    ) -> Option<Capabilities> {
560        let session_id = session_id.borrow();
561        self.sessions
562            .get(session_id)
563            .map(|client| client.read(cx).capabilities.clone())
564    }
565
566    pub fn breakpoint_store(&self) -> &Entity<BreakpointStore> {
567        &self.breakpoint_store
568    }
569
570    pub fn worktree_store(&self) -> &Entity<WorktreeStore> {
571        &self.worktree_store
572    }
573
574    #[allow(dead_code)]
575    async fn handle_ignore_breakpoint_state(
576        this: Entity<Self>,
577        envelope: TypedEnvelope<proto::IgnoreBreakpointState>,
578        mut cx: AsyncApp,
579    ) -> Result<()> {
580        let session_id = SessionId::from_proto(envelope.payload.session_id);
581
582        this.update(&mut cx, |this, cx| {
583            if let Some(session) = this.session_by_id(&session_id) {
584                session.update(cx, |session, cx| {
585                    session.set_ignore_breakpoints(envelope.payload.ignore, cx)
586                })
587            } else {
588                Task::ready(HashMap::default())
589            }
590        })
591        .await;
592
593        Ok(())
594    }
595
596    fn delegate(
597        &self,
598        worktree: &Entity<Worktree>,
599        console: UnboundedSender<String>,
600        cx: &mut App,
601    ) -> Arc<dyn DapDelegate> {
602        let Some(local_store) = self.as_local() else {
603            unimplemented!("Starting session on remote side");
604        };
605
606        Arc::new(DapAdapterDelegate::new(
607            local_store.fs.clone(),
608            worktree.read(cx).snapshot(),
609            console,
610            local_store.node_runtime.clone(),
611            local_store.http_client.clone(),
612            local_store.toolchain_store.clone(),
613            local_store
614                .environment
615                .update(cx, |env, cx| env.worktree_environment(worktree.clone(), cx)),
616            local_store.is_headless,
617        ))
618    }
619
620    pub fn resolve_inline_value_locations(
621        &self,
622        session: Entity<Session>,
623        stack_frame_id: StackFrameId,
624        buffer_handle: Entity<Buffer>,
625        inline_value_locations: Vec<dap::inline_value::InlineValueLocation>,
626        cx: &mut Context<Self>,
627    ) -> Task<Result<Vec<InlayHint>>> {
628        let snapshot = buffer_handle.read(cx).snapshot();
629        let local_variables =
630            session
631                .read(cx)
632                .variables_by_stack_frame_id(stack_frame_id, false, true);
633        let global_variables =
634            session
635                .read(cx)
636                .variables_by_stack_frame_id(stack_frame_id, true, false);
637
638        fn format_value(mut value: String) -> String {
639            const LIMIT: usize = 100;
640
641            if let Some(index) = value.find("\n") {
642                value.truncate(index);
643                value.push_str("…");
644            }
645
646            if value.len() > LIMIT {
647                let mut index = LIMIT;
648                // If index isn't a char boundary truncate will cause a panic
649                while !value.is_char_boundary(index) {
650                    index -= 1;
651                }
652                value.truncate(index);
653                value.push_str("…");
654            }
655
656            format!(": {}", value)
657        }
658
659        cx.spawn(async move |_, cx| {
660            let mut inlay_hints = Vec::with_capacity(inline_value_locations.len());
661            for inline_value_location in inline_value_locations.iter() {
662                let point = snapshot.point_to_point_utf16(language::Point::new(
663                    inline_value_location.row as u32,
664                    inline_value_location.column as u32,
665                ));
666                let position = snapshot.anchor_after(point);
667
668                match inline_value_location.lookup {
669                    VariableLookupKind::Variable => {
670                        let variable_search =
671                            if inline_value_location.scope
672                                == dap::inline_value::VariableScope::Local
673                            {
674                                local_variables.iter().chain(global_variables.iter()).find(
675                                    |variable| variable.name == inline_value_location.variable_name,
676                                )
677                            } else {
678                                global_variables.iter().find(|variable| {
679                                    variable.name == inline_value_location.variable_name
680                                })
681                            };
682
683                        let Some(variable) = variable_search else {
684                            continue;
685                        };
686
687                        inlay_hints.push(InlayHint {
688                            position,
689                            label: InlayHintLabel::String(format_value(variable.value.clone())),
690                            kind: Some(InlayHintKind::Type),
691                            padding_left: false,
692                            padding_right: false,
693                            tooltip: None,
694                            resolve_state: ResolveState::Resolved,
695                        });
696                    }
697                    VariableLookupKind::Expression => {
698                        let eval_task = session.read_with(cx, |session, _| {
699                            session.state.request_dap(EvaluateCommand {
700                                expression: inline_value_location.variable_name.clone(),
701                                frame_id: Some(stack_frame_id),
702                                source: None,
703                                context: Some(EvaluateArgumentsContext::Variables),
704                            })
705                        });
706
707                        if let Some(response) = eval_task.await.log_err() {
708                            inlay_hints.push(InlayHint {
709                                position,
710                                label: InlayHintLabel::String(format_value(response.result)),
711                                kind: Some(InlayHintKind::Type),
712                                padding_left: false,
713                                padding_right: false,
714                                tooltip: None,
715                                resolve_state: ResolveState::Resolved,
716                            });
717                        };
718                    }
719                };
720            }
721
722            Ok(inlay_hints)
723        })
724    }
725
726    pub fn shutdown_sessions(&mut self, cx: &mut Context<Self>) -> Task<()> {
727        let mut tasks = vec![];
728        for session_id in self.sessions.keys().cloned().collect::<Vec<_>>() {
729            tasks.push(self.shutdown_session(session_id, cx));
730        }
731
732        cx.background_executor().spawn(async move {
733            futures::future::join_all(tasks).await;
734        })
735    }
736
737    pub fn shutdown_session(
738        &mut self,
739        session_id: SessionId,
740        cx: &mut Context<Self>,
741    ) -> Task<Result<()>> {
742        let Some(session) = self.sessions.remove(&session_id) else {
743            return Task::ready(Err(anyhow!("Could not find session: {:?}", session_id)));
744        };
745
746        let shutdown_children = session
747            .read(cx)
748            .child_session_ids()
749            .iter()
750            .map(|session_id| self.shutdown_session(*session_id, cx))
751            .collect::<Vec<_>>();
752
753        let shutdown_parent_task = if let Some(parent_session) = session
754            .read(cx)
755            .parent_id(cx)
756            .and_then(|session_id| self.session_by_id(session_id))
757        {
758            let shutdown_id = parent_session.update(cx, |parent_session, _| {
759                parent_session.remove_child_session_id(session_id);
760
761                if parent_session.child_session_ids().is_empty() {
762                    Some(parent_session.session_id())
763                } else {
764                    None
765                }
766            });
767
768            shutdown_id.map(|session_id| self.shutdown_session(session_id, cx))
769        } else {
770            None
771        };
772
773        let shutdown_task = session.update(cx, |this, cx| this.shutdown(cx));
774
775        cx.emit(DapStoreEvent::DebugClientShutdown(session_id));
776
777        cx.background_spawn(async move {
778            if !shutdown_children.is_empty() {
779                let _ = join_all(shutdown_children).await;
780            }
781
782            shutdown_task.await;
783
784            if let Some(parent_task) = shutdown_parent_task {
785                parent_task.await?;
786            }
787
788            Ok(())
789        })
790    }
791
792    pub fn shared(
793        &mut self,
794        project_id: u64,
795        downstream_client: AnyProtoClient,
796        _: &mut Context<Self>,
797    ) {
798        self.downstream_client = Some((downstream_client, project_id));
799    }
800
801    pub fn unshared(&mut self, cx: &mut Context<Self>) {
802        self.downstream_client.take();
803
804        cx.notify();
805    }
806
807    async fn handle_run_debug_locator(
808        this: Entity<Self>,
809        envelope: TypedEnvelope<proto::RunDebugLocators>,
810        mut cx: AsyncApp,
811    ) -> Result<proto::DebugRequest> {
812        let task = envelope
813            .payload
814            .build_command
815            .context("missing definition")?;
816        let build_task = SpawnInTerminal::from_proto(task);
817        let locator = envelope.payload.locator;
818        let request = this
819            .update(&mut cx, |this, cx| {
820                this.run_debug_locator(&locator, build_task, cx)
821            })
822            .await?;
823
824        Ok(request.to_proto())
825    }
826
827    async fn handle_get_debug_adapter_binary(
828        this: Entity<Self>,
829        envelope: TypedEnvelope<proto::GetDebugAdapterBinary>,
830        mut cx: AsyncApp,
831    ) -> Result<proto::DebugAdapterBinary> {
832        let definition = DebugTaskDefinition::from_proto(
833            envelope.payload.definition.context("missing definition")?,
834        )?;
835        let (tx, mut rx) = mpsc::unbounded();
836        let session_id = envelope.payload.session_id;
837        cx.spawn({
838            let this = this.clone();
839            async move |cx| {
840                while let Some(message) = rx.next().await {
841                    this.read_with(cx, |this, _| {
842                        if let Some((downstream, project_id)) = this.downstream_client.clone() {
843                            downstream
844                                .send(proto::LogToDebugConsole {
845                                    project_id,
846                                    session_id,
847                                    message,
848                                })
849                                .ok();
850                        }
851                    });
852                }
853            }
854        })
855        .detach();
856
857        let worktree = this
858            .update(&mut cx, |this, cx| {
859                this.worktree_store
860                    .read(cx)
861                    .worktree_for_id(WorktreeId::from_proto(envelope.payload.worktree_id), cx)
862            })
863            .context("Failed to find worktree with a given ID")?;
864        let binary = this
865            .update(&mut cx, |this, cx| {
866                this.get_debug_adapter_binary(
867                    definition,
868                    SessionId::from_proto(session_id),
869                    &worktree,
870                    tx,
871                    cx,
872                )
873            })
874            .await?;
875        Ok(binary.to_proto())
876    }
877
878    async fn handle_log_to_debug_console(
879        this: Entity<Self>,
880        envelope: TypedEnvelope<proto::LogToDebugConsole>,
881        mut cx: AsyncApp,
882    ) -> Result<()> {
883        let session_id = SessionId::from_proto(envelope.payload.session_id);
884        this.update(&mut cx, |this, cx| {
885            let Some(session) = this.sessions.get(&session_id) else {
886                return;
887            };
888            session.update(cx, |session, cx| {
889                session
890                    .console_output(cx)
891                    .unbounded_send(envelope.payload.message)
892                    .ok();
893            })
894        });
895        Ok(())
896    }
897
898    pub fn sync_adapter_options(
899        &mut self,
900        session: &Entity<Session>,
901        cx: &App,
902    ) -> Arc<PersistedAdapterOptions> {
903        let session = session.read(cx);
904        let adapter = session.adapter();
905        let exceptions = session.exception_breakpoints();
906        let exception_breakpoints = exceptions
907            .map(|(exception, enabled)| {
908                (
909                    exception.filter.clone(),
910                    PersistedExceptionBreakpoint { enabled: *enabled },
911                )
912            })
913            .collect();
914        let options = Arc::new(PersistedAdapterOptions {
915            exception_breakpoints,
916        });
917        self.adapter_options.insert(adapter, options.clone());
918        options
919    }
920
921    pub fn set_adapter_options(
922        &mut self,
923        adapter: DebugAdapterName,
924        options: PersistedAdapterOptions,
925    ) {
926        self.adapter_options.insert(adapter, Arc::new(options));
927    }
928
929    pub fn adapter_options(&self, name: &str) -> Option<Arc<PersistedAdapterOptions>> {
930        self.adapter_options.get(name).cloned()
931    }
932
933    pub fn all_adapter_options(&self) -> &BTreeMap<DebugAdapterName, Arc<PersistedAdapterOptions>> {
934        &self.adapter_options
935    }
936}
937
938#[derive(Clone)]
939pub struct DapAdapterDelegate {
940    fs: Arc<dyn Fs>,
941    console: mpsc::UnboundedSender<String>,
942    worktree: worktree::Snapshot,
943    node_runtime: NodeRuntime,
944    http_client: Arc<dyn HttpClient>,
945    toolchain_store: Arc<dyn LanguageToolchainStore>,
946    load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
947    is_headless: bool,
948}
949
950impl DapAdapterDelegate {
951    pub fn new(
952        fs: Arc<dyn Fs>,
953        worktree: worktree::Snapshot,
954        status: mpsc::UnboundedSender<String>,
955        node_runtime: NodeRuntime,
956        http_client: Arc<dyn HttpClient>,
957        toolchain_store: Arc<dyn LanguageToolchainStore>,
958        load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
959        is_headless: bool,
960    ) -> Self {
961        Self {
962            fs,
963            console: status,
964            worktree,
965            http_client,
966            node_runtime,
967            toolchain_store,
968            load_shell_env_task,
969            is_headless,
970        }
971    }
972}
973
974#[async_trait]
975impl dap::adapters::DapDelegate for DapAdapterDelegate {
976    fn worktree_id(&self) -> WorktreeId {
977        self.worktree.id()
978    }
979
980    fn worktree_root_path(&self) -> &Path {
981        self.worktree.abs_path()
982    }
983    fn http_client(&self) -> Arc<dyn HttpClient> {
984        self.http_client.clone()
985    }
986
987    fn node_runtime(&self) -> NodeRuntime {
988        self.node_runtime.clone()
989    }
990
991    fn fs(&self) -> Arc<dyn Fs> {
992        self.fs.clone()
993    }
994
995    fn output_to_console(&self, msg: String) {
996        self.console.unbounded_send(msg).ok();
997    }
998
999    #[cfg(not(target_os = "windows"))]
1000    async fn which(&self, command: &OsStr) -> Option<PathBuf> {
1001        let worktree_abs_path = self.worktree.abs_path();
1002        let shell_path = self.shell_env().await.get("PATH").cloned();
1003        which::which_in(command, shell_path.as_ref(), worktree_abs_path).ok()
1004    }
1005
1006    #[cfg(target_os = "windows")]
1007    async fn which(&self, command: &OsStr) -> Option<PathBuf> {
1008        // On Windows, `PATH` is handled differently from Unix. Windows generally expects users to modify the `PATH` themselves,
1009        // and every program loads it directly from the system at startup.
1010        // There's also no concept of a default shell on Windows, and you can't really retrieve one, so trying to get shell environment variables
1011        // from a specific directory doesn’t make sense on Windows.
1012        which::which(command).ok()
1013    }
1014
1015    async fn shell_env(&self) -> HashMap<String, String> {
1016        let task = self.load_shell_env_task.clone();
1017        task.await.unwrap_or_default()
1018    }
1019
1020    fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore> {
1021        self.toolchain_store.clone()
1022    }
1023
1024    async fn read_text_file(&self, path: &RelPath) -> Result<String> {
1025        let entry = self
1026            .worktree
1027            .entry_for_path(path)
1028            .with_context(|| format!("no worktree entry for path {path:?}"))?;
1029        let abs_path = self.worktree.absolutize(&entry.path);
1030
1031        self.fs.load(&abs_path).await
1032    }
1033
1034    fn is_headless(&self) -> bool {
1035        self.is_headless
1036    }
1037}
1038
Served at tenant.openagents/omega Member data and write actions are omitted.