1use acp_thread::{AcpThreadMetadata, AgentConnection, AgentServerName};
2use agent_client_protocol as acp;
3use agent_servers::AgentServer;
4use assistant_context::SavedContextMetadata;
5use chrono::{DateTime, Utc};
6use collections::HashMap;
7use gpui::{Entity, Global, SharedString, Task, prelude::*};
8use project::Project;
9use serde::{Deserialize, Serialize};
10use ui::App;
11
12use std::{path::Path, rc::Rc, sync::Arc, time::Duration};
13
14use crate::NativeAgentServer;
15
16const MAX_RECENTLY_OPENED_ENTRIES: usize = 6;
17const NAVIGATION_HISTORY_PATH: &str = "agent-navigation-history.json";
18const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50);
19
20// todo!(put this in the UI)
21#[derive(Clone, Debug)]
22pub enum HistoryEntry {
23 AcpThread(AcpThreadMetadata),
24 TextThread(SavedContextMetadata),
25}
26
27impl HistoryEntry {
28 pub fn updated_at(&self) -> DateTime<Utc> {
29 match self {
30 HistoryEntry::AcpThread(thread) => thread.updated_at,
31 HistoryEntry::TextThread(context) => context.mtime.to_utc(),
32 }
33 }
34
35 pub fn id(&self) -> HistoryEntryId {
36 match self {
37 HistoryEntry::AcpThread(thread) => {
38 HistoryEntryId::Thread(thread.agent.clone(), thread.id.clone())
39 }
40 HistoryEntry::TextThread(context) => HistoryEntryId::Context(context.path.clone()),
41 }
42 }
43
44 pub fn title(&self) -> &SharedString {
45 match self {
46 HistoryEntry::AcpThread(thread) => &thread.title,
47 HistoryEntry::TextThread(context) => &context.title,
48 }
49 }
50}
51
52/// Generic identifier for a history entry.
53#[derive(Clone, PartialEq, Eq, Debug)]
54pub enum HistoryEntryId {
55 Thread(AgentServerName, acp::SessionId),
56 Context(Arc<Path>),
57}
58
59#[derive(Serialize, Deserialize)]
60enum SerializedRecentOpen {
61 Thread(String),
62 ContextName(String),
63 /// Old format which stores the full path
64 Context(String),
65}
66
67#[derive(Default)]
68pub struct AgentHistory {
69 entries: HashMap<acp::SessionId, AcpThreadMetadata>,
70 loaded: bool,
71}
72
73pub struct HistoryStore {
74 agents: HashMap<AgentServerName, AgentHistory>, // todo!() text threads
75}
76// note, we have to share the history store between all windows
77// because we only get updates from one connection at a time.
78struct GlobalHistoryStore(Entity<HistoryStore>);
79impl Global for GlobalHistoryStore {}
80
81impl HistoryStore {
82 pub fn get_or_init(project: &Entity<Project>, cx: &mut App) -> Entity<Self> {
83 if cx.has_global::<GlobalHistoryStore>() {
84 return cx.global::<GlobalHistoryStore>().0.clone();
85 }
86 let history_store = cx.new(|cx| HistoryStore::new(cx));
87 cx.set_global(GlobalHistoryStore(history_store.clone()));
88 let root_dir = project
89 .read(cx)
90 .visible_worktrees(cx)
91 .next()
92 .map(|worktree| worktree.read(cx).abs_path())
93 .unwrap_or_else(|| paths::home_dir().as_path().into());
94
95 let agent = NativeAgentServer::new(project.read(cx).fs().clone());
96 let connect = agent.connect(&root_dir, project, cx);
97 cx.spawn({
98 let history_store = history_store.clone();
99 async move |cx| {
100 let connection = connect.await?.history().unwrap();
101 history_store
102 .update(cx, |history_store, cx| {
103 history_store.load_history(agent.name(), connection.as_ref(), cx)
104 })?
105 .await
106 }
107 })
108 .detach_and_log_err(cx);
109 history_store
110 }
111
112 fn new(_cx: &mut Context<Self>) -> Self {
113 Self {
114 agents: HashMap::default(),
115 }
116 }
117
118 pub fn update_history(&mut self, entry: AcpThreadMetadata, cx: &mut Context<Self>) {
119 let agent = self
120 .agents
121 .entry(entry.agent.clone())
122 .or_insert(Default::default());
123
124 agent.entries.insert(entry.id.clone(), entry);
125 cx.notify()
126 }
127
128 pub fn load_history(
129 &mut self,
130 agent_name: AgentServerName,
131 connection: &dyn acp_thread::AgentHistory,
132 cx: &mut Context<Self>,
133 ) -> Task<anyhow::Result<()>> {
134 let threads = connection.list_threads(cx);
135 cx.spawn(async move |this, cx| {
136 let threads = threads.await?;
137
138 this.update(cx, |this, cx| {
139 this.agents.insert(
140 agent_name,
141 AgentHistory {
142 loaded: true,
143 entries: threads.into_iter().map(|t| (t.id.clone(), t)).collect(),
144 },
145 );
146 cx.notify()
147 })
148 })
149 }
150
151 pub fn entries(&mut self, _cx: &mut Context<Self>) -> Vec<HistoryEntry> {
152 let mut history_entries = Vec::new();
153
154 #[cfg(debug_assertions)]
155 if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
156 return history_entries;
157 }
158
159 history_entries.extend(
160 self.agents
161 .values_mut()
162 .flat_map(|history| history.entries.values().cloned()) // todo!("surface the loading state?")
163 .map(HistoryEntry::AcpThread),
164 );
165 // todo!() include the text threads in here.
166
167 history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
168 history_entries
169 }
170
171 pub fn recent_entries(&mut self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
172 self.entries(cx).into_iter().take(limit).collect()
173 }
174}