6929 lines · 260.0 KB · Rust
RawDownload
mod db;mod legacy_thread;mod native_agent_server;pub mod outline;mod pattern_extraction;mod sandboxing;mod templates;#[cfg(test)]mod tests;mod thread;mod thread_store;mod tool_permissions;mod tools; use context_server::ContextServerId;pub use db::*;use itertools::Itertools;pub use native_agent_server::NativeAgentServer;pub use pattern_extraction::*;pub use sandboxing::{    ThreadSandbox, sandbox_worktree_writable_paths, settings_sandbox_policy,    settings_thread_sandbox,};pub use shell_command_parser::extract_commands;pub use templates::*;pub use thread::*;pub use thread_store::*;pub use tool_permissions::*;pub use tools::*; use acp_thread::{    AcpThread, AgentModelId, AgentModelSelector, AgentSessionInfo, AgentSessionList,    AgentSessionListRequest, AgentSessionListResponse, ClientUserMessageId, TokenUsageRatio,};use agent_client_protocol::schema::v1 as acp;use agent_skills::{    AGENTS_DIR_NAME, MAX_SKILL_DESCRIPTIONS_SIZE, MAX_SKILL_FILE_SIZE, ProjectSkillGroup,    SKILL_FILE_NAME, Skill, SkillIndex, SkillLoadError, SkillLoadWarning, SkillScopeId,    SkillSource, SkillSummary, builtin_skills, global_skills_dir, load_skills_from_directory,    parse_skill_frontmatter, project_skills_relative_path, read_skill_body_from_content,};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::{FutureExt as _, StreamExt as _, future};use gpui::{    App, AppContext, AsyncApp, Context, Entity, EntityId, SharedString, Subscription, Task,    TaskExt, WeakEntity,};use language_model::{    IconOrSvg, LanguageModel, LanguageModelId, LanguageModelProvider, LanguageModelProviderId,    LanguageModelRegistry,};use project::{    AgentId, Project, ProjectItem, ProjectPath, Worktree, WorktreeId,    trusted_worktrees::TrustedWorktrees,};use prompt_store::{ProjectContext, RULES_FILE_NAMES, RulesFileContext, WorktreeContext};use serde::{Deserialize, Serialize};use settings::{LanguageModelSelection, Settings as _, update_settings_file};use std::any::Any;use std::path::PathBuf;use std::rc::Rc;use std::sync::{Arc, LazyLock};use util::ResultExt;use util::path_list::PathList;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,} #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]pub enum SkillLoadingIssueKind {    LoadFailed,    DescriptionTooLong,    CatalogBudgetExceeded,} #[derive(Clone, Debug, PartialEq, Eq, Hash)]pub struct SkillLoadingIssue {    pub project_id: EntityId,    pub path: PathBuf,    pub message: SharedString,    pub kind: SkillLoadingIssueKind,} #[derive(Clone, Debug, PartialEq, Eq)]struct SkillLoadingIssueData {    path: PathBuf,    message: String,    kind: SkillLoadingIssueKind,} impl SkillLoadingIssueData {    fn from_load_error(error: SkillLoadError) -> Self {        Self {            path: error.path,            message: error.message,            kind: SkillLoadingIssueKind::LoadFailed,        }    }     fn from_load_warning(skill: &Skill, warning: &SkillLoadWarning) -> Self {        let kind = match warning {            SkillLoadWarning::DescriptionTooLong { .. } => {                SkillLoadingIssueKind::DescriptionTooLong            }        };        Self {            path: skill.skill_file_path.clone(),            message: warning.message(),            kind,        }    }     fn catalog_budget_exceeded(path: PathBuf, message: String) -> Self {        Self {            path,            message,            kind: SkillLoadingIssueKind::CatalogBudgetExceeded,        }    }} /// Emitted whenever the set of skill loading issues for a project changes./// The `issues` field is the full replacement list; subscribers should treat/// it as a snapshot rather than appending. An empty `issues` list means all/// previously-reported issues have been resolved.#[derive(Clone, Debug)]pub struct SkillLoadingIssuesUpdated {    pub project_id: EntityId,    pub issues: Vec<SkillLoadingIssue>,} #[derive(Clone, Debug)]pub struct NativeAvailableSkill {    pub name: String,    pub description: String,    pub source: SharedString,    pub skill_file_path: PathBuf,    pub warning: Option<SharedString>,} impl From<&Skill> for NativeAvailableSkill {    fn from(skill: &Skill) -> Self {        Self {            name: skill.name.clone(),            description: skill.description.clone(),            source: skill.source.display_label().to_string().into(),            skill_file_path: skill.skill_file_path.clone(),            warning: skill                .load_warnings                .first()                .map(|warning| warning.message().into()),        }    }} pub const COMPACT_COMMAND_NAME: &str = "compact"; /// Returns the set of MCP prompt names that must be server-qualified/// (`/<server>.<name>`) to stay unambiguous in the slash-command popup: names/// shared by more than one MCP prompt, or names colliding with a reserved/// built-in command (e.g. `/compact`). A built-in always wins an unqualified/// invocation, so colliding MCP prompts are only reachable when prefixed.fn ambiguous_mcp_prompt_names<'a>(    reserved: impl IntoIterator<Item = &'a str>,    prompt_names: impl IntoIterator<Item = &'a str>,) -> HashSet<&'a str> {    let mut counts: HashMap<&str, usize> = HashMap::default();    for name in reserved.into_iter().chain(prompt_names) {        *counts.entry(name).or_insert(0) += 1;    }    counts        .into_iter()        .filter_map(|(name, count)| (count > 1).then_some(name))        .collect()} struct ProjectState {    project: Entity<Project>,    project_context: Entity<ProjectContext>,    skills: Arc<Vec<Skill>>,    skill_loading_issues: Vec<SkillLoadingIssue>,    project_context_needs_refresh: watch::Sender<()>,    _maintain_project_context: Task<Result<()>>,    context_server_registry: Entity<ContextServerRegistry>,    _subscriptions: Vec<Subscription>,} /// 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: Entity<acp_thread::AcpThread>,    project_id: EntityId,    pending_save: Task<Result<()>>,    _subscriptions: Vec<Subscription>,    ref_count: usize,} struct PendingSession {    task: Shared<Task<Result<Entity<AcpThread>, Arc<anyhow::Error>>>>,    ref_count: usize,} pub struct LanguageModels {    /// Access language model by ID    models: HashMap<AgentModelId, 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 notify_model_selection_changed(&mut self) {        self.refresh_models_tx.send(()).ok();    }     pub fn model_from_id(&self, model_id: &AgentModelId) -> 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),            }),            is_latest: model.is_latest(),            cost: model.model_cost_info().map(|cost| cost.to_shared_string()),            disabled: model.is_disabled(),        }    }     fn model_id(model: &Arc<dyn LanguageModel>) -> AgentModelId {        AgentModelId::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.spawn(async move |cx| {            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                                    );                                }                            }                        }                    }                }            }             cx.update(|cx| {                LanguageModelRegistry::global(cx)                    .update(cx, |registry, cx| registry.refresh_fallback_model(cx))            });        })    }} /// Implemented by the UI layer to provide the ability for agent tools to create/// sibling threads that appear in the agent panel.////// `agent_ui::AgentPanel` installs an implementation of this trait on the/// `NativeAgent` when it sets up a connection. Tools in a native-agent thread/// then discover and use the host via `NativeThreadEnvironment`. The UI side/// is responsible for keeping the installed host current; a host whose/// backing UI has been torn down will fail its first request with a clear/// error rather than being detected up front.pub trait SiblingThreadHost {    fn create_sibling_thread(        &self,        request: SiblingThreadRequest,        cx: &mut AsyncApp,    ) -> Task<Result<SiblingThreadInfo>>;     fn list_available_agents(&self, cx: &mut App) -> Result<AvailableAgents>;} pub struct NativeAgent {    /// Session ID -> Session mapping    sessions: HashMap<acp::SessionId, Session>,    pending_sessions: HashMap<acp::SessionId, PendingSession>,    thread_store: Entity<ThreadStore>,    /// Project-specific state keyed by project EntityId    projects: HashMap<EntityId, ProjectState>,    /// Shared templates for all threads    templates: Arc<Templates>,    /// Cached model information    models: LanguageModels,    /// Handler installed by the UI for `create_thread` / `list_agents_and_models` tools.    sibling_thread_host: Option<Rc<dyn SiblingThreadHost>>,    fs: Arc<dyn Fs>,    _subscriptions: Vec<Subscription>,    /// Tracks the lifecycle of global skills directory observation. We    /// don't eagerly watch (or even check for) `~/.agents/skills/` at    /// startup; users who never engage with the agent panel pay zero    /// filesystem cost. The watch is kicked off lazily by    /// [`Self::ensure_skills_scan_started`], which is called from the    /// three agent-panel interaction points: input box focus, slash    /// autocomplete, and conversation submit.    skills_state: SkillsState,} #[derive(Default)]enum SkillsState {    /// No scan or watch is active. A user-interaction trigger will kick    /// off a fresh scan.    #[default]    Idle,    /// A one-shot scan task is in flight. It checks whether    /// `~/.agents/skills/` exists; if so, transitions to `Watching`,    /// otherwise back to `Idle`.    Scanning,    /// A watch task is observing `~/.agents/skills/`. It transitions    /// back to `Idle` if the watched directory itself is removed.    Watching,} impl gpui::EventEmitter<SkillLoadingIssuesUpdated> for NativeAgent {} static RULES_FILE_REL_PATHS: LazyLock<Vec<Arc<RelPath>>> = LazyLock::new(|| {    RULES_FILE_NAMES        .iter()        .filter_map(|name| {            RelPath::from_unix_str(name)                .ok()                .map(|path| path.into_arc())        })        .collect()}); static AGENTS_PREFIX: LazyLock<Option<Arc<RelPath>>> = LazyLock::new(|| {    RelPath::from_unix_str(AGENTS_DIR_NAME)        .ok()        .map(|path| path.into_arc())}); static SKILLS_PREFIX: LazyLock<Option<Arc<RelPath>>> = LazyLock::new(|| {    RelPath::from_unix_str(project_skills_relative_path())        .ok()        .map(|path| path.into_arc())}); struct ProjectSkillFile {    relative_path: Arc<RelPath>,    display_path: PathBuf,    size: u64,} async fn expand_worktree_directory(    worktree: &Entity<Worktree>,    path: &RelPath,    cx: &mut AsyncApp,) -> Result<()> {    let expand_task = worktree.update(cx, |worktree, cx| {        let entry_id = worktree            .entry_for_path(path)            .filter(|entry| entry.is_dir())            .map(|entry| entry.id);        entry_id.and_then(|entry_id| worktree.expand_entry(entry_id, cx))    });     if let Some(expand_task) = expand_task {        expand_task.await?;    }     Ok(())} async fn expand_project_skills_directories(    worktree: &Entity<Worktree>,    cx: &mut AsyncApp,) -> Result<()> {    let agents_dir = RelPath::from_unix_str(AGENTS_DIR_NAME)?;    let Some(skills_prefix) = SKILLS_PREFIX.as_ref() else {

Lines 1–500 of 6929.