history_store.rs

  1use std::{collections::VecDeque, path::Path};
  2
  3use anyhow::{Context as _, anyhow};
  4use assistant_context_editor::{AssistantContext, SavedContextMetadata};
  5use chrono::{DateTime, Utc};
  6use futures::future::{TryFutureExt as _, join_all};
  7use gpui::{Entity, Task, prelude::*};
  8use serde::{Deserialize, Serialize};
  9use smol::future::FutureExt;
 10use std::time::Duration;
 11use ui::{App, SharedString, Window};
 12use util::ResultExt as _;
 13
 14use crate::{
 15    Thread,
 16    thread::ThreadId,
 17    thread_store::{SerializedThreadMetadata, ThreadStore},
 18};
 19
 20const MAX_RECENTLY_OPENED_ENTRIES: usize = 6;
 21const NAVIGATION_HISTORY_PATH: &str = "agent-navigation-history.json";
 22const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50);
 23
 24#[derive(Clone, Debug)]
 25pub enum HistoryEntry {
 26    Thread(SerializedThreadMetadata),
 27    Context(SavedContextMetadata),
 28}
 29
 30impl HistoryEntry {
 31    pub fn updated_at(&self) -> DateTime<Utc> {
 32        match self {
 33            HistoryEntry::Thread(thread) => thread.updated_at,
 34            HistoryEntry::Context(context) => context.mtime.to_utc(),
 35        }
 36    }
 37}
 38
 39#[derive(Clone, Debug)]
 40pub(crate) enum RecentEntry {
 41    Thread(ThreadId, Entity<Thread>),
 42    Context(Entity<AssistantContext>),
 43}
 44
 45impl PartialEq for RecentEntry {
 46    fn eq(&self, other: &Self) -> bool {
 47        match (self, other) {
 48            (Self::Thread(l0, _), Self::Thread(r0, _)) => l0 == r0,
 49            (Self::Context(l0), Self::Context(r0)) => l0 == r0,
 50            _ => false,
 51        }
 52    }
 53}
 54
 55impl Eq for RecentEntry {}
 56
 57impl RecentEntry {
 58    pub(crate) fn summary(&self, cx: &App) -> SharedString {
 59        match self {
 60            RecentEntry::Thread(_, thread) => thread.read(cx).summary_or_default(),
 61            RecentEntry::Context(context) => context.read(cx).summary_or_default(),
 62        }
 63    }
 64}
 65
 66#[derive(Serialize, Deserialize)]
 67enum SerializedRecentEntry {
 68    Thread(String),
 69    Context(String),
 70}
 71
 72pub struct HistoryStore {
 73    thread_store: Entity<ThreadStore>,
 74    context_store: Entity<assistant_context_editor::ContextStore>,
 75    recently_opened_entries: VecDeque<RecentEntry>,
 76    _subscriptions: Vec<gpui::Subscription>,
 77    _save_recently_opened_entries_task: Task<()>,
 78}
 79
 80impl HistoryStore {
 81    pub fn new(
 82        thread_store: Entity<ThreadStore>,
 83        context_store: Entity<assistant_context_editor::ContextStore>,
 84        initial_recent_entries: impl IntoIterator<Item = RecentEntry>,
 85        window: &mut Window,
 86        cx: &mut Context<Self>,
 87    ) -> Self {
 88        let subscriptions = vec![
 89            cx.observe(&thread_store, |_, _, cx| cx.notify()),
 90            cx.observe(&context_store, |_, _, cx| cx.notify()),
 91        ];
 92
 93        window
 94            .spawn(cx, {
 95                let thread_store = thread_store.downgrade();
 96                let context_store = context_store.downgrade();
 97                let this = cx.weak_entity();
 98                async move |cx| {
 99                    let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
100                    let contents = cx
101                        .background_spawn(async move { std::fs::read_to_string(path) })
102                        .await
103                        .ok()?;
104                    let entries = serde_json::from_str::<Vec<SerializedRecentEntry>>(&contents)
105                        .context("deserializing persisted agent panel navigation history")
106                        .log_err()?
107                        .into_iter()
108                        .take(MAX_RECENTLY_OPENED_ENTRIES)
109                        .map(|serialized| match serialized {
110                            SerializedRecentEntry::Thread(id) => thread_store
111                                .update_in(cx, |thread_store, window, cx| {
112                                    let thread_id = ThreadId::from(id.as_str());
113                                    thread_store
114                                        .open_thread(&thread_id, window, cx)
115                                        .map_ok(|thread| RecentEntry::Thread(thread_id, thread))
116                                        .boxed()
117                                })
118                                .unwrap_or_else(|_| {
119                                    async { Err(anyhow!("no thread store")) }.boxed()
120                                }),
121                            SerializedRecentEntry::Context(id) => context_store
122                                .update(cx, |context_store, cx| {
123                                    context_store
124                                        .open_local_context(Path::new(&id).into(), cx)
125                                        .map_ok(RecentEntry::Context)
126                                        .boxed()
127                                })
128                                .unwrap_or_else(|_| {
129                                    async { Err(anyhow!("no context store")) }.boxed()
130                                }),
131                        });
132                    let entries = join_all(entries)
133                        .await
134                        .into_iter()
135                        .filter_map(|result| result.log_err())
136                        .collect::<VecDeque<_>>();
137
138                    this.update(cx, |this, _| {
139                        this.recently_opened_entries.extend(entries);
140                        this.recently_opened_entries
141                            .truncate(MAX_RECENTLY_OPENED_ENTRIES);
142                    })
143                    .ok();
144
145                    Some(())
146                }
147            })
148            .detach();
149
150        Self {
151            thread_store,
152            context_store,
153            recently_opened_entries: initial_recent_entries.into_iter().collect(),
154            _subscriptions: subscriptions,
155            _save_recently_opened_entries_task: Task::ready(()),
156        }
157    }
158
159    pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
160        let mut history_entries = Vec::new();
161
162        #[cfg(debug_assertions)]
163        if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
164            return history_entries;
165        }
166
167        for thread in self
168            .thread_store
169            .update(cx, |this, _cx| this.reverse_chronological_threads())
170        {
171            history_entries.push(HistoryEntry::Thread(thread));
172        }
173
174        for context in self
175            .context_store
176            .update(cx, |this, _cx| this.reverse_chronological_contexts())
177        {
178            history_entries.push(HistoryEntry::Context(context));
179        }
180
181        history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
182        history_entries
183    }
184
185    pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
186        self.entries(cx).into_iter().take(limit).collect()
187    }
188
189    fn save_recently_opened_entries(&mut self, cx: &mut Context<Self>) {
190        let serialized_entries = self
191            .recently_opened_entries
192            .iter()
193            .filter_map(|entry| match entry {
194                RecentEntry::Context(context) => Some(SerializedRecentEntry::Context(
195                    context.read(cx).path()?.to_str()?.to_owned(),
196                )),
197                RecentEntry::Thread(id, _) => Some(SerializedRecentEntry::Thread(id.to_string())),
198            })
199            .collect::<Vec<_>>();
200
201        self._save_recently_opened_entries_task = cx.spawn(async move |_, cx| {
202            cx.background_executor()
203                .timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE)
204                .await;
205            cx.background_spawn(async move {
206                let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
207                let content = serde_json::to_string(&serialized_entries)?;
208                std::fs::write(path, content)?;
209                anyhow::Ok(())
210            })
211            .await
212            .log_err();
213        });
214    }
215
216    pub fn push_recently_opened_entry(&mut self, entry: RecentEntry, cx: &mut Context<Self>) {
217        self.recently_opened_entries
218            .retain(|old_entry| old_entry != &entry);
219        self.recently_opened_entries.push_front(entry);
220        self.recently_opened_entries
221            .truncate(MAX_RECENTLY_OPENED_ENTRIES);
222        self.save_recently_opened_entries(cx);
223    }
224
225    pub fn remove_recently_opened_thread(&mut self, id: ThreadId, cx: &mut Context<Self>) {
226        self.recently_opened_entries.retain(|entry| match entry {
227            RecentEntry::Thread(thread_id, _) if thread_id == &id => false,
228            _ => true,
229        });
230        self.save_recently_opened_entries(cx);
231    }
232
233    pub fn remove_recently_opened_entry(&mut self, entry: &RecentEntry, cx: &mut Context<Self>) {
234        self.recently_opened_entries
235            .retain(|old_entry| old_entry != entry);
236        self.save_recently_opened_entries(cx);
237    }
238
239    pub fn recently_opened_entries(&self, _cx: &mut Context<Self>) -> VecDeque<RecentEntry> {
240        #[cfg(debug_assertions)]
241        if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
242            return VecDeque::new();
243        }
244
245        self.recently_opened_entries.clone()
246    }
247}