History mostly working

Conrad Irwin and Antonio Scandurra created

Co-authored-by: Antonio Scandurra <me@as-cii.com>

Change summary

crates/agent2/Cargo.toml                   |  1 
crates/agent2/src/agent.rs                 | 68 ++++++++++++++++++++++++
crates/agent2/src/agent2.rs                |  3 
crates/agent2/src/db.rs                    |  3 +
crates/agent2/src/history_store.rs         | 19 +++---
crates/agent2/src/thread.rs                |  1 
crates/agent_ui/src/acp/thread_history.rs  |  4 
crates/agent_ui/src/agent_panel.rs         |  6 +-
crates/language_model/src/fake_provider.rs |  5 +
9 files changed, 95 insertions(+), 15 deletions(-)

Detailed changes

crates/agent2/Cargo.toml 🔗

@@ -66,6 +66,7 @@ assistant_context.workspace = true
 
 [dev-dependencies]
 agent = { workspace = true, "features" = ["test-support"] }
+acp_thread = { workspace = true, "features" = ["test-support"] }
 ctor.workspace = true
 client = { workspace = true, "features" = ["test-support"] }
 clock = { workspace = true, "features" = ["test-support"] }

crates/agent2/src/agent.rs 🔗

@@ -263,15 +263,20 @@ impl NativeAgent {
     }
 
     fn save_thread(&mut self, thread: Entity<Thread>, cx: &mut Context<Self>) {
+        dbg!();
         let id = thread.read(cx).id().clone();
+        dbg!();
         let Some(session) = self.sessions.get_mut(&id) else {
             return;
         };
+        dbg!();
 
         let thread = thread.downgrade();
         let thread_database = self.thread_database.clone();
+        dbg!();
         session.save_task = cx.spawn(async move |this, cx| {
             cx.background_executor().timer(SAVE_THREAD_DEBOUNCE).await;
+            dbg!();
             let db_thread = thread.update(cx, |thread, cx| thread.to_db(cx))?.await;
             thread_database.save_thread(id, db_thread).await?;
             this.update(cx, |this, cx| this.reload_history(cx))?;
@@ -1049,12 +1054,15 @@ impl acp_thread::AgentSessionResume for NativeAgentSessionResume {
 
 #[cfg(test)]
 mod tests {
+    use crate::{HistoryEntry, HistoryStore};
+
     use super::*;
     use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelId, AgentModelInfo};
     use fs::FakeFs;
     use gpui::TestAppContext;
     use serde_json::json;
     use settings::SettingsStore;
+    use util::path;
 
     #[gpui::test]
     async fn test_maintaining_project_context(cx: &mut TestAppContext) {
@@ -1229,6 +1237,66 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_history(cx: &mut TestAppContext) {
+        init_test(cx);
+        let fs = FakeFs::new(cx.executor());
+        let project = Project::test(fs.clone(), [], cx).await;
+
+        let agent = NativeAgent::new(
+            project.clone(),
+            Templates::new(),
+            None,
+            fs.clone(),
+            &mut cx.to_async(),
+        )
+        .await
+        .unwrap();
+        let model = cx.update(|cx| {
+            LanguageModelRegistry::global(cx)
+                .read(cx)
+                .default_model()
+                .unwrap()
+                .model
+        });
+        let connection = NativeAgentConnection(agent.clone());
+        let history_store = cx.new(|cx| {
+            let mut store = HistoryStore::new(cx);
+            store.register_agent(NATIVE_AGENT_SERVER_NAME.clone(), &connection, cx);
+            store
+        });
+
+        let acp_thread = cx
+            .update(|cx| {
+                Rc::new(connection.clone()).new_thread(project.clone(), Path::new(path!("")), cx)
+            })
+            .await
+            .unwrap();
+        let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone());
+        let selector = connection.model_selector().unwrap();
+
+        let model = cx
+            .update(|cx| selector.selected_model(&session_id, cx))
+            .await
+            .expect("selected_model should succeed");
+        let model = cx
+            .update(|cx| agent.read(cx).models().model_from_id(&model.id))
+            .unwrap();
+        let model = model.as_fake();
+
+        let send = acp_thread.update(cx, |thread, cx| thread.send_raw("Hi", cx));
+        let send = cx.foreground_executor().spawn(send);
+        cx.run_until_parked();
+        model.send_last_completion_stream_text_chunk("Hey");
+        model.end_last_completion_stream();
+        dbg!(send.await.unwrap());
+        cx.executor().advance_clock(SAVE_THREAD_DEBOUNCE);
+
+        let history = history_store.update(cx, |store, cx| store.entries(cx));
+        assert_eq!(history.len(), 1);
+        assert_eq!(history[0].title(), "Hi");
+    }
+
     fn init_test(cx: &mut TestAppContext) {
         env_logger::try_init().ok();
         cx.update(|cx| {

crates/agent2/src/agent2.rs 🔗

@@ -1,6 +1,6 @@
 mod agent;
 mod db;
-pub mod history_store;
+mod history_store;
 mod native_agent_server;
 mod templates;
 mod thread;
@@ -11,6 +11,7 @@ mod tests;
 
 pub use agent::*;
 pub use db::*;
+pub use history_store::*;
 pub use native_agent_server::NativeAgentServer;
 pub use templates::*;
 pub use thread::*;

crates/agent2/src/db.rs 🔗

@@ -386,6 +386,9 @@ impl ThreadsDatabase {
 
 #[cfg(test)]
 mod tests {
+    use crate::NativeAgent;
+    use crate::Templates;
+
     use super::*;
     use agent::MessageSegment;
     use agent::context::LoadedContext;

crates/agent2/src/history_store.rs 🔗

@@ -13,33 +13,34 @@ const MAX_RECENTLY_OPENED_ENTRIES: usize = 6;
 const NAVIGATION_HISTORY_PATH: &str = "agent-navigation-history.json";
 const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50);
 
+// todo!(put this in the UI)
 #[derive(Clone, Debug)]
 pub enum HistoryEntry {
-    Thread(AcpThreadMetadata),
-    Context(SavedContextMetadata),
+    AcpThread(AcpThreadMetadata),
+    TextThread(SavedContextMetadata),
 }
 
 impl HistoryEntry {
     pub fn updated_at(&self) -> DateTime<Utc> {
         match self {
-            HistoryEntry::Thread(thread) => thread.updated_at,
-            HistoryEntry::Context(context) => context.mtime.to_utc(),
+            HistoryEntry::AcpThread(thread) => thread.updated_at,
+            HistoryEntry::TextThread(context) => context.mtime.to_utc(),
         }
     }
 
     pub fn id(&self) -> HistoryEntryId {
         match self {
-            HistoryEntry::Thread(thread) => {
+            HistoryEntry::AcpThread(thread) => {
                 HistoryEntryId::Thread(thread.agent.clone(), thread.id.clone())
             }
-            HistoryEntry::Context(context) => HistoryEntryId::Context(context.path.clone()),
+            HistoryEntry::TextThread(context) => HistoryEntryId::Context(context.path.clone()),
         }
     }
 
     pub fn title(&self) -> &SharedString {
         match self {
-            HistoryEntry::Thread(thread) => &thread.title,
-            HistoryEntry::Context(context) => &context.title,
+            HistoryEntry::AcpThread(thread) => &thread.title,
+            HistoryEntry::TextThread(context) => &context.title,
         }
     }
 }
@@ -107,7 +108,7 @@ impl HistoryStore {
             self.agents
                 .values_mut()
                 .flat_map(|history| history.entries.borrow().clone().unwrap_or_default()) // todo!("surface the loading state?")
-                .map(HistoryEntry::Thread),
+                .map(HistoryEntry::AcpThread),
         );
         // todo!() include the text threads in here.
 

crates/agent2/src/thread.rs 🔗

@@ -1283,6 +1283,7 @@ impl Thread {
         }
 
         self.messages.push(Message::Agent(message));
+        dbg!("!!!!!!!!!!!!!!!!!!!!!!!");
         cx.notify()
     }
 

crates/agent_ui/src/acp/thread_history.rs 🔗

@@ -236,10 +236,10 @@ impl AcpThreadHistory {
 
                 for (idx, entry) in all_entries.iter().enumerate() {
                     match entry {
-                        HistoryEntry::Thread(thread) => {
+                        HistoryEntry::AcpThread(thread) => {
                             candidates.push(StringMatchCandidate::new(idx, &thread.title));
                         }
-                        HistoryEntry::Context(context) => {
+                        HistoryEntry::TextThread(context) => {
                             candidates.push(StringMatchCandidate::new(idx, &context.title));
                         }
                     }

crates/agent_ui/src/agent_panel.rs 🔗

@@ -6,7 +6,7 @@ use std::time::Duration;
 
 use acp_thread::AcpThreadMetadata;
 use agent_servers::AgentServer;
-use agent2::history_store::HistoryEntry;
+use agent2::HistoryEntry;
 use db::kvp::{Dismissable, KEY_VALUE_STORE};
 use serde::{Deserialize, Serialize};
 
@@ -752,7 +752,7 @@ impl AgentPanel {
             &acp_history,
             window,
             |this, _, event, window, cx| match event {
-                ThreadHistoryEvent::Open(HistoryEntry::Thread(thread)) => {
+                ThreadHistoryEvent::Open(HistoryEntry::AcpThread(thread)) => {
                     let agent_choice = match thread.agent.0.as_ref() {
                         "Claude Code" => Some(ExternalAgent::ClaudeCode),
                         "Gemini" => Some(ExternalAgent::Gemini),
@@ -761,7 +761,7 @@ impl AgentPanel {
                     };
                     this.new_external_thread(agent_choice, Some(thread.clone()), window, cx);
                 }
-                ThreadHistoryEvent::Open(HistoryEntry::Context(thread)) => {
+                ThreadHistoryEvent::Open(HistoryEntry::TextThread(thread)) => {
                     todo!()
                 }
             },

crates/language_model/src/fake_provider.rs 🔗

@@ -102,6 +102,8 @@ pub struct FakeLanguageModel {
 
 impl Default for FakeLanguageModel {
     fn default() -> Self {
+        dbg!("default......");
+        eprintln!("{}", std::backtrace::Backtrace::force_capture());
         Self {
             provider_id: LanguageModelProviderId::from("fake".to_string()),
             provider_name: LanguageModelProviderName::from("Fake".to_string()),
@@ -149,12 +151,14 @@ impl FakeLanguageModel {
     }
 
     pub fn end_completion_stream(&self, request: &LanguageModelRequest) {
+        dbg!("remove...");
         self.current_completion_txs
             .lock()
             .retain(|(req, _)| req != request);
     }
 
     pub fn send_last_completion_stream_text_chunk(&self, chunk: impl Into<String>) {
+        dbg!("read...");
         self.send_completion_stream_text_chunk(self.pending_completions().last().unwrap(), chunk);
     }
 
@@ -223,6 +227,7 @@ impl LanguageModel for FakeLanguageModel {
         >,
     > {
         let (tx, rx) = mpsc::unbounded();
+        dbg!("insert...");
         self.current_completion_txs.lock().push((request, tx));
         async move { Ok(rx.map(Ok).boxed()) }.boxed()
     }