use crate::AcpThread;use agent_client_protocol::{self as acp};use anyhow::Result;use chrono::{DateTime, Utc};use collections::{HashMap, IndexMap};use gpui::{Entity, SharedString, Task};use language_model::LanguageModelProviderId;use project::{AgentId, Project};use serde::{Deserialize, Serialize};use std::{any::Any, error::Error, fmt, path::PathBuf, rc::Rc, sync::Arc};use task::{HideStrategy, SpawnInTerminal, TaskId};use ui::{App, IconName};use util::path_list::PathList;use uuid::Uuid; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]pub struct UserMessageId(Arc<str>); impl UserMessageId { pub fn new() -> Self { Self(Uuid::new_v4().to_string().into()) }} pub fn build_terminal_auth_task( id: String, label: String, command: String, args: Vec<String>, env: HashMap<String, String>,) -> SpawnInTerminal { SpawnInTerminal { id: TaskId(id), full_label: label.clone(), label: label.clone(), command: Some(command), args, command_label: label, env, use_new_terminal: true, allow_concurrent_runs: true, hide: HideStrategy::Always, ..Default::default() }} pub trait AgentConnection { fn agent_id(&self) -> AgentId; fn telemetry_id(&self) -> SharedString; fn new_session( self: Rc<Self>, project: Entity<Project>, _work_dirs: PathList, cx: &mut App, ) -> Task<Result<Entity<AcpThread>>>; /// Whether this agent supports loading existing sessions. fn supports_load_session(&self) -> bool { false } /// Load an existing session by ID. fn load_session( self: Rc<Self>, _session_id: acp::SessionId, _project: Entity<Project>, _work_dirs: PathList, _title: Option<SharedString>, _cx: &mut App, ) -> Task<Result<Entity<AcpThread>>> { Task::ready(Err(anyhow::Error::msg("Loading sessions is not supported"))) } /// Whether this agent supports closing existing sessions. fn supports_close_session(&self) -> bool { false } /// Close an existing session. Allows the agent to free the session from memory. fn close_session( self: Rc<Self>, _session_id: &acp::SessionId, _cx: &mut App, ) -> Task<Result<()>> { Task::ready(Err(anyhow::Error::msg("Closing sessions is not supported"))) } /// Whether this agent supports resuming existing sessions without loading history. fn supports_resume_session(&self) -> bool { false } /// Resume an existing session by ID without replaying previous messages. fn resume_session( self: Rc<Self>, _session_id: acp::SessionId, _project: Entity<Project>, _work_dirs: PathList, _title: Option<SharedString>, _cx: &mut App, ) -> Task<Result<Entity<AcpThread>>> { Task::ready(Err(anyhow::Error::msg( "Resuming sessions is not supported", ))) } /// Whether this agent supports showing session history. fn supports_session_history(&self) -> bool { self.supports_load_session() || self.supports_resume_session() } fn auth_methods(&self) -> &[acp::AuthMethod]; fn terminal_auth_task( &self, _method: &acp::AuthMethodId, _cx: &App, ) -> Option<Task<Result<SpawnInTerminal>>> { None } fn authenticate(&self, method: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>>; fn prompt( &self, user_message_id: Option<UserMessageId>, params: acp::PromptRequest, cx: &mut App, ) -> Task<Result<acp::PromptResponse>>; fn retry(&self, _session_id: &acp::SessionId, _cx: &App) -> Option<Rc<dyn AgentSessionRetry>> { None } fn cancel(&self, session_id: &acp::SessionId, cx: &mut App); fn truncate( &self, _session_id: &acp::SessionId, _cx: &App, ) -> Option<Rc<dyn AgentSessionTruncate>> { None } fn set_title( &self, _session_id: &acp::SessionId, _cx: &App, ) -> Option<Rc<dyn AgentSessionSetTitle>> { None } /// Returns this agent as an [Rc<dyn ModelSelector>] if the model selection capability is supported. /// /// If the agent does not support model selection, returns [None]. /// This allows sharing the selector in UI components. fn model_selector(&self, _session_id: &acp::SessionId) -> Option<Rc<dyn AgentModelSelector>> { None } fn telemetry(&self) -> Option<Rc<dyn AgentTelemetry>> { None } fn session_modes( &self, _session_id: &acp::SessionId, _cx: &App, ) -> Option<Rc<dyn AgentSessionModes>> { None } fn session_config_options( &self, _session_id: &acp::SessionId, _cx: &App, ) -> Option<Rc<dyn AgentSessionConfigOptions>> { None } fn session_list(&self, _cx: &mut App) -> Option<Rc<dyn AgentSessionList>> { None } fn into_any(self: Rc<Self>) -> Rc<dyn Any>;} impl dyn AgentConnection { pub fn downcast<T: 'static + AgentConnection + Sized>(self: Rc<Self>) -> Option<Rc<T>> { self.into_any().downcast().ok() }} pub trait AgentSessionTruncate { fn run(&self, message_id: UserMessageId, cx: &mut App) -> Task<Result<()>>;} pub trait AgentSessionRetry { fn run(&self, cx: &mut App) -> Task<Result<acp::PromptResponse>>;} pub trait AgentSessionSetTitle { fn run(&self, title: SharedString, cx: &mut App) -> Task<Result<()>>;} pub trait AgentTelemetry { /// A representation of the current thread state that can be serialized for /// storage with telemetry events. fn thread_data( &self, session_id: &acp::SessionId, cx: &mut App, ) -> Task<Result<serde_json::Value>>;} pub trait AgentSessionModes { fn current_mode(&self) -> acp::SessionModeId; fn all_modes(&self) -> Vec<acp::SessionMode>; fn set_mode(&self, mode: acp::SessionModeId, cx: &mut App) -> Task<Result<()>>;} pub trait AgentSessionConfigOptions { /// Get all current config options with their state fn config_options(&self) -> Vec<acp::SessionConfigOption>; /// Set a config option value /// Returns the full updated list of config options fn set_config_option( &self, config_id: acp::SessionConfigId, value: acp::SessionConfigValueId, cx: &mut App, ) -> Task<Result<Vec<acp::SessionConfigOption>>>; /// Whenever the config options are updated the receiver will be notified. /// Optional for agents that don't update their config options dynamically. fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> { None }} #[derive(Debug, Clone, Default)]pub struct AgentSessionListRequest { pub cwd: Option<PathBuf>, pub cursor: Option<String>, pub meta: Option<acp::Meta>,} #[derive(Debug, Clone)]pub struct AgentSessionListResponse { pub sessions: Vec<AgentSessionInfo>, pub next_cursor: Option<String>, pub meta: Option<acp::Meta>,} impl AgentSessionListResponse { pub fn new(sessions: Vec<AgentSessionInfo>) -> Self { Self { sessions, next_cursor: None, meta: None, } }} #[derive(Debug, Clone, PartialEq)]pub struct AgentSessionInfo { pub session_id: acp::SessionId, pub work_dirs: Option<PathList>, pub title: Option<SharedString>, pub updated_at: Option<DateTime<Utc>>, pub created_at: Option<DateTime<Utc>>, pub meta: Option<acp::Meta>,} impl AgentSessionInfo { pub fn new(session_id: impl Into<acp::SessionId>) -> Self { Self { session_id: session_id.into(), work_dirs: None, title: None, updated_at: None, created_at: None, meta: None, } }} #[derive(Debug, Clone)]pub enum SessionListUpdate { Refresh, SessionInfo { session_id: acp::SessionId, update: acp::SessionInfoUpdate, },} pub trait AgentSessionList { fn list_sessions( &self, request: AgentSessionListRequest, cx: &mut App, ) -> Task<Result<AgentSessionListResponse>>; fn supports_delete(&self) -> bool { false } fn delete_session(&self, _session_id: &acp::SessionId, _cx: &mut App) -> Task<Result<()>> { Task::ready(Err(anyhow::anyhow!("delete_session not supported"))) } fn delete_sessions(&self, _cx: &mut App) -> Task<Result<()>> { Task::ready(Err(anyhow::anyhow!("delete_sessions not supported"))) } fn watch(&self, _cx: &mut App) -> Option<smol::channel::Receiver<SessionListUpdate>> { None } fn notify_refresh(&self) {} fn into_any(self: Rc<Self>) -> Rc<dyn Any>;} impl dyn AgentSessionList { pub fn downcast<T: 'static + AgentSessionList + Sized>(self: Rc<Self>) -> Option<Rc<T>> { self.into_any().downcast().ok() }} #[derive(Debug)]pub struct AuthRequired { pub description: Option<String>, pub provider_id: Option<LanguageModelProviderId>,} impl AuthRequired { pub fn new() -> Self { Self { description: None, provider_id: None, } } pub fn with_description(mut self, description: String) -> Self { self.description = Some(description); self } pub fn with_language_model_provider(mut self, provider_id: LanguageModelProviderId) -> Self { self.provider_id = Some(provider_id); self }} impl Error for AuthRequired {}impl fmt::Display for AuthRequired { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Authentication required") }} /// Trait for agents that support listing, selecting, and querying language models.////// This is an optional capability; agents indicate support via [AgentConnection::model_selector].pub trait AgentModelSelector: 'static { /// Lists all available language models for this agent. /// /// # Parameters /// - `cx`: The GPUI app context for async operations and global access. /// /// # Returns /// A task resolving to the list of models or an error (e.g., if no models are configured). fn list_models(&self, cx: &mut App) -> Task<Result<AgentModelList>>; /// Selects a model for a specific session (thread). /// /// This sets the default model for future interactions in the session. /// If the session doesn't exist or the model is invalid, it returns an error. /// /// # Parameters /// - `model`: The model to select (should be one from [list_models]). /// - `cx`: The GPUI app context. /// /// # Returns /// A task resolving to `Ok(())` on success or an error. fn select_model(&self, model_id: acp::ModelId, cx: &mut App) -> Task<Result<()>>; /// Retrieves the currently selected model for a specific session (thread). /// /// # Parameters /// - `cx`: The GPUI app context. /// /// # Returns /// A task resolving to the selected model (always set) or an error (e.g., session not found). fn selected_model(&self, cx: &mut App) -> Task<Result<AgentModelInfo>>; /// Whenever the model list is updated the receiver will be notified. /// Optional for agents that don't update their model list. fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> { None } /// Returns whether the model picker should render a footer. fn should_render_footer(&self) -> bool { false }} /// Icon for a model in the model selector.#[derive(Debug, Clone, PartialEq, Eq)]pub enum AgentModelIcon { /// A built-in icon from Zed's icon set. Named(IconName), /// Path to a custom SVG icon file. Path(SharedString),} #[derive(Debug, Clone, PartialEq, Eq)]pub struct AgentModelInfo { pub id: acp::ModelId, pub name: SharedString, pub description: Option<SharedString>, pub icon: Option<AgentModelIcon>, pub is_latest: bool, pub cost: Option<SharedString>,} impl From<acp::ModelInfo> for AgentModelInfo { fn from(info: acp::ModelInfo) -> Self { Self { id: info.model_id, name: info.name.into(), description: info.description.map(|desc| desc.into()), icon: None, is_latest: false, cost: None, } }} #[derive(Debug, Clone, PartialEq, Eq, Hash)]pub struct AgentModelGroupName(pub SharedString); #[derive(Debug, Clone)]pub enum AgentModelList { Flat(Vec<AgentModelInfo>), Grouped(IndexMap<AgentModelGroupName, Vec<AgentModelInfo>>),} impl AgentModelList { pub fn is_empty(&self) -> bool { match self { AgentModelList::Flat(models) => models.is_empty(), AgentModelList::Grouped(groups) => groups.is_empty(), } } pub fn is_flat(&self) -> bool { matches!(self, AgentModelList::Flat(_)) }} #[derive(Debug, Clone)]pub struct PermissionOptionChoice { pub allow: acp::PermissionOption, pub deny: acp::PermissionOption, pub sub_patterns: Vec<String>,} impl PermissionOptionChoice { pub fn label(&self) -> SharedString { self.allow.name.clone().into() } /// Build a `SelectedPermissionOutcome` for this choice. /// /// If the choice carries `sub_patterns`, they are attached as /// `SelectedPermissionParams::Terminal`. pub fn build_outcome(&self, is_allow: bool) -> crate::SelectedPermissionOutcome { let option = if is_allow { &self.allow } else { &self.deny }; let params = if !self.sub_patterns.is_empty() { Some(crate::SelectedPermissionParams::Terminal { patterns: self.sub_patterns.clone(), }) } else { None }; crate::SelectedPermissionOutcome::new(option.option_id.clone(), option.kind).params(params) }} /// Pairs a tool's permission pattern with its display nameLines 1–500 of 1014. Load more
use crate::AcpThread; use agent_client_protocol::{self as acp}; use anyhow::Result; use chrono::{DateTime, Utc}; use collections::{HashMap, IndexMap}; use gpui::{Entity, SharedString, Task}; use language_model::LanguageModelProviderId; use project::{AgentId, Project}; use serde::{Deserialize, Serialize}; use std::{any::Any, error::Error, fmt, path::PathBuf, rc::Rc, sync::Arc}; use task::{HideStrategy, SpawnInTerminal, TaskId}; use ui::{App, IconName}; use util::path_list::PathList; use uuid::Uuid; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] pub struct UserMessageId(Arc<str>); impl UserMessageId { pub fn new() -> Self { Self(Uuid::new_v4().to_string().into()) } } pub fn build_terminal_auth_task( id: String, label: String, command: String, args: Vec<String>, env: HashMap<String, String>, ) -> SpawnInTerminal { SpawnInTerminal { id: TaskId(id), full_label: label.clone(), label: label.clone(), command: Some(command), args, command_label: label, env, use_new_terminal: true, allow_concurrent_runs: true, hide: HideStrategy::Always, ..Default::default() } } pub trait AgentConnection { fn agent_id(&self) -> AgentId; fn telemetry_id(&self) -> SharedString; fn new_session( self: Rc<Self>, project: Entity<Project>, _work_dirs: PathList, cx: &mut App, ) -> Task<Result<Entity<AcpThread>>>; /// Whether this agent supports loading existing sessions. fn supports_load_session(&self) -> bool { false } /// Load an existing session by ID. fn load_session( self: Rc<Self>, _session_id: acp::SessionId, _project: Entity<Project>, _work_dirs: PathList, _title: Option<SharedString>, _cx: &mut App, ) -> Task<Result<Entity<AcpThread>>> { Task::ready(Err(anyhow::Error::msg("Loading sessions is not supported"))) } /// Whether this agent supports closing existing sessions. fn supports_close_session(&self) -> bool { false } /// Close an existing session. Allows the agent to free the session from memory. fn close_session( self: Rc<Self>, _session_id: &acp::SessionId, _cx: &mut App, ) -> Task<Result<()>> { Task::ready(Err(anyhow::Error::msg("Closing sessions is not supported"))) } /// Whether this agent supports resuming existing sessions without loading history. fn supports_resume_session(&self) -> bool { false } /// Resume an existing session by ID without replaying previous messages. fn resume_session( self: Rc<Self>, _session_id: acp::SessionId, _project: Entity<Project>, _work_dirs: PathList, _title: Option<SharedString>, _cx: &mut App, ) -> Task<Result<Entity<AcpThread>>> { Task::ready(Err(anyhow::Error::msg( "Resuming sessions is not supported", ))) } /// Whether this agent supports showing session history. fn supports_session_history(&self) -> bool { self.supports_load_session() || self.supports_resume_session() } fn auth_methods(&self) -> &[acp::AuthMethod]; fn terminal_auth_task( &self, _method: &acp::AuthMethodId, _cx: &App, ) -> Option<Task<Result<SpawnInTerminal>>> { None } fn authenticate(&self, method: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>>; fn prompt( &self, user_message_id: Option<UserMessageId>, params: acp::PromptRequest, cx: &mut App, ) -> Task<Result<acp::PromptResponse>>; fn retry(&self, _session_id: &acp::SessionId, _cx: &App) -> Option<Rc<dyn AgentSessionRetry>> { None } fn cancel(&self, session_id: &acp::SessionId, cx: &mut App); fn truncate( &self, _session_id: &acp::SessionId, _cx: &App, ) -> Option<Rc<dyn AgentSessionTruncate>> { None } fn set_title( &self, _session_id: &acp::SessionId, _cx: &App, ) -> Option<Rc<dyn AgentSessionSetTitle>> { None } /// Returns this agent as an [Rc<dyn ModelSelector>] if the model selection capability is supported. /// /// If the agent does not support model selection, returns [None]. /// This allows sharing the selector in UI components. fn model_selector(&self, _session_id: &acp::SessionId) -> Option<Rc<dyn AgentModelSelector>> { None } fn telemetry(&self) -> Option<Rc<dyn AgentTelemetry>> { None } fn session_modes( &self, _session_id: &acp::SessionId, _cx: &App, ) -> Option<Rc<dyn AgentSessionModes>> { None } fn session_config_options( &self, _session_id: &acp::SessionId, _cx: &App, ) -> Option<Rc<dyn AgentSessionConfigOptions>> { None } fn session_list(&self, _cx: &mut App) -> Option<Rc<dyn AgentSessionList>> { None } fn into_any(self: Rc<Self>) -> Rc<dyn Any>; } impl dyn AgentConnection { pub fn downcast<T: 'static + AgentConnection + Sized>(self: Rc<Self>) -> Option<Rc<T>> { self.into_any().downcast().ok() } } pub trait AgentSessionTruncate { fn run(&self, message_id: UserMessageId, cx: &mut App) -> Task<Result<()>>; } pub trait AgentSessionRetry { fn run(&self, cx: &mut App) -> Task<Result<acp::PromptResponse>>; } pub trait AgentSessionSetTitle { fn run(&self, title: SharedString, cx: &mut App) -> Task<Result<()>>; } pub trait AgentTelemetry { /// A representation of the current thread state that can be serialized for /// storage with telemetry events. fn thread_data( &self, session_id: &acp::SessionId, cx: &mut App, ) -> Task<Result<serde_json::Value>>; } pub trait AgentSessionModes { fn current_mode(&self) -> acp::SessionModeId; fn all_modes(&self) -> Vec<acp::SessionMode>; fn set_mode(&self, mode: acp::SessionModeId, cx: &mut App) -> Task<Result<()>>; } pub trait AgentSessionConfigOptions { /// Get all current config options with their state fn config_options(&self) -> Vec<acp::SessionConfigOption>; /// Set a config option value /// Returns the full updated list of config options fn set_config_option( &self, config_id: acp::SessionConfigId, value: acp::SessionConfigValueId, cx: &mut App, ) -> Task<Result<Vec<acp::SessionConfigOption>>>; /// Whenever the config options are updated the receiver will be notified. /// Optional for agents that don't update their config options dynamically. fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> { None } } #[derive(Debug, Clone, Default)] pub struct AgentSessionListRequest { pub cwd: Option<PathBuf>, pub cursor: Option<String>, pub meta: Option<acp::Meta>, } #[derive(Debug, Clone)] pub struct AgentSessionListResponse { pub sessions: Vec<AgentSessionInfo>, pub next_cursor: Option<String>, pub meta: Option<acp::Meta>, } impl AgentSessionListResponse { pub fn new(sessions: Vec<AgentSessionInfo>) -> Self { Self { sessions, next_cursor: None, meta: None, } } } #[derive(Debug, Clone, PartialEq)] pub struct AgentSessionInfo { pub session_id: acp::SessionId, pub work_dirs: Option<PathList>, pub title: Option<SharedString>, pub updated_at: Option<DateTime<Utc>>, pub created_at: Option<DateTime<Utc>>, pub meta: Option<acp::Meta>, } impl AgentSessionInfo { pub fn new(session_id: impl Into<acp::SessionId>) -> Self { Self { session_id: session_id.into(), work_dirs: None, title: None, updated_at: None, created_at: None, meta: None, } } } #[derive(Debug, Clone)] pub enum SessionListUpdate { Refresh, SessionInfo { session_id: acp::SessionId, update: acp::SessionInfoUpdate, }, } pub trait AgentSessionList { fn list_sessions( &self, request: AgentSessionListRequest, cx: &mut App, ) -> Task<Result<AgentSessionListResponse>>; fn supports_delete(&self) -> bool { false } fn delete_session(&self, _session_id: &acp::SessionId, _cx: &mut App) -> Task<Result<()>> { Task::ready(Err(anyhow::anyhow!("delete_session not supported"))) } fn delete_sessions(&self, _cx: &mut App) -> Task<Result<()>> { Task::ready(Err(anyhow::anyhow!("delete_sessions not supported"))) } fn watch(&self, _cx: &mut App) -> Option<smol::channel::Receiver<SessionListUpdate>> { None } fn notify_refresh(&self) {} fn into_any(self: Rc<Self>) -> Rc<dyn Any>; } impl dyn AgentSessionList { pub fn downcast<T: 'static + AgentSessionList + Sized>(self: Rc<Self>) -> Option<Rc<T>> { self.into_any().downcast().ok() } } #[derive(Debug)] pub struct AuthRequired { pub description: Option<String>, pub provider_id: Option<LanguageModelProviderId>, } impl AuthRequired { pub fn new() -> Self { Self { description: None, provider_id: None, } } pub fn with_description(mut self, description: String) -> Self { self.description = Some(description); self } pub fn with_language_model_provider(mut self, provider_id: LanguageModelProviderId) -> Self { self.provider_id = Some(provider_id); self } } impl Error for AuthRequired {} impl fmt::Display for AuthRequired { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Authentication required") } } /// Trait for agents that support listing, selecting, and querying language models. /// /// This is an optional capability; agents indicate support via [AgentConnection::model_selector]. pub trait AgentModelSelector: 'static { /// Lists all available language models for this agent. /// /// # Parameters /// - `cx`: The GPUI app context for async operations and global access. /// /// # Returns /// A task resolving to the list of models or an error (e.g., if no models are configured). fn list_models(&self, cx: &mut App) -> Task<Result<AgentModelList>>; /// Selects a model for a specific session (thread). /// /// This sets the default model for future interactions in the session. /// If the session doesn't exist or the model is invalid, it returns an error. /// /// # Parameters /// - `model`: The model to select (should be one from [list_models]). /// - `cx`: The GPUI app context. /// /// # Returns /// A task resolving to `Ok(())` on success or an error. fn select_model(&self, model_id: acp::ModelId, cx: &mut App) -> Task<Result<()>>; /// Retrieves the currently selected model for a specific session (thread). /// /// # Parameters /// - `cx`: The GPUI app context. /// /// # Returns /// A task resolving to the selected model (always set) or an error (e.g., session not found). fn selected_model(&self, cx: &mut App) -> Task<Result<AgentModelInfo>>; /// Whenever the model list is updated the receiver will be notified. /// Optional for agents that don't update their model list. fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> { None } /// Returns whether the model picker should render a footer. fn should_render_footer(&self) -> bool { false } } /// Icon for a model in the model selector. #[derive(Debug, Clone, PartialEq, Eq)] pub enum AgentModelIcon { /// A built-in icon from Zed's icon set. Named(IconName), /// Path to a custom SVG icon file. Path(SharedString), } #[derive(Debug, Clone, PartialEq, Eq)] pub struct AgentModelInfo { pub id: acp::ModelId, pub name: SharedString, pub description: Option<SharedString>, pub icon: Option<AgentModelIcon>, pub is_latest: bool, pub cost: Option<SharedString>, } impl From<acp::ModelInfo> for AgentModelInfo { fn from(info: acp::ModelInfo) -> Self { Self { id: info.model_id, name: info.name.into(), description: info.description.map(|desc| desc.into()), icon: None, is_latest: false, cost: None, } } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct AgentModelGroupName(pub SharedString); #[derive(Debug, Clone)] pub enum AgentModelList { Flat(Vec<AgentModelInfo>), Grouped(IndexMap<AgentModelGroupName, Vec<AgentModelInfo>>), } impl AgentModelList { pub fn is_empty(&self) -> bool { match self { AgentModelList::Flat(models) => models.is_empty(), AgentModelList::Grouped(groups) => groups.is_empty(), } } pub fn is_flat(&self) -> bool { matches!(self, AgentModelList::Flat(_)) } } #[derive(Debug, Clone)] pub struct PermissionOptionChoice { pub allow: acp::PermissionOption, pub deny: acp::PermissionOption, pub sub_patterns: Vec<String>, } impl PermissionOptionChoice { pub fn label(&self) -> SharedString { self.allow.name.clone().into() } /// Build a `SelectedPermissionOutcome` for this choice. /// /// If the choice carries `sub_patterns`, they are attached as /// `SelectedPermissionParams::Terminal`. pub fn build_outcome(&self, is_allow: bool) -> crate::SelectedPermissionOutcome { let option = if is_allow { &self.allow } else { &self.deny }; let params = if !self.sub_patterns.is_empty() { Some(crate::SelectedPermissionParams::Terminal { patterns: self.sub_patterns.clone(), }) } else { None }; crate::SelectedPermissionOutcome::new(option.option_id.clone(), option.kind).params(params) } } /// Pairs a tool's permission pattern with its display name