diff --git a/Cargo.lock b/Cargo.lock
index 4beafa67037c43624c3d3a0b39ad8a4996d46588..1c24bf93254a520eaf56f9065581d0f514c4e8e6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -495,6 +495,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
+ "smallvec",
"smol",
"streaming_diff",
"telemetry",
diff --git a/assets/icons/arrow_up_right_alt.svg b/assets/icons/arrow_up_right_alt.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4e923c6867ac1ac396d2180827af390941c291a4
--- /dev/null
+++ b/assets/icons/arrow_up_right_alt.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/brain.svg b/assets/icons/brain.svg
new file mode 100644
index 0000000000000000000000000000000000000000..80c93814f7c483f9e90d20f81e4ce7d32459ab57
--- /dev/null
+++ b/assets/icons/brain.svg
@@ -0,0 +1 @@
+
diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json
index 5d956ab591a7ecef3c59c0aef156cbf266cce2f1..e5e44e6418dd94bafb65c1ec098c59ff50260b0f 100644
--- a/assets/keymaps/default-macos.json
+++ b/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"
}
},
{
diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml
index f14b6c184a8c807eb337ee40720570fd6fa4c9f1..66663c7a2316f44daadba776e5b9733148b77887 100644
--- a/crates/assistant2/Cargo.toml
+++ b/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
diff --git a/crates/assistant2/src/active_thread.rs b/crates/assistant2/src/active_thread.rs
index 4cc89784fa48395a6336413ebd6d7c9cd01433ee..b3363fb73a9765698e13ab009071a8a676f80258 100644
--- a/crates/assistant2/src/active_thread.rs
+++ b/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,
+ ) {
+ 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) -> 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()
diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs
index 8048875495c88a9423862fd86754d764e72fca01..4baa1da3be5018d85468276046d6509903e26ece 100644
--- a/crates/assistant2/src/message_editor.rs
+++ b/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,
editor: Entity,
+ #[allow(dead_code)]
workspace: WeakEntity,
project: Entity,
context_store: Entity,
@@ -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,
- ) {
- 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::();
- 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) -> 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()
diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs
index 876662d386884495e9f71bf25ce254826bceeb24..c2cf4fe5509e55137feeb9f8ac4761e28d1a14c9 100644
--- a/crates/assistant2/src/thread.rs
+++ b/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>>>,
cumulative_token_usage: TokenUsage,
+ feedback: Option,
}
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 {
+ 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) -> Task> {
+ pub fn report_feedback(
+ &mut self,
+ feedback: ThreadFeedback,
+ cx: &mut Context,
+ ) -> Task> {
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,
diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs
index d4cd4da52e5e94e6d6768bd26e670bc955e12c6c..a5804b91a8fd3704021c7be6da7e292a2dd251d4 100644
--- a/crates/git_ui/src/git_panel.rs
+++ b/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,
))
diff --git a/crates/ui/src/components/disclosure.rs b/crates/ui/src/components/disclosure.rs
index c39bcf47d50b977a855d9ed6f6f47963a55ee201..1bf34ad6ed3332819cab90c327eb4445c7e9c636 100644
--- a/crates/ui/src/components/disclosure.rs
+++ b/crates/ui/src/components/disclosure.rs
@@ -11,6 +11,8 @@ pub struct Disclosure {
selected: bool,
on_toggle: Option>,
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)
diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs
index 19aaaf9c476877858fad6cedd4f39768b70484e3..8dc92b1fc32fe100c4e00eca03ce28fc78bdca2d 100644
--- a/crates/ui/src/components/icon.rs
+++ b/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,