history_store.rs

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