mod add_context_server_modal;mod configure_context_server_modal;mod manage_profiles_modal;mod tool_picker; use std::{sync::Arc, time::Duration}; use agent_settings::AgentSettings;use assistant_tool::{ToolSource, ToolWorkingSet};use collections::HashMap;use context_server::ContextServerId;use fs::Fs;use gpui::{ Action, Animation, AnimationExt as _, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle, Subscription, pulsating_between,};use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};use project::context_server_store::{ContextServerStatus, ContextServerStore};use settings::{Settings, update_settings_file};use ui::{ Disclosure, ElevationIndex, Indicator, Scrollbar, ScrollbarState, Switch, SwitchColor, Tooltip, prelude::*,};use util::ResultExt as _;use zed_actions::ExtensionCategoryFilter; pub(crate) use add_context_server_modal::AddContextServerModal;pub(crate) use configure_context_server_modal::ConfigureContextServerModal;pub(crate) use manage_profiles_modal::ManageProfilesModal; use crate::AddContextServer; pub struct AgentConfiguration { fs: Arc<dyn Fs>, focus_handle: FocusHandle, configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>, context_server_store: Entity<ContextServerStore>, expanded_context_server_tools: HashMap<ContextServerId, bool>, expanded_provider_configurations: HashMap<LanguageModelProviderId, bool>, tools: Entity<ToolWorkingSet>, _registry_subscription: Subscription, scroll_handle: ScrollHandle, scrollbar_state: ScrollbarState,} impl AgentConfiguration { pub fn new( fs: Arc<dyn Fs>, context_server_store: Entity<ContextServerStore>, tools: Entity<ToolWorkingSet>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let focus_handle = cx.focus_handle(); let registry_subscription = cx.subscribe_in( &LanguageModelRegistry::global(cx), window, |this, _, event: &language_model::Event, window, cx| match event { language_model::Event::AddedProvider(provider_id) => { let provider = LanguageModelRegistry::read_global(cx).provider(provider_id); if let Some(provider) = provider { this.add_provider_configuration_view(&provider, window, cx); } } language_model::Event::RemovedProvider(provider_id) => { this.remove_provider_configuration_view(provider_id); } _ => {} }, ); let scroll_handle = ScrollHandle::new(); let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); let mut this = Self { fs, focus_handle, configuration_views_by_provider: HashMap::default(), context_server_store, expanded_context_server_tools: HashMap::default(), expanded_provider_configurations: HashMap::default(), tools, _registry_subscription: registry_subscription, scroll_handle, scrollbar_state, }; this.build_provider_configuration_views(window, cx); this } fn build_provider_configuration_views(&mut self, window: &mut Window, cx: &mut Context<Self>) { let providers = LanguageModelRegistry::read_global(cx).providers(); for provider in providers { self.add_provider_configuration_view(&provider, window, cx); } } fn remove_provider_configuration_view(&mut self, provider_id: &LanguageModelProviderId) { self.configuration_views_by_provider.remove(provider_id); self.expanded_provider_configurations.remove(provider_id); } fn add_provider_configuration_view( &mut self, provider: &Arc<dyn LanguageModelProvider>, window: &mut Window, cx: &mut Context<Self>, ) { let configuration_view = provider.configuration_view(window, cx); self.configuration_views_by_provider .insert(provider.id(), configuration_view); }} impl Focusable for AgentConfiguration { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() }} pub enum AssistantConfigurationEvent { NewThread(Arc<dyn LanguageModelProvider>),} impl EventEmitter<AssistantConfigurationEvent> for AgentConfiguration {} impl AgentConfiguration { fn render_provider_configuration_block( &mut self, provider: &Arc<dyn LanguageModelProvider>, cx: &mut Context<Self>, ) -> impl IntoElement + use<> { let provider_id = provider.id().0.clone(); let provider_name = provider.name().0.clone(); let configuration_view = self .configuration_views_by_provider .get(&provider.id()) .cloned(); let is_expanded = self .expanded_provider_configurations .get(&provider.id()) .copied() .unwrap_or(false); v_flex() .pt_3() .gap_1p5() .border_t_1() .border_color(cx.theme().colors().border.opacity(0.6)) .child( h_flex() .justify_between() .child( h_flex() .gap_2() .child( Icon::new(provider.icon()) .size(IconSize::Small) .color(Color::Muted), ) .child(Label::new(provider_name.clone()).size(LabelSize::Large)) .when(provider.is_authenticated(cx) && !is_expanded, |parent| { parent.child(Icon::new(IconName::Check).color(Color::Success)) }), ) .child( h_flex() .gap_1() .when(provider.is_authenticated(cx), |parent| { parent.child( Button::new( SharedString::from(format!("new-thread-{provider_id}")), "Start New Thread", ) .icon_position(IconPosition::Start) .icon(IconName::Plus) .icon_size(IconSize::Small) .layer(ElevationIndex::ModalSurface) .label_size(LabelSize::Small) .on_click(cx.listener({ let provider = provider.clone(); move |_this, _event, _window, cx| { cx.emit(AssistantConfigurationEvent::NewThread( provider.clone(), )) } })), ) }) .child( Disclosure::new( SharedString::from(format!( "provider-disclosure-{provider_id}" )), is_expanded, ) .opened_icon(IconName::ChevronUp) .closed_icon(IconName::ChevronDown) .on_click(cx.listener({ let provider_id = provider.id().clone(); move |this, _event, _window, _cx| { let is_expanded = this .expanded_provider_configurations .entry(provider_id.clone()) .or_insert(false); *is_expanded = !*is_expanded; } })), ), ), ) .when(is_expanded, |parent| match configuration_view { Some(configuration_view) => parent.child(configuration_view), None => parent.child(Label::new(format!( "No configuration view for {provider_name}", ))), }) } fn render_provider_configuration_section( &mut self, cx: &mut Context<Self>, ) -> impl IntoElement { let providers = LanguageModelRegistry::read_global(cx).providers(); v_flex() .p(DynamicSpacing::Base16.rems(cx)) .pr(DynamicSpacing::Base20.rems(cx)) .gap_4() .border_b_1() .border_color(cx.theme().colors().border) .child( v_flex() .gap_0p5() .child(Headline::new("LLM Providers")) .child( Label::new("Add at least one provider to use AI-powered features.") .color(Color::Muted), ), ) .children( providers .into_iter() .map(|provider| self.render_provider_configuration_block(&provider, cx)), ) } fn render_command_permission(&mut self, cx: &mut Context<Self>) -> impl IntoElement { let always_allow_tool_actions = AgentSettings::get_global(cx).always_allow_tool_actions; h_flex() .gap_4() .justify_between() .flex_wrap() .child( v_flex() .gap_0p5() .max_w_5_6() .child(Label::new("Allow running editing tools without asking for confirmation")) .child( Label::new( "The agent can perform potentially destructive actions without asking for your confirmation.", ) .color(Color::Muted), ), ) .child( Switch::new( "always-allow-tool-actions-switch", always_allow_tool_actions.into(), ) .color(SwitchColor::Accent) .on_click({ let fs = self.fs.clone(); move |state, _window, cx| { let allow = state == &ToggleState::Selected; update_settings_file::<AgentSettings>( fs.clone(), cx, move |settings, _| { settings.set_always_allow_tool_actions(allow); }, ); } }), ) } fn render_single_file_review(&mut self, cx: &mut Context<Self>) -> impl IntoElement { let single_file_review = AgentSettings::get_global(cx).single_file_review; h_flex() .gap_4() .justify_between() .flex_wrap() .child( v_flex() .gap_0p5() .max_w_5_6() .child(Label::new("Enable single-file agent reviews")) .child( Label::new( "Agent edits are also displayed in single-file editors for review.", ) .color(Color::Muted), ), ) .child( Switch::new("single-file-review-switch", single_file_review.into()) .color(SwitchColor::Accent) .on_click({ let fs = self.fs.clone(); move |state, _window, cx| { let allow = state == &ToggleState::Selected; update_settings_file::<AgentSettings>( fs.clone(), cx, move |settings, _| { settings.set_single_file_review(allow); }, ); } }), ) } fn render_sound_notification(&mut self, cx: &mut Context<Self>) -> impl IntoElement { let play_sound_when_agent_done = AgentSettings::get_global(cx).play_sound_when_agent_done; h_flex() .gap_4() .justify_between() .flex_wrap() .child( v_flex() .gap_0p5() .max_w_5_6() .child(Label::new("Play sound when finished generating")) .child( Label::new( "Hear a notification sound when the agent is done generating changes or needs your input.", ) .color(Color::Muted), ), ) .child( Switch::new("play-sound-notification-switch", play_sound_when_agent_done.into()) .color(SwitchColor::Accent) .on_click({ let fs = self.fs.clone(); move |state, _window, cx| { let allow = state == &ToggleState::Selected; update_settings_file::<AgentSettings>( fs.clone(), cx, move |settings, _| { settings.set_play_sound_when_agent_done(allow); }, ); } }), ) } fn render_general_settings_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement { v_flex() .p(DynamicSpacing::Base16.rems(cx)) .pr(DynamicSpacing::Base20.rems(cx)) .gap_2p5() .border_b_1() .border_color(cx.theme().colors().border) .child(Headline::new("General Settings")) .child(self.render_command_permission(cx)) .child(self.render_single_file_review(cx)) .child(self.render_sound_notification(cx)) } fn render_context_servers_section( &mut self, window: &mut Window, cx: &mut Context<Self>, ) -> impl IntoElement { let context_server_ids = self.context_server_store.read(cx).all_server_ids().clone(); v_flex() .p(DynamicSpacing::Base16.rems(cx)) .pr(DynamicSpacing::Base20.rems(cx)) .gap_2() .border_b_1() .border_color(cx.theme().colors().border) .child( v_flex() .gap_0p5() .child(Headline::new("Model Context Protocol (MCP) Servers")) .child(Label::new("Connect to context servers via the Model Context Protocol either via Zed extensions or directly.").color(Color::Muted)), ) .children( context_server_ids.into_iter().map(|context_server_id| { self.render_context_server(context_server_id, window, cx) }), ) .child( h_flex() .justify_between() .gap_2() .child( h_flex().w_full().child( Button::new("add-context-server", "Add Custom Server") .style(ButtonStyle::Filled) .layer(ElevationIndex::ModalSurface) .full_width() .icon(IconName::Plus) .icon_size(IconSize::Small) .icon_position(IconPosition::Start) .on_click(|_event, window, cx| { window.dispatch_action(AddContextServer.boxed_clone(), cx) }), ), ) .child( h_flex().w_full().child( Button::new( "install-context-server-extensions", "Install MCP Extensions", ) .style(ButtonStyle::Filled) .layer(ElevationIndex::ModalSurface) .full_width() .icon(IconName::Hammer) .icon_size(IconSize::Small) .icon_position(IconPosition::Start) .on_click(|_event, window, cx| { window.dispatch_action( zed_actions::Extensions { category_filter: Some( ExtensionCategoryFilter::ContextServers, ), } .boxed_clone(), cx, ) }), ), ), ) } fn render_context_server( &self, context_server_id: ContextServerId, window: &mut Window, cx: &mut Context<Self>, ) -> impl use<> + IntoElement { let tools_by_source = self.tools.read(cx).tools_by_source(cx); let server_status = self .context_server_store .read(cx) .status_for_server(&context_server_id) .unwrap_or(ContextServerStatus::Stopped); let is_running = matches!(server_status, ContextServerStatus::Running); let item_id = SharedString::from(context_server_id.0.clone()); let error = if let ContextServerStatus::Error(error) = server_status.clone() { Some(error) } else { None }; let are_tools_expanded = self .expanded_context_server_tools .get(&context_server_id) .copied() .unwrap_or_default(); let tools = tools_by_source .get(&ToolSource::ContextServer { id: context_server_id.0.clone().into(), }) .map_or([].as_slice(), |tools| tools.as_slice()); let tool_count = tools.len(); let border_color = cx.theme().colors().border.opacity(0.6); let success_color = Color::Success.color(cx); let (status_indicator, tooltip_text) = match server_status { ContextServerStatus::Starting => ( Indicator::dot() .color(Color::Success) .with_animation( SharedString::from(format!("{}-starting", context_server_id.0.clone(),)), Animation::new(Duration::from_secs(2)) .repeat() .with_easing(pulsating_between(0.4, 1.)), move |this, delta| this.color(success_color.alpha(delta).into()), ) .into_any_element(),Lines 1–500 of 703. Load more
mod add_context_server_modal; mod configure_context_server_modal; mod manage_profiles_modal; mod tool_picker; use std::{sync::Arc, time::Duration}; use agent_settings::AgentSettings; use assistant_tool::{ToolSource, ToolWorkingSet}; use collections::HashMap; use context_server::ContextServerId; use fs::Fs; use gpui::{ Action, Animation, AnimationExt as _, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle, Subscription, pulsating_between, }; use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry}; use project::context_server_store::{ContextServerStatus, ContextServerStore}; use settings::{Settings, update_settings_file}; use ui::{ Disclosure, ElevationIndex, Indicator, Scrollbar, ScrollbarState, Switch, SwitchColor, Tooltip, prelude::*, }; use util::ResultExt as _; use zed_actions::ExtensionCategoryFilter; pub(crate) use add_context_server_modal::AddContextServerModal; pub(crate) use configure_context_server_modal::ConfigureContextServerModal; pub(crate) use manage_profiles_modal::ManageProfilesModal; use crate::AddContextServer; pub struct AgentConfiguration { fs: Arc<dyn Fs>, focus_handle: FocusHandle, configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>, context_server_store: Entity<ContextServerStore>, expanded_context_server_tools: HashMap<ContextServerId, bool>, expanded_provider_configurations: HashMap<LanguageModelProviderId, bool>, tools: Entity<ToolWorkingSet>, _registry_subscription: Subscription, scroll_handle: ScrollHandle, scrollbar_state: ScrollbarState, } impl AgentConfiguration { pub fn new( fs: Arc<dyn Fs>, context_server_store: Entity<ContextServerStore>, tools: Entity<ToolWorkingSet>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let focus_handle = cx.focus_handle(); let registry_subscription = cx.subscribe_in( &LanguageModelRegistry::global(cx), window, |this, _, event: &language_model::Event, window, cx| match event { language_model::Event::AddedProvider(provider_id) => { let provider = LanguageModelRegistry::read_global(cx).provider(provider_id); if let Some(provider) = provider { this.add_provider_configuration_view(&provider, window, cx); } } language_model::Event::RemovedProvider(provider_id) => { this.remove_provider_configuration_view(provider_id); } _ => {} }, ); let scroll_handle = ScrollHandle::new(); let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); let mut this = Self { fs, focus_handle, configuration_views_by_provider: HashMap::default(), context_server_store, expanded_context_server_tools: HashMap::default(), expanded_provider_configurations: HashMap::default(), tools, _registry_subscription: registry_subscription, scroll_handle, scrollbar_state, }; this.build_provider_configuration_views(window, cx); this } fn build_provider_configuration_views(&mut self, window: &mut Window, cx: &mut Context<Self>) { let providers = LanguageModelRegistry::read_global(cx).providers(); for provider in providers { self.add_provider_configuration_view(&provider, window, cx); } } fn remove_provider_configuration_view(&mut self, provider_id: &LanguageModelProviderId) { self.configuration_views_by_provider.remove(provider_id); self.expanded_provider_configurations.remove(provider_id); } fn add_provider_configuration_view( &mut self, provider: &Arc<dyn LanguageModelProvider>, window: &mut Window, cx: &mut Context<Self>, ) { let configuration_view = provider.configuration_view(window, cx); self.configuration_views_by_provider .insert(provider.id(), configuration_view); } } impl Focusable for AgentConfiguration { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } pub enum AssistantConfigurationEvent { NewThread(Arc<dyn LanguageModelProvider>), } impl EventEmitter<AssistantConfigurationEvent> for AgentConfiguration {} impl AgentConfiguration { fn render_provider_configuration_block( &mut self, provider: &Arc<dyn LanguageModelProvider>, cx: &mut Context<Self>, ) -> impl IntoElement + use<> { let provider_id = provider.id().0.clone(); let provider_name = provider.name().0.clone(); let configuration_view = self .configuration_views_by_provider .get(&provider.id()) .cloned(); let is_expanded = self .expanded_provider_configurations .get(&provider.id()) .copied() .unwrap_or(false); v_flex() .pt_3() .gap_1p5() .border_t_1() .border_color(cx.theme().colors().border.opacity(0.6)) .child( h_flex() .justify_between() .child( h_flex() .gap_2() .child( Icon::new(provider.icon()) .size(IconSize::Small) .color(Color::Muted), ) .child(Label::new(provider_name.clone()).size(LabelSize::Large)) .when(provider.is_authenticated(cx) && !is_expanded, |parent| { parent.child(Icon::new(IconName::Check).color(Color::Success)) }), ) .child( h_flex() .gap_1() .when(provider.is_authenticated(cx), |parent| { parent.child( Button::new( SharedString::from(format!("new-thread-{provider_id}")), "Start New Thread", ) .icon_position(IconPosition::Start) .icon(IconName::Plus) .icon_size(IconSize::Small) .layer(ElevationIndex::ModalSurface) .label_size(LabelSize::Small) .on_click(cx.listener({ let provider = provider.clone(); move |_this, _event, _window, cx| { cx.emit(AssistantConfigurationEvent::NewThread( provider.clone(), )) } })), ) }) .child( Disclosure::new( SharedString::from(format!( "provider-disclosure-{provider_id}" )), is_expanded, ) .opened_icon(IconName::ChevronUp) .closed_icon(IconName::ChevronDown) .on_click(cx.listener({ let provider_id = provider.id().clone(); move |this, _event, _window, _cx| { let is_expanded = this .expanded_provider_configurations .entry(provider_id.clone()) .or_insert(false); *is_expanded = !*is_expanded; } })), ), ), ) .when(is_expanded, |parent| match configuration_view { Some(configuration_view) => parent.child(configuration_view), None => parent.child(Label::new(format!( "No configuration view for {provider_name}", ))), }) } fn render_provider_configuration_section( &mut self, cx: &mut Context<Self>, ) -> impl IntoElement { let providers = LanguageModelRegistry::read_global(cx).providers(); v_flex() .p(DynamicSpacing::Base16.rems(cx)) .pr(DynamicSpacing::Base20.rems(cx)) .gap_4() .border_b_1() .border_color(cx.theme().colors().border) .child( v_flex() .gap_0p5() .child(Headline::new("LLM Providers")) .child( Label::new("Add at least one provider to use AI-powered features.") .color(Color::Muted), ), ) .children( providers .into_iter() .map(|provider| self.render_provider_configuration_block(&provider, cx)), ) } fn render_command_permission(&mut self, cx: &mut Context<Self>) -> impl IntoElement { let always_allow_tool_actions = AgentSettings::get_global(cx).always_allow_tool_actions; h_flex() .gap_4() .justify_between() .flex_wrap() .child( v_flex() .gap_0p5() .max_w_5_6() .child(Label::new("Allow running editing tools without asking for confirmation")) .child( Label::new( "The agent can perform potentially destructive actions without asking for your confirmation.", ) .color(Color::Muted), ), ) .child( Switch::new( "always-allow-tool-actions-switch", always_allow_tool_actions.into(), ) .color(SwitchColor::Accent) .on_click({ let fs = self.fs.clone(); move |state, _window, cx| { let allow = state == &ToggleState::Selected; update_settings_file::<AgentSettings>( fs.clone(), cx, move |settings, _| { settings.set_always_allow_tool_actions(allow); }, ); } }), ) } fn render_single_file_review(&mut self, cx: &mut Context<Self>) -> impl IntoElement { let single_file_review = AgentSettings::get_global(cx).single_file_review; h_flex() .gap_4() .justify_between() .flex_wrap() .child( v_flex() .gap_0p5() .max_w_5_6() .child(Label::new("Enable single-file agent reviews")) .child( Label::new( "Agent edits are also displayed in single-file editors for review.", ) .color(Color::Muted), ), ) .child( Switch::new("single-file-review-switch", single_file_review.into()) .color(SwitchColor::Accent) .on_click({ let fs = self.fs.clone(); move |state, _window, cx| { let allow = state == &ToggleState::Selected; update_settings_file::<AgentSettings>( fs.clone(), cx, move |settings, _| { settings.set_single_file_review(allow); }, ); } }), ) } fn render_sound_notification(&mut self, cx: &mut Context<Self>) -> impl IntoElement { let play_sound_when_agent_done = AgentSettings::get_global(cx).play_sound_when_agent_done; h_flex() .gap_4() .justify_between() .flex_wrap() .child( v_flex() .gap_0p5() .max_w_5_6() .child(Label::new("Play sound when finished generating")) .child( Label::new( "Hear a notification sound when the agent is done generating changes or needs your input.", ) .color(Color::Muted), ), ) .child( Switch::new("play-sound-notification-switch", play_sound_when_agent_done.into()) .color(SwitchColor::Accent) .on_click({ let fs = self.fs.clone(); move |state, _window, cx| { let allow = state == &ToggleState::Selected; update_settings_file::<AgentSettings>( fs.clone(), cx, move |settings, _| { settings.set_play_sound_when_agent_done(allow); }, ); } }), ) } fn render_general_settings_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement { v_flex() .p(DynamicSpacing::Base16.rems(cx)) .pr(DynamicSpacing::Base20.rems(cx)) .gap_2p5() .border_b_1() .border_color(cx.theme().colors().border) .child(Headline::new("General Settings")) .child(self.render_command_permission(cx)) .child(self.render_single_file_review(cx)) .child(self.render_sound_notification(cx)) } fn render_context_servers_section( &mut self, window: &mut Window, cx: &mut Context<Self>, ) -> impl IntoElement { let context_server_ids = self.context_server_store.read(cx).all_server_ids().clone(); v_flex() .p(DynamicSpacing::Base16.rems(cx)) .pr(DynamicSpacing::Base20.rems(cx)) .gap_2() .border_b_1() .border_color(cx.theme().colors().border) .child( v_flex() .gap_0p5() .child(Headline::new("Model Context Protocol (MCP) Servers")) .child(Label::new("Connect to context servers via the Model Context Protocol either via Zed extensions or directly.").color(Color::Muted)), ) .children( context_server_ids.into_iter().map(|context_server_id| { self.render_context_server(context_server_id, window, cx) }), ) .child( h_flex() .justify_between() .gap_2() .child( h_flex().w_full().child( Button::new("add-context-server", "Add Custom Server") .style(ButtonStyle::Filled) .layer(ElevationIndex::ModalSurface) .full_width() .icon(IconName::Plus) .icon_size(IconSize::Small) .icon_position(IconPosition::Start) .on_click(|_event, window, cx| { window.dispatch_action(AddContextServer.boxed_clone(), cx) }), ), ) .child( h_flex().w_full().child( Button::new( "install-context-server-extensions", "Install MCP Extensions", ) .style(ButtonStyle::Filled) .layer(ElevationIndex::ModalSurface) .full_width() .icon(IconName::Hammer) .icon_size(IconSize::Small) .icon_position(IconPosition::Start) .on_click(|_event, window, cx| { window.dispatch_action( zed_actions::Extensions { category_filter: Some( ExtensionCategoryFilter::ContextServers, ), } .boxed_clone(), cx, ) }), ), ), ) } fn render_context_server( &self, context_server_id: ContextServerId, window: &mut Window, cx: &mut Context<Self>, ) -> impl use<> + IntoElement { let tools_by_source = self.tools.read(cx).tools_by_source(cx); let server_status = self .context_server_store .read(cx) .status_for_server(&context_server_id) .unwrap_or(ContextServerStatus::Stopped); let is_running = matches!(server_status, ContextServerStatus::Running); let item_id = SharedString::from(context_server_id.0.clone()); let error = if let ContextServerStatus::Error(error) = server_status.clone() { Some(error) } else { None }; let are_tools_expanded = self .expanded_context_server_tools .get(&context_server_id) .copied() .unwrap_or_default(); let tools = tools_by_source .get(&ToolSource::ContextServer { id: context_server_id.0.clone().into(), }) .map_or([].as_slice(), |tools| tools.as_slice()); let tool_count = tools.len(); let border_color = cx.theme().colors().border.opacity(0.6); let success_color = Color::Success.color(cx); let (status_indicator, tooltip_text) = match server_status { ContextServerStatus::Starting => ( Indicator::dot() .color(Color::Success) .with_animation( SharedString::from(format!("{}-starting", context_server_id.0.clone(),)), Animation::new(Duration::from_secs(2)) .repeat() .with_easing(pulsating_between(0.4, 1.)), move |this, delta| this.color(success_color.alpha(delta).into()), ) .into_any_element(),