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