6719 lines · 233.2 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, DiffStats};use agent::{NativeAgentServer, NativeAgentSessionList, SharedThread, ThreadStore};use agent_client_protocol::{self as acp, PromptCapabilities};use agent_servers::AgentServer;#[cfg(test)]use agent_servers::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, EventEmitter, 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::{AgentId, AgentServerStore, 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;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::PathList;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::ThreadHistory;use crate::ModeSelector;use crate::ModelSelectorPopover;use crate::agent_connection_store::{    AgentConnectedState, AgentConnectionEntryEvent, AgentConnectionStore,};use crate::agent_diff::AgentDiff;use crate::entry_view_state::{EntryViewEvent, ViewEvent};use crate::message_editor::{MessageEditor, MessageEditorEvent};use crate::profile_selector::{ProfileProvider, ProfileSelector};use crate::thread_metadata_store::ThreadMetadataStore;use crate::ui::{AgentNotification, AgentNotificationEvent};use crate::{    Agent, AgentDiffPane, AgentInitialContent, AgentPanel, AllowAlways, AllowOnce,    AuthorizeToolCall, ClearMessageQueue, CycleFavoriteModels, CycleModeSelector,    CycleThinkingEffort, EditFirstQueuedMessage, ExpandMessageEditor, Follow, KeepAll, NewThread,    OpenAddContextMenu, OpenAgentDiff, OpenHistory, RejectAll, RejectOnce,    RemoveFirstQueuedMessage, SendImmediately, SendNextQueuedMessage, ToggleFastMode,    ToggleProfileSelector, ToggleThinkingEffortMenu, ToggleThinkingMode, UndoLastReject,}; const STOPWATCH_THRESHOLD: Duration = Duration::from_secs(30);const TOKEN_THRESHOLD: u64 = 250; mod thread_view;pub use thread_view::*; 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 From<anyhow::Error> for ThreadError {    fn from(error: anyhow::Error) -> 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()));             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>,    /// Tracks the selected granularity index for each tool call's permission dropdown.    /// The index corresponds to the position in the allow_options list.    selected_permission_granularity: HashMap<acp::SessionId, HashMap<acp::ToolCallId, usize>>,} 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 selected_permission_granularity(        &self,        session_id: &acp::SessionId,        tool_call_id: &acp::ToolCallId,    ) -> Option<usize> {        self.selected_permission_granularity            .get(session_id)            .and_then(|map| map.get(tool_call_id))            .copied()    }     pub fn set_selected_permission_granularity(        &mut self,        session_id: acp::SessionId,        tool_call_id: acp::ToolCallId,        granularity: usize,    ) {        self.selected_permission_granularity            .entry(session_id)            .or_default()            .insert(tool_call_id, granularity);    }     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 enum AcpServerViewEvent {    ActiveThreadChanged,} impl EventEmitter<AcpServerViewEvent> for ConversationView {} pub struct ConversationView {    agent: Rc<dyn AgentServer>,    connection_store: Entity<AgentConnectionStore>,    connection_key: Agent,    agent_server_store: Entity<AgentServerStore>,    workspace: WeakEntity<Workspace>,    project: Entity<Project>,    thread_store: Option<Entity<ThreadStore>>,    prompt_store: Option<Entity<PromptStore>>,    server_state: ServerState,    focus_handle: FocusHandle,    notifications: Vec<WindowHandle<AgentNotification>>,    notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,    auth_task: Option<Task<()>>,    _subscriptions: Vec<Subscription>,} impl ConversationView {    pub fn has_auth_methods(&self) -> bool {        self.as_connected().map_or(false, |connected| {            !connected.connection.auth_methods().is_empty()        })    }     pub fn active_thread(&self) -> Option<&Entity<ThreadView>> {        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<ThreadView>> {        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<ThreadView>> {        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.emit(AcpServerViewEvent::ActiveThreadChanged);        cx.notify();    }} enum ServerState {    Loading(Entity<LoadingView>),    LoadError {        error: LoadError,        session_id: Option<acp::SessionId>,    },    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<ThreadView>>,    connection: Rc<dyn AgentConnection>,    history: Entity<ThreadHistory>,    conversation: Entity<Conversation>,    _connection_entry_subscription: Subscription,} 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 {    session_id: Option<acp::SessionId>,    _load_task: Task<()>,} impl ConnectedServerState {    pub fn active_view(&self) -> Option<&Entity<ThreadView>> {        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().filter_map(|id| {            if self.connection.supports_close_session() {                Some(self.connection.clone().close_session(id, cx))            } else {                None            }        });        let task = futures::future::join_all(tasks);        cx.background_spawn(async move {            task.await;        })    }} impl ConversationView {    pub fn new(        agent: Rc<dyn AgentServer>,        connection_store: Entity<AgentConnectionStore>,        connection_key: Agent,        resume_session_id: Option<acp::SessionId>,        work_dirs: Option<PathList>,        title: Option<SharedString>,        initial_content: Option<AgentInitialContent>,        workspace: WeakEntity<Workspace>,        project: Entity<Project>,        thread_store: Option<Entity<ThreadStore>>,        prompt_store: Option<Entity<PromptStore>>,        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),

Lines 1–500 of 6719.

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