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