1248 lines · 42.4 KB · Rust
RawDownload
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::{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 _; pub static ZED_STATELESS: std::sync::LazyLock<bool> =    std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty())); #[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<()>> {

Lines 1–500 of 1248.