diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index 4d1385abdd..1791bb979c 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -1,4 +1,5 @@ mod active_thread; +mod assistant_configuration; mod assistant_model_selector; mod assistant_panel; mod buffer_codegen; @@ -41,6 +42,7 @@ actions!( RemoveAllContext, OpenHistory, OpenPromptEditorHistory, + OpenConfiguration, RemoveSelectedThread, Chat, ChatMode, diff --git a/crates/assistant2/src/assistant_configuration.rs b/crates/assistant2/src/assistant_configuration.rs new file mode 100644 index 0000000000..a69a5fa415 --- /dev/null +++ b/crates/assistant2/src/assistant_configuration.rs @@ -0,0 +1,159 @@ +use std::sync::Arc; + +use collections::HashMap; +use gpui::{AnyView, AppContext, EventEmitter, FocusHandle, FocusableView, Subscription}; +use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry}; +use ui::{prelude::*, ElevationIndex}; + +pub struct AssistantConfiguration { + focus_handle: FocusHandle, + configuration_views_by_provider: HashMap, + _registry_subscription: Subscription, +} + +impl AssistantConfiguration { + pub fn new(cx: &mut ViewContext) -> Self { + let focus_handle = cx.focus_handle(); + + let registry_subscription = cx.subscribe( + &LanguageModelRegistry::global(cx), + |this, _, event: &language_model::Event, 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, cx); + } + } + language_model::Event::RemovedProvider(provider_id) => { + this.remove_provider_configuration_view(provider_id); + } + _ => {} + }, + ); + + let mut this = Self { + focus_handle, + configuration_views_by_provider: HashMap::default(), + _registry_subscription: registry_subscription, + }; + this.build_provider_configuration_views(cx); + this + } + + fn build_provider_configuration_views(&mut self, cx: &mut ViewContext) { + let providers = LanguageModelRegistry::read_global(cx).providers(); + for provider in providers { + self.add_provider_configuration_view(&provider, 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, + cx: &mut ViewContext, + ) { + let configuration_view = provider.configuration_view(cx); + self.configuration_views_by_provider + .insert(provider.id(), configuration_view); + } +} + +impl FocusableView for AssistantConfiguration { + fn focus_handle(&self, _: &AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} + +pub enum AssistantConfigurationEvent { + NewThread(Arc), +} + +impl EventEmitter for AssistantConfiguration {} + +impl AssistantConfiguration { + fn render_provider_configuration( + &mut self, + provider: &Arc, + cx: &mut ViewContext, + ) -> impl IntoElement { + 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_2() + .child( + h_flex() + .justify_between() + .child(Headline::new(provider_name.clone()).size(HeadlineSize::Small)) + .when(provider.is_authenticated(cx), |parent| { + parent.child( + h_flex().justify_end().child( + Button::new( + SharedString::from(format!("new-thread-{provider_id}")), + "Open New Thread", + ) + .icon_position(IconPosition::Start) + .icon(IconName::Plus) + .style(ButtonStyle::Filled) + .layer(ElevationIndex::ModalSurface) + .on_click(cx.listener({ + let provider = provider.clone(); + move |_this, _event, cx| { + cx.emit(AssistantConfigurationEvent::NewThread( + provider.clone(), + )) + } + })), + ), + ) + }), + ) + .child( + div() + .p(DynamicSpacing::Base08.rems(cx)) + .bg(cx.theme().colors().surface_background) + .border_1() + .border_color(cx.theme().colors().border_variant) + .rounded_md() + .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}", + )))), + }), + ) + } +} + +impl Render for AssistantConfiguration { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let providers = LanguageModelRegistry::read_global(cx).providers(); + + v_flex() + .id("assistant-configuration") + .track_focus(&self.focus_handle(cx)) + .bg(cx.theme().colors().editor_background) + .size_full() + .overflow_y_scroll() + .child( + v_flex() + .p(DynamicSpacing::Base16.rems(cx)) + .mt_1() + .gap_6() + .flex_1() + .children( + providers + .into_iter() + .map(|provider| self.render_provider_configuration(&provider, cx)), + ), + ) + } +} diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index b730b4ef97..67a6da3ef1 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -12,13 +12,14 @@ use client::zed_urls; use fs::Fs; use gpui::{ prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, Corner, EventEmitter, - FocusHandle, FocusableView, FontWeight, Model, Pixels, Task, View, ViewContext, WeakView, - WindowContext, + FocusHandle, FocusableView, FontWeight, Model, Pixels, Subscription, Task, View, ViewContext, + WeakView, WindowContext, }; use language::LanguageRegistry; +use language_model::LanguageModelRegistry; use project::Project; use prompt_library::PromptBuilder; -use settings::Settings; +use settings::{update_settings_file, Settings}; use time::UtcOffset; use ui::{prelude::*, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip}; use util::ResultExt as _; @@ -27,11 +28,12 @@ use workspace::Workspace; use zed_actions::assistant::ToggleFocus; use crate::active_thread::ActiveThread; +use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent}; use crate::message_editor::MessageEditor; use crate::thread::{Thread, ThreadError, ThreadId}; use crate::thread_history::{PastThread, ThreadHistory}; use crate::thread_store::ThreadStore; -use crate::{NewPromptEditor, NewThread, OpenHistory, OpenPromptEditorHistory}; +use crate::{NewPromptEditor, NewThread, OpenConfiguration, OpenHistory, OpenPromptEditorHistory}; pub fn init(cx: &mut AppContext) { cx.observe_new_views( @@ -60,6 +62,12 @@ pub fn init(cx: &mut AppContext) { workspace.focus_panel::(cx); panel.update(cx, |panel, cx| panel.open_prompt_editor_history(cx)); } + }) + .register_action(|workspace, _: &OpenConfiguration, cx| { + if let Some(panel) = workspace.panel::(cx) { + workspace.focus_panel::(cx); + panel.update(cx, |panel, cx| panel.open_configuration(cx)); + } }); }, ) @@ -71,6 +79,7 @@ enum ActiveView { PromptEditor, History, PromptEditorHistory, + Configuration, } pub struct AssistantPanel { @@ -84,6 +93,8 @@ pub struct AssistantPanel { context_store: Model, context_editor: Option>, context_history: Option>, + configuration: Option>, + configuration_subscription: Option, tools: Arc, local_timezone: UtcOffset, active_view: ActiveView, @@ -173,6 +184,8 @@ impl AssistantPanel { context_store, context_editor: None, context_history: None, + configuration: None, + configuration_subscription: None, tools, local_timezone: UtcOffset::from_whole_seconds( chrono::Local::now().offset().local_minus_utc(), @@ -357,6 +370,46 @@ impl AssistantPanel { self.message_editor.focus_handle(cx).focus(cx); } + pub(crate) fn open_configuration(&mut self, cx: &mut ViewContext) { + self.active_view = ActiveView::Configuration; + self.configuration = Some(cx.new_view(AssistantConfiguration::new)); + + if let Some(configuration) = self.configuration.as_ref() { + self.configuration_subscription = + Some(cx.subscribe(configuration, Self::handle_assistant_configuration_event)); + + configuration.focus_handle(cx).focus(cx); + } + } + + fn handle_assistant_configuration_event( + &mut self, + _view: View, + event: &AssistantConfigurationEvent, + cx: &mut ViewContext, + ) { + match event { + AssistantConfigurationEvent::NewThread(provider) => { + if LanguageModelRegistry::read_global(cx) + .active_provider() + .map_or(true, |active_provider| { + active_provider.id() != provider.id() + }) + { + if let Some(model) = provider.provided_models(cx).first().cloned() { + update_settings_file::( + self.fs.clone(), + cx, + move |settings, _| settings.set_model(model), + ); + } + } + + self.new_thread(cx); + } + } + } + pub(crate) fn active_thread(&self, cx: &AppContext) -> Model { self.thread.read(cx).thread.clone() } @@ -386,6 +439,13 @@ impl FocusableView for AssistantPanel { cx.focus_handle() } } + ActiveView::Configuration => { + if let Some(configuration) = self.configuration.as_ref() { + configuration.focus_handle(cx) + } else { + cx.focus_handle() + } + } } } } @@ -493,6 +553,7 @@ impl AssistantPanel { .unwrap_or_else(|| SharedString::from("Loading Summary…")), ActiveView::History => "History / Thread".into(), ActiveView::PromptEditorHistory => "History / Prompt Editor".into(), + ActiveView::Configuration => "Configuration".into(), }; h_flex() @@ -555,8 +616,8 @@ impl AssistantPanel { .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) .tooltip(move |cx| Tooltip::text("Configure Assistant", cx)) - .on_click(move |_event, _cx| { - println!("Configure Assistant"); + .on_click(move |_event, cx| { + cx.dispatch_action(OpenConfiguration.boxed_clone()); }), ), ) @@ -810,6 +871,7 @@ impl Render for AssistantPanel { ActiveView::History => parent.child(self.history.clone()), ActiveView::PromptEditor => parent.children(self.context_editor.clone()), ActiveView::PromptEditorHistory => parent.children(self.context_history.clone()), + ActiveView::Configuration => parent.children(self.configuration.clone()), }) } }