thread_item.rs

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