thread_item.rs

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