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}