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