Allow moving the assistant panel to other docks

Antonio Scandurra created

Change summary

Cargo.lock                          |  3 
assets/settings/default.json        | 10 +++
crates/ai/Cargo.toml                |  3 
crates/ai/src/ai.rs                 |  1 
crates/ai/src/assistant.rs          | 98 ++++++++++++++++++++++++------
crates/ai/src/assistant_settings.rs | 42 +++++++++++++
crates/terminal_view/Cargo.toml     |  2 
7 files changed, 135 insertions(+), 24 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -104,13 +104,16 @@ dependencies = [
  "chrono",
  "collections",
  "editor",
+ "fs",
  "futures 0.3.28",
  "gpui",
  "isahc",
  "language",
+ "schemars",
  "search",
  "serde",
  "serde_json",
+ "settings",
  "theme",
  "util",
  "workspace",

assets/settings/default.json 🔗

@@ -81,6 +81,16 @@
       // Default width of the project panel.
       "default_width": 240
   },
+  "assistant": {
+      // Where to dock the assistant. Can be 'left', 'right' or 'bottom'.
+      "dock": "right",
+      // Default width when the assistant is docked to the left or right.
+      "default_width": 480,
+      // Default height when the assistant is docked to the bottom.
+      "default_height": 320,
+      // OpenAI API key.
+      "openai_api_key": null
+  },
   // Whether the screen sharing icon is shown in the os status bar.
   "show_call_status_icon": true,
   // Whether to use language servers to provide code intelligence.

crates/ai/Cargo.toml 🔗

@@ -12,9 +12,11 @@ doctest = false
 assets = { path = "../assets"}
 collections = { path = "../collections"}
 editor = { path = "../editor" }
+fs = { path = "../fs" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
 search = { path = "../search" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }
@@ -23,6 +25,7 @@ anyhow.workspace = true
 chrono = "0.4"
 futures.workspace = true
 isahc.workspace = true
+schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 

crates/ai/src/ai.rs 🔗

@@ -1,4 +1,5 @@
 pub mod assistant;
+mod assistant_settings;
 
 pub use assistant::AssistantPanel;
 use gpui::{actions, AppContext};

crates/ai/src/assistant.rs 🔗

@@ -1,8 +1,12 @@
-use crate::{OpenAIRequest, OpenAIResponseStreamEvent, RequestMessage, Role};
+use crate::{
+    assistant_settings::{AssistantDockPosition, AssistantSettings},
+    OpenAIRequest, OpenAIResponseStreamEvent, RequestMessage, Role,
+};
 use anyhow::{anyhow, Result};
 use chrono::{DateTime, Local};
 use collections::HashMap;
 use editor::{Editor, ExcerptId, ExcerptRange, MultiBuffer};
+use fs::Fs;
 use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
 use gpui::{
     actions, elements::*, executor::Background, Action, AppContext, AsyncAppContext, Entity,
@@ -11,6 +15,7 @@ use gpui::{
 };
 use isahc::{http::StatusCode, Request, RequestExt};
 use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
+use settings::SettingsStore;
 use std::{io, sync::Arc};
 use util::{post_inc, ResultExt, TryFutureExt};
 use workspace::{
@@ -22,6 +27,7 @@ use workspace::{
 actions!(assistant, [NewContext, Assist, QuoteSelection, ToggleFocus]);
 
 pub fn init(cx: &mut AppContext) {
+    settings::register::<AssistantSettings>(cx);
     cx.add_action(
         |workspace: &mut Workspace, _: &NewContext, cx: &mut ViewContext<Workspace>| {
             if let Some(this) = workspace.panel::<AssistantPanel>(cx) {
@@ -41,12 +47,15 @@ pub enum AssistantPanelEvent {
     ZoomOut,
     Focus,
     Close,
+    DockPositionChanged,
 }
 
 pub struct AssistantPanel {
     width: Option<f32>,
+    height: Option<f32>,
     pane: ViewHandle<Pane>,
     languages: Arc<LanguageRegistry>,
+    fs: Arc<dyn Fs>,
     _subscriptions: Vec<Subscription>,
 }
 
@@ -113,17 +122,40 @@ impl AssistantPanel {
                             .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx));
                         pane
                     });
-                    let subscriptions = vec![
-                        cx.observe(&pane, |_, _, cx| cx.notify()),
-                        cx.subscribe(&pane, Self::handle_pane_event),
-                    ];
-
-                    Self {
+                    let mut this = Self {
                         pane,
                         languages: workspace.app_state().languages.clone(),
+                        fs: workspace.app_state().fs.clone(),
                         width: None,
-                        _subscriptions: subscriptions,
-                    }
+                        height: None,
+                        _subscriptions: Default::default(),
+                    };
+
+                    let mut old_dock_position = this.position(cx);
+                    let mut old_openai_api_key = settings::get::<AssistantSettings>(cx)
+                        .openai_api_key
+                        .clone();
+                    this._subscriptions = vec![
+                        cx.observe(&this.pane, |_, _, cx| cx.notify()),
+                        cx.subscribe(&this.pane, Self::handle_pane_event),
+                        cx.observe_global::<SettingsStore, _>(move |this, cx| {
+                            let new_dock_position = this.position(cx);
+                            if new_dock_position != old_dock_position {
+                                old_dock_position = new_dock_position;
+                                cx.emit(AssistantPanelEvent::DockPositionChanged);
+                            }
+
+                            let new_openai_api_key = settings::get::<AssistantSettings>(cx)
+                                .openai_api_key
+                                .clone();
+                            if old_openai_api_key != new_openai_api_key {
+                                old_openai_api_key = new_openai_api_key;
+                                cx.notify();
+                            }
+                        }),
+                    ];
+
+                    this
                 })
             })
         })
