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