1915 lines · 66.5 KB · Rust
RawDownload
mod db;mod edit_agent;mod history_store;mod legacy_thread;mod native_agent_server;pub mod outline;mod templates;#[cfg(test)]mod tests;mod thread;mod tools; use context_server::ContextServerId;pub use db::*;pub use history_store::*;pub use native_agent_server::NativeAgentServer;pub use templates::*;pub use thread::*;pub use tools::*; use acp_thread::{AcpThread, AgentModelSelector, UserMessageId};use agent_client_protocol as acp;use anyhow::{Context as _, Result, anyhow};use chrono::{DateTime, Utc};use collections::{HashMap, HashSet, IndexMap};use fs::Fs;use futures::channel::{mpsc, oneshot};use futures::future::Shared;use futures::{StreamExt, future};use gpui::{    App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,};use language_model::{IconOrSvg, LanguageModel, LanguageModelProvider, LanguageModelRegistry};use project::{Project, ProjectItem, ProjectPath, Worktree};use prompt_store::{    ProjectContext, PromptStore, RULES_FILE_NAMES, RulesFileContext, UserRulesContext,    WorktreeContext,};use serde::{Deserialize, Serialize};use settings::{LanguageModelSelection, update_settings_file};use std::any::Any;use std::path::{Path, PathBuf};use std::rc::Rc;use std::sync::Arc;use util::ResultExt;use util::rel_path::RelPath; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]pub struct ProjectSnapshot {    pub worktree_snapshots: Vec<project::telemetry_snapshot::TelemetryWorktreeSnapshot>,    pub timestamp: DateTime<Utc>,} pub struct RulesLoadingError {    pub message: SharedString,} /// Holds both the internal Thread and the AcpThread for a sessionstruct Session {    /// The internal thread that processes messages    thread: Entity<Thread>,    /// The ACP thread that handles protocol communication    acp_thread: WeakEntity<acp_thread::AcpThread>,    pending_save: Task<()>,    _subscriptions: Vec<Subscription>,} pub struct LanguageModels {    /// Access language model by ID    models: HashMap<acp::ModelId, Arc<dyn LanguageModel>>,    /// Cached list for returning language model information    model_list: acp_thread::AgentModelList,    refresh_models_rx: watch::Receiver<()>,    refresh_models_tx: watch::Sender<()>,    _authenticate_all_providers_task: Task<()>,} impl LanguageModels {    fn new(cx: &mut App) -> Self {        let (refresh_models_tx, refresh_models_rx) = watch::channel(());         let mut this = Self {            models: HashMap::default(),            model_list: acp_thread::AgentModelList::Grouped(IndexMap::default()),            refresh_models_rx,            refresh_models_tx,            _authenticate_all_providers_task: Self::authenticate_all_language_model_providers(cx),        };        this.refresh_list(cx);        this    }     fn refresh_list(&mut self, cx: &App) {        let providers = LanguageModelRegistry::global(cx)            .read(cx)            .visible_providers()            .into_iter()            .filter(|provider| provider.is_authenticated(cx))            .collect::<Vec<_>>();         let mut language_model_list = IndexMap::default();        let mut recommended_models = HashSet::default();         let mut recommended = Vec::new();        for provider in &providers {            for model in provider.recommended_models(cx) {                recommended_models.insert((model.provider_id(), model.id()));                recommended.push(Self::map_language_model_to_info(&model, provider));            }        }        if !recommended.is_empty() {            language_model_list.insert(                acp_thread::AgentModelGroupName("Recommended".into()),                recommended,            );        }         let mut models = HashMap::default();        for provider in providers {            let mut provider_models = Vec::new();            for model in provider.provided_models(cx) {                let model_info = Self::map_language_model_to_info(&model, &provider);                let model_id = model_info.id.clone();                provider_models.push(model_info);                models.insert(model_id, model);            }            if !provider_models.is_empty() {                language_model_list.insert(                    acp_thread::AgentModelGroupName(provider.name().0.clone()),                    provider_models,                );            }        }         self.models = models;        self.model_list = acp_thread::AgentModelList::Grouped(language_model_list);        self.refresh_models_tx.send(()).ok();    }     fn watch(&self) -> watch::Receiver<()> {        self.refresh_models_rx.clone()    }     pub fn model_from_id(&self, model_id: &acp::ModelId) -> Option<Arc<dyn LanguageModel>> {        self.models.get(model_id).cloned()    }     fn map_language_model_to_info(        model: &Arc<dyn LanguageModel>,        provider: &Arc<dyn LanguageModelProvider>,    ) -> acp_thread::AgentModelInfo {        acp_thread::AgentModelInfo {            id: Self::model_id(model),            name: model.name().0,            description: None,            icon: Some(match provider.icon() {                IconOrSvg::Svg(path) => acp_thread::AgentModelIcon::Path(path),                IconOrSvg::Icon(name) => acp_thread::AgentModelIcon::Named(name),            }),        }    }     fn model_id(model: &Arc<dyn LanguageModel>) -> acp::ModelId {        acp::ModelId::new(format!("{}/{}", model.provider_id().0, model.id().0))    }     fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> {        let authenticate_all_providers = LanguageModelRegistry::global(cx)            .read(cx)            .visible_providers()            .iter()            .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))            .collect::<Vec<_>>();         cx.background_spawn(async move {            for (provider_id, provider_name, authenticate_task) in authenticate_all_providers {                if let Err(err) = authenticate_task.await {                    match err {                        language_model::AuthenticateError::CredentialsNotFound => {                            // Since we're authenticating these providers in the                            // background for the purposes of populating the                            // language selector, we don't care about providers                            // where the credentials are not found.                        }                        language_model::AuthenticateError::ConnectionRefused => {                            // Not logging connection refused errors as they are mostly from LM Studio's noisy auth failures.                            // LM Studio only has one auth method (endpoint call) which fails for users who haven't enabled it.                            // TODO: Better manage LM Studio auth logic to avoid these noisy failures.                        }                        _ => {                            // Some providers have noisy failure states that we                            // don't want to spam the logs with every time the                            // language model selector is initialized.                            //                            // Ideally these should have more clear failure modes                            // that we know are safe to ignore here, like what we do                            // with `CredentialsNotFound` above.                            match provider_id.0.as_ref() {                                "lmstudio" | "ollama" => {                                    // LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".                                    //                                    // These fail noisily, so we don't log them.                                }                                "copilot_chat" => {                                    // Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.                                }                                _ => {                                    log::error!(                                        "Failed to authenticate provider: {}: {err:#}",                                        provider_name.0                                    );                                }                            }                        }                    }                }            }        })    }} pub struct NativeAgent {    /// Session ID -> Session mapping    sessions: HashMap<acp::SessionId, Session>,    history: Entity<HistoryStore>,    /// Shared project context for all threads    project_context: Entity<ProjectContext>,    project_context_needs_refresh: watch::Sender<()>,    _maintain_project_context: Task<Result<()>>,    context_server_registry: Entity<ContextServerRegistry>,    /// Shared templates for all threads    templates: Arc<Templates>,    /// Cached model information    models: LanguageModels,    project: Entity<Project>,    prompt_store: Option<Entity<PromptStore>>,    fs: Arc<dyn Fs>,    _subscriptions: Vec<Subscription>,} impl NativeAgent {    pub async fn new(        project: Entity<Project>,        history: Entity<HistoryStore>,        templates: Arc<Templates>,        prompt_store: Option<Entity<PromptStore>>,        fs: Arc<dyn Fs>,        cx: &mut AsyncApp,    ) -> Result<Entity<NativeAgent>> {        log::debug!("Creating new NativeAgent");         let project_context = cx            .update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))?            .await;         cx.new(|cx| {            let context_server_store = project.read(cx).context_server_store();            let context_server_registry =                cx.new(|cx| ContextServerRegistry::new(context_server_store.clone(), cx));             let mut subscriptions = vec![                cx.subscribe(&project, Self::handle_project_event),                cx.subscribe(                    &LanguageModelRegistry::global(cx),                    Self::handle_models_updated_event,                ),                cx.subscribe(                    &context_server_store,                    Self::handle_context_server_store_updated,                ),                cx.subscribe(                    &context_server_registry,                    Self::handle_context_server_registry_event,                ),            ];            if let Some(prompt_store) = prompt_store.as_ref() {                subscriptions.push(cx.subscribe(prompt_store, Self::handle_prompts_updated_event))            }             let (project_context_needs_refresh_tx, project_context_needs_refresh_rx) =                watch::channel(());            Self {                sessions: HashMap::default(),                history,                project_context: cx.new(|_| project_context),                project_context_needs_refresh: project_context_needs_refresh_tx,                _maintain_project_context: cx.spawn(async move |this, cx| {                    Self::maintain_project_context(this, project_context_needs_refresh_rx, cx).await                }),                context_server_registry,                templates,                models: LanguageModels::new(cx),                project,                prompt_store,                fs,                _subscriptions: subscriptions,            }        })    }     fn register_session(        &mut self,        thread_handle: Entity<Thread>,        cx: &mut Context<Self>,    ) -> Entity<AcpThread> {        let connection = Rc::new(NativeAgentConnection(cx.entity()));         let thread = thread_handle.read(cx);        let session_id = thread.id().clone();        let title = thread.title();        let project = thread.project.clone();        let action_log = thread.action_log.clone();        let prompt_capabilities_rx = thread.prompt_capabilities_rx.clone();        let acp_thread = cx.new(|cx| {            acp_thread::AcpThread::new(                title,                connection,                project.clone(),                action_log.clone(),                session_id.clone(),                prompt_capabilities_rx,                cx,            )        });         let registry = LanguageModelRegistry::read_global(cx);        let summarization_model = registry.thread_summary_model().map(|c| c.model);         thread_handle.update(cx, |thread, cx| {            thread.set_summarization_model(summarization_model, cx);            thread.add_default_tools(                Rc::new(AcpThreadEnvironment {                    acp_thread: acp_thread.downgrade(),                }) as _,                cx,            )        });         let subscriptions = vec![            cx.observe_release(&acp_thread, |this, acp_thread, _cx| {                this.sessions.remove(acp_thread.session_id());            }),            cx.subscribe(&thread_handle, Self::handle_thread_title_updated),            cx.subscribe(&thread_handle, Self::handle_thread_token_usage_updated),            cx.observe(&thread_handle, move |this, thread, cx| {                this.save_thread(thread, cx)            }),        ];         self.sessions.insert(            session_id,            Session {                thread: thread_handle,                acp_thread: acp_thread.downgrade(),                _subscriptions: subscriptions,                pending_save: Task::ready(()),            },        );         self.update_available_commands(cx);         acp_thread    }     pub fn models(&self) -> &LanguageModels {        &self.models    }     async fn maintain_project_context(        this: WeakEntity<Self>,        mut needs_refresh: watch::Receiver<()>,        cx: &mut AsyncApp,    ) -> Result<()> {        while needs_refresh.changed().await.is_ok() {            let project_context = this                .update(cx, |this, cx| {                    Self::build_project_context(&this.project, this.prompt_store.as_ref(), cx)                })?                .await;            this.update(cx, |this, cx| {                this.project_context = cx.new(|_| project_context);            })?;        }         Ok(())    }     fn build_project_context(        project: &Entity<Project>,        prompt_store: Option<&Entity<PromptStore>>,        cx: &mut App,    ) -> Task<ProjectContext> {        let worktrees = project.read(cx).visible_worktrees(cx).collect::<Vec<_>>();        let worktree_tasks = worktrees            .into_iter()            .map(|worktree| {                Self::load_worktree_info_for_system_prompt(worktree, project.clone(), cx)            })            .collect::<Vec<_>>();        let default_user_rules_task = if let Some(prompt_store) = prompt_store.as_ref() {            prompt_store.read_with(cx, |prompt_store, cx| {                let prompts = prompt_store.default_prompt_metadata();                let load_tasks = prompts.into_iter().map(|prompt_metadata| {                    let contents = prompt_store.load(prompt_metadata.id, cx);                    async move { (contents.await, prompt_metadata) }                });                cx.background_spawn(future::join_all(load_tasks))            })        } else {            Task::ready(vec![])        };         cx.spawn(async move |_cx| {            let (worktrees, default_user_rules) =                future::join(future::join_all(worktree_tasks), default_user_rules_task).await;             let worktrees = worktrees                .into_iter()                .map(|(worktree, _rules_error)| {                    // TODO: show error message                    // if let Some(rules_error) = rules_error {                    //     this.update(cx, |_, cx| cx.emit(rules_error)).ok();                    // }                    worktree                })                .collect::<Vec<_>>();             let default_user_rules = default_user_rules                .into_iter()                .flat_map(|(contents, prompt_metadata)| match contents {                    Ok(contents) => Some(UserRulesContext {                        uuid: prompt_metadata.id.as_user()?,                        title: prompt_metadata.title.map(|title| title.to_string()),                        contents,                    }),                    Err(_err) => {                        // TODO: show error message                        // this.update(cx, |_, cx| {                        //     cx.emit(RulesLoadingError {                        //         message: format!("{err:?}").into(),                        //     });                        // })                        // .ok();                        None                    }                })                .collect::<Vec<_>>();             ProjectContext::new(worktrees, default_user_rules)        })    }     fn load_worktree_info_for_system_prompt(        worktree: Entity<Worktree>,        project: Entity<Project>,        cx: &mut App,    ) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {        let tree = worktree.read(cx);        let root_name = tree.root_name_str().into();        let abs_path = tree.abs_path();         let mut context = WorktreeContext {            root_name,            abs_path,            rules_file: None,        };         let rules_task = Self::load_worktree_rules_file(worktree, project, cx);        let Some(rules_task) = rules_task else {            return Task::ready((context, None));        };         cx.spawn(async move |_| {            let (rules_file, rules_file_error) = match rules_task.await {                Ok(rules_file) => (Some(rules_file), None),                Err(err) => (                    None,                    Some(RulesLoadingError {                        message: format!("{err}").into(),                    }),                ),            };            context.rules_file = rules_file;            (context, rules_file_error)        })    }     fn load_worktree_rules_file(        worktree: Entity<Worktree>,        project: Entity<Project>,        cx: &mut App,    ) -> Option<Task<Result<RulesFileContext>>> {        let worktree = worktree.read(cx);        let worktree_id = worktree.id();        let selected_rules_file = RULES_FILE_NAMES            .into_iter()            .filter_map(|name| {                worktree                    .entry_for_path(RelPath::unix(name).unwrap())                    .filter(|entry| entry.is_file())

Lines 1–500 of 1915.