1use crate::{Chip, Indicator, SpinnerLabel, prelude::*};
2use gpui::{ClickEvent, SharedString};
3
4#[derive(IntoElement, RegisterComponent)]
5pub struct ThreadItem {
6 id: ElementId,
7 icon: IconName,
8 title: SharedString,
9 timestamp: SharedString,
10 running: bool,
11 generation_done: bool,
12 selected: bool,
13 has_changes: bool,
14 worktree: Option<SharedString>,
15 on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
16}
17
18impl ThreadItem {
19 pub fn new(id: impl Into<ElementId>, title: impl Into<SharedString>) -> Self {
20 Self {
21 id: id.into(),
22 icon: IconName::ZedAgent,
23 title: title.into(),
24 timestamp: "".into(),
25 running: false,
26 generation_done: false,
27 selected: false,
28 has_changes: false,
29 worktree: None,
30 on_click: None,
31 }
32 }
33
34 pub fn timestamp(mut self, timestamp: impl Into<SharedString>) -> Self {
35 self.timestamp = timestamp.into();
36 self
37 }
38
39 pub fn icon(mut self, icon: IconName) -> Self {
40 self.icon = icon;
41 self
42 }
43
44 pub fn running(mut self, running: bool) -> Self {
45 self.running = running;
46 self
47 }
48
49 pub fn generation_done(mut self, generation_done: bool) -> Self {
50 self.generation_done = generation_done;
51 self
52 }
53
54 pub fn selected(mut self, selected: bool) -> Self {
55 self.selected = selected;
56 self
57 }
58
59 pub fn has_changes(mut self, has_changes: bool) -> Self {
60 self.has_changes = has_changes;
61 self
62 }
63
64 pub fn worktree(mut self, worktree: impl Into<SharedString>) -> Self {
65 self.worktree = Some(worktree.into());
66 self
67 }
68
69 pub fn on_click(
70 mut self,
71 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
72 ) -> Self {
73 self.on_click = Some(Box::new(handler));
74 self
75 }
76}
77
78impl RenderOnce for ThreadItem {
79 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
80 let icon_container = || h_flex().size_4().justify_center();
81 let icon = if self.generation_done {
82 icon_container().child(Indicator::dot().color(Color::Accent))
83 } else if self.running {
84 icon_container().child(SpinnerLabel::new().color(Color::Accent))
85 } else {
86 icon_container().child(
87 Icon::new(self.icon)
88 .color(Color::Muted)
89 .size(IconSize::Small),
90 )
91 };
92
93 v_flex()
94 .id(self.id)
95 .cursor_pointer()
96 .p_2()
97 .when(self.selected, |this| {
98 this.bg(cx.theme().colors().element_active)
99 })
100 .hover(|s| s.bg(cx.theme().colors().element_hover))
101 .child(
102 h_flex()
103 .w_full()
104 .gap_1p5()
105 .child(icon)
106 .child(Label::new(self.title).truncate()),
107 )
108 .child(
109 h_flex()
110 .gap_1p5()
111 .child(icon_container()) // Icon Spacing
112 .when_some(self.worktree, |this, name| {
113 this.child(Chip::new(name).label_size(LabelSize::XSmall))
114 })
115 .child(
116 Label::new(self.timestamp)
117 .size(LabelSize::Small)
118 .color(Color::Muted),
119 )
120 .child(
121 Label::new("•")
122 .size(LabelSize::Small)
123 .color(Color::Muted)
124 .alpha(0.5),
125 )
126 .when(!self.has_changes, |this| {
127 this.child(
128 Label::new("No Changes")
129 .size(LabelSize::Small)
130 .color(Color::Muted),
131 )
132 }),
133 )
134 .when_some(self.on_click, |this, on_click| this.on_click(on_click))
135 }
136}
137
138impl Component for ThreadItem {
139 fn scope() -> ComponentScope {
140 ComponentScope::Agent
141 }
142
143 fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
144 let container = || {
145 v_flex()
146 .w_72()
147 .border_1()
148 .border_color(cx.theme().colors().border_variant)
149 .bg(cx.theme().colors().panel_background)
150 };
151
152 let thread_item_examples = vec![
153 single_example(
154 "Default",
155 container()
156 .child(
157 ThreadItem::new("ti-1", "Linking to the Agent Panel Depending on Settings")
158 .icon(IconName::AiOpenAi)
159 .timestamp("1:33 AM"),
160 )
161 .into_any_element(),
162 ),
163 single_example(
164 "Generation Done",
165 container()
166 .child(
167 ThreadItem::new("ti-2", "Refine thread view scrolling behavior")
168 .timestamp("12:12 AM")
169 .generation_done(true),
170 )
171 .into_any_element(),
172 ),
173 single_example(
174 "Running Agent",
175 container()
176 .child(
177 ThreadItem::new("ti-3", "Add line numbers option to FileEditBlock")
178 .icon(IconName::AiClaude)
179 .timestamp("7:30 PM")
180 .running(true),
181 )
182 .into_any_element(),
183 ),
184 single_example(
185 "In Worktree",
186 container()
187 .child(
188 ThreadItem::new("ti-4", "Add line numbers option to FileEditBlock")
189 .icon(IconName::AiClaude)
190 .timestamp("7:37 PM")
191 .worktree("link-agent-panel"),
192 )
193 .into_any_element(),
194 ),
195 single_example(
196 "Selected Item",
197 container()
198 .child(
199 ThreadItem::new("ti-5", "Refine textarea interaction behavior")
200 .icon(IconName::AiGemini)
201 .timestamp("3:00 PM")
202 .selected(true),
203 )
204 .into_any_element(),
205 ),
206 ];
207
208 Some(
209 example_group(thread_item_examples)
210 .vertical()
211 .into_any_element(),
212 )
213 }
214}