thread_item.rs

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