thread_history.rs

  1use agent::{HistoryEntry, HistoryStore};
  2use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};
  3use editor::{Editor, EditorEvent};
  4use fuzzy::StringMatchCandidate;
  5use gpui::{
  6    App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Task,
  7    UniformListScrollHandle, Window, actions, uniform_list,
  8};
  9use std::{fmt::Display, ops::Range};
 10use text::Bias;
 11use time::{OffsetDateTime, UtcOffset};
 12use ui::{
 13    HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tab, Tooltip, WithScrollbar,
 14    prelude::*,
 15};
 16
 17actions!(
 18    agents,
 19    [
 20        /// Removes all thread history.
 21        RemoveHistory,
 22        /// Removes the currently selected thread.
 23        RemoveSelectedThread,
 24    ]
 25);
 26
 27pub struct AcpThreadHistory {
 28    pub(crate) history_store: Entity<HistoryStore>,
 29    scroll_handle: UniformListScrollHandle,
 30    selected_index: usize,
 31    hovered_index: Option<usize>,
 32    search_editor: Entity<Editor>,
 33    search_query: SharedString,
 34    visible_items: Vec<ListItemType>,
 35    local_timezone: UtcOffset,
 36    confirming_delete_history: bool,
 37    _update_task: Task<()>,
 38    _subscriptions: Vec<gpui::Subscription>,
 39}
 40
 41enum ListItemType {
 42    BucketSeparator(TimeBucket),
 43    Entry {
 44        entry: HistoryEntry,
 45        format: EntryTimeFormat,
 46    },
 47    SearchResult {
 48        entry: HistoryEntry,
 49        positions: Vec<usize>,
 50    },
 51}
 52
 53impl ListItemType {
 54    fn history_entry(&self) -> Option<&HistoryEntry> {
 55        match self {
 56            ListItemType::Entry { entry, .. } => Some(entry),
 57            ListItemType::SearchResult { entry, .. } => Some(entry),
 58            _ => None,
 59        }
 60    }
 61}
 62
 63#[allow(dead_code)]
 64pub enum ThreadHistoryEvent {
 65    Open(HistoryEntry),
 66}
 67
 68impl EventEmitter<ThreadHistoryEvent> for AcpThreadHistory {}
 69
 70impl AcpThreadHistory {
 71    pub fn new(
 72        history_store: Entity<agent::HistoryStore>,
 73        window: &mut Window,
 74        cx: &mut Context<Self>,
 75    ) -> Self {
 76        let search_editor = cx.new(|cx| {
 77            let mut editor = Editor::single_line(window, cx);
 78            editor.set_placeholder_text("Search threads...", window, cx);
 79            editor
 80        });
 81
 82        let search_editor_subscription =
 83            cx.subscribe(&search_editor, |this, search_editor, event, cx| {
 84                if let EditorEvent::BufferEdited = event {
 85                    let query = search_editor.read(cx).text(cx);
 86                    if this.search_query != query {
 87                        this.search_query = query.into();
 88                        this.update_visible_items(false, cx);
 89                    }
 90                }
 91            });
 92
 93        let history_store_subscription = cx.observe(&history_store, |this, _, cx| {
 94            this.update_visible_items(true, cx);
 95        });
 96
 97        let scroll_handle = UniformListScrollHandle::default();
 98
 99        let mut this = Self {
100            history_store,
101            scroll_handle,
102            selected_index: 0,
103            hovered_index: None,
104            visible_items: Default::default(),
105            search_editor,
106            local_timezone: UtcOffset::from_whole_seconds(
107                chrono::Local::now().offset().local_minus_utc(),
108            )
109            .unwrap(),
110            search_query: SharedString::default(),
111            confirming_delete_history: false,
112            _subscriptions: vec![search_editor_subscription, history_store_subscription],
113            _update_task: Task::ready(()),
114        };
115        this.update_visible_items(false, cx);
116        this
117    }
118
119    fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context<Self>) {
120        let entries = self
121            .history_store
122            .update(cx, |store, _| store.entries().collect());
123        let new_list_items = if self.search_query.is_empty() {
124            self.add_list_separators(entries, cx)
125        } else {
126            self.filter_search_results(entries, cx)
127        };
128        let selected_history_entry = if preserve_selected_item {
129            self.selected_history_entry().cloned()
130        } else {
131            None
132        };
133
134        self._update_task = cx.spawn(async move |this, cx| {
135            let new_visible_items = new_list_items.await;
136            this.update(cx, |this, cx| {
137                let new_selected_index = if let Some(history_entry) = selected_history_entry {
138                    let history_entry_id = history_entry.id();
139                    new_visible_items
140                        .iter()
141                        .position(|visible_entry| {
142                            visible_entry
143                                .history_entry()
144                                .is_some_and(|entry| entry.id() == history_entry_id)
145                        })
146                        .unwrap_or(0)
147                } else {
148                    0
149                };
150
151                this.visible_items = new_visible_items;
152                this.set_selected_index(new_selected_index, Bias::Right, cx);
153                cx.notify();
154            })
155            .ok();
156        });
157    }
158
159    fn add_list_separators(&self, entries: Vec<HistoryEntry>, cx: &App) -> Task<Vec<ListItemType>> {
160        cx.background_spawn(async move {
161            let mut items = Vec::with_capacity(entries.len() + 1);
162            let mut bucket = None;
163            let today = Local::now().naive_local().date();
164
165            for entry in entries.into_iter() {
166                let entry_date = entry
167                    .updated_at()
168                    .with_timezone(&Local)
169                    .naive_local()
170                    .date();
171                let entry_bucket = TimeBucket::from_dates(today, entry_date);
172
173                if Some(entry_bucket) != bucket {
174                    bucket = Some(entry_bucket);
175                    items.push(ListItemType::BucketSeparator(entry_bucket));
176                }
177
178                items.push(ListItemType::Entry {
179                    entry,
180                    format: entry_bucket.into(),
181                });
182            }
183            items
184        })
185    }
186
187    fn filter_search_results(
188        &self,
189        entries: Vec<HistoryEntry>,
190        cx: &App,
191    ) -> Task<Vec<ListItemType>> {
192        let query = self.search_query.clone();
193        cx.background_spawn({
194            let executor = cx.background_executor().clone();
195            async move {
196                let mut candidates = Vec::with_capacity(entries.len());
197
198                for (idx, entry) in entries.iter().enumerate() {
199                    candidates.push(StringMatchCandidate::new(idx, entry.title()));
200                }
201
202                const MAX_MATCHES: usize = 100;
203
204                let matches = fuzzy::match_strings(
205                    &candidates,
206                    &query,
207                    false,
208                    true,
209                    MAX_MATCHES,
210                    &Default::default(),
211                    executor,
212                )
213                .await;
214
215                matches
216                    .into_iter()
217                    .map(|search_match| ListItemType::SearchResult {
218                        entry: entries[search_match.candidate_id].clone(),
219                        positions: search_match.positions,
220                    })
221                    .collect()
222            }
223        })
224    }
225
226    fn search_produced_no_matches(&self) -> bool {
227        self.visible_items.is_empty() && !self.search_query.is_empty()
228    }
229
230    fn selected_history_entry(&self) -> Option<&HistoryEntry> {
231        self.get_history_entry(self.selected_index)
232    }
233
234    fn get_history_entry(&self, visible_items_ix: usize) -> Option<&HistoryEntry> {
235        self.visible_items.get(visible_items_ix)?.history_entry()
236    }
237
238    fn set_selected_index(&mut self, mut index: usize, bias: Bias, cx: &mut Context<Self>) {
239        if self.visible_items.is_empty() {
240            self.selected_index = 0;
241            return;
242        }
243        while matches!(
244            self.visible_items.get(index),
245            None | Some(ListItemType::BucketSeparator(..))
246        ) {
247            index = match bias {
248                Bias::Left => {
249                    if index == 0 {
250                        self.visible_items.len() - 1
251                    } else {
252                        index - 1
253                    }
254                }
255                Bias::Right => {
256                    if index >= self.visible_items.len() - 1 {
257                        0
258                    } else {
259                        index + 1
260                    }
261                }
262            };
263        }
264        self.selected_index = index;
265        self.scroll_handle
266            .scroll_to_item(index, ScrollStrategy::Top);
267        cx.notify()
268    }
269
270    pub fn select_previous(
271        &mut self,
272        _: &menu::SelectPrevious,
273        _window: &mut Window,
274        cx: &mut Context<Self>,
275    ) {
276        if self.selected_index == 0 {
277            self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
278        } else {
279            self.set_selected_index(self.selected_index - 1, Bias::Left, cx);
280        }
281    }
282
283    pub fn select_next(
284        &mut self,
285        _: &menu::SelectNext,
286        _window: &mut Window,
287        cx: &mut Context<Self>,
288    ) {
289        if self.selected_index == self.visible_items.len() - 1 {
290            self.set_selected_index(0, Bias::Right, cx);
291        } else {
292            self.set_selected_index(self.selected_index + 1, Bias::Right, cx);
293        }
294    }
295
296    fn select_first(
297        &mut self,
298        _: &menu::SelectFirst,
299        _window: &mut Window,
300        cx: &mut Context<Self>,
301    ) {
302        self.set_selected_index(0, Bias::Right, cx);
303    }
304
305    fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
306        self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
307    }
308
309    fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
310        self.confirm_entry(self.selected_index, cx);
311    }
312
313    fn confirm_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
314        let Some(entry) = self.get_history_entry(ix) else {
315            return;
316        };
317        cx.emit(ThreadHistoryEvent::Open(entry.clone()));
318    }
319
320    fn remove_selected_thread(
321        &mut self,
322        _: &RemoveSelectedThread,
323        _window: &mut Window,
324        cx: &mut Context<Self>,
325    ) {
326        self.remove_thread(self.selected_index, cx)
327    }
328
329    fn remove_thread(&mut self, visible_item_ix: usize, cx: &mut Context<Self>) {
330        let Some(entry) = self.get_history_entry(visible_item_ix) else {
331            return;
332        };
333
334        let task = match entry {
335            HistoryEntry::AcpThread(thread) => self
336                .history_store
337                .update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)),
338            HistoryEntry::TextThread(text_thread) => self.history_store.update(cx, |this, cx| {
339                this.delete_text_thread(text_thread.path.clone(), cx)
340            }),
341        };
342        task.detach_and_log_err(cx);
343    }
344
345    fn remove_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
346        self.history_store.update(cx, |store, cx| {
347            store.delete_threads(cx).detach_and_log_err(cx)
348        });
349        self.confirming_delete_history = false;
350        cx.notify();
351    }
352
353    fn prompt_delete_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
354        self.confirming_delete_history = true;
355        cx.notify();
356    }
357
358    fn cancel_delete_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
359        self.confirming_delete_history = false;
360        cx.notify();
361    }
362
363    fn render_list_items(
364        &mut self,
365        range: Range<usize>,
366        _window: &mut Window,
367        cx: &mut Context<Self>,
368    ) -> Vec<AnyElement> {
369        self.visible_items
370            .get(range.clone())
371            .into_iter()
372            .flatten()
373            .enumerate()
374            .map(|(ix, item)| self.render_list_item(item, range.start + ix, cx))
375            .collect()
376    }
377
378    fn render_list_item(&self, item: &ListItemType, ix: usize, cx: &Context<Self>) -> AnyElement {
379        match item {
380            ListItemType::Entry { entry, format } => self
381                .render_history_entry(entry, *format, ix, Vec::default(), cx)
382                .into_any(),
383            ListItemType::SearchResult { entry, positions } => self.render_history_entry(
384                entry,
385                EntryTimeFormat::DateAndTime,
386                ix,
387                positions.clone(),
388                cx,
389            ),
390            ListItemType::BucketSeparator(bucket) => div()
391                .px(DynamicSpacing::Base06.rems(cx))
392                .pt_2()
393                .pb_1()
394                .child(
395                    Label::new(bucket.to_string())
396                        .size(LabelSize::XSmall)
397                        .color(Color::Muted),
398                )
399                .into_any_element(),
400        }
401    }
402
403    fn render_history_entry(
404        &self,
405        entry: &HistoryEntry,
406        format: EntryTimeFormat,
407        ix: usize,
408        highlight_positions: Vec<usize>,
409        cx: &Context<Self>,
410    ) -> AnyElement {
411        let selected = ix == self.selected_index;
412        let hovered = Some(ix) == self.hovered_index;
413        let timestamp = entry.updated_at().timestamp();
414
415        let display_text = match format {
416            EntryTimeFormat::DateAndTime => {
417                let entry_time = entry.updated_at();
418                let now = Utc::now();
419                let duration = now.signed_duration_since(entry_time);
420                let days = duration.num_days();
421
422                format!("{}d", days)
423            }
424            EntryTimeFormat::TimeOnly => format.format_timestamp(timestamp, self.local_timezone),
425        };
426
427        let title = entry.title().clone();
428        let full_date =
429            EntryTimeFormat::DateAndTime.format_timestamp(timestamp, self.local_timezone);
430
431        h_flex()
432            .w_full()
433            .pb_1()
434            .child(
435                ListItem::new(ix)
436                    .rounded()
437                    .toggle_state(selected)
438                    .spacing(ListItemSpacing::Sparse)
439                    .start_slot(
440                        h_flex()
441                            .w_full()
442                            .gap_2()
443                            .justify_between()
444                            .child(
445                                HighlightedLabel::new(entry.title(), highlight_positions)
446                                    .size(LabelSize::Small)
447                                    .truncate(),
448                            )
449                            .child(
450                                Label::new(display_text)
451                                    .color(Color::Muted)
452                                    .size(LabelSize::XSmall),
453                            ),
454                    )
455                    .tooltip(move |_, cx| {
456                        Tooltip::with_meta(title.clone(), None, full_date.clone(), cx)
457                    })
458                    .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
459                        if *is_hovered {
460                            this.hovered_index = Some(ix);
461                        } else if this.hovered_index == Some(ix) {
462                            this.hovered_index = None;
463                        }
464
465                        cx.notify();
466                    }))
467                    .end_slot::<IconButton>(if hovered {
468                        Some(
469                            IconButton::new("delete", IconName::Trash)
470                                .shape(IconButtonShape::Square)
471                                .icon_size(IconSize::XSmall)
472                                .icon_color(Color::Muted)
473                                .tooltip(move |_window, cx| {
474                                    Tooltip::for_action("Delete", &RemoveSelectedThread, cx)
475                                })
476                                .on_click(cx.listener(move |this, _, _, cx| {
477                                    this.remove_thread(ix, cx);
478                                    cx.stop_propagation()
479                                })),
480                        )
481                    } else {
482                        None
483                    })
484                    .on_click(cx.listener(move |this, _, _, cx| this.confirm_entry(ix, cx))),
485            )
486            .into_any_element()
487    }
488}
489
490impl Focusable for AcpThreadHistory {
491    fn focus_handle(&self, cx: &App) -> FocusHandle {
492        self.search_editor.focus_handle(cx)
493    }
494}
495
496impl Render for AcpThreadHistory {
497    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
498        let has_no_history = self.history_store.read(cx).is_empty(cx);
499
500        v_flex()
501            .key_context("ThreadHistory")
502            .size_full()
503            .bg(cx.theme().colors().panel_background)
504            .on_action(cx.listener(Self::select_previous))
505            .on_action(cx.listener(Self::select_next))
506            .on_action(cx.listener(Self::select_first))
507            .on_action(cx.listener(Self::select_last))
508            .on_action(cx.listener(Self::confirm))
509            .on_action(cx.listener(Self::remove_selected_thread))
510            .on_action(cx.listener(|this, _: &RemoveHistory, window, cx| {
511                this.remove_history(window, cx);
512            }))
513            .child(
514                h_flex()
515                    .h(Tab::container_height(cx))
516                    .w_full()
517                    .py_1()
518                    .px_2()
519                    .gap_2()
520                    .justify_between()
521                    .border_b_1()
522                    .border_color(cx.theme().colors().border)
523                    .child(
524                        Icon::new(IconName::MagnifyingGlass)
525                            .color(Color::Muted)
526                            .size(IconSize::Small),
527                    )
528                    .child(self.search_editor.clone()),
529            )
530            .child({
531                let view = v_flex()
532                    .id("list-container")
533                    .relative()
534                    .overflow_hidden()
535                    .flex_grow();
536
537                if has_no_history {
538                    view.justify_center().items_center().child(
539                        Label::new("You don't have any past threads yet.")
540                            .size(LabelSize::Small)
541                            .color(Color::Muted),
542                    )
543                } else if self.search_produced_no_matches() {
544                    view.justify_center()
545                        .items_center()
546                        .child(Label::new("No threads match your search.").size(LabelSize::Small))
547                } else {
548                    view.child(
549                        uniform_list(
550                            "thread-history",
551                            self.visible_items.len(),
552                            cx.processor(|this, range: Range<usize>, window, cx| {
553                                this.render_list_items(range, window, cx)
554                            }),
555                        )
556                        .p_1()
557                        .pr_4()
558                        .track_scroll(&self.scroll_handle)
559                        .flex_grow(),
560                    )
561                    .vertical_scrollbar_for(&self.scroll_handle, window, cx)
562                }
563            })
564            .when(!has_no_history, |this| {
565                this.child(
566                    h_flex()
567                        .p_2()
568                        .border_t_1()
569                        .border_color(cx.theme().colors().border_variant)
570                        .when(!self.confirming_delete_history, |this| {
571                            this.child(
572                                Button::new("delete_history", "Delete All History")
573                                    .full_width()
574                                    .style(ButtonStyle::Outlined)
575                                    .label_size(LabelSize::Small)
576                                    .on_click(cx.listener(|this, _, window, cx| {
577                                        this.prompt_delete_history(window, cx);
578                                    })),
579                            )
580                        })
581                        .when(self.confirming_delete_history, |this| {
582                            this.w_full()
583                                .gap_2()
584                                .flex_wrap()
585                                .justify_between()
586                                .child(
587                                    h_flex()
588                                        .flex_wrap()
589                                        .gap_1()
590                                        .child(
591                                            Label::new("Delete all threads?")
592                                                .size(LabelSize::Small),
593                                        )
594                                        .child(
595                                            Label::new("You won't be able to recover them later.")
596                                                .size(LabelSize::Small)
597                                                .color(Color::Muted),
598                                        ),
599                                )
600                                .child(
601                                    h_flex()
602                                        .gap_1()
603                                        .child(
604                                            Button::new("cancel_delete", "Cancel")
605                                                .label_size(LabelSize::Small)
606                                                .on_click(cx.listener(|this, _, window, cx| {
607                                                    this.cancel_delete_history(window, cx);
608                                                })),
609                                        )
610                                        .child(
611                                            Button::new("confirm_delete", "Delete")
612                                                .style(ButtonStyle::Tinted(ui::TintColor::Error))
613                                                .color(Color::Error)
614                                                .label_size(LabelSize::Small)
615                                                .on_click(cx.listener(|_, _, window, cx| {
616                                                    window.dispatch_action(
617                                                        Box::new(RemoveHistory),
618                                                        cx,
619                                                    );
620                                                })),
621                                        ),
622                                )
623                        }),
624                )
625            })
626    }
627}
628
629#[derive(Clone, Copy)]
630pub enum EntryTimeFormat {
631    DateAndTime,
632    TimeOnly,
633}
634
635impl EntryTimeFormat {
636    fn format_timestamp(&self, timestamp: i64, timezone: UtcOffset) -> String {
637        let timestamp = OffsetDateTime::from_unix_timestamp(timestamp).unwrap();
638
639        match self {
640            EntryTimeFormat::DateAndTime => time_format::format_localized_timestamp(
641                timestamp,
642                OffsetDateTime::now_utc(),
643                timezone,
644                time_format::TimestampFormat::EnhancedAbsolute,
645            ),
646            EntryTimeFormat::TimeOnly => time_format::format_time(timestamp.to_offset(timezone)),
647        }
648    }
649}
650
651impl From<TimeBucket> for EntryTimeFormat {
652    fn from(bucket: TimeBucket) -> Self {
653        match bucket {
654            TimeBucket::Today => EntryTimeFormat::TimeOnly,
655            TimeBucket::Yesterday => EntryTimeFormat::TimeOnly,
656            TimeBucket::ThisWeek => EntryTimeFormat::DateAndTime,
657            TimeBucket::PastWeek => EntryTimeFormat::DateAndTime,
658            TimeBucket::All => EntryTimeFormat::DateAndTime,
659        }
660    }
661}
662
663#[derive(PartialEq, Eq, Clone, Copy, Debug)]
664enum TimeBucket {
665    Today,
666    Yesterday,
667    ThisWeek,
668    PastWeek,
669    All,
670}
671
672impl TimeBucket {
673    fn from_dates(reference: NaiveDate, date: NaiveDate) -> Self {
674        if date == reference {
675            return TimeBucket::Today;
676        }
677
678        if date == reference - TimeDelta::days(1) {
679            return TimeBucket::Yesterday;
680        }
681
682        let week = date.iso_week();
683
684        if reference.iso_week() == week {
685            return TimeBucket::ThisWeek;
686        }
687
688        let last_week = (reference - TimeDelta::days(7)).iso_week();
689
690        if week == last_week {
691            return TimeBucket::PastWeek;
692        }
693
694        TimeBucket::All
695    }
696}
697
698impl Display for TimeBucket {
699    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
700        match self {
701            TimeBucket::Today => write!(f, "Today"),
702            TimeBucket::Yesterday => write!(f, "Yesterday"),
703            TimeBucket::ThisWeek => write!(f, "This Week"),
704            TimeBucket::PastWeek => write!(f, "Past Week"),
705            TimeBucket::All => write!(f, "All"),
706        }
707    }
708}
709
710#[cfg(test)]
711mod tests {
712    use super::*;
713    use chrono::NaiveDate;
714
715    #[test]
716    fn test_time_bucket_from_dates() {
717        let today = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap();
718
719        let date = today;
720        assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Today);
721
722        let date = NaiveDate::from_ymd_opt(2023, 1, 14).unwrap();
723        assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Yesterday);
724
725        let date = NaiveDate::from_ymd_opt(2023, 1, 13).unwrap();
726        assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek);
727
728        let date = NaiveDate::from_ymd_opt(2023, 1, 11).unwrap();
729        assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek);
730
731        let date = NaiveDate::from_ymd_opt(2023, 1, 8).unwrap();
732        assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek);
733
734        let date = NaiveDate::from_ymd_opt(2023, 1, 5).unwrap();
735        assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek);
736
737        // All: not in this week or last week
738        let date = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
739        assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::All);
740
741        // Test year boundary cases
742        let new_year = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
743
744        let date = NaiveDate::from_ymd_opt(2022, 12, 31).unwrap();
745        assert_eq!(
746            TimeBucket::from_dates(new_year, date),
747            TimeBucket::Yesterday
748        );
749
750        let date = NaiveDate::from_ymd_opt(2022, 12, 28).unwrap();
751        assert_eq!(TimeBucket::from_dates(new_year, date), TimeBucket::ThisWeek);
752    }
753}