history_store.rs

  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}