history_store.rs

  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}