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};
 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, PartialEq, Eq)]
 40pub(crate) enum RecentEntry {
 41    Thread(Entity<Thread>),
 42    Context(Entity<AssistantContext>),
 43}
 44
 45impl RecentEntry {
 46    pub(crate) fn summary(&self, cx: &App) -> SharedString {
 47        match self {
 48            RecentEntry::Thread(thread) => thread.read(cx).summary_or_default(),
 49            RecentEntry::Context(context) => context.read(cx).summary_or_default(),
 50        }
 51    }
 52}
 53
 54#[derive(Serialize, Deserialize)]
 55enum SerializedRecentEntry {
 56    Thread(String),
 57    Context(String),
 58}
 59
 60pub struct HistoryStore {
 61    thread_store: Entity<ThreadStore>,
 62    context_store: Entity<assistant_context_editor::ContextStore>,
 63    recently_opened_entries: VecDeque<RecentEntry>,
 64    _subscriptions: Vec<gpui::Subscription>,
 65    _save_recently_opened_entries_task: Task<()>,
 66}
 67
 68impl HistoryStore {
 69    pub fn new(
 70        thread_store: Entity<ThreadStore>,
 71        context_store: Entity<assistant_context_editor::ContextStore>,
 72        initial_recent_entries: impl IntoIterator<Item = RecentEntry>,
 73        cx: &mut Context<Self>,
 74    ) -> Self {
 75        let subscriptions = vec![
 76            cx.observe(&thread_store, |_, _, cx| cx.notify()),
 77            cx.observe(&context_store, |_, _, cx| cx.notify()),
 78        ];
 79
 80        cx.spawn({
 81            let thread_store = thread_store.downgrade();
 82            let context_store = context_store.downgrade();
 83            async move |this, cx| {
 84                let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
 85                let contents = cx
 86                    .background_spawn(async move { std::fs::read_to_string(path) })
 87                    .await
 88                    .context("reading persisted agent panel navigation history")?;
 89                let entries = serde_json::from_str::<Vec<SerializedRecentEntry>>(&contents)
 90                    .context("deserializing persisted agent panel navigation history")?
 91                    .into_iter()
 92                    .take(MAX_RECENTLY_OPENED_ENTRIES)
 93                    .map(|serialized| match serialized {
 94                        SerializedRecentEntry::Thread(id) => thread_store
 95                            .update(cx, |thread_store, cx| {
 96                                thread_store
 97                                    .open_thread(&ThreadId::from(id.as_str()), cx)
 98                                    .map_ok(RecentEntry::Thread)
 99                                    .boxed()
100                            })
101                            .unwrap_or_else(|_| async { Err(anyhow!("no thread store")) }.boxed()),
102                        SerializedRecentEntry::Context(id) => context_store
103                            .update(cx, |context_store, cx| {
104                                context_store
105                                    .open_local_context(Path::new(&id).into(), cx)
106                                    .map_ok(RecentEntry::Context)
107                                    .boxed()
108                            })
109                            .unwrap_or_else(|_| async { Err(anyhow!("no context store")) }.boxed()),
110                    });
111                let entries = join_all(entries)
112                    .await
113                    .into_iter()
114                    .filter_map(|result| result.log_err())
115                    .collect::<VecDeque<_>>();
116
117                this.update(cx, |this, _| {
118                    this.recently_opened_entries.extend(entries);
119                    this.recently_opened_entries
120                        .truncate(MAX_RECENTLY_OPENED_ENTRIES);
121                })
122                .ok();
123
124                anyhow::Ok(())
125            }
126        })
127        .detach_and_log_err(cx);
128
129        Self {
130            thread_store,
131            context_store,
132            recently_opened_entries: initial_recent_entries.into_iter().collect(),
133            _subscriptions: subscriptions,
134            _save_recently_opened_entries_task: Task::ready(()),
135        }
136    }
137
138    pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
139        let mut history_entries = Vec::new();
140
141        #[cfg(debug_assertions)]
142        if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
143            return history_entries;
144        }
145
146        for thread in self
147            .thread_store
148            .update(cx, |this, _cx| this.reverse_chronological_threads())
149        {
150            history_entries.push(HistoryEntry::Thread(thread));
151        }
152
153        for context in self.context_store.update(cx, |this, _cx| this.contexts()) {
154            history_entries.push(HistoryEntry::Context(context));
155        }
156
157        history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
158        history_entries
159    }
160
161    pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
162        self.entries(cx).into_iter().take(limit).collect()
163    }
164
165    fn save_recently_opened_entries(&mut self, cx: &mut Context<Self>) {
166        let serialized_entries = self
167            .recently_opened_entries
168            .iter()
169            .filter_map(|entry| match entry {
170                RecentEntry::Context(context) => Some(SerializedRecentEntry::Context(
171                    context.read(cx).path()?.to_str()?.to_owned(),
172                )),
173                RecentEntry::Thread(thread) => Some(SerializedRecentEntry::Thread(
174                    thread.read(cx).id().to_string(),
175                )),
176            })
177            .collect::<Vec<_>>();
178
179        self._save_recently_opened_entries_task = cx.spawn(async move |_, cx| {
180            cx.background_executor()
181                .timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE)
182                .await;
183            cx.background_spawn(async move {
184                let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
185                let content = serde_json::to_string(&serialized_entries)?;
186                std::fs::write(path, content)?;
187                anyhow::Ok(())
188            })
189            .await
190            .log_err();
191        });
192    }
193
194    pub fn push_recently_opened_entry(&mut self, entry: RecentEntry, cx: &mut Context<Self>) {
195        self.recently_opened_entries
196            .retain(|old_entry| old_entry != &entry);
197        self.recently_opened_entries.push_front(entry);
198        self.recently_opened_entries
199            .truncate(MAX_RECENTLY_OPENED_ENTRIES);
200        self.save_recently_opened_entries(cx);
201    }
202
203    pub fn remove_recently_opened_entry(&mut self, entry: &RecentEntry, cx: &mut Context<Self>) {
204        self.recently_opened_entries
205            .retain(|old_entry| old_entry != entry);
206        self.save_recently_opened_entries(cx);
207    }
208
209    pub fn recently_opened_entries(&self, _cx: &mut Context<Self>) -> VecDeque<RecentEntry> {
210        #[cfg(debug_assertions)]
211        if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
212            return VecDeque::new();
213        }
214
215        self.recently_opened_entries.clone()
216    }
217}