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 6935. Load more
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 session struct 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 {