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}