assistant2: Add configuration (#23481)

Marshall Bowers created

This PR wires up the ability to configure Assistant2.

<img width="1309" alt="Screenshot 2025-01-22 at 1 52 56 PM"
src="https://github.com/user-attachments/assets/3de47797-7959-47af-bd93-51f105e87c28"
/>

Release Notes:

- N/A

Change summary

crates/assistant2/src/assistant.rs               |   2 
crates/assistant2/src/assistant_configuration.rs | 159 ++++++++++++++++++
crates/assistant2/src/assistant_panel.rs         |  74 +++++++
3 files changed, 229 insertions(+), 6 deletions(-)

Detailed changes

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,

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<LanguageModelProviderId, AnyView>,
+    _registry_subscription: Subscription,
+}
+
+impl AssistantConfiguration {
+    pub fn new(cx: &mut ViewContext<Self>) -> 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<Self>) {
+        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<dyn LanguageModelProvider>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        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<dyn LanguageModelProvider>),
+}
+
+impl EventEmitter<AssistantConfigurationEvent> for AssistantConfiguration {}
+
+impl AssistantConfiguration {
+    fn render_provider_configuration(
+        &mut self,
+        provider: &Arc<dyn LanguageModelProvider>,
+        cx: &mut ViewContext<Self>,
+    ) -> 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<Self>) -> 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)),
+                    ),
+            )
+    }
+}

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::<AssistantPanel>(cx);
                         panel.update(cx, |panel, cx| panel.open_prompt_editor_history(cx));
                     }
+                })
+                .register_action(|workspace, _: &OpenConfiguration, cx| {
+                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
+                        workspace.focus_panel::<AssistantPanel>(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<assistant_context_editor::ContextStore>,
     context_editor: Option<View<ContextEditor>>,
     context_history: Option<View<ContextHistory>>,
+    configuration: Option<View<AssistantConfiguration>>,
+    configuration_subscription: Option<Subscription>,
     tools: Arc<ToolWorkingSet>,
     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>) {
+        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<AssistantConfiguration>,
+        event: &AssistantConfigurationEvent,
+        cx: &mut ViewContext<Self>,
+    ) {
+        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::<AssistantSettings>(
+                            self.fs.clone(),
+                            cx,
+                            move |settings, _| settings.set_model(model),
+                        );
+                    }
+                }
+
+                self.new_thread(cx);
+            }
+        }
+    }
+
     pub(crate) fn active_thread(&self, cx: &AppContext) -> Model<Thread> {
         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()),
             })
     }
 }