Iterate on Assistant 2 composer UI (#11306)

Nate Butler , Marshall Bowers , and Marshall Bowers created

- Change project index tool rendering in composer
- Update composer UI style

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>

Change summary

assets/icons/code.svg                        |   1 
assets/icons/spinner.svg                     |  13 +
crates/assistant2/src/assistant2.rs          |   8 
crates/assistant2/src/tools/project_index.rs |  73 ++++++++-
crates/assistant2/src/ui/composer.rs         | 159 ++++++++-------------
crates/ui/src/components/icon.rs             |   4 
6 files changed, 141 insertions(+), 117 deletions(-)

Detailed changes

assets/icons/code.svg πŸ”—

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-code-xml"><path d="m18 16 4-4-4-4"/><path d="m6 8-4 4 4 4"/><path d="m14.5 4-5 16"/></svg>

assets/icons/spinner.svg πŸ”—

@@ -0,0 +1,13 @@
+<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_1803_28)">
+<path d="M0.5 2C0.5 1.17157 1.17157 0.5 2 0.5V0.5C2.82843 0.5 3.5 1.17157 3.5 2V2C3.5 2.82843 2.82843 3.5 2 3.5V3.5C1.17157 3.5 0.5 2.82843 0.5 2V2Z" fill="black" fill-opacity="0.3"/>
+<path d="M7.5 6C7.5 6.82843 6.82843 7.5 6 7.5V7.5C5.17157 7.5 4.5 6.82843 4.5 6V6C4.5 5.17157 5.17157 4.5 6 4.5V4.5C6.82843 4.5 7.5 5.17157 7.5 6V6Z" fill="black" fill-opacity="0.6"/>
+<path d="M2 7.5C1.17157 7.5 0.5 6.82843 0.5 6V6C0.5 5.17157 1.17157 4.5 2 4.5V4.5C2.82843 4.5 3.5 5.17157 3.5 6V6C3.5 6.82843 2.82843 7.5 2 7.5V7.5Z" fill="black" fill-opacity="0.8"/>
+<path d="M6 0.5C6.82843 0.5 7.5 1.17157 7.5 2V2C7.5 2.82843 6.82843 3.5 6 3.5V3.5C5.17157 3.5 4.5 2.82843 4.5 2V2C4.5 1.17157 5.17157 0.5 6 0.5V0.5Z" fill="black"/>
+</g>
+<defs>
+<clipPath id="clip0_1803_28">
+<rect width="8" height="8" fill="white"/>
+</clipPath>
+</defs>
+</svg>

crates/assistant2/src/assistant2.rs πŸ”—

@@ -269,7 +269,7 @@ impl AssistantChat {
             composer_editor: cx.new_view(|cx| {
                 let mut editor = Editor::auto_height(80, cx);
                 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
-                editor.set_placeholder_text("Type a message to the assistant", cx);
+                editor.set_placeholder_text("Send a message…", cx);
                 editor
             }),
             list_state,
@@ -372,10 +372,6 @@ impl AssistantChat {
         }));
     }
 
