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