use acp_thread::{ AcpThread, AcpThreadEvent, AgentSessionInfo, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, AuthRequired, LoadError, MentionUri, PermissionOptionChoice, PermissionOptions, RetryStatus, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, UserMessageId,};use acp_thread::{AgentConnection, Plan};use action_log::{ActionLog, ActionLogTelemetry, DiffStats};use agent::{NativeAgentServer, NativeAgentSessionList, SharedThread, ThreadStore};use agent_client_protocol::{self as acp, PromptCapabilities};use agent_servers::AgentServer;#[cfg(test)]use agent_servers::AgentServerDelegate;use agent_settings::{AgentProfileId, AgentSettings};use anyhow::{Result, anyhow};use arrayvec::ArrayVec;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 feature_flags::{AgentSharingFeatureFlag, AgentV2FeatureFlag, FeatureFlagAppExt as _};use file_icons::FileIcons;use fs::Fs;use futures::FutureExt as _;use gpui::{ Action, Animation, AnimationExt, AnyView, App, ClickEvent, ClipboardItem, CursorStyle, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable, Hsla, ListOffset, ListState, ObjectFit, PlatformDisplay, ScrollHandle, SharedString, Subscription, Task, TextStyle, WeakEntity, Window, WindowHandle, div, ease_in_out, img, linear_color_stop, linear_gradient, list, point, pulsating_between,};use language::Buffer;use language_model::LanguageModelRegistry;use markdown::{Markdown, MarkdownElement, MarkdownFont, MarkdownStyle};use project::{AgentId, AgentServerStore, Project, ProjectEntryId};use prompt_store::{PromptId, PromptStore};use rope::Point;use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};use std::cell::RefCell;use std::path::Path;use std::sync::Arc;use std::time::Instant;use std::{collections::BTreeMap, rc::Rc, time::Duration};use terminal_view::terminal_panel::TerminalPanel;use text::Anchor;use theme::AgentFontSize;use ui::{ Callout, CircularProgress, CommonAnimationExt, ContextMenu, ContextMenuEntry, CopyButton, DecoratedIcon, DiffStat, Disclosure, Divider, DividerColor, IconDecoration, IconDecorationKind, KeyBinding, PopoverMenu, PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar, prelude::*, right_click_menu,};use util::{ResultExt, size::format_file_size, time::duration_alt_display};use util::{debug_panic, defer};use workspace::PathList;use workspace::{ CollaboratorId, MultiWorkspace, NewTerminal, Toast, Workspace, notifications::NotificationId,};use zed_actions::agent::{Chat, ToggleModelSelector};use zed_actions::assistant::OpenRulesLibrary; use super::config_options::ConfigOptionsView;use super::entry_view_state::EntryViewState;use super::thread_history::ThreadHistory;use crate::ModeSelector;use crate::ModelSelectorPopover;use crate::agent_connection_store::{ AgentConnectedState, AgentConnectionEntryEvent, AgentConnectionStore,};use crate::agent_diff::AgentDiff;use crate::entry_view_state::{EntryViewEvent, ViewEvent};use crate::message_editor::{MessageEditor, MessageEditorEvent};use crate::profile_selector::{ProfileProvider, ProfileSelector};use crate::thread_metadata_store::ThreadMetadataStore;use crate::ui::{AgentNotification, AgentNotificationEvent};use crate::{ Agent, AgentDiffPane, AgentInitialContent, AgentPanel, AllowAlways, AllowOnce, AuthorizeToolCall, ClearMessageQueue, CycleFavoriteModels, CycleModeSelector, CycleThinkingEffort, EditFirstQueuedMessage, ExpandMessageEditor, Follow, KeepAll, NewThread, OpenAddContextMenu, OpenAgentDiff, OpenHistory, RejectAll, RejectOnce, RemoveFirstQueuedMessage, SendImmediately, SendNextQueuedMessage, ToggleFastMode, ToggleProfileSelector, ToggleThinkingEffortMenu, ToggleThinkingMode, UndoLastReject,}; const STOPWATCH_THRESHOLD: Duration = Duration::from_secs(30);const TOKEN_THRESHOLD: u64 = 250; mod thread_view;pub use thread_view::*; pub struct QueuedMessage { pub content: Vec<acp::ContentBlock>, pub tracked_buffers: Vec<Entity<Buffer>>,} #[derive(Copy, Clone, Debug, PartialEq, Eq)]enum ThreadFeedback { Positive, Negative,} #[derive(Debug)]pub(crate) enum ThreadError { PaymentRequired, Refusal, AuthenticationRequired(SharedString), Other { message: SharedString, acp_error_code: Option<SharedString>, },} impl From<anyhow::Error> for ThreadError { fn from(error: anyhow::Error) -> Self { if error.is::<language_model::PaymentRequiredError>() { Self::PaymentRequired } 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 { 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()) }} #[derive(Default)]pub(crate) struct Conversation { threads: HashMap<acp::SessionId, Entity<AcpThread>>, permission_requests: IndexMap<acp::SessionId, Vec<acp::ToolCallId>>, subscriptions: Vec<Subscription>, /// Tracks the selected granularity index for each tool call's permission dropdown. /// The index corresponds to the position in the allow_options list. selected_permission_granularity: HashMap<acp::SessionId, HashMap<acp::ToolCallId, usize>>,} 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, move |this, _thread, event, _cx| 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::NewEntry | 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(_) => {} }); self.subscriptions.push(subscription); self.threads .insert(thread.read(cx).session_id().clone(), thread); } pub fn selected_permission_granularity( &self, session_id: &acp::SessionId, tool_call_id: &acp::ToolCallId, ) -> Option<usize> { self.selected_permission_granularity .get(session_id) .and_then(|map| map.get(tool_call_id)) .copied() } pub fn set_selected_permission_granularity( &mut self, session_id: acp::SessionId, tool_call_id: acp::ToolCallId, granularity: usize, ) { self.selected_permission_granularity .entry(session_id) .or_default() .insert(tool_call_id, granularity); } 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 (thread, tool_id) = if is_subagent { let id = self.permission_requests.get(session_id)?.iter().next()?; (thread, id) } else { let (id, tool_calls) = self.permission_requests.first()?; let thread = self.threads.get(id)?; let id = tool_calls.iter().next()?; (thread, id) }; let (_, tool_call) = thread.read(cx).tool_call(tool_id)?; let ToolCallStatus::WaitingForConfirmation { options, .. } = &tool_call.status else { return None; }; Some(( thread.read(cx).session_id().clone(), tool_id.clone(), options, )) } pub fn authorize_pending_tool_call( &mut self, session_id: &acp::SessionId, kind: acp::PermissionOptionKind, cx: &mut Context<Self>, ) -> Option<()> { let (_, tool_call_id, options) = self.pending_tool_call(session_id, cx)?; let option = options.first_option_of_kind(kind)?; self.authorize_tool_call( session_id.clone(), tool_call_id, option.option_id.clone(), option.kind, cx, ); Some(()) } pub fn authorize_tool_call( &mut self, session_id: acp::SessionId, tool_call_id: acp::ToolCallId, option_id: acp::PermissionOptionId, option_kind: acp::PermissionOptionKind, cx: &mut Context<Self>, ) { let Some(thread) = self.threads.get(&session_id) else { return; }; let agent_telemetry_id = thread.read(cx).connection().telemetry_id(); telemetry::event!( "Agent Tool Call Authorized", agent = agent_telemetry_id, session = session_id, option = option_kind ); thread.update(cx, |thread, cx| { thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx); }); cx.notify(); }} pub enum AcpServerViewEvent { ActiveThreadChanged,} impl EventEmitter<AcpServerViewEvent> for ConversationView {} pub struct ConversationView { agent: Rc<dyn AgentServer>, connection_store: Entity<AgentConnectionStore>, connection_key: Agent, agent_server_store: Entity<AgentServerStore>, workspace: WeakEntity<Workspace>, project: Entity<Project>, thread_store: Option<Entity<ThreadStore>>, prompt_store: Option<Entity<PromptStore>>, server_state: ServerState, focus_handle: FocusHandle, notifications: Vec<WindowHandle<AgentNotification>>, notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>, auth_task: Option<Task<()>>, _subscriptions: Vec<Subscription>,} impl ConversationView { pub fn has_auth_methods(&self) -> bool { self.as_connected().map_or(false, |connected| { !connected.connection.auth_methods().is_empty() }) } pub fn active_thread(&self) -> Option<&Entity<ThreadView>> { match &self.server_state { ServerState::Connected(connected) => connected.active_view(), _ => None, } } pub fn pending_tool_call<'a>( &'a self, cx: &'a App, ) -> Option<(acp::SessionId, acp::ToolCallId, &'a PermissionOptions)> { let id = &self.active_thread()?.read(cx).id; self.as_connected()? .conversation .read(cx) .pending_tool_call(id, cx) } pub fn parent_thread(&self, cx: &App) -> Option<Entity<ThreadView>> { match &self.server_state { ServerState::Connected(connected) => { let mut current = connected.active_view()?; while let Some(parent_id) = current.read(cx).parent_id.clone() { if let Some(parent) = connected.threads.get(&parent_id) { current = parent; } else { break; } } Some(current.clone()) } _ => None, } } pub fn thread_view(&self, session_id: &acp::SessionId) -> Option<Entity<ThreadView>> { let connected = self.as_connected()?; connected.threads.get(session_id).cloned() } pub fn as_connected(&self) -> Option<&ConnectedServerState> { match &self.server_state { ServerState::Connected(connected) => Some(connected), _ => None, } } pub fn as_connected_mut(&mut self) -> Option<&mut ConnectedServerState> { match &mut self.server_state { ServerState::Connected(connected) => Some(connected), _ => None, } } pub fn navigate_to_session( &mut self, session_id: acp::SessionId, window: &mut Window, cx: &mut Context<Self>, ) { let Some(connected) = self.as_connected_mut() else { return; }; connected.navigate_to_session(session_id); if let Some(view) = self.active_thread() { view.focus_handle(cx).focus(window, cx); } cx.emit(AcpServerViewEvent::ActiveThreadChanged); cx.notify(); }} enum ServerState { Loading(Entity<LoadingView>), LoadError { error: LoadError, session_id: Option<acp::SessionId>, }, Connected(ConnectedServerState),} // current -> Entity// hashmap of threads, current becomes session_idpub struct ConnectedServerState { auth_state: AuthState, active_id: Option<acp::SessionId>, threads: HashMap<acp::SessionId, Entity<ThreadView>>, connection: Rc<dyn AgentConnection>, history: Entity<ThreadHistory>, conversation: Entity<Conversation>, _connection_entry_subscription: Subscription,} enum AuthState { Ok, Unauthenticated { description: Option<Entity<Markdown>>, configuration_view: Option<AnyView>, pending_auth_method: Option<acp::AuthMethodId>, _subscription: Option<Subscription>, },} impl AuthState { pub fn is_ok(&self) -> bool { matches!(self, Self::Ok) }} struct LoadingView { session_id: Option<acp::SessionId>, _load_task: Task<()>,} impl ConnectedServerState { pub fn active_view(&self) -> Option<&Entity<ThreadView>> { self.active_id.as_ref().and_then(|id| self.threads.get(id)) } pub fn has_thread_error(&self, cx: &App) -> bool { self.active_view() .map_or(false, |view| view.read(cx).thread_error.is_some()) } pub fn navigate_to_session(&mut self, session_id: acp::SessionId) { if self.threads.contains_key(&session_id) { self.active_id = Some(session_id); } } pub fn close_all_sessions(&self, cx: &mut App) -> Task<()> { let tasks = self.threads.keys().filter_map(|id| { if self.connection.supports_close_session() { Some(self.connection.clone().close_session(id, cx)) } else { None } }); let task = futures::future::join_all(tasks); cx.background_spawn(async move { task.await; }) }} impl ConversationView { pub fn new( agent: Rc<dyn AgentServer>, connection_store: Entity<AgentConnectionStore>, connection_key: Agent, resume_session_id: Option<acp::SessionId>, work_dirs: Option<PathList>, title: Option<SharedString>, initial_content: Option<AgentInitialContent>, workspace: WeakEntity<Workspace>, project: Entity<Project>, thread_store: Option<Entity<ThreadStore>>, prompt_store: Option<Entity<PromptStore>>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let agent_server_store = project.read(cx).agent_server_store().clone(); let subscriptions = vec![ cx.observe_global_in::<SettingsStore>(window, Self::agent_ui_font_size_changed), cx.observe_global_in::<AgentFontSize>(window, Self::agent_ui_font_size_changed),Lines 1–500 of 6719. Load more
use acp_thread::{ AcpThread, AcpThreadEvent, AgentSessionInfo, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, AuthRequired, LoadError, MentionUri, PermissionOptionChoice, PermissionOptions, RetryStatus, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, UserMessageId, }; use acp_thread::{AgentConnection, Plan}; use action_log::{ActionLog, ActionLogTelemetry, DiffStats}; use agent::{NativeAgentServer, NativeAgentSessionList, SharedThread, ThreadStore}; use agent_client_protocol::{self as acp, PromptCapabilities}; use agent_servers::AgentServer; #[cfg(test)] use agent_servers::AgentServerDelegate; use agent_settings::{AgentProfileId, AgentSettings}; use anyhow::{Result, anyhow}; use arrayvec::ArrayVec; 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 feature_flags::{AgentSharingFeatureFlag, AgentV2FeatureFlag, FeatureFlagAppExt as _}; use file_icons::FileIcons; use fs::Fs; use futures::FutureExt as _; use gpui::{ Action, Animation, AnimationExt, AnyView, App, ClickEvent, ClipboardItem, CursorStyle, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable, Hsla, ListOffset, ListState, ObjectFit, PlatformDisplay, ScrollHandle, SharedString, Subscription, Task, TextStyle, WeakEntity, Window, WindowHandle, div, ease_in_out, img, linear_color_stop, linear_gradient, list, point, pulsating_between, }; use language::Buffer; use language_model::LanguageModelRegistry; use markdown::{Markdown, MarkdownElement, MarkdownFont, MarkdownStyle}; use project::{AgentId, AgentServerStore, Project, ProjectEntryId}; use prompt_store::{PromptId, PromptStore}; use rope::Point; use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore}; use std::cell::RefCell; use std::path::Path; use std::sync::Arc; use std::time::Instant; use std::{collections::BTreeMap, rc::Rc, time::Duration}; use terminal_view::terminal_panel::TerminalPanel; use text::Anchor; use theme::AgentFontSize; use ui::{ Callout, CircularProgress, CommonAnimationExt, ContextMenu, ContextMenuEntry, CopyButton, DecoratedIcon, DiffStat, Disclosure, Divider, DividerColor, IconDecoration, IconDecorationKind, KeyBinding, PopoverMenu, PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar, prelude::*, right_click_menu, }; use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use util::{debug_panic, defer}; use workspace::PathList; use workspace::{ CollaboratorId, MultiWorkspace, NewTerminal, Toast, Workspace, notifications::NotificationId, }; use zed_actions::agent::{Chat, ToggleModelSelector}; use zed_actions::assistant::OpenRulesLibrary; use super::config_options::ConfigOptionsView; use super::entry_view_state::EntryViewState; use super::thread_history::ThreadHistory; use crate::ModeSelector; use crate::ModelSelectorPopover; use crate::agent_connection_store::{ AgentConnectedState, AgentConnectionEntryEvent, AgentConnectionStore, }; use crate::agent_diff::AgentDiff; use crate::entry_view_state::{EntryViewEvent, ViewEvent}; use crate::message_editor::{MessageEditor, MessageEditorEvent}; use crate::profile_selector::{ProfileProvider, ProfileSelector}; use crate::thread_metadata_store::ThreadMetadataStore; use crate::ui::{AgentNotification, AgentNotificationEvent}; use crate::{ Agent, AgentDiffPane, AgentInitialContent, AgentPanel, AllowAlways, AllowOnce, AuthorizeToolCall, ClearMessageQueue, CycleFavoriteModels, CycleModeSelector, CycleThinkingEffort, EditFirstQueuedMessage, ExpandMessageEditor, Follow, KeepAll, NewThread, OpenAddContextMenu, OpenAgentDiff, OpenHistory, RejectAll, RejectOnce, RemoveFirstQueuedMessage, SendImmediately, SendNextQueuedMessage, ToggleFastMode, ToggleProfileSelector, ToggleThinkingEffortMenu, ToggleThinkingMode, UndoLastReject, }; const STOPWATCH_THRESHOLD: Duration = Duration::from_secs(30); const TOKEN_THRESHOLD: u64 = 250; mod thread_view; pub use thread_view::*; pub struct QueuedMessage { pub content: Vec<acp::ContentBlock>, pub tracked_buffers: Vec<Entity<Buffer>>, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] enum ThreadFeedback { Positive, Negative, } #[derive(Debug)] pub(crate) enum ThreadError { PaymentRequired, Refusal, AuthenticationRequired(SharedString), Other { message: SharedString, acp_error_code: Option<SharedString>, }, } impl From<anyhow::Error> for ThreadError { fn from(error: anyhow::Error) -> Self { if error.is::<language_model::PaymentRequiredError>() { Self::PaymentRequired } 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 { 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()) } } #[derive(Default)] pub(crate) struct Conversation { threads: HashMap<acp::SessionId, Entity<AcpThread>>, permission_requests: IndexMap<acp::SessionId, Vec<acp::ToolCallId>>, subscriptions: Vec<Subscription>, /// Tracks the selected granularity index for each tool call's permission dropdown. /// The index corresponds to the position in the allow_options list. selected_permission_granularity: HashMap<acp::SessionId, HashMap<acp::ToolCallId, usize>>, } 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, move |this, _thread, event, _cx| 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::NewEntry | 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(_) => {} }); self.subscriptions.push(subscription); self.threads .insert(thread.read(cx).session_id().clone(), thread); } pub fn selected_permission_granularity( &self, session_id: &acp::SessionId, tool_call_id: &acp::ToolCallId, ) -> Option<usize> { self.selected_permission_granularity .get(session_id) .and_then(|map| map.get(tool_call_id)) .copied() } pub fn set_selected_permission_granularity( &mut self, session_id: acp::SessionId, tool_call_id: acp::ToolCallId, granularity: usize, ) { self.selected_permission_granularity .entry(session_id) .or_default() .insert(tool_call_id, granularity); } 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 (thread, tool_id) = if is_subagent { let id = self.permission_requests.get(session_id)?.iter().next()?; (thread, id) } else { let (id, tool_calls) = self.permission_requests.first()?; let thread = self.threads.get(id)?; let id = tool_calls.iter().next()?; (thread, id) }; let (_, tool_call) = thread.read(cx).tool_call(tool_id)?; let ToolCallStatus::WaitingForConfirmation { options, .. } = &tool_call.status else { return None; }; Some(( thread.read(cx).session_id().clone(), tool_id.clone(), options, )) } pub fn authorize_pending_tool_call( &mut self, session_id: &acp::SessionId, kind: acp::PermissionOptionKind, cx: &mut Context<Self>, ) -> Option<()> { let (_, tool_call_id, options) = self.pending_tool_call(session_id, cx)?; let option = options.first_option_of_kind(kind)?; self.authorize_tool_call( session_id.clone(), tool_call_id, option.option_id.clone(), option.kind, cx, ); Some(()) } pub fn authorize_tool_call( &mut self, session_id: acp::SessionId, tool_call_id: acp::ToolCallId, option_id: acp::PermissionOptionId, option_kind: acp::PermissionOptionKind, cx: &mut Context<Self>, ) { let Some(thread) = self.threads.get(&session_id) else { return; }; let agent_telemetry_id = thread.read(cx).connection().telemetry_id(); telemetry::event!( "Agent Tool Call Authorized", agent = agent_telemetry_id, session = session_id, option = option_kind ); thread.update(cx, |thread, cx| { thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx); }); cx.notify(); } } pub enum AcpServerViewEvent { ActiveThreadChanged, } impl EventEmitter<AcpServerViewEvent> for ConversationView {} pub struct ConversationView { agent: Rc<dyn AgentServer>, connection_store: Entity<AgentConnectionStore>, connection_key: Agent, agent_server_store: Entity<AgentServerStore>, workspace: WeakEntity<Workspace>, project: Entity<Project>, thread_store: Option<Entity<ThreadStore>>, prompt_store: Option<Entity<PromptStore>>, server_state: ServerState, focus_handle: FocusHandle, notifications: Vec<WindowHandle<AgentNotification>>, notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>, auth_task: Option<Task<()>>, _subscriptions: Vec<Subscription>, } impl ConversationView { pub fn has_auth_methods(&self) -> bool { self.as_connected().map_or(false, |connected| { !connected.connection.auth_methods().is_empty() }) } pub fn active_thread(&self) -> Option<&Entity<ThreadView>> { match &self.server_state { ServerState::Connected(connected) => connected.active_view(), _ => None, } } pub fn pending_tool_call<'a>( &'a self, cx: &'a App, ) -> Option<(acp::SessionId, acp::ToolCallId, &'a PermissionOptions)> { let id = &self.active_thread()?.read(cx).id; self.as_connected()? .conversation .read(cx) .pending_tool_call(id, cx) } pub fn parent_thread(&self, cx: &App) -> Option<Entity<ThreadView>> { match &self.server_state { ServerState::Connected(connected) => { let mut current = connected.active_view()?; while let Some(parent_id) = current.read(cx).parent_id.clone() { if let Some(parent) = connected.threads.get(&parent_id) { current = parent; } else { break; } } Some(current.clone()) } _ => None, } } pub fn thread_view(&self, session_id: &acp::SessionId) -> Option<Entity<ThreadView>> { let connected = self.as_connected()?; connected.threads.get(session_id).cloned() } pub fn as_connected(&self) -> Option<&ConnectedServerState> { match &self.server_state { ServerState::Connected(connected) => Some(connected), _ => None, } } pub fn as_connected_mut(&mut self) -> Option<&mut ConnectedServerState> { match &mut self.server_state { ServerState::Connected(connected) => Some(connected), _ => None, } } pub fn navigate_to_session( &mut self, session_id: acp::SessionId, window: &mut Window, cx: &mut Context<Self>, ) { let Some(connected) = self.as_connected_mut() else { return; }; connected.navigate_to_session(session_id); if let Some(view) = self.active_thread() { view.focus_handle(cx).focus(window, cx); } cx.emit(AcpServerViewEvent::ActiveThreadChanged); cx.notify(); } } enum ServerState { Loading(Entity<LoadingView>), LoadError { error: LoadError, session_id: Option<acp::SessionId>, }, Connected(ConnectedServerState), } // current -> Entity // hashmap of threads, current becomes session_id pub struct ConnectedServerState { auth_state: AuthState, active_id: Option<acp::SessionId>, threads: HashMap<acp::SessionId, Entity<ThreadView>>, connection: Rc<dyn AgentConnection>, history: Entity<ThreadHistory>, conversation: Entity<Conversation>, _connection_entry_subscription: Subscription, } enum AuthState { Ok, Unauthenticated { description: Option<Entity<Markdown>>, configuration_view: Option<AnyView>, pending_auth_method: Option<acp::AuthMethodId>, _subscription: Option<Subscription>, }, } impl AuthState { pub fn is_ok(&self) -> bool { matches!(self, Self::Ok) } } struct LoadingView { session_id: Option<acp::SessionId>, _load_task: Task<()>, } impl ConnectedServerState { pub fn active_view(&self) -> Option<&Entity<ThreadView>> { self.active_id.as_ref().and_then(|id| self.threads.get(id)) } pub fn has_thread_error(&self, cx: &App) -> bool { self.active_view() .map_or(false, |view| view.read(cx).thread_error.is_some()) } pub fn navigate_to_session(&mut self, session_id: acp::SessionId) { if self.threads.contains_key(&session_id) { self.active_id = Some(session_id); } } pub fn close_all_sessions(&self, cx: &mut App) -> Task<()> { let tasks = self.threads.keys().filter_map(|id| { if self.connection.supports_close_session() { Some(self.connection.clone().close_session(id, cx)) } else { None } }); let task = futures::future::join_all(tasks); cx.background_spawn(async move { task.await; }) } } impl ConversationView { pub fn new( agent: Rc<dyn AgentServer>, connection_store: Entity<AgentConnectionStore>, connection_key: Agent, resume_session_id: Option<acp::SessionId>, work_dirs: Option<PathList>, title: Option<SharedString>, initial_content: Option<AgentInitialContent>, workspace: WeakEntity<Workspace>, project: Entity<Project>, thread_store: Option<Entity<ThreadStore>>, prompt_store: Option<Entity<PromptStore>>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let agent_server_store = project.read(cx).agent_server_store().clone(); let subscriptions = vec![ cx.observe_global_in::<SettingsStore>(window, Self::agent_ui_font_size_changed), cx.observe_global_in::<AgentFontSize>(window, Self::agent_ui_font_size_changed),