use crate::{ context_server_tool::ContextServerTool, thread::{ DetailedSummaryState, ExceededWindowError, MessageId, ProjectSnapshot, Thread, ThreadId, },};use agent_settings::{AgentProfileId, CompletionMode};use anyhow::{Context as _, Result, anyhow};use assistant_tool::{ToolId, ToolWorkingSet};use chrono::{DateTime, Utc};use collections::HashMap;use context_server::ContextServerId;use futures::{ FutureExt as _, StreamExt as _, channel::{mpsc, oneshot}, future::{self, BoxFuture, Shared},};use gpui::{ App, BackgroundExecutor, Context, Entity, EventEmitter, Global, ReadGlobal, SharedString, Subscription, Task, Window, prelude::*,};use indoc::indoc;use language_model::{LanguageModelToolResultContent, LanguageModelToolUseId, Role, TokenUsage};use project::context_server_store::{ContextServerStatus, ContextServerStore};use project::{Project, ProjectItem, ProjectPath, Worktree};use prompt_store::{ ProjectContext, PromptBuilder, PromptId, PromptStore, PromptsUpdatedEvent, RulesFileContext, UserRulesContext, WorktreeContext,};use serde::{Deserialize, Serialize};use sqlez::{ bindable::{Bind, Column}, connection::Connection, statement::Statement,};use std::{ cell::{Ref, RefCell}, path::{Path, PathBuf}, rc::Rc, sync::{Arc, Mutex},};use util::ResultExt as _; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]pub enum DataType { #[serde(rename = "json")] Json, #[serde(rename = "zstd")] Zstd,} impl Bind for DataType { fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> { let value = match self { DataType::Json => "json", DataType::Zstd => "zstd", }; value.bind(statement, start_index) }} impl Column for DataType { fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { let (value, next_index) = String::column(statement, start_index)?; let data_type = match value.as_str() { "json" => DataType::Json, "zstd" => DataType::Zstd, _ => anyhow::bail!("Unknown data type: {}", value), }; Ok((data_type, next_index)) }} const RULES_FILE_NAMES: [&'static str; 9] = [ ".rules", ".cursorrules", ".windsurfrules", ".clinerules", ".github/copilot-instructions.md", "CLAUDE.md", "AGENT.md", "AGENTS.md", "GEMINI.md",]; pub fn init(cx: &mut App) { ThreadsDatabase::init(cx);} /// A system prompt shared by all threads created by this ThreadStore#[derive(Clone, Default)]pub struct SharedProjectContext(Rc<RefCell<Option<ProjectContext>>>); impl SharedProjectContext { pub fn borrow(&self) -> Ref<'_, Option<ProjectContext>> { self.0.borrow() }} pub type TextThreadStore = assistant_context::ContextStore; pub struct ThreadStore { project: Entity<Project>, tools: Entity<ToolWorkingSet>, prompt_builder: Arc<PromptBuilder>, prompt_store: Option<Entity<PromptStore>>, context_server_tool_ids: HashMap<ContextServerId, Vec<ToolId>>, threads: Vec<SerializedThreadMetadata>, project_context: SharedProjectContext, reload_system_prompt_tx: mpsc::Sender<()>, _reload_system_prompt_task: Task<()>, _subscriptions: Vec<Subscription>,} pub struct RulesLoadingError { pub message: SharedString,} impl EventEmitter<RulesLoadingError> for ThreadStore {} impl ThreadStore { pub fn load( project: Entity<Project>, tools: Entity<ToolWorkingSet>, prompt_store: Option<Entity<PromptStore>>, prompt_builder: Arc<PromptBuilder>, cx: &mut App, ) -> Task<Result<Entity<Self>>> { cx.spawn(async move |cx| { let (thread_store, ready_rx) = cx.update(|cx| { let mut option_ready_rx = None; let thread_store = cx.new(|cx| { let (thread_store, ready_rx) = Self::new(project, tools, prompt_builder, prompt_store, cx); option_ready_rx = Some(ready_rx); thread_store }); (thread_store, option_ready_rx.take().unwrap()) })?; ready_rx.await?; Ok(thread_store) }) } fn new( project: Entity<Project>, tools: Entity<ToolWorkingSet>, prompt_builder: Arc<PromptBuilder>, prompt_store: Option<Entity<PromptStore>>, cx: &mut Context<Self>, ) -> (Self, oneshot::Receiver<()>) { let mut subscriptions = vec![cx.subscribe(&project, Self::handle_project_event)]; if let Some(prompt_store) = prompt_store.as_ref() { subscriptions.push(cx.subscribe( prompt_store, |this, _prompt_store, PromptsUpdatedEvent, _cx| { this.enqueue_system_prompt_reload(); }, )) } // This channel and task prevent concurrent and redundant loading of the system prompt. let (reload_system_prompt_tx, mut reload_system_prompt_rx) = mpsc::channel(1); let (ready_tx, ready_rx) = oneshot::channel(); let mut ready_tx = Some(ready_tx); let reload_system_prompt_task = cx.spawn({ let prompt_store = prompt_store.clone(); async move |thread_store, cx| { loop { let Some(reload_task) = thread_store .update(cx, |thread_store, cx| { thread_store.reload_system_prompt(prompt_store.clone(), cx) }) .ok() else { return; }; reload_task.await; if let Some(ready_tx) = ready_tx.take() { ready_tx.send(()).ok(); } reload_system_prompt_rx.next().await; } } }); let this = Self { project, tools, prompt_builder, prompt_store, context_server_tool_ids: HashMap::default(), threads: Vec::new(), project_context: SharedProjectContext::default(), reload_system_prompt_tx, _reload_system_prompt_task: reload_system_prompt_task, _subscriptions: subscriptions, }; this.register_context_server_handlers(cx); this.reload(cx).detach_and_log_err(cx); (this, ready_rx) } fn handle_project_event( &mut self, _project: Entity<Project>, event: &project::Event, _cx: &mut Context<Self>, ) { match event { project::Event::WorktreeAdded(_) | project::Event::WorktreeRemoved(_) => { self.enqueue_system_prompt_reload(); } project::Event::WorktreeUpdatedEntries(_, items) => { if items.iter().any(|(path, _, _)| { RULES_FILE_NAMES .iter() .any(|name| path.as_ref() == Path::new(name)) }) { self.enqueue_system_prompt_reload(); } } _ => {} } } fn enqueue_system_prompt_reload(&mut self) { self.reload_system_prompt_tx.try_send(()).ok(); } // Note that this should only be called from `reload_system_prompt_task`. fn reload_system_prompt( &self, prompt_store: Option<Entity<PromptStore>>, cx: &mut Context<Self>, ) -> Task<()> { let worktrees = self .project .read(cx) .visible_worktrees(cx) .collect::<Vec<_>>(); let worktree_tasks = worktrees .into_iter() .map(|worktree| { Self::load_worktree_info_for_system_prompt(worktree, self.project.clone(), cx) }) .collect::<Vec<_>>(); let default_user_rules_task = match prompt_store { None => Task::ready(vec![]), Some(prompt_store) => prompt_store.read_with(cx, |prompt_store, cx| { let prompts = prompt_store.default_prompt_metadata(); let load_tasks = prompts.into_iter().map(|prompt_metadata| { let contents = prompt_store.load(prompt_metadata.id, cx); async move { (contents.await, prompt_metadata) } }); cx.background_spawn(future::join_all(load_tasks)) }), }; cx.spawn(async move |this, cx| { let (worktrees, default_user_rules) = future::join(future::join_all(worktree_tasks), default_user_rules_task).await; let worktrees = worktrees .into_iter() .map(|(worktree, rules_error)| { if let Some(rules_error) = rules_error { this.update(cx, |_, cx| cx.emit(rules_error)).ok(); } worktree }) .collect::<Vec<_>>(); let default_user_rules = default_user_rules .into_iter() .flat_map(|(contents, prompt_metadata)| match contents { Ok(contents) => Some(UserRulesContext { uuid: match prompt_metadata.id { PromptId::User { uuid } => uuid, PromptId::EditWorkflow => return None, }, title: prompt_metadata.title.map(|title| title.to_string()), contents, }), Err(err) => { this.update(cx, |_, cx| { cx.emit(RulesLoadingError { message: format!("{err:?}").into(), }); }) .ok(); None } }) .collect::<Vec<_>>(); this.update(cx, |this, _cx| { *this.project_context.0.borrow_mut() = Some(ProjectContext::new(worktrees, default_user_rules)); }) .ok(); }) } fn load_worktree_info_for_system_prompt( worktree: Entity<Worktree>, project: Entity<Project>, cx: &mut App, ) -> Task<(WorktreeContext, Option<RulesLoadingError>)> { let tree = worktree.read(cx); let root_name = tree.root_name().into(); let abs_path = tree.abs_path(); let mut context = WorktreeContext { root_name, abs_path, rules_file: None, }; let rules_task = Self::load_worktree_rules_file(worktree, project, cx); let Some(rules_task) = rules_task else { return Task::ready((context, None)); }; cx.spawn(async move |_| { let (rules_file, rules_file_error) = match rules_task.await { Ok(rules_file) => (Some(rules_file), None), Err(err) => ( None, Some(RulesLoadingError { message: format!("{err}").into(), }), ), }; context.rules_file = rules_file; (context, rules_file_error) }) } fn load_worktree_rules_file( worktree: Entity<Worktree>, project: Entity<Project>, cx: &mut App, ) -> Option<Task<Result<RulesFileContext>>> { let worktree = worktree.read(cx); let worktree_id = worktree.id(); let selected_rules_file = RULES_FILE_NAMES .into_iter() .filter_map(|name| { worktree .entry_for_path(name) .filter(|entry| entry.is_file()) .map(|entry| entry.path.clone()) }) .next(); // Note that Cline supports `.clinerules` being a directory, but that is not currently // supported. This doesn't seem to occur often in GitHub repositories. selected_rules_file.map(|path_in_worktree| { let project_path = ProjectPath { worktree_id, path: path_in_worktree.clone(), }; let buffer_task = project.update(cx, |project, cx| project.open_buffer(project_path, cx)); let rope_task = cx.spawn(async move |cx| { buffer_task.await?.read_with(cx, |buffer, cx| { let project_entry_id = buffer.entry_id(cx).context("buffer has no file")?; anyhow::Ok((project_entry_id, buffer.as_rope().clone())) })? }); // Build a string from the rope on a background thread. cx.background_spawn(async move { let (project_entry_id, rope) = rope_task.await?; anyhow::Ok(RulesFileContext { path_in_worktree, text: rope.to_string().trim().to_string(), project_entry_id: project_entry_id.to_usize(), }) }) }) } pub fn prompt_store(&self) -> &Option<Entity<PromptStore>> { &self.prompt_store } pub fn tools(&self) -> Entity<ToolWorkingSet> { self.tools.clone() } /// Returns the number of threads. pub fn thread_count(&self) -> usize { self.threads.len() } pub fn reverse_chronological_threads(&self) -> impl Iterator<Item = &SerializedThreadMetadata> { // ordering is from "ORDER BY" in `list_threads` self.threads.iter() } pub fn create_thread(&mut self, cx: &mut Context<Self>) -> Entity<Thread> { cx.new(|cx| { Thread::new( self.project.clone(), self.tools.clone(), self.prompt_builder.clone(), self.project_context.clone(), cx, ) }) } pub fn create_thread_from_serialized( &mut self, serialized: SerializedThread, cx: &mut Context<Self>, ) -> Entity<Thread> { cx.new(|cx| { Thread::deserialize( ThreadId::new(), serialized, self.project.clone(), self.tools.clone(), self.prompt_builder.clone(), self.project_context.clone(), None, cx, ) }) } pub fn open_thread( &self, id: &ThreadId, window: &mut Window, cx: &mut Context<Self>, ) -> Task<Result<Entity<Thread>>> { let id = id.clone(); let database_future = ThreadsDatabase::global_future(cx); let this = cx.weak_entity(); window.spawn(cx, async move |cx| { let database = database_future.await.map_err(|err| anyhow!(err))?; let thread = database .try_find_thread(id.clone()) .await? .with_context(|| format!("no thread found with ID: {id:?}"))?; let thread = this.update_in(cx, |this, window, cx| { cx.new(|cx| { Thread::deserialize( id.clone(), thread, this.project.clone(), this.tools.clone(), this.prompt_builder.clone(), this.project_context.clone(), Some(window), cx, ) }) })?; Ok(thread) }) } pub fn save_thread(&self, thread: &Entity<Thread>, cx: &mut Context<Self>) -> Task<Result<()>> { let (metadata, serialized_thread) = thread.update(cx, |thread, cx| (thread.id().clone(), thread.serialize(cx))); let database_future = ThreadsDatabase::global_future(cx); cx.spawn(async move |this, cx| { let serialized_thread = serialized_thread.await?; let database = database_future.await.map_err(|err| anyhow!(err))?; database.save_thread(metadata, serialized_thread).await?; this.update(cx, |this, cx| this.reload(cx))?.await }) } pub fn delete_thread(&mut self, id: &ThreadId, cx: &mut Context<Self>) -> Task<Result<()>> { let id = id.clone(); let database_future = ThreadsDatabase::global_future(cx); cx.spawn(async move |this, cx| { let database = database_future.await.map_err(|err| anyhow!(err))?; database.delete_thread(id.clone()).await?; this.update(cx, |this, cx| { this.threads.retain(|thread| thread.id != id); cx.notify(); }) }) } pub fn reload(&self, cx: &mut Context<Self>) -> Task<Result<()>> { let database_future = ThreadsDatabase::global_future(cx); cx.spawn(async move |this, cx| { let threads = database_futureLines 1–500 of 1243. Load more
use crate::{ context_server_tool::ContextServerTool, thread::{ DetailedSummaryState, ExceededWindowError, MessageId, ProjectSnapshot, Thread, ThreadId, }, }; use agent_settings::{AgentProfileId, CompletionMode}; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::{ToolId, ToolWorkingSet}; use chrono::{DateTime, Utc}; use collections::HashMap; use context_server::ContextServerId; use futures::{ FutureExt as _, StreamExt as _, channel::{mpsc, oneshot}, future::{self, BoxFuture, Shared}, }; use gpui::{ App, BackgroundExecutor, Context, Entity, EventEmitter, Global, ReadGlobal, SharedString, Subscription, Task, Window, prelude::*, }; use indoc::indoc; use language_model::{LanguageModelToolResultContent, LanguageModelToolUseId, Role, TokenUsage}; use project::context_server_store::{ContextServerStatus, ContextServerStore}; use project::{Project, ProjectItem, ProjectPath, Worktree}; use prompt_store::{ ProjectContext, PromptBuilder, PromptId, PromptStore, PromptsUpdatedEvent, RulesFileContext, UserRulesContext, WorktreeContext, }; use serde::{Deserialize, Serialize}; use sqlez::{ bindable::{Bind, Column}, connection::Connection, statement::Statement, }; use std::{ cell::{Ref, RefCell}, path::{Path, PathBuf}, rc::Rc, sync::{Arc, Mutex}, }; use util::ResultExt as _; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum DataType { #[serde(rename = "json")] Json, #[serde(rename = "zstd")] Zstd, } impl Bind for DataType { fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> { let value = match self { DataType::Json => "json", DataType::Zstd => "zstd", }; value.bind(statement, start_index) } } impl Column for DataType { fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { let (value, next_index) = String::column(statement, start_index)?; let data_type = match value.as_str() { "json" => DataType::Json, "zstd" => DataType::Zstd, _ => anyhow::bail!("Unknown data type: {}", value), }; Ok((data_type, next_index)) } } const RULES_FILE_NAMES: [&'static str; 9] = [ ".rules", ".cursorrules", ".windsurfrules", ".clinerules", ".github/copilot-instructions.md", "CLAUDE.md", "AGENT.md", "AGENTS.md", "GEMINI.md", ]; pub fn init(cx: &mut App) { ThreadsDatabase::init(cx); } /// A system prompt shared by all threads created by this ThreadStore #[derive(Clone, Default)] pub struct SharedProjectContext(Rc<RefCell<Option<ProjectContext>>>); impl SharedProjectContext { pub fn borrow(&self) -> Ref<'_, Option<ProjectContext>> { self.0.borrow() } } pub type TextThreadStore = assistant_context::ContextStore; pub struct ThreadStore { project: Entity<Project>, tools: Entity<ToolWorkingSet>, prompt_builder: Arc<PromptBuilder>, prompt_store: Option<Entity<PromptStore>>, context_server_tool_ids: HashMap<ContextServerId, Vec<ToolId>>, threads: Vec<SerializedThreadMetadata>, project_context: SharedProjectContext, reload_system_prompt_tx: mpsc::Sender<()>, _reload_system_prompt_task: Task<()>, _subscriptions: Vec<Subscription>, } pub struct RulesLoadingError { pub message: SharedString, } impl EventEmitter<RulesLoadingError> for ThreadStore {} impl ThreadStore { pub fn load( project: Entity<Project>, tools: Entity<ToolWorkingSet>, prompt_store: Option<Entity<PromptStore>>, prompt_builder: Arc<PromptBuilder>, cx: &mut App, ) -> Task<Result<Entity<Self>>> { cx.spawn(async move |cx| { let (thread_store, ready_rx) = cx.update(|cx| { let mut option_ready_rx = None; let thread_store = cx.new(|cx| { let (thread_store, ready_rx) = Self::new(project, tools, prompt_builder, prompt_store, cx); option_ready_rx = Some(ready_rx); thread_store }); (thread_store, option_ready_rx.take().unwrap()) })?; ready_rx.await?; Ok(thread_store) }) } fn new( project: Entity<Project>, tools: Entity<ToolWorkingSet>, prompt_builder: Arc<PromptBuilder>, prompt_store: Option<Entity<PromptStore>>, cx: &mut Context<Self>, ) -> (Self, oneshot::Receiver<()>) { let mut subscriptions = vec![cx.subscribe(&project, Self::handle_project_event)]; if let Some(prompt_store) = prompt_store.as_ref() { subscriptions.push(cx.subscribe( prompt_store, |this, _prompt_store, PromptsUpdatedEvent, _cx| { this.enqueue_system_prompt_reload(); }, )) } // This channel and task prevent concurrent and redundant loading of the system prompt. let (reload_system_prompt_tx, mut reload_system_prompt_rx) = mpsc::channel(1); let (ready_tx, ready_rx) = oneshot::channel(); let mut ready_tx = Some(ready_tx); let reload_system_prompt_task = cx.spawn({ let prompt_store = prompt_store.clone(); async move |thread_store, cx| { loop { let Some(reload_task) = thread_store .update(cx, |thread_store, cx| { thread_store.reload_system_prompt(prompt_store.clone(), cx) }) .ok() else { return; }; reload_task.await; if let Some(ready_tx) = ready_tx.take() { ready_tx.send(()).ok(); } reload_system_prompt_rx.next().await; } } }); let this = Self { project, tools, prompt_builder, prompt_store, context_server_tool_ids: HashMap::default(), threads: Vec::new(), project_context: SharedProjectContext::default(), reload_system_prompt_tx, _reload_system_prompt_task: reload_system_prompt_task, _subscriptions: subscriptions, }; this.register_context_server_handlers(cx); this.reload(cx).detach_and_log_err(cx); (this, ready_rx) } fn handle_project_event( &mut self, _project: Entity<Project>, event: &project::Event, _cx: &mut Context<Self>, ) { match event { project::Event::WorktreeAdded(_) | project::Event::WorktreeRemoved(_) => { self.enqueue_system_prompt_reload(); } project::Event::WorktreeUpdatedEntries(_, items) => { if items.iter().any(|(path, _, _)| { RULES_FILE_NAMES .iter() .any(|name| path.as_ref() == Path::new(name)) }) { self.enqueue_system_prompt_reload(); } } _ => {} } } fn enqueue_system_prompt_reload(&mut self) { self.reload_system_prompt_tx.try_send(()).ok(); } // Note that this should only be called from `reload_system_prompt_task`. fn reload_system_prompt( &self, prompt_store: Option<Entity<PromptStore>>, cx: &mut Context<Self>, ) -> Task<()> { let worktrees = self .project .read(cx) .visible_worktrees(cx) .collect::<Vec<_>>(); let worktree_tasks = worktrees .into_iter() .map(|worktree| { Self::load_worktree_info_for_system_prompt(worktree, self.project.clone(), cx) }) .collect::<Vec<_>>(); let default_user_rules_task = match prompt_store { None => Task::ready(vec![]), Some(prompt_store) => prompt_store.read_with(cx, |prompt_store, cx| { let prompts = prompt_store.default_prompt_metadata(); let load_tasks = prompts.into_iter().map(|prompt_metadata| { let contents = prompt_store.load(prompt_metadata.id, cx); async move { (contents.await, prompt_metadata) } }); cx.background_spawn(future::join_all(load_tasks)) }), }; cx.spawn(async move |this, cx| { let (worktrees, default_user_rules) = future::join(future::join_all(worktree_tasks), default_user_rules_task).await; let worktrees = worktrees .into_iter() .map(|(worktree, rules_error)| { if let Some(rules_error) = rules_error { this.update(cx, |_, cx| cx.emit(rules_error)).ok(); } worktree }) .collect::<Vec<_>>(); let default_user_rules = default_user_rules .into_iter() .flat_map(|(contents, prompt_metadata)| match contents { Ok(contents) => Some(UserRulesContext { uuid: match prompt_metadata.id { PromptId::User { uuid } => uuid, PromptId::EditWorkflow => return None, }, title: prompt_metadata.title.map(|title| title.to_string()), contents, }), Err(err) => { this.update(cx, |_, cx| { cx.emit(RulesLoadingError { message: format!("{err:?}").into(), }); }) .ok(); None } }) .collect::<Vec<_>>(); this.update(cx, |this, _cx| { *this.project_context.0.borrow_mut() = Some(ProjectContext::new(worktrees, default_user_rules)); }) .ok(); }) } fn load_worktree_info_for_system_prompt( worktree: Entity<Worktree>, project: Entity<Project>, cx: &mut App, ) -> Task<(WorktreeContext, Option<RulesLoadingError>)> { let tree = worktree.read(cx); let root_name = tree.root_name().into(); let abs_path = tree.abs_path(); let mut context = WorktreeContext { root_name, abs_path, rules_file: None, }; let rules_task = Self::load_worktree_rules_file(worktree, project, cx); let Some(rules_task) = rules_task else { return Task::ready((context, None)); }; cx.spawn(async move |_| { let (rules_file, rules_file_error) = match rules_task.await { Ok(rules_file) => (Some(rules_file), None), Err(err) => ( None, Some(RulesLoadingError { message: format!("{err}").into(), }), ), }; context.rules_file = rules_file; (context, rules_file_error) }) } fn load_worktree_rules_file( worktree: Entity<Worktree>, project: Entity<Project>, cx: &mut App, ) -> Option<Task<Result<RulesFileContext>>> { let worktree = worktree.read(cx); let worktree_id = worktree.id(); let selected_rules_file = RULES_FILE_NAMES .into_iter() .filter_map(|name| { worktree .entry_for_path(name) .filter(|entry| entry.is_file()) .map(|entry| entry.path.clone()) }) .next(); // Note that Cline supports `.clinerules` being a directory, but that is not currently // supported. This doesn't seem to occur often in GitHub repositories. selected_rules_file.map(|path_in_worktree| { let project_path = ProjectPath { worktree_id, path: path_in_worktree.clone(), }; let buffer_task = project.update(cx, |project, cx| project.open_buffer(project_path, cx)); let rope_task = cx.spawn(async move |cx| { buffer_task.await?.read_with(cx, |buffer, cx| { let project_entry_id = buffer.entry_id(cx).context("buffer has no file")?; anyhow::Ok((project_entry_id, buffer.as_rope().clone())) })? }); // Build a string from the rope on a background thread. cx.background_spawn(async move { let (project_entry_id, rope) = rope_task.await?; anyhow::Ok(RulesFileContext { path_in_worktree, text: rope.to_string().trim().to_string(), project_entry_id: project_entry_id.to_usize(), }) }) }) } pub fn prompt_store(&self) -> &Option<Entity<PromptStore>> { &self.prompt_store } pub fn tools(&self) -> Entity<ToolWorkingSet> { self.tools.clone() } /// Returns the number of threads. pub fn thread_count(&self) -> usize { self.threads.len() } pub fn reverse_chronological_threads(&self) -> impl Iterator<Item = &SerializedThreadMetadata> { // ordering is from "ORDER BY" in `list_threads` self.threads.iter() } pub fn create_thread(&mut self, cx: &mut Context<Self>) -> Entity<Thread> { cx.new(|cx| { Thread::new( self.project.clone(), self.tools.clone(), self.prompt_builder.clone(), self.project_context.clone(), cx, ) }) } pub fn create_thread_from_serialized( &mut self, serialized: SerializedThread, cx: &mut Context<Self>, ) -> Entity<Thread> { cx.new(|cx| { Thread::deserialize( ThreadId::new(), serialized, self.project.clone(), self.tools.clone(), self.prompt_builder.clone(), self.project_context.clone(), None, cx, ) }) } pub fn open_thread( &self, id: &ThreadId, window: &mut Window, cx: &mut Context<Self>, ) -> Task<Result<Entity<Thread>>> { let id = id.clone(); let database_future = ThreadsDatabase::global_future(cx); let this = cx.weak_entity(); window.spawn(cx, async move |cx| { let database = database_future.await.map_err(|err| anyhow!(err))?; let thread = database .try_find_thread(id.clone()) .await? .with_context(|| format!("no thread found with ID: {id:?}"))?; let thread = this.update_in(cx, |this, window, cx| { cx.new(|cx| { Thread::deserialize( id.clone(), thread, this.project.clone(), this.tools.clone(), this.prompt_builder.clone(), this.project_context.clone(), Some(window), cx, ) }) })?; Ok(thread) }) } pub fn save_thread(&self, thread: &Entity<Thread>, cx: &mut Context<Self>) -> Task<Result<()>> { let (metadata, serialized_thread) = thread.update(cx, |thread, cx| (thread.id().clone(), thread.serialize(cx))); let database_future = ThreadsDatabase::global_future(cx); cx.spawn(async move |this, cx| { let serialized_thread = serialized_thread.await?; let database = database_future.await.map_err(|err| anyhow!(err))?; database.save_thread(metadata, serialized_thread).await?; this.update(cx, |this, cx| this.reload(cx))?.await }) } pub fn delete_thread(&mut self, id: &ThreadId, cx: &mut Context<Self>) -> Task<Result<()>> { let id = id.clone(); let database_future = ThreadsDatabase::global_future(cx); cx.spawn(async move |this, cx| { let database = database_future.await.map_err(|err| anyhow!(err))?; database.delete_thread(id.clone()).await?; this.update(cx, |this, cx| { this.threads.retain(|thread| thread.id != id); cx.notify(); }) }) } pub fn reload(&self, cx: &mut Context<Self>) -> Task<Result<()>> { let database_future = ThreadsDatabase::global_future(cx); cx.spawn(async move |this, cx| { let threads = database_future