Skip to repository content

tenant.openagents/omega

No repository description is available.

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

headless_project.rs

1370 lines · 51.5 KB · rust
1use anyhow::{Context as _, Result, anyhow};
2use client::ProjectId;
3use collections::HashMap;
4use collections::HashSet;
5use gpui::TasksIncluded;
6use language::File;
7use lsp::LanguageServerId;
8
9use extension::ExtensionHostProxy;
10use extension_host::headless_host::HeadlessExtensionStore;
11use fs::Fs;
12use gpui::{App, AppContext as _, AsyncApp, Context, Entity, PromptLevel, TaskExt};
13use http_client::HttpClient;
14use language::{Buffer, BufferEvent, LanguageRegistry, proto::serialize_operation};
15use node_runtime::NodeRuntime;
16use project::{
17    AgentRegistryStore, LspStore, LspStoreEvent, ManifestTree, PrettierStore, ProjectEnvironment,
18    ProjectPath, ToolchainStore, WorktreeId,
19    agent_server_store::AgentServerStore,
20    buffer_store::{BufferStore, BufferStoreEvent},
21    context_server_store::ContextServerStore,
22    debugger::{breakpoint_store::BreakpointStore, dap_store::DapStore},
23    git_store::GitStore,
24    image_store::ImageId,
25    lsp_store::log_store::{self, GlobalLogStore, LanguageServerKind, LogKind},
26    project_settings::SettingsObserver,
27    search::SearchQuery,
28    task_store::TaskStore,
29    trusted_worktrees::{PathTrust, RemoteHostLocation, TrustedWorktrees},
30    worktree_store::{WorktreeIdCounter, WorktreeStore},
31};
32use rpc::{
33    AnyProtoClient, TypedEnvelope,
34    proto::{self, REMOTE_SERVER_PEER_ID, REMOTE_SERVER_PROJECT_ID},
35};
36use smol::process::Child;
37
38use settings::initial_server_settings_content;
39use std::{
40    num::NonZeroU64,
41    path::{Path, PathBuf},
42    sync::{
43        Arc,
44        atomic::{AtomicU64, AtomicUsize, Ordering},
45    },
46    time::Instant,
47};
48use sysinfo::{ProcessRefreshKind, RefreshKind, System, UpdateKind};
49use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
50use worktree::Worktree;
51
52pub struct HeadlessProject {
53    pub fs: Arc<dyn Fs>,
54    pub session: AnyProtoClient,
55    pub worktree_store: Entity<WorktreeStore>,
56    pub buffer_store: Entity<BufferStore>,
57    pub lsp_store: Entity<LspStore>,
58    pub task_store: Entity<TaskStore>,
59    pub dap_store: Entity<DapStore>,
60    pub breakpoint_store: Entity<BreakpointStore>,
61    pub agent_server_store: Entity<AgentServerStore>,
62    pub context_server_store: Entity<ContextServerStore>,
63    pub settings_observer: Entity<SettingsObserver>,
64    pub next_entry_id: Arc<AtomicUsize>,
65    pub languages: Arc<LanguageRegistry>,
66    pub extensions: Entity<HeadlessExtensionStore>,
67    pub git_store: Entity<GitStore>,
68    pub environment: Entity<ProjectEnvironment>,
69    pub profiling_collector: gpui::ProfilingCollector,
70    // Used mostly to keep alive the toolchain store for RPC handlers.
71    // Local variant is used within LSP store, but that's a separate entity.
72    pub _toolchain_store: Entity<ToolchainStore>,
73    pub kernels: HashMap<String, Child>,
74}
75
76pub struct HeadlessAppState {
77    pub session: AnyProtoClient,
78    pub fs: Arc<dyn Fs>,
79    pub http_client: Arc<dyn HttpClient>,
80    pub node_runtime: NodeRuntime,
81    pub languages: Arc<LanguageRegistry>,
82    pub extension_host_proxy: Arc<ExtensionHostProxy>,
83    pub startup_time: Instant,
84}
85
86impl HeadlessProject {
87    pub fn init(cx: &mut App) {
88        settings::init(cx);
89        log_store::init(true, cx);
90    }
91
92    pub fn new(
93        HeadlessAppState {
94            session,
95            fs,
96            http_client,
97            node_runtime,
98            languages,
99            extension_host_proxy: proxy,
100            startup_time,
101        }: HeadlessAppState,
102        init_worktree_trust: bool,
103        cx: &mut Context<Self>,
104    ) -> Self {
105        debug_adapter_extension::init(proxy.clone(), cx);
106        languages::init(languages.clone(), fs.clone(), node_runtime.clone(), cx);
107
108        let worktree_store = cx.new(|cx| {
109            let mut store = WorktreeStore::local(true, fs.clone(), WorktreeIdCounter::get(cx));
110            store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
111            store
112        });
113
114        if init_worktree_trust {
115            project::trusted_worktrees::track_worktree_trust(
116                worktree_store.clone(),
117                None::<RemoteHostLocation>,
118                Some((session.clone(), ProjectId(REMOTE_SERVER_PROJECT_ID))),
119                None,
120                cx,
121            );
122        }
123
124        let environment =
125            cx.new(|cx| ProjectEnvironment::new(None, worktree_store.downgrade(), None, true, cx));
126        let manifest_tree = ManifestTree::new(worktree_store.clone(), cx);
127        let toolchain_store = cx.new(|cx| {
128            ToolchainStore::local(
129                languages.clone(),
130                worktree_store.clone(),
131                environment.clone(),
132                manifest_tree.clone(),
133                cx,
134            )
135        });
136
137        let buffer_store = cx.new(|cx| {
138            let mut buffer_store = BufferStore::local(worktree_store.clone(), cx);
139            buffer_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
140            buffer_store
141        });
142
143        let breakpoint_store = cx.new(|_| {
144            let mut breakpoint_store =
145                BreakpointStore::local(worktree_store.clone(), buffer_store.clone());
146            breakpoint_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone());
147
148            breakpoint_store
149        });
150
151        let dap_store = cx.new(|cx| {
152            let mut dap_store = DapStore::new_local(
153                http_client.clone(),
154                node_runtime.clone(),
155                fs.clone(),
156                environment.clone(),
157                toolchain_store.read(cx).as_language_toolchain_store(),
158                worktree_store.clone(),
159                breakpoint_store.clone(),
160                true,
161                cx,
162            );
163            dap_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
164            dap_store
165        });
166
167        let git_store = cx.new(|cx| {
168            let mut store = GitStore::local(
169                &worktree_store,
170                buffer_store.clone(),
171                environment.clone(),
172                fs.clone(),
173                cx,
174            );
175            store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
176            store
177        });
178
179        let prettier_store = cx.new(|cx| {
180            PrettierStore::new(
181                node_runtime.clone(),
182                fs.clone(),
183                languages.clone(),
184                worktree_store.clone(),
185                cx,
186            )
187        });
188
189        let task_store = cx.new(|cx| {
190            let mut task_store = TaskStore::local(
191                buffer_store.downgrade(),
192                worktree_store.clone(),
193                toolchain_store.read(cx).as_language_toolchain_store(),
194                environment.clone(),
195                git_store.clone(),
196                cx,
197            );
198            task_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
199            task_store
200        });
201        let settings_observer = cx.new(|cx| {
202            let mut observer = SettingsObserver::new_local(
203                fs.clone(),
204                worktree_store.clone(),
205                task_store.clone(),
206                true,
207                cx,
208            );
209            observer.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
210            observer
211        });
212
213        let lsp_store = cx.new(|cx| {
214            let mut lsp_store = LspStore::new_local(
215                buffer_store.clone(),
216                worktree_store.clone(),
217                prettier_store.clone(),
218                toolchain_store
219                    .read(cx)
220                    .as_local_store()
221                    .expect("Toolchain store to be local")
222                    .clone(),
223                environment.clone(),
224                manifest_tree,
225                languages.clone(),
226                http_client.clone(),
227                fs.clone(),
228                cx,
229            );
230            lsp_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
231            lsp_store
232        });
233
234        AgentRegistryStore::init_global(cx, fs.clone(), http_client.clone());
235
236        let agent_server_store = cx.new(|cx| {
237            let mut agent_server_store = AgentServerStore::local(
238                node_runtime.clone(),
239                fs.clone(),
240                environment.clone(),
241                http_client.clone(),
242                cx,
243            );
244            agent_server_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
245            agent_server_store
246        });
247
248        let context_server_store = cx.new(|cx| {
249            let mut context_server_store =
250                ContextServerStore::local(worktree_store.clone(), None, true, cx);
251            context_server_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone());
252            context_server_store
253        });
254
255        cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
256        language_extension::init(
257            language_extension::LspAccess::ViaLspStore(lsp_store.downgrade()),
258            proxy.clone(),
259            languages.clone(),
260        );
261
262        cx.subscribe(&buffer_store, |_this, _buffer_store, event, cx| {
263            if let BufferStoreEvent::BufferAdded(buffer) = event {
264                cx.subscribe(buffer, Self::on_buffer_event).detach();
265            }
266        })
267        .detach();
268
269        let extensions = HeadlessExtensionStore::new(
270            fs.clone(),
271            http_client.clone(),
272            paths::remote_extensions_dir().to_path_buf(),
273            proxy,
274            node_runtime,
275            cx,
276        );
277
278        // local_machine -> ssh handlers
279        session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &worktree_store);
280        session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &buffer_store);
281        session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &cx.entity());
282        session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &lsp_store);
283        session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &task_store);
284        session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &toolchain_store);
285        session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &dap_store);
286        session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &breakpoint_store);
287        session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &settings_observer);
288        session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &git_store);
289        session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &agent_server_store);
290        session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &context_server_store);
291
292        session.add_request_handler(cx.weak_entity(), Self::handle_list_remote_directory);
293        session.add_request_handler(cx.weak_entity(), Self::handle_get_path_metadata);
294        session.add_request_handler(cx.weak_entity(), Self::handle_shutdown_remote_server);
295        session.add_request_handler(cx.weak_entity(), Self::handle_ping);
296        session.add_request_handler(cx.weak_entity(), Self::handle_get_processes);
297        session.add_request_handler(cx.weak_entity(), Self::handle_get_remote_profiling_data);
298
299        session.add_entity_request_handler(Self::handle_add_worktree);
300        session.add_request_handler(cx.weak_entity(), Self::handle_remove_worktree);
301
302        session.add_entity_request_handler(Self::handle_open_buffer_by_path);
303        session.add_entity_request_handler(Self::handle_open_new_buffer);
304        session.add_entity_request_handler(Self::handle_find_search_candidates);
305        session.add_entity_request_handler(Self::handle_open_server_settings);
306        session.add_entity_request_handler(Self::handle_get_directory_environment);
307        session.add_entity_message_handler(Self::handle_toggle_lsp_logs);
308        session.add_entity_request_handler(Self::handle_open_image_by_path);
309        session.add_entity_request_handler(Self::handle_trust_worktrees);
310        session.add_entity_request_handler(Self::handle_restrict_worktrees);
311        session.add_entity_request_handler(Self::handle_download_file_by_path);
312
313        session.add_entity_message_handler(Self::handle_find_search_candidates_cancel);
314        session.add_entity_request_handler(BufferStore::handle_update_buffer);
315        session.add_entity_message_handler(BufferStore::handle_close_buffer);
316
317        session.add_request_handler(
318            extensions.downgrade(),
319            HeadlessExtensionStore::handle_sync_extensions,
320        );
321        session.add_request_handler(
322            extensions.downgrade(),
323            HeadlessExtensionStore::handle_install_extension,
324        );
325
326        session.add_request_handler(cx.weak_entity(), Self::handle_spawn_kernel);
327        session.add_request_handler(cx.weak_entity(), Self::handle_kill_kernel);
328
329        BufferStore::init(&session);
330        WorktreeStore::init(&session);
331        SettingsObserver::init(&session);
332        LspStore::init(&session);
333        TaskStore::init(Some(&session));
334        ToolchainStore::init(&session);
335        DapStore::init(&session, cx);
336        // todo(debugger): Re init breakpoint store when we set it up for collab
337        BreakpointStore::init(&session);
338        GitStore::init(&session);
339        AgentServerStore::init_headless(&session);
340        ContextServerStore::init_headless(&session);
341
342        HeadlessProject {
343            next_entry_id: Default::default(),
344            session,
345            settings_observer,
346            fs,
347            worktree_store,
348            buffer_store,
349            lsp_store,
350            task_store,
351            dap_store,
352            breakpoint_store,
353            agent_server_store,
354            context_server_store,
355            languages,
356            extensions,
357            git_store,
358            environment,
359            profiling_collector: gpui::ProfilingCollector::new(startup_time),
360            _toolchain_store: toolchain_store,
361            kernels: Default::default(),
362        }
363    }
364
365    fn on_buffer_event(
366        &mut self,
367        buffer: Entity<Buffer>,
368        event: &BufferEvent,
369        cx: &mut Context<Self>,
370    ) {
371        if let BufferEvent::Operation {
372            operation,
373            is_local: true,
374        } = event
375        {
376            cx.background_spawn(self.session.request(proto::UpdateBuffer {
377                project_id: REMOTE_SERVER_PROJECT_ID,
378                buffer_id: buffer.read(cx).remote_id().to_proto(),
379                operations: vec![serialize_operation(operation)],
380            }))
381            .detach()
382        }
383    }
384
385    fn on_lsp_store_event(
386        &mut self,
387        lsp_store: Entity<LspStore>,
388        event: &LspStoreEvent,
389        cx: &mut Context<Self>,
390    ) {
391        match event {
392            LspStoreEvent::LanguageServerAdded(id, name, worktree_id) => {
393                let log_store = cx
394                    .try_global::<GlobalLogStore>()
395                    .map(|lsp_logs| lsp_logs.0.clone());
396                if let Some(log_store) = log_store {
397                    log_store.update(cx, |log_store, cx| {
398                        log_store.add_language_server(
399                            LanguageServerKind::LocalSsh {
400                                lsp_store: self.lsp_store.downgrade(),
401                            },
402                            *id,
403                            Some(name.clone()),
404                            *worktree_id,
405                            lsp_store.read(cx).language_server_for_id(*id),
406                            cx,
407                        );
408                    });
409                }
410            }
411            LspStoreEvent::LanguageServerRemoved(id) => {
412                let log_store = cx
413                    .try_global::<GlobalLogStore>()
414                    .map(|lsp_logs| lsp_logs.0.clone());
415                if let Some(log_store) = log_store {
416                    log_store.update(cx, |log_store, cx| {
417                        log_store.remove_language_server(*id, cx);
418                    });
419                }
420                self.session
421                    .send(proto::UpdateLanguageServer {
422                        project_id: REMOTE_SERVER_PROJECT_ID,
423                        server_name: None,
424                        language_server_id: id.to_proto(),
425                        variant: Some(proto::update_language_server::Variant::Removed(
426                            proto::ServerRemoved {},
427                        )),
428                    })
429                    .log_err();
430            }
431            LspStoreEvent::LanguageServerUpdate {
432                language_server_id,
433                name,
434                message,
435            } => {
436                self.session
437                    .send(proto::UpdateLanguageServer {
438                        project_id: REMOTE_SERVER_PROJECT_ID,
439                        server_name: name.as_ref().map(|name| name.to_string()),
440                        language_server_id: language_server_id.to_proto(),
441                        variant: Some(message.clone()),
442                    })
443                    .log_err();
444            }
445            LspStoreEvent::Notification(message) => {
446                self.session
447                    .send(proto::Toast {
448                        project_id: REMOTE_SERVER_PROJECT_ID,
449                        notification_id: "lsp".to_string(),
450                        message: message.clone(),
451                    })
452                    .log_err();
453            }
454            LspStoreEvent::LanguageServerPrompt(prompt) => {
455                let request = self.session.request(proto::LanguageServerPromptRequest {
456                    project_id: REMOTE_SERVER_PROJECT_ID,
457                    actions: prompt
458                        .actions
459                        .iter()
460                        .map(|action| action.title.to_string())
461                        .collect(),
462                    level: Some(prompt_to_proto(prompt)),
463                    lsp_name: prompt.lsp_name.clone(),
464                    message: prompt.message.clone(),
465                });
466                let prompt = prompt.clone();
467                cx.background_spawn(async move {
468                    let response = request.await?;
469                    if let Some(action_response) = response.action_response {
470                        prompt.respond(action_response as usize).await;
471                    }
472                    anyhow::Ok(())
473                })
474                .detach();
475            }
476            _ => {}
477        }
478    }
479
480    pub async fn handle_add_worktree(
481        this: Entity<Self>,
482        message: TypedEnvelope<proto::AddWorktree>,
483        mut cx: AsyncApp,
484    ) -> Result<proto::AddWorktreeResponse> {
485        use client::ErrorCodeExt;
486        let fs = this.read_with(&cx, |this, _| this.fs.clone());
487        let path = PathBuf::from(shellexpand::tilde(&message.payload.path).to_string());
488
489        let canonicalized = match fs.canonicalize(&path).await {
490            Ok(path) => path,
491            Err(e) => {
492                let mut parent = path
493                    .parent()
494                    .ok_or(e)
495                    .with_context(|| format!("{path:?} does not exist"))?;
496                if parent == Path::new("") {
497                    parent = util::paths::home_dir();
498                }
499                let parent = fs.canonicalize(parent).await.map_err(|_| {
500                    anyhow!(
501                        proto::ErrorCode::DevServerProjectPathDoesNotExist
502                            .with_tag("path", path.to_string_lossy().as_ref())
503                    )
504                })?;
505                if let Some(file_name) = path.file_name() {
506                    parent.join(file_name)
507                } else {
508                    parent
509                }
510            }
511        };
512        let next_worktree_id = this
513            .update(&mut cx, |this, cx| {
514                this.worktree_store
515                    .update(cx, |worktree_store, _| worktree_store.next_worktree_id())
516            })
517            .await?;
518        let worktree = this
519            .read_with(&cx.clone(), |this, _| {
520                Worktree::local(
521                    Arc::from(canonicalized.as_path()),
522                    message.payload.visible,
523                    this.fs.clone(),
524                    this.next_entry_id.clone(),
525                    true,
526                    next_worktree_id,
527                    &mut cx,
528                )
529            })
530            .await?;
531
532        let response = this.read_with(&cx, |_, cx| {
533            let worktree = worktree.read(cx);
534            proto::AddWorktreeResponse {
535                worktree_id: worktree.id().to_proto(),
536                canonicalized_path: canonicalized.to_string_lossy().into_owned(),
537                root_repo_common_dir: worktree
538                    .root_repo_common_dir()
539                    .map(|p| p.to_string_lossy().into_owned()),
540                root_repo_is_linked_worktree: worktree.root_repo_is_linked_worktree(),
541            }
542        });
543
544        // We spawn this asynchronously, so that we can send the response back
545        // *before* `worktree_store.add()` can send out UpdateProject requests
546        // to the client about the new worktree.
547        //
548        // That lets the client manage the reference/handles of the newly-added
549        // worktree, before getting interrupted by an UpdateProject request.
550        //
551        // This fixes the problem of the client sending the AddWorktree request,
552        // headless project sending out a project update, client receiving it
553        // and immediately dropping the reference of the new client, causing it
554        // to be dropped on the headless project, and the client only then
555        // receiving a response to AddWorktree.
556        cx.spawn(async move |cx| {
557            this.update(cx, |this, cx| {
558                this.worktree_store.update(cx, |worktree_store, cx| {
559                    worktree_store.add(&worktree, cx);
560                });
561            });
562        })
563        .detach();
564
565        Ok(response)
566    }
567
568    pub async fn handle_remove_worktree(
569        this: Entity<Self>,
570        envelope: TypedEnvelope<proto::RemoveWorktree>,
571        mut cx: AsyncApp,
572    ) -> Result<proto::Ack> {
573        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
574        this.update(&mut cx, |this, cx| {
575            this.worktree_store.update(cx, |worktree_store, cx| {
576                worktree_store.remove_worktree(worktree_id, cx);
577            });
578        });
579        Ok(proto::Ack {})
580    }
581
582    pub async fn handle_open_buffer_by_path(
583        this: Entity<Self>,
584        message: TypedEnvelope<proto::OpenBufferByPath>,
585        mut cx: AsyncApp,
586    ) -> Result<proto::OpenBufferResponse> {
587        let worktree_id = WorktreeId::from_proto(message.payload.worktree_id);
588        let path = RelPath::from_unix_str(&message.payload.path)?.into();
589        let (buffer_store, buffer) = this.update(&mut cx, |this, cx| {
590            let buffer_store = this.buffer_store.clone();
591            let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
592                buffer_store.open_buffer(ProjectPath { worktree_id, path }, cx)
593            });
594            (buffer_store, buffer)
595        });
596
597        let buffer = buffer.await?;
598        let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id());
599        buffer_store.update(&mut cx, |buffer_store, cx| {
600            buffer_store
601                .create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx)
602                .detach_and_log_err(cx);
603        });
604
605        Ok(proto::OpenBufferResponse {
606            buffer_id: buffer_id.to_proto(),
607        })
608    }
609
610    pub async fn handle_open_image_by_path(
611        this: Entity<Self>,
612        message: TypedEnvelope<proto::OpenImageByPath>,
613        mut cx: AsyncApp,
614    ) -> Result<proto::OpenImageResponse> {
615        static NEXT_ID: AtomicU64 = AtomicU64::new(1);
616        let worktree_id = WorktreeId::from_proto(message.payload.worktree_id);
617        let path = RelPath::from_unix_str(&message.payload.path)?;
618        let project_id = message.payload.project_id;
619        use proto::create_image_for_peer::Variant;
620
621        let (worktree_store, session) = this.read_with(&cx, |this, _| {
622            (this.worktree_store.clone(), this.session.clone())
623        });
624
625        let worktree = worktree_store
626            .read_with(&cx, |store, cx| store.worktree_for_id(worktree_id, cx))
627            .context("worktree not found")?;
628
629        let load_task = worktree.update(&mut cx, |worktree, cx| {
630            worktree.load_binary_file(path.as_ref(), cx)
631        });
632
633        let loaded_file = load_task.await?;
634        let content = loaded_file.content;
635        let file = loaded_file.file;
636
637        let proto_file = worktree.read_with(&cx, |_worktree, cx| file.to_proto(cx));
638        let image_id =
639            ImageId::from(NonZeroU64::new(NEXT_ID.fetch_add(1, Ordering::Relaxed)).unwrap());
640
641        let format = image::guess_format(&content)
642            .map(|f| format!("{:?}", f).to_lowercase())
643            .unwrap_or_else(|_| "unknown".to_string());
644
645        let state = proto::ImageState {
646            id: image_id.to_proto(),
647            file: Some(proto_file),
648            content_size: content.len() as u64,
649            format,
650        };
651
652        session.send(proto::CreateImageForPeer {
653            project_id,
654            peer_id: Some(REMOTE_SERVER_PEER_ID),
655            variant: Some(Variant::State(state)),
656        })?;
657
658        const CHUNK_SIZE: usize = 1024 * 1024; // 1MB chunks
659        for chunk in content.chunks(CHUNK_SIZE) {
660            session.send(proto::CreateImageForPeer {
661                project_id,
662                peer_id: Some(REMOTE_SERVER_PEER_ID),
663                variant: Some(Variant::Chunk(proto::ImageChunk {
664                    image_id: image_id.to_proto(),
665                    data: chunk.to_vec(),
666                })),
667            })?;
668        }
669
670        Ok(proto::OpenImageResponse {
671            image_id: image_id.to_proto(),
672        })
673    }
674
675    pub async fn handle_trust_worktrees(
676        this: Entity<Self>,
677        envelope: TypedEnvelope<proto::TrustWorktrees>,
678        mut cx: AsyncApp,
679    ) -> Result<proto::Ack> {
680        let trusted_worktrees = cx
681            .update(|cx| TrustedWorktrees::try_get_global(cx))
682            .context("missing trusted worktrees")?;
683        let worktree_store = this.read_with(&cx, |project, _| project.worktree_store.clone());
684        trusted_worktrees.update(&mut cx, |trusted_worktrees, cx| {
685            trusted_worktrees.trust(
686                &worktree_store,
687                envelope
688                    .payload
689                    .trusted_paths
690                    .into_iter()
691                    .filter_map(PathTrust::from_proto)
692                    .collect(),
693                cx,
694            );
695        });
696        Ok(proto::Ack {})
697    }
698
699    pub async fn handle_restrict_worktrees(
700        this: Entity<Self>,
701        envelope: TypedEnvelope<proto::RestrictWorktrees>,
702        mut cx: AsyncApp,
703    ) -> Result<proto::Ack> {
704        let trusted_worktrees = cx
705            .update(|cx| TrustedWorktrees::try_get_global(cx))
706            .context("missing trusted worktrees")?;
707        let worktree_store = this.read_with(&cx, |project, _| project.worktree_store.downgrade());
708        trusted_worktrees.update(&mut cx, |trusted_worktrees, cx| {
709            let restricted_paths = envelope
710                .payload
711                .worktree_ids
712                .into_iter()
713                .map(WorktreeId::from_proto)
714                .map(PathTrust::Worktree)
715                .collect::<HashSet<_>>();
716            trusted_worktrees.restrict(worktree_store, restricted_paths, cx);
717        });
718        Ok(proto::Ack {})
719    }
720
721    pub async fn handle_download_file_by_path(
722        this: Entity<Self>,
723        message: TypedEnvelope<proto::DownloadFileByPath>,
724        mut cx: AsyncApp,
725    ) -> Result<proto::DownloadFileResponse> {
726        log::debug!(
727            "handle_download_file_by_path: received request: {:?}",
728            message.payload
729        );
730
731        let worktree_id = WorktreeId::from_proto(message.payload.worktree_id);
732        let path = RelPath::from_unix_str(&message.payload.path)?;
733        let project_id = message.payload.project_id;
734        let file_id = message.payload.file_id;
735        log::debug!(
736            "handle_download_file_by_path: worktree_id={:?}, path={:?}, file_id={}",
737            worktree_id,
738            path,
739            file_id
740        );
741        use proto::create_file_for_peer::Variant;
742
743        let (worktree_store, session): (Entity<WorktreeStore>, AnyProtoClient) = this
744            .read_with(&cx, |this, _| {
745                (this.worktree_store.clone(), this.session.clone())
746            });
747
748        let worktree = worktree_store
749            .read_with(&cx, |store, cx| store.worktree_for_id(worktree_id, cx))
750            .context("worktree not found")?;
751
752        let download_task = worktree.update(&mut cx, |worktree: &mut Worktree, cx| {
753            worktree.load_binary_file(path.as_ref(), cx)
754        });
755
756        let downloaded_file = download_task.await?;
757        let content = downloaded_file.content;
758        let file = downloaded_file.file;
759        log::debug!(
760            "handle_download_file_by_path: file loaded, content_size={}",
761            content.len()
762        );
763
764        let proto_file = worktree.read_with(&cx, |_worktree: &Worktree, cx| file.to_proto(cx));
765        log::debug!(
766            "handle_download_file_by_path: using client-provided file_id={}",
767            file_id
768        );
769
770        let state = proto::FileState {
771            id: file_id,
772            file: Some(proto_file),
773            content_size: content.len() as u64,
774        };
775
776        log::debug!("handle_download_file_by_path: sending State message");
777        session.send(proto::CreateFileForPeer {
778            project_id,
779            peer_id: Some(REMOTE_SERVER_PEER_ID),
780            variant: Some(Variant::State(state)),
781        })?;
782
783        const CHUNK_SIZE: usize = 1024 * 1024; // 1MB chunks
784        let num_chunks = content.len().div_ceil(CHUNK_SIZE);
785        log::debug!(
786            "handle_download_file_by_path: sending {} chunks",
787            num_chunks
788        );
789        for (i, chunk) in content.chunks(CHUNK_SIZE).enumerate() {
790            log::trace!(
791                "handle_download_file_by_path: sending chunk {}/{}, size={}",
792                i + 1,
793                num_chunks,
794                chunk.len()
795            );
796            session.send(proto::CreateFileForPeer {
797                project_id,
798                peer_id: Some(REMOTE_SERVER_PEER_ID),
799                variant: Some(Variant::Chunk(proto::FileChunk {
800                    file_id,
801                    data: chunk.to_vec(),
802                })),
803            })?;
804        }
805
806        log::debug!(
807            "handle_download_file_by_path: returning file_id={}",
808            file_id
809        );
810        Ok(proto::DownloadFileResponse { file_id })
811    }
812
813    pub async fn handle_open_new_buffer(
814        this: Entity<Self>,
815        _message: TypedEnvelope<proto::OpenNewBuffer>,
816        mut cx: AsyncApp,
817    ) -> Result<proto::OpenBufferResponse> {
818        let (buffer_store, buffer) = this.update(&mut cx, |this, cx| {
819            let buffer_store = this.buffer_store.clone();
820            let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
821                buffer_store.create_buffer(None, true, cx)
822            });
823            (buffer_store, buffer)
824        });
825
826        let buffer = buffer.await?;
827        let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id());
828        buffer_store.update(&mut cx, |buffer_store, cx| {
829            buffer_store
830                .create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx)
831                .detach_and_log_err(cx);
832        });
833
834        Ok(proto::OpenBufferResponse {
835            buffer_id: buffer_id.to_proto(),
836        })
837    }
838
839    async fn handle_toggle_lsp_logs(
840        _: Entity<Self>,
841        envelope: TypedEnvelope<proto::ToggleLspLogs>,
842        cx: AsyncApp,
843    ) -> Result<()> {
844        let server_id = LanguageServerId::from_proto(envelope.payload.server_id);
845        cx.update(|cx| {
846            let log_store = cx
847                .try_global::<GlobalLogStore>()
848                .map(|global_log_store| global_log_store.0.clone())
849                .context("lsp logs store is missing")?;
850            let toggled_log_kind =
851                match proto::toggle_lsp_logs::LogType::from_i32(envelope.payload.log_type)
852                    .context("invalid log type")?
853                {
854                    proto::toggle_lsp_logs::LogType::Log => LogKind::Logs,
855                    proto::toggle_lsp_logs::LogType::Trace => LogKind::Trace,
856                    proto::toggle_lsp_logs::LogType::Rpc => LogKind::Rpc,
857                };
858            log_store.update(cx, |log_store, _| {
859                log_store.toggle_lsp_logs(server_id, envelope.payload.enabled, toggled_log_kind);
860            });
861            anyhow::Ok(())
862        })?;
863
864        Ok(())
865    }
866
867    async fn handle_open_server_settings(
868        this: Entity<Self>,
869        _: TypedEnvelope<proto::OpenServerSettings>,
870        mut cx: AsyncApp,
871    ) -> Result<proto::OpenBufferResponse> {
872        let settings_path = paths::settings_file();
873        let (worktree, path) = this
874            .update(&mut cx, |this, cx| {
875                this.worktree_store.update(cx, |worktree_store, cx| {
876                    worktree_store.find_or_create_worktree(settings_path, false, cx)
877                })
878            })
879            .await?;
880
881        let (buffer, buffer_store) = this.update(&mut cx, |this, cx| {
882            let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
883                buffer_store.open_buffer(
884                    ProjectPath {
885                        worktree_id: worktree.read(cx).id(),
886                        path,
887                    },
888                    cx,
889                )
890            });
891
892            (buffer, this.buffer_store.clone())
893        });
894
895        let buffer = buffer.await?;
896
897        let buffer_id = cx.update(|cx| {
898            if buffer.read(cx).is_empty() {
899                buffer.update(cx, |buffer, cx| {
900                    buffer.edit([(0..0, initial_server_settings_content())], None, cx)
901                });
902            }
903
904            let buffer_id = buffer.read(cx).remote_id();
905
906            buffer_store.update(cx, |buffer_store, cx| {
907                buffer_store
908                    .create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx)
909                    .detach_and_log_err(cx);
910            });
911
912            buffer_id
913        });
914
915        Ok(proto::OpenBufferResponse {
916            buffer_id: buffer_id.to_proto(),
917        })
918    }
919
920    async fn handle_spawn_kernel(
921        this: Entity<Self>,
922        envelope: TypedEnvelope<proto::SpawnKernel>,
923        cx: AsyncApp,
924    ) -> Result<proto::SpawnKernelResponse> {
925        let fs = this.update(&mut cx.clone(), |this, _| this.fs.clone());
926
927        let mut ports = Vec::new();
928        for _ in 0..5 {
929            let listener = std::net::TcpListener::bind("127.0.0.1:0")?;
930            let port = listener.local_addr()?.port();
931            ports.push(port);
932        }
933
934        let connection_info = serde_json::json!({
935            "shell_port": ports[0],
936            "iopub_port": ports[1],
937            "stdin_port": ports[2],
938            "control_port": ports[3],
939            "hb_port": ports[4],
940            "ip": "127.0.0.1",
941            "key": uuid::Uuid::new_v4().to_string(),
942            "transport": "tcp",
943            "signature_scheme": "hmac-sha256",
944            "kernel_name": envelope.payload.kernel_name,
945        });
946
947        let connection_file_content = serde_json::to_string_pretty(&connection_info)?;
948        let kernel_id = uuid::Uuid::new_v4().to_string();
949
950        let connection_file_path = std::env::temp_dir().join(format!("kernel-{}.json", kernel_id));
951        fs.save(
952            &connection_file_path,
953            &connection_file_content.as_str().into(),
954            language::LineEnding::Unix,
955        )
956        .await?;
957
958        let working_directory = if envelope.payload.working_directory.is_empty() {
959            std::env::current_dir()
960                .ok()
961                .map(|p| p.to_string_lossy().into_owned())
962        } else {
963            Some(envelope.payload.working_directory)
964        };
965
966        // Spawn kernel (Assuming python for now, or we'd need to parse kernelspec logic here or pass the command)
967
968        // Spawn kernel
969        let spawn_kernel = |binary: &str, args: &[String]| {
970            let mut command = smol::process::Command::new(binary);
971
972            if !args.is_empty() {
973                for arg in args {
974                    if arg == "{connection_file}" {
975                        command.arg(&connection_file_path);
976                    } else {
977                        command.arg(arg);
978                    }
979                }
980            } else {
981                command
982                    .arg("-m")
983                    .arg("ipykernel_launcher")
984                    .arg("-f")
985                    .arg(&connection_file_path);
986            }
987
988            // This ensures subprocesses spawned from the kernel use the correct Python environment
989            let python_bin_dir = std::path::Path::new(binary).parent();
990            if let Some(bin_dir) = python_bin_dir {
991                if let Some(path_var) = std::env::var_os("PATH") {
992                    let mut paths = std::env::split_paths(&path_var).collect::<Vec<_>>();
993                    paths.insert(0, bin_dir.to_path_buf());
994                    if let Ok(new_path) = std::env::join_paths(paths) {
995                        command.env("PATH", new_path);
996                    }
997                }
998
999                if let Some(venv_root) = bin_dir.parent() {
1000                    command.env("VIRTUAL_ENV", venv_root.to_string_lossy().to_string());
1001                }
1002            }
1003
1004            if let Some(wd) = &working_directory {
1005                command.current_dir(wd);
1006            }
1007            command.spawn()
1008        };
1009
1010        // We need to manage the child process lifecycle
1011        let child = if !envelope.payload.command.is_empty() {
1012            spawn_kernel(&envelope.payload.command, &envelope.payload.args).context(format!(
1013                "failed to spawn kernel process (command: {})",
1014                envelope.payload.command
1015            ))?
1016        } else if let Some(venv_python) = working_directory
1017            .as_ref()
1018            .and_then(|wd| find_venv_python(wd))
1019        {
1020            let path_str = venv_python.to_string_lossy().to_string();
1021            spawn_kernel(&path_str, &[]).context(format!(
1022                "failed to spawn kernel process (venv: {})",
1023                path_str
1024            ))?
1025        } else {
1026            spawn_kernel("python3", &[])
1027                .or_else(|_| spawn_kernel("python", &[]))
1028                .context("failed to spawn kernel process (tried python3 and python)")?
1029        };
1030
1031        this.update(&mut cx.clone(), |this, _cx| {
1032            this.kernels.insert(kernel_id.clone(), child);
1033        });
1034
1035        Ok(proto::SpawnKernelResponse {
1036            kernel_id,
1037            connection_file: connection_file_content,
1038        })
1039    }
1040
1041    async fn handle_kill_kernel(
1042        this: Entity<Self>,
1043        envelope: TypedEnvelope<proto::KillKernel>,
1044        mut cx: AsyncApp,
1045    ) -> Result<proto::Ack> {
1046        let kernel_id = envelope.payload.kernel_id;
1047        let child = this.update(&mut cx, |this, _| this.kernels.remove(&kernel_id));
1048        if let Some(mut child) = child {
1049            child.kill().log_err();
1050        }
1051        Ok(proto::Ack {})
1052    }
1053
1054    async fn handle_find_search_candidates(
1055        this: Entity<Self>,
1056        envelope: TypedEnvelope<proto::FindSearchCandidates>,
1057        mut cx: AsyncApp,
1058    ) -> Result<proto::Ack> {
1059        use futures::stream::StreamExt as _;
1060
1061        let peer_id = envelope.original_sender_id.unwrap_or(envelope.sender_id);
1062        let message = envelope.payload;
1063        let query = SearchQuery::from_proto(
1064            message.query.context("missing query field")?,
1065            PathStyle::local(),
1066        )?;
1067
1068        let project_id = message.project_id;
1069        let buffer_store = this.read_with(&cx, |this, _| this.buffer_store.clone());
1070        let handle = message.handle;
1071        let _buffer_store = buffer_store.clone();
1072        let client = this.read_with(&cx, |this, _| this.session.clone());
1073        let task = cx.spawn(async move |cx| {
1074            let results = this.update(cx, |this, cx| {
1075                project::Search::local(
1076                    this.fs.clone(),
1077                    this.buffer_store.clone(),
1078                    this.worktree_store.clone(),
1079                    message.limit as _,
1080                    cx,
1081                )
1082                .into_handle(query, cx)
1083                .matching_buffers(cx)
1084            });
1085            let (batcher, batches) =
1086                project::project_search::AdaptiveBatcher::new(cx.background_executor());
1087            let mut new_matches = Box::pin(results.rx);
1088
1089            let sender_task = cx.background_executor().spawn({
1090                let client = client.clone();
1091                async move {
1092                    let mut batches = std::pin::pin!(batches);
1093                    while let Some(buffer_ids) = batches.next().await {
1094                        client
1095                            .request(proto::FindSearchCandidatesChunk {
1096                                handle,
1097                                peer_id: Some(peer_id),
1098                                project_id,
1099                                variant: Some(
1100                                    proto::find_search_candidates_chunk::Variant::Matches(
1101                                        proto::FindSearchCandidatesMatches { buffer_ids },
1102                                    ),
1103                                ),
1104                            })
1105                            .await?;
1106                    }
1107                    anyhow::Ok(())
1108                }
1109            });
1110
1111            while let Some((buffer, _)) = new_matches.next().await {
1112                let _ = buffer_store
1113                    .update(cx, |this, cx| {
1114                        this.create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx)
1115                    })
1116                    .await;
1117                let buffer_id = buffer.read_with(cx, |this, _| this.remote_id().to_proto());
1118                batcher.push(buffer_id).await;
1119            }
1120            batcher.flush().await;
1121
1122            sender_task.await?;
1123
1124            client
1125                .request(proto::FindSearchCandidatesChunk {
1126                    handle,
1127                    peer_id: Some(peer_id),
1128                    project_id,
1129                    variant: Some(proto::find_search_candidates_chunk::Variant::Done(
1130                        proto::FindSearchCandidatesDone {},
1131                    )),
1132                })
1133                .await?;
1134            anyhow::Ok(())
1135        });
1136        _buffer_store.update(&mut cx, |this, _| {
1137            this.register_ongoing_project_search((peer_id, handle), task);
1138        });
1139
1140        Ok(proto::Ack {})
1141    }
1142
1143    // Goes from client to host.
1144    async fn handle_find_search_candidates_cancel(
1145        this: Entity<Self>,
1146        envelope: TypedEnvelope<proto::FindSearchCandidatesCancelled>,
1147        mut cx: AsyncApp,
1148    ) -> Result<()> {
1149        let buffer_store = this.read_with(&mut cx, |this, _| this.buffer_store.clone());
1150        BufferStore::handle_find_search_candidates_cancel(buffer_store, envelope, cx).await
1151    }
1152
1153    async fn handle_list_remote_directory(
1154        this: Entity<Self>,
1155        envelope: TypedEnvelope<proto::ListRemoteDirectory>,
1156        cx: AsyncApp,
1157    ) -> Result<proto::ListRemoteDirectoryResponse> {
1158        use smol::stream::StreamExt;
1159        let fs = cx.read_entity(&this, |this, _| this.fs.clone());
1160        let expanded = PathBuf::from(shellexpand::tilde(&envelope.payload.path).to_string());
1161        let check_info = envelope
1162            .payload
1163            .config
1164            .as_ref()
1165            .is_some_and(|config| config.is_dir);
1166
1167        let mut entries = Vec::new();
1168        let mut entry_info = Vec::new();
1169        let mut response = fs.read_dir(&expanded).await?;
1170        while let Some(path) = response.next().await {
1171            let path = path?;
1172            if let Some(file_name) = path.file_name() {
1173                entries.push(file_name.to_string_lossy().into_owned());
1174                if check_info {
1175                    let is_dir = fs.is_dir(&path).await;
1176                    entry_info.push(proto::EntryInfo { is_dir });
1177                }
1178            }
1179        }
1180        Ok(proto::ListRemoteDirectoryResponse {
1181            entries,
1182            entry_info,
1183        })
1184    }
1185
1186    async fn handle_get_path_metadata(
1187        this: Entity<Self>,
1188        envelope: TypedEnvelope<proto::GetPathMetadata>,
1189        cx: AsyncApp,
1190    ) -> Result<proto::GetPathMetadataResponse> {
1191        let fs = cx.read_entity(&this, |this, _| this.fs.clone());
1192        let expanded = PathBuf::from(shellexpand::tilde(&envelope.payload.path).to_string());
1193
1194        let metadata = fs.metadata(&expanded).await?;
1195        let is_dir = metadata.map(|metadata| metadata.is_dir).unwrap_or(false);
1196
1197        Ok(proto::GetPathMetadataResponse {
1198            exists: metadata.is_some(),
1199            is_dir,
1200            path: expanded.to_string_lossy().into_owned(),
1201        })
1202    }
1203
1204    async fn handle_shutdown_remote_server(
1205        _this: Entity<Self>,
1206        _envelope: TypedEnvelope<proto::ShutdownRemoteServer>,
1207        cx: AsyncApp,
1208    ) -> Result<proto::Ack> {
1209        cx.spawn(async move |cx| {
1210            cx.update(|cx| {
1211                // TODO: This is a hack, because in a headless project, shutdown isn't executed
1212                // when calling quit, but it should be.
1213                cx.shutdown();
1214                cx.quit();
1215            })
1216        })
1217        .detach();
1218
1219        Ok(proto::Ack {})
1220    }
1221
1222    pub async fn handle_ping(
1223        _this: Entity<Self>,
1224        _envelope: TypedEnvelope<proto::Ping>,
1225        _cx: AsyncApp,
1226    ) -> Result<proto::Ack> {
1227        log::debug!("Received ping from client");
1228        Ok(proto::Ack {})
1229    }
1230
1231    async fn handle_get_processes(
1232        _this: Entity<Self>,
1233        _envelope: TypedEnvelope<proto::GetProcesses>,
1234        _cx: AsyncApp,
1235    ) -> Result<proto::GetProcessesResponse> {
1236        let mut processes = Vec::new();
1237        let refresh_kind = RefreshKind::nothing().with_processes(
1238            ProcessRefreshKind::nothing()
1239                .without_tasks()
1240                .with_cmd(UpdateKind::Always),
1241        );
1242
1243        for process in System::new_with_specifics(refresh_kind)
1244            .processes()
1245            .values()
1246        {
1247            let name = process.name().to_string_lossy().into_owned();
1248            let command = process
1249                .cmd()
1250                .iter()
1251                .map(|s| s.to_string_lossy().into_owned())
1252                .collect::<Vec<_>>();
1253
1254            processes.push(proto::ProcessInfo {
1255                pid: process.pid().as_u32(),
1256                name,
1257                command,
1258            });
1259        }
1260
1261        processes.sort_by_key(|p| p.name.clone());
1262
1263        Ok(proto::GetProcessesResponse { processes })
1264    }
1265
1266    async fn handle_get_remote_profiling_data(
1267        this: Entity<Self>,
1268        envelope: TypedEnvelope<proto::GetRemoteProfilingData>,
1269        cx: AsyncApp,
1270    ) -> Result<proto::GetRemoteProfilingDataResponse> {
1271        let foreground_only = envelope.payload.foreground_only;
1272
1273        let (deltas, now_nanos) = cx.update(|cx| {
1274            let timings = if foreground_only {
1275                vec![gpui::profiler::get_current_thread_timings(
1276                    TasksIncluded::OnlyCompleted,
1277                )]
1278            } else {
1279                gpui::profiler::get_all_timings(TasksIncluded::OnlyCompleted)
1280            };
1281            this.update(cx, |this, _cx| {
1282                let deltas = this.profiling_collector.collect_unseen(timings);
1283                let now_nanos = Instant::now()
1284                    .duration_since(this.profiling_collector.startup_time())
1285                    .as_nanos() as u64;
1286                (deltas, now_nanos)
1287            })
1288        });
1289
1290        let threads = deltas
1291            .into_iter()
1292            .map(|delta| proto::RemoteProfilingThread {
1293                thread_name: delta.thread_name,
1294                thread_id: delta.thread_id,
1295                timings: delta
1296                    .new_timings
1297                    .into_iter()
1298                    .map(|t| proto::RemoteProfilingTiming {
1299                        location: Some(proto::RemoteProfilingLocation {
1300                            file: t.location.file.to_string(),
1301                            line: t.location.line,
1302                            column: t.location.column,
1303                        }),
1304                        start_nanos: t.start as u64,
1305                        duration_nanos: t.duration as u64,
1306                    })
1307                    .collect(),
1308            })
1309            .collect();
1310
1311        Ok(proto::GetRemoteProfilingDataResponse { threads, now_nanos })
1312    }
1313
1314    async fn handle_get_directory_environment(
1315        this: Entity<Self>,
1316        envelope: TypedEnvelope<proto::GetDirectoryEnvironment>,
1317        mut cx: AsyncApp,
1318    ) -> Result<proto::DirectoryEnvironment> {
1319        let shell = task::shell_from_proto(envelope.payload.shell.context("missing shell")?)?;
1320        let directory = PathBuf::from(envelope.payload.directory);
1321        let environment = this
1322            .update(&mut cx, |this, cx| {
1323                this.environment.update(cx, |environment, cx| {
1324                    environment.local_directory_environment(&shell, directory.into(), cx)
1325                })
1326            })
1327            .await
1328            .context("failed to get directory environment")?
1329            .into_iter()
1330            .collect();
1331        Ok(proto::DirectoryEnvironment { environment })
1332    }
1333}
1334
1335fn prompt_to_proto(
1336    prompt: &project::LanguageServerPromptRequest,
1337) -> proto::language_server_prompt_request::Level {
1338    match prompt.level {
1339        PromptLevel::Info => proto::language_server_prompt_request::Level::Info(
1340            proto::language_server_prompt_request::Info {},
1341        ),
1342        PromptLevel::Warning => proto::language_server_prompt_request::Level::Warning(
1343            proto::language_server_prompt_request::Warning {},
1344        ),
1345        PromptLevel::Critical => proto::language_server_prompt_request::Level::Critical(
1346            proto::language_server_prompt_request::Critical {},
1347        ),
1348    }
1349}
1350
1351fn find_venv_python(working_directory: &str) -> Option<std::path::PathBuf> {
1352    let wd = std::path::Path::new(working_directory);
1353    for dir_name in &[".venv", "venv", ".env", "env"] {
1354        let venv_dir = wd.join(dir_name);
1355        let has_pyvenv_cfg = venv_dir.join("pyvenv.cfg").is_file();
1356        let has_activate = venv_dir.join("bin").join("activate").is_file();
1357        if has_pyvenv_cfg || has_activate {
1358            let python = venv_dir.join("bin").join("python");
1359            if python.is_file() {
1360                return Some(python);
1361            }
1362            let python3 = venv_dir.join("bin").join("python3");
1363            if python3.is_file() {
1364                return Some(python3);
1365            }
1366        }
1367    }
1368    None
1369}
1370
Served at tenant.openagents/omega Member data and write actions are omitted.