History mostly working

Co-authored-by: Antonio Scandurra <me@as-cii.com>
This commit is contained in:
Conrad Irwin 2025-08-18 11:24:31 -06:00
parent 4b1a48e4de
commit fc076e84ca
9 changed files with 95 additions and 15 deletions

View file

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

View file

@ -263,15 +263,20 @@ impl NativeAgent {
} }
fn save_thread(&mut self, thread: Entity<Thread>, cx: &mut Context<Self>) { fn save_thread(&mut self, thread: Entity<Thread>, cx: &mut Context<Self>) {
dbg!();
let id = thread.read(cx).id().clone(); let id = thread.read(cx).id().clone();
dbg!();
let Some(session) = self.sessions.get_mut(&id) else { let Some(session) = self.sessions.get_mut(&id) else {
return; return;
}; };
dbg!();
let thread = thread.downgrade(); let thread = thread.downgrade();
let thread_database = self.thread_database.clone(); let thread_database = self.thread_database.clone();
dbg!();
session.save_task = cx.spawn(async move |this, cx| { session.save_task = cx.spawn(async move |this, cx| {
cx.background_executor().timer(SAVE_THREAD_DEBOUNCE).await; cx.background_executor().timer(SAVE_THREAD_DEBOUNCE).await;
dbg!();
let db_thread = thread.update(cx, |thread, cx| thread.to_db(cx))?.await; let db_thread = thread.update(cx, |thread, cx| thread.to_db(cx))?.await;
thread_database.save_thread(id, db_thread).await?; thread_database.save_thread(id, db_thread).await?;
this.update(cx, |this, cx| this.reload_history(cx))?; this.update(cx, |this, cx| this.reload_history(cx))?;
@ -1049,12 +1054,15 @@ impl acp_thread::AgentSessionResume for NativeAgentSessionResume {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{HistoryEntry, HistoryStore};
use super::*; use super::*;
use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelId, AgentModelInfo}; use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelId, AgentModelInfo};
use fs::FakeFs; use fs::FakeFs;
use gpui::TestAppContext; use gpui::TestAppContext;
use serde_json::json; use serde_json::json;
use settings::SettingsStore; use settings::SettingsStore;
use util::path;
#[gpui::test] #[gpui::test]
async fn test_maintaining_project_context(cx: &mut TestAppContext) { 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) { fn init_test(cx: &mut TestAppContext) {
env_logger::try_init().ok(); env_logger::try_init().ok();
cx.update(|cx| { cx.update(|cx| {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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