11184 lines · 397.8 KB · Rust
RawDownload
use acp_thread::{    AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk,    AuthRequired, ClientUserMessageId, ElicitationEntryId, ElicitationStatus, ElicitationStore,    LoadError, MaxOutputTokensError, MentionUri, PermissionOptionChoice, PermissionOptions,    PermissionPattern, RetryStatus, SelectedPermissionOutcome, ThreadStatus, ToolCall,    ToolCallContent, ToolCallStatus,};use acp_thread::{AgentConnection, Plan};use action_log::{ActionLog, ActionLogTelemetry, DiffStats};use agent::{NativeAgentServer, NoModelConfiguredError, ThreadStore};use agent_client_protocol::schema::v1 as acp;#[cfg(test)]use agent_servers::AgentServerDelegate;use agent_servers::{AgentServer, GEMINI_TERMINAL_AUTH_METHOD_ID};use agent_settings::{AgentProfileId, AgentSettings};use anyhow::{Result, anyhow};#[cfg(feature = "audio")]use audio::{Audio, Sound};use buffer_diff::BufferDiff;use client::zed_urls;use collections::{HashMap, HashSet, IndexMap};use editor::scroll::Autoscroll;use editor::{    Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects, SizingBehavior,};use file_icons::FileIcons;use fs::Fs;use futures::FutureExt as _;use gpui::{    Action, Animation, AnimationExt, App, ClickEvent, ClipboardItem, CursorStyle, ElementId, Empty,    Entity, EventEmitter, FocusHandle, Focusable, Hsla, ListOffset, ListState, ObjectFit,    PlatformDisplay, ScrollHandle, SharedString, StyledText, Subscription, Task, TextRun,    TextStyle, WeakEntity, Window, WindowHandle, div, ease_in_out, img, linear_color_stop,    linear_gradient, list, pulsating_between,};use language::{Buffer, Language, Rope};use language_model::LanguageModelCompletionError;use markdown::{    CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownFont, MarkdownStyle,};use parking_lot::{Mutex, RwLock};use project::{AgentId, AgentServerStore, Project, ProjectEntryId, ProjectPath}; use crate::conversation_view::elicitation::{    ElicitationCard, ElicitationCardHandlers, ElicitationFormState, should_render_elicitation,};use crate::message_editor::SessionCapabilities;use crate::{AgentThreadSource, DEFAULT_THREAD_TITLE, resolve_agent_image};use lru::LruCache;use rope::Point;use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};use std::num::NonZeroUsize;use std::path::{Path, PathBuf};use std::sync::Arc;use std::time::Instant;use std::{rc::Rc, time::Duration};use terminal_view::terminal_panel::TerminalPanel;use text::Anchor;use theme_settings::{AgentBufferFontSize, AgentUiFontSize};use ui::{    Callout, CircularProgress, CommonAnimationExt, ContextMenu, ContextMenuEntry, CopyButton,    DecoratedIcon, DiffStat, Disclosure, Divider, DividerColor, IconDecoration, IconDecorationKind,    KeyBinding, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip, WithScrollbar, prelude::*,    right_click_menu,};use util::{    ResultExt, debug_panic, defer,    paths::{PathStyle, PathWithPosition},    rel_path::RelPath,    size::format_file_size,    time::duration_alt_display,};use workspace::{    CollaboratorId, MultiWorkspace, NewTerminal, PathList, Workspace, path_link::sanitize_path_text,};use zed_actions::agent::{Chat, ToggleModelSelector}; use super::config_options::ConfigOptionsView;use super::entry_view_state::EntryViewState;use crate::ModeSelector;use crate::ModelSelectorPopover;use crate::agent_connection_store::{    AgentConnectedState, AgentConnectionEntryEvent, AgentConnectionStore,};use crate::agent_diff::AgentDiff;use crate::completion_provider::{AgentContextSelection, AvailableSkill};use crate::entry_view_state::{EntryViewEvent, ViewEvent};use crate::message_editor::{InputAttempt, MessageEditor, MessageEditorEvent};use crate::profile_selector::{ProfileProvider, ProfileSelector}; use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore};use crate::ui::{AgentNotification, AgentNotificationEvent};use crate::{    Agent, AgentDiffPane, AgentInitialContent, AgentPanel, AgentPanelEvent, AllowAlways, AllowOnce,    AuthorizeToolCall, ClearMessageQueue, CycleFavoriteModels, CycleModeSelector,    CycleThinkingEffort, EditFirstQueuedMessage, ExpandMessageEditor, Follow, KeepAll, NewThread,    OpenAddContextMenu, OpenAgentDiff, RejectAll, RejectOnce, RemoveFirstQueuedMessage,    ScrollOutputLineDown, ScrollOutputLineUp, ScrollOutputPageDown, ScrollOutputPageUp,    ScrollOutputToBottom, ScrollOutputToNextMessage, ScrollOutputToPreviousMessage,    ScrollOutputToTop, SendImmediately, SendNextQueuedMessage, ToggleFastMode,    ToggleProfileSelector, ToggleSteerFirstQueuedMessage, ToggleThinkingEffortMenu,    ToggleThinkingMode, UndoLastReject,}; const STOPWATCH_THRESHOLD: Duration = Duration::from_secs(30);const TOKEN_THRESHOLD: u64 = 250; pub(crate) const DRAFT_PROMPT_PERSIST_DEBOUNCE: Duration = Duration::from_millis(250); pub(crate) mod elicitation;mod message_queue;mod thread_search_bar;mod thread_view;pub use message_queue::*;pub use thread_view::*; #[derive(Copy, Clone, Debug, PartialEq, Eq)]enum ThreadFeedback {    Positive,    Negative,} #[derive(Debug)]pub(crate) enum ThreadError {    PaymentRequired,    DataRetentionConsentRequired,    Refusal,    AuthenticationRequired(SharedString),    RateLimitExceeded {        provider: SharedString,    },    ServerOverloaded {        provider: SharedString,    },    PromptTooLarge,    NoCredentials {        provider: SharedString,    },    StreamError {        provider: SharedString,    },    AuthenticationFailed {        provider: SharedString,    },    PermissionDenied {        provider: SharedString,        message: Option<SharedString>,    },    RequestFailed,    MaxOutputTokens,    NoModelSelected,    ApiError {        provider: SharedString,    },    Other {        message: SharedString,        acp_error_code: Option<SharedString>,    },} impl From<anyhow::Error> for ThreadError {    fn from(error: anyhow::Error) -> Self {        if error.is::<MaxOutputTokensError>() {            Self::MaxOutputTokens        } else if error.is::<NoModelConfiguredError>() {            Self::NoModelSelected        } else if let Some(acp_error) = error.downcast_ref::<acp::Error>()            && acp_error.code == acp::ErrorCode::AuthRequired        {            Self::AuthenticationRequired(acp_error.message.clone().into())        } else if let Some(lm_error) = error.downcast_ref::<LanguageModelCompletionError>() {            use LanguageModelCompletionError::*;            match lm_error {                RateLimitExceeded { provider, .. } => Self::RateLimitExceeded {                    provider: provider.to_string().into(),                },                ServerOverloaded { provider, .. } | ApiInternalServerError { provider, .. } => {                    Self::ServerOverloaded {                        provider: provider.to_string().into(),                    }                }                PromptTooLarge { .. } => Self::PromptTooLarge,                PaymentRequired => Self::PaymentRequired,                NoApiKey { provider } => Self::NoCredentials {                    provider: provider.to_string().into(),                },                StreamEndedUnexpectedly { provider }                | ApiReadResponseError { provider, .. }                | DeserializeResponse { provider, .. }                | HttpSend { provider, .. } => Self::StreamError {                    provider: provider.to_string().into(),                },                AuthenticationError { provider, .. } => Self::AuthenticationFailed {                    provider: provider.to_string().into(),                },                PermissionError { provider, message } => Self::PermissionDenied {                    provider: provider.to_string().into(),                    message: Some(message.clone().into()),                },                UpstreamProviderError { .. } => Self::RequestFailed,                DataRetentionConsentRequired { .. } => Self::DataRetentionConsentRequired,                BadRequestFormat { provider, .. }                | HttpResponseError { provider, .. }                | ApiEndpointNotFound { provider } => Self::ApiError {                    provider: provider.to_string().into(),                },                _ => {                    let message: SharedString = format!("{:#}", error).into();                    Self::Other {                        message,                        acp_error_code: None,                    }                }            }        } else {            let message: SharedString = format!("{:#}", error).into();             // Extract ACP error code if available            let acp_error_code = error                .downcast_ref::<acp::Error>()                .map(|acp_error| SharedString::from(acp_error.code.to_string()));             Self::Other {                message,                acp_error_code,            }        }    }} impl ProfileProvider for Entity<agent::Thread> {    fn profile_id(&self, cx: &App) -> AgentProfileId {        self.read(cx).profile().clone()    }     fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) {        self.update(cx, |thread, cx| {            // Apply the profile and let the thread swap to its default model.            thread.set_profile(profile_id, cx);        });    }     fn profiles_supported(&self, cx: &App) -> bool {        self.read(cx)            .model()            .is_some_and(|model| model.supports_tools())    }     fn model_selected(&self, cx: &App) -> bool {        self.read(cx).model().is_some()    }     fn is_restricted(&self, cx: &App) -> bool {        project::trusted_worktrees::TrustedWorktrees::has_restricted_worktrees(            &self.read(cx).project().read(cx).worktree_store(),            cx,        )    }     fn profile_downgraded(&self, cx: &App) -> bool {        self.read(cx).profile_was_downgraded()    }} #[derive(Default)]pub(crate) struct Conversation {    threads: HashMap<acp::SessionId, Entity<AcpThread>>,    permission_requests: IndexMap<acp::SessionId, Vec<acp::ToolCallId>>,    elicitation_requests: IndexMap<acp::SessionId, Vec<ElicitationEntryId>>,    subscriptions: Vec<Subscription>,    updated_at: Option<Instant>,} impl Conversation {    pub fn register_thread(&mut self, thread: Entity<AcpThread>, cx: &mut Context<Self>) {        let session_id = thread.read(cx).session_id().clone();        let subscription = cx.subscribe(&thread, {            let session_id = session_id.clone();            move |this, _thread, event, _cx| {                this.updated_at = Some(Instant::now());                match event {                    AcpThreadEvent::ToolAuthorizationRequested(id) => {                        this.permission_requests                            .entry(session_id.clone())                            .or_default()                            .push(id.clone());                    }                    AcpThreadEvent::ToolAuthorizationReceived(id) => {                        if let Some(tool_calls) = this.permission_requests.get_mut(&session_id) {                            tool_calls.retain(|tool_call_id| tool_call_id != id);                            if tool_calls.is_empty() {                                this.permission_requests.shift_remove(&session_id);                            }                        }                    }                    AcpThreadEvent::ElicitationRequested(id) => {                        this.elicitation_requests                            .entry(session_id.clone())                            .or_default()                            .push(id.clone());                    }                    AcpThreadEvent::ElicitationResponded(id) => {                        if let Some(elicitations) = this.elicitation_requests.get_mut(&session_id) {                            elicitations.retain(|elicitation_id| elicitation_id != id);                            if elicitations.is_empty() {                                this.elicitation_requests.shift_remove(&session_id);                            }                        }                    }                    AcpThreadEvent::NewEntry                    | AcpThreadEvent::StatusChanged                    | AcpThreadEvent::TitleUpdated                    | AcpThreadEvent::TokenUsageUpdated                    | AcpThreadEvent::EntryUpdated(_)                    | AcpThreadEvent::EntriesRemoved(_)                    | AcpThreadEvent::Retry(_)                    | AcpThreadEvent::SubagentSpawned(_)                    | AcpThreadEvent::Stopped(_)                    | AcpThreadEvent::Error                    | AcpThreadEvent::LoadError(_)                    | AcpThreadEvent::PromptCapabilitiesUpdated                    | AcpThreadEvent::Refusal                    | AcpThreadEvent::AvailableCommandsUpdated(_)                    | AcpThreadEvent::ModeUpdated(_)                    | AcpThreadEvent::ConfigOptionsUpdated(_)                    | AcpThreadEvent::WorkingDirectoriesUpdated                    | AcpThreadEvent::PromptUpdated => {}                }            }        });        self.subscriptions.push(subscription);        self.threads.insert(session_id, thread);    }     pub fn permission_options_for_tool_call<'a>(        &'a self,        session_id: &acp::SessionId,        tool_call_id: acp::ToolCallId,        cx: &'a App,    ) -> Option<&'a PermissionOptions> {        let thread = self.threads.get(session_id)?;        let (_, tool_call) = thread.read(cx).tool_call(&tool_call_id)?;        let ToolCallStatus::WaitingForConfirmation { options, .. } = &tool_call.status else {            return None;        };        Some(options)    }     pub fn pending_tool_call<'a>(        &'a self,        session_id: &acp::SessionId,        cx: &'a App,    ) -> Option<(acp::SessionId, acp::ToolCallId, &'a PermissionOptions)> {        let thread = self.threads.get(session_id)?;        let is_subagent = thread.read(cx).parent_session_id().is_some();        let (result_session_id, thread, tool_id) = if is_subagent {            let id = self.permission_requests.get(session_id)?.iter().next()?;            (session_id.clone(), thread, id)        } else {            let (id, tool_calls) = self.permission_requests.first()?;            let thread = self.threads.get(id)?;            let tool_id = tool_calls.iter().next()?;            (id.clone(), thread, tool_id)        };        let (_, tool_call) = thread.read(cx).tool_call(tool_id)?;         let ToolCallStatus::WaitingForConfirmation { options, .. } = &tool_call.status else {            return None;        };        Some((result_session_id, tool_id.clone(), options))    }     pub fn subagents_awaiting_permission(&self, cx: &App) -> Vec<(acp::SessionId, usize)> {        self.permission_requests            .iter()            .filter_map(|(session_id, tool_call_ids)| {                let thread = self.threads.get(session_id)?;                if thread.read(cx).parent_session_id().is_some() && !tool_call_ids.is_empty() {                    Some((session_id.clone(), tool_call_ids.len()))                } else {                    None                }            })            .collect()    }     /// Returns the first pending tool call request for exactly `session_id`.    /// Unlike `pending_tool_call`, this does not use the global FIFO pending    /// request for non-subagent sessions.    pub fn pending_tool_call_for_session(        &self,        session_id: &acp::SessionId,        cx: &App,    ) -> Option<acp::ToolCallId> {        let thread = self.threads.get(session_id)?;        let tool_call_id = self.permission_requests.get(session_id)?.iter().next()?;        let (_, tool_call) = thread.read(cx).tool_call(tool_call_id)?;        if !matches!(            tool_call.status,            ToolCallStatus::WaitingForConfirmation { .. }        ) {            return None;        }        Some(tool_call_id.clone())    }     pub fn pending_tool_call_count_for_session(&self, session_id: &acp::SessionId) -> usize {        self.permission_requests            .get(session_id)            .map(|tool_call_ids| tool_call_ids.len())            .unwrap_or(0)    }     pub fn respond_to_elicitation(        &mut self,        session_id: acp::SessionId,        elicitation_id: ElicitationEntryId,        response: acp::CreateElicitationResponse,        cx: &mut Context<Self>,    ) -> Option<()> {        let thread = self.threads.get(&session_id)?.clone();        thread.update(cx, |thread, cx| {            thread.respond_to_elicitation(&elicitation_id, response, cx);        });        Some(())    }     pub fn authorize_pending_tool_call(        &mut self,        session_id: &acp::SessionId,        kind: acp::PermissionOptionKind,        cx: &mut Context<Self>,    ) -> Option<()> {        let (authorize_session_id, tool_call_id, options) =            self.pending_tool_call(session_id, cx)?;        let option = permission_option_for_action(options, kind)?;        self.authorize_tool_call(            authorize_session_id,            tool_call_id,            SelectedPermissionOutcome::new(option.option_id.clone(), option.kind),            cx,        );        Some(())    }     pub fn authorize_with_granularity(        &mut self,        session_id: acp::SessionId,        tool_call_id: acp::ToolCallId,        selection: Option<&thread_view::PermissionSelection>,        is_allow: bool,        cx: &mut Context<Self>,    ) -> Option<()> {        let options =            self.permission_options_for_tool_call(&session_id, tool_call_id.clone(), cx)?;        let outcome = resolve_outcome_from_selection(options, selection, is_allow)?;        self.authorize_tool_call(session_id, tool_call_id, outcome, cx);        Some(())    }     pub fn authorize_tool_call(        &mut self,        session_id: acp::SessionId,        tool_call_id: acp::ToolCallId,        outcome: SelectedPermissionOutcome,        cx: &mut Context<Self>,    ) {        let Some(thread) = self.threads.get(&session_id) else {            return;        };        let agent_telemetry_id = thread.read(cx).connection().telemetry_id();        let session_id = thread.read(cx).session_id().clone();         telemetry::event!(            "Agent Tool Call Authorized",            agent = agent_telemetry_id,            session = session_id,            option = outcome.option_kind        );         thread.update(cx, |thread, cx| {            thread.authorize_tool_call(tool_call_id, outcome, cx);        });        cx.notify();    }     fn set_work_dirs(&mut self, work_dirs: PathList, cx: &mut Context<Self>) {        for thread in self.threads.values() {            thread.update(cx, |thread, cx| {                thread.set_work_dirs(work_dirs.clone(), cx);            });        }    }} pub(crate) struct RootThreadUpdated; impl EventEmitter<RootThreadUpdated> for ConversationView {} fn permission_option_for_action(

Lines 1–500 of 11184.