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 while history.changed().await.is_ok() {
91 this.update(cx, |_, cx| cx.notify()).ok();
92 }
93 }),
94 };
95 self.agents.insert(agent_name.clone(), history);
96 }
97
98 pub fn entries(&mut self, _cx: &mut Context<Self>) -> Vec<HistoryEntry> {
99 let mut history_entries = Vec::new();
100
101 #[cfg(debug_assertions)]
102 if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
103 return history_entries;
104 }
105
106 history_entries.extend(
107 self.agents
108 .values_mut()
109 .flat_map(|history| history.entries.borrow().clone().unwrap_or_default()) // todo!("surface the loading state?")
110 .map(HistoryEntry::AcpThread),
111 );
112 // todo!() include the text threads in here.
113
114 history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
115 history_entries
116 }
117
118 pub fn recent_entries(&mut self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
119 self.entries(cx).into_iter().take(limit).collect()
120 }
121}