thread_item.rs

  1use crate::{CommonAnimationExt, DiffStat, GradientFade, HighlightedLabel, Tooltip, prelude::*};
  2
  3use gpui::{
  4    Animation, AnimationExt, ClickEvent, Hsla, MouseButton, SharedString, pulsating_between,
  5};
  6use itertools::Itertools as _;
  7use std::{path::PathBuf, sync::Arc, time::Duration};
  8
  9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
 10pub enum AgentThreadStatus {
 11    #[default]
 12    Completed,
 13    Running,
 14    WaitingForConfirmation,
 15    Error,
 16}
 17
 18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
 19pub enum WorktreeKind {
 20    #[default]
 21    Main,
 22    Linked,
 23}
 24
 25#[derive(Clone)]
 26pub struct ThreadItemWorktreeInfo {
 27    pub name: SharedString,
 28    pub full_path: SharedString,
 29    pub highlight_positions: Vec<usize>,
 30    pub kind: WorktreeKind,
 31    pub branch_name: Option<SharedString>,
 32}
 33
 34#[derive(IntoElement, RegisterComponent)]
 35pub struct ThreadItem {
 36    id: ElementId,
 37    icon: IconName,
 38    icon_color: Option<Color>,
 39    icon_visible: bool,
 40    custom_icon_from_external_svg: Option<SharedString>,
 41    title: SharedString,
 42    title_label_color: Option<Color>,
 43    title_generating: bool,
 44    highlight_positions: Vec<usize>,
 45    timestamp: SharedString,
 46    notified: bool,
 47    status: AgentThreadStatus,
 48    selected: bool,
 49    focused: bool,
 50    hovered: bool,
 51    rounded: bool,
 52    added: Option<usize>,
 53    removed: Option<usize>,
 54    project_paths: Option<Arc<[PathBuf]>>,
 55    project_name: Option<SharedString>,
 56    worktrees: Vec<ThreadItemWorktreeInfo>,
 57    is_remote: bool,
 58    on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
 59    on_hover: Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>,
 60    action_slot: Option<AnyElement>,
 61    base_bg: Option<Hsla>,
 62}
 63
 64impl ThreadItem {
 65    pub fn new(id: impl Into<ElementId>, title: impl Into<SharedString>) -> Self {
 66        Self {
 67            id: id.into(),
 68            icon: IconName::ZedAgent,
 69            icon_color: None,
 70            icon_visible: true,
 71            custom_icon_from_external_svg: None,
 72            title: title.into(),
 73            title_label_color: None,
 74            title_generating: false,
 75            highlight_positions: Vec::new(),
 76            timestamp: "".into(),
 77            notified: false,
 78            status: AgentThreadStatus::default(),
 79            selected: false,
 80            focused: false,
 81            hovered: false,
 82            rounded: false,
 83            added: None,
 84            removed: None,
 85            project_paths: None,
 86            project_name: None,
 87            worktrees: Vec::new(),
 88            is_remote: false,
 89            on_click: None,
 90            on_hover: Box::new(|_, _, _| {}),
 91            action_slot: None,
 92            base_bg: None,
 93        }
 94    }
 95
 96    pub fn timestamp(mut self, timestamp: impl Into<SharedString>) -> Self {
 97        self.timestamp = timestamp.into();
 98        self
 99    }
100
101    pub fn icon(mut self, icon: IconName) -> Self {
102        self.icon = icon;
103        self
104    }
105
106    pub fn icon_color(mut self, color: Color) -> Self {
107        self.icon_color = Some(color);
108        self
109    }
110
111    pub fn icon_visible(mut self, visible: bool) -> Self {
112        self.icon_visible = visible;
113        self
114    }
115
116    pub fn custom_icon_from_external_svg(mut self, svg: impl Into<SharedString>) -> Self {
117        self.custom_icon_from_external_svg = Some(svg.into());
118        self
119    }
120
121    pub fn notified(mut self, notified: bool) -> Self {
122        self.notified = notified;
123        self
124    }
125
126    pub fn status(mut self, status: AgentThreadStatus) -> Self {
127        self.status = status;
128        self
129    }
130
131    pub fn title_generating(mut self, generating: bool) -> Self {
132        self.title_generating = generating;
133        self
134    }
135
136    pub fn title_label_color(mut self, color: Color) -> Self {
137        self.title_label_color = Some(color);
138        self
139    }
140
141    pub fn highlight_positions(mut self, positions: Vec<usize>) -> Self {
142        self.highlight_positions = positions;
143        self
144    }
145
146    pub fn selected(mut self, selected: bool) -> Self {
147        self.selected = selected;
148        self
149    }
150
151    pub fn focused(mut self, focused: bool) -> Self {
152        self.focused = focused;
153        self
154    }
155
156    pub fn added(mut self, added: usize) -> Self {
157        self.added = Some(added);
158        self
159    }
160
161    pub fn removed(mut self, removed: usize) -> Self {
162        self.removed = Some(removed);
163        self
164    }
165
166    pub fn project_paths(mut self, paths: Arc<[PathBuf]>) -> Self {
167        self.project_paths = Some(paths);
168        self
169    }
170
171    pub fn project_name(mut self, name: impl Into<SharedString>) -> Self {
172        self.project_name = Some(name.into());
173        self
174    }
175
176    pub fn worktrees(mut self, worktrees: Vec<ThreadItemWorktreeInfo>) -> Self {
177        self.worktrees = worktrees;
178        self
179    }
180
181    pub fn is_remote(mut self, is_remote: bool) -> Self {
182        self.is_remote = is_remote;
183        self
184    }
185
186    pub fn hovered(mut self, hovered: bool) -> Self {
187        self.hovered = hovered;
188        self
189    }
190
191    pub fn rounded(mut self, rounded: bool) -> Self {
192        self.rounded = rounded;
193        self
194    }
195
196    pub fn on_click(
197        mut self,
198        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
199    ) -> Self {
200        self.on_click = Some(Box::new(handler));
201        self
202    }
203
204    pub fn on_hover(mut self, on_hover: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
205        self.on_hover = Box::new(on_hover);
206        self
207    }
208
209    pub fn action_slot(mut self, element: impl IntoElement) -> Self {
210        self.action_slot = Some(element.into_any_element());
211        self
212    }
213
214    pub fn base_bg(mut self, color: Hsla) -> Self {
215        self.base_bg = Some(color);
216        self
217    }
218}
219
220impl RenderOnce for ThreadItem {
221    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
222        let color = cx.theme().colors();
223        let sidebar_base_bg = color
224            .title_bar_background
225            .blend(color.panel_background.opacity(0.25));
226
227        let raw_bg = self.base_bg.unwrap_or(sidebar_base_bg);
228        let apparent_bg = color.background.blend(raw_bg);
229
230        let base_bg = if self.selected {
231            apparent_bg.blend(color.element_active)
232        } else {
233            apparent_bg
234        };
235
236        let hover_color = color
237            .element_active
238            .blend(color.element_background.opacity(0.2));
239        let hover_bg = apparent_bg.blend(hover_color);
240
241        let gradient_overlay = GradientFade::new(base_bg, hover_bg, hover_bg)
242            .width(px(64.0))
243            .right(px(-10.0))
244            .gradient_stop(0.75)
245            .group_name("thread-item");
246
247        let dot_separator = || {
248            Label::new("")
249                .size(LabelSize::Small)
250                .color(Color::Muted)
251                .alpha(0.5)
252        };
253
254        let icon_id = format!("icon-{}", self.id);
255        let icon_visible = self.icon_visible;
256        let icon_container = || {
257            h_flex()
258                .id(icon_id.clone())
259                .size_4()
260                .flex_none()
261                .justify_center()
262                .when(!icon_visible, |this| this.invisible())
263        };
264        let icon_color = self.icon_color.unwrap_or(Color::Muted);
265        let agent_icon = if let Some(custom_svg) = self.custom_icon_from_external_svg {
266            Icon::from_external_svg(custom_svg)
267                .color(icon_color)
268                .size(IconSize::Small)
269        } else {
270            Icon::new(self.icon).color(icon_color).size(IconSize::Small)
271        };
272
273        let status_icon = if self.status == AgentThreadStatus::Error {
274            Some(
275                Icon::new(IconName::Close)
276                    .size(IconSize::Small)
277                    .color(Color::Error),
278            )
279        } else if self.status == AgentThreadStatus::WaitingForConfirmation {
280            Some(
281                Icon::new(IconName::Warning)
282                    .size(IconSize::XSmall)
283                    .color(Color::Warning),
284            )
285        } else if self.notified {
286            Some(
287                Icon::new(IconName::Circle)
288                    .size(IconSize::Small)
289                    .color(Color::Accent),
290            )
291        } else {
292            None
293        };
294
295        let icon = if self.status == AgentThreadStatus::Running {
296            icon_container()
297                .child(
298                    Icon::new(IconName::LoadCircle)
299                        .size(IconSize::Small)
300                        .color(Color::Muted)
301                        .with_rotate_animation(2),
302                )
303                .into_any_element()
304        } else if let Some(status_icon) = status_icon {
305            icon_container().child(status_icon).into_any_element()
306        } else {
307            icon_container().child(agent_icon).into_any_element()
308        };
309
310        let tooltip_title = self.title.clone();
311        let tooltip_status = self.status;
312        let tooltip_worktrees = self.worktrees.clone();
313        let tooltip_added = self.added;
314        let tooltip_removed = self.removed;
315
316        let title = self.title;
317        let highlight_positions = self.highlight_positions;
318
319        let title_label = if self.title_generating {
320            Label::new(title)
321                .color(Color::Muted)
322                .with_animation(
323                    "generating-title",
324                    Animation::new(Duration::from_secs(2))
325                        .repeat()
326                        .with_easing(pulsating_between(0.4, 0.8)),
327                    |label, delta| label.alpha(delta),
328                )
329                .into_any_element()
330        } else if highlight_positions.is_empty() {
331            Label::new(title)
332                .when_some(self.title_label_color, |label, color| label.color(color))
333                .into_any_element()
334        } else {
335            HighlightedLabel::new(title, highlight_positions)
336                .when_some(self.title_label_color, |label, color| label.color(color))
337                .into_any_element()
338        };
339
340        let has_diff_stats = self.added.is_some() || self.removed.is_some();
341        let diff_stat_id = self.id.clone();
342        let added_count = self.added.unwrap_or(0);
343        let removed_count = self.removed.unwrap_or(0);
344
345        let project_paths = self.project_paths.as_ref().and_then(|paths| {
346            let paths_str = paths
347                .as_ref()
348                .iter()
349                .filter_map(|p| p.file_name())
350                .filter_map(|name| name.to_str())
351                .join(", ");
352            if paths_str.is_empty() {
353                None
354            } else {
355                Some(paths_str)
356            }
357        });
358
359        let has_project_name = self.project_name.is_some();
360        let has_project_paths = project_paths.is_some();
361        let has_timestamp = !self.timestamp.is_empty();
362        let timestamp = self.timestamp;
363
364        let visible_worktree_count = self
365            .worktrees
366            .iter()
367            .filter(|wt| !(wt.kind == WorktreeKind::Main && wt.branch_name.is_none()))
368            .count();
369
370        let mut worktree_labels: Vec<AnyElement> = Vec::new();
371
372        let slash_color = Color::Custom(cx.theme().colors().text_muted.opacity(0.4));
373
374        for wt in self.worktrees {
375            match (wt.kind, wt.branch_name) {
376                (WorktreeKind::Main, None) => continue,
377                (WorktreeKind::Main, Some(branch)) => {
378                    let chip_index = worktree_labels.len();
379
380                    worktree_labels.push(
381                        h_flex()
382                            .id(format!("{}-worktree-{chip_index}", self.id.clone()))
383                            .min_w_0()
384                            .when(visible_worktree_count > 1, |this| {
385                                this.child(
386                                    Label::new(wt.name)
387                                        .size(LabelSize::Small)
388                                        .color(Color::Muted)
389                                        .truncate(),
390                                )
391                                .child(
392                                    Label::new("/")
393                                        .size(LabelSize::Small)
394                                        .color(slash_color)
395                                        .flex_shrink_0(),
396                                )
397                            })
398                            .child(
399                                Label::new(branch)
400                                    .size(LabelSize::Small)
401                                    .color(Color::Muted)
402                                    .truncate(),
403                            )
404                            .into_any_element(),
405                    );
406                }
407                (WorktreeKind::Linked, branch) => {
408                    let chip_index = worktree_labels.len();
409
410                    let label = if wt.highlight_positions.is_empty() {
411                        Label::new(wt.name)
412                            .size(LabelSize::Small)
413                            .color(Color::Muted)
414                            .truncate()
415                            .into_any_element()
416                    } else {
417                        HighlightedLabel::new(wt.name, wt.highlight_positions)
418                            .size(LabelSize::Small)
419                            .color(Color::Muted)
420                            .truncate()
421                            .into_any_element()
422                    };
423
424                    worktree_labels.push(
425                        h_flex()
426                            .id(format!("{}-worktree-{chip_index}", self.id.clone()))
427                            .min_w_0()
428                            .gap_0p5()
429                            .child(
430                                Icon::new(IconName::GitWorktree)
431                                    .size(IconSize::XSmall)
432                                    .color(Color::Muted),
433                            )
434                            .child(label)
435                            .when_some(branch, |this, branch| {
436                                this.child(
437                                    Label::new("/")
438                                        .size(LabelSize::Small)
439                                        .color(slash_color)
440                                        .flex_shrink_0(),
441                                )
442                                .child(
443                                    Label::new(branch)
444                                        .size(LabelSize::Small)
445                                        .color(Color::Muted)
446                                        .truncate(),
447                                )
448                            })
449                            .into_any_element(),
450                    );
451                }
452            }
453        }
454
455        let has_worktree = !worktree_labels.is_empty();
456
457        let unified_tooltip = {
458            let title = tooltip_title;
459            let status = tooltip_status;
460            let worktrees = tooltip_worktrees;
461            let added = tooltip_added;
462            let removed = tooltip_removed;
463
464            Tooltip::element(move |_window, cx| {
465                v_flex()
466                    .min_w_0()
467                    .gap_1()
468                    .child(Label::new(title.clone()))
469                    .children(worktrees.iter().map(|wt| {
470                        let is_linked = wt.kind == WorktreeKind::Linked;
471
472                        v_flex()
473                            .gap_1()
474                            .when(is_linked, |this| {
475                                this.child(
476                                    v_flex()
477                                        .child(
478                                            h_flex()
479                                                .gap_1()
480                                                .child(
481                                                    Icon::new(IconName::GitWorktree)
482                                                        .size(IconSize::Small)
483                                                        .color(Color::Muted),
484                                                )
485                                                .child(
486                                                    Label::new(wt.name.clone())
487                                                        .size(LabelSize::Small)
488                                                        .color(Color::Muted),
489                                                ),
490                                        )
491                                        .child(
492                                            div()
493                                                .pl(IconSize::Small.rems() + rems(0.25))
494                                                .w(px(280.))
495                                                .whitespace_normal()
496                                                .text_ui_sm(cx)
497                                                .text_color(
498                                                    cx.theme().colors().text_muted.opacity(0.8),
499                                                )
500                                                .child(wt.full_path.clone()),
501                                        ),
502                                )
503                            })
504                            .when_some(wt.branch_name.clone(), |this, branch| {
505                                this.child(
506                                    h_flex()
507                                        .gap_1()
508                                        .child(
509                                            Icon::new(IconName::GitBranch)
510                                                .size(IconSize::Small)
511                                                .color(Color::Muted),
512                                        )
513                                        .child(
514                                            Label::new(branch)
515                                                .size(LabelSize::Small)
516                                                .color(Color::Muted),
517                                        ),
518                                )
519                            })
520                    }))
521                    .when(status == AgentThreadStatus::Error, |this| {
522                        this.child(
523                            h_flex()
524                                .gap_1()
525                                .pt_1()
526                                .border_t_1()
527                                .border_color(cx.theme().colors().border_variant)
528                                .child(
529                                    Icon::new(IconName::Close)
530                                        .size(IconSize::Small)
531                                        .color(Color::Error),
532                                )
533                                .child(Label::new("Error").size(LabelSize::Small)),
534                        )
535                    })
536                    .when(
537                        status == AgentThreadStatus::WaitingForConfirmation,
538                        |this| {
539                            this.child(
540                                h_flex()
541                                    .pt_1()
542                                    .border_t_1()
543                                    .border_color(cx.theme().colors().border_variant)
544                                    .gap_1()
545                                    .child(
546                                        Icon::new(IconName::Warning)
547                                            .size(IconSize::Small)
548                                            .color(Color::Warning),
549                                    )
550                                    .child(
551                                        Label::new("Waiting for Confirmation")
552                                            .size(LabelSize::Small),
553                                    ),
554                            )
555                        },
556                    )
557                    .when(added.is_some() || removed.is_some(), |this| {
558                        this.child(
559                            h_flex()
560                                .pt_1()
561                                .border_t_1()
562                                .border_color(cx.theme().colors().border_variant)
563                                .gap_1()
564                                .child(DiffStat::new(
565                                    "diff",
566                                    added.unwrap_or(0),
567                                    removed.unwrap_or(0),
568                                ))
569                                .child(
570                                    Label::new("Unreviewed Changes")
571                                        .size(LabelSize::Small)
572                                        .color(Color::Muted),
573                                ),
574                        )
575                    })
576                    .into_any_element()
577            })
578        };
579
580        v_flex()
581            .id(self.id.clone())
582            .cursor_pointer()
583            .group("thread-item")
584            .relative()
585            .overflow_hidden()
586            .w_full()
587            .py_1()
588            .px_1p5()
589            .when(self.selected, |s| s.bg(color.element_active))
590            .border_1()
591            .border_color(gpui::transparent_black())
592            .when(self.focused, |s| s.border_color(color.border_focused))
593            .when(self.rounded, |s| s.rounded_sm())
594            .hover(|s| s.bg(hover_color))
595            .on_hover(self.on_hover)
596            .tooltip(unified_tooltip)
597            .child(
598                h_flex()
599                    .min_w_0()
600                    .w_full()
601                    .gap_2()
602                    .justify_between()
603                    .child(
604                        h_flex()
605                            .id("content")
606                            .min_w_0()
607                            .flex_1()
608                            .gap_1p5()
609                            .child(icon)
610                            .child(title_label),
611                    )
612                    .child(gradient_overlay)
613                    .when(self.hovered, |this| {
614                        this.when_some(self.action_slot, |this, slot| {
615                            let overlay = GradientFade::new(base_bg, hover_bg, hover_bg)
616                                .width(px(64.0))
617                                .right(px(6.))
618                                .gradient_stop(0.75)
619                                .group_name("thread-item");
620
621                            this.child(
622                                h_flex()
623                                    .relative()
624                                    .on_mouse_down(MouseButton::Left, |_, _, cx| {
625                                        cx.stop_propagation()
626                                    })
627                                    .child(overlay)
628                                    .child(slot),
629                            )
630                        })
631                    }),
632            )
633            .when(
634                has_project_name
635                    || has_project_paths
636                    || has_worktree
637                    || has_diff_stats
638                    || has_timestamp,
639                |this| {
640                    this.child(
641                        h_flex()
642                            .min_w_0()
643                            .gap_1p5()
644                            .child(icon_container()) // Icon Spacing
645                            .when(
646                                has_project_name || has_project_paths || has_worktree,
647                                |this| {
648                                    this.child(
649                                        h_flex()
650                                            .min_w_0()
651                                            .flex_shrink()
652                                            .overflow_hidden()
653                                            .gap_1p5()
654                                            .when_some(self.project_name, |this, name| {
655                                                this.child(
656                                                    Label::new(name)
657                                                        .size(LabelSize::Small)
658                                                        .color(Color::Muted),
659                                                )
660                                            })
661                                            .when(
662                                                has_project_name
663                                                    && (has_project_paths || has_worktree),
664                                                |this| this.child(dot_separator()),
665                                            )
666                                            .when_some(project_paths, |this, paths| {
667                                                this.child(
668                                                    Label::new(paths)
669                                                        .size(LabelSize::Small)
670                                                        .color(Color::Muted)
671                                                        .into_any_element(),
672                                                )
673                                            })
674                                            .when(has_project_paths && has_worktree, |this| {
675                                                this.child(dot_separator())
676                                            })
677                                            .children(worktree_labels),
678                                    )
679                                },
680                            )
681                            .when(
682                                (has_project_name || has_project_paths || has_worktree)
683                                    && (has_diff_stats || has_timestamp),
684                                |this| this.child(dot_separator()),
685                            )
686                            .when(has_diff_stats, |this| {
687                                this.child(DiffStat::new(diff_stat_id, added_count, removed_count))
688                            })
689                            .when(has_diff_stats && has_timestamp, |this| {
690                                this.child(dot_separator())
691                            })
692                            .when(has_timestamp, |this| {
693                                this.child(
694                                    Label::new(timestamp.clone())
695                                        .size(LabelSize::Small)
696                                        .color(Color::Muted),
697                                )
698                            }),
699                    )
700                },
701            )
702            .when_some(self.on_click, |this, on_click| this.on_click(on_click))
703    }
704}
705
706impl Component for ThreadItem {
707    fn scope() -> ComponentScope {
708        ComponentScope::Agent
709    }
710
711    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
712        let color = cx.theme().colors();
713        let bg = color
714            .title_bar_background
715            .blend(color.panel_background.opacity(0.25));
716
717        let container = || {
718            v_flex()
719                .w_72()
720                .border_1()
721                .border_color(color.border_variant)
722                .bg(bg)
723        };
724
725        let thread_item_examples = vec![
726            single_example(
727                "Default (minutes)",
728                container()
729                    .child(
730                        ThreadItem::new("ti-1", "Linking to the Agent Panel Depending on Settings")
731                            .icon(IconName::AiOpenAi)
732                            .timestamp("15m"),
733                    )
734                    .into_any_element(),
735            ),
736            single_example(
737                "Notified (weeks)",
738                container()
739                    .child(
740                        ThreadItem::new("ti-2", "Refine thread view scrolling behavior")
741                            .timestamp("1w")
742                            .notified(true),
743                    )
744                    .into_any_element(),
745            ),
746            single_example(
747                "Waiting for Confirmation",
748                container()
749                    .child(
750                        ThreadItem::new("ti-2b", "Execute shell command in terminal")
751                            .timestamp("2h")
752                            .status(AgentThreadStatus::WaitingForConfirmation),
753                    )
754                    .into_any_element(),
755            ),
756            single_example(
757                "Error",
758                container()
759                    .child(
760                        ThreadItem::new("ti-2c", "Failed to connect to language server")
761                            .timestamp("5h")
762                            .status(AgentThreadStatus::Error),
763                    )
764                    .into_any_element(),
765            ),
766            single_example(
767                "Running Agent",
768                container()
769                    .child(
770                        ThreadItem::new("ti-3", "Add line numbers option to FileEditBlock")
771                            .icon(IconName::AiClaude)
772                            .timestamp("23h")
773                            .status(AgentThreadStatus::Running),
774                    )
775                    .into_any_element(),
776            ),
777            single_example(
778                "In Worktree",
779                container()
780                    .child(
781                        ThreadItem::new("ti-4", "Add line numbers option to FileEditBlock")
782                            .icon(IconName::AiClaude)
783                            .timestamp("2w")
784                            .worktrees(vec![ThreadItemWorktreeInfo {
785                                name: "link-agent-panel".into(),
786                                full_path: "link-agent-panel".into(),
787                                highlight_positions: Vec::new(),
788                                kind: WorktreeKind::Linked,
789                                branch_name: None,
790                            }]),
791                    )
792                    .into_any_element(),
793            ),
794            single_example(
795                "With Changes (months)",
796                container()
797                    .child(
798                        ThreadItem::new("ti-5", "Managing user and project settings interactions")
799                            .icon(IconName::AiClaude)
800                            .timestamp("1mo")
801                            .added(10)
802                            .removed(3),
803                    )
804                    .into_any_element(),
805            ),
806            single_example(
807                "Worktree + Changes + Timestamp",
808                container()
809                    .child(
810                        ThreadItem::new("ti-5b", "Full metadata example")
811                            .icon(IconName::AiClaude)
812                            .worktrees(vec![ThreadItemWorktreeInfo {
813                                name: "my-project".into(),
814                                full_path: "my-project".into(),
815                                highlight_positions: Vec::new(),
816                                kind: WorktreeKind::Linked,
817                                branch_name: None,
818                            }])
819                            .added(42)
820                            .removed(17)
821                            .timestamp("3w"),
822                    )
823                    .into_any_element(),
824            ),
825            single_example(
826                "Worktree + Branch + Changes + Timestamp",
827                container()
828                    .child(
829                        ThreadItem::new("ti-5c", "Full metadata with branch")
830                            .icon(IconName::AiClaude)
831                            .worktrees(vec![ThreadItemWorktreeInfo {
832                                name: "my-project".into(),
833                                full_path: "/worktrees/my-project/zed".into(),
834                                highlight_positions: Vec::new(),
835                                kind: WorktreeKind::Linked,
836                                branch_name: Some("feature-branch".into()),
837                            }])
838                            .added(42)
839                            .removed(17)
840                            .timestamp("3w"),
841                    )
842                    .into_any_element(),
843            ),
844            single_example(
845                "Long Branch + Changes (truncation)",
846                container()
847                    .child(
848                        ThreadItem::new("ti-5d", "Metadata overflow with long branch name")
849                            .icon(IconName::AiClaude)
850                            .worktrees(vec![ThreadItemWorktreeInfo {
851                                name: "my-project".into(),
852                                full_path: "/worktrees/my-project/zed".into(),
853                                highlight_positions: Vec::new(),
854                                kind: WorktreeKind::Linked,
855                                branch_name: Some("fix-very-long-branch-name-here".into()),
856                            }])
857                            .added(108)
858                            .removed(53)
859                            .timestamp("2d"),
860                    )
861                    .into_any_element(),
862            ),
863            single_example(
864                "Main Branch + Changes + Timestamp",
865                container()
866                    .child(
867                        ThreadItem::new("ti-5e", "Main worktree branch with diff stats")
868                            .icon(IconName::ZedAgent)
869                            .worktrees(vec![ThreadItemWorktreeInfo {
870                                name: "zed".into(),
871                                full_path: "/projects/zed".into(),
872                                highlight_positions: Vec::new(),
873                                kind: WorktreeKind::Main,
874                                branch_name: Some("sidebar-show-branch-name".into()),
875                            }])
876                            .added(23)
877                            .removed(8)
878                            .timestamp("5m"),
879                    )
880                    .into_any_element(),
881            ),
882            single_example(
883                "Selected Item",
884                container()
885                    .child(
886                        ThreadItem::new("ti-6", "Refine textarea interaction behavior")
887                            .icon(IconName::AiGemini)
888                            .timestamp("45m")
889                            .selected(true),
890                    )
891                    .into_any_element(),
892            ),
893            single_example(
894                "Focused Item (Keyboard Selection)",
895                container()
896                    .child(
897                        ThreadItem::new("ti-7", "Implement keyboard navigation")
898                            .icon(IconName::AiClaude)
899                            .timestamp("12h")
900                            .focused(true),
901                    )
902                    .into_any_element(),
903            ),
904            single_example(
905                "Selected + Focused",
906                container()
907                    .child(
908                        ThreadItem::new("ti-8", "Active and keyboard-focused thread")
909                            .icon(IconName::AiGemini)
910                            .timestamp("2mo")
911                            .selected(true)
912                            .focused(true),
913                    )
914                    .into_any_element(),
915            ),
916            single_example(
917                "Hovered with Action Slot",
918                container()
919                    .child(
920                        ThreadItem::new("ti-9", "Hover to see action button")
921                            .icon(IconName::AiClaude)
922                            .timestamp("6h")
923                            .hovered(true)
924                            .action_slot(
925                                IconButton::new("delete", IconName::Trash)
926                                    .icon_size(IconSize::Small)
927                                    .icon_color(Color::Muted),
928                            ),
929                    )
930                    .into_any_element(),
931            ),
932            single_example(
933                "Search Highlight",
934                container()
935                    .child(
936                        ThreadItem::new("ti-10", "Implement keyboard navigation")
937                            .icon(IconName::AiClaude)
938                            .timestamp("4w")
939                            .highlight_positions(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
940                    )
941                    .into_any_element(),
942            ),
943            single_example(
944                "Worktree Search Highlight",
945                container()
946                    .child(
947                        ThreadItem::new("ti-11", "Search in worktree name")
948                            .icon(IconName::AiClaude)
949                            .timestamp("3mo")
950                            .worktrees(vec![ThreadItemWorktreeInfo {
951                                name: "my-project-name".into(),
952                                full_path: "my-project-name".into(),
953                                highlight_positions: vec![3, 4, 5, 6, 7, 8, 9, 10, 11],
954                                kind: WorktreeKind::Linked,
955                                branch_name: None,
956                            }]),
957                    )
958                    .into_any_element(),
959            ),
960        ];
961
962        Some(
963            example_group(thread_item_examples)
964                .vertical()
965                .into_any_element(),
966        )
967    }
968}