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}