464 lines · 20.4 KB · Rust
RawDownload
mod add_context_server_modal;mod manage_profiles_modal;mod tool_picker; use std::sync::Arc; use assistant_settings::AssistantSettings;use assistant_tool::{ToolSource, ToolWorkingSet};use collections::HashMap;use context_server::manager::ContextServerManager;use fs::Fs;use gpui::{Action, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription};use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};use settings::{Settings, update_settings_file};use ui::{    Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Switch, Tooltip, prelude::*,};use util::ResultExt as _;use zed_actions::ExtensionCategoryFilter; pub(crate) use add_context_server_modal::AddContextServerModal;pub(crate) use manage_profiles_modal::ManageProfilesModal; use crate::AddContextServer; pub struct AssistantConfiguration {    fs: Arc<dyn Fs>,    focus_handle: FocusHandle,    configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,    context_server_manager: Entity<ContextServerManager>,    expanded_context_server_tools: HashMap<Arc<str>, bool>,    tools: Entity<ToolWorkingSet>,    _registry_subscription: Subscription,} impl AssistantConfiguration {    pub fn new(        fs: Arc<dyn Fs>,        context_server_manager: Entity<ContextServerManager>,        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 mut this = Self {            fs,            focus_handle,            configuration_views_by_provider: HashMap::default(),            context_server_manager,            expanded_context_server_tools: HashMap::default(),            tools,            _registry_subscription: registry_subscription,        };        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);    }     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 AssistantConfiguration {    fn focus_handle(&self, _: &App) -> FocusHandle {        self.focus_handle.clone()    }} pub enum AssistantConfigurationEvent {    NewThread(Arc<dyn LanguageModelProvider>),} impl EventEmitter<AssistantConfigurationEvent> for AssistantConfiguration {} impl AssistantConfiguration {    fn render_provider_configuration(        &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();         v_flex()            .gap_1p5()            .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())),                    )                    .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)                            .style(ButtonStyle::Filled)                            .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(                div()                    .p(DynamicSpacing::Base08.rems(cx))                    .bg(cx.theme().colors().editor_background)                    .border_1()                    .border_color(cx.theme().colors().border_variant)                    .rounded_sm()                    .map(|parent| match configuration_view {                        Some(configuration_view) => parent.child(configuration_view),                        None => parent.child(div().child(Label::new(format!(                            "No configuration view for {provider_name}",                        )))),                    }),            )    }     fn render_command_permission(&mut self, cx: &mut Context<Self>) -> impl IntoElement {        let always_allow_tool_actions = AssistantSettings::get_global(cx).always_allow_tool_actions;         const HEADING: &str = "Allow running tools without asking for confirmation";         v_flex()            .p(DynamicSpacing::Base16.rems(cx))            .gap_2()            .flex_1()            .child(Headline::new("General Settings").size(HeadlineSize::Small))            .child(                h_flex()                    .p_2p5()                    .rounded_sm()                    .bg(cx.theme().colors().editor_background)                    .border_1()                    .border_color(cx.theme().colors().border)                    .gap_4()                    .justify_between()                    .flex_wrap()                    .child(                        v_flex()                            .gap_0p5()                            .max_w_5_6()                            .child(Label::new(HEADING))                            .child(Label::new("When enabled, 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(),                        )                        .on_click({                            let fs = self.fs.clone();                            move |state, _window, cx| {                                let allow = state == &ToggleState::Selected;                                update_settings_file::<AssistantSettings>(                                    fs.clone(),                                    cx,                                    move |settings, _| {                                        settings.set_always_allow_tool_actions(allow);                                    },                                );                            }                        }),                    ),            )    }     fn render_context_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {        let context_servers = self.context_server_manager.read(cx).all_servers().clone();        let tools_by_source = self.tools.read(cx).tools_by_source(cx);        let empty = Vec::new();         const SUBHEADING: &str = "Connect to context servers via the Model Context Protocol either via Zed extensions or directly.";         v_flex()            .p(DynamicSpacing::Base16.rems(cx))            .gap_2()            .flex_1()            .child(                v_flex()                    .gap_0p5()                    .child(                        Headline::new("Model Context Protocol (MCP) Servers")                            .size(HeadlineSize::Small),                    )                    .child(Label::new(SUBHEADING).color(Color::Muted)),            )            .children(context_servers.into_iter().map(|context_server| {                let is_running = context_server.client().is_some();                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().into(),                    })                    .unwrap_or_else(|| &empty);                let tool_count = tools.len();                 v_flex()                    .id(SharedString::from(context_server.id()))                    .border_1()                    .rounded_sm()                    .border_color(cx.theme().colors().border)                    .bg(cx.theme().colors().editor_background)                    .child(                        h_flex()                            .p_1()                            .justify_between()                            .when(are_tools_expanded && tool_count > 1, |element| {                                element                                    .border_b_1()                                    .border_color(cx.theme().colors().border)                            })                            .child(                                h_flex()                                    .gap_2()                                    .child(                                        Disclosure::new("tool-list-disclosure", are_tools_expanded)                                            .disabled(tool_count == 0)                                            .on_click(cx.listener({                                                let context_server_id = context_server.id();                                                move |this, _event, _window, _cx| {                                                    let is_open = this                                                        .expanded_context_server_tools                                                        .entry(context_server_id.clone())                                                        .or_insert(false);                                                     *is_open = !*is_open;                                                }                                            })),                                    )                                    .child(Indicator::dot().color(if is_running {                                        Color::Success                                    } else {                                        Color::Error                                    }))                                    .child(Label::new(context_server.id()))                                    .child(                                        Label::new(format!("{tool_count} tools"))                                            .color(Color::Muted)                                            .size(LabelSize::Small),                                    ),                            )                            .child(                                Switch::new("context-server-switch", is_running.into()).on_click({                                    let context_server_manager =                                        self.context_server_manager.clone();                                    let context_server = context_server.clone();                                    move |state, _window, cx| match state {                                        ToggleState::Unselected | ToggleState::Indeterminate => {                                            context_server_manager.update(cx, |this, cx| {                                                this.stop_server(context_server.clone(), cx)                                                    .log_err();                                            });                                        }                                        ToggleState::Selected => {                                            cx.spawn({                                                let context_server_manager =                                                    context_server_manager.clone();                                                let context_server = context_server.clone();                                                async move |cx| {                                                    if let Some(start_server_task) =                                                        context_server_manager                                                            .update(cx, |this, cx| {                                                                this.start_server(                                                                    context_server,                                                                    cx,                                                                )                                                            })                                                            .log_err()                                                    {                                                        start_server_task.await.log_err();                                                    }                                                }                                            })                                            .detach();                                        }                                    }                                }),                            ),                    )                    .map(|parent| {                        if !are_tools_expanded {                            return parent;                        }                         parent.child(v_flex().children(tools.into_iter().enumerate().map(                            |(ix, tool)| {                                h_flex()                                    .id("tool-item")                                    .pl_2()                                    .pr_1()                                    .py_1()                                    .gap_2()                                    .justify_between()                                    .when(ix < tool_count - 1, |element| {                                        element                                            .border_b_1()                                            .border_color(cx.theme().colors().border_variant)                                    })                                    .child(                                        Label::new(tool.name())                                            .buffer_font(cx)                                            .size(LabelSize::Small),                                    )                                    .child(                                        IconButton::new(("tool-description", ix), IconName::Info)                                            .shape(ui::IconButtonShape::Square)                                            .icon_size(IconSize::Small)                                            .icon_color(Color::Ignored)                                            .tooltip(Tooltip::text(tool.description())),                                    )                            },                        )))                    })            }))            .child(                h_flex()                    .justify_between()                    .gap_2()                    .child(                        h_flex().w_full().child(                            Button::new("add-context-server", "Add MCPs Directly")                                .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::DatabaseZap)                            .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,                                )                            }),                        ),                    ),            )    }} impl Render for AssistantConfiguration {    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {        let providers = LanguageModelRegistry::read_global(cx).providers();         v_flex()            .id("assistant-configuration")            .key_context("AgentConfiguration")            .track_focus(&self.focus_handle(cx))            .bg(cx.theme().colors().panel_background)            .size_full()            .overflow_y_scroll()            .child(self.render_command_permission(cx))            .child(Divider::horizontal().color(DividerColor::Border))            .child(self.render_context_servers_section(cx))            .child(Divider::horizontal().color(DividerColor::Border))            .child(                v_flex()                    .p(DynamicSpacing::Base16.rems(cx))                    .mt_1()                    .gap_6()                    .flex_1()                    .child(                        v_flex()                            .gap_0p5()                            .child(Headline::new("LLM Providers").size(HeadlineSize::Small))                            .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(&provider, cx)),                    ),            )    }}