context_history.rs

  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}