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}