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            .overflow_hidden()
391            .w_full()
392            .py_1()
393            .px_1p5()
394            .when(self.selected, |s| s.bg(color.element_active))
395            .border_1()
396            .border_color(gpui::transparent_black())
397            .when(self.focused, |s| s.border_color(color.border_focused))
398            .when(self.rounded, |s| s.rounded_sm())
399            .hover(|s| s.bg(hover_color))
400            .on_hover(self.on_hover)
401            .child(
402                h_flex()
403                    .min_w_0()
404                    .w_full()
405                    .gap_2()
406                    .justify_between()
407                    .child(
408                        h_flex()
409                            .id("content")
410                            .min_w_0()
411                            .flex_1()
412                            .gap_1p5()
413                            .child(icon)
414                            .child(title_label),
415                    )
416                    .child(gradient_overlay)
417                    .when(self.hovered, |this| {
418                        this.when_some(self.action_slot, |this, slot| {
419                            let overlay = GradientFade::new(base_bg, hover_bg, hover_bg)
420                                .width(px(64.0))
421                                .right(px(6.))
422                                .gradient_stop(0.75)
423                                .group_name("thread-item");
424
425                            this.child(
426                                h_flex()
427                                    .relative()
428                                    .on_mouse_down(MouseButton::Left, |_, _, cx| {
429                                        cx.stop_propagation()
430                                    })
431                                    .child(overlay)
432                                    .child(slot),
433                            )
434                        })
435                    }),
436            )
437            .when(has_metadata, |this| {
438                this.child(
439                    h_flex()
440                        .gap_1p5()
441                        .child(icon_container()) // Icon Spacing
442                        .when(self.archived, |this| {
443                            this.child(
444                                Icon::new(IconName::Archive).size(IconSize::XSmall).color(
445                                    Color::Custom(cx.theme().colors().icon_muted.opacity(0.5)),
446                                ),
447                            )
448                            // .child(dot_separator())
449                        })
450                        .when(
451                            has_project_name || has_project_paths || has_worktree,
452                            |this| {
453                                this.when_some(self.project_name, |this, name| {
454                                    this.child(
455                                        Label::new(name).size(LabelSize::Small).color(Color::Muted),
456                                    )
457                                })
458                                .when(
459                                    has_project_name && (has_project_paths || has_worktree),
460                                    |this| this.child(dot_separator()),
461                                )
462                                .when_some(project_paths, |this, paths| {
463                                    this.child(
464                                        Label::new(paths)
465                                            .size(LabelSize::Small)
466                                            .color(Color::Muted),
467                                    )
468                                })
469                                .when(has_project_paths && has_worktree, |this| {
470                                    this.child(dot_separator())
471                                })
472                                .children(
473                                    linked_worktrees.into_iter().map(|wt| {
474                                        let worktree_label = wt.worktree_name.clone().map(|name| {
475                                            if wt.highlight_positions.is_empty() {
476                                                Label::new(name)
477                                                    .size(LabelSize::Small)
478                                                    .color(Color::Muted)
479                                                    .truncate()
480                                                    .into_any_element()
481                                            } else {
482                                                HighlightedLabel::new(
483                                                    name,
484                                                    wt.highlight_positions.clone(),
485                                                )
486                                                .size(LabelSize::Small)
487                                                .color(Color::Muted)
488                                                .truncate()
489                                                .into_any_element()
490                                            }
491                                        });
492
493                                        // When only the branch is shown, lead with a branch icon;
494                                        // otherwise keep the worktree icon (which "covers" both the
495                                        // worktree and any accompanying branch).
496                                        let chip_icon = if wt.worktree_name.is_none()
497                                            && wt.branch_name.is_some()
498                                        {
499                                            IconName::GitBranch
500                                        } else {
501                                            IconName::GitWorktree
502                                        };
503
504                                        let branch_label = wt.branch_name.map(|branch| {
505                                            Label::new(branch)
506                                                .size(LabelSize::Small)
507                                                .color(Color::Muted)
508                                                .truncate()
509                                                .into_any_element()
510                                        });
511
512                                        let show_separator =
513                                            worktree_label.is_some() && branch_label.is_some();
514
515                                        h_flex()
516                                            .min_w_0()
517                                            .gap_0p5()
518                                            .child(
519                                                Icon::new(chip_icon)
520                                                    .size(IconSize::XSmall)
521                                                    .color(Color::Muted),
522                                            )
523                                            .when_some(worktree_label, |this, label| {
524                                                this.child(label)
525                                            })
526                                            .when(show_separator, |this| {
527                                                this.child(
528                                                    Label::new("/")
529                                                        .size(LabelSize::Small)
530                                                        .color(separator_color)
531                                                        .flex_shrink_0(),
532                                                )
533                                            })
534                                            .when_some(branch_label, |this, label| {
535                                                this.child(label)
536                                            })
537                                    }),
538                                )
539                            },
540                        )
541                        .when(
542                            (has_project_name || has_project_paths || has_worktree)
543                                && (has_diff_stats || has_timestamp),
544                            |this| this.child(dot_separator()),
545                        )
546                        .when(has_diff_stats, |this| {
547                            this.child(DiffStat::new(diff_stat_id, added_count, removed_count))
548                        })
549                        .when(has_diff_stats && has_timestamp, |this| {
550                            this.child(dot_separator())
551                        })
552                        .when(has_timestamp, |this| {
553                            this.child(
554                                Label::new(timestamp.clone())
555                                    .size(LabelSize::Small)
556                                    .color(Color::Muted),
557                            )
558                        }),
559                )
560            })
561            .when(show_tooltip, |this| {
562                let status = self.status;
563                this.tooltip(Tooltip::element(move |_, _| match status {
564                    AgentThreadStatus::Error => h_flex()
565                        .gap_1()
566                        .child(
567                            Icon::new(IconName::Close)
568                                .size(IconSize::Small)
569                                .color(Color::Error),
570                        )
571                        .child(Label::new("Thread has an Error"))
572                        .into_any_element(),
573                    AgentThreadStatus::WaitingForConfirmation => h_flex()
574                        .gap_1()
575                        .child(
576                            Icon::new(IconName::Warning)
577                                .size(IconSize::Small)
578                                .color(Color::Warning),
579                        )
580                        .child(Label::new("Waiting for Confirmation"))
581                        .into_any_element(),
582                    _ => gpui::Empty.into_any_element(),
583                }))
584            })
585            .when_some(self.on_click, |this, on_click| this.on_click(on_click))
586    }
587}
588
589impl Component for ThreadItem {
590    fn scope() -> ComponentScope {
591        ComponentScope::Agent
592    }
593
594    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
595        let color = cx.theme().colors();
596        let bg = color
597            .title_bar_background
598            .blend(color.panel_background.opacity(0.25));
599
600        let container = || {
601            v_flex()
602                .w_72()
603                .border_1()
604                .border_color(color.border_variant)
605                .bg(bg)
606        };
607
608        let thread_item_examples = vec![
609            single_example(
610                "Default",
611                container()
612                    .child(
613                        ThreadItem::new("ti-1", "Linking to the Agent Panel Depending on Settings")
614                            .icon(IconName::AiOpenAi)
615                            .timestamp("15m"),
616                    )
617                    .into_any_element(),
618            ),
619            single_example(
620                "Waiting for Confirmation",
621                container()
622                    .child(
623                        ThreadItem::new("ti-2b", "Execute shell command in terminal")
624                            .timestamp("2h")
625                            .status(AgentThreadStatus::WaitingForConfirmation),
626                    )
627                    .into_any_element(),
628            ),
629            single_example(
630                "Error",
631                container()
632                    .child(
633                        ThreadItem::new("ti-2c", "Failed to connect to language server")
634                            .timestamp("5h")
635                            .status(AgentThreadStatus::Error),
636                    )
637                    .into_any_element(),
638            ),
639            single_example(
640                "Running Agent",
641                container()
642                    .child(
643                        ThreadItem::new("ti-3", "Add line numbers option to FileEditBlock")
644                            .icon(IconName::AiClaude)
645                            .timestamp("23h")
646                            .status(AgentThreadStatus::Running),
647                    )
648                    .into_any_element(),
649            ),
650            single_example(
651                "In Worktree",
652                container()
653                    .child(
654                        ThreadItem::new("ti-4", "Add line numbers option to FileEditBlock")
655                            .icon(IconName::AiClaude)
656                            .timestamp("2w")
657                            .worktrees(vec![ThreadItemWorktreeInfo {
658                                worktree_name: Some("link-agent-panel".into()),
659                                full_path: "link-agent-panel".into(),
660                                highlight_positions: Vec::new(),
661                                kind: WorktreeKind::Linked,
662                                branch_name: None,
663                            }]),
664                    )
665                    .into_any_element(),
666            ),
667            single_example(
668                "With Changes",
669                container()
670                    .child(
671                        ThreadItem::new("ti-5", "Managing user and project settings interactions")
672                            .icon(IconName::AiClaude)
673                            .timestamp("1mo")
674                            .added(10)
675                            .removed(3),
676                    )
677                    .into_any_element(),
678            ),
679            single_example(
680                "Worktree + Changes + Timestamp",
681                container()
682                    .child(
683                        ThreadItem::new("ti-5b", "Full metadata example")
684                            .icon(IconName::AiClaude)
685                            .worktrees(vec![ThreadItemWorktreeInfo {
686                                worktree_name: Some("my-project".into()),
687                                full_path: "my-project".into(),
688                                highlight_positions: Vec::new(),
689                                kind: WorktreeKind::Linked,
690                                branch_name: None,
691                            }])
692                            .added(42)
693                            .removed(17)
694                            .timestamp("3w"),
695                    )
696                    .into_any_element(),
697            ),
698            single_example(
699                "Worktree + Branch + Changes + Timestamp",
700                container()
701                    .child(
702                        ThreadItem::new("ti-5c", "Full metadata with branch")
703                            .icon(IconName::AiClaude)
704                            .worktrees(vec![ThreadItemWorktreeInfo {
705                                worktree_name: Some("my-project".into()),
706                                full_path: "/worktrees/my-project/zed".into(),
707                                highlight_positions: Vec::new(),
708                                kind: WorktreeKind::Linked,
709                                branch_name: Some("feature-branch".into()),
710                            }])
711                            .added(42)
712                            .removed(17)
713                            .timestamp("3w"),
714                    )
715                    .into_any_element(),
716            ),
717            single_example(
718                "Long Branch + Changes (truncation)",
719                container()
720                    .child(
721                        ThreadItem::new("ti-5d", "Metadata overflow with long branch name")
722                            .icon(IconName::AiClaude)
723                            .worktrees(vec![ThreadItemWorktreeInfo {
724                                worktree_name: Some("my-project".into()),
725                                full_path: "/worktrees/my-project/zed".into(),
726                                highlight_positions: Vec::new(),
727                                kind: WorktreeKind::Linked,
728                                branch_name: Some("fix-very-long-branch-name-here".into()),
729                            }])
730                            .added(108)
731                            .removed(53)
732                            .timestamp("2d"),
733                    )
734                    .into_any_element(),
735            ),
736            single_example(
737                "Main Worktree (hidden) + Changes + Timestamp",
738                container()
739                    .child(
740                        ThreadItem::new("ti-5e", "Main worktree branch with diff stats")
741                            .icon(IconName::ZedAgent)
742                            .worktrees(vec![ThreadItemWorktreeInfo {
743                                worktree_name: Some("zed".into()),
744                                full_path: "/projects/zed".into(),
745                                highlight_positions: Vec::new(),
746                                kind: WorktreeKind::Main,
747                                branch_name: Some("sidebar-show-branch-name".into()),
748                            }])
749                            .added(23)
750                            .removed(8)
751                            .timestamp("5m"),
752                    )
753                    .into_any_element(),
754            ),
755            single_example(
756                "Long Worktree Name (truncation)",
757                container()
758                    .child(
759                        ThreadItem::new("ti-5f", "Thread with a very long worktree name")
760                            .icon(IconName::AiClaude)
761                            .worktrees(vec![ThreadItemWorktreeInfo {
762                                worktree_name: Some(
763                                    "very-long-worktree-name-that-should-truncate".into(),
764                                ),
765                                full_path: "/worktrees/very-long-worktree-name/zed".into(),
766                                highlight_positions: Vec::new(),
767                                kind: WorktreeKind::Linked,
768                                branch_name: None,
769                            }])
770                            .timestamp("1h"),
771                    )
772                    .into_any_element(),
773            ),
774            single_example(
775                "Worktree with Search Highlights",
776                container()
777                    .child(
778                        ThreadItem::new("ti-5g", "Filtered thread with highlighted worktree")
779                            .icon(IconName::AiClaude)
780                            .worktrees(vec![ThreadItemWorktreeInfo {
781                                worktree_name: Some("jade-glen".into()),
782                                full_path: "/worktrees/jade-glen/zed".into(),
783                                highlight_positions: vec![0, 1, 2, 3],
784                                kind: WorktreeKind::Linked,
785                                branch_name: Some("fix-scrolling".into()),
786                            }])
787                            .timestamp("3d"),
788                    )
789                    .into_any_element(),
790            ),
791            single_example(
792                "Multiple Worktrees (no branches)",
793                container()
794                    .child(
795                        ThreadItem::new("ti-5h", "Thread spanning multiple worktrees")
796                            .icon(IconName::AiClaude)
797                            .worktrees(vec![
798                                ThreadItemWorktreeInfo {
799                                    worktree_name: Some("jade-glen".into()),
800                                    full_path: "/worktrees/jade-glen/zed".into(),
801                                    highlight_positions: Vec::new(),
802                                    kind: WorktreeKind::Linked,
803                                    branch_name: None,
804                                },
805                                ThreadItemWorktreeInfo {
806                                    worktree_name: Some("fawn-otter".into()),
807                                    full_path: "/worktrees/fawn-otter/zed-slides".into(),
808                                    highlight_positions: Vec::new(),
809                                    kind: WorktreeKind::Linked,
810                                    branch_name: None,
811                                },
812                            ])
813                            .timestamp("2h"),
814                    )
815                    .into_any_element(),
816            ),
817            single_example(
818                "Multiple Worktrees with Branches",
819                container()
820                    .child(
821                        ThreadItem::new("ti-5i", "Multi-root with per-worktree branches")
822                            .icon(IconName::ZedAgent)
823                            .worktrees(vec![
824                                ThreadItemWorktreeInfo {
825                                    worktree_name: Some("jade-glen".into()),
826                                    full_path: "/worktrees/jade-glen/zed".into(),
827                                    highlight_positions: Vec::new(),
828                                    kind: WorktreeKind::Linked,
829                                    branch_name: Some("fix".into()),
830                                },
831                                ThreadItemWorktreeInfo {
832                                    worktree_name: Some("fawn-otter".into()),
833                                    full_path: "/worktrees/fawn-otter/zed-slides".into(),
834                                    highlight_positions: Vec::new(),
835                                    kind: WorktreeKind::Linked,
836                                    branch_name: Some("main".into()),
837                                },
838                            ])
839                            .timestamp("15m"),
840                    )
841                    .into_any_element(),
842            ),
843            single_example(
844                "Project Name + Worktree + Branch",
845                container()
846                    .child(
847                        ThreadItem::new("ti-5j", "Thread with project context")
848                            .icon(IconName::AiClaude)
849                            .project_name("my-remote-server")
850                            .worktrees(vec![ThreadItemWorktreeInfo {
851                                worktree_name: Some("jade-glen".into()),
852                                full_path: "/worktrees/jade-glen/zed".into(),
853                                highlight_positions: Vec::new(),
854                                kind: WorktreeKind::Linked,
855                                branch_name: Some("feature-branch".into()),
856                            }])
857                            .timestamp("1d"),
858                    )
859                    .into_any_element(),
860            ),
861            single_example(
862                "Project Paths + Worktree (archive view)",
863                container()
864                    .child(
865                        ThreadItem::new("ti-5k", "Archived thread with folder paths")
866                            .icon(IconName::AiClaude)
867                            .project_paths(Arc::from(vec![
868                                PathBuf::from("/projects/zed"),
869                                PathBuf::from("/projects/zed-slides"),
870                            ]))
871                            .worktrees(vec![ThreadItemWorktreeInfo {
872                                worktree_name: Some("jade-glen".into()),
873                                full_path: "/worktrees/jade-glen/zed".into(),
874                                highlight_positions: Vec::new(),
875                                kind: WorktreeKind::Linked,
876                                branch_name: Some("feature".into()),
877                            }])
878                            .timestamp("2mo"),
879                    )
880                    .into_any_element(),
881            ),
882            single_example(
883                "All Metadata",
884                container()
885                    .child(
886                        ThreadItem::new("ti-5l", "Thread with every metadata field populated")
887                            .icon(IconName::ZedAgent)
888                            .project_name("remote-dev")
889                            .worktrees(vec![ThreadItemWorktreeInfo {
890                                worktree_name: Some("my-worktree".into()),
891                                full_path: "/worktrees/my-worktree/zed".into(),
892                                highlight_positions: Vec::new(),
893                                kind: WorktreeKind::Linked,
894                                branch_name: Some("main".into()),
895                            }])
896                            .added(15)
897                            .removed(4)
898                            .timestamp("8h"),
899                    )
900                    .into_any_element(),
901            ),
902            single_example(
903                "Focused Item (Keyboard Selection)",
904                container()
905                    .child(
906                        ThreadItem::new("ti-7", "Implement keyboard navigation")
907                            .icon(IconName::AiClaude)
908                            .timestamp("12h")
909                            .focused(true),
910                    )
911                    .into_any_element(),
912            ),
913            single_example(
914                "Action Slot",
915                container()
916                    .child(
917                        ThreadItem::new("ti-9", "Hover to see action button")
918                            .icon(IconName::AiClaude)
919                            .timestamp("6h")
920                            .hovered(true)
921                            .action_slot(
922                                IconButton::new("delete", IconName::Trash)
923                                    .icon_size(IconSize::Small)
924                                    .icon_color(Color::Muted),
925                            ),
926                    )
927                    .into_any_element(),
928            ),
929        ];
930
931        Some(
932            example_group(thread_item_examples)
933                .vertical()
934                .into_any_element(),
935        )
936    }
937}