thread_history.rs

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