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