thread_context_picker.rs

  1use std::sync::Arc;
  2
  3use fuzzy::StringMatchCandidate;
  4use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
  5use picker::{Picker, PickerDelegate};
  6use ui::{prelude::*, ListItem};
  7
  8use crate::context::ContextKind;
  9use crate::context_picker::{ConfirmBehavior, ContextPicker};
 10use crate::context_store;
 11use crate::thread::ThreadId;
 12use crate::thread_store::ThreadStore;
 13
 14pub struct ThreadContextPicker {
 15    picker: View<Picker<ThreadContextPickerDelegate>>,
 16}
 17
 18impl ThreadContextPicker {
 19    pub fn new(
 20        thread_store: WeakModel<ThreadStore>,
 21        context_picker: WeakView<ContextPicker>,
 22        context_store: WeakModel<context_store::ContextStore>,
 23        confirm_behavior: ConfirmBehavior,
 24        cx: &mut ViewContext<Self>,
 25    ) -> Self {
 26        let delegate = ThreadContextPickerDelegate::new(
 27            thread_store,
 28            context_picker,
 29            context_store,
 30            confirm_behavior,
 31        );
 32        let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
 33
 34        ThreadContextPicker { picker }
 35    }
 36}
 37
 38impl FocusableView for ThreadContextPicker {
 39    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
 40        self.picker.focus_handle(cx)
 41    }
 42}
 43
 44impl Render for ThreadContextPicker {
 45    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
 46        self.picker.clone()
 47    }
 48}
 49
 50#[derive(Debug, Clone)]
 51struct ThreadContextEntry {
 52    id: ThreadId,
 53    summary: SharedString,
 54}
 55
 56pub struct ThreadContextPickerDelegate {
 57    thread_store: WeakModel<ThreadStore>,
 58    context_picker: WeakView<ContextPicker>,
 59    context_store: WeakModel<context_store::ContextStore>,
 60    confirm_behavior: ConfirmBehavior,
 61    matches: Vec<ThreadContextEntry>,
 62    selected_index: usize,
 63}
 64
 65impl ThreadContextPickerDelegate {
 66    pub fn new(
 67        thread_store: WeakModel<ThreadStore>,
 68        context_picker: WeakView<ContextPicker>,
 69        context_store: WeakModel<context_store::ContextStore>,
 70        confirm_behavior: ConfirmBehavior,
 71    ) -> Self {
 72        ThreadContextPickerDelegate {
 73            thread_store,
 74            context_picker,
 75            context_store,
 76            confirm_behavior,
 77            matches: Vec::new(),
 78            selected_index: 0,
 79        }
 80    }
 81}
 82
 83impl PickerDelegate for ThreadContextPickerDelegate {
 84    type ListItem = ListItem;
 85
 86    fn match_count(&self) -> usize {
 87        self.matches.len()
 88    }
 89
 90    fn selected_index(&self) -> usize {
 91        self.selected_index
 92    }
 93
 94    fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
 95        self.selected_index = ix;
 96    }
 97
 98    fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
 99        "Search threads…".into()
100    }
101
102    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
103        let Ok(threads) = self.thread_store.update(cx, |this, cx| {
104            this.threads(cx)
105                .into_iter()
106                .map(|thread| {
107                    const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
108
109                    let id = thread.read(cx).id().clone();
110                    let summary = thread.read(cx).summary().unwrap_or(DEFAULT_SUMMARY);
111                    ThreadContextEntry { id, summary }
112                })
113                .collect::<Vec<_>>()
114        }) else {
115            return Task::ready(());
116        };
117
118        let executor = cx.background_executor().clone();
119        let search_task = cx.background_executor().spawn(async move {
120            if query.is_empty() {
121                threads
122            } else {
123                let candidates = threads
124                    .iter()
125                    .enumerate()
126                    .map(|(id, thread)| StringMatchCandidate::new(id, &thread.summary))
127                    .collect::<Vec<_>>();
128                let matches = fuzzy::match_strings(
129                    &candidates,
130                    &query,
131                    false,
132                    100,
133                    &Default::default(),
134                    executor,
135                )
136                .await;
137
138                matches
139                    .into_iter()
140                    .map(|mat| threads[mat.candidate_id].clone())
141                    .collect()
142            }
143        });
144
145        cx.spawn(|this, mut cx| async move {
146            let matches = search_task.await;
147            this.update(&mut cx, |this, cx| {
148                this.delegate.matches = matches;
149                this.delegate.selected_index = 0;
150                cx.notify();
151            })
152            .ok();
153        })
154    }
155
156    fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
157        let Some(entry) = self.matches.get(self.selected_index) else {
158            return;
159        };
160
161        let Some(thread_store) = self.thread_store.upgrade() else {
162            return;
163        };
164
165        let Some(thread) = thread_store.update(cx, |this, cx| this.open_thread(&entry.id, cx))
166        else {
167            return;
168        };
169
170        self.context_store
171            .update(cx, |context_store, cx| {
172                let text = thread.update(cx, |thread, _cx| {
173                    let mut text = String::new();
174
175                    for message in thread.messages() {
176                        text.push_str(match message.role {
177                            language_model::Role::User => "User:",
178                            language_model::Role::Assistant => "Assistant:",
179                            language_model::Role::System => "System:",
180                        });
181                        text.push('\n');
182
183                        text.push_str(&message.text);
184                        text.push('\n');
185                    }
186
187                    text
188                });
189
190                context_store.insert_context(ContextKind::Thread, entry.summary.clone(), text);
191            })
192            .ok();
193
194        match self.confirm_behavior {
195            ConfirmBehavior::KeepOpen => {}
196            ConfirmBehavior::Close => self.dismissed(cx),
197        }
198    }
199
200    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
201        self.context_picker
202            .update(cx, |this, cx| {
203                this.reset_mode();
204                cx.emit(DismissEvent);
205            })
206            .ok();
207    }
208
209    fn render_match(
210        &self,
211        ix: usize,
212        selected: bool,
213        _cx: &mut ViewContext<Picker<Self>>,
214    ) -> Option<Self::ListItem> {
215        let thread = &self.matches[ix];
216
217        Some(
218            ListItem::new(ix)
219                .inset(true)
220                .toggle_state(selected)
221                .child(Label::new(thread.summary.clone())),
222        )
223    }
224}