10055 lines · 356.9 KB · Rust
RawDownload
mod connection;mod diff;mod mention;mod terminal;pub use ::terminal::HeadlessTerminal;use action_log::{ActionLog, ActionLogTelemetry};use agent_client_protocol::schema::{MaybeUndefined, v1 as acp};use anyhow::{Context as _, Result, anyhow};use collections::HashSet;pub use connection::*;pub use diff::*;use feature_flags::{AcpBetaFeatureFlag, FeatureFlagAppExt as _};use futures::{FutureExt, channel::oneshot, future::BoxFuture};use gpui::{    AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,    WeakEntity,};use itertools::Itertools;use language::language_settings::FormatOnSave;use language::{    Anchor, Buffer, BufferEditSource, BufferSnapshot, LanguageRegistry, Point, ToPoint, text_diff,};use markdown::{Markdown, MarkdownOptions};pub use mention::*;use project::lsp_store::{FormatTrigger, LspFormatTarget};use project::{    AgentLocation, Project,    git_store::{GitStoreCheckpoint, GitStoreEvent, RepositoryEvent},};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, is_absolute},};use uuid::Uuid; /// Returned when the model stops because it exhausted its output token budget.#[derive(Debug)]pub struct MaxOutputTokensError; impl std::fmt::Display for MaxOutputTokensError {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        write!(f, "output token limit reached")    }} impl std::error::Error for MaxOutputTokensError {} /// 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 `AvailableCommand` meta to record which source produced a/// slash command, so the completion popup can group commands by category.pub const COMMAND_CATEGORY_META_KEY: &str = "command_category"; /// The source category of a slash command, used to group commands in the/// completion popup. Only the native Zed agent annotates its commands; commands/// from external ACP agents carry no category and are grouped on their own.#[derive(Clone, Copy, Debug, PartialEq, Eq)]pub enum CommandCategory {    /// Built-in Zed agent commands (e.g. `/compact`).    Native,    /// Commands sourced from MCP server prompts.    Mcp,} impl CommandCategory {    fn as_str(self) -> &'static str {        match self {            Self::Native => "native",            Self::Mcp => "mcp",        }    }     fn from_str(value: &str) -> Option<Self> {        match value {            "native" => Some(Self::Native),            "mcp" => Some(Self::Mcp),            _ => None,        }    }} pub fn meta_with_command_category(category: CommandCategory) -> acp::Meta {    acp::Meta::from_iter([(COMMAND_CATEGORY_META_KEY.into(), category.as_str().into())])} pub fn command_category_from_meta(meta: &Option<acp::Meta>) -> Option<CommandCategory> {    meta.as_ref()        .and_then(|m| m.get(COMMAND_CATEGORY_META_KEY))        .and_then(|v| v.as_str())        .and_then(CommandCategory::from_str)} /// Key used in ACP ToolCall meta to store the session id and message indexespub const SUBAGENT_SESSION_INFO_META_KEY: &str = "subagent_session_info"; pub const SANDBOX_AUTHORIZATION_META_KEY: &str = "sandbox_authorization"; /// Stable `PermissionOption` ids for the sandbox-escalation approval prompt.////// These are shared across the option construction (in the agent), the outcome/// dispatch, and the UI so the distinct grant lifetimes stay in sync. Note/// that `AllowThread` and `AllowAlways` both use/// `PermissionOptionKind::AllowAlways`; the id is what distinguishes them.#[derive(Clone, Copy, Debug, PartialEq, Eq)]pub enum SandboxPermission {    AllowOnce,    AllowThread,    AllowAlways,    Deny,} impl SandboxPermission {    pub fn as_id(self) -> &'static str {        match self {            Self::AllowOnce => "allow",            Self::AllowThread => "allow_thread",            Self::AllowAlways => "allow_always",            Self::Deny => "deny",        }    }     pub fn from_id(id: &str) -> Option<Self> {        match id {            "allow" => Some(Self::AllowOnce),            "allow_thread" => Some(Self::AllowThread),            "allow_always" => Some(Self::AllowAlways),            "deny" => Some(Self::Deny),            _ => None,        }    }} #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]pub struct SandboxAuthorizationDetails {    #[serde(default)]    pub command: Option<String>,    /// Specific hosts the command requested network access to, in canonical    /// form (`github.com`, `*.npmjs.org`). Empty when no specific hosts were    /// requested (see `network_all_hosts`).    #[serde(default)]    pub network_hosts: Vec<String>,    /// Whether the command requested access to any host ("arbitrary network    /// access"). The `network` alias deserializes the field this replaced —    /// a plain bool meaning "network access" — so details persisted by older    /// builds still render the network request.    #[serde(default, alias = "network")]    pub network_all_hosts: bool,     #[serde(default)]    pub allow_fs_write_all: bool,    #[serde(default)]    pub unsandboxed: bool,    #[serde(default)]    pub write_paths: Vec<PathBuf>,    /// The agent-provided justification for requesting these permissions,    /// shown to the user (attributed to the agent) in the approval prompt.    #[serde(default)]    pub reason: String,} pub fn meta_with_sandbox_authorization(details: SandboxAuthorizationDetails) -> acp::Meta {    acp::Meta::from_iter([(        SANDBOX_AUTHORIZATION_META_KEY.into(),        serde_json::to_value(details).unwrap_or_default(),    )])} pub fn sandbox_authorization_details_from_meta(    meta: &Option<acp::Meta>,) -> Option<SandboxAuthorizationDetails> {    meta.as_ref()        .and_then(|m| m.get(SANDBOX_AUTHORIZATION_META_KEY))        .and_then(|v| serde_json::from_value(v.clone()).ok())} pub const SANDBOX_FALLBACK_AUTHORIZATION_META_KEY: &str = "sandbox_fallback_authorization"; /// Stable `PermissionOption` id for the "Retry" choice in the sandbox/// *fallback* prompt (shown when the OS sandbox can't be created on this/// system). The remaining choices reuse the [`SandboxPermission`] ids.pub const SANDBOX_FALLBACK_RETRY_OPTION_ID: &str = "retry"; /// Details shown when the OS sandbox could not be created for a command and/// the user is asked whether to run it without a sandbox. Distinct from/// [`SandboxAuthorizationDetails`] (a model-requested *escalation*): here the/// sandbox itself failed, so the prompt explains why and offers a retry.#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]pub struct SandboxFallbackAuthorizationDetails {    #[serde(default)]    pub command: Option<String>,    /// Human-readable reason the OS sandbox could not be created (for example,    /// "bwrap not found on PATH"), shown to the user so they can decide    /// whether to run the command without a sandbox.    #[serde(default)]    pub reason: String,    /// Slug of the sandboxing docs section that best explains how to fix this    /// failure (see [`crate::LinuxWslSandboxError::docs_section`]), rendered as a    /// "Learn more" link. `None` when the cause is unknown.    #[serde(default)]    pub docs_section: Option<String>,} pub fn meta_with_sandbox_fallback_authorization(    details: SandboxFallbackAuthorizationDetails,) -> acp::Meta {    acp::Meta::from_iter([(        SANDBOX_FALLBACK_AUTHORIZATION_META_KEY.into(),        serde_json::to_value(details).unwrap_or_default(),    )])} pub fn sandbox_fallback_authorization_details_from_meta(    meta: &Option<acp::Meta>,) -> Option<SandboxFallbackAuthorizationDetails> {    meta.as_ref()        .and_then(|m| m.get(SANDBOX_FALLBACK_AUTHORIZATION_META_KEY))        .and_then(|v| serde_json::from_value(v.clone()).ok())} /// Meta key recording why the OS sandbox was not applied to a terminal tool/// call, even though sandboxing was active for the thread. The value is a/// serialized [`SandboxNotAppliedReason`]. Surfaced as a warning in the UI and/// used to explain the situation to both the user and the agent.pub const SANDBOX_NOT_APPLIED_META_KEY: &str = "sandbox_not_applied"; pub fn meta_with_sandbox_not_applied(reason: &SandboxNotAppliedReason) -> acp::Meta {    acp::Meta::from_iter([(        SANDBOX_NOT_APPLIED_META_KEY.into(),        serde_json::to_value(reason).unwrap_or_default(),    )])} pub fn sandbox_not_applied_from_meta(meta: &Option<acp::Meta>) -> Option<SandboxNotAppliedReason> {    meta.as_ref()        .and_then(|m| m.get(SANDBOX_NOT_APPLIED_META_KEY))        .and_then(|v| serde_json::from_value(v.clone()).ok())} #[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 protocol_id: Option<acp::MessageId>,    pub client_id: Option<ClientUserMessageId>,    pub is_optimistic: bool,    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 {        id: Option<acp::MessageId>,        block: ContentBlock,    },    Thought {        id: Option<acp::MessageId>,        block: ContentBlock,    },} impl AssistantMessageChunk {    pub fn from_str(        chunk: &str,        language_registry: &Arc<LanguageRegistry>,        path_style: PathStyle,        cx: &mut App,    ) -> Self {        Self::Message {            id: None,            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))            }        }    }} fn can_merge_message_chunks(    existing: Option<&acp::MessageId>,    incoming: Option<&acp::MessageId>,) -> bool {    match (existing, incoming) {        (Some(existing), Some(incoming)) => existing == incoming,        _ => true,    }} #[derive(Debug)]pub enum AgentThreadEntry {    UserMessage(UserMessage),    AssistantMessage(AssistantMessage),    ToolCall(ToolCall),    Elicitation(ElicitationEntryId),    CompletedPlan(Vec<PlanEntry>),    ContextCompaction(ContextCompaction),} #[derive(Debug, Clone, PartialEq, Eq, Hash)]pub struct ElicitationEntryId(pub Arc<str>); #[derive(Debug)]pub struct Elicitation {    pub id: ElicitationEntryId,    pub request: acp::CreateElicitationRequest,    pub status: ElicitationStatus,} #[derive(Debug)]pub enum ElicitationStatus {    Pending {        respond_tx: oneshot::Sender<acp::CreateElicitationResponse>,    },    Accepted,    Declined,    Canceled,    Completed,} #[derive(Clone, Debug)]pub enum ElicitationStoreEvent {    ElicitationRequested(ElicitationEntryId),    ElicitationResponded(ElicitationEntryId),    ElicitationUpdated(ElicitationEntryId),} #[derive(Default)]pub struct ElicitationStore {    elicitations: Vec<Elicitation>,} impl EventEmitter<ElicitationStoreEvent> for ElicitationStore {} impl ElicitationStore {    pub fn elicitations(&self) -> &[Elicitation] {        &self.elicitations    }     fn validate_request(request: &acp::CreateElicitationRequest) -> Result<(), acp::Error> {        if let acp::ElicitationMode::Url(mode) = &request.mode {            url::Url::parse(&mode.url)                .map_err(|_| acp::Error::invalid_params().data("invalid elicitation URL"))?;        }         Ok(())    }     fn insert_pending_elicitation(        &mut self,        request: acp::CreateElicitationRequest,    ) -> (        ElicitationEntryId,        oneshot::Receiver<acp::CreateElicitationResponse>,    ) {        let (respond_tx, response_rx) = oneshot::channel();        let id = ElicitationEntryId(Uuid::new_v4().to_string().into());        self.elicitations.push(Elicitation {            id: id.clone(),            request,            status: ElicitationStatus::Pending { respond_tx },        });        (id, response_rx)    }     fn response_task<T>(        id: ElicitationEntryId,        response_rx: oneshot::Receiver<acp::CreateElicitationResponse>,        cx: &mut Context<T>,        emit_responded: impl FnOnce(&mut T, &mut Context<T>, ElicitationEntryId) + 'static,    ) -> Task<acp::CreateElicitationResponse>    where        T: 'static,    {        cx.spawn(async move |this, cx| {            let response = response_rx.await.unwrap_or_else(|oneshot::Canceled| {                acp::CreateElicitationResponse::new(acp::ElicitationAction::Cancel)            });            this.update(cx, |this, cx| emit_responded(this, cx, id))                .ok();            response        })    }     fn respond_to_elicitation_entry(        elicitation: &mut Elicitation,        response: acp::CreateElicitationResponse,    ) -> bool {        if !matches!(elicitation.status, ElicitationStatus::Pending { .. }) {            return false;        }        let ElicitationStatus::Pending { respond_tx } = mem::replace(            &mut elicitation.status,            elicitation_status_for_response(&response),        ) else {            return false;        };        respond_tx.send(response).ok();        true    }     fn complete_url_elicitation_entry(elicitation: &mut Elicitation) -> bool {

Lines 1–500 of 10055.