@@ -174,24 +206,44 @@ impl View for AssistantPanel {
 }
 
 impl Panel for AssistantPanel {
-    fn position(&self, _: &WindowContext) -> DockPosition {
-        DockPosition::Right
+    fn position(&self, cx: &WindowContext) -> DockPosition {
+        match settings::get::<AssistantSettings>(cx).dock {
+            AssistantDockPosition::Left => DockPosition::Left,
+            AssistantDockPosition::Bottom => DockPosition::Bottom,
+            AssistantDockPosition::Right => DockPosition::Right,
+        }
     }
 
-    fn position_is_valid(&self, position: DockPosition) -> bool {
-        matches!(position, DockPosition::Right)
+    fn position_is_valid(&self, _: DockPosition) -> bool {
+        true
     }
 
-    fn set_position(&mut self, _: DockPosition, _: &mut ViewContext<Self>) {
-        // TODO!
+    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
+        settings::update_settings_file::<AssistantSettings>(self.fs.clone(), cx, move |settings| {
+            let dock = match position {
+                DockPosition::Left => AssistantDockPosition::Left,
+                DockPosition::Bottom => AssistantDockPosition::Bottom,
+                DockPosition::Right => AssistantDockPosition::Right,
+            };
+            settings.dock = Some(dock);
+        });
     }
 
-    fn size(&self, _: &WindowContext) -> f32 {
-        self.width.unwrap_or(480.)
+    fn size(&self, cx: &WindowContext) -> f32 {
+        let settings = settings::get::<AssistantSettings>(cx);
+        match self.position(cx) {
+            DockPosition::Left | DockPosition::Right => {
+                self.width.unwrap_or_else(|| settings.default_width)
+            }
+            DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height),
+        }
     }
 
     fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
-        self.width = Some(size);
+        match self.position(cx) {
+            DockPosition::Left | DockPosition::Right => self.width = Some(size),
+            DockPosition::Bottom => self.height = Some(size),
+        }
         cx.notify();
     }
 
@@ -225,9 +277,8 @@ impl Panel for AssistantPanel {
         ("Assistant Panel".into(), Some(Box::new(ToggleFocus)))
     }
 
-    fn should_change_position_on_event(_: &Self::Event) -> bool {
-        // TODO!
-        false
+    fn should_change_position_on_event(event: &Self::Event) -> bool {
+        matches!(event, AssistantPanelEvent::DockPositionChanged)
     }
 
     fn should_activate_on_event(_: &Self::Event) -> bool {
@@ -289,7 +340,10 @@ impl Assistant {
             stream: true,
         };
 
-        if let Some(api_key) = std::env::var("OPENAI_API_KEY").log_err() {
+        if let Some(api_key) = settings::get::<AssistantSettings>(cx)
+            .openai_api_key
+            .clone()
+        {
             let stream = stream_completion(api_key, cx.background().clone(), request);
             let response = self.push_message(Role::Assistant, cx);
             self.push_message(Role::User, cx);

crates/ai/src/assistant_settings.rs 🔗

@@ -0,0 +1,42 @@
+use anyhow;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::Setting;
+
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum AssistantDockPosition {
+    Left,
+    Right,
+    Bottom,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct AssistantSettings {
+    pub dock: AssistantDockPosition,
+    pub default_width: f32,
+    pub default_height: f32,
+    pub openai_api_key: Option<String>,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct AssistantSettingsContent {
+    pub dock: Option<AssistantDockPosition>,
+    pub default_width: Option<f32>,
+    pub default_height: Option<f32>,
+    pub openai_api_key: Option<String>,
+}
+
+impl Setting for AssistantSettings {
+    const KEY: Option<&'static str> = Some("assistant");
+
+    type FileContent = AssistantSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &gpui::AppContext,
+    ) -> anyhow::Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}

crates/terminal_view/Cargo.toml 🔗

@@ -37,8 +37,6 @@ lazy_static.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 
-
-
 [dev-dependencies]
 editor = { path = "../editor", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }