assistant2: Add design refinements (#27160)

Danilo Leal , Bennet Bo Fenner , Antonio Scandurra , and Agus Zubiaga created

Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Agus Zubiaga <hi@aguz.me>

Change summary

Cargo.lock                              |   1 
assets/icons/arrow_up_right_alt.svg     |   3 
assets/icons/brain.svg                  |   1 
assets/keymaps/default-macos.json       |   4 
crates/assistant2/Cargo.toml            |   1 
crates/assistant2/src/active_thread.rs  | 383 +++++++++++++++++++-------
crates/assistant2/src/message_editor.rs | 327 +++++++++++++----------
crates/assistant2/src/thread.rs         |  27 +
crates/git_ui/src/git_panel.rs          |   2 
crates/ui/src/components/disclosure.rs  |  18 +
crates/ui/src/components/icon.rs        |   2 
11 files changed, 521 insertions(+), 248 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -495,6 +495,7 @@ dependencies = [
  "serde",
  "serde_json",
  "settings",
+ "smallvec",
  "smol",
  "streaming_diff",
  "telemetry",

assets/icons/arrow_up_right_alt.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5.4 2.6H5.75C5.75 2.50717 5.71312 2.41815 5.64749 2.35251C5.58185 2.28688 5.49283 2.25 5.4 2.25V2.6ZM2.6 2.25C2.4067 2.25 2.25 2.4067 2.25 2.6C2.25 2.7933 2.4067 2.95 2.6 2.95V2.25ZM5.05 5.4C5.05 5.5933 5.2067 5.75 5.4 5.75C5.5933 5.75 5.75 5.5933 5.75 5.4H5.05ZM2.35252 5.15251C2.21583 5.2892 2.21583 5.5108 2.35252 5.64748C2.4892 5.78417 2.7108 5.78417 2.84749 5.64748L2.35252 5.15251ZM5.4 2.25H2.6V2.95H5.4V2.25ZM5.05 2.6V5.4H5.75V2.6H5.05ZM5.15252 2.35251L2.35252 5.15251L2.84749 5.64748L5.64749 2.84748L5.15252 2.35251Z" fill="black"/>
+</svg>

assets/icons/brain.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-brain"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M17.599 6.5a3 3 0 0 0 .399-1.375"/><path d="M6.003 5.125A3 3 0 0 0 6.401 6.5"/><path d="M3.477 10.896a4 4 0 0 1 .585-.396"/><path d="M19.938 10.5a4 4 0 0 1 .585.396"/><path d="M6 18a4 4 0 0 1-1.967-.516"/><path d="M19.967 17.484A4 4 0 0 1 18 18"/></svg>

assets/keymaps/default-macos.json 🔗

@@ -287,7 +287,9 @@
     "context": "MessageEditor > Editor",
     "use_key_equivalents": true,
     "bindings": {
-      "enter": "assistant2::Chat"
+      "enter": "assistant2::Chat",
+      "cmd-g d": "git::Diff",
+      "shift-escape": "git::ExpandCommitEditor"
     }
   },
   {

crates/assistant2/Cargo.toml 🔗

@@ -65,6 +65,7 @@ scripting_tool.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true
+smallvec.workspace = true
 smol.workspace = true
 streaming_diff.workspace = true
 telemetry.workspace = true

crates/assistant2/src/active_thread.rs 🔗

@@ -1,5 +1,5 @@
 use crate::thread::{
-    LastRestoreCheckpoint, MessageId, RequestKind, Thread, ThreadError, ThreadEvent,
+    LastRestoreCheckpoint, MessageId, RequestKind, Thread, ThreadError, ThreadEvent, ThreadFeedback,
 };
 use crate::thread_store::ThreadStore;
 use crate::tool_use::{ToolUse, ToolUseStatus};
@@ -20,7 +20,7 @@ use settings::Settings as _;
 use std::sync::Arc;
 use std::time::Duration;
 use theme::ThemeSettings;
-use ui::{prelude::*, Disclosure, KeyBinding, Tooltip};
+use ui::{prelude::*, Disclosure, IconButton, KeyBinding, Tooltip};
 use util::ResultExt as _;
 use workspace::{OpenOptions, Workspace};
 
@@ -593,6 +593,24 @@ impl ActiveThread {
         self.confirm_editing_message(&menu::Confirm, window, cx);
     }
 
+    fn handle_feedback_click(
+        &mut self,
+        feedback: ThreadFeedback,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let report = self
+            .thread
+            .update(cx, |thread, cx| thread.report_feedback(feedback, cx));
+
+        let this = cx.entity().downgrade();
+        cx.spawn(async move |_, cx| {
+            report.await?;
+            this.update(cx, |_this, cx| cx.notify())
+        })
+        .detach_and_log_err(cx);
+    }
+
     fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
         let message_id = self.messages[ix];
         let Some(message) = self.thread.read(cx).message(message_id) else {
@@ -627,25 +645,127 @@ impl ActiveThread {
             .filter(|(id, _)| *id == message_id)
             .map(|(_, state)| state.editor.clone());
 
+        let first_message = ix == 0;
+        let is_last_message = ix == self.messages.len() - 1;
+
         let colors = cx.theme().colors();
+        let active_color = colors.element_active;
+        let editor_bg_color = colors.editor_background;
+        let bg_user_message_header = editor_bg_color.blend(active_color.opacity(0.25));
+
+        let feedback_container = h_flex().pb_4().px_4().gap_1().justify_between();
+        let feedback_items = match self.thread.read(cx).feedback() {
+            Some(feedback) => feedback_container
+                .child(
+                    Label::new(match feedback {
+                        ThreadFeedback::Positive => "Thanks for your feedback!",
+                        ThreadFeedback::Negative => {
+                            "We appreciate your feedback and will use it to improve."
+                        }
+                    })
+                    .color(Color::Muted)
+                    .size(LabelSize::XSmall),
+                )
+                .child(
+                    h_flex()
+                        .gap_1()
+                        .child(
+                            IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
+                                .icon_size(IconSize::XSmall)
+                                .icon_color(match feedback {
+                                    ThreadFeedback::Positive => Color::Accent,
+                                    ThreadFeedback::Negative => Color::Ignored,
+                                })
+                                .shape(ui::IconButtonShape::Square)
+                                .tooltip(Tooltip::text("Helpful Response"))
+                                .on_click(cx.listener(move |this, _, window, cx| {
+                                    this.handle_feedback_click(
+                                        ThreadFeedback::Positive,
+                                        window,
+                                        cx,
+                                    );
+                                })),
+                        )
+                        .child(
+                            IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
+                                .icon_size(IconSize::XSmall)
+                                .icon_color(match feedback {
+                                    ThreadFeedback::Positive => Color::Ignored,
+                                    ThreadFeedback::Negative => Color::Accent,
+                                })
+                                .shape(ui::IconButtonShape::Square)
+                                .tooltip(Tooltip::text("Not Helpful"))
+                                .on_click(cx.listener(move |this, _, window, cx| {
+                                    this.handle_feedback_click(
+                                        ThreadFeedback::Negative,
+                                        window,
+                                        cx,
+                                    );
+                                })),
+                        ),
+                )
+                .into_any_element(),
+            None => feedback_container
+                .child(
+                    Label::new(
+                        "Rating the thread sends all of your current conversation to the Zed team.",
+                    )
+                    .color(Color::Muted)
+                    .size(LabelSize::XSmall),
+                )
+                .child(
+                    h_flex()
+                        .gap_1()
+                        .child(
+                            IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
+                                .icon_size(IconSize::XSmall)
+                                .icon_color(Color::Ignored)
+                                .shape(ui::IconButtonShape::Square)
+                                .tooltip(Tooltip::text("Helpful Response"))
+                                .on_click(cx.listener(move |this, _, window, cx| {
+                                    this.handle_feedback_click(
+                                        ThreadFeedback::Positive,
+                                        window,
+                                        cx,
+                                    );
+                                })),
+                        )
+                        .child(
+                            IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
+                                .icon_size(IconSize::XSmall)
+                                .icon_color(Color::Ignored)
+                                .shape(ui::IconButtonShape::Square)
+                                .tooltip(Tooltip::text("Not Helpful"))
+                                .on_click(cx.listener(move |this, _, window, cx| {
+                                    this.handle_feedback_click(
+                                        ThreadFeedback::Negative,
+                                        window,
+                                        cx,
+                                    );
+                                })),
+                        ),
+                )
+                .into_any_element(),
+        };
 
         let message_content = v_flex()
+            .gap_1p5()
             .child(
                 if let Some(edit_message_editor) = edit_message_editor.clone() {
                     div()
                         .key_context("EditMessageEditor")
                         .on_action(cx.listener(Self::cancel_editing_message))
                         .on_action(cx.listener(Self::confirm_editing_message))
-                        .p_2p5()
+                        .min_h_6()
                         .child(edit_message_editor)
                 } else {
-                    div().text_ui(cx).child(markdown.clone())
+                    div().min_h_6().text_ui(cx).child(markdown.clone())
                 },
             )
             .when_some(context, |parent, context| {
                 if !context.is_empty() {
                     parent.child(
-                        h_flex().flex_wrap().gap_1().px_1p5().pb_1p5().children(
+                        h_flex().flex_wrap().gap_1().children(
                             context
                                 .into_iter()
                                 .map(|context| ContextPill::added(context, false, false, None)),
@@ -659,7 +779,7 @@ impl ActiveThread {
         let styled_message = match message.role {
             Role::User => v_flex()
                 .id(("message-container", ix))
-                .pt_2()
+                .py_2()
                 .pl_2()
                 .pr_2p5()
                 .child(
@@ -674,11 +794,11 @@ impl ActiveThread {
                                 .py_1()
                                 .pl_2()
                                 .pr_1()
-                                .bg(colors.editor_foreground.opacity(0.05))
+                                .bg(bg_user_message_header)
                                 .border_b_1()
                                 .border_color(colors.border)
                                 .justify_between()
-                                .rounded_t(px(6.))
+                                .rounded_t_md()
                                 .child(
                                     h_flex()
                                         .gap_1p5()
@@ -693,14 +813,19 @@ impl ActiveThread {
                                                 .color(Color::Muted),
                                         ),
                                 )
-                                .when_some(
-                                    edit_message_editor.clone(),
-                                    |this, edit_message_editor| {
-                                        let focus_handle = edit_message_editor.focus_handle(cx);
-                                        this.child(
-                                            h_flex()
-                                                .gap_1()
-                                                .child(
+                                .child(
+                                    h_flex()
+                                        // DL: To double-check whether we want to fully remove
+                                        // the editing feature from meassages. Checkpoint sort of
+                                        // solve the same problem.
+                                        .invisible()
+                                        .gap_1()
+                                        .when_some(
+                                            edit_message_editor.clone(),
+                                            |this, edit_message_editor| {
+                                                let focus_handle =
+                                                    edit_message_editor.focus_handle(cx);
+                                                this.child(
                                                     Button::new("cancel-edit-message", "Cancel")
                                                         .label_size(LabelSize::Small)
                                                         .key_binding(
@@ -734,36 +859,36 @@ impl ActiveThread {
                                                     .on_click(
                                                         cx.listener(Self::handle_regenerate_click),
                                                     ),
-                                                ),
-                                        )
-                                    },
-                                )
-                                .when(
-                                    edit_message_editor.is_none() && allow_editing_message,
-                                    |this| {
-                                        this.child(
-                                            Button::new("edit-message", "Edit")
-                                                .label_size(LabelSize::Small)
-                                                .on_click(cx.listener({
-                                                    let message_text = message.text.clone();
-                                                    move |this, _, window, cx| {
-                                                        this.start_editing_message(
-                                                            message_id,
-                                                            message_text.clone(),
-                                                            window,
-                                                            cx,
-                                                        );
-                                                    }
-                                                })),
+                                                )
+                                            },
                                         )
-                                    },
+                                        .when(
+                                            edit_message_editor.is_none() && allow_editing_message,
+                                            |this| {
+                                                this.child(
+                                                    Button::new("edit-message", "Edit")
+                                                        .label_size(LabelSize::Small)
+                                                        .on_click(cx.listener({
+                                                            let message_text = message.text.clone();
+                                                            move |this, _, window, cx| {
+                                                                this.start_editing_message(
+                                                                    message_id,
+                                                                    message_text.clone(),
+                                                                    window,
+                                                                    cx,
+                                                                );
+                                                            }
+                                                        })),
+                                                )
+                                            },
+                                        ),
                                 ),
                         )
                         .child(div().p_2().child(message_content)),
                 ),
             Role::Assistant => v_flex()
                 .id(("message-container", ix))
-                .child(div().py_3().px_4().child(message_content))
+                .child(v_flex().py_2().px_4().child(message_content))
                 .when(
                     !tool_uses.is_empty() || !scripting_tool_uses.is_empty(),
                     |parent| {
@@ -789,8 +914,12 @@ impl ActiveThread {
         };
 
         v_flex()
-            .when(ix == 0, |parent| parent.child(self.render_rules_item(cx)))
-            .when_some(checkpoint, |parent, checkpoint| {
+            .w_full()
+            .when(first_message, |parent| {
+                parent.child(self.render_rules_item(cx))
+            })
+            .when(!first_message && checkpoint.is_some(), |parent| {
+                let checkpoint = checkpoint.clone().unwrap();
                 let mut is_pending = false;
                 let mut error = None;
                 if let Some(last_restore_checkpoint) =
@@ -813,13 +942,15 @@ impl ActiveThread {
                         } else {
                             IconName::Undo
                         })
-                        .size(ButtonSize::Compact)
-                        .disabled(is_pending)
+                        .icon_size(IconSize::XSmall)
+                        .icon_position(IconPosition::Start)
                         .icon_color(if error.is_some() {
                             Some(Color::Error)
                         } else {
                             None
                         })
+                        .label_size(LabelSize::XSmall)
+                        .disabled(is_pending)
                         .on_click(cx.listener(move |this, _, _window, cx| {
                             this.thread.update(cx, |thread, cx| {
                                 thread
@@ -846,9 +977,21 @@ impl ActiveThread {
                     restore_checkpoint_button.into_any_element()
                 };
 
-                parent.child(h_flex().pl_2().child(restore_checkpoint_button))
+                parent.child(
+                    h_flex()
+                        .px_2p5()
+                        .w_full()
+                        .gap_1()
+                        .child(ui::Divider::horizontal())
+                        .child(restore_checkpoint_button)
+                        .child(ui::Divider::horizontal()),
+                )
             })
             .child(styled_message)
+            .when(
+                is_last_message && !self.thread.read(cx).is_generating(),
+                |parent| parent.child(feedback_items),
+            )
             .into_any()
     }
 
@@ -861,17 +1004,33 @@ impl ActiveThread {
 
         let lighter_border = cx.theme().colors().border.opacity(0.5);
 
+        let tool_icon = match tool_use.name.as_ref() {
+            "bash" => IconName::Terminal,
+            "delete-path" => IconName::Trash,
+            "diagnostics" => IconName::Warning,
+            "edit-files" => IconName::Pencil,
+            "fetch" => IconName::Globe,
+            "list-directory" => IconName::Folder,
+            "now" => IconName::Info,
+            "path-search" => IconName::SearchCode,
+            "read-file" => IconName::Eye,
+            "regex-search" => IconName::Regex,
+            "thinking" => IconName::Brain,
+            _ => IconName::Terminal,
+        };
+
         div().px_4().child(
             v_flex()
                 .rounded_lg()
                 .border_1()
                 .border_color(lighter_border)
+                .overflow_hidden()
                 .child(
                     h_flex()
+                        .group("disclosure-header")
                         .justify_between()
                         .py_1()
-                        .pl_1()
-                        .pr_2()
+                        .px_2()
                         .bg(cx.theme().colors().editor_foreground.opacity(0.025))
                         .map(|element| {
                             if is_open {
@@ -883,54 +1042,79 @@ impl ActiveThread {
                         .border_color(lighter_border)
                         .child(
                             h_flex()
-                                .gap_1()
-                                .child(Disclosure::new("tool-use-disclosure", is_open).on_click(
-                                    cx.listener({
-                                        let tool_use_id = tool_use.id.clone();
-                                        move |this, _event, _window, _cx| {
-                                            let is_open = this
-                                                .expanded_tool_uses
-                                                .entry(tool_use_id.clone())
-                                                .or_insert(false);
-
-                                            *is_open = !*is_open;
-                                        }
-                                    }),
-                                ))
-                                .child(div().text_ui_sm(cx).children(
-                                    self.rendered_tool_use_labels.get(&tool_use.id).cloned(),
-                                ))
-                                .truncate(),
+                                .gap_1p5()
+                                .child(
+                                    Icon::new(tool_icon)
+                                        .size(IconSize::XSmall)
+                                        .color(Color::Muted),
+                                )
+                                .child(
+                                    div()
+                                        .text_ui_sm(cx)
+                                        .children(
+                                            self.rendered_tool_use_labels
+                                                .get(&tool_use.id)
+                                                .cloned(),
+                                        )
+                                        .truncate(),
+                                ),
                         )
-                        .child({
-                            let (icon_name, color, animated) = match &tool_use.status {
-                                ToolUseStatus::Pending => {
-                                    (IconName::Warning, Color::Warning, false)
-                                }
-                                ToolUseStatus::Running => {
-                                    (IconName::ArrowCircle, Color::Accent, true)
-                                }
-                                ToolUseStatus::Finished(_) => {
-                                    (IconName::Check, Color::Success, false)
-                                }
-                                ToolUseStatus::Error(_) => (IconName::Close, Color::Error, false),
-                            };
-
-                            let icon = Icon::new(icon_name).color(color).size(IconSize::Small);
-
-                            if animated {
-                                icon.with_animation(
-                                    "arrow-circle",
-                                    Animation::new(Duration::from_secs(2)).repeat(),
-                                    |icon, delta| {
-                                        icon.transform(Transformation::rotate(percentage(delta)))
-                                    },
+                        .child(
+                            h_flex()
+                                .gap_1()
+                                .child(
+                                    div().visible_on_hover("disclosure-header").child(
+                                        Disclosure::new("tool-use-disclosure", is_open)
+                                            .opened_icon(IconName::ChevronUp)
+                                            .closed_icon(IconName::ChevronDown)
+                                            .on_click(cx.listener({
+                                                let tool_use_id = tool_use.id.clone();
+                                                move |this, _event, _window, _cx| {
+                                                    let is_open = this
+                                                        .expanded_tool_uses
+                                                        .entry(tool_use_id.clone())
+                                                        .or_insert(false);
+
+                                                    *is_open = !*is_open;
+                                                }
+                                            })),
+                                    ),
                                 )
-                                .into_any_element()
-                            } else {
-                                icon.into_any_element()
-                            }
-                        }),
+                                .child({
+                                    let (icon_name, color, animated) = match &tool_use.status {
+                                        ToolUseStatus::Pending => {
+                                            (IconName::Warning, Color::Warning, false)
+                                        }
+                                        ToolUseStatus::Running => {
+                                            (IconName::ArrowCircle, Color::Accent, true)
+                                        }
+                                        ToolUseStatus::Finished(_) => {
+                                            (IconName::Check, Color::Success, false)
+                                        }
+                                        ToolUseStatus::Error(_) => {
+                                            (IconName::Close, Color::Error, false)
+                                        }
+                                    };
+
+                                    let icon =
+                                        Icon::new(icon_name).color(color).size(IconSize::Small);
+
+                                    if animated {
+                                        icon.with_animation(
+                                            "arrow-circle",
+                                            Animation::new(Duration::from_secs(2)).repeat(),
+                                            |icon, delta| {
+                                                icon.transform(Transformation::rotate(percentage(
+                                                    delta,
+                                                )))
+                                            },
+                                        )
+                                        .into_any_element()
+                                    } else {
+                                        icon.into_any_element()
+                                    }
+                                }),
+                        ),
                 )
                 .map(|parent| {
                     if !is_open {
@@ -1171,10 +1355,8 @@ impl ActiveThread {
             .px_2p5()
             .child(
                 h_flex()
-                    .group("rules-item")
                     .w_full()
-                    .gap_2()
-                    .justify_between()
+                    .gap_0p5()
                     .child(
                         h_flex()
                             .gap_1p5()
@@ -1191,11 +1373,12 @@ impl ActiveThread {
                             ),
                     )
                     .child(
-                        div().visible_on_hover("rules-item").child(
-                            Button::new("open-rules", "Open Rules")
-                                .label_size(LabelSize::XSmall)
-                                .on_click(cx.listener(Self::handle_open_rules)),
-                        ),
+                        IconButton::new("open-rule", IconName::ArrowUpRightAlt)
+                            .shape(ui::IconButtonShape::Square)
+                            .icon_size(IconSize::XSmall)
+                            .icon_color(Color::Ignored)
+                            .on_click(cx.listener(Self::handle_open_rules))
+                            .tooltip(Tooltip::text("View Rules")),
                     ),
             )
             .into_any()

crates/assistant2/src/message_editor.rs 🔗

@@ -7,7 +7,7 @@ use fs::Fs;
 use git::ExpandCommitEditor;
 use git_ui::git_panel;
 use gpui::{
-    Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
+    point, Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
     WeakEntity,
 };
 use language_model::LanguageModelRegistry;
@@ -23,8 +23,7 @@ use ui::{
 };
 use util::ResultExt;
 use vim_mode_setting::VimModeSetting;
-use workspace::notifications::{NotificationId, NotifyTaskExt};
-use workspace::{Toast, Workspace};
+use workspace::Workspace;
 
 use crate::assistant_model_selector::AssistantModelSelector;
 use crate::context_picker::{ConfirmBehavior, ContextPicker};
@@ -38,6 +37,7 @@ use crate::{Chat, ChatMode, RemoveAllContext, ThreadEvent, ToggleContextPicker};
 pub struct MessageEditor {
     thread: Entity<Thread>,
     editor: Entity<Editor>,
+    #[allow(dead_code)]
     workspace: WeakEntity<Workspace>,
     project: Entity<Project>,
     context_store: Entity<ContextStore>,
@@ -144,7 +144,6 @@ impl MessageEditor {
     ) {
         self.context_picker_menu_handle.toggle(window, cx);
     }
-
     pub fn remove_all_context(
         &mut self,
         _: &RemoveAllContext,
@@ -301,34 +300,6 @@ impl MessageEditor {
             self.context_strip.focus_handle(cx).focus(window);
         }
     }
-
-    fn handle_feedback_click(
-        &mut self,
-        is_positive: bool,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        let workspace = self.workspace.clone();
-        let report = self
-            .thread
-            .update(cx, |thread, cx| thread.report_feedback(is_positive, cx));
-
-        cx.spawn(async move |_, cx| {
-            report.await?;
-            workspace.update(cx, |workspace, cx| {
-                let message = if is_positive {
-                    "Positive feedback recorded. Thank you!"
-                } else {
-                    "Negative feedback recorded. Thank you for helping us improve!"
-                };
-
-                struct ThreadFeedback;
-                let id = NotificationId::unique::<ThreadFeedback>();
-                workspace.show_toast(Toast::new(id, message).autohide(), cx)
-            })
-        })
-        .detach_and_notify_err(window, cx);
-    }
 }
 
 impl Focusable for MessageEditor {
@@ -341,9 +312,11 @@ impl Render for MessageEditor {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let font_size = TextSize::Default.rems(cx);
         let line_height = font_size.to_pixels(window.rem_size()) * 1.5;
+
         let focus_handle = self.editor.focus_handle(cx);
         let inline_context_picker = self.inline_context_picker.clone();
-        let bg_color = cx.theme().colors().editor_background;
+
+        let empty_thread = self.thread.read(cx).is_empty();
         let is_generating = self.thread.read(cx).is_generating();
         let is_model_selected = self.is_model_selected(cx);
         let is_editor_empty = self.is_editor_empty(cx);
@@ -370,6 +343,24 @@ impl Render for MessageEditor {
             0
         };
 
+        let border_color = cx.theme().colors().border;
+        let active_color = cx.theme().colors().element_selected;
+        let editor_bg_color = cx.theme().colors().editor_background;
+        let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
+
+        let edit_files_container = || {
+            h_flex()
+                .mx_2()
+                .py_1()
+                .pl_2p5()
+                .pr_1()
+                .bg(bg_edit_files_disclosure)
+                .border_1()
+                .border_color(border_color)
+                .justify_between()
+                .flex_wrap()
+        };
+
         v_flex()
             .size_full()
             .when(is_generating, |parent| {
@@ -381,7 +372,7 @@ impl Render for MessageEditor {
                             .pl_2()
                             .pr_1()
                             .py_1()
-                            .bg(cx.theme().colors().editor_background)
+                            .bg(editor_bg_color)
                             .border_1()
                             .border_color(cx.theme().colors().border_variant)
                             .rounded_lg()
@@ -430,73 +421,163 @@ impl Render for MessageEditor {
                     ),
                 )
             })
-            .when(changed_files > 0, |parent| {
-                parent.child(
-                    v_flex()
-                        .mx_2()
-                        .bg(cx.theme().colors().element_background)
-                        .border_1()
-                        .border_b_0()
-                        .border_color(cx.theme().colors().border)
-                        .rounded_t_md()
-                        .child(
-                            h_flex()
-                                .justify_between()
-                                .p_2()
-                                .child(
-                                    h_flex()
-                                        .gap_2()
-                                        .child(
-                                            IconButton::new(
-                                                "edits-disclosure",
-                                                IconName::GitBranchSmall,
-                                            )
-                                            .icon_size(IconSize::Small)
-                                            .on_click(
-                                                |_ev, _window, cx| {
-                                                    cx.defer(|cx| {
-                                                        cx.dispatch_action(&git_panel::ToggleFocus)
-                                                    });
-                                                },
-                                            ),
-                                        )
-                                        .child(
-                                            Label::new(format!(
-                                                "{} {} changed",
-                                                changed_files,
-                                                if changed_files == 1 { "file" } else { "files" }
-                                            ))
-                                            .size(LabelSize::XSmall)
-                                            .color(Color::Muted),
-                                        ),
-                                )
-                                .child(
-                                    h_flex()
-                                        .gap_2()
-                                        .child(
-                                            Button::new("review", "Review")
-                                                .label_size(LabelSize::XSmall)
-                                                .on_click(|_event, _window, cx| {
-                                                    cx.defer(|cx| {
-                                                        cx.dispatch_action(
-                                                            &git_ui::project_diff::Diff,
-                                                        );
-                                                    });
-                                                }),
-                                        )
-                                        .child(
-                                            Button::new("commit", "Commit")
-                                                .label_size(LabelSize::XSmall)
-                                                .on_click(|_event, _window, cx| {
-                                                    cx.defer(|cx| {
-                                                        cx.dispatch_action(&ExpandCommitEditor)
-                                                    });
-                                                }),
-                                        ),
-                                ),
-                        ),
-                )
-            })
+            .when(
+                changed_files > 0 && !is_generating && !empty_thread,
+                |parent| {
+                    parent.child(
+                        edit_files_container()
+                            .border_b_0()
+                            .rounded_t_md()
+                            .shadow(smallvec::smallvec![gpui::BoxShadow {
+                                color: gpui::black().opacity(0.15),
+                                offset: point(px(1.), px(-1.)),
+                                blur_radius: px(3.),
+                                spread_radius: px(0.),
+                            }])
+                            .child(
+                                h_flex()
+                                    .gap_2()
+                                    .child(Label::new("Edits").size(LabelSize::XSmall))
+                                    .child(div().size_1().rounded_full().bg(border_color))
+                                    .child(
+                                        Label::new(format!(
+                                            "{} {}",
+                                            changed_files,
+                                            if changed_files == 1 { "file" } else { "files" }
+                                        ))
+                                        .size(LabelSize::XSmall),
+                                    ),
+                            )
+                            .child(
+                                h_flex()
+                                    .gap_1()
+                                    .child(
+                                        Button::new("panel", "Open Git Panel")
+                                            .label_size(LabelSize::XSmall)
+                                            .key_binding({
+                                                let focus_handle = focus_handle.clone();
+                                                KeyBinding::for_action_in(
+                                                    &git_panel::ToggleFocus,
+                                                    &focus_handle,
+                                                    window,
+                                                    cx,
+                                                )
+                                                .map(|kb| kb.size(rems_from_px(10.)))
+                                            })
+                                            .on_click(|_ev, _window, cx| {
+                                                cx.defer(|cx| {
+                                                    cx.dispatch_action(&git_panel::ToggleFocus)
+                                                });
+                                            }),
+                                    )
+                                    .child(
+                                        Button::new("review", "Review Diff")
+                                            .label_size(LabelSize::XSmall)
+                                            .key_binding({
+                                                let focus_handle = focus_handle.clone();
+                                                KeyBinding::for_action_in(
+                                                    &git_ui::project_diff::Diff,
+                                                    &focus_handle,
+                                                    window,
+                                                    cx,
+                                                )
+                                                .map(|kb| kb.size(rems_from_px(10.)))
+                                            })
+                                            .on_click(|_event, _window, cx| {
+                                                cx.defer(|cx| {
+                                                    cx.dispatch_action(&git_ui::project_diff::Diff)
+                                                });
+                                            }),
+                                    )
+                                    .child(
+                                        Button::new("commit", "Commit Changes")
+                                            .label_size(LabelSize::XSmall)
+                                            .key_binding({
+                                                let focus_handle = focus_handle.clone();
+                                                KeyBinding::for_action_in(
+                                                    &ExpandCommitEditor,
+                                                    &focus_handle,
+                                                    window,
+                                                    cx,
+                                                )
+                                                .map(|kb| kb.size(rems_from_px(10.)))
+                                            })
+                                            .on_click(|_event, _window, cx| {
+                                                cx.defer(|cx| {
+                                                    cx.dispatch_action(&ExpandCommitEditor)
+                                                });
+                                            }),
+                                    ),
+                            ),
+                    )
+                },
+            )
+            .when(
+                changed_files > 0 && !is_generating && empty_thread,
+                |parent| {
+                    parent.child(
+                        edit_files_container()
+                            .mb_2()
+                            .rounded_md()
+                            .child(
+                                h_flex()
+                                    .gap_2()
+                                    .child(Label::new("Consider committing your changes before starting a fresh thread").size(LabelSize::XSmall))
+                                    .child(div().size_1().rounded_full().bg(border_color))
+                                    .child(
+                                        Label::new(format!(
+                                            "{} {}",
+                                            changed_files,
+                                            if changed_files == 1 { "file" } else { "files" }
+                                        ))
+                                        .size(LabelSize::XSmall),
+                                    ),
+                            )
+                            .child(
+                                h_flex()
+                                    .gap_1()
+                                    .child(
+                                        Button::new("review", "Review Diff")
+                                            .label_size(LabelSize::XSmall)
+                                            .key_binding({
+                                                let focus_handle = focus_handle.clone();
+                                                KeyBinding::for_action_in(
+                                                    &git_ui::project_diff::Diff,
+                                                    &focus_handle,
+                                                    window,
+                                                    cx,
+                                                )
+                                                .map(|kb| kb.size(rems_from_px(10.)))
+                                            })
+                                            .on_click(|_event, _window, cx| {
+                                                cx.defer(|cx| {
+                                                    cx.dispatch_action(&git_ui::project_diff::Diff)
+                                                });
+                                            }),
+                                    )
+                                    .child(
+                                        Button::new("commit", "Commit Changes")
+                                            .label_size(LabelSize::XSmall)
+                                            .key_binding({
+                                                let focus_handle = focus_handle.clone();
+                                                KeyBinding::for_action_in(
+                                                    &ExpandCommitEditor,
+                                                    &focus_handle,
+                                                    window,
+                                                    cx,
+                                                )
+                                                .map(|kb| kb.size(rems_from_px(10.)))
+                                            })
+                                            .on_click(|_event, _window, cx| {
+                                                cx.defer(|cx| {
+                                                    cx.dispatch_action(&ExpandCommitEditor)
+                                                });
+                                            }),
+                                    ),
+                            ),
+                    )
+                },
+            )
             .child(
                 v_flex()
                     .key_context("MessageEditor")
@@ -511,48 +592,10 @@ impl Render for MessageEditor {
                     .on_action(cx.listener(Self::toggle_chat_mode))
                     .gap_2()
                     .p_2()
-                    .bg(bg_color)
+                    .bg(editor_bg_color)
                     .border_t_1()
                     .border_color(cx.theme().colors().border)
-                    .child(
-                        h_flex()
-                            .justify_between()
-                            .child(self.context_strip.clone())
-                            .when(!self.thread.read(cx).is_empty(), |this| {
-                                this.child(
-                                    h_flex()
-                                        .gap_2()
-                                        .child(
-                                            IconButton::new(
-                                                "feedback-thumbs-up",
-                                                IconName::ThumbsUp,
-                                            )
-                                            .style(ButtonStyle::Subtle)
-                                            .icon_size(IconSize::Small)
-                                            .tooltip(Tooltip::text("Helpful"))
-                                            .on_click(
-                                                cx.listener(|this, _, window, cx| {
-                                                    this.handle_feedback_click(true, window, cx);
-                                                }),
-                                            ),
-                                        )
-                                        .child(
-                                            IconButton::new(
-                                                "feedback-thumbs-down",
-                                                IconName::ThumbsDown,
-                                            )
-                                            .style(ButtonStyle::Subtle)
-                                            .icon_size(IconSize::Small)
-                                            .tooltip(Tooltip::text("Not Helpful"))
-                                            .on_click(
-                                                cx.listener(|this, _, window, cx| {
-                                                    this.handle_feedback_click(false, window, cx);
-                                                }),
-                                            ),
-                                        ),
-                                )
-                            }),
-                    )
+                    .child(h_flex().justify_between().child(self.context_strip.clone()))
                     .child(
                         v_flex()
                             .gap_5()
@@ -572,7 +615,7 @@ impl Render for MessageEditor {
                                 EditorElement::new(
                                     &self.editor,
                                     EditorStyle {
-                                        background: bg_color,
+                                        background: editor_bg_color,
                                         local_player: cx.theme().players().local(),
                                         text: text_style,
                                         ..Default::default()

crates/assistant2/src/thread.rs 🔗

@@ -99,6 +99,12 @@ pub struct ThreadCheckpoint {
     git_checkpoint: GitStoreCheckpoint,
 }
 
+#[derive(Copy, Clone, Debug)]
+pub enum ThreadFeedback {
+    Positive,
+    Negative,
+}
+
 pub enum LastRestoreCheckpoint {
     Pending {
         message_id: MessageId,
@@ -142,6 +148,7 @@ pub struct Thread {
     scripting_tool_use: ToolUseState,
     initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>,
     cumulative_token_usage: TokenUsage,
+    feedback: Option<ThreadFeedback>,
 }
 
 impl Thread {
@@ -179,6 +186,7 @@ impl Thread {
                     .shared()
             },
             cumulative_token_usage: TokenUsage::default(),
+            feedback: None,
         }
     }
 
@@ -239,6 +247,7 @@ impl Thread {
             initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(),
             // TODO: persist token usage?
             cumulative_token_usage: TokenUsage::default(),
+            feedback: None,
         }
     }
 
@@ -1187,12 +1196,23 @@ impl Thread {
         }
     }
 
+    /// Returns the feedback given to the thread, if any.
+    pub fn feedback(&self) -> Option<ThreadFeedback> {
+        self.feedback
+    }
+
     /// Reports feedback about the thread and stores it in our telemetry backend.
-    pub fn report_feedback(&self, is_positive: bool, cx: &mut Context<Self>) -> Task<Result<()>> {
+    pub fn report_feedback(
+        &mut self,
+        feedback: ThreadFeedback,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<()>> {
         let final_project_snapshot = Self::project_snapshot(self.project.clone(), cx);
         let serialized_thread = self.serialize(cx);
         let thread_id = self.id().clone();
         let client = self.project.read(cx).client();
+        self.feedback = Some(feedback);
+        cx.notify();
 
         cx.background_spawn(async move {
             let final_project_snapshot = final_project_snapshot.await;
@@ -1200,7 +1220,10 @@ impl Thread {
             let thread_data =
                 serde_json::to_value(serialized_thread).unwrap_or_else(|_| serde_json::Value::Null);
 
-            let rating = if is_positive { "positive" } else { "negative" };
+            let rating = match feedback {
+                ThreadFeedback::Positive => "positive",
+                ThreadFeedback::Negative => "negative",
+            };
             telemetry::event!(
                 "Assistant Thread Rated",
                 rating,

crates/git_ui/src/git_panel.rs 🔗

@@ -2786,7 +2786,7 @@ impl GitPanel {
                     panel_button(change_string)
                         .color(Color::Muted)
                         .tooltip(Tooltip::for_action_title_in(
-                            "Open diff",
+                            "Open Diff",
                             &Diff,
                             &self.focus_handle,
                         ))

crates/ui/src/components/disclosure.rs 🔗

@@ -11,6 +11,8 @@ pub struct Disclosure {
     selected: bool,
     on_toggle: Option<Arc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
     cursor_style: CursorStyle,
+    opened_icon: IconName,
+    closed_icon: IconName,
 }
 
 impl Disclosure {
@@ -21,6 +23,8 @@ impl Disclosure {
             selected: false,
             on_toggle: None,
             cursor_style: CursorStyle::PointingHand,
+            opened_icon: IconName::ChevronDown,
+            closed_icon: IconName::ChevronRight,
         }
     }
 
@@ -31,6 +35,16 @@ impl Disclosure {
         self.on_toggle = handler.into();
         self
     }
+
+    pub fn opened_icon(mut self, icon: IconName) -> Self {
+        self.opened_icon = icon;
+        self
+    }
+
+    pub fn closed_icon(mut self, icon: IconName) -> Self {
+        self.closed_icon = icon;
+        self
+    }
 }
 
 impl Toggleable for Disclosure {
@@ -57,8 +71,8 @@ impl RenderOnce for Disclosure {
         IconButton::new(
             self.id,
             match self.is_open {
-                true => IconName::ChevronDown,
-                false => IconName::ChevronRight,
+                true => self.opened_icon,
+                false => self.closed_icon,
             },
         )
         .shape(IconButtonShape::Square)

crates/ui/src/components/icon.rs 🔗

@@ -143,6 +143,7 @@ pub enum IconName {
     ArrowUp,
     ArrowUpFromLine,
     ArrowUpRight,
+    ArrowUpRightAlt,
     AtSign,
     AudioOff,
     AudioOn,
@@ -156,6 +157,7 @@ pub enum IconName {
     Book,
     BookCopy,
     BookPlus,
+    Brain,
     CaseSensitive,
     Check,
     ChevronDown,