1use std::sync::Arc;
2use std::sync::atomic::AtomicBool;
3
4use crate::{
5 context_picker::ContextPicker,
6 context_store::{self, ContextStore},
7};
8use agent::{HistoryEntry, HistoryStore};
9use fuzzy::StringMatchCandidate;
10use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
11use picker::{Picker, PickerDelegate};
12use ui::{ListItem, prelude::*};
13use workspace::Workspace;
14
15pub struct ThreadContextPicker {
16 picker: Entity<Picker<ThreadContextPickerDelegate>>,
17}
18
19impl ThreadContextPicker {
20 pub fn new(
21 thread_store: WeakEntity<HistoryStore>,
22 context_picker: WeakEntity<ContextPicker>,
23 context_store: WeakEntity<context_store::ContextStore>,
24 workspace: WeakEntity<Workspace>,
25 window: &mut Window,
26 cx: &mut Context<Self>,
27 ) -> Self {
28 let delegate = ThreadContextPickerDelegate::new(
29 thread_store,
30 context_picker,
31 context_store,
32 workspace,
33 );
34 let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
35
36 ThreadContextPicker { picker }
37 }
38}
39
40impl Focusable for ThreadContextPicker {
41 fn focus_handle(&self, cx: &App) -> FocusHandle {
42 self.picker.focus_handle(cx)
43 }
44}
45
46impl Render for ThreadContextPicker {
47 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
48 self.picker.clone()
49 }
50}
51
52pub struct ThreadContextPickerDelegate {
53 thread_store: WeakEntity<HistoryStore>,
54 context_picker: WeakEntity<ContextPicker>,
55 context_store: WeakEntity<context_store::ContextStore>,
56 workspace: WeakEntity<Workspace>,
57 matches: Vec<HistoryEntry>,
58 selected_index: usize,
59}
60
61impl ThreadContextPickerDelegate {
62 pub fn new(
63 thread_store: WeakEntity<HistoryStore>,
64 context_picker: WeakEntity<ContextPicker>,
65 context_store: WeakEntity<context_store::ContextStore>,
66 workspace: WeakEntity<Workspace>,
67 ) -> Self {
68 ThreadContextPickerDelegate {
69 thread_store,
70 context_picker,
71 context_store,
72 workspace,
73 matches: Vec::new(),
74 selected_index: 0,
75 }
76 }
77}
78
79impl PickerDelegate for ThreadContextPickerDelegate {
80 type ListItem = ListItem;
81
82 fn match_count(&self) -> usize {
83 self.matches.len()
84 }
85
86 fn selected_index(&self) -> usize {
87 self.selected_index
88 }
89
90 fn set_selected_index(
91 &mut self,
92 ix: usize,
93 _window: &mut Window,
94 _cx: &mut Context<Picker<Self>>,
95 ) {
96 self.selected_index = ix;
97 }
98
99 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
100 "Search threads…".into()
101 }
102
103 fn update_matches(
104 &mut self,
105 query: String,
106 window: &mut Window,
107 cx: &mut Context<Picker<Self>>,
108 ) -> Task<()> {
109 let Some(thread_store) = self.thread_store.upgrade() else {
110 return Task::ready(());
111 };
112
113 let search_task = search_threads(query, Arc::new(AtomicBool::default()), &thread_store, cx);
114 cx.spawn_in(window, async move |this, cx| {
115 let matches = search_task.await;
116 this.update(cx, |this, cx| {
117 this.delegate.matches = matches;
118 this.delegate.selected_index = 0;
119 cx.notify();
120 })
121 .ok();
122 })
123 }
124
125 fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
126 let Some(project) = self
127 .workspace
128 .upgrade()
129 .map(|w| w.read(cx).project().clone())
130 else {
131 return;
132 };
133 let Some((entry, thread_store)) = self
134 .matches
135 .get(self.selected_index)
136 .zip(self.thread_store.upgrade())
137 else {
138 return;
139 };
140
141 match entry {
142 HistoryEntry::AcpThread(thread) => {
143 let load_thread_task =
144 agent::load_agent_thread(thread.id.clone(), thread_store, project, cx);
145
146 cx.spawn(async move |this, cx| {
147 let thread = load_thread_task.await?;
148 this.update(cx, |this, cx| {
149 this.delegate
150 .context_store
151 .update(cx, |context_store, cx| {
152 context_store.add_thread(thread, true, cx)
153 })
154 .ok();
155 })
156 })
157 .detach_and_log_err(cx);
158 }
159 HistoryEntry::TextThread(thread) => {
160 let task = thread_store.update(cx, |this, cx| {
161 this.load_text_thread(thread.path.clone(), cx)
162 });
163
164 cx.spawn(async move |this, cx| {
165 let thread = task.await?;
166 this.update(cx, |this, cx| {
167 this.delegate
168 .context_store
169 .update(cx, |context_store, cx| {
170 context_store.add_text_thread(thread, true, cx)
171 })
172 .ok();
173 })
174 })
175 .detach_and_log_err(cx);
176 }
177 }
178 }
179
180 fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
181 self.context_picker
182 .update(cx, |_, cx| {
183 cx.emit(DismissEvent);
184 })
185 .ok();
186 }
187
188 fn render_match(
189 &self,
190 ix: usize,
191 selected: bool,
192 _window: &mut Window,
193 cx: &mut Context<Picker<Self>>,
194 ) -> Option<Self::ListItem> {
195 let thread = &self.matches.get(ix)?;
196
197 Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
198 render_thread_context_entry(thread, self.context_store.clone(), cx),
199 ))
200 }
201}
202
203pub fn render_thread_context_entry(
204 entry: &HistoryEntry,
205 context_store: WeakEntity<ContextStore>,
206 cx: &mut App,
207) -> Div {
208 let is_added = match entry {
209 HistoryEntry::AcpThread(thread) => context_store
210 .upgrade()
211 .is_some_and(|ctx_store| ctx_store.read(cx).includes_thread(&thread.id)),
212 HistoryEntry::TextThread(thread) => context_store
213 .upgrade()
214 .is_some_and(|ctx_store| ctx_store.read(cx).includes_text_thread(&thread.path)),
215 };
216
217 h_flex()
218 .gap_1p5()
219 .w_full()
220 .justify_between()
221 .child(
222 h_flex()
223 .gap_1p5()
224 .max_w_72()
225 .child(
226 Icon::new(IconName::Thread)
227 .size(IconSize::XSmall)
228 .color(Color::Muted),
229 )
230 .child(Label::new(entry.title().clone()).truncate()),
231 )
232 .when(is_added, |el| {
233 el.child(
234 h_flex()
235 .gap_1()
236 .child(
237 Icon::new(IconName::Check)
238 .size(IconSize::Small)
239 .color(Color::Success),
240 )
241 .child(Label::new("Added").size(LabelSize::Small)),
242 )
243 })
244}
245
246pub(crate) fn search_threads(
247 query: String,
248 cancellation_flag: Arc<AtomicBool>,
249 thread_store: &Entity<HistoryStore>,
250 cx: &mut App,
251) -> Task<Vec<HistoryEntry>> {
252 let threads = thread_store.read(cx).entries().collect();
253 if query.is_empty() {
254 return Task::ready(threads);
255 }
256
257 let executor = cx.background_executor().clone();
258 cx.background_spawn(async move {
259 let candidates = threads
260 .iter()
261 .enumerate()
262 .map(|(id, thread)| StringMatchCandidate::new(id, thread.title()))
263 .collect::<Vec<_>>();
264 let matches = fuzzy::match_strings(
265 &candidates,
266 &query,
267 false,
268 true,
269 100,
270 &cancellation_flag,
271 executor,
272 )
273 .await;
274
275 matches
276 .into_iter()
277 .map(|mat| threads[mat.candidate_id].clone())
278 .collect()
279 })
280}