use acp_thread::{ AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, AuthRequired, ClientUserMessageId, ElicitationEntryId, ElicitationStatus, ElicitationStore, LoadError, MaxOutputTokensError, MentionUri, PermissionOptionChoice, PermissionOptions, PermissionPattern, RetryStatus, SelectedPermissionOutcome, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus,};use acp_thread::{AgentConnection, Plan};use action_log::{ActionLog, ActionLogTelemetry, DiffStats};use agent::{NativeAgentServer, NoModelConfiguredError, ThreadStore};use agent_client_protocol::schema::v1 as acp;#[cfg(test)]use agent_servers::AgentServerDelegate;use agent_servers::{AgentServer, GEMINI_TERMINAL_AUTH_METHOD_ID};use agent_settings::{AgentProfileId, AgentSettings};use anyhow::{Result, anyhow};#[cfg(feature = "audio")]use audio::{Audio, Sound};use buffer_diff::BufferDiff;use client::zed_urls;use collections::{HashMap, HashSet, IndexMap};use editor::scroll::Autoscroll;use editor::{ Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects, SizingBehavior,};use file_icons::FileIcons;use fs::Fs;use futures::FutureExt as _;use gpui::{ Action, Animation, AnimationExt, App, ClickEvent, ClipboardItem, CursorStyle, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable, Hsla, ListOffset, ListState, ObjectFit, PlatformDisplay, ScrollHandle, SharedString, StyledText, Subscription, Task, TextRun, TextStyle, WeakEntity, Window, WindowHandle, div, ease_in_out, img, linear_color_stop, linear_gradient, list, pulsating_between,};use language::{Buffer, Language, Rope};use language_model::LanguageModelCompletionError;use markdown::{ CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownFont, MarkdownStyle,};use parking_lot::{Mutex, RwLock};use project::{AgentId, AgentServerStore, Project, ProjectEntryId, ProjectPath}; use crate::conversation_view::elicitation::{ ElicitationCard, ElicitationCardHandlers, ElicitationFormState, should_render_elicitation,};use crate::message_editor::SessionCapabilities;use crate::{AgentThreadSource, DEFAULT_THREAD_TITLE, resolve_agent_image};use lru::LruCache;use rope::Point;use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};use std::num::NonZeroUsize;use std::path::{Path, PathBuf};use std::sync::Arc;use std::time::Instant;use std::{rc::Rc, time::Duration};use terminal_view::terminal_panel::TerminalPanel;use text::Anchor;use theme_settings::{AgentBufferFontSize, AgentUiFontSize};use ui::{ Callout, CircularProgress, CommonAnimationExt, ContextMenu, ContextMenuEntry, CopyButton, DecoratedIcon, DiffStat, Disclosure, Divider, DividerColor, IconDecoration, IconDecorationKind, KeyBinding, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip, WithScrollbar, prelude::*, right_click_menu,};use util::{ ResultExt, debug_panic, defer, paths::{PathStyle, PathWithPosition}, rel_path::RelPath, size::format_file_size, time::duration_alt_display,};use workspace::{ CollaboratorId, MultiWorkspace, NewTerminal, PathList, Workspace, path_link::sanitize_path_text,};use zed_actions::agent::{Chat, ToggleModelSelector}; use super::config_options::ConfigOptionsView;use super::entry_view_state::EntryViewState;use crate::ModeSelector;use crate::ModelSelectorPopover;use crate::agent_connection_store::{ AgentConnectedState, AgentConnectionEntryEvent, AgentConnectionStore,};use crate::agent_diff::AgentDiff;use crate::completion_provider::{AgentContextSelection, AvailableSkill};use crate::entry_view_state::{EntryViewEvent, ViewEvent};use crate::message_editor::{InputAttempt, MessageEditor, MessageEditorEvent};use crate::profile_selector::{ProfileProvider, ProfileSelector}; use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore};use crate::ui::{AgentNotification, AgentNotificationEvent};use crate::{ Agent, AgentDiffPane, AgentInitialContent, AgentPanel, AgentPanelEvent, AllowAlways, AllowOnce, AuthorizeToolCall, ClearMessageQueue, CycleFavoriteModels, CycleModeSelector, CycleThinkingEffort, EditFirstQueuedMessage, ExpandMessageEditor, Follow, KeepAll, NewThread, OpenAddContextMenu, OpenAgentDiff, RejectAll, RejectOnce, RemoveFirstQueuedMessage, ScrollOutputLineDown, ScrollOutputLineUp, ScrollOutputPageDown, ScrollOutputPageUp, ScrollOutputToBottom, ScrollOutputToNextMessage, ScrollOutputToPreviousMessage, ScrollOutputToTop, SendImmediately, SendNextQueuedMessage, ToggleFastMode, ToggleProfileSelector, ToggleSteerFirstQueuedMessage, ToggleThinkingEffortMenu, ToggleThinkingMode, UndoLastReject,}; const STOPWATCH_THRESHOLD: Duration = Duration::from_secs(30);const TOKEN_THRESHOLD: u64 = 250; pub(crate) const DRAFT_PROMPT_PERSIST_DEBOUNCE: Duration = Duration::from_millis(250); pub(crate) mod elicitation;mod message_queue;mod thread_search_bar;mod thread_view;pub use message_queue::*;pub use thread_view::*; #[derive(Copy, Clone, Debug, PartialEq, Eq)]enum ThreadFeedback { Positive, Negative,} #[derive(Debug)]pub(crate) enum ThreadError { PaymentRequired, DataRetentionConsentRequired, Refusal, AuthenticationRequired(SharedString), RateLimitExceeded { provider: SharedString, }, ServerOverloaded { provider: SharedString, }, PromptTooLarge, NoCredentials { provider: SharedString, }, StreamError { provider: SharedString, }, AuthenticationFailed { provider: SharedString, }, PermissionDenied { provider: SharedString, message: Option<SharedString>, }, RequestFailed, MaxOutputTokens, NoModelSelected, ApiError { provider: SharedString, }, Other { message: SharedString, acp_error_code: Option<SharedString>, },} impl From<anyhow::Error> for ThreadError { fn from(error: anyhow::Error) -> Self { if error.is::<MaxOutputTokensError>() { Self::MaxOutputTokens } else if error.is::<NoModelConfiguredError>() { Self::NoModelSelected } else if let Some(acp_error) = error.downcast_ref::<acp::Error>() && acp_error.code == acp::ErrorCode::AuthRequired { Self::AuthenticationRequired(acp_error.message.clone().into()) } else if let Some(lm_error) = error.downcast_ref::<LanguageModelCompletionError>() { use LanguageModelCompletionError::*; match lm_error { RateLimitExceeded { provider, .. } => Self::RateLimitExceeded { provider: provider.to_string().into(), }, ServerOverloaded { provider, .. } | ApiInternalServerError { provider, .. } => { Self::ServerOverloaded { provider: provider.to_string().into(), } } PromptTooLarge { .. } => Self::PromptTooLarge, PaymentRequired => Self::PaymentRequired, NoApiKey { provider } => Self::NoCredentials { provider: provider.to_string().into(), }, StreamEndedUnexpectedly { provider } | ApiReadResponseError { provider, .. } | DeserializeResponse { provider, .. } | HttpSend { provider, .. } => Self::StreamError { provider: provider.to_string().into(), }, AuthenticationError { provider, .. } => Self::AuthenticationFailed { provider: provider.to_string().into(), }, PermissionError { provider, message } => Self::PermissionDenied { provider: provider.to_string().into(), message: Some(message.clone().into()), }, UpstreamProviderError { .. } => Self::RequestFailed, DataRetentionConsentRequired { .. } => Self::DataRetentionConsentRequired, BadRequestFormat { provider, .. } | HttpResponseError { provider, .. } | ApiEndpointNotFound { provider } => Self::ApiError { provider: provider.to_string().into(), }, _ => { let message: SharedString = format!("{:#}", error).into(); Self::Other { message, acp_error_code: None, } } } } else { let message: SharedString = format!("{:#}", error).into(); // Extract ACP error code if available let acp_error_code = error .downcast_ref::<acp::Error>() .map(|acp_error| SharedString::from(acp_error.code.to_string())); Self::Other { message, acp_error_code, } } }} impl ProfileProvider for Entity<agent::Thread> { fn profile_id(&self, cx: &App) -> AgentProfileId { self.read(cx).profile().clone() } fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) { self.update(cx, |thread, cx| { // Apply the profile and let the thread swap to its default model. thread.set_profile(profile_id, cx); }); } fn profiles_supported(&self, cx: &App) -> bool { self.read(cx) .model() .is_some_and(|model| model.supports_tools()) } fn model_selected(&self, cx: &App) -> bool { self.read(cx).model().is_some() } fn is_restricted(&self, cx: &App) -> bool { project::trusted_worktrees::TrustedWorktrees::has_restricted_worktrees( &self.read(cx).project().read(cx).worktree_store(), cx, ) } fn profile_downgraded(&self, cx: &App) -> bool { self.read(cx).profile_was_downgraded() }} #[derive(Default)]pub(crate) struct Conversation { threads: HashMap<acp::SessionId, Entity<AcpThread>>, permission_requests: IndexMap<acp::SessionId, Vec<acp::ToolCallId>>, elicitation_requests: IndexMap<acp::SessionId, Vec<ElicitationEntryId>>, subscriptions: Vec<Subscription>, updated_at: Option<Instant>,} impl Conversation { pub fn register_thread(&mut self, thread: Entity<AcpThread>, cx: &mut Context<Self>) { let session_id = thread.read(cx).session_id().clone(); let subscription = cx.subscribe(&thread, { let session_id = session_id.clone(); move |this, _thread, event, _cx| { this.updated_at = Some(Instant::now()); match event { AcpThreadEvent::ToolAuthorizationRequested(id) => { this.permission_requests .entry(session_id.clone()) .or_default() .push(id.clone()); } AcpThreadEvent::ToolAuthorizationReceived(id) => { if let Some(tool_calls) = this.permission_requests.get_mut(&session_id) { tool_calls.retain(|tool_call_id| tool_call_id != id); if tool_calls.is_empty() { this.permission_requests.shift_remove(&session_id); } } } AcpThreadEvent::ElicitationRequested(id) => { this.elicitation_requests .entry(session_id.clone()) .or_default() .push(id.clone()); } AcpThreadEvent::ElicitationResponded(id) => { if let Some(elicitations) = this.elicitation_requests.get_mut(&session_id) { elicitations.retain(|elicitation_id| elicitation_id != id); if elicitations.is_empty() { this.elicitation_requests.shift_remove(&session_id); } } } AcpThreadEvent::NewEntry | AcpThreadEvent::StatusChanged | AcpThreadEvent::TitleUpdated | AcpThreadEvent::TokenUsageUpdated | AcpThreadEvent::EntryUpdated(_) | AcpThreadEvent::EntriesRemoved(_) | AcpThreadEvent::Retry(_) | AcpThreadEvent::SubagentSpawned(_) | AcpThreadEvent::Stopped(_) | AcpThreadEvent::Error | AcpThreadEvent::LoadError(_) | AcpThreadEvent::PromptCapabilitiesUpdated | AcpThreadEvent::Refusal | AcpThreadEvent::AvailableCommandsUpdated(_) | AcpThreadEvent::ModeUpdated(_) | AcpThreadEvent::ConfigOptionsUpdated(_) | AcpThreadEvent::WorkingDirectoriesUpdated | AcpThreadEvent::PromptUpdated => {} } } }); self.subscriptions.push(subscription); self.threads.insert(session_id, thread); } pub fn permission_options_for_tool_call<'a>( &'a self, session_id: &acp::SessionId, tool_call_id: acp::ToolCallId, cx: &'a App, ) -> Option<&'a PermissionOptions> { let thread = self.threads.get(session_id)?; let (_, tool_call) = thread.read(cx).tool_call(&tool_call_id)?; let ToolCallStatus::WaitingForConfirmation { options, .. } = &tool_call.status else { return None; }; Some(options) } pub fn pending_tool_call<'a>( &'a self, session_id: &acp::SessionId, cx: &'a App, ) -> Option<(acp::SessionId, acp::ToolCallId, &'a PermissionOptions)> { let thread = self.threads.get(session_id)?; let is_subagent = thread.read(cx).parent_session_id().is_some(); let (result_session_id, thread, tool_id) = if is_subagent { let id = self.permission_requests.get(session_id)?.iter().next()?; (session_id.clone(), thread, id) } else { let (id, tool_calls) = self.permission_requests.first()?; let thread = self.threads.get(id)?; let tool_id = tool_calls.iter().next()?; (id.clone(), thread, tool_id) }; let (_, tool_call) = thread.read(cx).tool_call(tool_id)?; let ToolCallStatus::WaitingForConfirmation { options, .. } = &tool_call.status else { return None; }; Some((result_session_id, tool_id.clone(), options)) } pub fn subagents_awaiting_permission(&self, cx: &App) -> Vec<(acp::SessionId, usize)> { self.permission_requests .iter() .filter_map(|(session_id, tool_call_ids)| { let thread = self.threads.get(session_id)?; if thread.read(cx).parent_session_id().is_some() && !tool_call_ids.is_empty() { Some((session_id.clone(), tool_call_ids.len())) } else { None } }) .collect() } /// Returns the first pending tool call request for exactly `session_id`. /// Unlike `pending_tool_call`, this does not use the global FIFO pending /// request for non-subagent sessions. pub fn pending_tool_call_for_session( &self, session_id: &acp::SessionId, cx: &App, ) -> Option<acp::ToolCallId> { let thread = self.threads.get(session_id)?; let tool_call_id = self.permission_requests.get(session_id)?.iter().next()?; let (_, tool_call) = thread.read(cx).tool_call(tool_call_id)?; if !matches!( tool_call.status, ToolCallStatus::WaitingForConfirmation { .. } ) { return None; } Some(tool_call_id.clone()) } pub fn pending_tool_call_count_for_session(&self, session_id: &acp::SessionId) -> usize { self.permission_requests .get(session_id) .map(|tool_call_ids| tool_call_ids.len()) .unwrap_or(0) } pub fn respond_to_elicitation( &mut self, session_id: acp::SessionId, elicitation_id: ElicitationEntryId, response: acp::CreateElicitationResponse, cx: &mut Context<Self>, ) -> Option<()> { let thread = self.threads.get(&session_id)?.clone(); thread.update(cx, |thread, cx| { thread.respond_to_elicitation(&elicitation_id, response, cx); }); Some(()) } pub fn authorize_pending_tool_call( &mut self, session_id: &acp::SessionId, kind: acp::PermissionOptionKind, cx: &mut Context<Self>, ) -> Option<()> { let (authorize_session_id, tool_call_id, options) = self.pending_tool_call(session_id, cx)?; let option = permission_option_for_action(options, kind)?; self.authorize_tool_call( authorize_session_id, tool_call_id, SelectedPermissionOutcome::new(option.option_id.clone(), option.kind), cx, ); Some(()) } pub fn authorize_with_granularity( &mut self, session_id: acp::SessionId, tool_call_id: acp::ToolCallId, selection: Option<&thread_view::PermissionSelection>, is_allow: bool, cx: &mut Context<Self>, ) -> Option<()> { let options = self.permission_options_for_tool_call(&session_id, tool_call_id.clone(), cx)?; let outcome = resolve_outcome_from_selection(options, selection, is_allow)?; self.authorize_tool_call(session_id, tool_call_id, outcome, cx); Some(()) } pub fn authorize_tool_call( &mut self, session_id: acp::SessionId, tool_call_id: acp::ToolCallId, outcome: SelectedPermissionOutcome, cx: &mut Context<Self>, ) { let Some(thread) = self.threads.get(&session_id) else { return; }; let agent_telemetry_id = thread.read(cx).connection().telemetry_id(); let session_id = thread.read(cx).session_id().clone(); telemetry::event!( "Agent Tool Call Authorized", agent = agent_telemetry_id, session = session_id, option = outcome.option_kind ); thread.update(cx, |thread, cx| { thread.authorize_tool_call(tool_call_id, outcome, cx); }); cx.notify(); } fn set_work_dirs(&mut self, work_dirs: PathList, cx: &mut Context<Self>) { for thread in self.threads.values() { thread.update(cx, |thread, cx| { thread.set_work_dirs(work_dirs.clone(), cx); }); } }} pub(crate) struct RootThreadUpdated; impl EventEmitter<RootThreadUpdated> for ConversationView {} fn permission_option_for_action(Lines 1–500 of 11184. Load more
use acp_thread::{ AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, AuthRequired, ClientUserMessageId, ElicitationEntryId, ElicitationStatus, ElicitationStore, LoadError, MaxOutputTokensError, MentionUri, PermissionOptionChoice, PermissionOptions, PermissionPattern, RetryStatus, SelectedPermissionOutcome, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, }; use acp_thread::{AgentConnection, Plan}; use action_log::{ActionLog, ActionLogTelemetry, DiffStats}; use agent::{NativeAgentServer, NoModelConfiguredError, ThreadStore}; use agent_client_protocol::schema::v1 as acp; #[cfg(test)] use agent_servers::AgentServerDelegate; use agent_servers::{AgentServer, GEMINI_TERMINAL_AUTH_METHOD_ID}; use agent_settings::{AgentProfileId, AgentSettings}; use anyhow::{Result, anyhow}; #[cfg(feature = "audio")] use audio::{Audio, Sound}; use buffer_diff::BufferDiff; use client::zed_urls; use collections::{HashMap, HashSet, IndexMap}; use editor::scroll::Autoscroll; use editor::{ Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects, SizingBehavior, }; use file_icons::FileIcons; use fs::Fs; use futures::FutureExt as _; use gpui::{ Action, Animation, AnimationExt, App, ClickEvent, ClipboardItem, CursorStyle, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable, Hsla, ListOffset, ListState, ObjectFit, PlatformDisplay, ScrollHandle, SharedString, StyledText, Subscription, Task, TextRun, TextStyle, WeakEntity, Window, WindowHandle, div, ease_in_out, img, linear_color_stop, linear_gradient, list, pulsating_between, }; use language::{Buffer, Language, Rope}; use language_model::LanguageModelCompletionError; use markdown::{ CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownFont, MarkdownStyle, }; use parking_lot::{Mutex, RwLock}; use project::{AgentId, AgentServerStore, Project, ProjectEntryId, ProjectPath}; use crate::conversation_view::elicitation::{ ElicitationCard, ElicitationCardHandlers, ElicitationFormState, should_render_elicitation, }; use crate::message_editor::SessionCapabilities; use crate::{AgentThreadSource, DEFAULT_THREAD_TITLE, resolve_agent_image}; use lru::LruCache; use rope::Point; use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore}; use std::num::NonZeroUsize; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Instant; use std::{rc::Rc, time::Duration}; use terminal_view::terminal_panel::TerminalPanel; use text::Anchor; use theme_settings::{AgentBufferFontSize, AgentUiFontSize}; use ui::{ Callout, CircularProgress, CommonAnimationExt, ContextMenu, ContextMenuEntry, CopyButton, DecoratedIcon, DiffStat, Disclosure, Divider, DividerColor, IconDecoration, IconDecorationKind, KeyBinding, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip, WithScrollbar, prelude::*, right_click_menu, }; use util::{ ResultExt, debug_panic, defer, paths::{PathStyle, PathWithPosition}, rel_path::RelPath, size::format_file_size, time::duration_alt_display, }; use workspace::{ CollaboratorId, MultiWorkspace, NewTerminal, PathList, Workspace, path_link::sanitize_path_text, }; use zed_actions::agent::{Chat, ToggleModelSelector}; use super::config_options::ConfigOptionsView; use super::entry_view_state::EntryViewState; use crate::ModeSelector; use crate::ModelSelectorPopover; use crate::agent_connection_store::{ AgentConnectedState, AgentConnectionEntryEvent, AgentConnectionStore, }; use crate::agent_diff::AgentDiff; use crate::completion_provider::{AgentContextSelection, AvailableSkill}; use crate::entry_view_state::{EntryViewEvent, ViewEvent}; use crate::message_editor::{InputAttempt, MessageEditor, MessageEditorEvent}; use crate::profile_selector::{ProfileProvider, ProfileSelector}; use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore}; use crate::ui::{AgentNotification, AgentNotificationEvent}; use crate::{ Agent, AgentDiffPane, AgentInitialContent, AgentPanel, AgentPanelEvent, AllowAlways, AllowOnce, AuthorizeToolCall, ClearMessageQueue, CycleFavoriteModels, CycleModeSelector, CycleThinkingEffort, EditFirstQueuedMessage, ExpandMessageEditor, Follow, KeepAll, NewThread, OpenAddContextMenu, OpenAgentDiff, RejectAll, RejectOnce, RemoveFirstQueuedMessage, ScrollOutputLineDown, ScrollOutputLineUp, ScrollOutputPageDown, ScrollOutputPageUp, ScrollOutputToBottom, ScrollOutputToNextMessage, ScrollOutputToPreviousMessage, ScrollOutputToTop, SendImmediately, SendNextQueuedMessage, ToggleFastMode, ToggleProfileSelector, ToggleSteerFirstQueuedMessage, ToggleThinkingEffortMenu, ToggleThinkingMode, UndoLastReject, }; const STOPWATCH_THRESHOLD: Duration = Duration::from_secs(30); const TOKEN_THRESHOLD: u64 = 250; pub(crate) const DRAFT_PROMPT_PERSIST_DEBOUNCE: Duration = Duration::from_millis(250); pub(crate) mod elicitation; mod message_queue; mod thread_search_bar; mod thread_view; pub use message_queue::*; pub use thread_view::*; #[derive(Copy, Clone, Debug, PartialEq, Eq)] enum ThreadFeedback { Positive, Negative, } #[derive(Debug)] pub(crate) enum ThreadError { PaymentRequired, DataRetentionConsentRequired, Refusal, AuthenticationRequired(SharedString), RateLimitExceeded { provider: SharedString, }, ServerOverloaded { provider: SharedString, }, PromptTooLarge, NoCredentials { provider: SharedString, }, StreamError { provider: SharedString, }, AuthenticationFailed { provider: SharedString, }, PermissionDenied { provider: SharedString, message: Option<SharedString>, }, RequestFailed, MaxOutputTokens, NoModelSelected, ApiError { provider: SharedString, }, Other { message: SharedString, acp_error_code: Option<SharedString>, }, } impl From<anyhow::Error> for ThreadError { fn from(error: anyhow::Error) -> Self { if error.is::<MaxOutputTokensError>() { Self::MaxOutputTokens } else if error.is::<NoModelConfiguredError>() { Self::NoModelSelected } else if let Some(acp_error) = error.downcast_ref::<acp::Error>() && acp_error.code == acp::ErrorCode::AuthRequired { Self::AuthenticationRequired(acp_error.message.clone().into()) } else if let Some(lm_error) = error.downcast_ref::<LanguageModelCompletionError>() { use LanguageModelCompletionError::*; match lm_error { RateLimitExceeded { provider, .. } => Self::RateLimitExceeded { provider: provider.to_string().into(), }, ServerOverloaded { provider, .. } | ApiInternalServerError { provider, .. } => { Self::ServerOverloaded { provider: provider.to_string().into(), } } PromptTooLarge { .. } => Self::PromptTooLarge, PaymentRequired => Self::PaymentRequired, NoApiKey { provider } => Self::NoCredentials { provider: provider.to_string().into(), }, StreamEndedUnexpectedly { provider } | ApiReadResponseError { provider, .. } | DeserializeResponse { provider, .. } | HttpSend { provider, .. } => Self::StreamError { provider: provider.to_string().into(), }, AuthenticationError { provider, .. } => Self::AuthenticationFailed { provider: provider.to_string().into(), }, PermissionError { provider, message } => Self::PermissionDenied { provider: provider.to_string().into(), message: Some(message.clone().into()), }, UpstreamProviderError { .. } => Self::RequestFailed, DataRetentionConsentRequired { .. } => Self::DataRetentionConsentRequired, BadRequestFormat { provider, .. } | HttpResponseError { provider, .. } | ApiEndpointNotFound { provider } => Self::ApiError { provider: provider.to_string().into(), }, _ => { let message: SharedString = format!("{:#}", error).into(); Self::Other { message, acp_error_code: None, } } } } else { let message: SharedString = format!("{:#}", error).into(); // Extract ACP error code if available let acp_error_code = error .downcast_ref::<acp::Error>() .map(|acp_error| SharedString::from(acp_error.code.to_string())); Self::Other { message, acp_error_code, } } } } impl ProfileProvider for Entity<agent::Thread> { fn profile_id(&self, cx: &App) -> AgentProfileId { self.read(cx).profile().clone() } fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) { self.update(cx, |thread, cx| { // Apply the profile and let the thread swap to its default model. thread.set_profile(profile_id, cx); }); } fn profiles_supported(&self, cx: &App) -> bool { self.read(cx) .model() .is_some_and(|model| model.supports_tools()) } fn model_selected(&self, cx: &App) -> bool { self.read(cx).model().is_some() } fn is_restricted(&self, cx: &App) -> bool { project::trusted_worktrees::TrustedWorktrees::has_restricted_worktrees( &self.read(cx).project().read(cx).worktree_store(), cx, ) } fn profile_downgraded(&self, cx: &App) -> bool { self.read(cx).profile_was_downgraded() } } #[derive(Default)] pub(crate) struct Conversation { threads: HashMap<acp::SessionId, Entity<AcpThread>>, permission_requests: IndexMap<acp::SessionId, Vec<acp::ToolCallId>>, elicitation_requests: IndexMap<acp::SessionId, Vec<ElicitationEntryId>>, subscriptions: Vec<Subscription>, updated_at: Option<Instant>, } impl Conversation { pub fn register_thread(&mut self, thread: Entity<AcpThread>, cx: &mut Context<Self>) { let session_id = thread.read(cx).session_id().clone(); let subscription = cx.subscribe(&thread, { let session_id = session_id.clone(); move |this, _thread, event, _cx| { this.updated_at = Some(Instant::now()); match event { AcpThreadEvent::ToolAuthorizationRequested(id) => { this.permission_requests .entry(session_id.clone()) .or_default() .push(id.clone()); } AcpThreadEvent::ToolAuthorizationReceived(id) => { if let Some(tool_calls) = this.permission_requests.get_mut(&session_id) { tool_calls.retain(|tool_call_id| tool_call_id != id); if tool_calls.is_empty() { this.permission_requests.shift_remove(&session_id); } } } AcpThreadEvent::ElicitationRequested(id) => { this.elicitation_requests .entry(session_id.clone()) .or_default() .push(id.clone()); } AcpThreadEvent::ElicitationResponded(id) => { if let Some(elicitations) = this.elicitation_requests.get_mut(&session_id) { elicitations.retain(|elicitation_id| elicitation_id != id); if elicitations.is_empty() { this.elicitation_requests.shift_remove(&session_id); } } } AcpThreadEvent::NewEntry | AcpThreadEvent::StatusChanged | AcpThreadEvent::TitleUpdated | AcpThreadEvent::TokenUsageUpdated | AcpThreadEvent::EntryUpdated(_) | AcpThreadEvent::EntriesRemoved(_) | AcpThreadEvent::Retry(_) | AcpThreadEvent::SubagentSpawned(_) | AcpThreadEvent::Stopped(_) | AcpThreadEvent::Error | AcpThreadEvent::LoadError(_) | AcpThreadEvent::PromptCapabilitiesUpdated | AcpThreadEvent::Refusal | AcpThreadEvent::AvailableCommandsUpdated(_) | AcpThreadEvent::ModeUpdated(_) | AcpThreadEvent::ConfigOptionsUpdated(_) | AcpThreadEvent::WorkingDirectoriesUpdated | AcpThreadEvent::PromptUpdated => {} } } }); self.subscriptions.push(subscription); self.threads.insert(session_id, thread); } pub fn permission_options_for_tool_call<'a>( &'a self, session_id: &acp::SessionId, tool_call_id: acp::ToolCallId, cx: &'a App, ) -> Option<&'a PermissionOptions> { let thread = self.threads.get(session_id)?; let (_, tool_call) = thread.read(cx).tool_call(&tool_call_id)?; let ToolCallStatus::WaitingForConfirmation { options, .. } = &tool_call.status else { return None; }; Some(options) } pub fn pending_tool_call<'a>( &'a self, session_id: &acp::SessionId, cx: &'a App, ) -> Option<(acp::SessionId, acp::ToolCallId, &'a PermissionOptions)> { let thread = self.threads.get(session_id)?; let is_subagent = thread.read(cx).parent_session_id().is_some(); let (result_session_id, thread, tool_id) = if is_subagent { let id = self.permission_requests.get(session_id)?.iter().next()?; (session_id.clone(), thread, id) } else { let (id, tool_calls) = self.permission_requests.first()?; let thread = self.threads.get(id)?; let tool_id = tool_calls.iter().next()?; (id.clone(), thread, tool_id) }; let (_, tool_call) = thread.read(cx).tool_call(tool_id)?; let ToolCallStatus::WaitingForConfirmation { options, .. } = &tool_call.status else { return None; }; Some((result_session_id, tool_id.clone(), options)) } pub fn subagents_awaiting_permission(&self, cx: &App) -> Vec<(acp::SessionId, usize)> { self.permission_requests .iter() .filter_map(|(session_id, tool_call_ids)| { let thread = self.threads.get(session_id)?; if thread.read(cx).parent_session_id().is_some() && !tool_call_ids.is_empty() { Some((session_id.clone(), tool_call_ids.len())) } else { None } }) .collect() } /// Returns the first pending tool call request for exactly `session_id`. /// Unlike `pending_tool_call`, this does not use the global FIFO pending /// request for non-subagent sessions. pub fn pending_tool_call_for_session( &self, session_id: &acp::SessionId, cx: &App, ) -> Option<acp::ToolCallId> { let thread = self.threads.get(session_id)?; let tool_call_id = self.permission_requests.get(session_id)?.iter().next()?; let (_, tool_call) = thread.read(cx).tool_call(tool_call_id)?; if !matches!( tool_call.status, ToolCallStatus::WaitingForConfirmation { .. } ) { return None; } Some(tool_call_id.clone()) } pub fn pending_tool_call_count_for_session(&self, session_id: &acp::SessionId) -> usize { self.permission_requests .get(session_id) .map(|tool_call_ids| tool_call_ids.len()) .unwrap_or(0) } pub fn respond_to_elicitation( &mut self, session_id: acp::SessionId, elicitation_id: ElicitationEntryId, response: acp::CreateElicitationResponse, cx: &mut Context<Self>, ) -> Option<()> { let thread = self.threads.get(&session_id)?.clone(); thread.update(cx, |thread, cx| { thread.respond_to_elicitation(&elicitation_id, response, cx); }); Some(()) } pub fn authorize_pending_tool_call( &mut self, session_id: &acp::SessionId, kind: acp::PermissionOptionKind, cx: &mut Context<Self>, ) -> Option<()> { let (authorize_session_id, tool_call_id, options) = self.pending_tool_call(session_id, cx)?; let option = permission_option_for_action(options, kind)?; self.authorize_tool_call( authorize_session_id, tool_call_id, SelectedPermissionOutcome::new(option.option_id.clone(), option.kind), cx, ); Some(()) } pub fn authorize_with_granularity( &mut self, session_id: acp::SessionId, tool_call_id: acp::ToolCallId, selection: Option<&thread_view::PermissionSelection>, is_allow: bool, cx: &mut Context<Self>, ) -> Option<()> { let options = self.permission_options_for_tool_call(&session_id, tool_call_id.clone(), cx)?; let outcome = resolve_outcome_from_selection(options, selection, is_allow)?; self.authorize_tool_call(session_id, tool_call_id, outcome, cx); Some(()) } pub fn authorize_tool_call( &mut self, session_id: acp::SessionId, tool_call_id: acp::ToolCallId, outcome: SelectedPermissionOutcome, cx: &mut Context<Self>, ) { let Some(thread) = self.threads.get(&session_id) else { return; }; let agent_telemetry_id = thread.read(cx).connection().telemetry_id(); let session_id = thread.read(cx).session_id().clone(); telemetry::event!( "Agent Tool Call Authorized", agent = agent_telemetry_id, session = session_id, option = outcome.option_kind ); thread.update(cx, |thread, cx| { thread.authorize_tool_call(tool_call_id, outcome, cx); }); cx.notify(); } fn set_work_dirs(&mut self, work_dirs: PathList, cx: &mut Context<Self>) { for thread in self.threads.values() { thread.update(cx, |thread, cx| { thread.set_work_dirs(work_dirs.clone(), cx); }); } } } pub(crate) struct RootThreadUpdated; impl EventEmitter<RootThreadUpdated> for ConversationView {} fn permission_option_for_action(