1use acp_thread::{AcpThreadMetadata, AgentConnection, AgentServerName};
2use agent_client_protocol as acp;
3use assistant_context::SavedContextMetadata;
4use chrono::{DateTime, Utc};
5use collections::HashMap;
6use gpui::{SharedString, Task, prelude::*};
7use serde::{Deserialize, Serialize};
8
9use std::{path::Path, sync::Arc, time::Duration};
10
11const MAX_RECENTLY_OPENED_ENTRIES: usize = 6;
12const NAVIGATION_HISTORY_PATH: &str = "agent-navigation-history.json";
13const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50);
14
15// todo!(put this in the UI)
16#[derive(Clone, Debug)]
17pub enum HistoryEntry {
18 AcpThread(AcpThreadMetadata),
19 TextThread(SavedContextMetadata),
20}
21
22impl HistoryEntry {
23 pub fn updated_at(&self) -> DateTime<Utc> {
24 match self {
25 HistoryEntry::AcpThread(thread) => thread.updated_at,
26 HistoryEntry::TextThread(context) => context.mtime.to_utc(),
27 }
28 }
29
30 pub fn id(&self) -> HistoryEntryId {
31 match self {
32 HistoryEntry::AcpThread(thread) => {
33 HistoryEntryId::Thread(thread.agent.clone(), thread.id.clone())
34 }
35 HistoryEntry::TextThread(context) => HistoryEntryId::Context(context.path.clone()),
36 }
37 }
38
39 pub fn title(&self) -> &SharedString {
40 match self {
41 HistoryEntry::AcpThread(thread) => &thread.title,
42 HistoryEntry::TextThread(context) => &context.title,
43 }
44 }
45}
46
47/// Generic identifier for a history entry.
48#[derive(Clone, PartialEq, Eq, Debug)]
49pub enum HistoryEntryId {
50 Thread(AgentServerName, acp::SessionId),
51 Context(Arc<Path>),
52}
53
54#[derive(Serialize, Deserialize)]
55enum SerializedRecentOpen {
56 Thread(String),
57 ContextName(String),
58 /// Old format which stores the full path
59 Context(String),
60}
61
62pub struct AgentHistory {
63 entries: watch::Receiver<Option<Vec<AcpThreadMetadata>>>,
64 _task: Task<()>,
65}
66
67pub struct HistoryStore {
68 agents: HashMap<AgentServerName, AgentHistory>, // todo!() text threads
69}
70
71impl HistoryStore {
72 pub fn new(_cx: &mut Context<Self>) -> Self {
73 Self {
74 agents: HashMap::default(),
75 }
76 }
77
78 pub fn register_agent(
79 &mut self,
80 agent_name: AgentServerName,
81 connection: &dyn AgentConnection,
82 cx: &mut Context<Self>,
83 ) {
84 let Some(mut history) = connection.list_threads(cx) else {
85 return;
86 };
87 let history = AgentHistory {
88 entries: history.clone(),
89 _task: cx.spawn(async move |this, cx| {
90 dbg!("loaded", history.borrow().as_ref().map(|b| b.len()));
91 while history.changed().await.is_ok() {
92 this.update(cx, |_, cx| cx.notify()).ok();
93 }
94 }),
95 };
96 self.agents.insert(agent_name.clone(), history);
97 }
98
99 pub fn entries(&mut self, _cx: &mut Context<Self>) -> Vec<HistoryEntry> {
100 let mut history_entries = Vec::new();
101
102 #[cfg(debug_assertions)]
103 if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
104 return history_entries;
105 }
106
107 history_entries.extend(
108 self.agents
109 .values_mut()
110 .flat_map(|history| history.entries.borrow().clone().unwrap_or_default()) // todo!("surface the loading state?")
111 .map(HistoryEntry::AcpThread),
112 );
113 // todo!() include the text threads in here.
114
115 history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
116 history_entries
117 }
118
119 pub fn recent_entries(&mut self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
120 self.entries(cx).into_iter().take(limit).collect()
121 }
122}