thread_item.rs

  1use crate::{Chip, Indicator, SpinnerLabel, prelude::*};
  2use gpui::{ClickEvent, SharedString};
  3
  4#[derive(IntoElement, RegisterComponent)]
  5pub struct ThreadItem {
  6    id: ElementId,
  7    icon: IconName,
  8    title: SharedString,
  9    timestamp: SharedString,
 10    running: bool,
 11    generation_done: bool,
 12    selected: bool,
 13    has_changes: bool,
 14    worktree: Option<SharedString>,
 15    on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
 16}
 17
 18impl ThreadItem {
 19    pub fn new(id: impl Into<ElementId>, title: impl Into<SharedString>) -> Self {
 20        Self {
 21            id: id.into(),
 22            icon: IconName::ZedAgent,
 23            title: title.into(),
 24            timestamp: "".into(),
 25            running: false,
 26            generation_done: false,
 27            selected: false,
 28            has_changes: false,
 29            worktree: None,
 30            on_click: None,
 31        }
 32    }
 33
 34    pub fn timestamp(mut self, timestamp: impl Into<SharedString>) -> Self {
 35        self.timestamp = timestamp.into();
 36        self
 37    }
 38
 39    pub fn icon(mut self, icon: IconName) -> Self {
 40        self.icon = icon;
 41        self
 42    }
 43
 44    pub fn running(mut self, running: bool) -> Self {
 45        self.running = running;
 46        self
 47    }
 48
 49    pub fn generation_done(mut self, generation_done: bool) -> Self {
 50        self.generation_done = generation_done;
 51        self
 52    }
 53
 54    pub fn selected(mut self, selected: bool) -> Self {
 55        self.selected = selected;
 56        self
 57    }
 58
 59    pub fn has_changes(mut self, has_changes: bool) -> Self {
 60        self.has_changes = has_changes;
 61        self
 62    }
 63
 64    pub fn worktree(mut self, worktree: impl Into<SharedString>) -> Self {
 65        self.worktree = Some(worktree.into());
 66        self
 67    }
 68
 69    pub fn on_click(
 70        mut self,
 71        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
 72    ) -> Self {
 73        self.on_click = Some(Box::new(handler));
 74        self
 75    }
 76}
 77
 78impl RenderOnce for ThreadItem {
 79    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
 80        let icon_container = || h_flex().size_4().justify_center();
 81        let icon = if self.generation_done {
 82            icon_container().child(Indicator::dot().color(Color::Accent))
 83        } else if self.running {
 84            icon_container().child(SpinnerLabel::new().color(Color::Accent))
 85        } else {
 86            icon_container().child(
 87                Icon::new(self.icon)
 88                    .color(Color::Muted)
 89                    .size(IconSize::Small),
 90            )
 91        };
 92
 93        v_flex()
 94            .id(self.id)
 95            .cursor_pointer()
 96            .p_2()
 97            .when(self.selected, |this| {
 98                this.bg(cx.theme().colors().element_active)
 99            })
100            .hover(|s| s.bg(cx.theme().colors().element_hover))
101            .child(
102                h_flex()
103                    .w_full()
104                    .gap_1p5()
105                    .child(icon)
106                    .child(Label::new(self.title).truncate()),
107            )
108            .child(
109                h_flex()
110                    .gap_1p5()
111                    .child(icon_container()) // Icon Spacing
112                    .when_some(self.worktree, |this, name| {
113                        this.child(Chip::new(name).label_size(LabelSize::XSmall))
114                    })
115                    .child(
116                        Label::new(self.timestamp)
117                            .size(LabelSize::Small)
118                            .color(Color::Muted),
119                    )
120                    .child(
121                        Label::new("")
122                            .size(LabelSize::Small)
123                            .color(Color::Muted)
124                            .alpha(0.5),
125                    )
126                    .when(!self.has_changes, |this| {
127                        this.child(
128                            Label::new("No Changes")
129                                .size(LabelSize::Small)
130                                .color(Color::Muted),
131                        )
132                    }),
133            )
134            .when_some(self.on_click, |this, on_click| this.on_click(on_click))
135    }
136}
137
138impl Component for ThreadItem {
139    fn scope() -> ComponentScope {
140        ComponentScope::Agent
141    }
142
143    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
144        let container = || {
145            v_flex()
146                .w_72()
147                .border_1()
148                .border_color(cx.theme().colors().border_variant)
149                .bg(cx.theme().colors().panel_background)
150        };
151
152        let thread_item_examples = vec![
153            single_example(
154                "Default",
155                container()
156                    .child(
157                        ThreadItem::new("ti-1", "Linking to the Agent Panel Depending on Settings")
158                            .icon(IconName::AiOpenAi)
159                            .timestamp("1:33 AM"),
160                    )
161                    .into_any_element(),
162            ),
163            single_example(
164                "Generation Done",
165                container()
166                    .child(
167                        ThreadItem::new("ti-2", "Refine thread view scrolling behavior")
168                            .timestamp("12:12 AM")
169                            .generation_done(true),
170                    )
171                    .into_any_element(),
172            ),
173            single_example(
174                "Running Agent",
175                container()
176                    .child(
177                        ThreadItem::new("ti-3", "Add line numbers option to FileEditBlock")
178                            .icon(IconName::AiClaude)
179                            .timestamp("7:30 PM")
180                            .running(true),
181                    )
182                    .into_any_element(),
183            ),
184            single_example(
185                "In Worktree",
186                container()
187                    .child(
188                        ThreadItem::new("ti-4", "Add line numbers option to FileEditBlock")
189                            .icon(IconName::AiClaude)
190                            .timestamp("7:37 PM")
191                            .worktree("link-agent-panel"),
192                    )
193                    .into_any_element(),
194            ),
195            single_example(
196                "Selected Item",
197                container()
198                    .child(
199                        ThreadItem::new("ti-5", "Refine textarea interaction behavior")
200                            .icon(IconName::AiGemini)
201                            .timestamp("3:00 PM")
202                            .selected(true),
203                    )
204                    .into_any_element(),
205            ),
206        ];
207
208        Some(
209            example_group(thread_item_examples)
210                .vertical()
211                .into_any_element(),
212        )
213    }
214}