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<settings::GrantedWritePath>, /// Windows/WSL only: the command will write to a path on a Windows drive /// (DrvFs), whose sandbox-integrity guarantees are weaker. Drives the /// weaker-guarantee warning banner in the approval prompt. #[serde(default)] pub warn_windows_fs: bool, /// 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> { match &request.mode { acp::ElicitationMode::Form(_) => {} acp::ElicitationMode::Url(mode) => { let url = url::Url::parse(&mode.url) .map_err(|_| acp::Error::invalid_params().data("invalid elicitation URL"))?; if !matches!(url.scheme(), "http" | "https") || url.host_str().is_none() { return Err(acp::Error::invalid_params() .data("elicitation URL must use HTTP or HTTPS and include a host")); } } _ => { return Err(acp::Error::invalid_params().data("unsupported elicitation mode")); } } 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,Lines 1–500 of 10196. Load more
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 meta pub 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 name pub 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 indexes pub 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<settings::GrantedWritePath>, /// Windows/WSL only: the command will write to a path on a Windows drive /// (DrvFs), whose sandbox-integrity guarantees are weaker. Drives the /// weaker-guarantee warning banner in the approval prompt. #[serde(default)] pub warn_windows_fs: bool, /// 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 meta pub 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> { match &request.mode { acp::ElicitationMode::Form(_) => {} acp::ElicitationMode::Url(mode) => { let url = url::Url::parse(&mode.url) .map_err(|_| acp::Error::invalid_params().data("invalid elicitation URL"))?; if !matches!(url.scheme(), "http" | "https") || url.host_str().is_none() { return Err(acp::Error::invalid_params() .data("elicitation URL must use HTTP or HTTPS and include a host")); } } _ => { return Err(acp::Error::invalid_params().data("unsupported elicitation mode")); } } 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,