history_store.rs

  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}