history_store.rs

  1use std::{collections::VecDeque, path::Path, sync::Arc};
  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    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 { Err(anyhow!("no thread store")) }.boxed()
134                                }),
135                            SerializedRecentEntry::Context(id) => context_store
136                                .update(cx, |context_store, cx| {
137                                    context_store
138                                        .open_local_context(Path::new(&id).into(), cx)
139                                        .map_ok(RecentEntry::Context)
140                                        .boxed()
141                                })
142                                .unwrap_or_else(|_| {
143                                    async { Err(anyhow!("no context store")) }.boxed()
144                                }),
145                        });
146                    let entries = join_all(entries)
147                        .await
148                        .into_iter()
149                        .filter_map(|result| result.log_err())
150                        .collect::<VecDeque<_>>();
151
152                    this.update(cx, |this, _| {
153                        this.recently_opened_entries.extend(entries);
154                        this.recently_opened_entries
155                            .truncate(MAX_RECENTLY_OPENED_ENTRIES);
156                    })
157                    .ok();
158
159                    Some(())
160                }
161            })
162            .detach();
163
164        Self {
165            thread_store,
166            context_store,
167            recently_opened_entries: initial_recent_entries.into_iter().collect(),
168            _subscriptions: subscriptions,
169            _save_recently_opened_entries_task: Task::ready(()),
170        }
171    }
172
173    pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
174        let mut history_entries = Vec::new();
175
176        #[cfg(debug_assertions)]
177        if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
178            return history_entries;
179        }
180
181        for thread in self
182            .thread_store
183            .update(cx, |this, _cx| this.reverse_chronological_threads())
184        {
185            history_entries.push(HistoryEntry::Thread(thread));
186        }
187
188        for context in self
189            .context_store
190            .update(cx, |this, _cx| this.reverse_chronological_contexts())
191        {
192            history_entries.push(HistoryEntry::Context(context));
193        }
194
195        history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
196        history_entries
197    }
198
199    pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
200        self.entries(cx).into_iter().take(limit).collect()
201    }
202
203    fn save_recently_opened_entries(&mut self, cx: &mut Context<Self>) {
204        let serialized_entries = self
205            .recently_opened_entries
206            .iter()
207            .filter_map(|entry| match entry {
208                RecentEntry::Context(context) => Some(SerializedRecentEntry::Context(
209                    context.read(cx).path()?.to_str()?.to_owned(),
210                )),
211                RecentEntry::Thread(id, _) => Some(SerializedRecentEntry::Thread(id.to_string())),
212            })
213            .collect::<Vec<_>>();
214
215        self._save_recently_opened_entries_task = cx.spawn(async move |_, cx| {
216            cx.background_executor()
217                .timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE)
218                .await;
219            cx.background_spawn(async move {
220                let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
221                let content = serde_json::to_string(&serialized_entries)?;
222                std::fs::write(path, content)?;
223                anyhow::Ok(())
224            })
225            .await
226            .log_err();
227        });
228    }
229
230    pub fn push_recently_opened_entry(&mut self, entry: RecentEntry, cx: &mut Context<Self>) {
231        self.recently_opened_entries
232            .retain(|old_entry| old_entry != &entry);
233        self.recently_opened_entries.push_front(entry);
234        self.recently_opened_entries
235            .truncate(MAX_RECENTLY_OPENED_ENTRIES);
236        self.save_recently_opened_entries(cx);
237    }
238
239    pub fn remove_recently_opened_thread(&mut self, id: ThreadId, cx: &mut Context<Self>) {
240        self.recently_opened_entries.retain(|entry| match entry {
241            RecentEntry::Thread(thread_id, _) if thread_id == &id => false,
242            _ => true,
243        });
244        self.save_recently_opened_entries(cx);
245    }
246
247    pub fn remove_recently_opened_entry(&mut self, entry: &RecentEntry, cx: &mut Context<Self>) {
248        self.recently_opened_entries
249            .retain(|old_entry| old_entry != entry);
250        self.save_recently_opened_entries(cx);
251    }
252
253    pub fn recently_opened_entries(&self, _cx: &mut Context<Self>) -> VecDeque<RecentEntry> {
254        #[cfg(debug_assertions)]
255        if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
256            return VecDeque::new();
257        }
258
259        self.recently_opened_entries.clone()
260    }
261}