history_store.rs

  1use std::{collections::VecDeque, path::Path, sync::Arc};
  2
  3use anyhow::{Context as _, Result};
  4use assistant_context_editor::SavedContextMetadata;
  5use chrono::{DateTime, Utc};
  6use gpui::{AsyncApp, Entity, SharedString, Task, prelude::*};
  7use itertools::Itertools;
  8use paths::contexts_dir;
  9use serde::{Deserialize, Serialize};
 10use std::time::Duration;
 11use ui::App;
 12use util::ResultExt as _;
 13
 14use crate::{
 15    thread::ThreadId,
 16    thread_store::{SerializedThreadMetadata, ThreadStore},
 17};
 18
 19const MAX_RECENTLY_OPENED_ENTRIES: usize = 6;
 20const NAVIGATION_HISTORY_PATH: &str = "agent-navigation-history.json";
 21const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50);
 22
 23#[derive(Clone, Debug)]
 24pub enum HistoryEntry {
 25    Thread(SerializedThreadMetadata),
 26    Context(SavedContextMetadata),
 27}
 28
 29impl HistoryEntry {
 30    pub fn updated_at(&self) -> DateTime<Utc> {
 31        match self {
 32            HistoryEntry::Thread(thread) => thread.updated_at,
 33            HistoryEntry::Context(context) => context.mtime.to_utc(),
 34        }
 35    }
 36
 37    pub fn id(&self) -> HistoryEntryId {
 38        match self {
 39            HistoryEntry::Thread(thread) => HistoryEntryId::Thread(thread.id.clone()),
 40            HistoryEntry::Context(context) => HistoryEntryId::Context(context.path.clone()),
 41        }
 42    }
 43
 44    pub fn title(&self) -> &SharedString {
 45        match self {
 46            HistoryEntry::Thread(thread) => &thread.summary,
 47            HistoryEntry::Context(context) => &context.title,
 48        }
 49    }
 50}
 51
 52/// Generic identifier for a history entry.
 53#[derive(Clone, PartialEq, Eq, Debug)]
 54pub enum HistoryEntryId {
 55    Thread(ThreadId),
 56    Context(Arc<Path>),
 57}
 58
 59#[derive(Serialize, Deserialize)]
 60enum SerializedRecentOpen {
 61    Thread(String),
 62    ContextName(String),
 63    /// Old format which stores the full path
 64    Context(String),
 65}
 66
 67pub struct HistoryStore {
 68    thread_store: Entity<ThreadStore>,
 69    context_store: Entity<assistant_context_editor::ContextStore>,
 70    recently_opened_entries: VecDeque<HistoryEntryId>,
 71    _subscriptions: Vec<gpui::Subscription>,
 72    _save_recently_opened_entries_task: Task<()>,
 73}
 74
 75impl HistoryStore {
 76    pub fn new(
 77        thread_store: Entity<ThreadStore>,
 78        context_store: Entity<assistant_context_editor::ContextStore>,
 79        initial_recent_entries: impl IntoIterator<Item = HistoryEntryId>,
 80        cx: &mut Context<Self>,
 81    ) -> Self {
 82        let subscriptions = vec![
 83            cx.observe(&thread_store, |_, _, cx| cx.notify()),
 84            cx.observe(&context_store, |_, _, cx| cx.notify()),
 85        ];
 86
 87        cx.spawn(async move |this, cx| {
 88            let entries = Self::load_recently_opened_entries(cx).await.log_err()?;
 89            this.update(cx, |this, _| {
 90                this.recently_opened_entries
 91                    .extend(
 92                        entries.into_iter().take(
 93                            MAX_RECENTLY_OPENED_ENTRIES
 94                                .saturating_sub(this.recently_opened_entries.len()),
 95                        ),
 96                    );
 97            })
 98            .ok()
 99        })
100        .detach();
101
102        Self {
103            thread_store,
104            context_store,
105            recently_opened_entries: initial_recent_entries.into_iter().collect(),
106            _subscriptions: subscriptions,
107            _save_recently_opened_entries_task: Task::ready(()),
108        }
109    }
110
111    pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
112        let mut history_entries = Vec::new();
113
114        #[cfg(debug_assertions)]
115        if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
116            return history_entries;
117        }
118
119        history_entries.extend(
120            self.thread_store
121                .read(cx)
122                .reverse_chronological_threads()
123                .cloned()
124                .map(HistoryEntry::Thread),
125        );
126        history_entries.extend(
127            self.context_store
128                .read(cx)
129                .unordered_contexts()
130                .cloned()
131                .map(HistoryEntry::Context),
132        );
133
134        history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
135        history_entries
136    }
137
138    pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
139        self.entries(cx).into_iter().take(limit).collect()
140    }
141
142    pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> {
143        #[cfg(debug_assertions)]
144        if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
145            return Vec::new();
146        }
147
148        let thread_entries = self
149            .thread_store
150            .read(cx)
151            .reverse_chronological_threads()
152            .flat_map(|thread| {
153                self.recently_opened_entries
154                    .iter()
155                    .enumerate()
156                    .flat_map(|(index, entry)| match entry {
157                        HistoryEntryId::Thread(id) if &thread.id == id => {
158                            Some((index, HistoryEntry::Thread(thread.clone())))
159                        }
160                        _ => None,
161                    })
162            });
163
164        let context_entries =
165            self.context_store
166                .read(cx)
167                .unordered_contexts()
168                .flat_map(|context| {
169                    self.recently_opened_entries
170                        .iter()
171                        .enumerate()
172                        .flat_map(|(index, entry)| match entry {
173                            HistoryEntryId::Context(path) if &context.path == path => {
174                                Some((index, HistoryEntry::Context(context.clone())))
175                            }
176                            _ => None,
177                        })
178                });
179
180        thread_entries
181            .chain(context_entries)
182            // optimization to halt iteration early
183            .take(self.recently_opened_entries.len())
184            .sorted_unstable_by_key(|(index, _)| *index)
185            .map(|(_, entry)| entry)
186            .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                HistoryEntryId::Context(path) => path.file_name().map(|file| {
195                    SerializedRecentOpen::ContextName(file.to_string_lossy().to_string())
196                }),
197                HistoryEntryId::Thread(id) => Some(SerializedRecentOpen::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    fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<Vec<HistoryEntryId>>> {
217        cx.background_spawn(async move {
218            let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
219            let contents = smol::fs::read_to_string(path).await?;
220            let entries = serde_json::from_str::<Vec<SerializedRecentOpen>>(&contents)
221                .context("deserializing persisted agent panel navigation history")?
222                .into_iter()
223                .take(MAX_RECENTLY_OPENED_ENTRIES)
224                .flat_map(|entry| match entry {
225                    SerializedRecentOpen::Thread(id) => {
226                        Some(HistoryEntryId::Thread(id.as_str().into()))
227                    }
228                    SerializedRecentOpen::ContextName(file_name) => Some(HistoryEntryId::Context(
229                        contexts_dir().join(file_name).into(),
230                    )),
231                    SerializedRecentOpen::Context(path) => {
232                        Path::new(&path).file_name().map(|file_name| {
233                            HistoryEntryId::Context(contexts_dir().join(file_name).into())
234                        })
235                    }
236                })
237                .collect::<Vec<_>>();
238            Ok(entries)
239        })
240    }
241
242    pub fn push_recently_opened_entry(&mut self, entry: HistoryEntryId, cx: &mut Context<Self>) {
243        self.recently_opened_entries
244            .retain(|old_entry| old_entry != &entry);
245        self.recently_opened_entries.push_front(entry);
246        self.recently_opened_entries
247            .truncate(MAX_RECENTLY_OPENED_ENTRIES);
248        self.save_recently_opened_entries(cx);
249    }
250
251    pub fn remove_recently_opened_thread(&mut self, id: ThreadId, cx: &mut Context<Self>) {
252        self.recently_opened_entries.retain(|entry| match entry {
253            HistoryEntryId::Thread(thread_id) if thread_id == &id => false,
254            _ => true,
255        });
256        self.save_recently_opened_entries(cx);
257    }
258
259    pub fn replace_recently_opened_text_thread(
260        &mut self,
261        old_path: &Path,
262        new_path: &Arc<Path>,
263        cx: &mut Context<Self>,
264    ) {
265        for entry in &mut self.recently_opened_entries {
266            match entry {
267                HistoryEntryId::Context(path) if path.as_ref() == old_path => {
268                    *entry = HistoryEntryId::Context(new_path.clone());
269                    break;
270                }
271                _ => {}
272            }
273        }
274        self.save_recently_opened_entries(cx);
275    }
276
277    pub fn remove_recently_opened_entry(&mut self, entry: &HistoryEntryId, cx: &mut Context<Self>) {
278        self.recently_opened_entries
279            .retain(|old_entry| old_entry != entry);
280        self.save_recently_opened_entries(cx);
281    }
282}