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, Window};
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 window: &mut Window,
86 cx: &mut Context<Self>,
87 ) -> Self {
88 let subscriptions = vec![
89 cx.observe(&thread_store, |_, _, cx| cx.notify()),
90 cx.observe(&context_store, |_, _, cx| cx.notify()),
91 ];
92
93 window
94 .spawn(cx, {
95 let thread_store = thread_store.downgrade();
96 let context_store = context_store.downgrade();
97 let this = cx.weak_entity();
98 async move |cx| {
99 let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
100 let contents = cx
101 .background_spawn(async move { std::fs::read_to_string(path) })
102 .await
103 .ok()?;
104 let entries = serde_json::from_str::<Vec<SerializedRecentEntry>>(&contents)
105 .context("deserializing persisted agent panel navigation history")
106 .log_err()?
107 .into_iter()
108 .take(MAX_RECENTLY_OPENED_ENTRIES)
109 .map(|serialized| match serialized {
110 SerializedRecentEntry::Thread(id) => thread_store
111 .update_in(cx, |thread_store, window, cx| {
112 let thread_id = ThreadId::from(id.as_str());
113 thread_store
114 .open_thread(&thread_id, window, cx)
115 .map_ok(|thread| RecentEntry::Thread(thread_id, thread))
116 .boxed()
117 })
118 .unwrap_or_else(|_| {
119 async { Err(anyhow!("no thread store")) }.boxed()
120 }),
121 SerializedRecentEntry::Context(id) => context_store
122 .update(cx, |context_store, cx| {
123 context_store
124 .open_local_context(Path::new(&id).into(), cx)
125 .map_ok(RecentEntry::Context)
126 .boxed()
127 })
128 .unwrap_or_else(|_| {
129 async { Err(anyhow!("no context store")) }.boxed()
130 }),
131 });
132 let entries = join_all(entries)
133 .await
134 .into_iter()
135 .filter_map(|result| result.log_err())
136 .collect::<VecDeque<_>>();
137
138 this.update(cx, |this, _| {
139 this.recently_opened_entries.extend(entries);
140 this.recently_opened_entries
141 .truncate(MAX_RECENTLY_OPENED_ENTRIES);
142 })
143 .ok();
144
145 Some(())
146 }
147 })
148 .detach();
149
150 Self {
151 thread_store,
152 context_store,
153 recently_opened_entries: initial_recent_entries.into_iter().collect(),
154 _subscriptions: subscriptions,
155 _save_recently_opened_entries_task: Task::ready(()),
156 }
157 }
158
159 pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
160 let mut history_entries = Vec::new();
161
162 #[cfg(debug_assertions)]
163 if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
164 return history_entries;
165 }
166
167 for thread in self
168 .thread_store
169 .update(cx, |this, _cx| this.reverse_chronological_threads())
170 {
171 history_entries.push(HistoryEntry::Thread(thread));
172 }
173
174 for context in self
175 .context_store
176 .update(cx, |this, _cx| this.reverse_chronological_contexts())
177 {
178 history_entries.push(HistoryEntry::Context(context));
179 }
180
181 history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
182 history_entries
183 }
184
185 pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
186 self.entries(cx).into_iter().take(limit).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 RecentEntry::Context(context) => Some(SerializedRecentEntry::Context(
195 context.read(cx).path()?.to_str()?.to_owned(),
196 )),
197 RecentEntry::Thread(id, _) => Some(SerializedRecentEntry::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 pub fn push_recently_opened_entry(&mut self, entry: RecentEntry, cx: &mut Context<Self>) {
217 self.recently_opened_entries
218 .retain(|old_entry| old_entry != &entry);
219 self.recently_opened_entries.push_front(entry);
220 self.recently_opened_entries
221 .truncate(MAX_RECENTLY_OPENED_ENTRIES);
222 self.save_recently_opened_entries(cx);
223 }
224
225 pub fn remove_recently_opened_thread(&mut self, id: ThreadId, cx: &mut Context<Self>) {
226 self.recently_opened_entries.retain(|entry| match entry {
227 RecentEntry::Thread(thread_id, _) if thread_id == &id => false,
228 _ => true,
229 });
230 self.save_recently_opened_entries(cx);
231 }
232
233 pub fn remove_recently_opened_entry(&mut self, entry: &RecentEntry, cx: &mut Context<Self>) {
234 self.recently_opened_entries
235 .retain(|old_entry| old_entry != entry);
236 self.save_recently_opened_entries(cx);
237 }
238
239 pub fn recently_opened_entries(&self, _cx: &mut Context<Self>) -> VecDeque<RecentEntry> {
240 #[cfg(debug_assertions)]
241 if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
242 return VecDeque::new();
243 }
244
245 self.recently_opened_entries.clone()
246 }
247}