diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index fae444c0ef81d0f7b631769112f4286f8e75ea23..0b02b4315ac04dceed170890f86b336a8d2a27c4 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -35,6 +35,7 @@ mod stack; mod sticky_items; mod tab; mod tab_bar; +mod thread_item; mod toggle; mod tooltip; mod tree_view_item; @@ -79,6 +80,7 @@ pub use stack::*; pub use sticky_items::*; pub use tab::*; pub use tab_bar::*; +pub use thread_item::*; pub use toggle::*; pub use tooltip::*; pub use tree_view_item::*; diff --git a/crates/ui/src/components/thread_item.rs b/crates/ui/src/components/thread_item.rs new file mode 100644 index 0000000000000000000000000000000000000000..0cb6a42ad11d16eddd3a2afb3d8a9dc9475b6165 --- /dev/null +++ b/crates/ui/src/components/thread_item.rs @@ -0,0 +1,214 @@ +use crate::{Chip, Indicator, SpinnerLabel, prelude::*}; +use gpui::{ClickEvent, SharedString}; + +#[derive(IntoElement, RegisterComponent)] +pub struct ThreadItem { + id: ElementId, + icon: IconName, + title: SharedString, + timestamp: SharedString, + running: bool, + generation_done: bool, + selected: bool, + has_changes: bool, + worktree: Option, + on_click: Option>, +} + +impl ThreadItem { + pub fn new(id: impl Into, title: impl Into) -> Self { + Self { + id: id.into(), + icon: IconName::ZedAgent, + title: title.into(), + timestamp: "".into(), + running: false, + generation_done: false, + selected: false, + has_changes: false, + worktree: None, + on_click: None, + } + } + + pub fn timestamp(mut self, timestamp: impl Into) -> Self { + self.timestamp = timestamp.into(); + self + } + + pub fn icon(mut self, icon: IconName) -> Self { + self.icon = icon; + self + } + + pub fn running(mut self, running: bool) -> Self { + self.running = running; + self + } + + pub fn generation_done(mut self, generation_done: bool) -> Self { + self.generation_done = generation_done; + self + } + + pub fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } + + pub fn has_changes(mut self, has_changes: bool) -> Self { + self.has_changes = has_changes; + self + } + + pub fn worktree(mut self, worktree: impl Into) -> Self { + self.worktree = Some(worktree.into()); + self + } + + pub fn on_click( + mut self, + handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_click = Some(Box::new(handler)); + self + } +} + +impl RenderOnce for ThreadItem { + fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + let icon_container = || h_flex().size_4().justify_center(); + let icon = if self.generation_done { + icon_container().child(Indicator::dot().color(Color::Accent)) + } else if self.running { + icon_container().child(SpinnerLabel::new().color(Color::Accent)) + } else { + icon_container().child( + Icon::new(self.icon) + .color(Color::Muted) + .size(IconSize::Small), + ) + }; + + v_flex() + .id(self.id) + .cursor_pointer() + .p_2() + .when(self.selected, |this| { + this.bg(cx.theme().colors().element_active) + }) + .hover(|s| s.bg(cx.theme().colors().element_hover)) + .child( + h_flex() + .w_full() + .gap_1p5() + .child(icon) + .child(Label::new(self.title).truncate()), + ) + .child( + h_flex() + .gap_1p5() + .child(icon_container()) // Icon Spacing + .when_some(self.worktree, |this, name| { + this.child(Chip::new(name).label_size(LabelSize::XSmall)) + }) + .child( + Label::new(self.timestamp) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child( + Label::new("•") + .size(LabelSize::Small) + .color(Color::Muted) + .alpha(0.5), + ) + .when(!self.has_changes, |this| { + this.child( + Label::new("No Changes") + .size(LabelSize::Small) + .color(Color::Muted), + ) + }), + ) + .when_some(self.on_click, |this, on_click| this.on_click(on_click)) + } +} + +impl Component for ThreadItem { + fn scope() -> ComponentScope { + ComponentScope::Agent + } + + fn preview(_window: &mut Window, cx: &mut App) -> Option { + let container = || { + v_flex() + .w_72() + .border_1() + .border_color(cx.theme().colors().border_variant) + .bg(cx.theme().colors().panel_background) + }; + + let thread_item_examples = vec![ + single_example( + "Default", + container() + .child( + ThreadItem::new("ti-1", "Linking to the Agent Panel Depending on Settings") + .icon(IconName::AiOpenAi) + .timestamp("1:33 AM"), + ) + .into_any_element(), + ), + single_example( + "Generation Done", + container() + .child( + ThreadItem::new("ti-2", "Refine thread view scrolling behavior") + .timestamp("12:12 AM") + .generation_done(true), + ) + .into_any_element(), + ), + single_example( + "Running Agent", + container() + .child( + ThreadItem::new("ti-3", "Add line numbers option to FileEditBlock") + .icon(IconName::AiClaude) + .timestamp("7:30 PM") + .running(true), + ) + .into_any_element(), + ), + single_example( + "In Worktree", + container() + .child( + ThreadItem::new("ti-4", "Add line numbers option to FileEditBlock") + .icon(IconName::AiClaude) + .timestamp("7:37 PM") + .worktree("link-agent-panel"), + ) + .into_any_element(), + ), + single_example( + "Selected Item", + container() + .child( + ThreadItem::new("ti-5", "Refine textarea interaction behavior") + .icon(IconName::AiGemini) + .timestamp("3:00 PM") + .selected(true), + ) + .into_any_element(), + ), + ]; + + Some( + example_group(thread_item_examples) + .vertical() + .into_any_element(), + ) + } +} diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs index d62f39ef6306593eba4b5fe6bff427db036e82dc..18279d8ee88821d44166fb5aedebca2e51ae9491 100644 --- a/crates/zed/src/zed/component_preview.rs +++ b/crates/zed/src/zed/component_preview.rs @@ -934,15 +934,16 @@ impl ComponentPreviewPage { fn render_header(&self, _: &Window, cx: &App) -> impl IntoElement { v_flex() - .py_12() - .px_16() + .min_w_0() + .w_full() + .p_12() .gap_6() .bg(cx.theme().colors().surface_background) .border_b_1() .border_color(cx.theme().colors().border) .child( v_flex() - .gap_0p5() + .gap_1() .child( Label::new(self.component.scope().to_string()) .size(LabelSize::Small) @@ -959,7 +960,7 @@ impl ComponentPreviewPage { ), ) .when_some(self.component.description(), |this, description| { - this.child(div().text_sm().child(description)) + this.child(Label::new(description).size(LabelSize::Small)) }) }