-    fn can_submit(&self) -> bool {
-        self.pending_completion.is_none()
-    }
-
     fn debug_project_index(&mut self, _: &DebugProjectIndex, cx: &mut ViewContext<Self>) {
         if let Some(index) = &self.project_index {
             index.update(cx, |project_index, cx| {
@@ -594,7 +590,6 @@ impl AssistantChat {
                         element.child(Composer::new(
                             body.clone(),
                             self.user_store.read(cx).current_user(),
-                            true,
                             self.tool_registry.clone(),
                             crate::ui::ModelSelector::new(
                                 cx.view().downgrade(),
@@ -773,7 +768,6 @@ impl Render for AssistantChat {
             .child(Composer::new(
                 self.composer_editor.clone(),
                 self.user_store.read(cx).current_user(),
-                self.can_submit(),
                 self.tool_registry.clone(),
                 crate::ui::ModelSelector::new(cx.view().downgrade(), self.model.clone())
                     .into_any_element(),

crates/assistant2/src/tools/project_index.rs πŸ”—

@@ -1,14 +1,14 @@
 use anyhow::Result;
 use assistant_tooling::LanguageModelTool;
-use gpui::{prelude::*, AnyView, Model, Task};
+use gpui::{percentage, prelude::*, Animation, AnimationExt, AnyView, Model, Task, Transformation};
 use project::Fs;
 use schemars::JsonSchema;
 use semantic_index::{ProjectIndex, Status};
 use serde::Deserialize;
-use std::sync::Arc;
+use std::{sync::Arc, time::Duration};
 use ui::{
-    div, prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, SharedString,
-    WindowContext,
+    div, prelude::*, ButtonLike, CollapsibleContainer, Color, Icon, IconName, Indicator, Label,
+    SharedString, Tooltip, WindowContext,
 };
 use util::ResultExt as _;
 
@@ -255,12 +255,63 @@ impl Render for ProjectIndexStatusView {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let status = self.project_index.read(cx).status();
 
-        h_flex().gap_2().map(|element| match status {
-            Status::Idle => element.child(Label::new("Project index ready")),
-            Status::Loading => element.child(Label::new("Project index loading...")),
-            Status::Scanning { remaining_count } => element.child(Label::new(format!(
-                "Project index scanning: {remaining_count} remaining..."
-            ))),
-        })
+        let is_enabled = match status {
+            Status::Idle => true,
+            _ => false,
+        };
+
+        let icon = match status {
+            Status::Idle => Icon::new(IconName::Code)
+                .size(IconSize::XSmall)
+                .color(Color::Default),
+            Status::Loading => Icon::new(IconName::Code)
+                .size(IconSize::XSmall)
+                .color(Color::Muted),
+            Status::Scanning { .. } => Icon::new(IconName::Code)
+                .size(IconSize::XSmall)
+                .color(Color::Muted),
+        };
+
+        let indicator = match status {
+            Status::Idle => Some(Indicator::dot().color(Color::Success)),
+            Status::Scanning { .. } => Some(Indicator::dot().color(Color::Warning)),
+            Status::Loading => Some(Indicator::icon(
+                Icon::new(IconName::Spinner)
+                    .color(Color::Accent)
+                    .with_animation(
+                        "arrow-circle",
+                        Animation::new(Duration::from_secs(2)).repeat(),
+                        |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
+                    ),
+            )),
+        };
+
+        ButtonLike::new("project-index")
+            .disabled(!is_enabled)
+            .child(
+                ui::IconWithIndicator::new(icon, indicator)
+                    .indicator_border_color(Some(gpui::transparent_black())),
+            )
+            .tooltip({
+                move |cx| {
+                    let (tooltip, meta) = match status {
+                        Status::Idle => (
+                            "Project index ready".to_string(),
+                            Some("Click to disable".to_string()),
+                        ),
+                        Status::Loading => ("Project index loading...".to_string(), None),
+                        Status::Scanning { remaining_count } => (
+                            "Project index scanning...".to_string(),
+                            Some(format!("{} remaining...", remaining_count)),
+                        ),
+                    };
+
+                    if let Some(meta) = meta {
+                        Tooltip::with_meta(tooltip, None, meta, cx)
+                    } else {
+                        Tooltip::text(tooltip, cx)
+                    }
+                }
+            })
     }
 }

crates/assistant2/src/ui/composer.rs πŸ”—

@@ -7,13 +7,12 @@ use std::sync::Arc;
 use theme::ThemeSettings;
 use ui::{popover_menu, prelude::*, Avatar, ButtonLike, ContextMenu, Tooltip};
 
-use crate::{AssistantChat, CompletionProvider, Submit, SubmitMode};
+use crate::{AssistantChat, CompletionProvider};
 
 #[derive(IntoElement)]
 pub struct Composer {
     editor: View<Editor>,
     player: Option<Arc<User>>,
-    can_submit: bool,
     tool_registry: Arc<ToolRegistry>,
     model_selector: AnyElement,
 }
@@ -22,14 +21,12 @@ impl Composer {
     pub fn new(
         editor: View<Editor>,
         player: Option<Arc<User>>,
-        can_submit: bool,
         tool_registry: Arc<ToolRegistry>,
         model_selector: AnyElement,
     ) -> Self {
         Self {
             editor,
             player,
-            can_submit,
             tool_registry,
             model_selector,
         }
@@ -55,100 +52,56 @@ impl RenderOnce for Composer {
             .gap_3()
             .child(player_avatar)
             .child(
-                v_flex()
-                    .size_full()
-                    .gap_1()
-                    .pr_4()
-                    .child(
-                        v_flex()
-                            .w_full()
-                            .p_4()
-                            .bg(cx.theme().colors().editor_background)
-                            .rounded_lg()
-                            .child(
-                                v_flex()
-                                    .justify_between()
-                                    .w_full()
-                                    .gap_2()
-                                    .child({
-                                        let settings = ThemeSettings::get_global(cx);
-                                        let text_style = TextStyle {
-                                            color: cx.theme().colors().editor_foreground,
-                                            font_family: settings.buffer_font.family.clone(),
-                                            font_features: settings.buffer_font.features.clone(),
-                                            font_size: font_size.into(),
-                                            font_weight: FontWeight::NORMAL,
-                                            font_style: FontStyle::Normal,
-                                            line_height: line_height.into(),
-                                            background_color: None,
-                                            underline: None,
-                                            strikethrough: None,
-                                            white_space: WhiteSpace::Normal,
-                                        };
+                v_flex().size_full().gap_1().child(
+                    v_flex()
+                        .w_full()
+                        .p_4()
+                        .bg(cx.theme().colors().editor_background)
+                        .rounded_lg()
+                        .child(
+                            v_flex()
+                                .justify_between()
+                                .w_full()
+                                .gap_2()
+                                .child({
+                                    let settings = ThemeSettings::get_global(cx);
+                                    let text_style = TextStyle {
+                                        color: cx.theme().colors().editor_foreground,
+                                        font_family: settings.buffer_font.family.clone(),
+                                        font_features: settings.buffer_font.features.clone(),
+                                        font_size: font_size.into(),
+                                        font_weight: FontWeight::NORMAL,
+                                        font_style: FontStyle::Normal,
+                                        line_height: line_height.into(),
+                                        background_color: None,
+                                        underline: None,
+                                        strikethrough: None,
+                                        white_space: WhiteSpace::Normal,
+                                    };
 
-                                        EditorElement::new(
-                                            &self.editor,
-                                            EditorStyle {
-                                                background: cx.theme().colors().editor_background,
-                                                local_player: cx.theme().players().local(),
-                                                text: text_style,
-                                                ..Default::default()
-                                            },
-                                        )
-                                    })
-                                    .child(
-                                        h_flex()
-                                            .flex_none()
-                                            .gap_2()
-                                            .justify_between()
-                                            .w_full()
-                                            .child(
-                                                h_flex().gap_1().child(
-                                                    // IconButton/button
-                                                    // Toggle - if enabled, .selected(true).selected_style(IconButtonStyle::Filled)
-                                                    //
-                                                    // match status
-                                                    // Tooltip::with_meta("some label explaining project index + status", "click to enable")
-                                                    IconButton::new(
-                                                        "add-context",
-                                                        IconName::FileDoc,
-                                                    )
-                                                    .icon_color(Color::Muted),
-                                                ), // .child(
-                                                   //     IconButton::new(
-                                                   //         "add-context",
-                                                   //         IconName::Plus,
-                                                   //     )
-                                                   //     .icon_color(Color::Muted),
-                                                   // ),
-                                            )
-                                            .child(
-                                                Button::new("send-button", "Send")
-                                                    .style(ButtonStyle::Filled)
-                                                    .disabled(!self.can_submit)
-                                                    .on_click(|_, cx| {
-                                                        cx.dispatch_action(Box::new(Submit(
-                                                            SubmitMode::Codebase,
-                                                        )))
-                                                    })
-                                                    .tooltip(|cx| {
-                                                        Tooltip::for_action(
-                                                            "Submit message",
-                                                            &Submit(SubmitMode::Codebase),
-                                                            cx,
-                                                        )
-                                                    }),
-                                            ),
-                                    ),
-                            ),
-                    )
-                    .child(
-                        h_flex()
-                            .w_full()
-                            .justify_between()
-                            .child(self.model_selector)
-                            .children(self.tool_registry.status_views().iter().cloned()),
-                    ),
+                                    EditorElement::new(
+                                        &self.editor,
+                                        EditorStyle {
+                                            background: cx.theme().colors().editor_background,
+                                            local_player: cx.theme().players().local(),
+                                            text: text_style,
+                                            ..Default::default()
+                                        },
+                                    )
+                                })
+                                .child(
+                                    h_flex()
+                                        .flex_none()
+                                        .gap_2()
+                                        .justify_between()
+                                        .w_full()
+                                        .child(h_flex().gap_1().children(
+                                            self.tool_registry.status_views().iter().cloned(),
+                                        ))
+                                        .child(h_flex().gap_1().child(self.model_selector)),
+                                ),
+                        ),
+                ),
             )
     }
 }
@@ -205,10 +158,18 @@ impl RenderOnce for ModelSelector {
                                     .overflow_x_hidden()
                                     .flex_grow()
                                     .whitespace_nowrap()
-                                    .child(Label::new(self.model)),
+                                    .child(
+                                        Label::new(self.model)
+                                            .size(LabelSize::Small)
+                                            .color(Color::Muted),
+                                    ),
                             )
                             .child(
-                                div().child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
+                                div().child(
+                                    Icon::new(IconName::ChevronDown)
+                                        .color(Color::Muted)
+                                        .size(IconSize::XSmall),
+                                ),
                             ),
                     )
                     .style(ButtonStyle::Subtle)

crates/ui/src/components/icon.rs πŸ”—

@@ -87,6 +87,7 @@ pub enum IconName {
     ChevronUp,
     ExpandVertical,
     Close,
+    Code,
     Collab,
     Command,
     Control,
@@ -153,6 +154,7 @@ pub enum IconName {
     Snip,
     Space,
     Split,
+    Spinner,
     Tab,
     Terminal,
     Trash,
@@ -191,6 +193,7 @@ impl IconName {
             IconName::ChevronUp => "icons/chevron_up.svg",
             IconName::ExpandVertical => "icons/expand_vertical.svg",
             IconName::Close => "icons/x.svg",
+            IconName::Code => "icons/code.svg",
             IconName::Collab => "icons/user_group_16.svg",
             IconName::Command => "icons/command.svg",
             IconName::Control => "icons/control.svg",
@@ -257,6 +260,7 @@ impl IconName {
             IconName::Snip => "icons/snip.svg",
             IconName::Space => "icons/space.svg",
             IconName::Split => "icons/split.svg",
+            IconName::Spinner => "icons/spinner.svg",
             IconName::Tab => "icons/tab.svg",
             IconName::Terminal => "icons/terminal.svg",
             IconName::Trash => "icons/trash.svg",