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