thread_view.rs

   1use acp_thread::{
   2    AcpThread, AcpThreadEvent, AgentSessionInfo, AgentThreadEntry, AssistantMessage,
   3    AssistantMessageChunk, AuthRequired, LoadError, MentionUri, PermissionOptionChoice,
   4    PermissionOptions, RetryStatus, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus,
   5    UserMessageId,
   6};
   7use acp_thread::{AgentConnection, Plan};
   8use action_log::{ActionLog, ActionLogTelemetry};
   9use agent::{NativeAgentServer, NativeAgentSessionList, SharedThread, ThreadStore};
  10use agent_client_protocol::{self as acp, PromptCapabilities};
  11use agent_servers::{AgentServer, AgentServerDelegate};
  12use agent_settings::{AgentProfileId, AgentSettings};
  13use anyhow::{Result, anyhow};
  14use arrayvec::ArrayVec;
  15use audio::{Audio, Sound};
  16use buffer_diff::BufferDiff;
  17use client::zed_urls;
  18use collections::{HashMap, HashSet};
  19use editor::scroll::Autoscroll;
  20use editor::{
  21    Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects, SizingBehavior,
  22};
  23use feature_flags::{
  24    AgentSharingFeatureFlag, AgentV2FeatureFlag, CloudThinkingEffortFeatureFlag,
  25    FeatureFlagAppExt as _,
  26};
  27use file_icons::FileIcons;
  28use fs::Fs;
  29use futures::FutureExt as _;
  30use gpui::{
  31    Action, Animation, AnimationExt, AnyView, App, ClickEvent, ClipboardItem, CursorStyle,
  32    ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, ListOffset, ListState, ObjectFit,
  33    PlatformDisplay, ScrollHandle, SharedString, Subscription, Task, TextStyle, WeakEntity, Window,
  34    WindowHandle, div, ease_in_out, img, linear_color_stop, linear_gradient, list, point,
  35    pulsating_between,
  36};
  37use language::Buffer;
  38use language_model::LanguageModelRegistry;
  39use markdown::{Markdown, MarkdownElement, MarkdownFont, MarkdownStyle};
  40use project::{AgentServerStore, ExternalAgentServerName, Project, ProjectEntryId};
  41use prompt_store::{PromptId, PromptStore};
  42use rope::Point;
  43use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};
  44use std::cell::RefCell;
  45use std::path::Path;
  46use std::sync::Arc;
  47use std::time::Instant;
  48use std::{collections::BTreeMap, rc::Rc, time::Duration};
  49use terminal_view::terminal_panel::TerminalPanel;
  50use text::{Anchor, ToPoint as _};
  51use theme::AgentFontSize;
  52use ui::{
  53    Callout, CommonAnimationExt, ContextMenu, ContextMenuEntry, CopyButton, DecoratedIcon,
  54    DiffStat, Disclosure, Divider, DividerColor, IconButtonShape, IconDecoration,
  55    IconDecorationKind, KeyBinding, PopoverMenu, PopoverMenuHandle, SpinnerLabel, TintColor,
  56    Tooltip, WithScrollbar, prelude::*, right_click_menu,
  57};
  58use util::defer;
  59use util::{ResultExt, size::format_file_size, time::duration_alt_display};
  60use workspace::{CollaboratorId, NewTerminal, Toast, Workspace, notifications::NotificationId};
  61use zed_actions::agent::{Chat, ToggleModelSelector};
  62use zed_actions::assistant::OpenRulesLibrary;
  63
  64use super::config_options::ConfigOptionsView;
  65use super::entry_view_state::EntryViewState;
  66use super::thread_history::AcpThreadHistory;
  67use crate::acp::AcpModelSelectorPopover;
  68use crate::acp::ModeSelector;
  69use crate::acp::entry_view_state::{EntryViewEvent, ViewEvent};
  70use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
  71use crate::agent_diff::AgentDiff;
  72use crate::profile_selector::{ProfileProvider, ProfileSelector};
  73use crate::ui::{AgentNotification, AgentNotificationEvent};
  74use crate::{
  75    AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, AuthorizeToolCall, ClearMessageQueue,
  76    CycleFavoriteModels, CycleModeSelector, CycleThinkingEffort, EditFirstQueuedMessage,
  77    ExpandMessageEditor, ExternalAgentInitialContent, Follow, KeepAll, NewThread,
  78    OpenAddContextMenu, OpenAgentDiff, OpenHistory, RejectAll, RejectOnce,
  79    RemoveFirstQueuedMessage, SelectPermissionGranularity, SendImmediately, SendNextQueuedMessage,
  80    ToggleProfileSelector, ToggleThinkingEffortMenu, ToggleThinkingMode,
  81};
  82
  83const STOPWATCH_THRESHOLD: Duration = Duration::from_secs(30);
  84const TOKEN_THRESHOLD: u64 = 250;
  85
  86mod active_thread;
  87pub use active_thread::*;
  88
  89pub struct QueuedMessage {
  90    pub content: Vec<acp::ContentBlock>,
  91    pub tracked_buffers: Vec<Entity<Buffer>>,
  92}
  93
  94#[derive(Copy, Clone, Debug, PartialEq, Eq)]
  95enum ThreadFeedback {
  96    Positive,
  97    Negative,
  98}
  99
 100#[derive(Debug)]
 101pub(crate) enum ThreadError {
 102    PaymentRequired,
 103    Refusal,
 104    AuthenticationRequired(SharedString),
 105    Other {
 106        message: SharedString,
 107        acp_error_code: Option<SharedString>,
 108    },
 109}
 110
 111impl ThreadError {
 112    fn from_err(error: anyhow::Error, agent_name: &str) -> Self {
 113        if error.is::<language_model::PaymentRequiredError>() {
 114            Self::PaymentRequired
 115        } else if let Some(acp_error) = error.downcast_ref::<acp::Error>()
 116            && acp_error.code == acp::ErrorCode::AuthRequired
 117        {
 118            Self::AuthenticationRequired(acp_error.message.clone().into())
 119        } else {
 120            let message: SharedString = format!("{:#}", error).into();
 121
 122            // Extract ACP error code if available
 123            let acp_error_code = error
 124                .downcast_ref::<acp::Error>()
 125                .map(|acp_error| SharedString::from(acp_error.code.to_string()));
 126
 127            // TODO: we should have Gemini return better errors here.
 128            if agent_name == "Gemini CLI"
 129                && message.contains("Could not load the default credentials")
 130                || message.contains("API key not valid")
 131                || message.contains("Request had invalid authentication credentials")
 132            {
 133                Self::AuthenticationRequired(message)
 134            } else {
 135                Self::Other {
 136                    message,
 137                    acp_error_code,
 138                }
 139            }
 140        }
 141    }
 142}
 143
 144impl ProfileProvider for Entity<agent::Thread> {
 145    fn profile_id(&self, cx: &App) -> AgentProfileId {
 146        self.read(cx).profile().clone()
 147    }
 148
 149    fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) {
 150        self.update(cx, |thread, cx| {
 151            // Apply the profile and let the thread swap to its default model.
 152            thread.set_profile(profile_id, cx);
 153        });
 154    }
 155
 156    fn profiles_supported(&self, cx: &App) -> bool {
 157        self.read(cx)
 158            .model()
 159            .is_some_and(|model| model.supports_tools())
 160    }
 161}
 162
 163pub struct AcpServerView {
 164    agent: Rc<dyn AgentServer>,
 165    agent_server_store: Entity<AgentServerStore>,
 166    workspace: WeakEntity<Workspace>,
 167    project: Entity<Project>,
 168    thread_store: Option<Entity<ThreadStore>>,
 169    prompt_store: Option<Entity<PromptStore>>,
 170    server_state: ServerState,
 171    login: Option<task::SpawnInTerminal>, // is some <=> Active | Unauthenticated
 172    history: Entity<AcpThreadHistory>,
 173    focus_handle: FocusHandle,
 174    notifications: Vec<WindowHandle<AgentNotification>>,
 175    notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
 176    auth_task: Option<Task<()>>,
 177    _subscriptions: Vec<Subscription>,
 178}
 179
 180impl AcpServerView {
 181    pub fn as_active_thread(&self) -> Option<Entity<AcpThreadView>> {
 182        match &self.server_state {
 183            ServerState::Connected(connected) => Some(connected.current.clone()),
 184            _ => None,
 185        }
 186    }
 187
 188    pub fn as_connected(&self) -> Option<&ConnectedServerState> {
 189        match &self.server_state {
 190            ServerState::Connected(connected) => Some(connected),
 191            _ => None,
 192        }
 193    }
 194
 195    pub fn as_connected_mut(&mut self) -> Option<&mut ConnectedServerState> {
 196        match &mut self.server_state {
 197            ServerState::Connected(connected) => Some(connected),
 198            _ => None,
 199        }
 200    }
 201}
 202
 203enum ServerState {
 204    Loading(Entity<LoadingView>),
 205    LoadError(LoadError),
 206    Connected(ConnectedServerState),
 207}
 208
 209// current -> Entity
 210// hashmap of threads, current becomes session_id
 211pub struct ConnectedServerState {
 212    auth_state: AuthState,
 213    current: Entity<AcpThreadView>,
 214    connection: Rc<dyn AgentConnection>,
 215}
 216
 217enum AuthState {
 218    Ok,
 219    Unauthenticated {
 220        description: Option<Entity<Markdown>>,
 221        configuration_view: Option<AnyView>,
 222        pending_auth_method: Option<acp::AuthMethodId>,
 223        _subscription: Option<Subscription>,
 224    },
 225}
 226
 227impl AuthState {
 228    pub fn is_ok(&self) -> bool {
 229        matches!(self, Self::Ok)
 230    }
 231}
 232
 233struct LoadingView {
 234    title: SharedString,
 235    _load_task: Task<()>,
 236    _update_title_task: Task<anyhow::Result<()>>,
 237}
 238
 239impl ConnectedServerState {
 240    pub fn has_thread_error(&self, cx: &App) -> bool {
 241        self.current.read(cx).thread_error.is_some()
 242    }
 243}
 244
 245impl AcpServerView {
 246    pub fn new(
 247        agent: Rc<dyn AgentServer>,
 248        resume_thread: Option<AgentSessionInfo>,
 249        initial_content: Option<ExternalAgentInitialContent>,
 250        workspace: WeakEntity<Workspace>,
 251        project: Entity<Project>,
 252        thread_store: Option<Entity<ThreadStore>>,
 253        prompt_store: Option<Entity<PromptStore>>,
 254        history: Entity<AcpThreadHistory>,
 255        window: &mut Window,
 256        cx: &mut Context<Self>,
 257    ) -> Self {
 258        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
 259        let available_commands = Rc::new(RefCell::new(vec![]));
 260
 261        let agent_server_store = project.read(cx).agent_server_store().clone();
 262        let subscriptions = vec![
 263            cx.observe_global_in::<SettingsStore>(window, Self::agent_ui_font_size_changed),
 264            cx.observe_global_in::<AgentFontSize>(window, Self::agent_ui_font_size_changed),
 265            cx.subscribe_in(
 266                &agent_server_store,
 267                window,
 268                Self::handle_agent_servers_updated,
 269            ),
 270        ];
 271
 272        cx.on_release(|this, cx| {
 273            for window in this.notifications.drain(..) {
 274                window
 275                    .update(cx, |_, window, _| {
 276                        window.remove_window();
 277                    })
 278                    .ok();
 279            }
 280        })
 281        .detach();
 282
 283        let workspace_for_state = workspace.clone();
 284        let project_for_state = project.clone();
 285
 286        Self {
 287            agent: agent.clone(),
 288            agent_server_store,
 289            workspace,
 290            project,
 291            thread_store,
 292            prompt_store,
 293            server_state: Self::initial_state(
 294                agent.clone(),
 295                resume_thread,
 296                workspace_for_state,
 297                project_for_state,
 298                prompt_capabilities,
 299                available_commands,
 300                initial_content,
 301                window,
 302                cx,
 303            ),
 304            login: None,
 305            notifications: Vec::new(),
 306            notification_subscriptions: HashMap::default(),
 307            auth_task: None,
 308            history,
 309            _subscriptions: subscriptions,
 310            focus_handle: cx.focus_handle(),
 311        }
 312    }
 313
 314    fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 315        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
 316        let available_commands = Rc::new(RefCell::new(vec![]));
 317
 318        let resume_thread_metadata = self
 319            .as_active_thread()
 320            .and_then(|thread| thread.read(cx).resume_thread_metadata.clone());
 321
 322        self.server_state = Self::initial_state(
 323            self.agent.clone(),
 324            resume_thread_metadata,
 325            self.workspace.clone(),
 326            self.project.clone(),
 327            prompt_capabilities.clone(),
 328            available_commands.clone(),
 329            None,
 330            window,
 331            cx,
 332        );
 333
 334        if let Some(connected) = self.as_connected() {
 335            connected.current.update(cx, |this, cx| {
 336                this.message_editor.update(cx, |editor, cx| {
 337                    editor.set_command_state(prompt_capabilities, available_commands, cx);
 338                });
 339            });
 340        }
 341        cx.notify();
 342    }
 343
 344    fn initial_state(
 345        agent: Rc<dyn AgentServer>,
 346        resume_thread: Option<AgentSessionInfo>,
 347        workspace: WeakEntity<Workspace>,
 348        project: Entity<Project>,
 349        prompt_capabilities: Rc<RefCell<PromptCapabilities>>,
 350        available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
 351        initial_content: Option<ExternalAgentInitialContent>,
 352        window: &mut Window,
 353        cx: &mut Context<Self>,
 354    ) -> ServerState {
 355        if project.read(cx).is_via_collab()
 356            && agent.clone().downcast::<NativeAgentServer>().is_none()
 357        {
 358            return ServerState::LoadError(LoadError::Other(
 359                "External agents are not yet supported in shared projects.".into(),
 360            ));
 361        }
 362        let mut worktrees = project.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
 363        // Pick the first non-single-file worktree for the root directory if there are any,
 364        // and otherwise the parent of a single-file worktree, falling back to $HOME if there are no visible worktrees.
 365        worktrees.sort_by(|l, r| {
 366            l.read(cx)
 367                .is_single_file()
 368                .cmp(&r.read(cx).is_single_file())
 369        });
 370        let root_dir = worktrees
 371            .into_iter()
 372            .filter_map(|worktree| {
 373                if worktree.read(cx).is_single_file() {
 374                    Some(worktree.read(cx).abs_path().parent()?.into())
 375                } else {
 376                    Some(worktree.read(cx).abs_path())
 377                }
 378            })
 379            .next();
 380        let fallback_cwd = root_dir
 381            .clone()
 382            .unwrap_or_else(|| paths::home_dir().as_path().into());
 383        let (status_tx, mut status_rx) = watch::channel("Loading…".into());
 384        let (new_version_available_tx, mut new_version_available_rx) = watch::channel(None);
 385        let delegate = AgentServerDelegate::new(
 386            project.read(cx).agent_server_store().clone(),
 387            project.clone(),
 388            Some(status_tx),
 389            Some(new_version_available_tx),
 390        );
 391
 392        let connect_task = agent.connect(root_dir.as_deref(), delegate, cx);
 393        let load_task = cx.spawn_in(window, async move |this, cx| {
 394            let connection = match connect_task.await {
 395                Ok((connection, login)) => {
 396                    this.update(cx, |this, _| this.login = login).ok();
 397                    connection
 398                }
 399                Err(err) => {
 400                    this.update_in(cx, |this, window, cx| {
 401                        if err.downcast_ref::<LoadError>().is_some() {
 402                            this.handle_load_error(err, window, cx);
 403                        } else if let Some(active) = this.as_active_thread() {
 404                            active.update(cx, |active, cx| active.handle_any_thread_error(err, cx));
 405                        }
 406                        cx.notify();
 407                    })
 408                    .log_err();
 409                    return;
 410                }
 411            };
 412
 413            telemetry::event!("Agent Thread Started", agent = connection.telemetry_id());
 414
 415            let mut resumed_without_history = false;
 416            let result = if let Some(resume) = resume_thread.clone() {
 417                cx.update(|_, cx| {
 418                    let session_cwd = resume
 419                        .cwd
 420                        .clone()
 421                        .unwrap_or_else(|| fallback_cwd.as_ref().to_path_buf());
 422                    if connection.supports_load_session(cx) {
 423                        connection.clone().load_session(
 424                            resume,
 425                            project.clone(),
 426                            session_cwd.as_path(),
 427                            cx,
 428                        )
 429                    } else if connection.supports_resume_session(cx) {
 430                        resumed_without_history = true;
 431                        connection.clone().resume_session(
 432                            resume,
 433                            project.clone(),
 434                            session_cwd.as_path(),
 435                            cx,
 436                        )
 437                    } else {
 438                        Task::ready(Err(anyhow!(LoadError::Other(
 439                            "Loading or resuming sessions is not supported by this agent.".into()
 440                        ))))
 441                    }
 442                })
 443                .log_err()
 444            } else {
 445                cx.update(|_, cx| {
 446                    connection
 447                        .clone()
 448                        .new_thread(project.clone(), fallback_cwd.as_ref(), cx)
 449                })
 450                .log_err()
 451            };
 452
 453            let Some(result) = result else {
 454                return;
 455            };
 456
 457            let result = match result.await {
 458                Err(e) => match e.downcast::<acp_thread::AuthRequired>() {
 459                    Ok(err) => {
 460                        cx.update(|window, cx| {
 461                            Self::handle_auth_required(this, err, agent.name(), window, cx)
 462                        })
 463                        .log_err();
 464                        return;
 465                    }
 466                    Err(err) => Err(err),
 467                },
 468                Ok(thread) => Ok(thread),
 469            };
 470
 471            this.update_in(cx, |this, window, cx| {
 472                match result {
 473                    Ok(thread) => {
 474                        let action_log = thread.read(cx).action_log().clone();
 475
 476                        prompt_capabilities.replace(thread.read(cx).prompt_capabilities());
 477
 478                        let entry_view_state = cx.new(|_| {
 479                            EntryViewState::new(
 480                                this.workspace.clone(),
 481                                this.project.downgrade(),
 482                                this.thread_store.clone(),
 483                                this.history.downgrade(),
 484                                this.prompt_store.clone(),
 485                                prompt_capabilities.clone(),
 486                                available_commands.clone(),
 487                                this.agent.name(),
 488                            )
 489                        });
 490
 491                        let count = thread.read(cx).entries().len();
 492                        let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0));
 493                        entry_view_state.update(cx, |view_state, cx| {
 494                            for ix in 0..count {
 495                                view_state.sync_entry(ix, &thread, window, cx);
 496                            }
 497                            list_state.splice_focusable(
 498                                0..0,
 499                                (0..count).map(|ix| view_state.entry(ix)?.focus_handle(cx)),
 500                            );
 501                        });
 502
 503                        AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
 504
 505                        let connection = thread.read(cx).connection().clone();
 506                        let session_id = thread.read(cx).session_id().clone();
 507                        let session_list = if connection.supports_session_history(cx) {
 508                            connection.session_list(cx)
 509                        } else {
 510                            None
 511                        };
 512                        this.history.update(cx, |history, cx| {
 513                            history.set_session_list(session_list, cx);
 514                        });
 515
 516                        // Check for config options first
 517                        // Config options take precedence over legacy mode/model selectors
 518                        // (feature flag gating happens at the data layer)
 519                        let config_options_provider =
 520                            connection.session_config_options(&session_id, cx);
 521
 522                        let config_options_view;
 523                        let mode_selector;
 524                        let model_selector;
 525                        if let Some(config_options) = config_options_provider {
 526                            // Use config options - don't create mode_selector or model_selector
 527                            let agent_server = this.agent.clone();
 528                            let fs = this.project.read(cx).fs().clone();
 529                            config_options_view = Some(cx.new(|cx| {
 530                                ConfigOptionsView::new(config_options, agent_server, fs, window, cx)
 531                            }));
 532                            model_selector = None;
 533                            mode_selector = None;
 534                        } else {
 535                            // Fall back to legacy mode/model selectors
 536                            config_options_view = None;
 537                            model_selector =
 538                                connection.model_selector(&session_id).map(|selector| {
 539                                    let agent_server = this.agent.clone();
 540                                    let fs = this.project.read(cx).fs().clone();
 541                                    cx.new(|cx| {
 542                                        AcpModelSelectorPopover::new(
 543                                            selector,
 544                                            agent_server,
 545                                            fs,
 546                                            PopoverMenuHandle::default(),
 547                                            this.focus_handle(cx),
 548                                            window,
 549                                            cx,
 550                                        )
 551                                    })
 552                                });
 553
 554                            mode_selector =
 555                                connection
 556                                    .session_modes(&session_id, cx)
 557                                    .map(|session_modes| {
 558                                        let fs = this.project.read(cx).fs().clone();
 559                                        let focus_handle = this.focus_handle(cx);
 560                                        cx.new(|_cx| {
 561                                            ModeSelector::new(
 562                                                session_modes,
 563                                                this.agent.clone(),
 564                                                fs,
 565                                                focus_handle,
 566                                            )
 567                                        })
 568                                    });
 569                        }
 570
 571                        let mut subscriptions = vec![
 572                            cx.subscribe_in(&thread, window, Self::handle_thread_event),
 573                            cx.observe(&action_log, |_, _, cx| cx.notify()),
 574                            // cx.subscribe_in(
 575                            //     &entry_view_state,
 576                            //     window,
 577                            //     Self::handle_entry_view_event,
 578                            // ),
 579                        ];
 580
 581                        let title_editor =
 582                            if thread.update(cx, |thread, cx| thread.can_set_title(cx)) {
 583                                let editor = cx.new(|cx| {
 584                                    let mut editor = Editor::single_line(window, cx);
 585                                    editor.set_text(thread.read(cx).title(), window, cx);
 586                                    editor
 587                                });
 588                                subscriptions.push(cx.subscribe_in(
 589                                    &editor,
 590                                    window,
 591                                    Self::handle_title_editor_event,
 592                                ));
 593                                Some(editor)
 594                            } else {
 595                                None
 596                            };
 597
 598                        let profile_selector: Option<Rc<agent::NativeAgentConnection>> =
 599                            connection.clone().downcast();
 600                        let profile_selector = profile_selector
 601                            .and_then(|native_connection| native_connection.thread(&session_id, cx))
 602                            .map(|native_thread| {
 603                                cx.new(|cx| {
 604                                    ProfileSelector::new(
 605                                        <dyn Fs>::global(cx),
 606                                        Arc::new(native_thread),
 607                                        this.focus_handle(cx),
 608                                        cx,
 609                                    )
 610                                })
 611                            });
 612
 613                        let agent_display_name = this
 614                            .agent_server_store
 615                            .read(cx)
 616                            .agent_display_name(&ExternalAgentServerName(agent.name()))
 617                            .unwrap_or_else(|| agent.name());
 618
 619                        let weak = cx.weak_entity();
 620                        let current = cx.new(|cx| {
 621                            AcpThreadView::new(
 622                                thread,
 623                                this.login.clone(),
 624                                weak,
 625                                agent.name(),
 626                                agent_display_name,
 627                                workspace.clone(),
 628                                entry_view_state,
 629                                title_editor,
 630                                config_options_view,
 631                                mode_selector,
 632                                model_selector,
 633                                profile_selector,
 634                                list_state,
 635                                prompt_capabilities,
 636                                available_commands,
 637                                resumed_without_history,
 638                                resume_thread.clone(),
 639                                project.downgrade(),
 640                                this.thread_store.clone(),
 641                                this.history.clone(),
 642                                this.prompt_store.clone(),
 643                                initial_content,
 644                                subscriptions,
 645                                window,
 646                                cx,
 647                            )
 648                        });
 649
 650                        if this.focus_handle.contains_focused(window, cx) {
 651                            current
 652                                .read(cx)
 653                                .message_editor
 654                                .focus_handle(cx)
 655                                .focus(window, cx);
 656                        }
 657
 658                        this.server_state = ServerState::Connected(ConnectedServerState {
 659                            connection,
 660                            auth_state: AuthState::Ok,
 661                            current,
 662                        });
 663
 664                        cx.notify();
 665                    }
 666                    Err(err) => {
 667                        this.handle_load_error(err, window, cx);
 668                    }
 669                };
 670            })
 671            .log_err();
 672        });
 673
 674        cx.spawn(async move |this, cx| {
 675            while let Ok(new_version) = new_version_available_rx.recv().await {
 676                if let Some(new_version) = new_version {
 677                    this.update(cx, |this, cx| {
 678                        if let Some(thread) = this.as_active_thread() {
 679                            thread.update(cx, |thread, _cx| {
 680                                thread.new_server_version_available = Some(new_version.into());
 681                            });
 682                        }
 683                        cx.notify();
 684                    })
 685                    .ok();
 686                }
 687            }
 688        })
 689        .detach();
 690
 691        let loading_view = cx.new(|cx| {
 692            let update_title_task = cx.spawn(async move |this, cx| {
 693                loop {
 694                    let status = status_rx.recv().await?;
 695                    this.update(cx, |this: &mut LoadingView, cx| {
 696                        this.title = status;
 697                        cx.notify();
 698                    })?;
 699                }
 700            });
 701
 702            LoadingView {
 703                title: "Loading…".into(),
 704                _load_task: load_task,
 705                _update_title_task: update_title_task,
 706            }
 707        });
 708
 709        ServerState::Loading(loading_view)
 710    }
 711
 712    fn handle_auth_required(
 713        this: WeakEntity<Self>,
 714        err: AuthRequired,
 715        agent_name: SharedString,
 716        window: &mut Window,
 717        cx: &mut App,
 718    ) {
 719        let (configuration_view, subscription) = if let Some(provider_id) = &err.provider_id {
 720            let registry = LanguageModelRegistry::global(cx);
 721
 722            let sub = window.subscribe(&registry, cx, {
 723                let provider_id = provider_id.clone();
 724                let this = this.clone();
 725                move |_, ev, window, cx| {
 726                    if let language_model::Event::ProviderStateChanged(updated_provider_id) = &ev
 727                        && &provider_id == updated_provider_id
 728                        && LanguageModelRegistry::global(cx)
 729                            .read(cx)
 730                            .provider(&provider_id)
 731                            .map_or(false, |provider| provider.is_authenticated(cx))
 732                    {
 733                        this.update(cx, |this, cx| {
 734                            this.reset(window, cx);
 735                        })
 736                        .ok();
 737                    }
 738                }
 739            });
 740
 741            let view = registry.read(cx).provider(&provider_id).map(|provider| {
 742                provider.configuration_view(
 743                    language_model::ConfigurationViewTargetAgent::Other(agent_name),
 744                    window,
 745                    cx,
 746                )
 747            });
 748
 749            (view, Some(sub))
 750        } else {
 751            (None, None)
 752        };
 753
 754        this.update(cx, |this, cx| {
 755            if let Some(connected) = this.as_connected_mut() {
 756                let description = err
 757                    .description
 758                    .map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx)));
 759
 760                connected.auth_state = AuthState::Unauthenticated {
 761                    pending_auth_method: None,
 762                    configuration_view,
 763                    description,
 764                    _subscription: subscription,
 765                };
 766                if connected
 767                    .current
 768                    .read(cx)
 769                    .message_editor
 770                    .focus_handle(cx)
 771                    .is_focused(window)
 772                {
 773                    this.focus_handle.focus(window, cx)
 774                }
 775            }
 776            cx.notify();
 777        })
 778        .ok();
 779    }
 780
 781    fn handle_load_error(
 782        &mut self,
 783        err: anyhow::Error,
 784        window: &mut Window,
 785        cx: &mut Context<Self>,
 786    ) {
 787        match &self.server_state {
 788            ServerState::Connected(connected) => {
 789                if connected
 790                    .current
 791                    .read(cx)
 792                    .message_editor
 793                    .focus_handle(cx)
 794                    .is_focused(window)
 795                {
 796                    self.focus_handle.focus(window, cx)
 797                }
 798            }
 799            _ => {}
 800        }
 801        let load_error = if let Some(load_err) = err.downcast_ref::<LoadError>() {
 802            load_err.clone()
 803        } else {
 804            LoadError::Other(format!("{:#}", err).into())
 805        };
 806        self.emit_load_error_telemetry(&load_error);
 807        self.server_state = ServerState::LoadError(load_error);
 808        cx.notify();
 809    }
 810
 811    fn handle_agent_servers_updated(
 812        &mut self,
 813        _agent_server_store: &Entity<project::AgentServerStore>,
 814        _event: &project::AgentServersUpdated,
 815        window: &mut Window,
 816        cx: &mut Context<Self>,
 817    ) {
 818        // If we're in a LoadError state OR have a thread_error set (which can happen
 819        // when agent.connect() fails during loading), retry loading the thread.
 820        // This handles the case where a thread is restored before authentication completes.
 821        let should_retry = match &self.server_state {
 822            ServerState::Loading(_) => false,
 823            ServerState::LoadError(_) => true,
 824            ServerState::Connected(connected) => {
 825                connected.auth_state.is_ok() && connected.has_thread_error(cx)
 826            }
 827        };
 828
 829        if should_retry {
 830            if let Some(active) = self.as_active_thread() {
 831                active.update(cx, |active, cx| {
 832                    active.clear_thread_error(cx);
 833                });
 834            }
 835            self.reset(window, cx);
 836        }
 837    }
 838
 839    pub fn workspace(&self) -> &WeakEntity<Workspace> {
 840        &self.workspace
 841    }
 842
 843    pub fn title(&self, cx: &App) -> SharedString {
 844        match &self.server_state {
 845            ServerState::Connected(_) => "New Thread".into(),
 846            ServerState::Loading(loading_view) => loading_view.read(cx).title.clone(),
 847            ServerState::LoadError(error) => match error {
 848                LoadError::Unsupported { .. } => format!("Upgrade {}", self.agent.name()).into(),
 849                LoadError::FailedToInstall(_) => {
 850                    format!("Failed to Install {}", self.agent.name()).into()
 851                }
 852                LoadError::Exited { .. } => format!("{} Exited", self.agent.name()).into(),
 853                LoadError::Other(_) => format!("Error Loading {}", self.agent.name()).into(),
 854            },
 855        }
 856    }
 857
 858    pub fn cancel_generation(&mut self, cx: &mut Context<Self>) {
 859        if let Some(active) = self.as_active_thread() {
 860            active.update(cx, |active, cx| {
 861                active.cancel_generation(cx);
 862            });
 863        }
 864    }
 865
 866    pub fn handle_title_editor_event(
 867        &mut self,
 868        title_editor: &Entity<Editor>,
 869        event: &EditorEvent,
 870        window: &mut Window,
 871        cx: &mut Context<Self>,
 872    ) {
 873        if let Some(active) = self.as_active_thread() {
 874            active.update(cx, |active, cx| {
 875                active.handle_title_editor_event(title_editor, event, window, cx);
 876            });
 877        }
 878    }
 879
 880    pub fn is_loading(&self) -> bool {
 881        matches!(self.server_state, ServerState::Loading { .. })
 882    }
 883
 884    fn update_turn_tokens(&mut self, cx: &mut Context<Self>) {
 885        if let Some(active) = self.as_active_thread() {
 886            active.update(cx, |active, cx| {
 887                active.update_turn_tokens(cx);
 888            });
 889        }
 890    }
 891
 892    fn send_queued_message_at_index(
 893        &mut self,
 894        index: usize,
 895        is_send_now: bool,
 896        window: &mut Window,
 897        cx: &mut Context<Self>,
 898    ) {
 899        if let Some(active) = self.as_active_thread() {
 900            active.update(cx, |active, cx| {
 901                active.send_queued_message_at_index(index, is_send_now, window, cx);
 902            });
 903        }
 904    }
 905
 906    fn handle_thread_event(
 907        &mut self,
 908        thread: &Entity<AcpThread>,
 909        event: &AcpThreadEvent,
 910        window: &mut Window,
 911        cx: &mut Context<Self>,
 912    ) {
 913        match event {
 914            AcpThreadEvent::NewEntry => {
 915                let len = thread.read(cx).entries().len();
 916                let index = len - 1;
 917                if let Some(active) = self.as_active_thread() {
 918                    let entry_view_state = active.read(cx).entry_view_state.clone();
 919                    let list_state = active.read(cx).list_state.clone();
 920                    entry_view_state.update(cx, |view_state, cx| {
 921                        view_state.sync_entry(index, thread, window, cx);
 922                        list_state.splice_focusable(
 923                            index..index,
 924                            [view_state
 925                                .entry(index)
 926                                .and_then(|entry| entry.focus_handle(cx))],
 927                        );
 928                    });
 929                }
 930            }
 931            AcpThreadEvent::EntryUpdated(index) => {
 932                if let Some(entry_view_state) = self
 933                    .as_active_thread()
 934                    .map(|active| active.read(cx).entry_view_state.clone())
 935                {
 936                    entry_view_state.update(cx, |view_state, cx| {
 937                        view_state.sync_entry(*index, thread, window, cx)
 938                    });
 939                }
 940            }
 941            AcpThreadEvent::EntriesRemoved(range) => {
 942                if let Some(active) = self.as_active_thread() {
 943                    let entry_view_state = active.read(cx).entry_view_state.clone();
 944                    let list_state = active.read(cx).list_state.clone();
 945                    entry_view_state.update(cx, |view_state, _cx| view_state.remove(range.clone()));
 946                    list_state.splice(range.clone(), 0);
 947                }
 948            }
 949            AcpThreadEvent::ToolAuthorizationRequired => {
 950                self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx);
 951            }
 952            AcpThreadEvent::Retry(retry) => {
 953                if let Some(active) = self.as_active_thread() {
 954                    active.update(cx, |active, _cx| {
 955                        active.thread_retry_status = Some(retry.clone());
 956                    });
 957                }
 958            }
 959            AcpThreadEvent::Stopped => {
 960                if let Some(active) = self.as_active_thread() {
 961                    active.update(cx, |active, _cx| {
 962                        active.thread_retry_status.take();
 963                    });
 964                }
 965                let used_tools = thread.read(cx).used_tools_since_last_user_message();
 966                self.notify_with_sound(
 967                    if used_tools {
 968                        "Finished running tools"
 969                    } else {
 970                        "New message"
 971                    },
 972                    IconName::ZedAssistant,
 973                    window,
 974                    cx,
 975                );
 976
 977                let should_send_queued = if let Some(active) = self.as_active_thread() {
 978                    active.update(cx, |active, cx| {
 979                        if active.skip_queue_processing_count > 0 {
 980                            active.skip_queue_processing_count -= 1;
 981                            false
 982                        } else if active.user_interrupted_generation {
 983                            // Manual interruption: don't auto-process queue.
 984                            // Reset the flag so future completions can process normally.
 985                            active.user_interrupted_generation = false;
 986                            false
 987                        } else {
 988                            let has_queued = !active.local_queued_messages.is_empty();
 989                            // Don't auto-send if the first message editor is currently focused
 990                            let is_first_editor_focused = active
 991                                .queued_message_editors
 992                                .first()
 993                                .is_some_and(|editor| editor.focus_handle(cx).is_focused(window));
 994                            has_queued && !is_first_editor_focused
 995                        }
 996                    })
 997                } else {
 998                    false
 999                };
1000                if should_send_queued {
1001                    self.send_queued_message_at_index(0, false, window, cx);
1002                }
1003
1004                self.history.update(cx, |history, cx| history.refresh(cx));
1005            }
1006            AcpThreadEvent::Refusal => {
1007                let error = ThreadError::Refusal;
1008                if let Some(active) = self.as_active_thread() {
1009                    active.update(cx, |active, cx| {
1010                        active.handle_thread_error(error, cx);
1011                        active.thread_retry_status.take();
1012                    });
1013                }
1014                let model_or_agent_name = self.current_model_name(cx);
1015                let notification_message =
1016                    format!("{} refused to respond to this request", model_or_agent_name);
1017                self.notify_with_sound(&notification_message, IconName::Warning, window, cx);
1018            }
1019            AcpThreadEvent::Error => {
1020                if let Some(active) = self.as_active_thread() {
1021                    active.update(cx, |active, _cx| {
1022                        active.thread_retry_status.take();
1023                    });
1024                }
1025                self.notify_with_sound(
1026                    "Agent stopped due to an error",
1027                    IconName::Warning,
1028                    window,
1029                    cx,
1030                );
1031            }
1032            AcpThreadEvent::LoadError(error) => {
1033                match &self.server_state {
1034                    ServerState::Connected(connected) => {
1035                        if connected
1036                            .current
1037                            .read(cx)
1038                            .message_editor
1039                            .focus_handle(cx)
1040                            .is_focused(window)
1041                        {
1042                            self.focus_handle.focus(window, cx)
1043                        }
1044                    }
1045                    _ => {}
1046                }
1047                self.server_state = ServerState::LoadError(error.clone());
1048            }
1049            AcpThreadEvent::TitleUpdated => {
1050                let title = thread.read(cx).title();
1051                if let Some(title_editor) = self
1052                    .as_active_thread()
1053                    .and_then(|active| active.read(cx).title_editor.clone())
1054                {
1055                    title_editor.update(cx, |editor, cx| {
1056                        if editor.text(cx) != title {
1057                            editor.set_text(title, window, cx);
1058                        }
1059                    });
1060                }
1061                self.history.update(cx, |history, cx| history.refresh(cx));
1062            }
1063            AcpThreadEvent::PromptCapabilitiesUpdated => {
1064                if let Some(active) = self.as_active_thread() {
1065                    active.update(cx, |active, _cx| {
1066                        active
1067                            .prompt_capabilities
1068                            .replace(thread.read(_cx).prompt_capabilities());
1069                    });
1070                }
1071            }
1072            AcpThreadEvent::TokenUsageUpdated => {
1073                self.update_turn_tokens(cx);
1074                self.emit_token_limit_telemetry_if_needed(thread, cx);
1075            }
1076            AcpThreadEvent::AvailableCommandsUpdated(available_commands) => {
1077                let mut available_commands = available_commands.clone();
1078
1079                if thread
1080                    .read(cx)
1081                    .connection()
1082                    .auth_methods()
1083                    .iter()
1084                    .any(|method| method.id.0.as_ref() == "claude-login")
1085                {
1086                    available_commands.push(acp::AvailableCommand::new("login", "Authenticate"));
1087                    available_commands.push(acp::AvailableCommand::new("logout", "Authenticate"));
1088                }
1089
1090                let has_commands = !available_commands.is_empty();
1091                if let Some(active) = self.as_active_thread() {
1092                    active.update(cx, |active, _cx| {
1093                        active.available_commands.replace(available_commands);
1094                    });
1095                }
1096
1097                let agent_display_name = self
1098                    .agent_server_store
1099                    .read(cx)
1100                    .agent_display_name(&ExternalAgentServerName(self.agent.name()))
1101                    .unwrap_or_else(|| self.agent.name());
1102
1103                if let Some(active) = self.as_active_thread() {
1104                    let new_placeholder =
1105                        placeholder_text(agent_display_name.as_ref(), has_commands);
1106                    active.update(cx, |active, cx| {
1107                        active.message_editor.update(cx, |editor, cx| {
1108                            editor.set_placeholder_text(&new_placeholder, window, cx);
1109                        });
1110                    });
1111                }
1112            }
1113            AcpThreadEvent::ModeUpdated(_mode) => {
1114                // The connection keeps track of the mode
1115                cx.notify();
1116            }
1117            AcpThreadEvent::ConfigOptionsUpdated(_) => {
1118                // The watch task in ConfigOptionsView handles rebuilding selectors
1119                cx.notify();
1120            }
1121        }
1122        cx.notify();
1123    }
1124
1125    fn authenticate(
1126        &mut self,
1127        method: acp::AuthMethodId,
1128        window: &mut Window,
1129        cx: &mut Context<Self>,
1130    ) {
1131        let Some(connected) = self.as_connected_mut() else {
1132            return;
1133        };
1134        let connection = connected.connection.clone();
1135
1136        let AuthState::Unauthenticated {
1137            configuration_view,
1138            pending_auth_method,
1139            ..
1140        } = &mut connected.auth_state
1141        else {
1142            return;
1143        };
1144
1145        let agent_telemetry_id = connection.telemetry_id();
1146
1147        // Check for the experimental "terminal-auth" _meta field
1148        let auth_method = connection.auth_methods().iter().find(|m| m.id == method);
1149
1150        if let Some(terminal_auth) = auth_method
1151            .and_then(|a| a.meta.as_ref())
1152            .and_then(|m| m.get("terminal-auth"))
1153        {
1154            // Extract terminal auth details from meta
1155            if let (Some(command), Some(label)) = (
1156                terminal_auth.get("command").and_then(|v| v.as_str()),
1157                terminal_auth.get("label").and_then(|v| v.as_str()),
1158            ) {
1159                let args = terminal_auth
1160                    .get("args")
1161                    .and_then(|v| v.as_array())
1162                    .map(|arr| {
1163                        arr.iter()
1164                            .filter_map(|v| v.as_str().map(String::from))
1165                            .collect()
1166                    })
1167                    .unwrap_or_default();
1168
1169                let env = terminal_auth
1170                    .get("env")
1171                    .and_then(|v| v.as_object())
1172                    .map(|obj| {
1173                        obj.iter()
1174                            .filter_map(|(k, v)| v.as_str().map(|val| (k.clone(), val.to_string())))
1175                            .collect::<HashMap<String, String>>()
1176                    })
1177                    .unwrap_or_default();
1178
1179                // Run SpawnInTerminal in the same dir as the ACP server
1180                let cwd = connected
1181                    .connection
1182                    .clone()
1183                    .downcast::<agent_servers::AcpConnection>()
1184                    .map(|acp_conn| acp_conn.root_dir().to_path_buf());
1185
1186                // Build SpawnInTerminal from _meta
1187                let login = task::SpawnInTerminal {
1188                    id: task::TaskId(format!("external-agent-{}-login", label)),
1189                    full_label: label.to_string(),
1190                    label: label.to_string(),
1191                    command: Some(command.to_string()),
1192                    args,
1193                    command_label: label.to_string(),
1194                    cwd,
1195                    env,
1196                    use_new_terminal: true,
1197                    allow_concurrent_runs: true,
1198                    hide: task::HideStrategy::Always,
1199                    ..Default::default()
1200                };
1201
1202                configuration_view.take();
1203                pending_auth_method.replace(method.clone());
1204
1205                if let Some(workspace) = self.workspace.upgrade() {
1206                    let project = self.project.clone();
1207                    let authenticate = Self::spawn_external_agent_login(
1208                        login,
1209                        workspace,
1210                        project,
1211                        method.clone(),
1212                        false,
1213                        window,
1214                        cx,
1215                    );
1216                    cx.notify();
1217                    self.auth_task = Some(cx.spawn_in(window, {
1218                        async move |this, cx| {
1219                            let result = authenticate.await;
1220
1221                            match &result {
1222                                Ok(_) => telemetry::event!(
1223                                    "Authenticate Agent Succeeded",
1224                                    agent = agent_telemetry_id
1225                                ),
1226                                Err(_) => {
1227                                    telemetry::event!(
1228                                        "Authenticate Agent Failed",
1229                                        agent = agent_telemetry_id,
1230                                    )
1231                                }
1232                            }
1233
1234                            this.update_in(cx, |this, window, cx| {
1235                                if let Err(err) = result {
1236                                    if let Some(ConnectedServerState {
1237                                        auth_state:
1238                                            AuthState::Unauthenticated {
1239                                                pending_auth_method,
1240                                                ..
1241                                            },
1242                                        ..
1243                                    }) = this.as_connected_mut()
1244                                    {
1245                                        pending_auth_method.take();
1246                                    }
1247                                    if let Some(active) = this.as_active_thread() {
1248                                        active.update(cx, |active, cx| {
1249                                            active.handle_any_thread_error(err, cx);
1250                                        })
1251                                    }
1252                                } else {
1253                                    this.reset(window, cx);
1254                                }
1255                                this.auth_task.take()
1256                            })
1257                            .ok();
1258                        }
1259                    }));
1260                }
1261                return;
1262            }
1263        }
1264
1265        if method.0.as_ref() == "gemini-api-key" {
1266            let registry = LanguageModelRegistry::global(cx);
1267            let provider = registry
1268                .read(cx)
1269                .provider(&language_model::GOOGLE_PROVIDER_ID)
1270                .unwrap();
1271            if !provider.is_authenticated(cx) {
1272                let this = cx.weak_entity();
1273                let agent_name = self.agent.name();
1274                window.defer(cx, |window, cx| {
1275                    Self::handle_auth_required(
1276                        this,
1277                        AuthRequired {
1278                            description: Some("GEMINI_API_KEY must be set".to_owned()),
1279                            provider_id: Some(language_model::GOOGLE_PROVIDER_ID),
1280                        },
1281                        agent_name,
1282                        window,
1283                        cx,
1284                    );
1285                });
1286                return;
1287            }
1288        } else if method.0.as_ref() == "vertex-ai"
1289            && std::env::var("GOOGLE_API_KEY").is_err()
1290            && (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
1291                || (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()))
1292        {
1293            let this = cx.weak_entity();
1294            let agent_name = self.agent.name();
1295
1296            window.defer(cx, |window, cx| {
1297                    Self::handle_auth_required(
1298                        this,
1299                        AuthRequired {
1300                            description: Some(
1301                                "GOOGLE_API_KEY must be set in the environment to use Vertex AI authentication for Gemini CLI. Please export it and restart Zed."
1302                                    .to_owned(),
1303                            ),
1304                            provider_id: None,
1305                        },
1306                        agent_name,
1307                        window,
1308                        cx,
1309                    )
1310                });
1311            return;
1312        }
1313
1314        configuration_view.take();
1315        pending_auth_method.replace(method.clone());
1316        let authenticate = if let Some(login) = self.login.clone() {
1317            if let Some(workspace) = self.workspace.upgrade() {
1318                let project = self.project.clone();
1319                Self::spawn_external_agent_login(
1320                    login,
1321                    workspace,
1322                    project,
1323                    method.clone(),
1324                    false,
1325                    window,
1326                    cx,
1327                )
1328            } else {
1329                Task::ready(Ok(()))
1330            }
1331        } else {
1332            connection.authenticate(method, cx)
1333        };
1334        cx.notify();
1335        self.auth_task = Some(cx.spawn_in(window, {
1336            async move |this, cx| {
1337                let result = authenticate.await;
1338
1339                match &result {
1340                    Ok(_) => telemetry::event!(
1341                        "Authenticate Agent Succeeded",
1342                        agent = agent_telemetry_id
1343                    ),
1344                    Err(_) => {
1345                        telemetry::event!("Authenticate Agent Failed", agent = agent_telemetry_id,)
1346                    }
1347                }
1348
1349                this.update_in(cx, |this, window, cx| {
1350                    if let Err(err) = result {
1351                        if let Some(ConnectedServerState {
1352                            auth_state:
1353                                AuthState::Unauthenticated {
1354                                    pending_auth_method,
1355                                    ..
1356                                },
1357                            ..
1358                        }) = this.as_connected_mut()
1359                        {
1360                            pending_auth_method.take();
1361                        }
1362                        if let Some(active) = this.as_active_thread() {
1363                            active.update(cx, |active, cx| active.handle_any_thread_error(err, cx));
1364                        }
1365                    } else {
1366                        this.reset(window, cx);
1367                    }
1368                    this.auth_task.take()
1369                })
1370                .ok();
1371            }
1372        }));
1373    }
1374
1375    fn spawn_external_agent_login(
1376        login: task::SpawnInTerminal,
1377        workspace: Entity<Workspace>,
1378        project: Entity<Project>,
1379        method: acp::AuthMethodId,
1380        previous_attempt: bool,
1381        window: &mut Window,
1382        cx: &mut App,
1383    ) -> Task<Result<()>> {
1384        let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
1385            return Task::ready(Ok(()));
1386        };
1387
1388        window.spawn(cx, async move |cx| {
1389            let mut task = login.clone();
1390            if let Some(cmd) = &task.command {
1391                // Have "node" command use Zed's managed Node runtime by default
1392                if cmd == "node" {
1393                    let resolved_node_runtime = project
1394                        .update(cx, |project, cx| {
1395                            let agent_server_store = project.agent_server_store().clone();
1396                            agent_server_store.update(cx, |store, cx| {
1397                                store.node_runtime().map(|node_runtime| {
1398                                    cx.background_spawn(async move {
1399                                        node_runtime.binary_path().await
1400                                    })
1401                                })
1402                            })
1403                        });
1404
1405                    if let Some(resolve_task) = resolved_node_runtime {
1406                        if let Ok(node_path) = resolve_task.await {
1407                            task.command = Some(node_path.to_string_lossy().to_string());
1408                        }
1409                    }
1410                }
1411            }
1412            task.shell = task::Shell::WithArguments {
1413                program: task.command.take().expect("login command should be set"),
1414                args: std::mem::take(&mut task.args),
1415                title_override: None
1416            };
1417            task.full_label = task.label.clone();
1418            task.id = task::TaskId(format!("external-agent-{}-login", task.label));
1419            task.command_label = task.label.clone();
1420            task.use_new_terminal = true;
1421            task.allow_concurrent_runs = true;
1422            task.hide = task::HideStrategy::Always;
1423
1424            let terminal = terminal_panel
1425                .update_in(cx, |terminal_panel, window, cx| {
1426                    terminal_panel.spawn_task(&task, window, cx)
1427                })?
1428                .await?;
1429
1430            let success_patterns = match method.0.as_ref() {
1431                "claude-login" | "spawn-gemini-cli" => vec![
1432                    "Login successful".to_string(),
1433                    "Type your message".to_string(),
1434                ],
1435                _ => Vec::new(),
1436            };
1437            if success_patterns.is_empty() {
1438                // No success patterns specified: wait for the process to exit and check exit code
1439                let exit_status = terminal
1440                    .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
1441                    .await;
1442
1443                match exit_status {
1444                    Some(status) if status.success() => Ok(()),
1445                    Some(status) => Err(anyhow!(
1446                        "Login command failed with exit code: {:?}",
1447                        status.code()
1448                    )),
1449                    None => Err(anyhow!("Login command terminated without exit status")),
1450                }
1451            } else {
1452                // Look for specific output patterns to detect successful login
1453                let mut exit_status = terminal
1454                    .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
1455                    .fuse();
1456
1457                let logged_in = cx
1458                    .spawn({
1459                        let terminal = terminal.clone();
1460                        async move |cx| {
1461                            loop {
1462                                cx.background_executor().timer(Duration::from_secs(1)).await;
1463                                let content =
1464                                    terminal.update(cx, |terminal, _cx| terminal.get_content())?;
1465                                if success_patterns.iter().any(|pattern| content.contains(pattern))
1466                                {
1467                                    return anyhow::Ok(());
1468                                }
1469                            }
1470                        }
1471                    })
1472                    .fuse();
1473                futures::pin_mut!(logged_in);
1474                futures::select_biased! {
1475                    result = logged_in => {
1476                        if let Err(e) = result {
1477                            log::error!("{e}");
1478                            return Err(anyhow!("exited before logging in"));
1479                        }
1480                    }
1481                    _ = exit_status => {
1482                        if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server()) && login.label.contains("gemini") {
1483                            return cx.update(|window, cx| Self::spawn_external_agent_login(login, workspace, project.clone(), method, true, window, cx))?.await
1484                        }
1485                        return Err(anyhow!("exited before logging in"));
1486                    }
1487                }
1488                terminal.update(cx, |terminal, _| terminal.kill_active_task())?;
1489                Ok(())
1490            }
1491        })
1492    }
1493
1494    pub fn has_user_submitted_prompt(&self, cx: &App) -> bool {
1495        self.as_active_thread().is_some_and(|active| {
1496            active
1497                .read(cx)
1498                .thread
1499                .read(cx)
1500                .entries()
1501                .iter()
1502                .any(|entry| {
1503                    matches!(
1504                        entry,
1505                        AgentThreadEntry::UserMessage(user_message) if user_message.id.is_some()
1506                    )
1507                })
1508        })
1509    }
1510
1511    fn render_auth_required_state(
1512        &self,
1513        connection: &Rc<dyn AgentConnection>,
1514        description: Option<&Entity<Markdown>>,
1515        configuration_view: Option<&AnyView>,
1516        pending_auth_method: Option<&acp::AuthMethodId>,
1517        window: &mut Window,
1518        cx: &Context<Self>,
1519    ) -> impl IntoElement {
1520        let auth_methods = connection.auth_methods();
1521
1522        let agent_display_name = self
1523            .agent_server_store
1524            .read(cx)
1525            .agent_display_name(&ExternalAgentServerName(self.agent.name()))
1526            .unwrap_or_else(|| self.agent.name());
1527
1528        let show_fallback_description = auth_methods.len() > 1
1529            && configuration_view.is_none()
1530            && description.is_none()
1531            && pending_auth_method.is_none();
1532
1533        let auth_buttons = || {
1534            h_flex().justify_end().flex_wrap().gap_1().children(
1535                connection
1536                    .auth_methods()
1537                    .iter()
1538                    .enumerate()
1539                    .rev()
1540                    .map(|(ix, method)| {
1541                        let (method_id, name) = if self.project.read(cx).is_via_remote_server()
1542                            && method.id.0.as_ref() == "oauth-personal"
1543                            && method.name == "Log in with Google"
1544                        {
1545                            ("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
1546                        } else {
1547                            (method.id.0.clone(), method.name.clone())
1548                        };
1549
1550                        let agent_telemetry_id = connection.telemetry_id();
1551
1552                        Button::new(method_id.clone(), name)
1553                            .label_size(LabelSize::Small)
1554                            .map(|this| {
1555                                if ix == 0 {
1556                                    this.style(ButtonStyle::Tinted(TintColor::Accent))
1557                                } else {
1558                                    this.style(ButtonStyle::Outlined)
1559                                }
1560                            })
1561                            .when_some(method.description.clone(), |this, description| {
1562                                this.tooltip(Tooltip::text(description))
1563                            })
1564                            .on_click({
1565                                cx.listener(move |this, _, window, cx| {
1566                                    telemetry::event!(
1567                                        "Authenticate Agent Started",
1568                                        agent = agent_telemetry_id,
1569                                        method = method_id
1570                                    );
1571
1572                                    this.authenticate(
1573                                        acp::AuthMethodId::new(method_id.clone()),
1574                                        window,
1575                                        cx,
1576                                    )
1577                                })
1578                            })
1579                    }),
1580            )
1581        };
1582
1583        if pending_auth_method.is_some() {
1584            return Callout::new()
1585                .icon(IconName::Info)
1586                .title(format!("Authenticating to {}", agent_display_name))
1587                .actions_slot(
1588                    Icon::new(IconName::ArrowCircle)
1589                        .size(IconSize::Small)
1590                        .color(Color::Muted)
1591                        .with_rotate_animation(2)
1592                        .into_any_element(),
1593                )
1594                .into_any_element();
1595        }
1596
1597        Callout::new()
1598            .icon(IconName::Info)
1599            .title(format!("Authenticate to {}", agent_display_name))
1600            .when(auth_methods.len() == 1, |this| {
1601                this.actions_slot(auth_buttons())
1602            })
1603            .description_slot(
1604                v_flex()
1605                    .text_ui(cx)
1606                    .map(|this| {
1607                        if show_fallback_description {
1608                            this.child(
1609                                Label::new("Choose one of the following authentication options:")
1610                                    .size(LabelSize::Small)
1611                                    .color(Color::Muted),
1612                            )
1613                        } else {
1614                            this.children(
1615                                configuration_view
1616                                    .cloned()
1617                                    .map(|view| div().w_full().child(view)),
1618                            )
1619                            .children(description.map(|desc| {
1620                                self.render_markdown(
1621                                    desc.clone(),
1622                                    MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
1623                                )
1624                            }))
1625                        }
1626                    })
1627                    .when(auth_methods.len() > 1, |this| {
1628                        this.gap_1().child(auth_buttons())
1629                    }),
1630            )
1631            .into_any_element()
1632    }
1633
1634    fn emit_token_limit_telemetry_if_needed(
1635        &mut self,
1636        thread: &Entity<AcpThread>,
1637        cx: &mut Context<Self>,
1638    ) {
1639        let Some(active_thread) = self.as_active_thread() else {
1640            return;
1641        };
1642
1643        let (ratio, agent_telemetry_id, session_id) = {
1644            let thread_data = thread.read(cx);
1645            let Some(token_usage) = thread_data.token_usage() else {
1646                return;
1647            };
1648            (
1649                token_usage.ratio(),
1650                thread_data.connection().telemetry_id(),
1651                thread_data.session_id().clone(),
1652            )
1653        };
1654
1655        let kind = match ratio {
1656            acp_thread::TokenUsageRatio::Normal => {
1657                active_thread.update(cx, |active, _cx| {
1658                    active.last_token_limit_telemetry = None;
1659                });
1660                return;
1661            }
1662            acp_thread::TokenUsageRatio::Warning => "warning",
1663            acp_thread::TokenUsageRatio::Exceeded => "exceeded",
1664        };
1665
1666        let should_skip = active_thread
1667            .read(cx)
1668            .last_token_limit_telemetry
1669            .as_ref()
1670            .is_some_and(|last| *last >= ratio);
1671        if should_skip {
1672            return;
1673        }
1674
1675        active_thread.update(cx, |active, _cx| {
1676            active.last_token_limit_telemetry = Some(ratio);
1677        });
1678
1679        telemetry::event!(
1680            "Agent Token Limit Warning",
1681            agent = agent_telemetry_id,
1682            session_id = session_id,
1683            kind = kind,
1684        );
1685    }
1686
1687    fn emit_load_error_telemetry(&self, error: &LoadError) {
1688        let error_kind = match error {
1689            LoadError::Unsupported { .. } => "unsupported",
1690            LoadError::FailedToInstall(_) => "failed_to_install",
1691            LoadError::Exited { .. } => "exited",
1692            LoadError::Other(_) => "other",
1693        };
1694
1695        let agent_name = self.agent.name();
1696
1697        telemetry::event!(
1698            "Agent Panel Error Shown",
1699            agent = agent_name,
1700            kind = error_kind,
1701            message = error.to_string(),
1702        );
1703    }
1704
1705    fn render_load_error(
1706        &self,
1707        e: &LoadError,
1708        window: &mut Window,
1709        cx: &mut Context<Self>,
1710    ) -> AnyElement {
1711        let (title, message, action_slot): (_, SharedString, _) = match e {
1712            LoadError::Unsupported {
1713                command: path,
1714                current_version,
1715                minimum_version,
1716            } => {
1717                return self.render_unsupported(path, current_version, minimum_version, window, cx);
1718            }
1719            LoadError::FailedToInstall(msg) => (
1720                "Failed to Install",
1721                msg.into(),
1722                Some(self.create_copy_button(msg.to_string()).into_any_element()),
1723            ),
1724            LoadError::Exited { status } => (
1725                "Failed to Launch",
1726                format!("Server exited with status {status}").into(),
1727                None,
1728            ),
1729            LoadError::Other(msg) => (
1730                "Failed to Launch",
1731                msg.into(),
1732                Some(self.create_copy_button(msg.to_string()).into_any_element()),
1733            ),
1734        };
1735
1736        Callout::new()
1737            .severity(Severity::Error)
1738            .icon(IconName::XCircleFilled)
1739            .title(title)
1740            .description(message)
1741            .actions_slot(div().children(action_slot))
1742            .into_any_element()
1743    }
1744
1745    fn render_unsupported(
1746        &self,
1747        path: &SharedString,
1748        version: &SharedString,
1749        minimum_version: &SharedString,
1750        _window: &mut Window,
1751        cx: &mut Context<Self>,
1752    ) -> AnyElement {
1753        let (heading_label, description_label) = (
1754            format!("Upgrade {} to work with Zed", self.agent.name()),
1755            if version.is_empty() {
1756                format!(
1757                    "Currently using {}, which does not report a valid --version",
1758                    path,
1759                )
1760            } else {
1761                format!(
1762                    "Currently using {}, which is only version {} (need at least {minimum_version})",
1763                    path, version
1764                )
1765            },
1766        );
1767
1768        v_flex()
1769            .w_full()
1770            .p_3p5()
1771            .gap_2p5()
1772            .border_t_1()
1773            .border_color(cx.theme().colors().border)
1774            .bg(linear_gradient(
1775                180.,
1776                linear_color_stop(cx.theme().colors().editor_background.opacity(0.4), 4.),
1777                linear_color_stop(cx.theme().status().info_background.opacity(0.), 0.),
1778            ))
1779            .child(
1780                v_flex().gap_0p5().child(Label::new(heading_label)).child(
1781                    Label::new(description_label)
1782                        .size(LabelSize::Small)
1783                        .color(Color::Muted),
1784                ),
1785            )
1786            .into_any_element()
1787    }
1788
1789    pub(crate) fn as_native_connection(
1790        &self,
1791        cx: &App,
1792    ) -> Option<Rc<agent::NativeAgentConnection>> {
1793        let acp_thread = self.as_active_thread()?.read(cx).thread.read(cx);
1794        acp_thread.connection().clone().downcast()
1795    }
1796
1797    pub(crate) fn as_native_thread(&self, cx: &App) -> Option<Entity<agent::Thread>> {
1798        let acp_thread = self.as_active_thread()?.read(cx).thread.read(cx);
1799        self.as_native_connection(cx)?
1800            .thread(acp_thread.session_id(), cx)
1801    }
1802
1803    fn queued_messages_len(&self, cx: &App) -> usize {
1804        self.as_active_thread()
1805            .map(|thread| thread.read(cx).local_queued_messages.len())
1806            .unwrap_or_default()
1807    }
1808
1809    fn update_queued_message(
1810        &mut self,
1811        index: usize,
1812        content: Vec<acp::ContentBlock>,
1813        tracked_buffers: Vec<Entity<Buffer>>,
1814        cx: &mut Context<Self>,
1815    ) -> bool {
1816        match self.as_active_thread() {
1817            Some(thread) => thread.update(cx, |thread, _cx| {
1818                if index < thread.local_queued_messages.len() {
1819                    thread.local_queued_messages[index] = QueuedMessage {
1820                        content,
1821                        tracked_buffers,
1822                    };
1823                    true
1824                } else {
1825                    false
1826                }
1827            }),
1828            None => false,
1829        }
1830    }
1831
1832    fn queued_message_contents(&self, cx: &App) -> Vec<Vec<acp::ContentBlock>> {
1833        match self.as_active_thread() {
1834            None => Vec::new(),
1835            Some(thread) => thread
1836                .read(cx)
1837                .local_queued_messages
1838                .iter()
1839                .map(|q| q.content.clone())
1840                .collect(),
1841        }
1842    }
1843
1844    fn save_queued_message_at_index(&mut self, index: usize, cx: &mut Context<Self>) {
1845        let editor = match self.as_active_thread() {
1846            Some(thread) => thread.read(cx).queued_message_editors.get(index).cloned(),
1847            None => None,
1848        };
1849        let Some(editor) = editor else {
1850            return;
1851        };
1852
1853        let contents_task = editor.update(cx, |editor, cx| editor.contents(false, cx));
1854
1855        cx.spawn(async move |this, cx| {
1856            let Ok((content, tracked_buffers)) = contents_task.await else {
1857                return Ok::<(), anyhow::Error>(());
1858            };
1859
1860            this.update(cx, |this, cx| {
1861                this.update_queued_message(index, content, tracked_buffers, cx);
1862                cx.notify();
1863            })?;
1864
1865            Ok(())
1866        })
1867        .detach_and_log_err(cx);
1868    }
1869
1870    fn sync_queued_message_editors(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1871        let needed_count = self.queued_messages_len(cx);
1872        let queued_messages = self.queued_message_contents(cx);
1873
1874        let agent_name = self.agent.name();
1875        let workspace = self.workspace.clone();
1876        let project = self.project.downgrade();
1877        let history = self.history.downgrade();
1878
1879        let Some(thread) = self.as_active_thread() else {
1880            return;
1881        };
1882        let prompt_capabilities = thread.read(cx).prompt_capabilities.clone();
1883        let available_commands = thread.read(cx).available_commands.clone();
1884
1885        let current_count = thread.read(cx).queued_message_editors.len();
1886        let last_synced = thread.read(cx).last_synced_queue_length;
1887
1888        if current_count == needed_count && needed_count == last_synced {
1889            return;
1890        }
1891
1892        if current_count > needed_count {
1893            thread.update(cx, |thread, _cx| {
1894                thread.queued_message_editors.truncate(needed_count);
1895                thread
1896                    .queued_message_editor_subscriptions
1897                    .truncate(needed_count);
1898            });
1899
1900            let editors = thread.read(cx).queued_message_editors.clone();
1901            for (index, editor) in editors.into_iter().enumerate() {
1902                if let Some(content) = queued_messages.get(index) {
1903                    editor.update(cx, |editor, cx| {
1904                        editor.set_message(content.clone(), window, cx);
1905                    });
1906                }
1907            }
1908        }
1909
1910        while thread.read(cx).queued_message_editors.len() < needed_count {
1911            let index = thread.read(cx).queued_message_editors.len();
1912            let content = queued_messages.get(index).cloned().unwrap_or_default();
1913
1914            let editor = cx.new(|cx| {
1915                let mut editor = MessageEditor::new(
1916                    workspace.clone(),
1917                    project.clone(),
1918                    None,
1919                    history.clone(),
1920                    None,
1921                    prompt_capabilities.clone(),
1922                    available_commands.clone(),
1923                    agent_name.clone(),
1924                    "",
1925                    EditorMode::AutoHeight {
1926                        min_lines: 1,
1927                        max_lines: Some(10),
1928                    },
1929                    window,
1930                    cx,
1931                );
1932                editor.set_message(content, window, cx);
1933                editor
1934            });
1935
1936            let subscription = cx.subscribe_in(
1937                &editor,
1938                window,
1939                move |this, _editor, event, window, cx| match event {
1940                    MessageEditorEvent::LostFocus => {
1941                        this.save_queued_message_at_index(index, cx);
1942                    }
1943                    MessageEditorEvent::Cancel => {
1944                        window.focus(&this.focus_handle(cx), cx);
1945                    }
1946                    MessageEditorEvent::Send => {
1947                        window.focus(&this.focus_handle(cx), cx);
1948                    }
1949                    MessageEditorEvent::SendImmediately => {
1950                        this.send_queued_message_at_index(index, true, window, cx);
1951                    }
1952                    _ => {}
1953                },
1954            );
1955
1956            thread.update(cx, |thread, _cx| {
1957                thread.queued_message_editors.push(editor);
1958                thread
1959                    .queued_message_editor_subscriptions
1960                    .push(subscription);
1961            });
1962        }
1963
1964        if let Some(active) = self.as_active_thread() {
1965            active.update(cx, |active, _cx| {
1966                active.last_synced_queue_length = needed_count;
1967            });
1968        }
1969    }
1970
1971    fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
1972        let workspace = self.workspace.clone();
1973        MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
1974            crate::acp::thread_view::active_thread::open_link(text, &workspace, window, cx);
1975        })
1976    }
1977
1978    fn notify_with_sound(
1979        &mut self,
1980        caption: impl Into<SharedString>,
1981        icon: IconName,
1982        window: &mut Window,
1983        cx: &mut Context<Self>,
1984    ) {
1985        self.play_notification_sound(window, cx);
1986        self.show_notification(caption, icon, window, cx);
1987    }
1988
1989    fn play_notification_sound(&self, window: &Window, cx: &mut App) {
1990        let settings = AgentSettings::get_global(cx);
1991        if settings.play_sound_when_agent_done && !window.is_window_active() {
1992            Audio::play_sound(Sound::AgentDone, cx);
1993        }
1994    }
1995
1996    fn show_notification(
1997        &mut self,
1998        caption: impl Into<SharedString>,
1999        icon: IconName,
2000        window: &mut Window,
2001        cx: &mut Context<Self>,
2002    ) {
2003        if !self.notifications.is_empty() {
2004            return;
2005        }
2006
2007        let settings = AgentSettings::get_global(cx);
2008
2009        let window_is_inactive = !window.is_window_active();
2010        let panel_is_hidden = self
2011            .workspace
2012            .upgrade()
2013            .map(|workspace| AgentPanel::is_hidden(&workspace, cx))
2014            .unwrap_or(true);
2015
2016        let should_notify = window_is_inactive || panel_is_hidden;
2017
2018        if !should_notify {
2019            return;
2020        }
2021
2022        // TODO: Change this once we have title summarization for external agents.
2023        let title = self.agent.name();
2024
2025        match settings.notify_when_agent_waiting {
2026            NotifyWhenAgentWaiting::PrimaryScreen => {
2027                if let Some(primary) = cx.primary_display() {
2028                    self.pop_up(icon, caption.into(), title, window, primary, cx);
2029                }
2030            }
2031            NotifyWhenAgentWaiting::AllScreens => {
2032                let caption = caption.into();
2033                for screen in cx.displays() {
2034                    self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
2035                }
2036            }
2037            NotifyWhenAgentWaiting::Never => {
2038                // Don't show anything
2039            }
2040        }
2041    }
2042
2043    fn pop_up(
2044        &mut self,
2045        icon: IconName,
2046        caption: SharedString,
2047        title: SharedString,
2048        window: &mut Window,
2049        screen: Rc<dyn PlatformDisplay>,
2050        cx: &mut Context<Self>,
2051    ) {
2052        let options = AgentNotification::window_options(screen, cx);
2053
2054        let project_name = self.workspace.upgrade().and_then(|workspace| {
2055            workspace
2056                .read(cx)
2057                .project()
2058                .read(cx)
2059                .visible_worktrees(cx)
2060                .next()
2061                .map(|worktree| worktree.read(cx).root_name_str().to_string())
2062        });
2063
2064        if let Some(screen_window) = cx
2065            .open_window(options, |_window, cx| {
2066                cx.new(|_cx| {
2067                    AgentNotification::new(title.clone(), caption.clone(), icon, project_name)
2068                })
2069            })
2070            .log_err()
2071            && let Some(pop_up) = screen_window.entity(cx).log_err()
2072        {
2073            self.notification_subscriptions
2074                .entry(screen_window)
2075                .or_insert_with(Vec::new)
2076                .push(cx.subscribe_in(&pop_up, window, {
2077                    |this, _, event, window, cx| match event {
2078                        AgentNotificationEvent::Accepted => {
2079                            let handle = window.window_handle();
2080                            cx.activate(true);
2081
2082                            let workspace_handle = this.workspace.clone();
2083
2084                            // If there are multiple Zed windows, activate the correct one.
2085                            cx.defer(move |cx| {
2086                                handle
2087                                    .update(cx, |_view, window, _cx| {
2088                                        window.activate_window();
2089
2090                                        if let Some(workspace) = workspace_handle.upgrade() {
2091                                            workspace.update(_cx, |workspace, cx| {
2092                                                workspace.focus_panel::<AgentPanel>(window, cx);
2093                                            });
2094                                        }
2095                                    })
2096                                    .log_err();
2097                            });
2098
2099                            this.dismiss_notifications(cx);
2100                        }
2101                        AgentNotificationEvent::Dismissed => {
2102                            this.dismiss_notifications(cx);
2103                        }
2104                    }
2105                }));
2106
2107            self.notifications.push(screen_window);
2108
2109            // If the user manually refocuses the original window, dismiss the popup.
2110            self.notification_subscriptions
2111                .entry(screen_window)
2112                .or_insert_with(Vec::new)
2113                .push({
2114                    let pop_up_weak = pop_up.downgrade();
2115
2116                    cx.observe_window_activation(window, move |_, window, cx| {
2117                        if window.is_window_active()
2118                            && let Some(pop_up) = pop_up_weak.upgrade()
2119                        {
2120                            pop_up.update(cx, |_, cx| {
2121                                cx.emit(AgentNotificationEvent::Dismissed);
2122                            });
2123                        }
2124                    })
2125                });
2126        }
2127    }
2128
2129    fn dismiss_notifications(&mut self, cx: &mut Context<Self>) {
2130        for window in self.notifications.drain(..) {
2131            window
2132                .update(cx, |_, window, _| {
2133                    window.remove_window();
2134                })
2135                .ok();
2136
2137            self.notification_subscriptions.remove(&window);
2138        }
2139    }
2140
2141    fn agent_ui_font_size_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
2142        if let Some(entry_view_state) = self
2143            .as_active_thread()
2144            .map(|active| active.read(cx).entry_view_state.clone())
2145        {
2146            entry_view_state.update(cx, |entry_view_state, cx| {
2147                entry_view_state.agent_ui_font_size_changed(cx);
2148            });
2149        }
2150    }
2151
2152    pub(crate) fn insert_dragged_files(
2153        &self,
2154        paths: Vec<project::ProjectPath>,
2155        added_worktrees: Vec<Entity<project::Worktree>>,
2156        window: &mut Window,
2157        cx: &mut Context<Self>,
2158    ) {
2159        if let Some(active_thread) = self.as_active_thread() {
2160            active_thread.update(cx, |thread, cx| {
2161                thread.message_editor.update(cx, |editor, cx| {
2162                    editor.insert_dragged_files(paths, added_worktrees, window, cx);
2163                })
2164            });
2165        }
2166    }
2167
2168    /// Inserts the selected text into the message editor or the message being
2169    /// edited, if any.
2170    pub(crate) fn insert_selections(&self, window: &mut Window, cx: &mut Context<Self>) {
2171        if let Some(active_thread) = self.as_active_thread() {
2172            active_thread.update(cx, |thread, cx| {
2173                thread.active_editor(cx).update(cx, |editor, cx| {
2174                    editor.insert_selections(window, cx);
2175                })
2176            });
2177        }
2178    }
2179
2180    /// Inserts terminal text as a crease into the message editor.
2181    pub(crate) fn insert_terminal_text(
2182        &self,
2183        text: String,
2184        window: &mut Window,
2185        cx: &mut Context<Self>,
2186    ) {
2187        if let Some(active_thread) = self.as_active_thread() {
2188            active_thread.update(cx, |thread, cx| {
2189                thread.message_editor.update(cx, |editor, cx| {
2190                    editor.insert_terminal_crease(text, window, cx);
2191                })
2192            });
2193        }
2194    }
2195
2196    fn current_model_name(&self, cx: &App) -> SharedString {
2197        // For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet")
2198        // For ACP agents, use the agent name (e.g., "Claude Code", "Gemini CLI")
2199        // This provides better clarity about what refused the request
2200        if self.as_native_connection(cx).is_some() {
2201            self.as_active_thread()
2202                .and_then(|active| active.read(cx).model_selector.clone())
2203                .and_then(|selector| selector.read(cx).active_model(cx))
2204                .map(|model| model.name.clone())
2205                .unwrap_or_else(|| SharedString::from("The model"))
2206        } else {
2207            // ACP agent - use the agent name (e.g., "Claude Code", "Gemini CLI")
2208            self.agent.name()
2209        }
2210    }
2211
2212    fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
2213        let message = message.into();
2214
2215        CopyButton::new("copy-error-message", message).tooltip_label("Copy Error Message")
2216    }
2217
2218    pub(crate) fn reauthenticate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2219        let agent_name = self.agent.name();
2220        if let Some(active) = self.as_active_thread() {
2221            active.update(cx, |active, cx| active.clear_thread_error(cx));
2222        }
2223        let this = cx.weak_entity();
2224        window.defer(cx, |window, cx| {
2225            Self::handle_auth_required(this, AuthRequired::new(), agent_name, window, cx);
2226        })
2227    }
2228
2229    pub fn delete_history_entry(&mut self, entry: AgentSessionInfo, cx: &mut Context<Self>) {
2230        let task = self.history.update(cx, |history, cx| {
2231            history.delete_session(&entry.session_id, cx)
2232        });
2233        task.detach_and_log_err(cx);
2234    }
2235}
2236
2237fn loading_contents_spinner(size: IconSize) -> AnyElement {
2238    Icon::new(IconName::LoadCircle)
2239        .size(size)
2240        .color(Color::Accent)
2241        .with_rotate_animation(3)
2242        .into_any_element()
2243}
2244
2245fn placeholder_text(agent_name: &str, has_commands: bool) -> String {
2246    if agent_name == "Zed Agent" {
2247        format!("Message the {} — @ to include context", agent_name)
2248    } else if has_commands {
2249        format!(
2250            "Message {} — @ to include context, / for commands",
2251            agent_name
2252        )
2253    } else {
2254        format!("Message {} — @ to include context", agent_name)
2255    }
2256}
2257
2258impl Focusable for AcpServerView {
2259    fn focus_handle(&self, cx: &App) -> FocusHandle {
2260        match self.as_active_thread() {
2261            Some(thread) => thread.read(cx).focus_handle(cx),
2262            None => self.focus_handle.clone(),
2263        }
2264    }
2265}
2266
2267#[cfg(any(test, feature = "test-support"))]
2268impl AcpServerView {
2269    /// Expands a tool call so its content is visible.
2270    /// This is primarily useful for visual testing.
2271    pub fn expand_tool_call(&mut self, tool_call_id: acp::ToolCallId, cx: &mut Context<Self>) {
2272        if let Some(active) = self.as_active_thread() {
2273            active.update(cx, |active, _cx| {
2274                active.expanded_tool_calls.insert(tool_call_id);
2275            });
2276            cx.notify();
2277        }
2278    }
2279
2280    /// Expands a subagent card so its content is visible.
2281    /// This is primarily useful for visual testing.
2282    pub fn expand_subagent(&mut self, session_id: acp::SessionId, cx: &mut Context<Self>) {
2283        if let Some(active) = self.as_active_thread() {
2284            active.update(cx, |active, _cx| {
2285                active.expanded_subagents.insert(session_id);
2286            });
2287            cx.notify();
2288        }
2289    }
2290}
2291
2292impl Render for AcpServerView {
2293    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2294        self.sync_queued_message_editors(window, cx);
2295
2296        v_flex()
2297            .size_full()
2298            .track_focus(&self.focus_handle)
2299            .bg(cx.theme().colors().panel_background)
2300            .child(match &self.server_state {
2301                ServerState::Loading { .. } => v_flex()
2302                    .flex_1()
2303                    // .child(self.render_recent_history(cx))
2304                    .into_any(),
2305                ServerState::LoadError(e) => v_flex()
2306                    .flex_1()
2307                    .size_full()
2308                    .items_center()
2309                    .justify_end()
2310                    .child(self.render_load_error(e, window, cx))
2311                    .into_any(),
2312                ServerState::Connected(ConnectedServerState {
2313                    connection,
2314                    auth_state:
2315                        AuthState::Unauthenticated {
2316                            description,
2317                            configuration_view,
2318                            pending_auth_method,
2319                            _subscription,
2320                        },
2321                    ..
2322                }) => v_flex()
2323                    .flex_1()
2324                    .size_full()
2325                    .justify_end()
2326                    .child(self.render_auth_required_state(
2327                        connection,
2328                        description.as_ref(),
2329                        configuration_view.as_ref(),
2330                        pending_auth_method.as_ref(),
2331                        window,
2332                        cx,
2333                    ))
2334                    .into_any_element(),
2335                ServerState::Connected(connected) => connected.current.clone().into_any_element(),
2336            })
2337    }
2338}
2339
2340fn plan_label_markdown_style(
2341    status: &acp::PlanEntryStatus,
2342    window: &Window,
2343    cx: &App,
2344) -> MarkdownStyle {
2345    let default_md_style = MarkdownStyle::themed(MarkdownFont::Agent, window, cx);
2346
2347    MarkdownStyle {
2348        base_text_style: TextStyle {
2349            color: cx.theme().colors().text_muted,
2350            strikethrough: if matches!(status, acp::PlanEntryStatus::Completed) {
2351                Some(gpui::StrikethroughStyle {
2352                    thickness: px(1.),
2353                    color: Some(cx.theme().colors().text_muted.opacity(0.8)),
2354                })
2355            } else {
2356                None
2357            },
2358            ..default_md_style.base_text_style
2359        },
2360        ..default_md_style
2361    }
2362}
2363
2364#[cfg(test)]
2365pub(crate) mod tests {
2366    use acp_thread::{
2367        AgentSessionList, AgentSessionListRequest, AgentSessionListResponse, StubAgentConnection,
2368    };
2369    use action_log::ActionLog;
2370    use agent::{AgentTool, EditFileTool, FetchTool, TerminalTool, ToolPermissionContext};
2371    use agent_client_protocol::SessionId;
2372    use editor::MultiBufferOffset;
2373    use fs::FakeFs;
2374    use gpui::{EventEmitter, TestAppContext, VisualTestContext};
2375    use project::Project;
2376    use serde_json::json;
2377    use settings::SettingsStore;
2378    use std::any::Any;
2379    use std::path::Path;
2380    use std::rc::Rc;
2381    use workspace::Item;
2382
2383    use super::*;
2384
2385    #[gpui::test]
2386    async fn test_drop(cx: &mut TestAppContext) {
2387        init_test(cx);
2388
2389        let (thread_view, _cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
2390        let weak_view = thread_view.downgrade();
2391        drop(thread_view);
2392        assert!(!weak_view.is_upgradable());
2393    }
2394
2395    #[gpui::test]
2396    async fn test_notification_for_stop_event(cx: &mut TestAppContext) {
2397        init_test(cx);
2398
2399        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
2400
2401        let message_editor = message_editor(&thread_view, cx);
2402        message_editor.update_in(cx, |editor, window, cx| {
2403            editor.set_text("Hello", window, cx);
2404        });
2405
2406        cx.deactivate_window();
2407
2408        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
2409
2410        cx.run_until_parked();
2411
2412        assert!(
2413            cx.windows()
2414                .iter()
2415                .any(|window| window.downcast::<AgentNotification>().is_some())
2416        );
2417    }
2418
2419    #[gpui::test]
2420    async fn test_notification_for_error(cx: &mut TestAppContext) {
2421        init_test(cx);
2422
2423        let (thread_view, cx) =
2424            setup_thread_view(StubAgentServer::new(SaboteurAgentConnection), cx).await;
2425
2426        let message_editor = message_editor(&thread_view, cx);
2427        message_editor.update_in(cx, |editor, window, cx| {
2428            editor.set_text("Hello", window, cx);
2429        });
2430
2431        cx.deactivate_window();
2432
2433        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
2434
2435        cx.run_until_parked();
2436
2437        assert!(
2438            cx.windows()
2439                .iter()
2440                .any(|window| window.downcast::<AgentNotification>().is_some())
2441        );
2442    }
2443
2444    #[gpui::test]
2445    async fn test_recent_history_refreshes_when_history_cache_updated(cx: &mut TestAppContext) {
2446        init_test(cx);
2447
2448        let session_a = AgentSessionInfo::new(SessionId::new("session-a"));
2449        let session_b = AgentSessionInfo::new(SessionId::new("session-b"));
2450
2451        let fs = FakeFs::new(cx.executor());
2452        let project = Project::test(fs, [], cx).await;
2453        let (workspace, cx) =
2454            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2455
2456        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
2457        // Create history without an initial session list - it will be set after connection
2458        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
2459
2460        let thread_view = cx.update(|window, cx| {
2461            cx.new(|cx| {
2462                AcpServerView::new(
2463                    Rc::new(StubAgentServer::default_response()),
2464                    None,
2465                    None,
2466                    workspace.downgrade(),
2467                    project,
2468                    Some(thread_store),
2469                    None,
2470                    history.clone(),
2471                    window,
2472                    cx,
2473                )
2474            })
2475        });
2476
2477        // Wait for connection to establish
2478        cx.run_until_parked();
2479
2480        // Initially empty because StubAgentConnection.session_list() returns None
2481        active_thread(&thread_view, cx).read_with(cx, |view, _cx| {
2482            assert_eq!(view.recent_history_entries.len(), 0);
2483        });
2484
2485        // Now set the session list - this simulates external agents providing their history
2486        let list_a: Rc<dyn AgentSessionList> =
2487            Rc::new(StubSessionList::new(vec![session_a.clone()]));
2488        history.update(cx, |history, cx| {
2489            history.set_session_list(Some(list_a), cx);
2490        });
2491        cx.run_until_parked();
2492
2493        active_thread(&thread_view, cx).read_with(cx, |view, _cx| {
2494            assert_eq!(view.recent_history_entries.len(), 1);
2495            assert_eq!(
2496                view.recent_history_entries[0].session_id,
2497                session_a.session_id
2498            );
2499        });
2500
2501        // Update to a different session list
2502        let list_b: Rc<dyn AgentSessionList> =
2503            Rc::new(StubSessionList::new(vec![session_b.clone()]));
2504        history.update(cx, |history, cx| {
2505            history.set_session_list(Some(list_b), cx);
2506        });
2507        cx.run_until_parked();
2508
2509        active_thread(&thread_view, cx).read_with(cx, |view, _cx| {
2510            assert_eq!(view.recent_history_entries.len(), 1);
2511            assert_eq!(
2512                view.recent_history_entries[0].session_id,
2513                session_b.session_id
2514            );
2515        });
2516    }
2517
2518    #[gpui::test]
2519    async fn test_resume_without_history_adds_notice(cx: &mut TestAppContext) {
2520        init_test(cx);
2521
2522        let session = AgentSessionInfo::new(SessionId::new("resume-session"));
2523        let fs = FakeFs::new(cx.executor());
2524        let project = Project::test(fs, [], cx).await;
2525        let (workspace, cx) =
2526            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2527
2528        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
2529        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
2530
2531        let thread_view = cx.update(|window, cx| {
2532            cx.new(|cx| {
2533                AcpServerView::new(
2534                    Rc::new(StubAgentServer::new(ResumeOnlyAgentConnection)),
2535                    Some(session),
2536                    None,
2537                    workspace.downgrade(),
2538                    project,
2539                    Some(thread_store),
2540                    None,
2541                    history,
2542                    window,
2543                    cx,
2544                )
2545            })
2546        });
2547
2548        cx.run_until_parked();
2549
2550        thread_view.read_with(cx, |view, cx| {
2551            let state = view.as_active_thread().unwrap();
2552            assert!(state.read(cx).resumed_without_history);
2553            assert_eq!(state.read(cx).list_state.item_count(), 0);
2554        });
2555    }
2556
2557    #[gpui::test]
2558    async fn test_refusal_handling(cx: &mut TestAppContext) {
2559        init_test(cx);
2560
2561        let (thread_view, cx) =
2562            setup_thread_view(StubAgentServer::new(RefusalAgentConnection), cx).await;
2563
2564        let message_editor = message_editor(&thread_view, cx);
2565        message_editor.update_in(cx, |editor, window, cx| {
2566            editor.set_text("Do something harmful", window, cx);
2567        });
2568
2569        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
2570
2571        cx.run_until_parked();
2572
2573        // Check that the refusal error is set
2574        thread_view.read_with(cx, |thread_view, cx| {
2575            let state = thread_view.as_active_thread().unwrap();
2576            assert!(
2577                matches!(state.read(cx).thread_error, Some(ThreadError::Refusal)),
2578                "Expected refusal error to be set"
2579            );
2580        });
2581    }
2582
2583    #[gpui::test]
2584    async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
2585        init_test(cx);
2586
2587        let tool_call_id = acp::ToolCallId::new("1");
2588        let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Label")
2589            .kind(acp::ToolKind::Edit)
2590            .content(vec!["hi".into()]);
2591        let connection =
2592            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
2593                tool_call_id,
2594                PermissionOptions::Flat(vec![acp::PermissionOption::new(
2595                    "1",
2596                    "Allow",
2597                    acp::PermissionOptionKind::AllowOnce,
2598                )]),
2599            )]));
2600
2601        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
2602
2603        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
2604
2605        let message_editor = message_editor(&thread_view, cx);
2606        message_editor.update_in(cx, |editor, window, cx| {
2607            editor.set_text("Hello", window, cx);
2608        });
2609
2610        cx.deactivate_window();
2611
2612        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
2613
2614        cx.run_until_parked();
2615
2616        assert!(
2617            cx.windows()
2618                .iter()
2619                .any(|window| window.downcast::<AgentNotification>().is_some())
2620        );
2621    }
2622
2623    #[gpui::test]
2624    async fn test_notification_when_panel_hidden(cx: &mut TestAppContext) {
2625        init_test(cx);
2626
2627        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
2628
2629        add_to_workspace(thread_view.clone(), cx);
2630
2631        let message_editor = message_editor(&thread_view, cx);
2632
2633        message_editor.update_in(cx, |editor, window, cx| {
2634            editor.set_text("Hello", window, cx);
2635        });
2636
2637        // Window is active (don't deactivate), but panel will be hidden
2638        // Note: In the test environment, the panel is not actually added to the dock,
2639        // so is_agent_panel_hidden will return true
2640
2641        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
2642
2643        cx.run_until_parked();
2644
2645        // Should show notification because window is active but panel is hidden
2646        assert!(
2647            cx.windows()
2648                .iter()
2649                .any(|window| window.downcast::<AgentNotification>().is_some()),
2650            "Expected notification when panel is hidden"
2651        );
2652    }
2653
2654    #[gpui::test]
2655    async fn test_notification_still_works_when_window_inactive(cx: &mut TestAppContext) {
2656        init_test(cx);
2657
2658        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
2659
2660        let message_editor = message_editor(&thread_view, cx);
2661        message_editor.update_in(cx, |editor, window, cx| {
2662            editor.set_text("Hello", window, cx);
2663        });
2664
2665        // Deactivate window - should show notification regardless of setting
2666        cx.deactivate_window();
2667
2668        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
2669
2670        cx.run_until_parked();
2671
2672        // Should still show notification when window is inactive (existing behavior)
2673        assert!(
2674            cx.windows()
2675                .iter()
2676                .any(|window| window.downcast::<AgentNotification>().is_some()),
2677            "Expected notification when window is inactive"
2678        );
2679    }
2680
2681    #[gpui::test]
2682    async fn test_notification_respects_never_setting(cx: &mut TestAppContext) {
2683        init_test(cx);
2684
2685        // Set notify_when_agent_waiting to Never
2686        cx.update(|cx| {
2687            AgentSettings::override_global(
2688                AgentSettings {
2689                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
2690                    ..AgentSettings::get_global(cx).clone()
2691                },
2692                cx,
2693            );
2694        });
2695
2696        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
2697
2698        let message_editor = message_editor(&thread_view, cx);
2699        message_editor.update_in(cx, |editor, window, cx| {
2700            editor.set_text("Hello", window, cx);
2701        });
2702
2703        // Window is active
2704
2705        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
2706
2707        cx.run_until_parked();
2708
2709        // Should NOT show notification because notify_when_agent_waiting is Never
2710        assert!(
2711            !cx.windows()
2712                .iter()
2713                .any(|window| window.downcast::<AgentNotification>().is_some()),
2714            "Expected no notification when notify_when_agent_waiting is Never"
2715        );
2716    }
2717
2718    #[gpui::test]
2719    async fn test_notification_closed_when_thread_view_dropped(cx: &mut TestAppContext) {
2720        init_test(cx);
2721
2722        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
2723
2724        let weak_view = thread_view.downgrade();
2725
2726        let message_editor = message_editor(&thread_view, cx);
2727        message_editor.update_in(cx, |editor, window, cx| {
2728            editor.set_text("Hello", window, cx);
2729        });
2730
2731        cx.deactivate_window();
2732
2733        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
2734
2735        cx.run_until_parked();
2736
2737        // Verify notification is shown
2738        assert!(
2739            cx.windows()
2740                .iter()
2741                .any(|window| window.downcast::<AgentNotification>().is_some()),
2742            "Expected notification to be shown"
2743        );
2744
2745        // Drop the thread view (simulating navigation to a new thread)
2746        drop(thread_view);
2747        drop(message_editor);
2748        // Trigger an update to flush effects, which will call release_dropped_entities
2749        cx.update(|_window, _cx| {});
2750        cx.run_until_parked();
2751
2752        // Verify the entity was actually released
2753        assert!(
2754            !weak_view.is_upgradable(),
2755            "Thread view entity should be released after dropping"
2756        );
2757
2758        // The notification should be automatically closed via on_release
2759        assert!(
2760            !cx.windows()
2761                .iter()
2762                .any(|window| window.downcast::<AgentNotification>().is_some()),
2763            "Notification should be closed when thread view is dropped"
2764        );
2765    }
2766
2767    async fn setup_thread_view(
2768        agent: impl AgentServer + 'static,
2769        cx: &mut TestAppContext,
2770    ) -> (Entity<AcpServerView>, &mut VisualTestContext) {
2771        let fs = FakeFs::new(cx.executor());
2772        let project = Project::test(fs, [], cx).await;
2773        let (workspace, cx) =
2774            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2775
2776        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
2777        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
2778
2779        let thread_view = cx.update(|window, cx| {
2780            cx.new(|cx| {
2781                AcpServerView::new(
2782                    Rc::new(agent),
2783                    None,
2784                    None,
2785                    workspace.downgrade(),
2786                    project,
2787                    Some(thread_store),
2788                    None,
2789                    history,
2790                    window,
2791                    cx,
2792                )
2793            })
2794        });
2795        cx.run_until_parked();
2796        (thread_view, cx)
2797    }
2798
2799    fn add_to_workspace(thread_view: Entity<AcpServerView>, cx: &mut VisualTestContext) {
2800        let workspace = thread_view.read_with(cx, |thread_view, _cx| thread_view.workspace.clone());
2801
2802        workspace
2803            .update_in(cx, |workspace, window, cx| {
2804                workspace.add_item_to_active_pane(
2805                    Box::new(cx.new(|_| ThreadViewItem(thread_view.clone()))),
2806                    None,
2807                    true,
2808                    window,
2809                    cx,
2810                );
2811            })
2812            .unwrap();
2813    }
2814
2815    struct ThreadViewItem(Entity<AcpServerView>);
2816
2817    impl Item for ThreadViewItem {
2818        type Event = ();
2819
2820        fn include_in_nav_history() -> bool {
2821            false
2822        }
2823
2824        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
2825            "Test".into()
2826        }
2827    }
2828
2829    impl EventEmitter<()> for ThreadViewItem {}
2830
2831    impl Focusable for ThreadViewItem {
2832        fn focus_handle(&self, cx: &App) -> FocusHandle {
2833            self.0.read(cx).focus_handle(cx)
2834        }
2835    }
2836
2837    impl Render for ThreadViewItem {
2838        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
2839            self.0.clone().into_any_element()
2840        }
2841    }
2842
2843    struct StubAgentServer<C> {
2844        connection: C,
2845    }
2846
2847    impl<C> StubAgentServer<C> {
2848        fn new(connection: C) -> Self {
2849            Self { connection }
2850        }
2851    }
2852
2853    impl StubAgentServer<StubAgentConnection> {
2854        fn default_response() -> Self {
2855            let conn = StubAgentConnection::new();
2856            conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
2857                acp::ContentChunk::new("Default response".into()),
2858            )]);
2859            Self::new(conn)
2860        }
2861    }
2862
2863    impl<C> AgentServer for StubAgentServer<C>
2864    where
2865        C: 'static + AgentConnection + Send + Clone,
2866    {
2867        fn logo(&self) -> ui::IconName {
2868            ui::IconName::Ai
2869        }
2870
2871        fn name(&self) -> SharedString {
2872            "Test".into()
2873        }
2874
2875        fn connect(
2876            &self,
2877            _root_dir: Option<&Path>,
2878            _delegate: AgentServerDelegate,
2879            _cx: &mut App,
2880        ) -> Task<gpui::Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
2881            Task::ready(Ok((Rc::new(self.connection.clone()), None)))
2882        }
2883
2884        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
2885            self
2886        }
2887    }
2888
2889    #[derive(Clone)]
2890    struct StubSessionList {
2891        sessions: Vec<AgentSessionInfo>,
2892    }
2893
2894    impl StubSessionList {
2895        fn new(sessions: Vec<AgentSessionInfo>) -> Self {
2896            Self { sessions }
2897        }
2898    }
2899
2900    impl AgentSessionList for StubSessionList {
2901        fn list_sessions(
2902            &self,
2903            _request: AgentSessionListRequest,
2904            _cx: &mut App,
2905        ) -> Task<anyhow::Result<AgentSessionListResponse>> {
2906            Task::ready(Ok(AgentSessionListResponse::new(self.sessions.clone())))
2907        }
2908        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
2909            self
2910        }
2911    }
2912
2913    #[derive(Clone)]
2914    struct ResumeOnlyAgentConnection;
2915
2916    impl AgentConnection for ResumeOnlyAgentConnection {
2917        fn telemetry_id(&self) -> SharedString {
2918            "resume-only".into()
2919        }
2920
2921        fn new_thread(
2922            self: Rc<Self>,
2923            project: Entity<Project>,
2924            _cwd: &Path,
2925            cx: &mut gpui::App,
2926        ) -> Task<gpui::Result<Entity<AcpThread>>> {
2927            let action_log = cx.new(|_| ActionLog::new(project.clone()));
2928            let thread = cx.new(|cx| {
2929                AcpThread::new(
2930                    "ResumeOnlyAgentConnection",
2931                    self.clone(),
2932                    project,
2933                    action_log,
2934                    SessionId::new("new-session"),
2935                    watch::Receiver::constant(
2936                        acp::PromptCapabilities::new()
2937                            .image(true)
2938                            .audio(true)
2939                            .embedded_context(true),
2940                    ),
2941                    cx,
2942                )
2943            });
2944            Task::ready(Ok(thread))
2945        }
2946
2947        fn supports_resume_session(&self, _cx: &App) -> bool {
2948            true
2949        }
2950
2951        fn resume_session(
2952            self: Rc<Self>,
2953            session: AgentSessionInfo,
2954            project: Entity<Project>,
2955            _cwd: &Path,
2956            cx: &mut App,
2957        ) -> Task<gpui::Result<Entity<AcpThread>>> {
2958            let action_log = cx.new(|_| ActionLog::new(project.clone()));
2959            let thread = cx.new(|cx| {
2960                AcpThread::new(
2961                    "ResumeOnlyAgentConnection",
2962                    self.clone(),
2963                    project,
2964                    action_log,
2965                    session.session_id,
2966                    watch::Receiver::constant(
2967                        acp::PromptCapabilities::new()
2968                            .image(true)
2969                            .audio(true)
2970                            .embedded_context(true),
2971                    ),
2972                    cx,
2973                )
2974            });
2975            Task::ready(Ok(thread))
2976        }
2977
2978        fn auth_methods(&self) -> &[acp::AuthMethod] {
2979            &[]
2980        }
2981
2982        fn authenticate(
2983            &self,
2984            _method_id: acp::AuthMethodId,
2985            _cx: &mut App,
2986        ) -> Task<gpui::Result<()>> {
2987            Task::ready(Ok(()))
2988        }
2989
2990        fn prompt(
2991            &self,
2992            _id: Option<acp_thread::UserMessageId>,
2993            _params: acp::PromptRequest,
2994            _cx: &mut App,
2995        ) -> Task<gpui::Result<acp::PromptResponse>> {
2996            Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)))
2997        }
2998
2999        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {}
3000
3001        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
3002            self
3003        }
3004    }
3005
3006    #[derive(Clone)]
3007    struct SaboteurAgentConnection;
3008
3009    impl AgentConnection for SaboteurAgentConnection {
3010        fn telemetry_id(&self) -> SharedString {
3011            "saboteur".into()
3012        }
3013
3014        fn new_thread(
3015            self: Rc<Self>,
3016            project: Entity<Project>,
3017            _cwd: &Path,
3018            cx: &mut gpui::App,
3019        ) -> Task<gpui::Result<Entity<AcpThread>>> {
3020            Task::ready(Ok(cx.new(|cx| {
3021                let action_log = cx.new(|_| ActionLog::new(project.clone()));
3022                AcpThread::new(
3023                    "SaboteurAgentConnection",
3024                    self,
3025                    project,
3026                    action_log,
3027                    SessionId::new("test"),
3028                    watch::Receiver::constant(
3029                        acp::PromptCapabilities::new()
3030                            .image(true)
3031                            .audio(true)
3032                            .embedded_context(true),
3033                    ),
3034                    cx,
3035                )
3036            })))
3037        }
3038
3039        fn auth_methods(&self) -> &[acp::AuthMethod] {
3040            &[]
3041        }
3042
3043        fn authenticate(
3044            &self,
3045            _method_id: acp::AuthMethodId,
3046            _cx: &mut App,
3047        ) -> Task<gpui::Result<()>> {
3048            unimplemented!()
3049        }
3050
3051        fn prompt(
3052            &self,
3053            _id: Option<acp_thread::UserMessageId>,
3054            _params: acp::PromptRequest,
3055            _cx: &mut App,
3056        ) -> Task<gpui::Result<acp::PromptResponse>> {
3057            Task::ready(Err(anyhow::anyhow!("Error prompting")))
3058        }
3059
3060        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
3061            unimplemented!()
3062        }
3063
3064        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
3065            self
3066        }
3067    }
3068
3069    /// Simulates a model which always returns a refusal response
3070    #[derive(Clone)]
3071    struct RefusalAgentConnection;
3072
3073    impl AgentConnection for RefusalAgentConnection {
3074        fn telemetry_id(&self) -> SharedString {
3075            "refusal".into()
3076        }
3077
3078        fn new_thread(
3079            self: Rc<Self>,
3080            project: Entity<Project>,
3081            _cwd: &Path,
3082            cx: &mut gpui::App,
3083        ) -> Task<gpui::Result<Entity<AcpThread>>> {
3084            Task::ready(Ok(cx.new(|cx| {
3085                let action_log = cx.new(|_| ActionLog::new(project.clone()));
3086                AcpThread::new(
3087                    "RefusalAgentConnection",
3088                    self,
3089                    project,
3090                    action_log,
3091                    SessionId::new("test"),
3092                    watch::Receiver::constant(
3093                        acp::PromptCapabilities::new()
3094                            .image(true)
3095                            .audio(true)
3096                            .embedded_context(true),
3097                    ),
3098                    cx,
3099                )
3100            })))
3101        }
3102
3103        fn auth_methods(&self) -> &[acp::AuthMethod] {
3104            &[]
3105        }
3106
3107        fn authenticate(
3108            &self,
3109            _method_id: acp::AuthMethodId,
3110            _cx: &mut App,
3111        ) -> Task<gpui::Result<()>> {
3112            unimplemented!()
3113        }
3114
3115        fn prompt(
3116            &self,
3117            _id: Option<acp_thread::UserMessageId>,
3118            _params: acp::PromptRequest,
3119            _cx: &mut App,
3120        ) -> Task<gpui::Result<acp::PromptResponse>> {
3121            Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::Refusal)))
3122        }
3123
3124        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
3125            unimplemented!()
3126        }
3127
3128        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
3129            self
3130        }
3131    }
3132
3133    pub(crate) fn init_test(cx: &mut TestAppContext) {
3134        cx.update(|cx| {
3135            let settings_store = SettingsStore::test(cx);
3136            cx.set_global(settings_store);
3137            theme::init(theme::LoadThemes::JustBase, cx);
3138            editor::init(cx);
3139            release_channel::init(semver::Version::new(0, 0, 0), cx);
3140            prompt_store::init(cx)
3141        });
3142    }
3143
3144    fn active_thread(
3145        thread_view: &Entity<AcpServerView>,
3146        cx: &TestAppContext,
3147    ) -> Entity<AcpThreadView> {
3148        cx.read(|cx| thread_view.read(cx).as_connected().unwrap().current.clone())
3149    }
3150
3151    fn message_editor(
3152        thread_view: &Entity<AcpServerView>,
3153        cx: &TestAppContext,
3154    ) -> Entity<MessageEditor> {
3155        let thread = active_thread(thread_view, cx);
3156        cx.read(|cx| thread.read(cx).message_editor.clone())
3157    }
3158
3159    #[gpui::test]
3160    async fn test_rewind_views(cx: &mut TestAppContext) {
3161        init_test(cx);
3162
3163        let fs = FakeFs::new(cx.executor());
3164        fs.insert_tree(
3165            "/project",
3166            json!({
3167                "test1.txt": "old content 1",
3168                "test2.txt": "old content 2"
3169            }),
3170        )
3171        .await;
3172        let project = Project::test(fs, [Path::new("/project")], cx).await;
3173        let (workspace, cx) =
3174            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3175
3176        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
3177        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
3178
3179        let connection = Rc::new(StubAgentConnection::new());
3180        let thread_view = cx.update(|window, cx| {
3181            cx.new(|cx| {
3182                AcpServerView::new(
3183                    Rc::new(StubAgentServer::new(connection.as_ref().clone())),
3184                    None,
3185                    None,
3186                    workspace.downgrade(),
3187                    project.clone(),
3188                    Some(thread_store.clone()),
3189                    None,
3190                    history,
3191                    window,
3192                    cx,
3193                )
3194            })
3195        });
3196
3197        cx.run_until_parked();
3198
3199        let thread = thread_view
3200            .read_with(cx, |view, cx| {
3201                view.as_active_thread().map(|r| r.read(cx).thread.clone())
3202            })
3203            .unwrap();
3204
3205        // First user message
3206        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(
3207            acp::ToolCall::new("tool1", "Edit file 1")
3208                .kind(acp::ToolKind::Edit)
3209                .status(acp::ToolCallStatus::Completed)
3210                .content(vec![acp::ToolCallContent::Diff(
3211                    acp::Diff::new("/project/test1.txt", "new content 1").old_text("old content 1"),
3212                )]),
3213        )]);
3214
3215        thread
3216            .update(cx, |thread, cx| thread.send_raw("Give me a diff", cx))
3217            .await
3218            .unwrap();
3219        cx.run_until_parked();
3220
3221        thread.read_with(cx, |thread, _cx| {
3222            assert_eq!(thread.entries().len(), 2);
3223        });
3224
3225        thread_view.read_with(cx, |view, cx| {
3226            let entry_view_state = view
3227                .as_active_thread()
3228                .map(|active| active.read(cx).entry_view_state.clone())
3229                .unwrap();
3230            entry_view_state.read_with(cx, |entry_view_state, _| {
3231                assert!(
3232                    entry_view_state
3233                        .entry(0)
3234                        .unwrap()
3235                        .message_editor()
3236                        .is_some()
3237                );
3238                assert!(entry_view_state.entry(1).unwrap().has_content());
3239            });
3240        });
3241
3242        // Second user message
3243        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(
3244            acp::ToolCall::new("tool2", "Edit file 2")
3245                .kind(acp::ToolKind::Edit)
3246                .status(acp::ToolCallStatus::Completed)
3247                .content(vec![acp::ToolCallContent::Diff(
3248                    acp::Diff::new("/project/test2.txt", "new content 2").old_text("old content 2"),
3249                )]),
3250        )]);
3251
3252        thread
3253            .update(cx, |thread, cx| thread.send_raw("Another one", cx))
3254            .await
3255            .unwrap();
3256        cx.run_until_parked();
3257
3258        let second_user_message_id = thread.read_with(cx, |thread, _| {
3259            assert_eq!(thread.entries().len(), 4);
3260            let AgentThreadEntry::UserMessage(user_message) = &thread.entries()[2] else {
3261                panic!();
3262            };
3263            user_message.id.clone().unwrap()
3264        });
3265
3266        thread_view.read_with(cx, |view, cx| {
3267            let entry_view_state = view
3268                .as_active_thread()
3269                .unwrap()
3270                .read(cx)
3271                .entry_view_state
3272                .clone();
3273            entry_view_state.read_with(cx, |entry_view_state, _| {
3274                assert!(
3275                    entry_view_state
3276                        .entry(0)
3277                        .unwrap()
3278                        .message_editor()
3279                        .is_some()
3280                );
3281                assert!(entry_view_state.entry(1).unwrap().has_content());
3282                assert!(
3283                    entry_view_state
3284                        .entry(2)
3285                        .unwrap()
3286                        .message_editor()
3287                        .is_some()
3288                );
3289                assert!(entry_view_state.entry(3).unwrap().has_content());
3290            });
3291        });
3292
3293        // Rewind to first message
3294        thread
3295            .update(cx, |thread, cx| thread.rewind(second_user_message_id, cx))
3296            .await
3297            .unwrap();
3298
3299        cx.run_until_parked();
3300
3301        thread.read_with(cx, |thread, _| {
3302            assert_eq!(thread.entries().len(), 2);
3303        });
3304
3305        thread_view.read_with(cx, |view, cx| {
3306            let active = view.as_active_thread().unwrap();
3307            active
3308                .read(cx)
3309                .entry_view_state
3310                .read_with(cx, |entry_view_state, _| {
3311                    assert!(
3312                        entry_view_state
3313                            .entry(0)
3314                            .unwrap()
3315                            .message_editor()
3316                            .is_some()
3317                    );
3318                    assert!(entry_view_state.entry(1).unwrap().has_content());
3319
3320                    // Old views should be dropped
3321                    assert!(entry_view_state.entry(2).is_none());
3322                    assert!(entry_view_state.entry(3).is_none());
3323                });
3324        });
3325    }
3326
3327    #[gpui::test]
3328    async fn test_scroll_to_most_recent_user_prompt(cx: &mut TestAppContext) {
3329        init_test(cx);
3330
3331        let connection = StubAgentConnection::new();
3332
3333        // Each user prompt will result in a user message entry plus an agent message entry.
3334        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
3335            acp::ContentChunk::new("Response 1".into()),
3336        )]);
3337
3338        let (thread_view, cx) =
3339            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
3340
3341        let thread = thread_view
3342            .read_with(cx, |view, cx| {
3343                view.as_active_thread().map(|r| r.read(cx).thread.clone())
3344            })
3345            .unwrap();
3346
3347        thread
3348            .update(cx, |thread, cx| thread.send_raw("Prompt 1", cx))
3349            .await
3350            .unwrap();
3351        cx.run_until_parked();
3352
3353        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
3354            acp::ContentChunk::new("Response 2".into()),
3355        )]);
3356
3357        thread
3358            .update(cx, |thread, cx| thread.send_raw("Prompt 2", cx))
3359            .await
3360            .unwrap();
3361        cx.run_until_parked();
3362
3363        // Move somewhere else first so we're not trivially already on the last user prompt.
3364        active_thread(&thread_view, cx).update(cx, |view, cx| {
3365            view.scroll_to_top(cx);
3366        });
3367        cx.run_until_parked();
3368
3369        active_thread(&thread_view, cx).update(cx, |view, cx| {
3370            view.scroll_to_most_recent_user_prompt(cx);
3371            let scroll_top = view.list_state.logical_scroll_top();
3372            // Entries layout is: [User1, Assistant1, User2, Assistant2]
3373            assert_eq!(scroll_top.item_ix, 2);
3374        });
3375    }
3376
3377    #[gpui::test]
3378    async fn test_scroll_to_most_recent_user_prompt_falls_back_to_bottom_without_user_messages(
3379        cx: &mut TestAppContext,
3380    ) {
3381        init_test(cx);
3382
3383        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
3384
3385        // With no entries, scrolling should be a no-op and must not panic.
3386        active_thread(&thread_view, cx).update(cx, |view, cx| {
3387            view.scroll_to_most_recent_user_prompt(cx);
3388            let scroll_top = view.list_state.logical_scroll_top();
3389            assert_eq!(scroll_top.item_ix, 0);
3390        });
3391    }
3392
3393    #[gpui::test]
3394    async fn test_message_editing_cancel(cx: &mut TestAppContext) {
3395        init_test(cx);
3396
3397        let connection = StubAgentConnection::new();
3398
3399        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
3400            acp::ContentChunk::new("Response".into()),
3401        )]);
3402
3403        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
3404        add_to_workspace(thread_view.clone(), cx);
3405
3406        let message_editor = message_editor(&thread_view, cx);
3407        message_editor.update_in(cx, |editor, window, cx| {
3408            editor.set_text("Original message to edit", window, cx);
3409        });
3410        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
3411
3412        cx.run_until_parked();
3413
3414        let user_message_editor = thread_view.read_with(cx, |view, cx| {
3415            assert_eq!(
3416                view.as_active_thread()
3417                    .and_then(|active| active.read(cx).editing_message),
3418                None
3419            );
3420
3421            view.as_active_thread()
3422                .map(|active| &active.read(cx).entry_view_state)
3423                .as_ref()
3424                .unwrap()
3425                .read(cx)
3426                .entry(0)
3427                .unwrap()
3428                .message_editor()
3429                .unwrap()
3430                .clone()
3431        });
3432
3433        // Focus
3434        cx.focus(&user_message_editor);
3435        thread_view.read_with(cx, |view, cx| {
3436            assert_eq!(
3437                view.as_active_thread()
3438                    .and_then(|active| active.read(cx).editing_message),
3439                Some(0)
3440            );
3441        });
3442
3443        // Edit
3444        user_message_editor.update_in(cx, |editor, window, cx| {
3445            editor.set_text("Edited message content", window, cx);
3446        });
3447
3448        // Cancel
3449        user_message_editor.update_in(cx, |_editor, window, cx| {
3450            window.dispatch_action(Box::new(editor::actions::Cancel), cx);
3451        });
3452
3453        thread_view.read_with(cx, |view, cx| {
3454            assert_eq!(
3455                view.as_active_thread()
3456                    .and_then(|active| active.read(cx).editing_message),
3457                None
3458            );
3459        });
3460
3461        user_message_editor.read_with(cx, |editor, cx| {
3462            assert_eq!(editor.text(cx), "Original message to edit");
3463        });
3464    }
3465
3466    #[gpui::test]
3467    async fn test_message_doesnt_send_if_empty(cx: &mut TestAppContext) {
3468        init_test(cx);
3469
3470        let connection = StubAgentConnection::new();
3471
3472        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
3473        add_to_workspace(thread_view.clone(), cx);
3474
3475        let message_editor = message_editor(&thread_view, cx);
3476        message_editor.update_in(cx, |editor, window, cx| {
3477            editor.set_text("", window, cx);
3478        });
3479
3480        let thread = cx.read(|cx| {
3481            thread_view
3482                .read(cx)
3483                .as_active_thread()
3484                .unwrap()
3485                .read(cx)
3486                .thread
3487                .clone()
3488        });
3489        let entries_before = cx.read(|cx| thread.read(cx).entries().len());
3490
3491        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| {
3492            view.send(window, cx);
3493        });
3494        cx.run_until_parked();
3495
3496        let entries_after = cx.read(|cx| thread.read(cx).entries().len());
3497        assert_eq!(
3498            entries_before, entries_after,
3499            "No message should be sent when editor is empty"
3500        );
3501    }
3502
3503    #[gpui::test]
3504    async fn test_message_editing_regenerate(cx: &mut TestAppContext) {
3505        init_test(cx);
3506
3507        let connection = StubAgentConnection::new();
3508
3509        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
3510            acp::ContentChunk::new("Response".into()),
3511        )]);
3512
3513        let (thread_view, cx) =
3514            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
3515        add_to_workspace(thread_view.clone(), cx);
3516
3517        let message_editor = message_editor(&thread_view, cx);
3518        message_editor.update_in(cx, |editor, window, cx| {
3519            editor.set_text("Original message to edit", window, cx);
3520        });
3521        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
3522
3523        cx.run_until_parked();
3524
3525        let user_message_editor = thread_view.read_with(cx, |view, cx| {
3526            assert_eq!(
3527                view.as_active_thread()
3528                    .and_then(|active| active.read(cx).editing_message),
3529                None
3530            );
3531            assert_eq!(
3532                view.as_active_thread()
3533                    .unwrap()
3534                    .read(cx)
3535                    .thread
3536                    .read(cx)
3537                    .entries()
3538                    .len(),
3539                2
3540            );
3541
3542            view.as_active_thread()
3543                .map(|active| &active.read(cx).entry_view_state)
3544                .as_ref()
3545                .unwrap()
3546                .read(cx)
3547                .entry(0)
3548                .unwrap()
3549                .message_editor()
3550                .unwrap()
3551                .clone()
3552        });
3553
3554        // Focus
3555        cx.focus(&user_message_editor);
3556
3557        // Edit
3558        user_message_editor.update_in(cx, |editor, window, cx| {
3559            editor.set_text("Edited message content", window, cx);
3560        });
3561
3562        // Send
3563        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
3564            acp::ContentChunk::new("New Response".into()),
3565        )]);
3566
3567        user_message_editor.update_in(cx, |_editor, window, cx| {
3568            window.dispatch_action(Box::new(Chat), cx);
3569        });
3570
3571        cx.run_until_parked();
3572
3573        thread_view.read_with(cx, |view, cx| {
3574            assert_eq!(
3575                view.as_active_thread()
3576                    .and_then(|active| active.read(cx).editing_message),
3577                None
3578            );
3579
3580            let entries = view
3581                .as_active_thread()
3582                .unwrap()
3583                .read(cx)
3584                .thread
3585                .read(cx)
3586                .entries();
3587            assert_eq!(entries.len(), 2);
3588            assert_eq!(
3589                entries[0].to_markdown(cx),
3590                "## User\n\nEdited message content\n\n"
3591            );
3592            assert_eq!(
3593                entries[1].to_markdown(cx),
3594                "## Assistant\n\nNew Response\n\n"
3595            );
3596
3597            let entry_view_state = view
3598                .as_active_thread()
3599                .map(|active| &active.read(cx).entry_view_state)
3600                .unwrap();
3601            let new_editor = entry_view_state.read_with(cx, |state, _cx| {
3602                assert!(!state.entry(1).unwrap().has_content());
3603                state.entry(0).unwrap().message_editor().unwrap().clone()
3604            });
3605
3606            assert_eq!(new_editor.read(cx).text(cx), "Edited message content");
3607        })
3608    }
3609
3610    #[gpui::test]
3611    async fn test_message_editing_while_generating(cx: &mut TestAppContext) {
3612        init_test(cx);
3613
3614        let connection = StubAgentConnection::new();
3615
3616        let (thread_view, cx) =
3617            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
3618        add_to_workspace(thread_view.clone(), cx);
3619
3620        let message_editor = message_editor(&thread_view, cx);
3621        message_editor.update_in(cx, |editor, window, cx| {
3622            editor.set_text("Original message to edit", window, cx);
3623        });
3624        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
3625
3626        cx.run_until_parked();
3627
3628        let (user_message_editor, session_id) = thread_view.read_with(cx, |view, cx| {
3629            let thread = view.as_active_thread().unwrap().read(cx).thread.read(cx);
3630            assert_eq!(thread.entries().len(), 1);
3631
3632            let editor = view
3633                .as_active_thread()
3634                .map(|active| &active.read(cx).entry_view_state)
3635                .as_ref()
3636                .unwrap()
3637                .read(cx)
3638                .entry(0)
3639                .unwrap()
3640                .message_editor()
3641                .unwrap()
3642                .clone();
3643
3644            (editor, thread.session_id().clone())
3645        });
3646
3647        // Focus
3648        cx.focus(&user_message_editor);
3649
3650        thread_view.read_with(cx, |view, cx| {
3651            assert_eq!(
3652                view.as_active_thread()
3653                    .and_then(|active| active.read(cx).editing_message),
3654                Some(0)
3655            );
3656        });
3657
3658        // Edit
3659        user_message_editor.update_in(cx, |editor, window, cx| {
3660            editor.set_text("Edited message content", window, cx);
3661        });
3662
3663        thread_view.read_with(cx, |view, cx| {
3664            assert_eq!(
3665                view.as_active_thread()
3666                    .and_then(|active| active.read(cx).editing_message),
3667                Some(0)
3668            );
3669        });
3670
3671        // Finish streaming response
3672        cx.update(|_, cx| {
3673            connection.send_update(
3674                session_id.clone(),
3675                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("Response".into())),
3676                cx,
3677            );
3678            connection.end_turn(session_id, acp::StopReason::EndTurn);
3679        });
3680
3681        thread_view.read_with(cx, |view, cx| {
3682            assert_eq!(
3683                view.as_active_thread()
3684                    .and_then(|active| active.read(cx).editing_message),
3685                Some(0)
3686            );
3687        });
3688
3689        cx.run_until_parked();
3690
3691        // Should still be editing
3692        cx.update(|window, cx| {
3693            assert!(user_message_editor.focus_handle(cx).is_focused(window));
3694            assert_eq!(
3695                thread_view
3696                    .read(cx)
3697                    .as_active_thread()
3698                    .and_then(|active| active.read(cx).editing_message),
3699                Some(0)
3700            );
3701            assert_eq!(
3702                user_message_editor.read(cx).text(cx),
3703                "Edited message content"
3704            );
3705        });
3706    }
3707
3708    struct GeneratingThreadSetup {
3709        thread_view: Entity<AcpServerView>,
3710        thread: Entity<AcpThread>,
3711        message_editor: Entity<MessageEditor>,
3712    }
3713
3714    async fn setup_generating_thread(
3715        cx: &mut TestAppContext,
3716    ) -> (GeneratingThreadSetup, &mut VisualTestContext) {
3717        let connection = StubAgentConnection::new();
3718
3719        let (thread_view, cx) =
3720            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
3721        add_to_workspace(thread_view.clone(), cx);
3722
3723        let message_editor = message_editor(&thread_view, cx);
3724        message_editor.update_in(cx, |editor, window, cx| {
3725            editor.set_text("Hello", window, cx);
3726        });
3727        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
3728
3729        let (thread, session_id) = thread_view.read_with(cx, |view, cx| {
3730            let thread = view
3731                .as_active_thread()
3732                .as_ref()
3733                .unwrap()
3734                .read(cx)
3735                .thread
3736                .clone();
3737            (thread.clone(), thread.read(cx).session_id().clone())
3738        });
3739
3740        cx.run_until_parked();
3741
3742        cx.update(|_, cx| {
3743            connection.send_update(
3744                session_id.clone(),
3745                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
3746                    "Response chunk".into(),
3747                )),
3748                cx,
3749            );
3750        });
3751
3752        cx.run_until_parked();
3753
3754        thread.read_with(cx, |thread, _cx| {
3755            assert_eq!(thread.status(), ThreadStatus::Generating);
3756        });
3757
3758        (
3759            GeneratingThreadSetup {
3760                thread_view,
3761                thread,
3762                message_editor,
3763            },
3764            cx,
3765        )
3766    }
3767
3768    #[gpui::test]
3769    async fn test_escape_cancels_generation_from_conversation_focus(cx: &mut TestAppContext) {
3770        init_test(cx);
3771
3772        let (setup, cx) = setup_generating_thread(cx).await;
3773
3774        let focus_handle = setup
3775            .thread_view
3776            .read_with(cx, |view, cx| view.focus_handle(cx));
3777        cx.update(|window, cx| {
3778            window.focus(&focus_handle, cx);
3779        });
3780
3781        setup.thread_view.update_in(cx, |_, window, cx| {
3782            window.dispatch_action(menu::Cancel.boxed_clone(), cx);
3783        });
3784
3785        cx.run_until_parked();
3786
3787        setup.thread.read_with(cx, |thread, _cx| {
3788            assert_eq!(thread.status(), ThreadStatus::Idle);
3789        });
3790    }
3791
3792    #[gpui::test]
3793    async fn test_escape_cancels_generation_from_editor_focus(cx: &mut TestAppContext) {
3794        init_test(cx);
3795
3796        let (setup, cx) = setup_generating_thread(cx).await;
3797
3798        let editor_focus_handle = setup
3799            .message_editor
3800            .read_with(cx, |editor, cx| editor.focus_handle(cx));
3801        cx.update(|window, cx| {
3802            window.focus(&editor_focus_handle, cx);
3803        });
3804
3805        setup.message_editor.update_in(cx, |_, window, cx| {
3806            window.dispatch_action(editor::actions::Cancel.boxed_clone(), cx);
3807        });
3808
3809        cx.run_until_parked();
3810
3811        setup.thread.read_with(cx, |thread, _cx| {
3812            assert_eq!(thread.status(), ThreadStatus::Idle);
3813        });
3814    }
3815
3816    #[gpui::test]
3817    async fn test_escape_when_idle_is_noop(cx: &mut TestAppContext) {
3818        init_test(cx);
3819
3820        let (thread_view, cx) =
3821            setup_thread_view(StubAgentServer::new(StubAgentConnection::new()), cx).await;
3822        add_to_workspace(thread_view.clone(), cx);
3823
3824        let thread = thread_view.read_with(cx, |view, cx| {
3825            view.as_active_thread().unwrap().read(cx).thread.clone()
3826        });
3827
3828        thread.read_with(cx, |thread, _cx| {
3829            assert_eq!(thread.status(), ThreadStatus::Idle);
3830        });
3831
3832        let focus_handle = thread_view.read_with(cx, |view, _cx| view.focus_handle.clone());
3833        cx.update(|window, cx| {
3834            window.focus(&focus_handle, cx);
3835        });
3836
3837        thread_view.update_in(cx, |_, window, cx| {
3838            window.dispatch_action(menu::Cancel.boxed_clone(), cx);
3839        });
3840
3841        cx.run_until_parked();
3842
3843        thread.read_with(cx, |thread, _cx| {
3844            assert_eq!(thread.status(), ThreadStatus::Idle);
3845        });
3846    }
3847
3848    #[gpui::test]
3849    async fn test_interrupt(cx: &mut TestAppContext) {
3850        init_test(cx);
3851
3852        let connection = StubAgentConnection::new();
3853
3854        let (thread_view, cx) =
3855            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
3856        add_to_workspace(thread_view.clone(), cx);
3857
3858        let message_editor = message_editor(&thread_view, cx);
3859        message_editor.update_in(cx, |editor, window, cx| {
3860            editor.set_text("Message 1", window, cx);
3861        });
3862        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
3863
3864        let (thread, session_id) = thread_view.read_with(cx, |view, cx| {
3865            let thread = view.as_active_thread().unwrap().read(cx).thread.clone();
3866
3867            (thread.clone(), thread.read(cx).session_id().clone())
3868        });
3869
3870        cx.run_until_parked();
3871
3872        cx.update(|_, cx| {
3873            connection.send_update(
3874                session_id.clone(),
3875                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
3876                    "Message 1 resp".into(),
3877                )),
3878                cx,
3879            );
3880        });
3881
3882        cx.run_until_parked();
3883
3884        thread.read_with(cx, |thread, cx| {
3885            assert_eq!(
3886                thread.to_markdown(cx),
3887                indoc::indoc! {"
3888                        ## User
3889
3890                        Message 1
3891
3892                        ## Assistant
3893
3894                        Message 1 resp
3895
3896                    "}
3897            )
3898        });
3899
3900        message_editor.update_in(cx, |editor, window, cx| {
3901            editor.set_text("Message 2", window, cx);
3902        });
3903        active_thread(&thread_view, cx)
3904            .update_in(cx, |view, window, cx| view.interrupt_and_send(window, cx));
3905
3906        cx.update(|_, cx| {
3907            // Simulate a response sent after beginning to cancel
3908            connection.send_update(
3909                session_id.clone(),
3910                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("onse".into())),
3911                cx,
3912            );
3913        });
3914
3915        cx.run_until_parked();
3916
3917        // Last Message 1 response should appear before Message 2
3918        thread.read_with(cx, |thread, cx| {
3919            assert_eq!(
3920                thread.to_markdown(cx),
3921                indoc::indoc! {"
3922                        ## User
3923
3924                        Message 1
3925
3926                        ## Assistant
3927
3928                        Message 1 response
3929
3930                        ## User
3931
3932                        Message 2
3933
3934                    "}
3935            )
3936        });
3937
3938        cx.update(|_, cx| {
3939            connection.send_update(
3940                session_id.clone(),
3941                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
3942                    "Message 2 response".into(),
3943                )),
3944                cx,
3945            );
3946            connection.end_turn(session_id.clone(), acp::StopReason::EndTurn);
3947        });
3948
3949        cx.run_until_parked();
3950
3951        thread.read_with(cx, |thread, cx| {
3952            assert_eq!(
3953                thread.to_markdown(cx),
3954                indoc::indoc! {"
3955                        ## User
3956
3957                        Message 1
3958
3959                        ## Assistant
3960
3961                        Message 1 response
3962
3963                        ## User
3964
3965                        Message 2
3966
3967                        ## Assistant
3968
3969                        Message 2 response
3970
3971                    "}
3972            )
3973        });
3974    }
3975
3976    #[gpui::test]
3977    async fn test_message_editing_insert_selections(cx: &mut TestAppContext) {
3978        init_test(cx);
3979
3980        let connection = StubAgentConnection::new();
3981        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
3982            acp::ContentChunk::new("Response".into()),
3983        )]);
3984
3985        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
3986        add_to_workspace(thread_view.clone(), cx);
3987
3988        let message_editor = message_editor(&thread_view, cx);
3989        message_editor.update_in(cx, |editor, window, cx| {
3990            editor.set_text("Original message to edit", window, cx)
3991        });
3992        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
3993        cx.run_until_parked();
3994
3995        let user_message_editor = thread_view.read_with(cx, |thread_view, cx| {
3996            thread_view
3997                .as_active_thread()
3998                .map(|active| &active.read(cx).entry_view_state)
3999                .as_ref()
4000                .unwrap()
4001                .read(cx)
4002                .entry(0)
4003                .expect("Should have at least one entry")
4004                .message_editor()
4005                .expect("Should have message editor")
4006                .clone()
4007        });
4008
4009        cx.focus(&user_message_editor);
4010        thread_view.read_with(cx, |view, cx| {
4011            assert_eq!(
4012                view.as_active_thread()
4013                    .and_then(|active| active.read(cx).editing_message),
4014                Some(0)
4015            );
4016        });
4017
4018        // Ensure to edit the focused message before proceeding otherwise, since
4019        // its content is not different from what was sent, focus will be lost.
4020        user_message_editor.update_in(cx, |editor, window, cx| {
4021            editor.set_text("Original message to edit with ", window, cx)
4022        });
4023
4024        // Create a simple buffer with some text so we can create a selection
4025        // that will then be added to the message being edited.
4026        let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| {
4027            (thread_view.workspace.clone(), thread_view.project.clone())
4028        });
4029        let buffer = project.update(cx, |project, cx| {
4030            project.create_local_buffer("let a = 10 + 10;", None, false, cx)
4031        });
4032
4033        workspace
4034            .update_in(cx, |workspace, window, cx| {
4035                let editor = cx.new(|cx| {
4036                    let mut editor =
4037                        Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
4038
4039                    editor.change_selections(Default::default(), window, cx, |selections| {
4040                        selections.select_ranges([MultiBufferOffset(8)..MultiBufferOffset(15)]);
4041                    });
4042
4043                    editor
4044                });
4045                workspace.add_item_to_active_pane(Box::new(editor), None, false, window, cx);
4046            })
4047            .unwrap();
4048
4049        thread_view.update_in(cx, |view, window, cx| {
4050            assert_eq!(
4051                view.as_active_thread()
4052                    .and_then(|active| active.read(cx).editing_message),
4053                Some(0)
4054            );
4055            view.insert_selections(window, cx);
4056        });
4057
4058        user_message_editor.read_with(cx, |editor, cx| {
4059            let text = editor.editor().read(cx).text(cx);
4060            let expected_text = String::from("Original message to edit with selection ");
4061
4062            assert_eq!(text, expected_text);
4063        });
4064    }
4065
4066    #[gpui::test]
4067    async fn test_insert_selections(cx: &mut TestAppContext) {
4068        init_test(cx);
4069
4070        let connection = StubAgentConnection::new();
4071        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
4072            acp::ContentChunk::new("Response".into()),
4073        )]);
4074
4075        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
4076        add_to_workspace(thread_view.clone(), cx);
4077
4078        let message_editor = message_editor(&thread_view, cx);
4079        message_editor.update_in(cx, |editor, window, cx| {
4080            editor.set_text("Can you review this snippet ", window, cx)
4081        });
4082
4083        // Create a simple buffer with some text so we can create a selection
4084        // that will then be added to the message being edited.
4085        let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| {
4086            (thread_view.workspace.clone(), thread_view.project.clone())
4087        });
4088        let buffer = project.update(cx, |project, cx| {
4089            project.create_local_buffer("let a = 10 + 10;", None, false, cx)
4090        });
4091
4092        workspace
4093            .update_in(cx, |workspace, window, cx| {
4094                let editor = cx.new(|cx| {
4095                    let mut editor =
4096                        Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
4097
4098                    editor.change_selections(Default::default(), window, cx, |selections| {
4099                        selections.select_ranges([MultiBufferOffset(8)..MultiBufferOffset(15)]);
4100                    });
4101
4102                    editor
4103                });
4104                workspace.add_item_to_active_pane(Box::new(editor), None, false, window, cx);
4105            })
4106            .unwrap();
4107
4108        thread_view.update_in(cx, |view, window, cx| {
4109            assert_eq!(
4110                view.as_active_thread()
4111                    .and_then(|active| active.read(cx).editing_message),
4112                None
4113            );
4114            view.insert_selections(window, cx);
4115        });
4116
4117        message_editor.read_with(cx, |editor, cx| {
4118            let text = editor.text(cx);
4119            let expected_txt = String::from("Can you review this snippet selection ");
4120
4121            assert_eq!(text, expected_txt);
4122        })
4123    }
4124
4125    #[gpui::test]
4126    async fn test_tool_permission_buttons_terminal_with_pattern(cx: &mut TestAppContext) {
4127        init_test(cx);
4128
4129        let tool_call_id = acp::ToolCallId::new("terminal-1");
4130        let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Run `cargo build --release`")
4131            .kind(acp::ToolKind::Edit);
4132
4133        let permission_options =
4134            ToolPermissionContext::new(TerminalTool::NAME, "cargo build --release")
4135                .build_permission_options();
4136
4137        let connection =
4138            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
4139                tool_call_id.clone(),
4140                permission_options,
4141            )]));
4142
4143        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
4144
4145        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
4146
4147        // Disable notifications to avoid popup windows
4148        cx.update(|_window, cx| {
4149            AgentSettings::override_global(
4150                AgentSettings {
4151                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
4152                    ..AgentSettings::get_global(cx).clone()
4153                },
4154                cx,
4155            );
4156        });
4157
4158        let message_editor = message_editor(&thread_view, cx);
4159        message_editor.update_in(cx, |editor, window, cx| {
4160            editor.set_text("Run cargo build", window, cx);
4161        });
4162
4163        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
4164
4165        cx.run_until_parked();
4166
4167        // Verify the tool call is in WaitingForConfirmation state with the expected options
4168        thread_view.read_with(cx, |thread_view, cx| {
4169            let thread = thread_view
4170                .as_active_thread()
4171                .expect("Thread should exist")
4172                .read(cx)
4173                .thread
4174                .clone();
4175            let thread = thread.read(cx);
4176
4177            let tool_call = thread.entries().iter().find_map(|entry| {
4178                if let acp_thread::AgentThreadEntry::ToolCall(call) = entry {
4179                    Some(call)
4180                } else {
4181                    None
4182                }
4183            });
4184
4185            assert!(tool_call.is_some(), "Expected a tool call entry");
4186            let tool_call = tool_call.unwrap();
4187
4188            // Verify it's waiting for confirmation
4189            assert!(
4190                matches!(
4191                    tool_call.status,
4192                    acp_thread::ToolCallStatus::WaitingForConfirmation { .. }
4193                ),
4194                "Expected WaitingForConfirmation status, got {:?}",
4195                tool_call.status
4196            );
4197
4198            // Verify the options count (granularity options only, no separate Deny option)
4199            if let acp_thread::ToolCallStatus::WaitingForConfirmation { options, .. } =
4200                &tool_call.status
4201            {
4202                let PermissionOptions::Dropdown(choices) = options else {
4203                    panic!("Expected dropdown permission options");
4204                };
4205
4206                assert_eq!(
4207                    choices.len(),
4208                    3,
4209                    "Expected 3 permission options (granularity only)"
4210                );
4211
4212                // Verify specific button labels (now using neutral names)
4213                let labels: Vec<&str> = choices
4214                    .iter()
4215                    .map(|choice| choice.allow.name.as_ref())
4216                    .collect();
4217                assert!(
4218                    labels.contains(&"Always for terminal"),
4219                    "Missing 'Always for terminal' option"
4220                );
4221                assert!(
4222                    labels.contains(&"Always for `cargo` commands"),
4223                    "Missing pattern option"
4224                );
4225                assert!(
4226                    labels.contains(&"Only this time"),
4227                    "Missing 'Only this time' option"
4228                );
4229            }
4230        });
4231    }
4232
4233    #[gpui::test]
4234    async fn test_tool_permission_buttons_edit_file_with_path_pattern(cx: &mut TestAppContext) {
4235        init_test(cx);
4236
4237        let tool_call_id = acp::ToolCallId::new("edit-file-1");
4238        let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Edit `src/main.rs`")
4239            .kind(acp::ToolKind::Edit);
4240
4241        let permission_options = ToolPermissionContext::new(EditFileTool::NAME, "src/main.rs")
4242            .build_permission_options();
4243
4244        let connection =
4245            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
4246                tool_call_id.clone(),
4247                permission_options,
4248            )]));
4249
4250        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
4251
4252        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
4253
4254        // Disable notifications
4255        cx.update(|_window, cx| {
4256            AgentSettings::override_global(
4257                AgentSettings {
4258                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
4259                    ..AgentSettings::get_global(cx).clone()
4260                },
4261                cx,
4262            );
4263        });
4264
4265        let message_editor = message_editor(&thread_view, cx);
4266        message_editor.update_in(cx, |editor, window, cx| {
4267            editor.set_text("Edit the main file", window, cx);
4268        });
4269
4270        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
4271
4272        cx.run_until_parked();
4273
4274        // Verify the options
4275        thread_view.read_with(cx, |thread_view, cx| {
4276            let thread = thread_view
4277                .as_active_thread()
4278                .expect("Thread should exist")
4279                .read(cx)
4280                .thread
4281                .clone();
4282            let thread = thread.read(cx);
4283
4284            let tool_call = thread.entries().iter().find_map(|entry| {
4285                if let acp_thread::AgentThreadEntry::ToolCall(call) = entry {
4286                    Some(call)
4287                } else {
4288                    None
4289                }
4290            });
4291
4292            assert!(tool_call.is_some(), "Expected a tool call entry");
4293            let tool_call = tool_call.unwrap();
4294
4295            if let acp_thread::ToolCallStatus::WaitingForConfirmation { options, .. } =
4296                &tool_call.status
4297            {
4298                let PermissionOptions::Dropdown(choices) = options else {
4299                    panic!("Expected dropdown permission options");
4300                };
4301
4302                let labels: Vec<&str> = choices
4303                    .iter()
4304                    .map(|choice| choice.allow.name.as_ref())
4305                    .collect();
4306                assert!(
4307                    labels.contains(&"Always for edit file"),
4308                    "Missing 'Always for edit file' option"
4309                );
4310                assert!(
4311                    labels.contains(&"Always for `src/`"),
4312                    "Missing path pattern option"
4313                );
4314            } else {
4315                panic!("Expected WaitingForConfirmation status");
4316            }
4317        });
4318    }
4319
4320    #[gpui::test]
4321    async fn test_tool_permission_buttons_fetch_with_domain_pattern(cx: &mut TestAppContext) {
4322        init_test(cx);
4323
4324        let tool_call_id = acp::ToolCallId::new("fetch-1");
4325        let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Fetch `https://docs.rs/gpui`")
4326            .kind(acp::ToolKind::Fetch);
4327
4328        let permission_options =
4329            ToolPermissionContext::new(FetchTool::NAME, "https://docs.rs/gpui")
4330                .build_permission_options();
4331
4332        let connection =
4333            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
4334                tool_call_id.clone(),
4335                permission_options,
4336            )]));
4337
4338        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
4339
4340        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
4341
4342        // Disable notifications
4343        cx.update(|_window, cx| {
4344            AgentSettings::override_global(
4345                AgentSettings {
4346                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
4347                    ..AgentSettings::get_global(cx).clone()
4348                },
4349                cx,
4350            );
4351        });
4352
4353        let message_editor = message_editor(&thread_view, cx);
4354        message_editor.update_in(cx, |editor, window, cx| {
4355            editor.set_text("Fetch the docs", window, cx);
4356        });
4357
4358        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
4359
4360        cx.run_until_parked();
4361
4362        // Verify the options
4363        thread_view.read_with(cx, |thread_view, cx| {
4364            let thread = thread_view
4365                .as_active_thread()
4366                .expect("Thread should exist")
4367                .read(cx)
4368                .thread
4369                .clone();
4370            let thread = thread.read(cx);
4371
4372            let tool_call = thread.entries().iter().find_map(|entry| {
4373                if let acp_thread::AgentThreadEntry::ToolCall(call) = entry {
4374                    Some(call)
4375                } else {
4376                    None
4377                }
4378            });
4379
4380            assert!(tool_call.is_some(), "Expected a tool call entry");
4381            let tool_call = tool_call.unwrap();
4382
4383            if let acp_thread::ToolCallStatus::WaitingForConfirmation { options, .. } =
4384                &tool_call.status
4385            {
4386                let PermissionOptions::Dropdown(choices) = options else {
4387                    panic!("Expected dropdown permission options");
4388                };
4389
4390                let labels: Vec<&str> = choices
4391                    .iter()
4392                    .map(|choice| choice.allow.name.as_ref())
4393                    .collect();
4394                assert!(
4395                    labels.contains(&"Always for fetch"),
4396                    "Missing 'Always for fetch' option"
4397                );
4398                assert!(
4399                    labels.contains(&"Always for `docs.rs`"),
4400                    "Missing domain pattern option"
4401                );
4402            } else {
4403                panic!("Expected WaitingForConfirmation status");
4404            }
4405        });
4406    }
4407
4408    #[gpui::test]
4409    async fn test_tool_permission_buttons_without_pattern(cx: &mut TestAppContext) {
4410        init_test(cx);
4411
4412        let tool_call_id = acp::ToolCallId::new("terminal-no-pattern-1");
4413        let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Run `./deploy.sh --production`")
4414            .kind(acp::ToolKind::Edit);
4415
4416        // No pattern button since ./deploy.sh doesn't match the alphanumeric pattern
4417        let permission_options =
4418            ToolPermissionContext::new(TerminalTool::NAME, "./deploy.sh --production")
4419                .build_permission_options();
4420
4421        let connection =
4422            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
4423                tool_call_id.clone(),
4424                permission_options,
4425            )]));
4426
4427        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
4428
4429        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
4430
4431        // Disable notifications
4432        cx.update(|_window, cx| {
4433            AgentSettings::override_global(
4434                AgentSettings {
4435                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
4436                    ..AgentSettings::get_global(cx).clone()
4437                },
4438                cx,
4439            );
4440        });
4441
4442        let message_editor = message_editor(&thread_view, cx);
4443        message_editor.update_in(cx, |editor, window, cx| {
4444            editor.set_text("Run the deploy script", window, cx);
4445        });
4446
4447        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
4448
4449        cx.run_until_parked();
4450
4451        // Verify only 2 options (no pattern button when command doesn't match pattern)
4452        thread_view.read_with(cx, |thread_view, cx| {
4453            let thread = thread_view
4454                .as_active_thread()
4455                .expect("Thread should exist")
4456                .read(cx)
4457                .thread
4458                .clone();
4459            let thread = thread.read(cx);
4460
4461            let tool_call = thread.entries().iter().find_map(|entry| {
4462                if let acp_thread::AgentThreadEntry::ToolCall(call) = entry {
4463                    Some(call)
4464                } else {
4465                    None
4466                }
4467            });
4468
4469            assert!(tool_call.is_some(), "Expected a tool call entry");
4470            let tool_call = tool_call.unwrap();
4471
4472            if let acp_thread::ToolCallStatus::WaitingForConfirmation { options, .. } =
4473                &tool_call.status
4474            {
4475                let PermissionOptions::Dropdown(choices) = options else {
4476                    panic!("Expected dropdown permission options");
4477                };
4478
4479                assert_eq!(
4480                    choices.len(),
4481                    2,
4482                    "Expected 2 permission options (no pattern option)"
4483                );
4484
4485                let labels: Vec<&str> = choices
4486                    .iter()
4487                    .map(|choice| choice.allow.name.as_ref())
4488                    .collect();
4489                assert!(
4490                    labels.contains(&"Always for terminal"),
4491                    "Missing 'Always for terminal' option"
4492                );
4493                assert!(
4494                    labels.contains(&"Only this time"),
4495                    "Missing 'Only this time' option"
4496                );
4497                // Should NOT contain a pattern option
4498                assert!(
4499                    !labels.iter().any(|l| l.contains("commands")),
4500                    "Should not have pattern option"
4501                );
4502            } else {
4503                panic!("Expected WaitingForConfirmation status");
4504            }
4505        });
4506    }
4507
4508    #[gpui::test]
4509    async fn test_authorize_tool_call_action_triggers_authorization(cx: &mut TestAppContext) {
4510        init_test(cx);
4511
4512        let tool_call_id = acp::ToolCallId::new("action-test-1");
4513        let tool_call =
4514            acp::ToolCall::new(tool_call_id.clone(), "Run `cargo test`").kind(acp::ToolKind::Edit);
4515
4516        let permission_options =
4517            ToolPermissionContext::new(TerminalTool::NAME, "cargo test").build_permission_options();
4518
4519        let connection =
4520            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
4521                tool_call_id.clone(),
4522                permission_options,
4523            )]));
4524
4525        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
4526
4527        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
4528        add_to_workspace(thread_view.clone(), cx);
4529
4530        cx.update(|_window, cx| {
4531            AgentSettings::override_global(
4532                AgentSettings {
4533                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
4534                    ..AgentSettings::get_global(cx).clone()
4535                },
4536                cx,
4537            );
4538        });
4539
4540        let message_editor = message_editor(&thread_view, cx);
4541        message_editor.update_in(cx, |editor, window, cx| {
4542            editor.set_text("Run tests", window, cx);
4543        });
4544
4545        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
4546
4547        cx.run_until_parked();
4548
4549        // Verify tool call is waiting for confirmation
4550        thread_view.read_with(cx, |thread_view, cx| {
4551            let thread = thread_view
4552                .as_active_thread()
4553                .expect("Thread should exist")
4554                .read(cx)
4555                .thread
4556                .clone();
4557            let thread = thread.read(cx);
4558            let tool_call = thread.first_tool_awaiting_confirmation();
4559            assert!(
4560                tool_call.is_some(),
4561                "Expected a tool call waiting for confirmation"
4562            );
4563        });
4564
4565        // Dispatch the AuthorizeToolCall action (simulating dropdown menu selection)
4566        thread_view.update_in(cx, |_, window, cx| {
4567            window.dispatch_action(
4568                crate::AuthorizeToolCall {
4569                    tool_call_id: "action-test-1".to_string(),
4570                    option_id: "allow".to_string(),
4571                    option_kind: "AllowOnce".to_string(),
4572                }
4573                .boxed_clone(),
4574                cx,
4575            );
4576        });
4577
4578        cx.run_until_parked();
4579
4580        // Verify tool call is no longer waiting for confirmation (was authorized)
4581        thread_view.read_with(cx, |thread_view, cx| {
4582                let thread = thread_view.as_active_thread().expect("Thread should exist").read(cx).thread.clone();
4583                let thread = thread.read(cx);
4584                let tool_call = thread.first_tool_awaiting_confirmation();
4585                assert!(
4586                    tool_call.is_none(),
4587                    "Tool call should no longer be waiting for confirmation after AuthorizeToolCall action"
4588                );
4589            });
4590    }
4591
4592    #[gpui::test]
4593    async fn test_authorize_tool_call_action_with_pattern_option(cx: &mut TestAppContext) {
4594        init_test(cx);
4595
4596        let tool_call_id = acp::ToolCallId::new("pattern-action-test-1");
4597        let tool_call =
4598            acp::ToolCall::new(tool_call_id.clone(), "Run `npm install`").kind(acp::ToolKind::Edit);
4599
4600        let permission_options = ToolPermissionContext::new(TerminalTool::NAME, "npm install")
4601            .build_permission_options();
4602
4603        let connection =
4604            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
4605                tool_call_id.clone(),
4606                permission_options.clone(),
4607            )]));
4608
4609        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
4610
4611        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
4612        add_to_workspace(thread_view.clone(), cx);
4613
4614        cx.update(|_window, cx| {
4615            AgentSettings::override_global(
4616                AgentSettings {
4617                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
4618                    ..AgentSettings::get_global(cx).clone()
4619                },
4620                cx,
4621            );
4622        });
4623
4624        let message_editor = message_editor(&thread_view, cx);
4625        message_editor.update_in(cx, |editor, window, cx| {
4626            editor.set_text("Install dependencies", window, cx);
4627        });
4628
4629        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
4630
4631        cx.run_until_parked();
4632
4633        // Find the pattern option ID
4634        let pattern_option = match &permission_options {
4635            PermissionOptions::Dropdown(choices) => choices
4636                .iter()
4637                .find(|choice| {
4638                    choice
4639                        .allow
4640                        .option_id
4641                        .0
4642                        .starts_with("always_allow_pattern:")
4643                })
4644                .map(|choice| &choice.allow)
4645                .expect("Should have a pattern option for npm command"),
4646            _ => panic!("Expected dropdown permission options"),
4647        };
4648
4649        // Dispatch action with the pattern option (simulating "Always allow `npm` commands")
4650        thread_view.update_in(cx, |_, window, cx| {
4651            window.dispatch_action(
4652                crate::AuthorizeToolCall {
4653                    tool_call_id: "pattern-action-test-1".to_string(),
4654                    option_id: pattern_option.option_id.0.to_string(),
4655                    option_kind: "AllowAlways".to_string(),
4656                }
4657                .boxed_clone(),
4658                cx,
4659            );
4660        });
4661
4662        cx.run_until_parked();
4663
4664        // Verify tool call was authorized
4665        thread_view.read_with(cx, |thread_view, cx| {
4666            let thread = thread_view
4667                .as_active_thread()
4668                .expect("Thread should exist")
4669                .read(cx)
4670                .thread
4671                .clone();
4672            let thread = thread.read(cx);
4673            let tool_call = thread.first_tool_awaiting_confirmation();
4674            assert!(
4675                tool_call.is_none(),
4676                "Tool call should be authorized after selecting pattern option"
4677            );
4678        });
4679    }
4680
4681    #[gpui::test]
4682    async fn test_granularity_selection_updates_state(cx: &mut TestAppContext) {
4683        init_test(cx);
4684
4685        let tool_call_id = acp::ToolCallId::new("granularity-test-1");
4686        let tool_call =
4687            acp::ToolCall::new(tool_call_id.clone(), "Run `cargo build`").kind(acp::ToolKind::Edit);
4688
4689        let permission_options = ToolPermissionContext::new(TerminalTool::NAME, "cargo build")
4690            .build_permission_options();
4691
4692        let connection =
4693            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
4694                tool_call_id.clone(),
4695                permission_options.clone(),
4696            )]));
4697
4698        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
4699
4700        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
4701        add_to_workspace(thread_view.clone(), cx);
4702
4703        cx.update(|_window, cx| {
4704            AgentSettings::override_global(
4705                AgentSettings {
4706                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
4707                    ..AgentSettings::get_global(cx).clone()
4708                },
4709                cx,
4710            );
4711        });
4712
4713        let message_editor = message_editor(&thread_view, cx);
4714        message_editor.update_in(cx, |editor, window, cx| {
4715            editor.set_text("Build the project", window, cx);
4716        });
4717
4718        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
4719
4720        cx.run_until_parked();
4721
4722        // Verify default granularity is the last option (index 2 = "Only this time")
4723        thread_view.read_with(cx, |thread_view, cx| {
4724            let state = thread_view.as_active_thread().unwrap();
4725            let selected = state
4726                .read(cx)
4727                .selected_permission_granularity
4728                .get(&tool_call_id);
4729            assert!(
4730                selected.is_none(),
4731                "Should have no selection initially (defaults to last)"
4732            );
4733        });
4734
4735        // Select the first option (index 0 = "Always for terminal")
4736        thread_view.update_in(cx, |_, window, cx| {
4737            window.dispatch_action(
4738                crate::SelectPermissionGranularity {
4739                    tool_call_id: "granularity-test-1".to_string(),
4740                    index: 0,
4741                }
4742                .boxed_clone(),
4743                cx,
4744            );
4745        });
4746
4747        cx.run_until_parked();
4748
4749        // Verify the selection was updated
4750        thread_view.read_with(cx, |thread_view, cx| {
4751            let state = thread_view.as_active_thread().unwrap();
4752            let selected = state
4753                .read(cx)
4754                .selected_permission_granularity
4755                .get(&tool_call_id);
4756            assert_eq!(selected, Some(&0), "Should have selected index 0");
4757        });
4758    }
4759
4760    #[gpui::test]
4761    async fn test_allow_button_uses_selected_granularity(cx: &mut TestAppContext) {
4762        init_test(cx);
4763
4764        let tool_call_id = acp::ToolCallId::new("allow-granularity-test-1");
4765        let tool_call =
4766            acp::ToolCall::new(tool_call_id.clone(), "Run `npm install`").kind(acp::ToolKind::Edit);
4767
4768        let permission_options = ToolPermissionContext::new(TerminalTool::NAME, "npm install")
4769            .build_permission_options();
4770
4771        // Verify we have the expected options
4772        let PermissionOptions::Dropdown(choices) = &permission_options else {
4773            panic!("Expected dropdown permission options");
4774        };
4775
4776        assert_eq!(choices.len(), 3);
4777        assert!(
4778            choices[0]
4779                .allow
4780                .option_id
4781                .0
4782                .contains("always_allow:terminal")
4783        );
4784        assert!(
4785            choices[1]
4786                .allow
4787                .option_id
4788                .0
4789                .contains("always_allow_pattern:terminal")
4790        );
4791        assert_eq!(choices[2].allow.option_id.0.as_ref(), "allow");
4792
4793        let connection =
4794            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
4795                tool_call_id.clone(),
4796                permission_options.clone(),
4797            )]));
4798
4799        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
4800
4801        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
4802        add_to_workspace(thread_view.clone(), cx);
4803
4804        cx.update(|_window, cx| {
4805            AgentSettings::override_global(
4806                AgentSettings {
4807                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
4808                    ..AgentSettings::get_global(cx).clone()
4809                },
4810                cx,
4811            );
4812        });
4813
4814        let message_editor = message_editor(&thread_view, cx);
4815        message_editor.update_in(cx, |editor, window, cx| {
4816            editor.set_text("Install dependencies", window, cx);
4817        });
4818
4819        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
4820
4821        cx.run_until_parked();
4822
4823        // Select the pattern option (index 1 = "Always for `npm` commands")
4824        thread_view.update_in(cx, |_, window, cx| {
4825            window.dispatch_action(
4826                crate::SelectPermissionGranularity {
4827                    tool_call_id: "allow-granularity-test-1".to_string(),
4828                    index: 1,
4829                }
4830                .boxed_clone(),
4831                cx,
4832            );
4833        });
4834
4835        cx.run_until_parked();
4836
4837        // Simulate clicking the Allow button by dispatching AllowOnce action
4838        // which should use the selected granularity
4839        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| {
4840            view.allow_once(&AllowOnce, window, cx)
4841        });
4842
4843        cx.run_until_parked();
4844
4845        // Verify tool call was authorized
4846        thread_view.read_with(cx, |thread_view, cx| {
4847            let thread = thread_view
4848                .as_active_thread()
4849                .expect("Thread should exist")
4850                .read(cx)
4851                .thread
4852                .clone();
4853            let thread = thread.read(cx);
4854            let tool_call = thread.first_tool_awaiting_confirmation();
4855            assert!(
4856                tool_call.is_none(),
4857                "Tool call should be authorized after Allow with pattern granularity"
4858            );
4859        });
4860    }
4861
4862    #[gpui::test]
4863    async fn test_deny_button_uses_selected_granularity(cx: &mut TestAppContext) {
4864        init_test(cx);
4865
4866        let tool_call_id = acp::ToolCallId::new("deny-granularity-test-1");
4867        let tool_call =
4868            acp::ToolCall::new(tool_call_id.clone(), "Run `git push`").kind(acp::ToolKind::Edit);
4869
4870        let permission_options =
4871            ToolPermissionContext::new(TerminalTool::NAME, "git push").build_permission_options();
4872
4873        let connection =
4874            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
4875                tool_call_id.clone(),
4876                permission_options.clone(),
4877            )]));
4878
4879        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
4880
4881        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
4882        add_to_workspace(thread_view.clone(), cx);
4883
4884        cx.update(|_window, cx| {
4885            AgentSettings::override_global(
4886                AgentSettings {
4887                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
4888                    ..AgentSettings::get_global(cx).clone()
4889                },
4890                cx,
4891            );
4892        });
4893
4894        let message_editor = message_editor(&thread_view, cx);
4895        message_editor.update_in(cx, |editor, window, cx| {
4896            editor.set_text("Push changes", window, cx);
4897        });
4898
4899        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
4900
4901        cx.run_until_parked();
4902
4903        // Use default granularity (last option = "Only this time")
4904        // Simulate clicking the Deny button
4905        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| {
4906            view.reject_once(&RejectOnce, window, cx)
4907        });
4908
4909        cx.run_until_parked();
4910
4911        // Verify tool call was rejected (no longer waiting for confirmation)
4912        thread_view.read_with(cx, |thread_view, cx| {
4913            let thread = thread_view
4914                .as_active_thread()
4915                .expect("Thread should exist")
4916                .read(cx)
4917                .thread
4918                .clone();
4919            let thread = thread.read(cx);
4920            let tool_call = thread.first_tool_awaiting_confirmation();
4921            assert!(
4922                tool_call.is_none(),
4923                "Tool call should be rejected after Deny"
4924            );
4925        });
4926    }
4927
4928    #[gpui::test]
4929    async fn test_option_id_transformation_for_allow() {
4930        let permission_options =
4931            ToolPermissionContext::new(TerminalTool::NAME, "cargo build --release")
4932                .build_permission_options();
4933
4934        let PermissionOptions::Dropdown(choices) = permission_options else {
4935            panic!("Expected dropdown permission options");
4936        };
4937
4938        let allow_ids: Vec<String> = choices
4939            .iter()
4940            .map(|choice| choice.allow.option_id.0.to_string())
4941            .collect();
4942
4943        assert!(allow_ids.contains(&"always_allow:terminal".to_string()));
4944        assert!(allow_ids.contains(&"allow".to_string()));
4945        assert!(
4946            allow_ids
4947                .iter()
4948                .any(|id| id.starts_with("always_allow_pattern:terminal\n")),
4949            "Missing allow pattern option"
4950        );
4951    }
4952
4953    #[gpui::test]
4954    async fn test_option_id_transformation_for_deny() {
4955        let permission_options =
4956            ToolPermissionContext::new(TerminalTool::NAME, "cargo build --release")
4957                .build_permission_options();
4958
4959        let PermissionOptions::Dropdown(choices) = permission_options else {
4960            panic!("Expected dropdown permission options");
4961        };
4962
4963        let deny_ids: Vec<String> = choices
4964            .iter()
4965            .map(|choice| choice.deny.option_id.0.to_string())
4966            .collect();
4967
4968        assert!(deny_ids.contains(&"always_deny:terminal".to_string()));
4969        assert!(deny_ids.contains(&"deny".to_string()));
4970        assert!(
4971            deny_ids
4972                .iter()
4973                .any(|id| id.starts_with("always_deny_pattern:terminal\n")),
4974            "Missing deny pattern option"
4975        );
4976    }
4977}