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