Add assistant icons to the toolbar

Antonio Scandurra and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

assets/icons/assist_15.svg        |   0 
assets/icons/split_message_15.svg |   0 
crates/ai/src/assistant.rs        | 181 ++++++++++++++++----------------
crates/gpui/src/elements/label.rs |   1 
crates/theme/src/theme.rs         |   3 
styles/src/styleTree/assistant.ts |  41 ++++++-
6 files changed, 127 insertions(+), 99 deletions(-)

Detailed changes

crates/ai/src/assistant.rs 🔗

@@ -28,7 +28,6 @@ use search::BufferSearchBar;
 use serde::Deserialize;
 use settings::SettingsStore;
 use std::{
-    borrow::Cow,
     cell::RefCell,
     cmp, env,
     fmt::Write,
@@ -40,13 +39,9 @@ use std::{
     time::Duration,
 };
 use theme::{ui::IconStyle, AssistantStyle};
-use util::{
-    channel::ReleaseChannel, paths::CONVERSATIONS_DIR, post_inc, truncate_and_trailoff, ResultExt,
-    TryFutureExt,
-};
+use util::{channel::ReleaseChannel, paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel},
-    item::Item,
     searchable::Direction,
     Save, ToggleZoom, Toolbar, Workspace,
 };
@@ -361,64 +356,43 @@ impl AssistantPanel {
             })
     }
 
