5250 lines · 179.6 KB · Rust
RawDownload
mod connection;mod diff;mod mention;mod terminal;use action_log::{ActionLog, ActionLogTelemetry};use agent_client_protocol::{self as acp};use anyhow::{Context as _, Result, anyhow};use collections::HashSet;pub use connection::*;pub use diff::*;use futures::{FutureExt, channel::oneshot, future::BoxFuture};use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};use itertools::Itertools;use language::language_settings::FormatOnSave;use language::{Anchor, Buffer, BufferSnapshot, LanguageRegistry, Point, ToPoint, text_diff};use markdown::Markdown;pub use mention::*;use project::lsp_store::{FormatTrigger, LspFormatTarget};use project::{AgentLocation, Project, git_store::GitStoreCheckpoint};use serde::{Deserialize, Serialize};use serde_json::to_string_pretty;use std::collections::HashMap;use std::error::Error;use std::fmt::{Formatter, Write};use std::ops::Range;use std::process::ExitStatus;use std::rc::Rc;use std::time::{Duration, Instant};use std::{fmt::Display, mem, path::PathBuf, sync::Arc};use task::{Shell, ShellBuilder};pub use terminal::*;use text::Bias;use ui::App;use util::markdown::MarkdownEscaped;use util::path_list::PathList;use util::{ResultExt, get_default_system_shell_preferring_bash, paths::PathStyle};use uuid::Uuid; /// Key used in ACP ToolCall meta to store the tool's programmatic name./// This is a workaround since ACP's ToolCall doesn't have a dedicated name field.pub const TOOL_NAME_META_KEY: &str = "tool_name"; /// Helper to extract tool name from ACP metapub fn tool_name_from_meta(meta: &Option<acp::Meta>) -> Option<SharedString> {    meta.as_ref()        .and_then(|m| m.get(TOOL_NAME_META_KEY))        .and_then(|v| v.as_str())        .map(|s| SharedString::from(s.to_owned()))} /// Helper to create meta with tool namepub fn meta_with_tool_name(tool_name: &str) -> acp::Meta {    acp::Meta::from_iter([(TOOL_NAME_META_KEY.into(), tool_name.into())])} /// Key used in ACP ToolCall meta to store the session id and message indexespub const SUBAGENT_SESSION_INFO_META_KEY: &str = "subagent_session_info"; #[derive(Clone, Debug, Deserialize, Serialize)]pub struct SubagentSessionInfo {    /// The session id of the subagent sessiont that was spawned    pub session_id: acp::SessionId,    /// The index of the message of the start of the "turn" run by this tool call    pub message_start_index: usize,    /// The index of the output of the message that the subagent has returned    #[serde(skip_serializing_if = "Option::is_none")]    pub message_end_index: Option<usize>,} /// Helper to extract subagent session id from ACP metapub fn subagent_session_info_from_meta(meta: &Option<acp::Meta>) -> Option<SubagentSessionInfo> {    meta.as_ref()        .and_then(|m| m.get(SUBAGENT_SESSION_INFO_META_KEY))        .and_then(|v| serde_json::from_value(v.clone()).ok())} #[derive(Debug)]pub struct UserMessage {    pub id: Option<UserMessageId>,    pub content: ContentBlock,    pub chunks: Vec<acp::ContentBlock>,    pub checkpoint: Option<Checkpoint>,    pub indented: bool,} #[derive(Debug)]pub struct Checkpoint {    git_checkpoint: GitStoreCheckpoint,    pub show: bool,} impl UserMessage {    fn to_markdown(&self, cx: &App) -> String {        let mut markdown = String::new();        if self            .checkpoint            .as_ref()            .is_some_and(|checkpoint| checkpoint.show)        {            writeln!(markdown, "## User (checkpoint)").unwrap();        } else {            writeln!(markdown, "## User").unwrap();        }        writeln!(markdown).unwrap();        writeln!(markdown, "{}", self.content.to_markdown(cx)).unwrap();        writeln!(markdown).unwrap();        markdown    }} #[derive(Debug, PartialEq)]pub struct AssistantMessage {    pub chunks: Vec<AssistantMessageChunk>,    pub indented: bool,    pub is_subagent_output: bool,} impl AssistantMessage {    pub fn to_markdown(&self, cx: &App) -> String {        format!(            "## Assistant\n\n{}\n\n",            self.chunks                .iter()                .map(|chunk| chunk.to_markdown(cx))                .join("\n\n")        )    }} #[derive(Debug, PartialEq)]pub enum AssistantMessageChunk {    Message { block: ContentBlock },    Thought { block: ContentBlock },} impl AssistantMessageChunk {    pub fn from_str(        chunk: &str,        language_registry: &Arc<LanguageRegistry>,        path_style: PathStyle,        cx: &mut App,    ) -> Self {        Self::Message {            block: ContentBlock::new(chunk.into(), language_registry, path_style, cx),        }    }     fn to_markdown(&self, cx: &App) -> String {        match self {            Self::Message { block } => block.to_markdown(cx).to_string(),            Self::Thought { block } => {                format!("<thinking>\n{}\n</thinking>", block.to_markdown(cx))            }        }    }} #[derive(Debug)]pub enum AgentThreadEntry {    UserMessage(UserMessage),    AssistantMessage(AssistantMessage),    ToolCall(ToolCall),    CompletedPlan(Vec<PlanEntry>),} impl AgentThreadEntry {    pub fn is_indented(&self) -> bool {        match self {            Self::UserMessage(message) => message.indented,            Self::AssistantMessage(message) => message.indented,            Self::ToolCall(_) => false,            Self::CompletedPlan(_) => false,        }    }     pub fn to_markdown(&self, cx: &App) -> String {        match self {            Self::UserMessage(message) => message.to_markdown(cx),            Self::AssistantMessage(message) => message.to_markdown(cx),            Self::ToolCall(tool_call) => tool_call.to_markdown(cx),            Self::CompletedPlan(entries) => {                let mut md = String::from("## Plan\n\n");                for entry in entries {                    let source = entry.content.read(cx).source().to_string();                    md.push_str(&format!("- [x] {}\n", source));                }                md            }        }    }     pub fn user_message(&self) -> Option<&UserMessage> {        if let AgentThreadEntry::UserMessage(message) = self {            Some(message)        } else {            None        }    }     pub fn diffs(&self) -> impl Iterator<Item = &Entity<Diff>> {        if let AgentThreadEntry::ToolCall(call) = self {            itertools::Either::Left(call.diffs())        } else {            itertools::Either::Right(std::iter::empty())        }    }     pub fn terminals(&self) -> impl Iterator<Item = &Entity<Terminal>> {        if let AgentThreadEntry::ToolCall(call) = self {            itertools::Either::Left(call.terminals())        } else {            itertools::Either::Right(std::iter::empty())        }    }     pub fn location(&self, ix: usize) -> Option<(acp::ToolCallLocation, AgentLocation)> {        if let AgentThreadEntry::ToolCall(ToolCall {            locations,            resolved_locations,            ..        }) = self        {            Some((                locations.get(ix)?.clone(),                resolved_locations.get(ix)?.clone()?,            ))        } else {            None        }    }} #[derive(Debug)]pub struct ToolCall {    pub id: acp::ToolCallId,    pub label: Entity<Markdown>,    pub kind: acp::ToolKind,    pub content: Vec<ToolCallContent>,    pub status: ToolCallStatus,    pub locations: Vec<acp::ToolCallLocation>,    pub resolved_locations: Vec<Option<AgentLocation>>,    pub raw_input: Option<serde_json::Value>,    pub raw_input_markdown: Option<Entity<Markdown>>,    pub raw_output: Option<serde_json::Value>,    pub tool_name: Option<SharedString>,    pub subagent_session_info: Option<SubagentSessionInfo>,} impl ToolCall {    fn from_acp(        tool_call: acp::ToolCall,        status: ToolCallStatus,        language_registry: Arc<LanguageRegistry>,        path_style: PathStyle,        terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,        cx: &mut App,    ) -> Result<Self> {        let title = if tool_call.kind == acp::ToolKind::Execute {            tool_call.title        } else if tool_call.kind == acp::ToolKind::Edit {            MarkdownEscaped(tool_call.title.as_str()).to_string()        } else if let Some((first_line, _)) = tool_call.title.split_once("\n") {            first_line.to_owned() + "…"        } else {            tool_call.title        };        let mut content = Vec::with_capacity(tool_call.content.len());        for item in tool_call.content {            if let Some(item) = ToolCallContent::from_acp(                item,                language_registry.clone(),                path_style,                terminals,                cx,            )? {                content.push(item);            }        }         let raw_input_markdown = tool_call            .raw_input            .as_ref()            .and_then(|input| markdown_for_raw_output(input, &language_registry, cx));         let tool_name = tool_name_from_meta(&tool_call.meta);         let subagent_session_info = subagent_session_info_from_meta(&tool_call.meta);         let result = Self {            id: tool_call.tool_call_id,            label: cx                .new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)),            kind: tool_call.kind,            content,            locations: tool_call.locations,            resolved_locations: Vec::default(),            status,            raw_input: tool_call.raw_input,            raw_input_markdown,            raw_output: tool_call.raw_output,            tool_name,            subagent_session_info,        };        Ok(result)    }     fn update_fields(        &mut self,        fields: acp::ToolCallUpdateFields,        meta: Option<acp::Meta>,        language_registry: Arc<LanguageRegistry>,        path_style: PathStyle,        terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,        cx: &mut App,    ) -> Result<()> {        let acp::ToolCallUpdateFields {            kind,            status,            title,            content,            locations,            raw_input,            raw_output,            ..        } = fields;         if let Some(kind) = kind {            self.kind = kind;        }         if let Some(status) = status {            self.status = status.into();        }         if let Some(subagent_session_info) = subagent_session_info_from_meta(&meta) {            self.subagent_session_info = Some(subagent_session_info);        }         if let Some(title) = title {            if self.kind == acp::ToolKind::Execute {                for terminal in self.terminals() {                    terminal.update(cx, |terminal, cx| {                        terminal.update_command_label(&title, cx);                    });                }            }            self.label.update(cx, |label, cx| {                if self.kind == acp::ToolKind::Execute {                    label.replace(title, cx);                } else if self.kind == acp::ToolKind::Edit {                    label.replace(MarkdownEscaped(&title).to_string(), cx)                } else if let Some((first_line, _)) = title.split_once("\n") {                    label.replace(first_line.to_owned() + "…", cx);                } else {                    label.replace(title, cx);                }            });        }         if let Some(content) = content {            let mut new_content_len = content.len();            let mut content = content.into_iter();             // Reuse existing content if we can            for (old, new) in self.content.iter_mut().zip(content.by_ref()) {                let valid_content =                    old.update_from_acp(new, language_registry.clone(), path_style, terminals, cx)?;                if !valid_content {                    new_content_len -= 1;                }            }            for new in content {                if let Some(new) = ToolCallContent::from_acp(                    new,                    language_registry.clone(),                    path_style,                    terminals,                    cx,                )? {                    self.content.push(new);                } else {                    new_content_len -= 1;                }            }            self.content.truncate(new_content_len);        }         if let Some(locations) = locations {            self.locations = locations;        }         if let Some(raw_input) = raw_input {            self.raw_input_markdown = markdown_for_raw_output(&raw_input, &language_registry, cx);            self.raw_input = Some(raw_input);        }         if let Some(raw_output) = raw_output {            if self.content.is_empty()                && let Some(markdown) = markdown_for_raw_output(&raw_output, &language_registry, cx)            {                self.content                    .push(ToolCallContent::ContentBlock(ContentBlock::Markdown {                        markdown,                    }));            }            self.raw_output = Some(raw_output);        }        Ok(())    }     pub fn diffs(&self) -> impl Iterator<Item = &Entity<Diff>> {        self.content.iter().filter_map(|content| match content {            ToolCallContent::Diff(diff) => Some(diff),            ToolCallContent::ContentBlock(_) => None,            ToolCallContent::Terminal(_) => None,        })    }     pub fn terminals(&self) -> impl Iterator<Item = &Entity<Terminal>> {        self.content.iter().filter_map(|content| match content {            ToolCallContent::Terminal(terminal) => Some(terminal),            ToolCallContent::ContentBlock(_) => None,            ToolCallContent::Diff(_) => None,        })    }     pub fn is_subagent(&self) -> bool {        self.tool_name.as_ref().is_some_and(|s| s == "spawn_agent")            || self.subagent_session_info.is_some()    }     pub fn to_markdown(&self, cx: &App) -> String {        let mut markdown = format!(            "**Tool Call: {}**\nStatus: {}\n\n",            self.label.read(cx).source(),            self.status        );        for content in &self.content {            markdown.push_str(content.to_markdown(cx).as_str());            markdown.push_str("\n\n");        }        markdown    }     async fn resolve_location(        location: acp::ToolCallLocation,        project: WeakEntity<Project>,        cx: &mut AsyncApp,    ) -> Option<ResolvedLocation> {        let buffer = project            .update(cx, |project, cx| {                project                    .project_path_for_absolute_path(&location.path, cx)                    .map(|path| project.open_buffer(path, cx))            })            .ok()??;        let buffer = buffer.await.log_err()?;        let position = buffer.update(cx, |buffer, _| {            let snapshot = buffer.snapshot();            if let Some(row) = location.line {                let column = snapshot.indent_size_for_line(row).len;                let point = snapshot.clip_point(Point::new(row, column), Bias::Left);                snapshot.anchor_before(point)            } else {                Anchor::min_for_buffer(snapshot.remote_id())            }        });         Some(ResolvedLocation { buffer, position })    }     fn resolve_locations(        &self,        project: Entity<Project>,        cx: &mut App,    ) -> Task<Vec<Option<ResolvedLocation>>> {        let locations = self.locations.clone();        project.update(cx, |_, cx| {            cx.spawn(async move |project, cx| {                let mut new_locations = Vec::new();                for location in locations {                    new_locations.push(Self::resolve_location(location, project.clone(), cx).await);                }                new_locations            })        })    }} // Separate so we can hold a strong reference to the buffer// for saving on the thread#[derive(Clone, Debug, PartialEq, Eq)]struct ResolvedLocation {    buffer: Entity<Buffer>,    position: Anchor,} impl From<&ResolvedLocation> for AgentLocation {    fn from(value: &ResolvedLocation) -> Self {        Self {

Lines 1–500 of 5250.