6254 lines · 218.6 KB · Rust
RawDownload
use acp_thread::{    AcpThread, AcpThreadEvent, AgentSessionInfo, AgentThreadEntry, AssistantMessage,    AssistantMessageChunk, AuthRequired, LoadError, MentionUri, PermissionOptionChoice,    PermissionOptions, RetryStatus, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus,    UserMessageId,};use acp_thread::{AgentConnection, Plan};use action_log::{ActionLog, ActionLogTelemetry};use agent::{NativeAgentServer, NativeAgentSessionList, SharedThread, ThreadStore};use agent_client_protocol::{self as acp, PromptCapabilities};use agent_servers::{AgentServer, AgentServerDelegate};use agent_settings::{AgentProfileId, AgentSettings};use anyhow::{Result, anyhow};use arrayvec::ArrayVec;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 feature_flags::{AgentSharingFeatureFlag, AgentV2FeatureFlag, FeatureFlagAppExt as _};use file_icons::FileIcons;use fs::Fs;use futures::FutureExt as _;use gpui::{    Action, Animation, AnimationExt, AnyView, App, ClickEvent, ClipboardItem, CursorStyle,    ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, ListOffset, ListState, ObjectFit,    PlatformDisplay, ScrollHandle, SharedString, Subscription, Task, TextStyle, WeakEntity, Window,    WindowHandle, div, ease_in_out, img, linear_color_stop, linear_gradient, list, point,    pulsating_between,};use language::Buffer;use language_model::LanguageModelRegistry;use markdown::{Markdown, MarkdownElement, MarkdownFont, MarkdownStyle};use project::{AgentServerStore, ExternalAgentServerName, Project, ProjectEntryId};use prompt_store::{PromptId, PromptStore};use rope::Point;use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};use std::cell::RefCell;use std::path::Path;use std::sync::Arc;use std::time::Instant;use std::{collections::BTreeMap, rc::Rc, time::Duration};use terminal_view::terminal_panel::TerminalPanel;use text::{Anchor, ToPoint as _};use theme::AgentFontSize;use ui::{    Callout, CircularProgress, CommonAnimationExt, ContextMenu, ContextMenuEntry, CopyButton,    DecoratedIcon, DiffStat, Disclosure, Divider, DividerColor, IconDecoration, IconDecorationKind,    KeyBinding, PopoverMenu, PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar,    prelude::*, right_click_menu,};use util::{ResultExt, size::format_file_size, time::duration_alt_display};use util::{debug_panic, defer};use workspace::{    CollaboratorId, MultiWorkspace, NewTerminal, Toast, Workspace, notifications::NotificationId,};use zed_actions::agent::{Chat, ToggleModelSelector};use zed_actions::assistant::OpenRulesLibrary; use super::config_options::ConfigOptionsView;use super::entry_view_state::EntryViewState;use super::thread_history::AcpThreadHistory;use crate::acp::AcpModelSelectorPopover;use crate::acp::ModeSelector;use crate::acp::entry_view_state::{EntryViewEvent, ViewEvent};use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};use crate::agent_diff::AgentDiff;use crate::profile_selector::{ProfileProvider, ProfileSelector};use crate::ui::{AgentNotification, AgentNotificationEvent};use crate::{    AgentDiffPane, AgentInitialContent, AgentPanel, AllowAlways, AllowOnce, AuthorizeToolCall,    ClearMessageQueue, CycleFavoriteModels, CycleModeSelector, CycleThinkingEffort,    EditFirstQueuedMessage, ExpandMessageEditor, Follow, KeepAll, NewThread, OpenAddContextMenu,    OpenAgentDiff, OpenHistory, RejectAll, RejectOnce, RemoveFirstQueuedMessage,    SelectPermissionGranularity, SendImmediately, SendNextQueuedMessage, ToggleProfileSelector,    ToggleThinkingEffortMenu, ToggleThinkingMode, UndoLastReject,}; const STOPWATCH_THRESHOLD: Duration = Duration::from_secs(30);const TOKEN_THRESHOLD: u64 = 250; mod active_thread;pub use active_thread::*; pub struct QueuedMessage {    pub content: Vec<acp::ContentBlock>,    pub tracked_buffers: Vec<Entity<Buffer>>,} #[derive(Copy, Clone, Debug, PartialEq, Eq)]enum ThreadFeedback {    Positive,    Negative,} #[derive(Debug)]pub(crate) enum ThreadError {    PaymentRequired,    Refusal,    AuthenticationRequired(SharedString),    Other {        message: SharedString,        acp_error_code: Option<SharedString>,    },} impl ThreadError {    fn from_err(error: anyhow::Error, agent_name: &str) -> Self {        if error.is::<language_model::PaymentRequiredError>() {            Self::PaymentRequired        } 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 {            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()));             // TODO: we should have Gemini return better errors here.            if agent_name == "Gemini CLI"                && message.contains("Could not load the default credentials")                || message.contains("API key not valid")                || message.contains("Request had invalid authentication credentials")            {                Self::AuthenticationRequired(message)            } else {                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())    }} #[derive(Default)]pub(crate) struct Conversation {    threads: HashMap<acp::SessionId, Entity<AcpThread>>,    permission_requests: IndexMap<acp::SessionId, Vec<acp::ToolCallId>>,    subscriptions: Vec<Subscription>,} 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, move |this, _thread, event, _cx| 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::NewEntry            | 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(_) => {}        });        self.subscriptions.push(subscription);        self.threads            .insert(thread.read(cx).session_id().clone(), thread);    }     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 (thread, tool_id) = if is_subagent {            let id = self.permission_requests.get(session_id)?.iter().next()?;            (thread, id)        } else {            let (id, tool_calls) = self.permission_requests.first()?;            let thread = self.threads.get(id)?;            let id = tool_calls.iter().next()?;            (thread, id)        };        let (_, tool_call) = thread.read(cx).tool_call(tool_id)?;         let ToolCallStatus::WaitingForConfirmation { options, .. } = &tool_call.status else {            return None;        };        Some((            thread.read(cx).session_id().clone(),            tool_id.clone(),            options,        ))    }     pub fn authorize_pending_tool_call(        &mut self,        session_id: &acp::SessionId,        kind: acp::PermissionOptionKind,        cx: &mut Context<Self>,    ) -> Option<()> {        let (_, tool_call_id, options) = self.pending_tool_call(session_id, cx)?;        let option = options.first_option_of_kind(kind)?;        self.authorize_tool_call(            session_id.clone(),            tool_call_id,            option.option_id.clone(),            option.kind,            cx,        );        Some(())    }     pub fn authorize_tool_call(        &mut self,        session_id: acp::SessionId,        tool_call_id: acp::ToolCallId,        option_id: acp::PermissionOptionId,        option_kind: acp::PermissionOptionKind,        cx: &mut Context<Self>,    ) {        let Some(thread) = self.threads.get(&session_id) else {            return;        };        let agent_telemetry_id = thread.read(cx).connection().telemetry_id();         telemetry::event!(            "Agent Tool Call Authorized",            agent = agent_telemetry_id,            session = session_id,            option = option_kind        );         thread.update(cx, |thread, cx| {            thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx);        });        cx.notify();    }} pub struct AcpServerView {    agent: Rc<dyn AgentServer>,    agent_server_store: Entity<AgentServerStore>,    workspace: WeakEntity<Workspace>,    project: Entity<Project>,    thread_store: Option<Entity<ThreadStore>>,    prompt_store: Option<Entity<PromptStore>>,    server_state: ServerState,    login: Option<task::SpawnInTerminal>, // is some <=> Active | Unauthenticated    history: Entity<AcpThreadHistory>,    focus_handle: FocusHandle,    notifications: Vec<WindowHandle<AgentNotification>>,    notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,    auth_task: Option<Task<()>>,    _subscriptions: Vec<Subscription>,} impl AcpServerView {    pub fn active_thread(&self) -> Option<&Entity<AcpThreadView>> {        match &self.server_state {            ServerState::Connected(connected) => connected.active_view(),            _ => None,        }    }     pub fn pending_tool_call<'a>(        &'a self,        cx: &'a App,    ) -> Option<(acp::SessionId, acp::ToolCallId, &'a PermissionOptions)> {        let id = &self.active_thread()?.read(cx).id;        self.as_connected()?            .conversation            .read(cx)            .pending_tool_call(id, cx)    }     pub fn parent_thread(&self, cx: &App) -> Option<Entity<AcpThreadView>> {        match &self.server_state {            ServerState::Connected(connected) => {                let mut current = connected.active_view()?;                while let Some(parent_id) = current.read(cx).parent_id.clone() {                    if let Some(parent) = connected.threads.get(&parent_id) {                        current = parent;                    } else {                        break;                    }                }                Some(current.clone())            }            _ => None,        }    }     pub fn thread_view(&self, session_id: &acp::SessionId) -> Option<Entity<AcpThreadView>> {        let connected = self.as_connected()?;        connected.threads.get(session_id).cloned()    }     pub fn as_connected(&self) -> Option<&ConnectedServerState> {        match &self.server_state {            ServerState::Connected(connected) => Some(connected),            _ => None,        }    }     pub fn as_connected_mut(&mut self) -> Option<&mut ConnectedServerState> {        match &mut self.server_state {            ServerState::Connected(connected) => Some(connected),            _ => None,        }    }     pub fn navigate_to_session(        &mut self,        session_id: acp::SessionId,        window: &mut Window,        cx: &mut Context<Self>,    ) {        let Some(connected) = self.as_connected_mut() else {            return;        };         connected.navigate_to_session(session_id);        if let Some(view) = self.active_thread() {            view.focus_handle(cx).focus(window, cx);        }        cx.notify();    }} enum ServerState {    Loading(Entity<LoadingView>),    LoadError(LoadError),    Connected(ConnectedServerState),} // current -> Entity// hashmap of threads, current becomes session_idpub struct ConnectedServerState {    auth_state: AuthState,    active_id: Option<acp::SessionId>,    threads: HashMap<acp::SessionId, Entity<AcpThreadView>>,    connection: Rc<dyn AgentConnection>,    conversation: Entity<Conversation>,} enum AuthState {    Ok,    Unauthenticated {        description: Option<Entity<Markdown>>,        configuration_view: Option<AnyView>,        pending_auth_method: Option<acp::AuthMethodId>,        _subscription: Option<Subscription>,    },} impl AuthState {    pub fn is_ok(&self) -> bool {        matches!(self, Self::Ok)    }} struct LoadingView {    title: SharedString,    _load_task: Task<()>,    _update_title_task: Task<anyhow::Result<()>>,} impl ConnectedServerState {    pub fn active_view(&self) -> Option<&Entity<AcpThreadView>> {        self.active_id.as_ref().and_then(|id| self.threads.get(id))    }     pub fn has_thread_error(&self, cx: &App) -> bool {        self.active_view()            .map_or(false, |view| view.read(cx).thread_error.is_some())    }     pub fn navigate_to_session(&mut self, session_id: acp::SessionId) {        if self.threads.contains_key(&session_id) {            self.active_id = Some(session_id);        }    }     pub fn close_all_sessions(&self, cx: &mut App) -> Task<()> {        let tasks = self            .threads            .keys()            .map(|id| self.connection.close_session(id, cx));        let task = futures::future::join_all(tasks);        cx.background_spawn(async move {            task.await;        })    }} impl AcpServerView {    pub fn new(        agent: Rc<dyn AgentServer>,        resume_thread: Option<AgentSessionInfo>,        initial_content: Option<AgentInitialContent>,        workspace: WeakEntity<Workspace>,        project: Entity<Project>,        thread_store: Option<Entity<ThreadStore>>,        prompt_store: Option<Entity<PromptStore>>,        history: Entity<AcpThreadHistory>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let agent_server_store = project.read(cx).agent_server_store().clone();        let subscriptions = vec![            cx.observe_global_in::<SettingsStore>(window, Self::agent_ui_font_size_changed),            cx.observe_global_in::<AgentFontSize>(window, Self::agent_ui_font_size_changed),            cx.subscribe_in(                &agent_server_store,                window,                Self::handle_agent_servers_updated,            ),        ];         cx.on_release(|this, cx| {            if let Some(connected) = this.as_connected() {                connected.close_all_sessions(cx).detach();            }            for window in this.notifications.drain(..) {                window                    .update(cx, |_, window, _| {                        window.remove_window();                    })                    .ok();            }        })        .detach();         Self {            agent: agent.clone(),            agent_server_store,            workspace,            project: project.clone(),            thread_store,            prompt_store,            server_state: Self::initial_state(                agent.clone(),                resume_thread,                project,                initial_content,                window,                cx,            ),            login: None,            notifications: Vec::new(),            notification_subscriptions: HashMap::default(),            auth_task: None,            history,            _subscriptions: subscriptions,            focus_handle: cx.focus_handle(),        }    }     fn set_server_state(&mut self, state: ServerState, cx: &mut Context<Self>) {

Lines 1–500 of 6254.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t