-    fn render_current_model(
-        &self,
-        style: &AssistantStyle,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<impl Element<Self>> {
-        enum Model {}
-
-        let model = self
-            .active_editor()?
-            .read(cx)
-            .conversation
-            .read(cx)
-            .model
-            .clone();
+    fn render_editor_tools(&self, style: &AssistantStyle) -> Vec<AnyElement<Self>> {
+        if self.active_editor().is_some() {
+            vec![
+                Self::render_split_button(&style.split_button).into_any(),
+                Self::render_assist_button(&style.assist_button).into_any(),
+            ]
+        } else {
+            Default::default()
+        }
+    }
 
-        Some(
-            MouseEventHandler::<Model, _>::new(0, cx, |state, _| {
-                let style = style.model.style_for(state);
-                Label::new(model, style.text.clone())
-                    .contained()
-                    .with_style(style.container)
-            })
+    fn render_split_button(style: &IconStyle) -> impl Element<Self> {
+        enum SplitMessage {}
+        Svg::for_style(style.icon.clone())
+            .contained()
+            .with_style(style.container)
+            .mouse::<SplitMessage>(0)
             .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(MouseButton::Left, |_, this, cx| {
-                if let Some(editor) = this.active_editor() {
-                    editor.update(cx, |editor, cx| {
-                        editor.cycle_model(cx);
-                    });
+            .on_click(MouseButton::Left, |_, this: &mut Self, cx| {
+                if let Some(active_editor) = this.active_editor() {
+                    active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx));
                 }
-            }),
-        )
+            })
     }
 
-    fn render_remaining_tokens(
-        &self,
-        style: &AssistantStyle,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<impl Element<Self>> {
-        self.active_editor().and_then(|editor| {
-            editor
-                .read(cx)
-                .conversation
-                .read(cx)
-                .remaining_tokens()
-                .map(|remaining_tokens| {
-                    let remaining_tokens_style = if remaining_tokens <= 0 {
-                        &style.no_remaining_tokens
-                    } else {
-                        &style.remaining_tokens
-                    };
-                    Label::new(
-                        remaining_tokens.to_string(),
-                        remaining_tokens_style.text.clone(),
-                    )
-                    .contained()
-                    .with_style(remaining_tokens_style.container)
-                })
-        })
+    fn render_assist_button(style: &IconStyle) -> impl Element<Self> {
+        enum Assist {}
+        Svg::for_style(style.icon.clone())
+            .contained()
+            .with_style(style.container)
+            .mouse::<Assist>(0)
+            .with_cursor_style(CursorStyle::PointingHand)
+            .on_click(MouseButton::Left, |_, this: &mut Self, cx| {
+                if let Some(active_editor) = this.active_editor() {
+                    active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx));
+                }
+            })
     }
 
     fn render_plus_button(style: &IconStyle) -> impl Element<Self> {
@@ -589,19 +563,16 @@ impl View for AssistantPanel {
                         )
                         .with_children(title)
                         .with_children(
-                            self.render_current_model(&style, cx)
-                                .map(|current_model| current_model.aligned().flex_float()),
-                        )
-                        .with_children(
-                            self.render_remaining_tokens(&style, cx)
-                                .map(|remaining_tokens| remaining_tokens.aligned().flex_float()),
+                            self.render_editor_tools(&style)
+                                .into_iter()
+                                .map(|tool| tool.aligned().flex_float()),
                         )
                         .with_child(
                             Self::render_plus_button(&style.plus_button)
                                 .aligned()
                                 .flex_float(),
                         )
-                        .with_child(self.render_zoom_button(&style, cx).aligned().flex_float())
+                        .with_child(self.render_zoom_button(&style, cx).aligned())
                         .contained()
                         .with_style(theme.workspace.tab_bar.container)
                         .expanded()
@@ -1995,6 +1966,44 @@ impl ConversationEditor {
             .map(|summary| summary.text.clone())
             .unwrap_or_else(|| "New Conversation".into())
     }
+
+    fn render_current_model(
+        &self,
+        style: &AssistantStyle,
+        cx: &mut ViewContext<Self>,
+    ) -> impl Element<Self> {
+        enum Model {}
+
+        MouseEventHandler::<Model, _>::new(0, cx, |state, cx| {
+            let style = style.model.style_for(state);
+            Label::new(self.conversation.read(cx).model.clone(), style.text.clone())
+                .contained()
+                .with_style(style.container)
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, |_, this, cx| this.cycle_model(cx))
+    }
+
+    fn render_remaining_tokens(
+        &self,
+        style: &AssistantStyle,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<impl Element<Self>> {
+        let remaining_tokens = self.conversation.read(cx).remaining_tokens()?;
+        let remaining_tokens_style = if remaining_tokens <= 0 {
+            &style.no_remaining_tokens
+        } else {
+            &style.remaining_tokens
+        };
+        Some(
+            Label::new(
+                remaining_tokens.to_string(),
+                remaining_tokens_style.text.clone(),
+            )
+            .contained()
+            .with_style(remaining_tokens_style.container),
+        )
+    }
 }
 
 impl Entity for ConversationEditor {
@@ -2008,9 +2017,20 @@ impl View for ConversationEditor {
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
         let theme = &theme::current(cx).assistant;
-        ChildView::new(&self.editor, cx)
-            .contained()
-            .with_style(theme.container)
+        Stack::new()
+            .with_child(
+                ChildView::new(&self.editor, cx)
+                    .contained()
+                    .with_style(theme.container),
+            )
+            .with_child(
+                Flex::row()
+                    .with_child(self.render_current_model(theme, cx))
+                    .with_children(self.render_remaining_tokens(theme, cx))
+                    .aligned()
+                    .top()
+                    .right(),
+            )
             .into_any()
     }
 
@@ -2021,29 +2041,6 @@ impl View for ConversationEditor {
     }
 }
 
-impl Item for ConversationEditor {
-    fn tab_content<V: View>(
-        &self,
-        _: Option<usize>,
-        style: &theme::Tab,
-        cx: &gpui::AppContext,
-    ) -> AnyElement<V> {
-        let title = truncate_and_trailoff(&self.title(cx), editor::MAX_TAB_TITLE_LEN);
-        Label::new(title, style.label.clone()).into_any()
-    }
-
-    fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
-        Some(self.title(cx).into())
-    }
-
-    fn as_searchable(
-        &self,
-        _: &ViewHandle<Self>,
-    ) -> Option<Box<dyn workspace::searchable::SearchableItemHandle>> {
-        Some(Box::new(self.editor.clone()))
-    }
-}
-
 #[derive(Clone, Debug)]
 struct MessageAnchor {
     id: MessageId,

crates/gpui/src/elements/label.rs 🔗

@@ -165,6 +165,7 @@ impl<V: View> Element<V> for Label {
         _: &mut V,
         cx: &mut ViewContext<V>,
     ) -> Self::PaintState {
+        let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
         line.paint(
             scene,
             bounds.origin(),

crates/theme/src/theme.rs 🔗

@@ -994,6 +994,8 @@ pub struct TerminalStyle {
 pub struct AssistantStyle {
     pub container: ContainerStyle,
     pub hamburger_button: IconStyle,
+    pub split_button: IconStyle,
+    pub assist_button: IconStyle,
     pub zoom_in_button: IconStyle,
     pub zoom_out_button: IconStyle,
     pub plus_button: IconStyle,
@@ -1003,7 +1005,6 @@ pub struct AssistantStyle {
     pub user_sender: Interactive<ContainedText>,
     pub assistant_sender: Interactive<ContainedText>,
     pub system_sender: Interactive<ContainedText>,
-    pub model_info_container: ContainerStyle,
     pub model: Interactive<ContainedText>,
     pub remaining_tokens: ContainedText,
     pub no_remaining_tokens: ContainedText,

styles/src/styleTree/assistant.ts 🔗

@@ -28,6 +28,32 @@ export default function assistant(colorScheme: ColorScheme) {
                 margin: { left: 12 },
             }
         },
+        splitButton: {
+            icon: {
+                color: text(layer, "sans", "default", { size: "sm" }).color,
+                asset: "icons/split_message_15.svg",
+                dimensions: {
+                    width: 15,
+                    height: 15,
+                },
+            },
+            container: {
+                margin: { left: 12 },
+            }
+        },
+        assistButton: {
+            icon: {
+                color: text(layer, "sans", "default", { size: "sm" }).color,
+                asset: "icons/assist_15.svg",
+                dimensions: {
+                    width: 15,
+                    height: 15,
+                },
+            },
+            container: {
+                margin: { left: 12, right: 12 },
+            }
+        },
         zoomInButton: {
             icon: {
                 color: text(layer, "sans", "default", { size: "sm" }).color,
@@ -120,13 +146,10 @@ export default function assistant(colorScheme: ColorScheme) {
             margin: { top: 2, left: 8 },
             ...text(layer, "sans", "default", { size: "2xs" }),
         },
-        modelInfoContainer: {
-            margin: { right: 16, top: 4 },
-        },
         model: interactive({
             base: {
                 background: background(layer, "on"),
-                margin: { right: 8 },
+                margin: { left: 12, right: 12, top: 12 },
                 padding: 4,
                 cornerRadius: 4,
                 ...text(layer, "sans", "default", { size: "xs" }),
@@ -139,11 +162,17 @@ export default function assistant(colorScheme: ColorScheme) {
             },
         }),
         remainingTokens: {
-            margin: { right: 12 },
+            background: background(layer, "on"),
+            margin: { top: 12, right: 12 },
+            padding: 4,
+            cornerRadius: 4,
             ...text(layer, "sans", "positive", { size: "xs" }),
         },
         noRemainingTokens: {
-            margin: { right: 12 },
+            background: background(layer, "on"),
+            margin: { top: 12, right: 12 },
+            padding: 4,
+            cornerRadius: 4,
             ...text(layer, "sans", "negative", { size: "xs" }),
         },
         errorIcon: {