1use acp_thread::{AcpThreadMetadata, AgentConnection, AgentServerName};
2use agent::{ThreadId, thread_store::ThreadStore};
3use agent_client_protocol as acp;
4use anyhow::{Context as _, Result};
5use assistant_context::SavedContextMetadata;
6use chrono::{DateTime, Utc};
7use collections::HashMap;
8use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::*};
9use itertools::Itertools;
10use paths::contexts_dir;
11use serde::{Deserialize, Serialize};
12use smol::stream::StreamExt;
13use std::{collections::VecDeque, path::Path, sync::Arc, time::Duration};
14use util::ResultExt as _;
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#[derive(Clone, Debug)]
21pub enum HistoryEntry {
22 Thread(AcpThreadMetadata),
23 Context(SavedContextMetadata),
24}
25
26impl HistoryEntry {
27 pub fn updated_at(&self) -> DateTime<Utc> {
28 match self {
29 HistoryEntry::Thread(thread) => thread.updated_at,
30 HistoryEntry::Context(context) => context.mtime.to_utc(),
31 }
32 }
33
34 pub fn id(&self) -> HistoryEntryId {
35 match self {
36 HistoryEntry::Thread(thread) => {
37 HistoryEntryId::Thread(thread.agent.clone(), thread.id.clone())
38 }
39 HistoryEntry::Context(context) => HistoryEntryId::Context(context.path.clone()),
40 }
41 }
42
43 pub fn title(&self) -> &SharedString {
44 match self {
45 HistoryEntry::Thread(thread) => &thread.title,
46 HistoryEntry::Context(context) => &context.title,
47 }
48 }
49}
50
51/// Generic identifier for a history entry.
52#[derive(Clone, PartialEq, Eq, Debug)]
53pub enum HistoryEntryId {
54 Thread(AgentServerName, acp::SessionId),
55 Context(Arc<Path>),
56}
57
58#[derive(Serialize, Deserialize)]
59enum SerializedRecentOpen {
60 Thread(String),
61 ContextName(String),
62 /// Old format which stores the full path
63 Context(String),
64}
65
66pub struct AgentHistory {
67 entries: HashMap<acp::SessionId, AcpThreadMetadata>,
68 _task: Task<Result<()>>,
69}
70
71pub struct HistoryStore {
72 agents: HashMap<AgentServerName, AgentHistory>,
73}
74
75impl HistoryStore {
76 pub fn new(cx: &mut Context<Self>) -> Self {
77 Self {
78 agents: HashMap::default(),
79 }
80 }
81
82 pub fn register_agent(
83 &mut self,
84 agent_name: AgentServerName,
85 connection: &dyn AgentConnection,
86 cx: &mut Context<Self>,
87 ) {
88 let Some(mut history) = connection.list_threads(cx) else {
89 return;
90 };
91 let task = cx.spawn(async move |this, cx| {
92 while let Some(updated_history) = history.next().await {
93 dbg!(&updated_history);
94 this.update(cx, |this, cx| {
95 for entry in updated_history {
96 let agent = this
97 .agents
98 .get_mut(&entry.agent)
99 .context("agent not found")?;
100 agent.entries.insert(entry.id.clone(), entry);
101 }
102 cx.notify();
103 anyhow::Ok(())
104 })??
105 }
106 Ok(())
107 });
108 self.agents.insert(
109 agent_name,
110 AgentHistory {
111 entries: Default::default(),
112 _task: task,
113 },
114 );
115 }
116
117 pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
118 let mut history_entries = Vec::new();
119
120 #[cfg(debug_assertions)]
121 if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
122 return history_entries;
123 }
124
125 history_entries.extend(
126 self.agents
127 .values()
128 .flat_map(|agent| agent.entries.values())
129 .cloned()
130 .map(HistoryEntry::Thread),
131 );
132 // todo!() include the text threads in here.
133
134 history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
135 history_entries
136 }
137
138 pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
139 self.entries(cx).into_iter().take(limit).collect()
140 }
141}