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