history_store.rs

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