1use std::sync::Arc;
2
3use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity};
4use picker::{Picker, PickerDelegate};
5use project::Project;
6use ui::utils::{DateTimeType, format_distance_from_now};
7use ui::{Avatar, ListItem, ListItemSpacing, prelude::*};
8use workspace::{Item, Workspace};
9
10use crate::{
11 AgentPanelDelegate, ContextStore, DEFAULT_TAB_TITLE, RemoteContextMetadata,
12 SavedContextMetadata,
13};
14
15#[derive(Clone)]
16pub enum ContextMetadata {
17 Remote(RemoteContextMetadata),
18 Saved(SavedContextMetadata),
19}
20
21enum SavedContextPickerEvent {
22 Confirmed(ContextMetadata),
23}
24
25pub struct ContextHistory {
26 picker: Entity<Picker<SavedContextPickerDelegate>>,
27 _subscriptions: Vec<Subscription>,
28 workspace: WeakEntity<Workspace>,
29}
30
31impl ContextHistory {
32 pub fn new(
33 project: Entity<Project>,
34 context_store: Entity<ContextStore>,
35 workspace: WeakEntity<Workspace>,
36 window: &mut Window,
37 cx: &mut Context<Self>,
38 ) -> Self {
39 let picker = cx.new(|cx| {
40 Picker::uniform_list(
41 SavedContextPickerDelegate::new(project, context_store.clone()),
42 window,
43 cx,
44 )
45 .modal(false)
46 .max_height(None)
47 });
48
49 let subscriptions = vec![
50 cx.observe_in(&context_store, window, |this, _, window, cx| {
51 this.picker
52 .update(cx, |picker, cx| picker.refresh(window, cx));
53 }),
54 cx.subscribe_in(&picker, window, Self::handle_picker_event),
55 ];
56
57 Self {
58 picker,
59 _subscriptions: subscriptions,
60 workspace,
61 }
62 }
63
64 fn handle_picker_event(
65 &mut self,
66 _: &Entity<Picker<SavedContextPickerDelegate>>,
67 event: &SavedContextPickerEvent,
68 window: &mut Window,
69 cx: &mut Context<Self>,
70 ) {
71 let SavedContextPickerEvent::Confirmed(context) = event;
72
73 let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
74 return;
75 };
76
77 self.workspace
78 .update(cx, |workspace, cx| match context {
79 ContextMetadata::Remote(metadata) => {
80 agent_panel_delegate
81 .open_remote_context(workspace, metadata.id.clone(), window, cx)
82 .detach_and_log_err(cx);
83 }
84 ContextMetadata::Saved(metadata) => {
85 agent_panel_delegate
86 .open_saved_context(workspace, metadata.path.clone(), window, cx)
87 .detach_and_log_err(cx);
88 }
89 })
90 .ok();
91 }
92}
93
94impl Render for ContextHistory {
95 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
96 div().size_full().child(self.picker.clone())
97 }
98}
99
100impl Focusable for ContextHistory {
101 fn focus_handle(&self, cx: &App) -> FocusHandle {
102 self.picker.focus_handle(cx)
103 }
104}
105
106impl EventEmitter<()> for ContextHistory {}
107
108impl Item for ContextHistory {
109 type Event = ();
110
111 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
112 "History".into()
113 }
114}
115
116struct SavedContextPickerDelegate {
117 store: Entity<ContextStore>,
118 project: Entity<Project>,
119 matches: Vec<ContextMetadata>,
120 selected_index: usize,
121}
122
123impl EventEmitter<SavedContextPickerEvent> for Picker<SavedContextPickerDelegate> {}
124
125impl SavedContextPickerDelegate {
126 fn new(project: Entity<Project>, store: Entity<ContextStore>) -> Self {
127 Self {
128 project,
129 store,
130 matches: Vec::new(),
131 selected_index: 0,
132 }
133 }
134}
135
136impl PickerDelegate for SavedContextPickerDelegate {
137 type ListItem = ListItem;
138
139 fn match_count(&self) -> usize {
140 self.matches.len()
141 }
142
143 fn selected_index(&self) -> usize {
144 self.selected_index
145 }
146
147 fn set_selected_index(
148 &mut self,
149 ix: usize,
150 _window: &mut Window,
151 _cx: &mut Context<Picker<Self>>,
152 ) {
153 self.selected_index = ix;
154 }
155
156 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
157 "Search...".into()
158 }
159
160 fn update_matches(
161 &mut self,
162 query: String,
163 _window: &mut Window,
164 cx: &mut Context<Picker<Self>>,
165 ) -> Task<()> {
166 let search = self.store.read(cx).search(query, cx);
167 cx.spawn(async move |this, cx| {
168 let matches = search.await;
169 this.update(cx, |this, cx| {
170 let host_contexts = this.delegate.store.read(cx).host_contexts();
171 this.delegate.matches = host_contexts
172 .iter()
173 .cloned()
174 .map(ContextMetadata::Remote)
175 .chain(matches.into_iter().map(ContextMetadata::Saved))
176 .collect();
177 this.delegate.selected_index = 0;
178 cx.notify();
179 })
180 .ok();
181 })
182 }
183
184 fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
185 if let Some(metadata) = self.matches.get(self.selected_index) {
186 cx.emit(SavedContextPickerEvent::Confirmed(metadata.clone()));
187 }
188 }
189
190 fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {}
191
192 fn render_match(
193 &self,
194 ix: usize,
195 selected: bool,
196 _window: &mut Window,
197 cx: &mut Context<Picker<Self>>,
198 ) -> Option<Self::ListItem> {
199 let context = self.matches.get(ix)?;
200 let item = match context {
201 ContextMetadata::Remote(context) => {
202 let host_user = self.project.read(cx).host().and_then(|collaborator| {
203 self.project
204 .read(cx)
205 .user_store()
206 .read(cx)
207 .get_cached_user(collaborator.user_id)
208 });
209 div()
210 .flex()
211 .w_full()
212 .justify_between()
213 .gap_2()
214 .child(
215 h_flex().flex_1().overflow_x_hidden().child(
216 Label::new(context.summary.clone().unwrap_or(DEFAULT_TAB_TITLE.into()))
217 .size(LabelSize::Small),
218 ),
219 )
220 .child(
221 h_flex()
222 .gap_2()
223 .children(if let Some(host_user) = host_user {
224 vec![
225 Avatar::new(host_user.avatar_uri.clone()).into_any_element(),
226 Label::new(format!("Shared by @{}", host_user.github_login))
227 .color(Color::Muted)
228 .size(LabelSize::Small)
229 .into_any_element(),
230 ]
231 } else {
232 vec![
233 Label::new("Shared by host")
234 .color(Color::Muted)
235 .size(LabelSize::Small)
236 .into_any_element(),
237 ]
238 }),
239 )
240 }
241 ContextMetadata::Saved(context) => div()
242 .flex()
243 .w_full()
244 .justify_between()
245 .gap_2()
246 .child(
247 h_flex()
248 .flex_1()
249 .child(Label::new(context.title.clone()).size(LabelSize::Small))
250 .overflow_x_hidden(),
251 )
252 .child(
253 Label::new(format_distance_from_now(
254 DateTimeType::Local(context.mtime),
255 false,
256 true,
257 true,
258 ))
259 .color(Color::Muted)
260 .size(LabelSize::Small),
261 ),
262 };
263 Some(
264 ListItem::new(ix)
265 .inset(true)
266 .spacing(ListItemSpacing::Sparse)
267 .toggle_state(selected)
268 .child(item),
269 )
270 }
271}