Re-wire history
This commit is contained in:
parent
8998cdee26
commit
3b7ad6236d
8 changed files with 294 additions and 210 deletions
|
@ -937,7 +937,6 @@ impl AcpThread {
|
|||
}
|
||||
|
||||
pub fn update_title(&mut self, title: SharedString, cx: &mut Context<Self>) -> Result<()> {
|
||||
dbg!("update title", &title);
|
||||
self.title = title;
|
||||
cx.emit(AcpThreadEvent::TitleUpdated);
|
||||
Ok(())
|
||||
|
|
|
@ -2,6 +2,7 @@ use crate::{AcpThread, AcpThreadMetadata};
|
|||
use agent_client_protocol::{self as acp};
|
||||
use anyhow::Result;
|
||||
use collections::IndexMap;
|
||||
use futures::channel::mpsc::UnboundedReceiver;
|
||||
use gpui::{Entity, SharedString, Task};
|
||||
use project::Project;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -26,25 +27,6 @@ pub trait AgentConnection {
|
|||
cx: &mut App,
|
||||
) -> Task<Result<Entity<AcpThread>>>;
|
||||
|
||||
// todo!(expose a history trait, and include list_threads and load_thread)
|
||||
// todo!(write a test)
|
||||
fn list_threads(
|
||||
&self,
|
||||
_cx: &mut App,
|
||||
) -> Option<watch::Receiver<Option<Vec<AcpThreadMetadata>>>> {
|
||||
return None;
|
||||
}
|
||||
|
||||
fn load_thread(
|
||||
self: Rc<Self>,
|
||||
_project: Entity<Project>,
|
||||
_cwd: &Path,
|
||||
_session_id: acp::SessionId,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<Entity<AcpThread>>> {
|
||||
Task::ready(Err(anyhow::anyhow!("load thread not implemented")))
|
||||
}
|
||||
|
||||
fn auth_methods(&self) -> &[acp::AuthMethod];
|
||||
|
||||
fn authenticate(&self, method: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>>;
|
||||
|
@ -82,6 +64,10 @@ pub trait AgentConnection {
|
|||
None
|
||||
}
|
||||
|
||||
fn history(self: Rc<Self>) -> Option<Rc<dyn AgentHistory>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
|
||||
}
|
||||
|
||||
|
@ -99,6 +85,18 @@ pub trait AgentSessionResume {
|
|||
fn run(&self, cx: &mut App) -> Task<Result<acp::PromptResponse>>;
|
||||
}
|
||||
|
||||
pub trait AgentHistory {
|
||||
fn list_threads(&self, cx: &mut App) -> Task<Result<Vec<AcpThreadMetadata>>>;
|
||||
fn observe_history(&self, cx: &mut App) -> UnboundedReceiver<AcpThreadMetadata>;
|
||||
fn load_thread(
|
||||
self: Rc<Self>,
|
||||
_project: Entity<Project>,
|
||||
_cwd: &Path,
|
||||
_session_id: acp::SessionId,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<Entity<AcpThread>>>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AuthRequired;
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::{
|
|||
UserMessageContent, WebSearchTool, templates::Templates,
|
||||
};
|
||||
use crate::{ThreadsDatabase, generate_session_id};
|
||||
use acp_thread::{AcpThread, AcpThreadMetadata, AgentModelSelector};
|
||||
use acp_thread::{AcpThread, AcpThreadMetadata, AgentHistory, AgentModelSelector};
|
||||
use agent_client_protocol as acp;
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
|
@ -56,7 +56,7 @@ struct Session {
|
|||
thread: Entity<Thread>,
|
||||
/// The ACP thread that handles protocol communication
|
||||
acp_thread: WeakEntity<acp_thread::AcpThread>,
|
||||
save_task: Task<Result<()>>,
|
||||
save_task: Task<()>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
|
@ -173,8 +173,7 @@ pub struct NativeAgent {
|
|||
project: Entity<Project>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
thread_database: Arc<ThreadsDatabase>,
|
||||
history: watch::Sender<Option<Vec<AcpThreadMetadata>>>,
|
||||
load_history: Task<()>,
|
||||
history_watchers: Vec<mpsc::UnboundedSender<AcpThreadMetadata>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
@ -212,7 +211,7 @@ impl NativeAgent {
|
|||
|
||||
let (project_context_needs_refresh_tx, project_context_needs_refresh_rx) =
|
||||
watch::channel(());
|
||||
let mut this = Self {
|
||||
Self {
|
||||
sessions: HashMap::new(),
|
||||
project_context: Rc::new(RefCell::new(project_context)),
|
||||
project_context_needs_refresh: project_context_needs_refresh_tx,
|
||||
|
@ -228,12 +227,9 @@ impl NativeAgent {
|
|||
project,
|
||||
prompt_store,
|
||||
fs,
|
||||
history: watch::channel(None).0,
|
||||
load_history: Task::ready(()),
|
||||
history_watchers: Vec::new(),
|
||||
_subscriptions: subscriptions,
|
||||
};
|
||||
this.reload_history(cx);
|
||||
this
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -250,7 +246,7 @@ impl NativeAgent {
|
|||
Session {
|
||||
thread: thread.clone(),
|
||||
acp_thread: weak_thread.clone(),
|
||||
save_task: Task::ready(Ok(())),
|
||||
save_task: Task::ready(()),
|
||||
_subscriptions: vec![
|
||||
cx.observe_release(&acp_thread, |this, acp_thread, _cx| {
|
||||
this.sessions.remove(acp_thread.session_id());
|
||||
|
@ -285,35 +281,23 @@ impl NativeAgent {
|
|||
session.save_task = cx.spawn(async move |this, cx| {
|
||||
cx.background_executor().timer(SAVE_THREAD_DEBOUNCE).await;
|
||||
|
||||
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))?;
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
fn reload_history(&mut self, cx: &mut Context<Self>) {
|
||||
let thread_database = self.thread_database.clone();
|
||||
self.load_history = cx.spawn(async move |this, cx| {
|
||||
let results = cx
|
||||
.background_spawn(async move {
|
||||
let results = thread_database.list_threads().await?;
|
||||
anyhow::Ok(
|
||||
results
|
||||
.into_iter()
|
||||
.map(|thread| AcpThreadMetadata {
|
||||
agent: NATIVE_AGENT_SERVER_NAME.clone(),
|
||||
id: thread.id.into(),
|
||||
title: thread.title,
|
||||
updated_at: thread.updated_at,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
let Some(task) = thread.update(cx, |thread, cx| thread.to_db(cx)).ok() else {
|
||||
return;
|
||||
};
|
||||
let db_thread = task.await;
|
||||
let metadata = thread_database
|
||||
.save_thread(id.clone(), db_thread)
|
||||
.await
|
||||
.log_err();
|
||||
if let Some(metadata) = metadata {
|
||||
this.update(cx, |this, _| {
|
||||
for watcher in this.history_watchers.iter_mut() {
|
||||
watcher
|
||||
.unbounded_send(metadata.clone().to_acp(NATIVE_AGENT_SERVER_NAME))
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.await;
|
||||
if let Some(results) = results.log_err() {
|
||||
this.update(cx, |this, _| this.history.send(Some(results)))
|
||||
.ok();
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -667,7 +651,6 @@ impl NativeAgentConnection {
|
|||
})??;
|
||||
}
|
||||
ThreadEvent::TitleUpdate(title) => {
|
||||
dbg!("updating title");
|
||||
acp_thread
|
||||
.update(cx, |thread, cx| thread.update_title(title, cx))??;
|
||||
}
|
||||
|
@ -884,11 +867,106 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
|||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn list_threads(
|
||||
fn model_selector(&self) -> Option<Rc<dyn AgentModelSelector>> {
|
||||
Some(Rc::new(self.clone()) as Rc<dyn AgentModelSelector>)
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
id: Option<acp_thread::UserMessageId>,
|
||||
params: acp::PromptRequest,
|
||||
cx: &mut App,
|
||||
) -> Option<watch::Receiver<Option<Vec<AcpThreadMetadata>>>> {
|
||||
Some(self.0.read(cx).history.receiver())
|
||||
) -> Task<Result<acp::PromptResponse>> {
|
||||
let id = id.expect("UserMessageId is required");
|
||||
let session_id = params.session_id.clone();
|
||||
log::info!("Received prompt request for session: {}", session_id);
|
||||
log::debug!("Prompt blocks count: {}", params.prompt.len());
|
||||
|
||||
self.run_turn(session_id, cx, |thread, cx| {
|
||||
let content: Vec<UserMessageContent> = params
|
||||
.prompt
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>();
|
||||
log::info!("Converted prompt to message: {} chars", content.len());
|
||||
log::debug!("Message id: {:?}", id);
|
||||
log::debug!("Message content: {:?}", content);
|
||||
|
||||
thread.update(cx, |thread, cx| thread.send(id, content, cx))
|
||||
})
|
||||
}
|
||||
|
||||
fn resume(
|
||||
&self,
|
||||
session_id: &acp::SessionId,
|
||||
_cx: &mut App,
|
||||
) -> Option<Rc<dyn acp_thread::AgentSessionResume>> {
|
||||
Some(Rc::new(NativeAgentSessionResume {
|
||||
connection: self.clone(),
|
||||
session_id: session_id.clone(),
|
||||
}) as _)
|
||||
}
|
||||
|
||||
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
||||
log::info!("Cancelling on session: {}", session_id);
|
||||
self.0.update(cx, |agent, cx| {
|
||||
if let Some(agent) = agent.sessions.get(session_id) {
|
||||
agent.thread.update(cx, |thread, cx| thread.cancel(cx));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn session_editor(
|
||||
&self,
|
||||
session_id: &agent_client_protocol::SessionId,
|
||||
cx: &mut App,
|
||||
) -> Option<Rc<dyn acp_thread::AgentSessionEditor>> {
|
||||
self.0.update(cx, |agent, _cx| {
|
||||
agent
|
||||
.sessions
|
||||
.get(session_id)
|
||||
.map(|session| Rc::new(NativeAgentSessionEditor(session.thread.clone())) as _)
|
||||
})
|
||||
}
|
||||
|
||||
fn history(self: Rc<Self>) -> Option<Rc<dyn AgentHistory>> {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct NativeAgentSessionEditor(Entity<Thread>);
|
||||
|
||||
impl acp_thread::AgentSessionEditor for NativeAgentSessionEditor {
|
||||
fn truncate(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task<Result<()>> {
|
||||
Task::ready(
|
||||
self.0
|
||||
.update(cx, |thread, cx| thread.truncate(message_id, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl acp_thread::AgentHistory for NativeAgentConnection {
|
||||
fn list_threads(&self, cx: &mut App) -> Task<Result<Vec<AcpThreadMetadata>>> {
|
||||
let database = self.0.read(cx).thread_database.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
let threads = database.list_threads().await?;
|
||||
anyhow::Ok(
|
||||
threads
|
||||
.into_iter()
|
||||
.map(|thread| thread.to_acp(NATIVE_AGENT_SERVER_NAME))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn observe_history(&self, cx: &mut App) -> mpsc::UnboundedReceiver<AcpThreadMetadata> {
|
||||
let (tx, rx) = mpsc::unbounded();
|
||||
self.0.update(cx, |this, _| this.history_watchers.push(tx));
|
||||
rx
|
||||
}
|
||||
|
||||
fn load_thread(
|
||||
|
@ -980,83 +1058,6 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
|||
Ok(acp_thread)
|
||||
})
|
||||
}
|
||||
|
||||
fn model_selector(&self) -> Option<Rc<dyn AgentModelSelector>> {
|
||||
Some(Rc::new(self.clone()) as Rc<dyn AgentModelSelector>)
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
id: Option<acp_thread::UserMessageId>,
|
||||
params: acp::PromptRequest,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<acp::PromptResponse>> {
|
||||
let id = id.expect("UserMessageId is required");
|
||||
let session_id = params.session_id.clone();
|
||||
log::info!("Received prompt request for session: {}", session_id);
|
||||
log::debug!("Prompt blocks count: {}", params.prompt.len());
|
||||
|
||||
self.run_turn(session_id, cx, |thread, cx| {
|
||||
let content: Vec<UserMessageContent> = params
|
||||
.prompt
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>();
|
||||
log::info!("Converted prompt to message: {} chars", content.len());
|
||||
log::debug!("Message id: {:?}", id);
|
||||
log::debug!("Message content: {:?}", content);
|
||||
|
||||
thread.update(cx, |thread, cx| thread.send(id, content, cx))
|
||||
})
|
||||
}
|
||||
|
||||
fn resume(
|
||||
&self,
|
||||
session_id: &acp::SessionId,
|
||||
_cx: &mut App,
|
||||
) -> Option<Rc<dyn acp_thread::AgentSessionResume>> {
|
||||
Some(Rc::new(NativeAgentSessionResume {
|
||||
connection: self.clone(),
|
||||
session_id: session_id.clone(),
|
||||
}) as _)
|
||||
}
|
||||
|
||||
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
||||
log::info!("Cancelling on session: {}", session_id);
|
||||
self.0.update(cx, |agent, cx| {
|
||||
if let Some(agent) = agent.sessions.get(session_id) {
|
||||
agent.thread.update(cx, |thread, cx| thread.cancel(cx));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn session_editor(
|
||||
&self,
|
||||
session_id: &agent_client_protocol::SessionId,
|
||||
cx: &mut App,
|
||||
) -> Option<Rc<dyn acp_thread::AgentSessionEditor>> {
|
||||
self.0.update(cx, |agent, _cx| {
|
||||
agent
|
||||
.sessions
|
||||
.get(session_id)
|
||||
.map(|session| Rc::new(NativeAgentSessionEditor(session.thread.clone())) as _)
|
||||
})
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct NativeAgentSessionEditor(Entity<Thread>);
|
||||
|
||||
impl acp_thread::AgentSessionEditor for NativeAgentSessionEditor {
|
||||
fn truncate(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task<Result<()>> {
|
||||
Task::ready(
|
||||
self.0
|
||||
.update(cx, |thread, cx| thread.truncate(message_id, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct NativeAgentSessionResume {
|
||||
|
@ -1274,16 +1275,22 @@ mod tests {
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
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 connection = Rc::new(NativeAgentConnection(agent.clone()));
|
||||
let history = connection.clone().history().unwrap();
|
||||
let history_store = cx.new(|cx| HistoryStore::get_or_init(cx));
|
||||
|
||||
history_store
|
||||
.update(cx, |history_store, cx| {
|
||||
history_store.load_history(NATIVE_AGENT_SERVER_NAME.clone(), history.as_ref(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let acp_thread = cx
|
||||
.update(|cx| {
|
||||
Rc::new(connection.clone()).new_thread(project.clone(), Path::new(path!("")), cx)
|
||||
connection
|
||||
.clone()
|
||||
.new_thread(project.clone(), Path::new(path!("")), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::{AgentMessage, AgentMessageContent, UserMessage, UserMessageContent};
|
||||
use acp_thread::{AcpThreadMetadata, AgentServerName};
|
||||
use agent::thread_store;
|
||||
use agent_client_protocol as acp;
|
||||
use agent_settings::{AgentProfileId, CompletionMode};
|
||||
|
@ -30,6 +31,17 @@ pub struct DbThreadMetadata {
|
|||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl DbThreadMetadata {
|
||||
pub fn to_acp(self, agent: AgentServerName) -> AcpThreadMetadata {
|
||||
AcpThreadMetadata {
|
||||
agent,
|
||||
id: self.id,
|
||||
title: self.title,
|
||||
updated_at: self.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct DbThread {
|
||||
pub title: SharedString,
|
||||
|
@ -288,7 +300,7 @@ impl ThreadsDatabase {
|
|||
connection: &Arc<Mutex<Connection>>,
|
||||
id: acp::SessionId,
|
||||
thread: DbThread,
|
||||
) -> Result<()> {
|
||||
) -> Result<DbThreadMetadata> {
|
||||
let json_data = serde_json::to_string(&thread)?;
|
||||
let title = thread.title.to_string();
|
||||
let updated_at = thread.updated_at.to_rfc3339();
|
||||
|
@ -303,9 +315,13 @@ impl ThreadsDatabase {
|
|||
INSERT OR REPLACE INTO threads (id, summary, updated_at, data_type, data) VALUES (?, ?, ?, ?, ?)
|
||||
"})?;
|
||||
|
||||
insert((id.0, title, updated_at, data_type, data))?;
|
||||
insert((id.0.clone(), title, updated_at, data_type, data))?;
|
||||
|
||||
Ok(())
|
||||
Ok(DbThreadMetadata {
|
||||
id,
|
||||
title: thread.title,
|
||||
updated_at: thread.updated_at,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_threads(&self) -> Task<Result<Vec<DbThreadMetadata>>> {
|
||||
|
@ -360,7 +376,11 @@ impl ThreadsDatabase {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn save_thread(&self, id: acp::SessionId, thread: DbThread) -> Task<Result<()>> {
|
||||
pub fn save_thread(
|
||||
&self,
|
||||
id: acp::SessionId,
|
||||
thread: DbThread,
|
||||
) -> Task<Result<DbThreadMetadata>> {
|
||||
let connection = self.connection.clone();
|
||||
|
||||
self.executor
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
use acp_thread::{AcpThreadMetadata, AgentConnection, AgentServerName};
|
||||
use agent_client_protocol as acp;
|
||||
use agent_servers::AgentServer;
|
||||
use assistant_context::SavedContextMetadata;
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
use gpui::{SharedString, Task, prelude::*};
|
||||
use gpui::{Entity, Global, SharedString, Task, prelude::*};
|
||||
use project::Project;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ui::App;
|
||||
|
||||
use std::{path::Path, sync::Arc, time::Duration};
|
||||
use std::{path::Path, rc::Rc, sync::Arc, time::Duration};
|
||||
|
||||
use crate::NativeAgentServer;
|
||||
|
||||
const MAX_RECENTLY_OPENED_ENTRIES: usize = 6;
|
||||
const NAVIGATION_HISTORY_PATH: &str = "agent-navigation-history.json";
|
||||
|
@ -59,41 +64,88 @@ enum SerializedRecentOpen {
|
|||
Context(String),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AgentHistory {
|
||||
entries: watch::Receiver<Option<Vec<AcpThreadMetadata>>>,
|
||||
_task: Task<()>,
|
||||
entries: HashMap<acp::SessionId, AcpThreadMetadata>,
|
||||
loaded: bool,
|
||||
}
|
||||
|
||||
pub struct HistoryStore {
|
||||
agents: HashMap<AgentServerName, AgentHistory>, // todo!() text threads
|
||||
}
|
||||
// note, we have to share the history store between all windows
|
||||
// because we only get updates from one connection at a time.
|
||||
struct GlobalHistoryStore(Entity<HistoryStore>);
|
||||
impl Global for GlobalHistoryStore {}
|
||||
|
||||
impl HistoryStore {
|
||||
pub fn new(_cx: &mut Context<Self>) -> Self {
|
||||
pub fn get_or_init(project: &Entity<Project>, cx: &mut App) -> Entity<Self> {
|
||||
if cx.has_global::<GlobalHistoryStore>() {
|
||||
return cx.global::<GlobalHistoryStore>().0.clone();
|
||||
}
|
||||
let history_store = cx.new(|cx| HistoryStore::new(cx));
|
||||
cx.set_global(GlobalHistoryStore(history_store.clone()));
|
||||
let root_dir = project
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.map(|worktree| worktree.read(cx).abs_path())
|
||||
.unwrap_or_else(|| paths::home_dir().as_path().into());
|
||||
|
||||
let agent = NativeAgentServer::new(project.read(cx).fs().clone());
|
||||
let connect = agent.connect(&root_dir, project, cx);
|
||||
cx.spawn({
|
||||
let history_store = history_store.clone();
|
||||
async move |cx| {
|
||||
let connection = connect.await?.history().unwrap();
|
||||
history_store
|
||||
.update(cx, |history_store, cx| {
|
||||
history_store.load_history(agent.name(), connection.as_ref(), cx)
|
||||
})?
|
||||
.await
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
history_store
|
||||
}
|
||||
|
||||
fn new(_cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
agents: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_agent(
|
||||
pub fn update_history(&mut self, entry: AcpThreadMetadata, cx: &mut Context<Self>) {
|
||||
let agent = self
|
||||
.agents
|
||||
.entry(entry.agent.clone())
|
||||
.or_insert(Default::default());
|
||||
|
||||
agent.entries.insert(entry.id.clone(), entry);
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
pub fn load_history(
|
||||
&mut self,
|
||||
agent_name: AgentServerName,
|
||||
connection: &dyn AgentConnection,
|
||||
connection: &dyn acp_thread::AgentHistory,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(mut history) = connection.list_threads(cx) else {
|
||||
return;
|
||||
};
|
||||
let history = AgentHistory {
|
||||
entries: history.clone(),
|
||||
_task: cx.spawn(async move |this, cx| {
|
||||
dbg!("loaded", history.borrow().as_ref().map(|b| b.len()));
|
||||
while history.changed().await.is_ok() {
|
||||
this.update(cx, |_, cx| cx.notify()).ok();
|
||||
}
|
||||
}),
|
||||
};
|
||||
self.agents.insert(agent_name.clone(), history);
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
let threads = connection.list_threads(cx);
|
||||
cx.spawn(async move |this, cx| {
|
||||
let threads = threads.await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.agents.insert(
|
||||
agent_name,
|
||||
AgentHistory {
|
||||
loaded: true,
|
||||
entries: threads.into_iter().map(|t| (t.id.clone(), t)).collect(),
|
||||
},
|
||||
);
|
||||
cx.notify()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn entries(&mut self, _cx: &mut Context<Self>) -> Vec<HistoryEntry> {
|
||||
|
@ -107,7 +159,7 @@ impl HistoryStore {
|
|||
history_entries.extend(
|
||||
self.agents
|
||||
.values_mut()
|
||||
.flat_map(|history| history.entries.borrow().clone().unwrap_or_default()) // todo!("surface the loading state?")
|
||||
.flat_map(|history| history.entries.values().cloned()) // todo!("surface the loading state?")
|
||||
.map(HistoryEntry::AcpThread),
|
||||
);
|
||||
// todo!() include the text threads in here.
|
||||
|
|
|
@ -5,8 +5,8 @@ use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
|||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
App, Empty, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
|
||||
UniformListScrollHandle, Window, uniform_list,
|
||||
App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ScrollStrategy, Stateful,
|
||||
Task, UniformListScrollHandle, Window, uniform_list,
|
||||
};
|
||||
use project::Project;
|
||||
use std::{fmt::Display, ops::Range, sync::Arc};
|
||||
|
@ -18,7 +18,7 @@ use ui::{
|
|||
use util::ResultExt;
|
||||
|
||||
pub struct AcpThreadHistory {
|
||||
history_store: Entity<HistoryStore>,
|
||||
pub(crate) history_store: Entity<HistoryStore>,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
selected_index: usize,
|
||||
hovered_index: Option<usize>,
|
||||
|
@ -69,37 +69,12 @@ impl AcpThreadHistory {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let history_store = cx.new(|cx| agent2::HistoryStore::new(cx));
|
||||
|
||||
let agent = NativeAgentServer::new(project.read(cx).fs().clone());
|
||||
|
||||
let root_dir = project
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.map(|worktree| worktree.read(cx).abs_path())
|
||||
.unwrap_or_else(|| paths::home_dir().as_path().into());
|
||||
|
||||
// todo!() reuse this connection for sending messages
|
||||
let connect = agent.connect(&root_dir, project, cx);
|
||||
cx.spawn(async move |this, cx| {
|
||||
let connection = connect.await?;
|
||||
this.update(cx, |this, cx| {
|
||||
this.history_store.update(cx, |this, cx| {
|
||||
this.register_agent(agent.name(), connection.as_ref(), cx)
|
||||
})
|
||||
})?;
|
||||
// todo!() we must keep it alive
|
||||
std::mem::forget(connection);
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
let search_editor = cx.new(|cx| {
|
||||
let mut editor = Editor::single_line(window, cx);
|
||||
editor.set_placeholder_text("Search threads...", cx);
|
||||
editor
|
||||
});
|
||||
let history_store = HistoryStore::get_or_init(project, cx);
|
||||
|
||||
let search_editor_subscription =
|
||||
cx.subscribe(&search_editor, |this, search_editor, event, cx| {
|
||||
|
|
|
@ -18,6 +18,7 @@ use editor::scroll::Autoscroll;
|
|||
use editor::{Editor, EditorMode, MultiBuffer, PathKey, SelectionEffects};
|
||||
use file_icons::FileIcons;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt, App, BorderStyle, ClickEvent, ClipboardItem, EdgesRefinement,
|
||||
Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton,
|
||||
|
@ -123,6 +124,7 @@ pub struct AcpThreadView {
|
|||
editor_expanded: bool,
|
||||
terminal_expanded: bool,
|
||||
editing_message: Option<usize>,
|
||||
history_store: Entity<agent2::HistoryStore>,
|
||||
_cancel_task: Option<Task<()>>,
|
||||
_subscriptions: [Subscription; 3],
|
||||
}
|
||||
|
@ -134,6 +136,7 @@ enum ThreadState {
|
|||
Ready {
|
||||
thread: Entity<AcpThread>,
|
||||
_subscription: [Subscription; 2],
|
||||
_history_task: Option<Task<()>>,
|
||||
},
|
||||
LoadError(LoadError),
|
||||
Unauthenticated {
|
||||
|
@ -149,6 +152,7 @@ impl AcpThreadView {
|
|||
agent: Rc<dyn AgentServer>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
history_store: Entity<agent2::HistoryStore>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
text_thread_store: Entity<TextThreadStore>,
|
||||
restore_thread: Option<AcpThreadMetadata>,
|
||||
|
@ -196,11 +200,13 @@ impl AcpThreadView {
|
|||
thread_state: Self::initial_state(
|
||||
agent,
|
||||
restore_thread,
|
||||
history_store.clone(),
|
||||
workspace,
|
||||
project,
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
history_store,
|
||||
message_editor,
|
||||
model_selector: None,
|
||||
profile_selector: None,
|
||||
|
@ -225,6 +231,7 @@ impl AcpThreadView {
|
|||
fn initial_state(
|
||||
agent: Rc<dyn AgentServer>,
|
||||
restore_thread: Option<AcpThreadMetadata>,
|
||||
history_store: Entity<agent2::HistoryStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
|
@ -251,6 +258,25 @@ impl AcpThreadView {
|
|||
}
|
||||
};
|
||||
|
||||
let mut history_task = None;
|
||||
let history = connection.clone().history();
|
||||
if let Some(history) = history.clone() {
|
||||
if let Some(mut history) = cx.update(|_, cx| history.observe_history(cx)).ok() {
|
||||
history_task = Some(cx.spawn(async move |cx| {
|
||||
while let Some(update) = history.next().await {
|
||||
if !history_store
|
||||
.update(cx, |history_store, cx| {
|
||||
history_store.update_history(update, cx)
|
||||
})
|
||||
.is_ok()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// this.update_in(cx, |_this, _window, cx| {
|
||||
// let status = connection.exit_status(cx);
|
||||
// cx.spawn(async move |this, cx| {
|
||||
|
@ -264,15 +290,12 @@ impl AcpThreadView {
|
|||
// .detach();
|
||||
// })
|
||||
// .ok();
|
||||
//
|
||||
let history = connection.clone().history();
|
||||
let task = cx.update(|_, cx| {
|
||||
if let Some(restore_thread) = restore_thread {
|
||||
connection.clone().load_thread(
|
||||
project.clone(),
|
||||
&root_dir,
|
||||
restore_thread.id,
|
||||
cx,
|
||||
)
|
||||
if let Some(restore_thread) = restore_thread
|
||||
&& let Some(history) = history
|
||||
{
|
||||
history.load_thread(project.clone(), &root_dir, restore_thread.id, cx)
|
||||
} else {
|
||||
connection
|
||||
.clone()
|
||||
|
@ -342,6 +365,7 @@ impl AcpThreadView {
|
|||
this.thread_state = ThreadState::Ready {
|
||||
thread,
|
||||
_subscription: [thread_subscription, action_log_subscription],
|
||||
_history_task: history_task,
|
||||
};
|
||||
|
||||
this.profile_selector = this.as_native_thread(cx).map(|thread| {
|
||||
|
@ -751,6 +775,7 @@ impl AcpThreadView {
|
|||
this.thread_state = Self::initial_state(
|
||||
agent,
|
||||
None, // todo!()
|
||||
this.history_store.clone(),
|
||||
this.workspace.clone(),
|
||||
project.clone(),
|
||||
window,
|
||||
|
@ -3755,6 +3780,8 @@ pub(crate) mod tests {
|
|||
cx.update(|_window, cx| cx.new(|cx| ThreadStore::fake(project.clone(), cx)));
|
||||
let text_thread_store =
|
||||
cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx)));
|
||||
let history_store =
|
||||
cx.update(|_window, cx| cx.new(|cx| agent2::HistoryStore::get_or_init(cx)));
|
||||
|
||||
let thread_view = cx.update(|window, cx| {
|
||||
cx.new(|cx| {
|
||||
|
@ -3762,6 +3789,7 @@ pub(crate) mod tests {
|
|||
Rc::new(agent),
|
||||
workspace.downgrade(),
|
||||
project,
|
||||
history_store.clone(),
|
||||
thread_store.clone(),
|
||||
text_thread_store.clone(),
|
||||
None,
|
||||
|
@ -3954,6 +3982,8 @@ pub(crate) mod tests {
|
|||
cx.update(|_window, cx| cx.new(|cx| ThreadStore::fake(project.clone(), cx)));
|
||||
let text_thread_store =
|
||||
cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx)));
|
||||
let history_store =
|
||||
cx.update(|_window, cx| cx.new(|cx| agent2::HistoryStore::get_or_init(cx)));
|
||||
|
||||
let connection = Rc::new(StubAgentConnection::new());
|
||||
let thread_view = cx.update(|window, cx| {
|
||||
|
@ -3962,6 +3992,7 @@ pub(crate) mod tests {
|
|||
Rc::new(StubAgentServer::new(connection.as_ref().clone())),
|
||||
workspace.downgrade(),
|
||||
project.clone(),
|
||||
history_store,
|
||||
thread_store.clone(),
|
||||
text_thread_store.clone(),
|
||||
None,
|
||||
|
|
|
@ -1031,11 +1031,13 @@ impl AgentPanel {
|
|||
};
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
let acp_history_store = this.acp_history.read(cx).history_store.clone();
|
||||
let thread_view = cx.new(|cx| {
|
||||
crate::acp::AcpThreadView::new(
|
||||
server,
|
||||
workspace.clone(),
|
||||
project,
|
||||
acp_history_store,
|
||||
thread_store.clone(),
|
||||
text_thread_store.clone(),
|
||||
restore_thread,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue