editor: Show hints for using AI features on empty lines (#20824)

Bennet Bo Fenner , Thorsten , Antonio , and Thorsten Ball created

Co-Authored-by: Thorsten <thorsten@zed.dev>
Co-Authored-by: Antonio <antonio@zed.dev>

Screenshot:

![screenshot-2024-11-18-17 11
08@2x](https://github.com/user-attachments/assets/610fd7db-7476-4b9b-9465-a3d55df12340)

TODO:
- [x] docs

Release Notes:

- Added inline hints that guide users on how to invoke the inline
assistant and open the assistant panel. (These hints can be disabled by
setting `{"assistant": {"show_hints": false}}`.)

---------

Co-authored-by: Thorsten <thorsten@zed.dev>
Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Thorsten Ball <mrnugget@gmail.com>

Change summary

assets/settings/default.json                  |   3 
crates/assistant/src/assistant_settings.rs    |  11 ++
crates/editor/src/editor.rs                   |  36 ++++++
crates/editor/src/element.rs                  | 114 +++++++++++---------
crates/gpui/src/window.rs                     |  22 +++
crates/outline_panel/src/outline_panel.rs     |   6 
crates/recent_projects/src/recent_projects.rs |   8 
crates/zed/src/main.rs                        |   3 
crates/zed/src/zed.rs                         |   1 
crates/zed/src/zed/assistant_hints.rs         | 115 +++++++++++++++++++++
docs/src/assistant/configuration.md           |  26 ++--
docs/src/configuring-zed.md                   |  21 ++-
12 files changed, 283 insertions(+), 83 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -490,6 +490,9 @@
     "version": "2",
     // Whether the assistant is enabled.
     "enabled": true,
+    // Whether to show inline hints showing the keybindings to use the inline assistant and the
+    // assistant panel.
+    "show_hints": true,
     // Whether to show the assistant panel button in the status bar.
     "button": true,
     // Where to dock the assistant panel. Can be 'left', 'right' or 'bottom'.

crates/assistant/src/assistant_settings.rs 🔗

@@ -60,6 +60,7 @@ pub struct AssistantSettings {
     pub inline_alternatives: Vec<LanguageModelSelection>,
     pub using_outdated_settings_version: bool,
     pub enable_experimental_live_diffs: bool,
+    pub show_hints: bool,
 }
 
 impl AssistantSettings {
@@ -202,6 +203,7 @@ impl AssistantSettingsContent {
             AssistantSettingsContent::Versioned(settings) => match settings {
                 VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 {
                     enabled: settings.enabled,
+                    show_hints: None,
                     button: settings.button,
                     dock: settings.dock,
                     default_width: settings.default_width,
@@ -242,6 +244,7 @@ impl AssistantSettingsContent {
             },
             AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
                 enabled: None,
+                show_hints: None,
                 button: settings.button,
                 dock: settings.dock,
                 default_width: settings.default_width,
@@ -354,6 +357,7 @@ impl Default for VersionedAssistantSettingsContent {
     fn default() -> Self {
         Self::V2(AssistantSettingsContentV2 {
             enabled: None,
+            show_hints: None,
             button: None,
             dock: None,
             default_width: None,
@@ -371,6 +375,11 @@ pub struct AssistantSettingsContentV2 {
     ///
     /// Default: true
     enabled: Option<bool>,
+    /// Whether to show inline hints that show keybindings for inline assistant
+    /// and assistant panel.
+    ///
+    /// Default: true
+    show_hints: Option<bool>,
     /// Whether to show the assistant panel button in the status bar.
     ///
     /// Default: true
@@ -505,6 +514,7 @@ impl Settings for AssistantSettings {
 
             let value = value.upgrade();
             merge(&mut settings.enabled, value.enabled);
+            merge(&mut settings.show_hints, value.show_hints);
             merge(&mut settings.button, value.button);
             merge(&mut settings.dock, value.dock);
             merge(
@@ -575,6 +585,7 @@ mod tests {
                             }),
                             inline_alternatives: None,
                             enabled: None,
+                            show_hints: None,
                             button: None,
                             dock: None,
                             default_width: None,

crates/editor/src/editor.rs 🔗

@@ -540,6 +540,15 @@ pub enum IsVimMode {
     No,
 }
 
+pub trait ActiveLineTrailerProvider {
+    fn render_active_line_trailer(
+        &mut self,
+        style: &EditorStyle,
+        focus_handle: &FocusHandle,
+        cx: &mut WindowContext,
+    ) -> Option<AnyElement>;
+}
+
 /// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`]
 ///
 /// See the [module level documentation](self) for more information.
@@ -667,6 +676,7 @@ pub struct Editor {
     next_scroll_position: NextScrollCursorCenterTopBottom,
     addons: HashMap<TypeId, Box<dyn Addon>>,
     _scroll_cursor_center_top_bottom_task: Task<()>,
+    active_line_trailer_provider: Option<Box<dyn ActiveLineTrailerProvider>>,
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
@@ -2200,6 +2210,7 @@ impl Editor {
             addons: HashMap::default(),
             _scroll_cursor_center_top_bottom_task: Task::ready(()),
             text_style_refinement: None,
+            active_line_trailer_provider: None,
         };
         this.tasks_update_task = Some(this.refresh_runnables(cx));
         this._subscriptions.extend(project_subscriptions);
@@ -2488,6 +2499,16 @@ impl Editor {
         self.refresh_inline_completion(false, false, cx);
     }
 
+    pub fn set_active_line_trailer_provider<T>(
+        &mut self,
+        provider: Option<T>,
+        _cx: &mut ViewContext<Self>,
+    ) where
+        T: ActiveLineTrailerProvider + 'static,
+    {
+        self.active_line_trailer_provider = provider.map(|provider| Box::new(provider) as Box<_>);
+    }
+
     pub fn placeholder_text(&self, _cx: &WindowContext) -> Option<&str> {
         self.placeholder_text.as_deref()
     }
@@ -11844,6 +11865,21 @@ impl Editor {
             && self.has_blame_entries(cx)
     }
 
+    pub fn render_active_line_trailer(
+        &mut self,
+        style: &EditorStyle,
+        cx: &mut WindowContext,
+    ) -> Option<AnyElement> {
+        if !self.newest_selection_head_on_empty_line(cx) || self.has_active_inline_completion(cx) {
+            return None;
+        }
+
+        let focus_handle = self.focus_handle.clone();
+        self.active_line_trailer_provider
+            .as_mut()?
+            .render_active_line_trailer(style, &focus_handle, cx)
+    }
+
     fn has_blame_entries(&self, cx: &mut WindowContext) -> bool {
         self.blame()
             .map_or(false, |blame| blame.read(cx).has_generated_entries())

crates/editor/src/element.rs 🔗

@@ -1412,7 +1412,7 @@ impl EditorElement {
     }
 
     #[allow(clippy::too_many_arguments)]
-    fn layout_inline_blame(
+    fn layout_active_line_trailer(
         &self,
         display_row: DisplayRow,
         display_snapshot: &DisplaySnapshot,
@@ -1424,61 +1424,71 @@ impl EditorElement {
         line_height: Pixels,
         cx: &mut WindowContext,
     ) -> Option<AnyElement> {
-        if !self
+        let render_inline_blame = self
             .editor
-            .update(cx, |editor, cx| editor.render_git_blame_inline(cx))
-        {
-            return None;
-        }
+            .update(cx, |editor, cx| editor.render_git_blame_inline(cx));
+        if render_inline_blame {
+            let workspace = self
+                .editor
+                .read(cx)
+                .workspace
+                .as_ref()
+                .map(|(w, _)| w.clone());
 
-        let workspace = self
-            .editor
-            .read(cx)
-            .workspace
-            .as_ref()
-            .map(|(w, _)| w.clone());
+            let display_point = DisplayPoint::new(display_row, 0);
+            let buffer_row = MultiBufferRow(display_point.to_point(display_snapshot).row);
 
-        let display_point = DisplayPoint::new(display_row, 0);
-        let buffer_row = MultiBufferRow(display_point.to_point(display_snapshot).row);
+            let blame = self.editor.read(cx).blame.clone()?;
+            let blame_entry = blame
+                .update(cx, |blame, cx| {
+                    blame.blame_for_rows([Some(buffer_row)], cx).next()
+                })
+                .flatten()?;
 
-        let blame = self.editor.read(cx).blame.clone()?;
-        let blame_entry = blame
-            .update(cx, |blame, cx| {
-                blame.blame_for_rows([Some(buffer_row)], cx).next()
-            })
-            .flatten()?;
+            let mut element =
+                render_inline_blame_entry(&blame, blame_entry, &self.style, workspace, cx);
 
-        let mut element =
-            render_inline_blame_entry(&blame, blame_entry, &self.style, workspace, cx);
+            let start_y = content_origin.y
+                + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height);
 
-        let start_y = content_origin.y
-            + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height);
+            let start_x = {
+                const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.;
 
-        let start_x = {
-            const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.;
+                let line_end = if let Some(crease_trailer) = crease_trailer {
+                    crease_trailer.bounds.right()
+                } else {
+                    content_origin.x - scroll_pixel_position.x + line_layout.width
+                };
+                let padded_line_end = line_end + em_width * INLINE_BLAME_PADDING_EM_WIDTHS;
 
-            let line_end = if let Some(crease_trailer) = crease_trailer {
-                crease_trailer.bounds.right()
-            } else {
-                content_origin.x - scroll_pixel_position.x + line_layout.width
-            };
-            let padded_line_end = line_end + em_width * INLINE_BLAME_PADDING_EM_WIDTHS;
+                let min_column_in_pixels = ProjectSettings::get_global(cx)
+                    .git
+                    .inline_blame
+                    .and_then(|settings| settings.min_column)
+                    .map(|col| self.column_pixels(col as usize, cx))
+                    .unwrap_or(px(0.));
+                let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels;
 
-            let min_column_in_pixels = ProjectSettings::get_global(cx)
-                .git
-                .inline_blame
-                .and_then(|settings| settings.min_column)
-                .map(|col| self.column_pixels(col as usize, cx))
-                .unwrap_or(px(0.));
-            let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels;
+                cmp::max(padded_line_end, min_start)
+            };
 
-            cmp::max(padded_line_end, min_start)
-        };
+            let absolute_offset = point(start_x, start_y);
+            element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx);
 
-        let absolute_offset = point(start_x, start_y);
-        element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx);
+            Some(element)
+        } else if let Some(mut element) = self.editor.update(cx, |editor, cx| {
+            editor.render_active_line_trailer(&self.style, cx)
+        }) {
+            let start_y = content_origin.y
+                + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height);
+            let start_x = content_origin.x - scroll_pixel_position.x + em_width;
+            let absolute_offset = point(start_x, start_y);
+            element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx);
 
-        Some(element)
+            Some(element)
+        } else {
+            None
+        }
     }
 
     #[allow(clippy::too_many_arguments)]
@@ -3454,7 +3464,7 @@ impl EditorElement {
                 self.paint_lines(&invisible_display_ranges, layout, cx);
                 self.paint_redactions(layout, cx);
                 self.paint_cursors(layout, cx);
-                self.paint_inline_blame(layout, cx);
+                self.paint_active_line_trailer(layout, cx);
                 cx.with_element_namespace("crease_trailers", |cx| {
                     for trailer in layout.crease_trailers.iter_mut().flatten() {
                         trailer.element.paint(cx);
@@ -3936,10 +3946,10 @@ impl EditorElement {
         }
     }
 
-    fn paint_inline_blame(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
-        if let Some(mut inline_blame) = layout.inline_blame.take() {
+    fn paint_active_line_trailer(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
+        if let Some(mut element) = layout.active_line_trailer.take() {
             cx.paint_layer(layout.text_hitbox.bounds, |cx| {
-                inline_blame.paint(cx);
+                element.paint(cx);
             })
         }
     }
@@ -5331,14 +5341,14 @@ impl Element for EditorElement {
                         )
                     });
 
-                    let mut inline_blame = None;
+                    let mut active_line_trailer = None;
                     if let Some(newest_selection_head) = newest_selection_head {
                         let display_row = newest_selection_head.row();
                         if (start_row..end_row).contains(&display_row) {
                             let line_ix = display_row.minus(start_row) as usize;
                             let line_layout = &line_layouts[line_ix];
                             let crease_trailer_layout = crease_trailers[line_ix].as_ref();
-                            inline_blame = self.layout_inline_blame(
+                            active_line_trailer = self.layout_active_line_trailer(
                                 display_row,
                                 &snapshot.display_snapshot,
                                 line_layout,
@@ -5657,7 +5667,7 @@ impl Element for EditorElement {
                         line_elements,
                         line_numbers,
                         blamed_display_rows,
-                        inline_blame,
+                        active_line_trailer,
                         blocks,
                         cursors,
                         visible_cursors,
@@ -5794,7 +5804,7 @@ pub struct EditorLayout {
     line_numbers: Vec<Option<ShapedLine>>,
     display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
     blamed_display_rows: Option<Vec<AnyElement>>,
-    inline_blame: Option<AnyElement>,
+    active_line_trailer: Option<AnyElement>,
     blocks: Vec<BlockLayout>,
     highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
     highlighted_gutter_ranges: Vec<(Range<DisplayPoint>, Hsla)>,

crates/gpui/src/window.rs 🔗

@@ -3050,7 +3050,7 @@ impl<'a> WindowContext<'a> {
     }
 
     /// Represent this action as a key binding string, to display in the UI.
-    pub fn keystroke_text_for(&self, action: &dyn Action) -> String {
+    pub fn keystroke_text_for_action(&self, action: &dyn Action) -> String {
         self.bindings_for_action(action)
             .into_iter()
             .next()
@@ -3065,6 +3065,26 @@ impl<'a> WindowContext<'a> {
             .unwrap_or_else(|| action.name().to_string())
     }
 
+    /// Represent this action as a key binding string, to display in the UI.
+    pub fn keystroke_text_for_action_in(
+        &self,
+        action: &dyn Action,
+        focus_handle: &FocusHandle,
+    ) -> String {
+        self.bindings_for_action_in(action, focus_handle)
+            .into_iter()
+            .next()
+            .map(|binding| {
+                binding
+                    .keystrokes()
+                    .iter()
+                    .map(ToString::to_string)
+                    .collect::<Vec<_>>()
+                    .join(" ")
+            })
+            .unwrap_or_else(|| action.name().to_string())
+    }
+
     /// Dispatch a mouse or keyboard event on the window.
     #[profiling::function]
     pub fn dispatch_event(&mut self, event: PlatformInput) -> DispatchEventResult {

crates/outline_panel/src/outline_panel.rs 🔗

@@ -3875,13 +3875,13 @@ impl OutlinePanel {
                         .child({
                             let keystroke = match self.position(cx) {
                                 DockPosition::Left => {
-                                    cx.keystroke_text_for(&workspace::ToggleLeftDock)
+                                    cx.keystroke_text_for_action(&workspace::ToggleLeftDock)
                                 }
                                 DockPosition::Bottom => {
-                                    cx.keystroke_text_for(&workspace::ToggleBottomDock)
+                                    cx.keystroke_text_for_action(&workspace::ToggleBottomDock)
                                 }
                                 DockPosition::Right => {
-                                    cx.keystroke_text_for(&workspace::ToggleRightDock)
+                                    cx.keystroke_text_for_action(&workspace::ToggleRightDock)
                                 }
                             };
                             Label::new(format!("Toggle this panel with {keystroke}"))

crates/recent_projects/src/recent_projects.rs 🔗

@@ -185,13 +185,13 @@ impl PickerDelegate for RecentProjectsDelegate {
     fn placeholder_text(&self, cx: &mut WindowContext) -> Arc<str> {
         let (create_window, reuse_window) = if self.create_new_window {
             (
-                cx.keystroke_text_for(&menu::Confirm),
-                cx.keystroke_text_for(&menu::SecondaryConfirm),
+                cx.keystroke_text_for_action(&menu::Confirm),
+                cx.keystroke_text_for_action(&menu::SecondaryConfirm),
             )
         } else {
             (
-                cx.keystroke_text_for(&menu::SecondaryConfirm),
-                cx.keystroke_text_for(&menu::Confirm),
+                cx.keystroke_text_for_action(&menu::SecondaryConfirm),
+                cx.keystroke_text_for_action(&menu::Confirm),
             )
         };
         Arc::from(format!(

crates/zed/src/main.rs 🔗

@@ -66,7 +66,7 @@ use zed::{
     OpenRequest,
 };
 
-use crate::zed::inline_completion_registry;
+use crate::zed::{assistant_hints, inline_completion_registry};
 
 #[cfg(feature = "mimalloc")]
 #[global_allocator]
@@ -401,6 +401,7 @@ fn main() {
             stdout_is_a_pty(),
             cx,
         );
+        assistant_hints::init(cx);
         repl::init(
             app_state.fs.clone(),
             app_state.client.telemetry().clone(),

crates/zed/src/zed.rs 🔗

@@ -1,4 +1,5 @@
 mod app_menus;
+pub mod assistant_hints;
 pub mod inline_completion_registry;
 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
 pub(crate) mod linux_prompts;

crates/zed/src/zed/assistant_hints.rs 🔗

@@ -0,0 +1,115 @@
+use assistant::assistant_settings::AssistantSettings;
+use collections::HashMap;
+use editor::{ActiveLineTrailerProvider, Editor, EditorMode};
+use gpui::{AnyWindowHandle, AppContext, ViewContext, WeakView, WindowContext};
+use settings::{Settings, SettingsStore};
+use std::{cell::RefCell, rc::Rc};
+use theme::ActiveTheme;
+use ui::prelude::*;
+use workspace::Workspace;
+
+pub fn init(cx: &mut AppContext) {
+    let editors: Rc<RefCell<HashMap<WeakView<Editor>, AnyWindowHandle>>> = Rc::default();
+
+    cx.observe_new_views({
+        let editors = editors.clone();
+        move |_: &mut Workspace, cx: &mut ViewContext<Workspace>| {
+            let workspace_handle = cx.view().clone();
+            cx.subscribe(&workspace_handle, {
+                let editors = editors.clone();
+                move |_, _, event, cx| match event {
+                    workspace::Event::ItemAdded { item } => {
+                        if let Some(editor) = item.act_as::<Editor>(cx) {
+                            if editor.read(cx).mode() != EditorMode::Full {
+                                return;
+                            }
+
+                            cx.on_release({
+                                let editor_handle = editor.downgrade();
+                                let editors = editors.clone();
+                                move |_, _, _| {
+                                    editors.borrow_mut().remove(&editor_handle);
+                                }
+                            })
+                            .detach();
+                            editors
+                                .borrow_mut()
+                                .insert(editor.downgrade(), cx.window_handle());
+
+                            let show_hints = should_show_hints(cx);
+                            editor.update(cx, |editor, cx| {
+                                assign_active_line_trailer_provider(editor, show_hints, cx)
+                            })
+                        }
+                    }
+                    _ => {}
+                }
+            })
+            .detach();
+        }
+    })
+    .detach();
+
+    let mut show_hints = AssistantSettings::get_global(cx).show_hints;
+    cx.observe_global::<SettingsStore>(move |cx| {
+        let new_show_hints = should_show_hints(cx);
+        if new_show_hints != show_hints {
+            show_hints = new_show_hints;
+            for (editor, window) in editors.borrow().iter() {
+                _ = window.update(cx, |_window, cx| {
+                    _ = editor.update(cx, |editor, cx| {
+                        assign_active_line_trailer_provider(editor, show_hints, cx);
+                    })
+                });
+            }
+        }
+    })
+    .detach();
+}
+
+struct AssistantHintsProvider;
+
+impl ActiveLineTrailerProvider for AssistantHintsProvider {
+    fn render_active_line_trailer(
+        &mut self,
+        style: &editor::EditorStyle,
+        focus_handle: &gpui::FocusHandle,
+        cx: &mut WindowContext,
+    ) -> Option<gpui::AnyElement> {
+        if !focus_handle.is_focused(cx) {
+            return None;
+        }
+
+        let chat_keybinding =
+            cx.keystroke_text_for_action_in(&assistant::ToggleFocus, focus_handle);
+        let generate_keybinding =
+            cx.keystroke_text_for_action_in(&zed_actions::InlineAssist::default(), focus_handle);
+
+        Some(
+            h_flex()
+                .id("inline-assistant-instructions")
+                .w_full()
+                .font_family(style.text.font().family)
+                .text_color(cx.theme().status().hint)
+                .line_height(style.text.line_height)
+                .child(format!(
+                    "{chat_keybinding} to chat, {generate_keybinding} to generate"
+                ))
+                .into_any(),
+        )
+    }
+}
+
+fn assign_active_line_trailer_provider(
+    editor: &mut Editor,
+    show_hints: bool,
+    cx: &mut ViewContext<Editor>,
+) {
+    let provider = show_hints.then_some(AssistantHintsProvider);
+    editor.set_active_line_trailer_provider(provider, cx);
+}
+
+fn should_show_hints(cx: &AppContext) -> bool {
+    let assistant_settings = AssistantSettings::get_global(cx);
+    assistant_settings.enabled && assistant_settings.show_hints
+}

docs/src/assistant/configuration.md 🔗

@@ -200,18 +200,28 @@ You must provide the model's Context Window in the `max_tokens` parameter, this
 {
   "assistant": {
     "enabled": true,
+    "show_hints": true,
+    "button": true,
+    "dock": "right"
+    "default_width": 480,
     "default_model": {
       "provider": "zed.dev",
       "model": "claude-3-5-sonnet"
     },
     "version": "2",
-    "button": true,
-    "default_width": 480,
-    "dock": "right"
   }
 }
 ```
 
+| key            | type    | default | description                                                                           |
+| -------------- | ------- | ------- | ------------------------------------------------------------------------------------- |
+| enabled        | boolean | true    | Setting this to `false` will completely disable the assistant                         |
+| show_hints     | boolean | true    | Whether to to show hints in the editor explaining how to use assistant                |
+| button         | boolean | true    | Show the assistant icon in the status bar                                             |
+| dock           | string  | "right" | The default dock position for the assistant panel. Can be ["left", "right", "bottom"] |
+| default_height | string  | null    | The pixel height of the assistant panel when docked to the bottom                     |
+| default_width  | string  | null    | The pixel width of the assistant panel when docked to the left or right               |
+
 #### Custom endpoints {#custom-endpoint}
 
 You can use a custom API endpoint for different providers, as long as it's compatible with the providers API structure.
@@ -271,13 +281,3 @@ will generate two outputs for every assist. One with Claude 3.5 Sonnet, and one
   }
 }
 ```
-
-#### Common Panel Settings
-
-| key            | type    | default | description                                                                           |
-| -------------- | ------- | ------- | ------------------------------------------------------------------------------------- |
-| enabled        | boolean | true    | Setting this to `false` will completely disable the assistant                         |
-| button         | boolean | true    | Show the assistant icon in the status bar                                             |
-| dock           | string  | "right" | The default dock position for the assistant panel. Can be ["left", "right", "bottom"] |
-| default_height | string  | null    | The pixel height of the assistant panel when docked to the bottom                     |
-| default_width  | string  | null    | The pixel width of the assistant panel when docked to the left or right               |

docs/src/configuring-zed.md 🔗

@@ -2327,15 +2327,18 @@ Run the `theme selector: toggle` action in the command palette to see a current
 - Default:
 
 ```json
-"assistant": {
-  "enabled": true,
-  "button": true,
-  "dock": "right",
-  "default_width": 640,
-  "default_height": 320,
-  "provider": "openai",
-  "version": "1",
-},
+{
+  "assistant": {
+    "enabled": true,
+    "button": true,
+    "dock": "right",
+    "default_width": 640,
+    "default_height": 320,
+    "provider": "openai",
+    "version": "1",
+    "show_hints": true
+  }
+}
 ```
 
 ## Outline Panel