diff --git a/assets/icons/assist_15.svg b/assets/icons/assist_15.svg new file mode 100644 index 0000000000..3baf8df3e9 --- /dev/null +++ b/assets/icons/assist_15.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/split_message_15.svg b/assets/icons/split_message_15.svg new file mode 100644 index 0000000000..54d9e81224 --- /dev/null +++ b/assets/icons/split_message_15.svg @@ -0,0 +1 @@ + diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 7dcd8830ed..929b54fded 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -28,7 +28,6 @@ use search::BufferSearchBar; use serde::Deserialize; use settings::SettingsStore; use std::{ - borrow::Cow, cell::RefCell, cmp, env, fmt::Write, @@ -40,13 +39,9 @@ use std::{ time::Duration, }; use theme::{ui::IconStyle, AssistantStyle}; -use util::{ - channel::ReleaseChannel, paths::CONVERSATIONS_DIR, post_inc, truncate_and_trailoff, ResultExt, - TryFutureExt, -}; +use util::{channel::ReleaseChannel, paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, - item::Item, searchable::Direction, Save, ToggleZoom, Toolbar, Workspace, }; @@ -361,64 +356,43 @@ impl AssistantPanel { }) } - fn render_current_model( - &self, - style: &AssistantStyle, - cx: &mut ViewContext, - ) -> Option> { - enum Model {} - - let model = self - .active_editor()? - .read(cx) - .conversation - .read(cx) - .model - .clone(); - - Some( - MouseEventHandler::::new(0, cx, |state, _| { - let style = style.model.style_for(state); - Label::new(model, style.text.clone()) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this, cx| { - if let Some(editor) = this.active_editor() { - editor.update(cx, |editor, cx| { - editor.cycle_model(cx); - }); - } - }), - ) + fn render_editor_tools(&self, style: &AssistantStyle) -> Vec> { + if self.active_editor().is_some() { + vec![ + Self::render_split_button(&style.split_button).into_any(), + Self::render_assist_button(&style.assist_button).into_any(), + ] + } else { + Default::default() + } } - fn render_remaining_tokens( - &self, - style: &AssistantStyle, - cx: &mut ViewContext, - ) -> Option> { - self.active_editor().and_then(|editor| { - editor - .read(cx) - .conversation - .read(cx) - .remaining_tokens() - .map(|remaining_tokens| { - let remaining_tokens_style = if remaining_tokens <= 0 { - &style.no_remaining_tokens - } else { - &style.remaining_tokens - }; - Label::new( - remaining_tokens.to_string(), - remaining_tokens_style.text.clone(), - ) - .contained() - .with_style(remaining_tokens_style.container) - }) - }) + fn render_split_button(style: &IconStyle) -> impl Element { + enum SplitMessage {} + Svg::for_style(style.icon.clone()) + .contained() + .with_style(style.container) + .mouse::(0) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, this: &mut Self, cx| { + if let Some(active_editor) = this.active_editor() { + active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx)); + } + }) + } + + fn render_assist_button(style: &IconStyle) -> impl Element { + enum Assist {} + Svg::for_style(style.icon.clone()) + .contained() + .with_style(style.container) + .mouse::(0) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, this: &mut Self, cx| { + if let Some(active_editor) = this.active_editor() { + active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx)); + } + }) } fn render_plus_button(style: &IconStyle) -> impl Element { @@ -589,19 +563,16 @@ impl View for AssistantPanel { ) .with_children(title) .with_children( - self.render_current_model(&style, cx) - .map(|current_model| current_model.aligned().flex_float()), - ) - .with_children( - self.render_remaining_tokens(&style, cx) - .map(|remaining_tokens| remaining_tokens.aligned().flex_float()), + self.render_editor_tools(&style) + .into_iter() + .map(|tool| tool.aligned().flex_float()), ) .with_child( Self::render_plus_button(&style.plus_button) .aligned() .flex_float(), ) - .with_child(self.render_zoom_button(&style, cx).aligned().flex_float()) + .with_child(self.render_zoom_button(&style, cx).aligned()) .contained() .with_style(theme.workspace.tab_bar.container) .expanded() @@ -1995,6 +1966,44 @@ impl ConversationEditor { .map(|summary| summary.text.clone()) .unwrap_or_else(|| "New Conversation".into()) } + + fn render_current_model( + &self, + style: &AssistantStyle, + cx: &mut ViewContext, + ) -> impl Element { + enum Model {} + + MouseEventHandler::::new(0, cx, |state, cx| { + let style = style.model.style_for(state); + Label::new(self.conversation.read(cx).model.clone(), style.text.clone()) + .contained() + .with_style(style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, this, cx| this.cycle_model(cx)) + } + + fn render_remaining_tokens( + &self, + style: &AssistantStyle, + cx: &mut ViewContext, + ) -> Option> { + let remaining_tokens = self.conversation.read(cx).remaining_tokens()?; + let remaining_tokens_style = if remaining_tokens <= 0 { + &style.no_remaining_tokens + } else { + &style.remaining_tokens + }; + Some( + Label::new( + remaining_tokens.to_string(), + remaining_tokens_style.text.clone(), + ) + .contained() + .with_style(remaining_tokens_style.container), + ) + } } impl Entity for ConversationEditor { @@ -2008,9 +2017,20 @@ impl View for ConversationEditor { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { let theme = &theme::current(cx).assistant; - ChildView::new(&self.editor, cx) - .contained() - .with_style(theme.container) + Stack::new() + .with_child( + ChildView::new(&self.editor, cx) + .contained() + .with_style(theme.container), + ) + .with_child( + Flex::row() + .with_child(self.render_current_model(theme, cx)) + .with_children(self.render_remaining_tokens(theme, cx)) + .aligned() + .top() + .right(), + ) .into_any() } @@ -2021,29 +2041,6 @@ impl View for ConversationEditor { } } -impl Item for ConversationEditor { - fn tab_content( - &self, - _: Option, - style: &theme::Tab, - cx: &gpui::AppContext, - ) -> AnyElement { - let title = truncate_and_trailoff(&self.title(cx), editor::MAX_TAB_TITLE_LEN); - Label::new(title, style.label.clone()).into_any() - } - - fn tab_tooltip_text(&self, cx: &AppContext) -> Option> { - Some(self.title(cx).into()) - } - - fn as_searchable( - &self, - _: &ViewHandle, - ) -> Option> { - Some(Box::new(self.editor.clone())) - } -} - #[derive(Clone, Debug)] struct MessageAnchor { id: MessageId, diff --git a/crates/gpui/src/elements/label.rs b/crates/gpui/src/elements/label.rs index 57aeba2886..d9cf537333 100644 --- a/crates/gpui/src/elements/label.rs +++ b/crates/gpui/src/elements/label.rs @@ -165,6 +165,7 @@ impl Element for Label { _: &mut V, cx: &mut ViewContext, ) -> Self::PaintState { + let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); line.paint( scene, bounds.origin(), diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c012e04807..7a6b554247 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -994,6 +994,8 @@ pub struct TerminalStyle { pub struct AssistantStyle { pub container: ContainerStyle, pub hamburger_button: IconStyle, + pub split_button: IconStyle, + pub assist_button: IconStyle, pub zoom_in_button: IconStyle, pub zoom_out_button: IconStyle, pub plus_button: IconStyle, @@ -1003,7 +1005,6 @@ pub struct AssistantStyle { pub user_sender: Interactive, pub assistant_sender: Interactive, pub system_sender: Interactive, - pub model_info_container: ContainerStyle, pub model: Interactive, pub remaining_tokens: ContainedText, pub no_remaining_tokens: ContainedText, diff --git a/styles/src/styleTree/assistant.ts b/styles/src/styleTree/assistant.ts index abdd55818b..153b2f9e42 100644 --- a/styles/src/styleTree/assistant.ts +++ b/styles/src/styleTree/assistant.ts @@ -28,6 +28,32 @@ export default function assistant(colorScheme: ColorScheme) { margin: { left: 12 }, } }, + splitButton: { + icon: { + color: text(layer, "sans", "default", { size: "sm" }).color, + asset: "icons/split_message_15.svg", + dimensions: { + width: 15, + height: 15, + }, + }, + container: { + margin: { left: 12 }, + } + }, + assistButton: { + icon: { + color: text(layer, "sans", "default", { size: "sm" }).color, + asset: "icons/assist_15.svg", + dimensions: { + width: 15, + height: 15, + }, + }, + container: { + margin: { left: 12, right: 12 }, + } + }, zoomInButton: { icon: { color: text(layer, "sans", "default", { size: "sm" }).color, @@ -120,13 +146,10 @@ export default function assistant(colorScheme: ColorScheme) { margin: { top: 2, left: 8 }, ...text(layer, "sans", "default", { size: "2xs" }), }, - modelInfoContainer: { - margin: { right: 16, top: 4 }, - }, model: interactive({ base: { background: background(layer, "on"), - margin: { right: 8 }, + margin: { left: 12, right: 12, top: 12 }, padding: 4, cornerRadius: 4, ...text(layer, "sans", "default", { size: "xs" }), @@ -139,11 +162,17 @@ export default function assistant(colorScheme: ColorScheme) { }, }), remainingTokens: { - margin: { right: 12 }, + background: background(layer, "on"), + margin: { top: 12, right: 12 }, + padding: 4, + cornerRadius: 4, ...text(layer, "sans", "positive", { size: "xs" }), }, noRemainingTokens: { - margin: { right: 12 }, + background: background(layer, "on"), + margin: { top: 12, right: 12 }, + padding: 4, + cornerRadius: 4, ...text(layer, "sans", "negative", { size: "xs" }), }, errorIcon: {