thread_history.rs

  1use gpui::{
  2    uniform_list, AppContext, FocusHandle, FocusableView, Model, ScrollStrategy,
  3    UniformListScrollHandle, WeakView,
  4};
  5use time::{OffsetDateTime, UtcOffset};
  6use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip};
  7
  8use crate::thread_store::{SavedThreadMetadata, ThreadStore};
  9use crate::{AssistantPanel, RemoveSelectedThread};
 10
 11pub struct ThreadHistory {
 12    focus_handle: FocusHandle,
 13    assistant_panel: WeakView<AssistantPanel>,
 14    thread_store: Model<ThreadStore>,
 15    scroll_handle: UniformListScrollHandle,
 16    selected_index: usize,
 17}
 18
 19impl ThreadHistory {
 20    pub(crate) fn new(
 21        assistant_panel: WeakView<AssistantPanel>,
 22        thread_store: Model<ThreadStore>,
 23        cx: &mut ViewContext<Self>,
 24    ) -> Self {
 25        Self {
 26            focus_handle: cx.focus_handle(),
 27            assistant_panel,
 28            thread_store,
 29            scroll_handle: UniformListScrollHandle::default(),
 30            selected_index: 0,
 31        }
 32    }
 33
 34    pub fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
 35        let count = self.thread_store.read(cx).thread_count();
 36        if count > 0 {
 37            if self.selected_index == 0 {
 38                self.set_selected_index(count - 1, cx);
 39            } else {
 40                self.set_selected_index(self.selected_index - 1, cx);
 41            }
 42        }
 43    }
 44
 45    pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
 46        let count = self.thread_store.read(cx).thread_count();
 47        if count > 0 {
 48            if self.selected_index == count - 1 {
 49                self.set_selected_index(0, cx);
 50            } else {
 51                self.set_selected_index(self.selected_index + 1, cx);
 52            }
 53        }
 54    }
 55
 56    fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
 57        let count = self.thread_store.read(cx).thread_count();
 58        if count > 0 {
 59            self.set_selected_index(0, cx);
 60        }
 61    }
 62
 63    fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
 64        let count = self.thread_store.read(cx).thread_count();
 65        if count > 0 {
 66            self.set_selected_index(count - 1, cx);
 67        }
 68    }
 69
 70    fn set_selected_index(&mut self, index: usize, cx: &mut ViewContext<Self>) {
 71        self.selected_index = index;
 72        self.scroll_handle
 73            .scroll_to_item(index, ScrollStrategy::Top);
 74        cx.notify();
 75    }
 76
 77    fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
 78        let threads = self.thread_store.update(cx, |this, _cx| this.threads());
 79
 80        if let Some(thread) = threads.get(self.selected_index) {
 81            self.assistant_panel
 82                .update(cx, move |this, cx| this.open_thread(&thread.id, cx))
 83                .ok();
 84
 85            cx.notify();
 86        }
 87    }
 88
 89    fn remove_selected_thread(&mut self, _: &RemoveSelectedThread, cx: &mut ViewContext<Self>) {
 90        let threads = self.thread_store.update(cx, |this, _cx| this.threads());
 91
 92        if let Some(thread) = threads.get(self.selected_index) {
 93            self.assistant_panel
 94                .update(cx, |this, cx| {
 95                    this.delete_thread(&thread.id, cx);
 96                })
 97                .ok();
 98
 99            cx.notify();
100        }
101    }
102}
103
104impl FocusableView for ThreadHistory {
105    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
106        self.focus_handle.clone()
107    }
108}
109
110impl Render for ThreadHistory {
111    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
112        let threads = self.thread_store.update(cx, |this, _cx| this.threads());
113        let selected_index = self.selected_index;
114
115        v_flex()
116            .id("thread-history-container")
117            .key_context("ThreadHistory")
118            .track_focus(&self.focus_handle)
119            .overflow_y_scroll()
120            .size_full()
121            .p_1()
122            .on_action(cx.listener(Self::select_prev))
123            .on_action(cx.listener(Self::select_next))
124            .on_action(cx.listener(Self::select_first))
125            .on_action(cx.listener(Self::select_last))
126            .on_action(cx.listener(Self::confirm))
127            .on_action(cx.listener(Self::remove_selected_thread))
128            .map(|history| {
129                if threads.is_empty() {
130                    history
131                        .justify_center()
132                        .child(
133                            h_flex().w_full().justify_center().child(
134                                Label::new("You don't have any past threads yet.")
135                                    .size(LabelSize::Small),
136                            ),
137                        )
138                } else {
139                    history.child(
140                        uniform_list(
141                            cx.view().clone(),
142                            "thread-history",
143                            threads.len(),
144                            move |history, range, _cx| {
145                                threads[range]
146                                    .iter()
147                                    .enumerate()
148                                    .map(|(index, thread)| {
149                                        h_flex().w_full().pb_1().child(PastThread::new(
150                                            thread.clone(),
151                                            history.assistant_panel.clone(),
152                                            selected_index == index,
153                                        ))
154                                    })
155                                    .collect()
156                            },
157                        )
158                        .track_scroll(self.scroll_handle.clone())
159                        .flex_grow(),
160                    )
161                }
162            })
163    }
164}
165
166#[derive(IntoElement)]
167pub struct PastThread {
168    thread: SavedThreadMetadata,
169    assistant_panel: WeakView<AssistantPanel>,
170    selected: bool,
171}
172
173impl PastThread {
174    pub fn new(
175        thread: SavedThreadMetadata,
176        assistant_panel: WeakView<AssistantPanel>,
177        selected: bool,
178    ) -> Self {
179        Self {
180            thread,
181            assistant_panel,
182            selected,
183        }
184    }
185}
186
187impl RenderOnce for PastThread {
188    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
189        let summary = self.thread.summary;
190
191        let thread_timestamp = time_format::format_localized_timestamp(
192            OffsetDateTime::from_unix_timestamp(self.thread.updated_at.timestamp()).unwrap(),
193            OffsetDateTime::now_utc(),
194            self.assistant_panel
195                .update(cx, |this, _cx| this.local_timezone())
196                .unwrap_or(UtcOffset::UTC),
197            time_format::TimestampFormat::EnhancedAbsolute,
198        );
199
200        ListItem::new(SharedString::from(self.thread.id.to_string()))
201            .outlined()
202            .toggle_state(self.selected)
203            .start_slot(
204                Icon::new(IconName::MessageCircle)
205                    .size(IconSize::Small)
206                    .color(Color::Muted),
207            )
208            .spacing(ListItemSpacing::Sparse)
209            .child(Label::new(summary).size(LabelSize::Small).text_ellipsis())
210            .end_slot(
211                h_flex()
212                    .gap_2()
213                    .child(
214                        Label::new(thread_timestamp)
215                            .color(Color::Disabled)
216                            .size(LabelSize::Small),
217                    )
218                    .child(
219                        IconButton::new("delete", IconName::TrashAlt)
220                            .shape(IconButtonShape::Square)
221                            .icon_size(IconSize::Small)
222                            .tooltip(|cx| Tooltip::text("Delete Thread", cx))
223                            .on_click({
224                                let assistant_panel = self.assistant_panel.clone();
225                                let id = self.thread.id.clone();
226                                move |_event, cx| {
227                                    assistant_panel
228                                        .update(cx, |this, cx| {
229                                            this.delete_thread(&id, cx);
230                                        })
231                                        .ok();
232                                }
233                            }),
234                    ),
235            )
236            .on_click({
237                let assistant_panel = self.assistant_panel.clone();
238                let id = self.thread.id.clone();
239                move |_event, cx| {
240                    assistant_panel
241                        .update(cx, |this, cx| {
242                            this.open_thread(&id, cx).detach_and_log_err(cx);
243                        })
244                        .ok();
245                }
246            })
247    }
248}