assistant2: Revise thread visual design (#23083)

This PR adjusts the design of the assistant 2 threads with the goal of
reducing visual busyness. My intention is to remove the amount of lines
and borders given it is a relatively tight space. It also refines the
"generating" floating container style, finally leveraging linear
gradients that were recently added to GPUI! Now, we only display headers
for "you" messages. Assistant responses will be rendered right in the
panel; not bounded by a card container.

<img width="800" alt="Screenshot 2025-01-14 at 7 08 39 PM"
src="https://github.com/user-attachments/assets/a8ffa780-0ef2-4d4b-ae19-3f02fd2d63a6"
/>

Release Notes:

- N/A
This commit is contained in:
Danilo Leal 2025-01-14 19:29:39 -03:00 committed by GitHub
parent 077767a3b0
commit 07d582401a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 127 additions and 118 deletions

View file

@ -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-circle-user"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="10" r="3"/><path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/></svg>

After

Width:  |  Height:  |  Size: 345 B

View file

@ -4,17 +4,17 @@ use std::time::Duration;
use assistant_tool::ToolWorkingSet; use assistant_tool::ToolWorkingSet;
use collections::HashMap; use collections::HashMap;
use gpui::{ use gpui::{
list, percentage, AbsoluteLength, Animation, AnimationExt, AnyElement, AppContext, linear_color_stop, linear_gradient, list, percentage, AbsoluteLength, Animation, AnimationExt,
DefiniteLength, EdgesRefinement, Empty, FocusHandle, Length, ListAlignment, ListOffset, AnyElement, AppContext, DefiniteLength, EdgesRefinement, Empty, FocusHandle, Length,
ListState, Model, StyleRefinement, Subscription, TextStyleRefinement, Transformation, ListAlignment, ListOffset, ListState, Model, StyleRefinement, Subscription,
UnderlineStyle, View, WeakView, TextStyleRefinement, Transformation, UnderlineStyle, View, WeakView,
}; };
use language::LanguageRegistry; use language::LanguageRegistry;
use language_model::Role; use language_model::Role;
use markdown::{Markdown, MarkdownStyle}; use markdown::{Markdown, MarkdownStyle};
use settings::Settings as _; use settings::Settings as _;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{prelude::*, ButtonLike, KeyBinding}; use ui::{prelude::*, Divider, KeyBinding};
use workspace::Workspace; use workspace::Workspace;
use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent}; use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent};
@ -123,10 +123,10 @@ impl ActiveThread {
selection_background_color: cx.theme().players().local().selection, selection_background_color: cx.theme().players().local().selection,
code_block: StyleRefinement { code_block: StyleRefinement {
margin: EdgesRefinement { margin: EdgesRefinement {
top: Some(Length::Definite(rems(1.0).into())), top: Some(Length::Definite(rems(0.).into())),
left: Some(Length::Definite(rems(0.).into())), left: Some(Length::Definite(rems(0.).into())),
right: Some(Length::Definite(rems(0.).into())), right: Some(Length::Definite(rems(0.).into())),
bottom: Some(Length::Definite(rems(1.).into())), bottom: Some(Length::Definite(rems(0.5).into())),
}, },
padding: EdgesRefinement { padding: EdgesRefinement {
top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))), top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
@ -134,10 +134,10 @@ impl ActiveThread {
right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))), right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))), bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
}, },
background: Some(colors.editor_foreground.opacity(0.01).into()), background: Some(colors.editor_background.into()),
border_color: Some(colors.border_variant.opacity(0.3)), border_color: Some(colors.border_variant),
border_widths: EdgesRefinement { border_widths: EdgesRefinement {
top: Some(AbsoluteLength::Pixels(Pixels(1.0))), top: Some(AbsoluteLength::Pixels(Pixels(1.))),
left: Some(AbsoluteLength::Pixels(Pixels(1.))), left: Some(AbsoluteLength::Pixels(Pixels(1.))),
right: Some(AbsoluteLength::Pixels(Pixels(1.))), right: Some(AbsoluteLength::Pixels(Pixels(1.))),
bottom: Some(AbsoluteLength::Pixels(Pixels(1.))), bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
@ -245,7 +245,6 @@ impl ActiveThread {
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement { fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
let message_id = self.messages[ix]; let message_id = self.messages[ix];
let is_last_message = ix == self.messages.len() - 1;
let Some(message) = self.thread.read(cx).message(message_id) else { let Some(message) = self.thread.read(cx).message(message_id) else {
return Empty.into_any(); return Empty.into_any();
}; };
@ -254,58 +253,112 @@ impl ActiveThread {
return Empty.into_any(); return Empty.into_any();
}; };
let is_streaming_completion = self.thread.read(cx).is_streaming();
let context = self.thread.read(cx).context_for_message(message_id); let context = self.thread.read(cx).context_for_message(message_id);
let colors = cx.theme().colors(); let colors = cx.theme().colors();
let (role_icon, role_name, role_color) = match message.role { let message_content = v_flex()
Role::User => (IconName::Person, "You", Color::Muted), .child(div().p_2p5().text_ui(cx).child(markdown.clone()))
Role::Assistant => (IconName::ZedAssistant, "Assistant", Color::Accent), .when_some(context, |parent, context| {
Role::System => (IconName::Settings, "System", Color::Default), if !context.is_empty() {
}; parent.child(
h_flex().flex_wrap().gap_1().px_1p5().pb_1p5().children(
context
.into_iter()
.map(|context| ContextPill::new_added(context, false, false, None)),
),
)
} else {
parent
}
});
div() let styled_message = match message.role {
Role::User => v_flex()
.id(("message-container", ix)) .id(("message-container", ix))
.py_1() .py_1()
.px_2() .px_2p5()
.child( .child(
v_flex() v_flex()
.border_1()
.border_color(colors.border_variant)
.bg(colors.editor_background) .bg(colors.editor_background)
.rounded_md() .ml_16()
.rounded_t_lg()
.rounded_bl_lg()
.rounded_br_none()
.border_1()
.border_color(colors.border)
.child( .child(
h_flex() h_flex()
.py_1p5() .py_1()
.px_2p5() .px_2()
.bg(colors.editor_foreground.opacity(0.05))
.border_b_1() .border_b_1()
.border_color(colors.border_variant) .border_color(colors.border)
.justify_between() .justify_between()
.rounded_t(px(6.))
.child( .child(
h_flex() h_flex()
.gap_1p5() .gap_1p5()
.child( .child(
Icon::new(role_icon) Icon::new(IconName::PersonCircle)
.size(IconSize::XSmall) .size(IconSize::XSmall)
.color(role_color), .color(Color::Muted),
) )
.child( .child(
Label::new(role_name) Label::new("You")
.size(LabelSize::XSmall) .size(LabelSize::Small)
.color(role_color), .color(Color::Muted),
), ),
), ),
) )
.child(div().p_2p5().text_ui(cx).child(markdown.clone())) .child(message_content),
.when( ),
message.role == Role::Assistant Role::Assistant => div().id(("message-container", ix)).child(message_content),
&& is_last_message Role::System => div().id(("message-container", ix)).py_1().px_2().child(
&& is_streaming_completion, v_flex()
|parent| { .bg(colors.editor_background)
.rounded_md()
.child(message_content),
),
};
styled_message.into_any()
}
}
impl Render for ActiveThread {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let is_streaming_completion = self.thread.read(cx).is_streaming();
let panel_bg = cx.theme().colors().panel_background;
let focus_handle = self.focus_handle.clone();
v_flex()
.size_full()
.pt_1p5()
.child(list(self.list_state.clone()).flex_grow())
.when(is_streaming_completion, |parent| {
parent.child( parent.child(
h_flex() h_flex()
.w_full()
.pb_2p5()
.absolute()
.bottom_0()
.flex_shrink()
.justify_center()
.bg(linear_gradient(
180.,
linear_color_stop(panel_bg.opacity(0.0), 0.),
linear_color_stop(panel_bg, 1.),
))
.child(
h_flex()
.flex_none()
.p_1p5()
.bg(cx.theme().colors().editor_background)
.border_1()
.border_color(cx.theme().colors().border)
.rounded_md()
.shadow_lg()
.gap_1() .gap_1()
.p_2p5()
.child( .child(
Icon::new(IconName::ArrowCircle) Icon::new(IconName::ArrowCircle)
.size(IconSize::Small) .size(IconSize::Small)
@ -314,9 +367,9 @@ impl ActiveThread {
"arrow-circle", "arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(), Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| { |icon, delta| {
icon.transform(Transformation::rotate( icon.transform(Transformation::rotate(percentage(
percentage(delta), delta,
)) )))
}, },
), ),
) )
@ -324,69 +377,23 @@ impl ActiveThread {
Label::new("Generating…") Label::new("Generating…")
.size(LabelSize::Small) .size(LabelSize::Small)
.color(Color::Muted), .color(Color::Muted),
),
) )
}, .child(Divider::vertical())
)
.when_some(context, |parent, context| {
if !context.is_empty() {
parent.child(h_flex().flex_wrap().gap_1().px_1p5().pb_1p5().children(
context.into_iter().map(|context| {
ContextPill::new_added(context, false, false, None)
}),
))
} else {
parent
}
}),
)
.into_any()
}
}
impl Render for ActiveThread {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let is_streaming_completion = self.thread.read(cx).is_streaming();
let focus_handle = self.focus_handle.clone();
v_flex()
.size_full()
.child(list(self.list_state.clone()).flex_grow())
.child( .child(
h_flex() Button::new("cancel-generation", "Cancel")
.absolute() .label_size(LabelSize::Small)
.bottom_1() .key_binding(KeyBinding::for_action_in(
.flex_shrink()
.justify_center()
.w_full()
.when(is_streaming_completion, |parent| {
parent.child(
h_flex()
.gap_2()
.p_1p5()
.rounded_md()
.bg(cx.theme().colors().elevated_surface_background)
.child(Label::new("Generating…").size(LabelSize::Small))
.child(
ButtonLike::new("cancel-generation")
.style(ButtonStyle::Filled)
.child(Label::new("Cancel").size(LabelSize::Small))
.children(
KeyBinding::for_action_in(
&editor::actions::Cancel, &editor::actions::Cancel,
&self.focus_handle, &self.focus_handle,
cx, cx,
) ))
.map(|binding| binding.into_any_element()),
)
.on_click(move |_event, cx| { .on_click(move |_event, cx| {
focus_handle focus_handle
.dispatch_action(&editor::actions::Cancel, cx); .dispatch_action(&editor::actions::Cancel, cx);
}), }),
), ),
),
) )
}), })
)
} }
} }

View file

@ -231,6 +231,7 @@ pub enum IconName {
PanelRight, PanelRight,
Pencil, Pencil,
Person, Person,
PersonCircle,
PhoneIncoming, PhoneIncoming,
Pin, Pin,
Play, Play,