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