thread_view.rs

   1use acp_thread::{
   2    AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk,
   3    AuthRequired, LoadError, MentionUri, RetryStatus, ThreadStatus, ToolCall, ToolCallContent,
   4    ToolCallStatus, UserMessageId,
   5};
   6use acp_thread::{AgentConnection, Plan};
   7use action_log::ActionLog;
   8use agent_client_protocol::{self as acp, PromptCapabilities};
   9use agent_servers::{AgentServer, ClaudeCode};
  10use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting};
  11use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore};
  12use anyhow::bail;
  13use audio::{Audio, Sound};
  14use buffer_diff::BufferDiff;
  15use client::zed_urls;
  16use collections::{HashMap, HashSet};
  17use editor::scroll::Autoscroll;
  18use editor::{Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects};
  19use file_icons::FileIcons;
  20use fs::Fs;
  21use gpui::{
  22    Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem,
  23    EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset,
  24    ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription,
  25    Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window,
  26    WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, point,
  27    prelude::*, pulsating_between,
  28};
  29use language::Buffer;
  30
  31use language_model::LanguageModelRegistry;
  32use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
  33use project::{Project, ProjectEntryId};
  34use prompt_store::{PromptId, PromptStore};
  35use rope::Point;
  36use settings::{Settings as _, SettingsStore};
  37use std::cell::Cell;
  38use std::sync::Arc;
  39use std::time::Instant;
  40use std::{collections::BTreeMap, rc::Rc, time::Duration};
  41use text::Anchor;
  42use theme::ThemeSettings;
  43use ui::{
  44    Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle,
  45    Scrollbar, ScrollbarState, SpinnerLabel, Tooltip, prelude::*,
  46};
  47use util::{ResultExt, size::format_file_size, time::duration_alt_display};
  48use workspace::{CollaboratorId, Workspace};
  49use zed_actions::agent::{Chat, ToggleModelSelector};
  50use zed_actions::assistant::OpenRulesLibrary;
  51
  52use super::entry_view_state::EntryViewState;
  53use crate::acp::AcpModelSelectorPopover;
  54use crate::acp::entry_view_state::{EntryViewEvent, ViewEvent};
  55use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
  56use crate::agent_diff::AgentDiff;
  57use crate::profile_selector::{ProfileProvider, ProfileSelector};
  58
  59use crate::ui::preview::UsageCallout;
  60use crate::ui::{
  61    AgentNotification, AgentNotificationEvent, BurnModeTooltip, UnavailableEditingTooltip,
  62};
  63use crate::{
  64    AgentDiffPane, AgentPanel, ContinueThread, ContinueWithBurnMode, ExpandMessageEditor, Follow,
  65    KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector,
  66};
  67
  68const RESPONSE_PADDING_X: Pixels = px(19.);
  69pub const MIN_EDITOR_LINES: usize = 4;
  70pub const MAX_EDITOR_LINES: usize = 8;
  71
  72#[derive(Copy, Clone, Debug, PartialEq, Eq)]
  73enum ThreadFeedback {
  74    Positive,
  75    Negative,
  76}
  77
  78enum ThreadError {
  79    PaymentRequired,
  80    ModelRequestLimitReached(cloud_llm_client::Plan),
  81    ToolUseLimitReached,
  82    AuthenticationRequired(SharedString),
  83    Other(SharedString),
  84}
  85
  86impl ThreadError {
  87    fn from_err(error: anyhow::Error, agent: &Rc<dyn AgentServer>) -> Self {
  88        if error.is::<language_model::PaymentRequiredError>() {
  89            Self::PaymentRequired
  90        } else if error.is::<language_model::ToolUseLimitReachedError>() {
  91            Self::ToolUseLimitReached
  92        } else if let Some(error) =
  93            error.downcast_ref::<language_model::ModelRequestLimitReachedError>()
  94        {
  95            Self::ModelRequestLimitReached(error.plan)
  96        } else {
  97            let string = error.to_string();
  98            // TODO: we should have Gemini return better errors here.
  99            if agent.clone().downcast::<agent_servers::Gemini>().is_some()
 100                && string.contains("Could not load the default credentials")
 101                || string.contains("API key not valid")
 102                || string.contains("Request had invalid authentication credentials")
 103            {
 104                Self::AuthenticationRequired(string.into())
 105            } else {
 106                Self::Other(error.to_string().into())
 107            }
 108        }
 109    }
 110}
 111
 112impl ProfileProvider for Entity<agent2::Thread> {
 113    fn profile_id(&self, cx: &App) -> AgentProfileId {
 114        self.read(cx).profile().clone()
 115    }
 116
 117    fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) {
 118        self.update(cx, |thread, _cx| {
 119            thread.set_profile(profile_id);
 120        });
 121    }
 122
 123    fn profiles_supported(&self, cx: &App) -> bool {
 124        self.read(cx)
 125            .model()
 126            .is_some_and(|model| model.supports_tools())
 127    }
 128}
 129
 130#[derive(Default)]
 131struct ThreadFeedbackState {
 132    feedback: Option<ThreadFeedback>,
 133    comments_editor: Option<Entity<Editor>>,
 134}
 135
 136impl ThreadFeedbackState {
 137    pub fn submit(
 138        &mut self,
 139        thread: Entity<AcpThread>,
 140        feedback: ThreadFeedback,
 141        window: &mut Window,
 142        cx: &mut App,
 143    ) {
 144        let Some(telemetry) = thread.read(cx).connection().telemetry() else {
 145            return;
 146        };
 147
 148        if self.feedback == Some(feedback) {
 149            return;
 150        }
 151
 152        self.feedback = Some(feedback);
 153        match feedback {
 154            ThreadFeedback::Positive => {
 155                self.comments_editor = None;
 156            }
 157            ThreadFeedback::Negative => {
 158                self.comments_editor = Some(Self::build_feedback_comments_editor(window, cx));
 159            }
 160        }
 161        let session_id = thread.read(cx).session_id().clone();
 162        let agent_name = telemetry.agent_name();
 163        let task = telemetry.thread_data(&session_id, cx);
 164        let rating = match feedback {
 165            ThreadFeedback::Positive => "positive",
 166            ThreadFeedback::Negative => "negative",
 167        };
 168        cx.background_spawn(async move {
 169            let thread = task.await?;
 170            telemetry::event!(
 171                "Agent Thread Rated",
 172                session_id = session_id,
 173                rating = rating,
 174                agent = agent_name,
 175                thread = thread
 176            );
 177            anyhow::Ok(())
 178        })
 179        .detach_and_log_err(cx);
 180    }
 181
 182    pub fn submit_comments(&mut self, thread: Entity<AcpThread>, cx: &mut App) {
 183        let Some(telemetry) = thread.read(cx).connection().telemetry() else {
 184            return;
 185        };
 186
 187        let Some(comments) = self
 188            .comments_editor
 189            .as_ref()
 190            .map(|editor| editor.read(cx).text(cx))
 191            .filter(|text| !text.trim().is_empty())
 192        else {
 193            return;
 194        };
 195
 196        self.comments_editor.take();
 197
 198        let session_id = thread.read(cx).session_id().clone();
 199        let agent_name = telemetry.agent_name();
 200        let task = telemetry.thread_data(&session_id, cx);
 201        cx.background_spawn(async move {
 202            let thread = task.await?;
 203            telemetry::event!(
 204                "Agent Thread Feedback Comments",
 205                session_id = session_id,
 206                comments = comments,
 207                agent = agent_name,
 208                thread = thread
 209            );
 210            anyhow::Ok(())
 211        })
 212        .detach_and_log_err(cx);
 213    }
 214
 215    pub fn clear(&mut self) {
 216        *self = Self::default()
 217    }
 218
 219    pub fn dismiss_comments(&mut self) {
 220        self.comments_editor.take();
 221    }
 222
 223    fn build_feedback_comments_editor(window: &mut Window, cx: &mut App) -> Entity<Editor> {
 224        let buffer = cx.new(|cx| {
 225            let empty_string = String::new();
 226            MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx)
 227        });
 228
 229        let editor = cx.new(|cx| {
 230            let mut editor = Editor::new(
 231                editor::EditorMode::AutoHeight {
 232                    min_lines: 1,
 233                    max_lines: Some(4),
 234                },
 235                buffer,
 236                None,
 237                window,
 238                cx,
 239            );
 240            editor.set_placeholder_text(
 241                "What went wrong? Share your feedback so we can improve.",
 242                cx,
 243            );
 244            editor
 245        });
 246
 247        editor.read(cx).focus_handle(cx).focus(window);
 248        editor
 249    }
 250}
 251
 252pub struct AcpThreadView {
 253    agent: Rc<dyn AgentServer>,
 254    workspace: WeakEntity<Workspace>,
 255    project: Entity<Project>,
 256    thread_state: ThreadState,
 257    history_store: Entity<HistoryStore>,
 258    hovered_recent_history_item: Option<usize>,
 259    entry_view_state: Entity<EntryViewState>,
 260    message_editor: Entity<MessageEditor>,
 261    focus_handle: FocusHandle,
 262    model_selector: Option<Entity<AcpModelSelectorPopover>>,
 263    profile_selector: Option<Entity<ProfileSelector>>,
 264    notifications: Vec<WindowHandle<AgentNotification>>,
 265    notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
 266    thread_retry_status: Option<RetryStatus>,
 267    thread_error: Option<ThreadError>,
 268    thread_feedback: ThreadFeedbackState,
 269    list_state: ListState,
 270    scrollbar_state: ScrollbarState,
 271    auth_task: Option<Task<()>>,
 272    expanded_tool_calls: HashSet<acp::ToolCallId>,
 273    expanded_thinking_blocks: HashSet<(usize, usize)>,
 274    edits_expanded: bool,
 275    plan_expanded: bool,
 276    editor_expanded: bool,
 277    editing_message: Option<usize>,
 278    prompt_capabilities: Rc<Cell<PromptCapabilities>>,
 279    is_loading_contents: bool,
 280    _cancel_task: Option<Task<()>>,
 281    _subscriptions: [Subscription; 3],
 282}
 283
 284enum ThreadState {
 285    Loading {
 286        _task: Task<()>,
 287    },
 288    Ready {
 289        thread: Entity<AcpThread>,
 290        title_editor: Option<Entity<Editor>>,
 291        _subscriptions: Vec<Subscription>,
 292    },
 293    LoadError(LoadError),
 294    Unauthenticated {
 295        connection: Rc<dyn AgentConnection>,
 296        description: Option<Entity<Markdown>>,
 297        configuration_view: Option<AnyView>,
 298        pending_auth_method: Option<acp::AuthMethodId>,
 299        _subscription: Option<Subscription>,
 300    },
 301}
 302
 303impl AcpThreadView {
 304    pub fn new(
 305        agent: Rc<dyn AgentServer>,
 306        resume_thread: Option<DbThreadMetadata>,
 307        summarize_thread: Option<DbThreadMetadata>,
 308        workspace: WeakEntity<Workspace>,
 309        project: Entity<Project>,
 310        history_store: Entity<HistoryStore>,
 311        prompt_store: Option<Entity<PromptStore>>,
 312        window: &mut Window,
 313        cx: &mut Context<Self>,
 314    ) -> Self {
 315        let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
 316        let prevent_slash_commands = agent.clone().downcast::<ClaudeCode>().is_some();
 317
 318        let placeholder = if agent.name() == "Zed Agent" {
 319            format!("Message the {} — @ to include context", agent.name())
 320        } else {
 321            format!("Message {} — @ to include context", agent.name())
 322        };
 323
 324        let message_editor = cx.new(|cx| {
 325            let mut editor = MessageEditor::new(
 326                workspace.clone(),
 327                project.clone(),
 328                history_store.clone(),
 329                prompt_store.clone(),
 330                prompt_capabilities.clone(),
 331                placeholder,
 332                prevent_slash_commands,
 333                editor::EditorMode::AutoHeight {
 334                    min_lines: MIN_EDITOR_LINES,
 335                    max_lines: Some(MAX_EDITOR_LINES),
 336                },
 337                window,
 338                cx,
 339            );
 340            if let Some(entry) = summarize_thread {
 341                editor.insert_thread_summary(entry, window, cx);
 342            }
 343            editor
 344        });
 345
 346        let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0));
 347
 348        let entry_view_state = cx.new(|_| {
 349            EntryViewState::new(
 350                workspace.clone(),
 351                project.clone(),
 352                history_store.clone(),
 353                prompt_store.clone(),
 354                prompt_capabilities.clone(),
 355                prevent_slash_commands,
 356            )
 357        });
 358
 359        let subscriptions = [
 360            cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
 361            cx.subscribe_in(&message_editor, window, Self::handle_message_editor_event),
 362            cx.subscribe_in(&entry_view_state, window, Self::handle_entry_view_event),
 363        ];
 364
 365        Self {
 366            agent: agent.clone(),
 367            workspace: workspace.clone(),
 368            project: project.clone(),
 369            entry_view_state,
 370            thread_state: Self::initial_state(agent, resume_thread, workspace, project, window, cx),
 371            message_editor,
 372            model_selector: None,
 373            profile_selector: None,
 374            notifications: Vec::new(),
 375            notification_subscriptions: HashMap::default(),
 376            list_state: list_state.clone(),
 377            scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()),
 378            thread_retry_status: None,
 379            thread_error: None,
 380            thread_feedback: Default::default(),
 381            auth_task: None,
 382            expanded_tool_calls: HashSet::default(),
 383            expanded_thinking_blocks: HashSet::default(),
 384            editing_message: None,
 385            edits_expanded: false,
 386            plan_expanded: false,
 387            editor_expanded: false,
 388            history_store,
 389            hovered_recent_history_item: None,
 390            prompt_capabilities,
 391            is_loading_contents: false,
 392            _subscriptions: subscriptions,
 393            _cancel_task: None,
 394            focus_handle: cx.focus_handle(),
 395        }
 396    }
 397
 398    fn initial_state(
 399        agent: Rc<dyn AgentServer>,
 400        resume_thread: Option<DbThreadMetadata>,
 401        workspace: WeakEntity<Workspace>,
 402        project: Entity<Project>,
 403        window: &mut Window,
 404        cx: &mut Context<Self>,
 405    ) -> ThreadState {
 406        let root_dir = project
 407            .read(cx)
 408            .visible_worktrees(cx)
 409            .next()
 410            .map(|worktree| worktree.read(cx).abs_path())
 411            .unwrap_or_else(|| paths::home_dir().as_path().into());
 412
 413        let connect_task = agent.connect(&root_dir, &project, cx);
 414        let load_task = cx.spawn_in(window, async move |this, cx| {
 415            let connection = match connect_task.await {
 416                Ok(connection) => connection,
 417                Err(err) => {
 418                    this.update_in(cx, |this, window, cx| {
 419                        if err.downcast_ref::<LoadError>().is_some() {
 420                            this.handle_load_error(err, window, cx);
 421                        } else {
 422                            this.handle_thread_error(err, cx);
 423                        }
 424                        cx.notify();
 425                    })
 426                    .log_err();
 427                    return;
 428                }
 429            };
 430
 431            let result = if let Some(native_agent) = connection
 432                .clone()
 433                .downcast::<agent2::NativeAgentConnection>()
 434                && let Some(resume) = resume_thread.clone()
 435            {
 436                cx.update(|_, cx| {
 437                    native_agent
 438                        .0
 439                        .update(cx, |agent, cx| agent.open_thread(resume.id, cx))
 440                })
 441                .log_err()
 442            } else {
 443                cx.update(|_, cx| {
 444                    connection
 445                        .clone()
 446                        .new_thread(project.clone(), &root_dir, cx)
 447                })
 448                .log_err()
 449            };
 450
 451            let Some(result) = result else {
 452                return;
 453            };
 454
 455            let result = match result.await {
 456                Err(e) => match e.downcast::<acp_thread::AuthRequired>() {
 457                    Ok(err) => {
 458                        cx.update(|window, cx| {
 459                            Self::handle_auth_required(this, err, agent, connection, window, cx)
 460                        })
 461                        .log_err();
 462                        return;
 463                    }
 464                    Err(err) => Err(err),
 465                },
 466                Ok(thread) => Ok(thread),
 467            };
 468
 469            this.update_in(cx, |this, window, cx| {
 470                match result {
 471                    Ok(thread) => {
 472                        let action_log = thread.read(cx).action_log().clone();
 473
 474                        this.prompt_capabilities
 475                            .set(connection.prompt_capabilities());
 476
 477                        let count = thread.read(cx).entries().len();
 478                        this.list_state.splice(0..0, count);
 479                        this.entry_view_state.update(cx, |view_state, cx| {
 480                            for ix in 0..count {
 481                                view_state.sync_entry(ix, &thread, window, cx);
 482                            }
 483                        });
 484
 485                        if let Some(resume) = resume_thread {
 486                            this.history_store.update(cx, |history, cx| {
 487                                history.push_recently_opened_entry(
 488                                    HistoryEntryId::AcpThread(resume.id),
 489                                    cx,
 490                                );
 491                            });
 492                        }
 493
 494                        AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
 495
 496                        this.model_selector =
 497                            thread
 498                                .read(cx)
 499                                .connection()
 500                                .model_selector()
 501                                .map(|selector| {
 502                                    cx.new(|cx| {
 503                                        AcpModelSelectorPopover::new(
 504                                            thread.read(cx).session_id().clone(),
 505                                            selector,
 506                                            PopoverMenuHandle::default(),
 507                                            this.focus_handle(cx),
 508                                            window,
 509                                            cx,
 510                                        )
 511                                    })
 512                                });
 513
 514                        let mut subscriptions = vec![
 515                            cx.subscribe_in(&thread, window, Self::handle_thread_event),
 516                            cx.observe(&action_log, |_, _, cx| cx.notify()),
 517                        ];
 518
 519                        let title_editor =
 520                            if thread.update(cx, |thread, cx| thread.can_set_title(cx)) {
 521                                let editor = cx.new(|cx| {
 522                                    let mut editor = Editor::single_line(window, cx);
 523                                    editor.set_text(thread.read(cx).title(), window, cx);
 524                                    editor
 525                                });
 526                                subscriptions.push(cx.subscribe_in(
 527                                    &editor,
 528                                    window,
 529                                    Self::handle_title_editor_event,
 530                                ));
 531                                Some(editor)
 532                            } else {
 533                                None
 534                            };
 535                        this.thread_state = ThreadState::Ready {
 536                            thread,
 537                            title_editor,
 538                            _subscriptions: subscriptions,
 539                        };
 540                        this.message_editor.focus_handle(cx).focus(window);
 541
 542                        this.profile_selector = this.as_native_thread(cx).map(|thread| {
 543                            cx.new(|cx| {
 544                                ProfileSelector::new(
 545                                    <dyn Fs>::global(cx),
 546                                    Arc::new(thread.clone()),
 547                                    this.focus_handle(cx),
 548                                    cx,
 549                                )
 550                            })
 551                        });
 552
 553                        cx.notify();
 554                    }
 555                    Err(err) => {
 556                        this.handle_load_error(err, window, cx);
 557                    }
 558                };
 559            })
 560            .log_err();
 561        });
 562
 563        ThreadState::Loading { _task: load_task }
 564    }
 565
 566    fn handle_auth_required(
 567        this: WeakEntity<Self>,
 568        err: AuthRequired,
 569        agent: Rc<dyn AgentServer>,
 570        connection: Rc<dyn AgentConnection>,
 571        window: &mut Window,
 572        cx: &mut App,
 573    ) {
 574        let agent_name = agent.name();
 575        let (configuration_view, subscription) = if let Some(provider_id) = err.provider_id {
 576            let registry = LanguageModelRegistry::global(cx);
 577
 578            let sub = window.subscribe(&registry, cx, {
 579                let provider_id = provider_id.clone();
 580                let this = this.clone();
 581                move |_, ev, window, cx| {
 582                    if let language_model::Event::ProviderStateChanged(updated_provider_id) = &ev
 583                        && &provider_id == updated_provider_id
 584                    {
 585                        this.update(cx, |this, cx| {
 586                            this.thread_state = Self::initial_state(
 587                                agent.clone(),
 588                                None,
 589                                this.workspace.clone(),
 590                                this.project.clone(),
 591                                window,
 592                                cx,
 593                            );
 594                            cx.notify();
 595                        })
 596                        .ok();
 597                    }
 598                }
 599            });
 600
 601            let view = registry.read(cx).provider(&provider_id).map(|provider| {
 602                provider.configuration_view(
 603                    language_model::ConfigurationViewTargetAgent::Other(agent_name.clone()),
 604                    window,
 605                    cx,
 606                )
 607            });
 608
 609            (view, Some(sub))
 610        } else {
 611            (None, None)
 612        };
 613
 614        this.update(cx, |this, cx| {
 615            this.thread_state = ThreadState::Unauthenticated {
 616                pending_auth_method: None,
 617                connection,
 618                configuration_view,
 619                description: err
 620                    .description
 621                    .clone()
 622                    .map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))),
 623                _subscription: subscription,
 624            };
 625            if this.message_editor.focus_handle(cx).is_focused(window) {
 626                this.focus_handle.focus(window)
 627            }
 628            cx.notify();
 629        })
 630        .ok();
 631    }
 632
 633    fn handle_load_error(
 634        &mut self,
 635        err: anyhow::Error,
 636        window: &mut Window,
 637        cx: &mut Context<Self>,
 638    ) {
 639        if let Some(load_err) = err.downcast_ref::<LoadError>() {
 640            self.thread_state = ThreadState::LoadError(load_err.clone());
 641        } else {
 642            self.thread_state = ThreadState::LoadError(LoadError::Other(err.to_string().into()))
 643        }
 644        if self.message_editor.focus_handle(cx).is_focused(window) {
 645            self.focus_handle.focus(window)
 646        }
 647        cx.notify();
 648    }
 649
 650    pub fn workspace(&self) -> &WeakEntity<Workspace> {
 651        &self.workspace
 652    }
 653
 654    pub fn thread(&self) -> Option<&Entity<AcpThread>> {
 655        match &self.thread_state {
 656            ThreadState::Ready { thread, .. } => Some(thread),
 657            ThreadState::Unauthenticated { .. }
 658            | ThreadState::Loading { .. }
 659            | ThreadState::LoadError { .. } => None,
 660        }
 661    }
 662
 663    pub fn title(&self) -> SharedString {
 664        match &self.thread_state {
 665            ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(),
 666            ThreadState::Loading { .. } => "Loading…".into(),
 667            ThreadState::LoadError(_) => "Failed to load".into(),
 668        }
 669    }
 670
 671    pub fn title_editor(&self) -> Option<Entity<Editor>> {
 672        if let ThreadState::Ready { title_editor, .. } = &self.thread_state {
 673            title_editor.clone()
 674        } else {
 675            None
 676        }
 677    }
 678
 679    pub fn cancel_generation(&mut self, cx: &mut Context<Self>) {
 680        self.thread_error.take();
 681        self.thread_retry_status.take();
 682
 683        if let Some(thread) = self.thread() {
 684            self._cancel_task = Some(thread.update(cx, |thread, cx| thread.cancel(cx)));
 685        }
 686    }
 687
 688    pub fn expand_message_editor(
 689        &mut self,
 690        _: &ExpandMessageEditor,
 691        _window: &mut Window,
 692        cx: &mut Context<Self>,
 693    ) {
 694        self.set_editor_is_expanded(!self.editor_expanded, cx);
 695        cx.notify();
 696    }
 697
 698    fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
 699        self.editor_expanded = is_expanded;
 700        self.message_editor.update(cx, |editor, cx| {
 701            if is_expanded {
 702                editor.set_mode(
 703                    EditorMode::Full {
 704                        scale_ui_elements_with_buffer_font_size: false,
 705                        show_active_line_background: false,
 706                        sized_by_content: false,
 707                    },
 708                    cx,
 709                )
 710            } else {
 711                editor.set_mode(
 712                    EditorMode::AutoHeight {
 713                        min_lines: MIN_EDITOR_LINES,
 714                        max_lines: Some(MAX_EDITOR_LINES),
 715                    },
 716                    cx,
 717                )
 718            }
 719        });
 720        cx.notify();
 721    }
 722
 723    pub fn handle_title_editor_event(
 724        &mut self,
 725        title_editor: &Entity<Editor>,
 726        event: &EditorEvent,
 727        window: &mut Window,
 728        cx: &mut Context<Self>,
 729    ) {
 730        let Some(thread) = self.thread() else { return };
 731
 732        match event {
 733            EditorEvent::BufferEdited => {
 734                let new_title = title_editor.read(cx).text(cx);
 735                thread.update(cx, |thread, cx| {
 736                    thread
 737                        .set_title(new_title.into(), cx)
 738                        .detach_and_log_err(cx);
 739                })
 740            }
 741            EditorEvent::Blurred => {
 742                if title_editor.read(cx).text(cx).is_empty() {
 743                    title_editor.update(cx, |editor, cx| {
 744                        editor.set_text("New Thread", window, cx);
 745                    });
 746                }
 747            }
 748            _ => {}
 749        }
 750    }
 751
 752    pub fn handle_message_editor_event(
 753        &mut self,
 754        _: &Entity<MessageEditor>,
 755        event: &MessageEditorEvent,
 756        window: &mut Window,
 757        cx: &mut Context<Self>,
 758    ) {
 759        match event {
 760            MessageEditorEvent::Send => self.send(window, cx),
 761            MessageEditorEvent::Cancel => self.cancel_generation(cx),
 762            MessageEditorEvent::Focus => {
 763                self.cancel_editing(&Default::default(), window, cx);
 764            }
 765            MessageEditorEvent::LostFocus => {}
 766        }
 767    }
 768
 769    pub fn handle_entry_view_event(
 770        &mut self,
 771        _: &Entity<EntryViewState>,
 772        event: &EntryViewEvent,
 773        window: &mut Window,
 774        cx: &mut Context<Self>,
 775    ) {
 776        match &event.view_event {
 777            ViewEvent::NewDiff(tool_call_id) => {
 778                if AgentSettings::get_global(cx).expand_edit_card {
 779                    self.expanded_tool_calls.insert(tool_call_id.clone());
 780                }
 781            }
 782            ViewEvent::NewTerminal(tool_call_id) => {
 783                if AgentSettings::get_global(cx).expand_terminal_card {
 784                    self.expanded_tool_calls.insert(tool_call_id.clone());
 785                }
 786            }
 787            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => {
 788                if let Some(thread) = self.thread()
 789                    && let Some(AgentThreadEntry::UserMessage(user_message)) =
 790                        thread.read(cx).entries().get(event.entry_index)
 791                    && user_message.id.is_some()
 792                {
 793                    self.editing_message = Some(event.entry_index);
 794                    cx.notify();
 795                }
 796            }
 797            ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::LostFocus) => {
 798                if let Some(thread) = self.thread()
 799                    && let Some(AgentThreadEntry::UserMessage(user_message)) =
 800                        thread.read(cx).entries().get(event.entry_index)
 801                    && user_message.id.is_some()
 802                {
 803                    if editor.read(cx).text(cx).as_str() == user_message.content.to_markdown(cx) {
 804                        self.editing_message = None;
 805                        cx.notify();
 806                    }
 807                }
 808            }
 809            ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => {
 810                self.regenerate(event.entry_index, editor, window, cx);
 811            }
 812            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Cancel) => {
 813                self.cancel_editing(&Default::default(), window, cx);
 814            }
 815        }
 816    }
 817
 818    fn resume_chat(&mut self, cx: &mut Context<Self>) {
 819        self.thread_error.take();
 820        let Some(thread) = self.thread() else {
 821            return;
 822        };
 823
 824        let task = thread.update(cx, |thread, cx| thread.resume(cx));
 825        cx.spawn(async move |this, cx| {
 826            let result = task.await;
 827
 828            this.update(cx, |this, cx| {
 829                if let Err(err) = result {
 830                    this.handle_thread_error(err, cx);
 831                }
 832            })
 833        })
 834        .detach();
 835    }
 836
 837    fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 838        let Some(thread) = self.thread() else { return };
 839
 840        if self.is_loading_contents {
 841            return;
 842        }
 843
 844        self.history_store.update(cx, |history, cx| {
 845            history.push_recently_opened_entry(
 846                HistoryEntryId::AcpThread(thread.read(cx).session_id().clone()),
 847                cx,
 848            );
 849        });
 850
 851        if thread.read(cx).status() != ThreadStatus::Idle {
 852            self.stop_current_and_send_new_message(window, cx);
 853            return;
 854        }
 855
 856        let contents = self
 857            .message_editor
 858            .update(cx, |message_editor, cx| message_editor.contents(cx));
 859        self.send_impl(contents, window, cx)
 860    }
 861
 862    fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 863        let Some(thread) = self.thread().cloned() else {
 864            return;
 865        };
 866
 867        let cancelled = thread.update(cx, |thread, cx| thread.cancel(cx));
 868
 869        let contents = self
 870            .message_editor
 871            .update(cx, |message_editor, cx| message_editor.contents(cx));
 872
 873        cx.spawn_in(window, async move |this, cx| {
 874            cancelled.await;
 875
 876            this.update_in(cx, |this, window, cx| {
 877                this.send_impl(contents, window, cx);
 878            })
 879            .ok();
 880        })
 881        .detach();
 882    }
 883
 884    fn send_impl(
 885        &mut self,
 886        contents: Task<anyhow::Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>>,
 887        window: &mut Window,
 888        cx: &mut Context<Self>,
 889    ) {
 890        self.thread_error.take();
 891        self.editing_message.take();
 892        self.thread_feedback.clear();
 893
 894        let Some(thread) = self.thread().cloned() else {
 895            return;
 896        };
 897
 898        self.is_loading_contents = true;
 899        let guard = cx.new(|_| ());
 900        cx.observe_release(&guard, |this, _guard, cx| {
 901            this.is_loading_contents = false;
 902            cx.notify();
 903        })
 904        .detach();
 905
 906        let task = cx.spawn_in(window, async move |this, cx| {
 907            let (contents, tracked_buffers) = contents.await?;
 908
 909            if contents.is_empty() {
 910                return Ok(());
 911            }
 912
 913            this.update_in(cx, |this, window, cx| {
 914                this.set_editor_is_expanded(false, cx);
 915                this.scroll_to_bottom(cx);
 916                this.message_editor.update(cx, |message_editor, cx| {
 917                    message_editor.clear(window, cx);
 918                });
 919            })?;
 920            let send = thread.update(cx, |thread, cx| {
 921                thread.action_log().update(cx, |action_log, cx| {
 922                    for buffer in tracked_buffers {
 923                        action_log.buffer_read(buffer, cx)
 924                    }
 925                });
 926                drop(guard);
 927                thread.send(contents, cx)
 928            })?;
 929            send.await
 930        });
 931
 932        cx.spawn(async move |this, cx| {
 933            if let Err(err) = task.await {
 934                this.update(cx, |this, cx| {
 935                    this.handle_thread_error(err, cx);
 936                })
 937                .ok();
 938            }
 939        })
 940        .detach();
 941    }
 942
 943    fn cancel_editing(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
 944        let Some(thread) = self.thread().cloned() else {
 945            return;
 946        };
 947
 948        if let Some(index) = self.editing_message.take()
 949            && let Some(editor) = self
 950                .entry_view_state
 951                .read(cx)
 952                .entry(index)
 953                .and_then(|e| e.message_editor())
 954                .cloned()
 955        {
 956            editor.update(cx, |editor, cx| {
 957                if let Some(user_message) = thread
 958                    .read(cx)
 959                    .entries()
 960                    .get(index)
 961                    .and_then(|e| e.user_message())
 962                {
 963                    editor.set_message(user_message.chunks.clone(), window, cx);
 964                }
 965            })
 966        };
 967        self.focus_handle(cx).focus(window);
 968        cx.notify();
 969    }
 970
 971    fn regenerate(
 972        &mut self,
 973        entry_ix: usize,
 974        message_editor: &Entity<MessageEditor>,
 975        window: &mut Window,
 976        cx: &mut Context<Self>,
 977    ) {
 978        let Some(thread) = self.thread().cloned() else {
 979            return;
 980        };
 981        if self.is_loading_contents {
 982            return;
 983        }
 984
 985        let Some(user_message_id) = thread.update(cx, |thread, _| {
 986            thread.entries().get(entry_ix)?.user_message()?.id.clone()
 987        }) else {
 988            return;
 989        };
 990
 991        let contents = message_editor.update(cx, |message_editor, cx| message_editor.contents(cx));
 992
 993        let task = cx.spawn(async move |_, cx| {
 994            let contents = contents.await?;
 995            thread
 996                .update(cx, |thread, cx| thread.rewind(user_message_id, cx))?
 997                .await?;
 998            Ok(contents)
 999        });
1000        self.send_impl(task, window, cx);
1001    }
1002
1003    fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) {
1004        if let Some(thread) = self.thread() {
1005            AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err();
1006        }
1007    }
1008
1009    fn open_edited_buffer(
1010        &mut self,
1011        buffer: &Entity<Buffer>,
1012        window: &mut Window,
1013        cx: &mut Context<Self>,
1014    ) {
1015        let Some(thread) = self.thread() else {
1016            return;
1017        };
1018
1019        let Some(diff) =
1020            AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err()
1021        else {
1022            return;
1023        };
1024
1025        diff.update(cx, |diff, cx| {
1026            diff.move_to_path(PathKey::for_buffer(buffer, cx), window, cx)
1027        })
1028    }
1029
1030    fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
1031        let Some(thread) = self.as_native_thread(cx) else {
1032            return;
1033        };
1034        let project_context = thread.read(cx).project_context().read(cx);
1035
1036        let project_entry_ids = project_context
1037            .worktrees
1038            .iter()
1039            .flat_map(|worktree| worktree.rules_file.as_ref())
1040            .map(|rules_file| ProjectEntryId::from_usize(rules_file.project_entry_id))
1041            .collect::<Vec<_>>();
1042
1043        self.workspace
1044            .update(cx, move |workspace, cx| {
1045                // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
1046                // files clear. For example, if rules file 1 is already open but rules file 2 is not,
1047                // this would open and focus rules file 2 in a tab that is not next to rules file 1.
1048                let project = workspace.project().read(cx);
1049                let project_paths = project_entry_ids
1050                    .into_iter()
1051                    .flat_map(|entry_id| project.path_for_entry(entry_id, cx))
1052                    .collect::<Vec<_>>();
1053                for project_path in project_paths {
1054                    workspace
1055                        .open_path(project_path, None, true, window, cx)
1056                        .detach_and_log_err(cx);
1057                }
1058            })
1059            .ok();
1060    }
1061
1062    fn handle_thread_error(&mut self, error: anyhow::Error, cx: &mut Context<Self>) {
1063        self.thread_error = Some(ThreadError::from_err(error, &self.agent));
1064        cx.notify();
1065    }
1066
1067    fn clear_thread_error(&mut self, cx: &mut Context<Self>) {
1068        self.thread_error = None;
1069        cx.notify();
1070    }
1071
1072    fn handle_thread_event(
1073        &mut self,
1074        thread: &Entity<AcpThread>,
1075        event: &AcpThreadEvent,
1076        window: &mut Window,
1077        cx: &mut Context<Self>,
1078    ) {
1079        match event {
1080            AcpThreadEvent::NewEntry => {
1081                let len = thread.read(cx).entries().len();
1082                let index = len - 1;
1083                self.entry_view_state.update(cx, |view_state, cx| {
1084                    view_state.sync_entry(index, thread, window, cx)
1085                });
1086                self.list_state.splice(index..index, 1);
1087            }
1088            AcpThreadEvent::EntryUpdated(index) => {
1089                self.entry_view_state.update(cx, |view_state, cx| {
1090                    view_state.sync_entry(*index, thread, window, cx)
1091                });
1092            }
1093            AcpThreadEvent::EntriesRemoved(range) => {
1094                self.entry_view_state
1095                    .update(cx, |view_state, _cx| view_state.remove(range.clone()));
1096                self.list_state.splice(range.clone(), 0);
1097            }
1098            AcpThreadEvent::ToolAuthorizationRequired => {
1099                self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx);
1100            }
1101            AcpThreadEvent::Retry(retry) => {
1102                self.thread_retry_status = Some(retry.clone());
1103            }
1104            AcpThreadEvent::Stopped => {
1105                self.thread_retry_status.take();
1106                let used_tools = thread.read(cx).used_tools_since_last_user_message();
1107                self.notify_with_sound(
1108                    if used_tools {
1109                        "Finished running tools"
1110                    } else {
1111                        "New message"
1112                    },
1113                    IconName::ZedAssistant,
1114                    window,
1115                    cx,
1116                );
1117            }
1118            AcpThreadEvent::Error => {
1119                self.thread_retry_status.take();
1120                self.notify_with_sound(
1121                    "Agent stopped due to an error",
1122                    IconName::Warning,
1123                    window,
1124                    cx,
1125                );
1126            }
1127            AcpThreadEvent::LoadError(error) => {
1128                self.thread_retry_status.take();
1129                self.thread_state = ThreadState::LoadError(error.clone());
1130                if self.message_editor.focus_handle(cx).is_focused(window) {
1131                    self.focus_handle.focus(window)
1132                }
1133            }
1134            AcpThreadEvent::TitleUpdated => {
1135                let title = thread.read(cx).title();
1136                if let Some(title_editor) = self.title_editor() {
1137                    title_editor.update(cx, |editor, cx| {
1138                        if editor.text(cx) != title {
1139                            editor.set_text(title, window, cx);
1140                        }
1141                    });
1142                }
1143            }
1144            AcpThreadEvent::TokenUsageUpdated => {}
1145        }
1146        cx.notify();
1147    }
1148
1149    fn authenticate(
1150        &mut self,
1151        method: acp::AuthMethodId,
1152        window: &mut Window,
1153        cx: &mut Context<Self>,
1154    ) {
1155        let ThreadState::Unauthenticated {
1156            connection,
1157            pending_auth_method,
1158            configuration_view,
1159            ..
1160        } = &mut self.thread_state
1161        else {
1162            return;
1163        };
1164
1165        if method.0.as_ref() == "gemini-api-key" {
1166            let registry = LanguageModelRegistry::global(cx);
1167            let provider = registry
1168                .read(cx)
1169                .provider(&language_model::GOOGLE_PROVIDER_ID)
1170                .unwrap();
1171            if !provider.is_authenticated(cx) {
1172                let this = cx.weak_entity();
1173                let agent = self.agent.clone();
1174                let connection = connection.clone();
1175                window.defer(cx, |window, cx| {
1176                    Self::handle_auth_required(
1177                        this,
1178                        AuthRequired {
1179                            description: Some("GEMINI_API_KEY must be set".to_owned()),
1180                            provider_id: Some(language_model::GOOGLE_PROVIDER_ID),
1181                        },
1182                        agent,
1183                        connection,
1184                        window,
1185                        cx,
1186                    );
1187                });
1188                return;
1189            }
1190        } else if method.0.as_ref() == "vertex-ai"
1191            && std::env::var("GOOGLE_API_KEY").is_err()
1192            && (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
1193                || (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()))
1194        {
1195            let this = cx.weak_entity();
1196            let agent = self.agent.clone();
1197            let connection = connection.clone();
1198
1199            window.defer(cx, |window, cx| {
1200                    Self::handle_auth_required(
1201                        this,
1202                        AuthRequired {
1203                            description: Some(
1204                                "GOOGLE_API_KEY must be set in the environment to use Vertex AI authentication for Gemini CLI. Please export it and restart Zed."
1205                                    .to_owned(),
1206                            ),
1207                            provider_id: None,
1208                        },
1209                        agent,
1210                        connection,
1211                        window,
1212                        cx,
1213                    )
1214                });
1215            return;
1216        }
1217
1218        self.thread_error.take();
1219        configuration_view.take();
1220        pending_auth_method.replace(method.clone());
1221        let authenticate = connection.authenticate(method, cx);
1222        cx.notify();
1223        self.auth_task = Some(cx.spawn_in(window, {
1224            let project = self.project.clone();
1225            let agent = self.agent.clone();
1226            async move |this, cx| {
1227                let result = authenticate.await;
1228
1229                this.update_in(cx, |this, window, cx| {
1230                    if let Err(err) = result {
1231                        this.handle_thread_error(err, cx);
1232                    } else {
1233                        this.thread_state = Self::initial_state(
1234                            agent,
1235                            None,
1236                            this.workspace.clone(),
1237                            project.clone(),
1238                            window,
1239                            cx,
1240                        )
1241                    }
1242                    this.auth_task.take()
1243                })
1244                .ok();
1245            }
1246        }));
1247    }
1248
1249    fn authorize_tool_call(
1250        &mut self,
1251        tool_call_id: acp::ToolCallId,
1252        option_id: acp::PermissionOptionId,
1253        option_kind: acp::PermissionOptionKind,
1254        cx: &mut Context<Self>,
1255    ) {
1256        let Some(thread) = self.thread() else {
1257            return;
1258        };
1259        thread.update(cx, |thread, cx| {
1260            thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx);
1261        });
1262        cx.notify();
1263    }
1264
1265    fn rewind(&mut self, message_id: &UserMessageId, cx: &mut Context<Self>) {
1266        let Some(thread) = self.thread() else {
1267            return;
1268        };
1269        thread
1270            .update(cx, |thread, cx| thread.rewind(message_id.clone(), cx))
1271            .detach_and_log_err(cx);
1272        cx.notify();
1273    }
1274
1275    fn render_entry(
1276        &self,
1277        entry_ix: usize,
1278        total_entries: usize,
1279        entry: &AgentThreadEntry,
1280        window: &mut Window,
1281        cx: &Context<Self>,
1282    ) -> AnyElement {
1283        let primary = match &entry {
1284            AgentThreadEntry::UserMessage(message) => {
1285                let Some(editor) = self
1286                    .entry_view_state
1287                    .read(cx)
1288                    .entry(entry_ix)
1289                    .and_then(|entry| entry.message_editor())
1290                    .cloned()
1291                else {
1292                    return Empty.into_any_element();
1293                };
1294
1295                let editing = self.editing_message == Some(entry_ix);
1296                let editor_focus = editor.focus_handle(cx).is_focused(window);
1297                let focus_border = cx.theme().colors().border_focused;
1298
1299                let rules_item = if entry_ix == 0 {
1300                    self.render_rules_item(cx)
1301                } else {
1302                    None
1303                };
1304
1305                let agent_name = self.agent.name();
1306
1307                v_flex()
1308                    .id(("user_message", entry_ix))
1309                    .pt_2()
1310                    .pb_4()
1311                    .px_2()
1312                    .gap_1p5()
1313                    .w_full()
1314                    .children(rules_item)
1315                    .children(message.id.clone().and_then(|message_id| {
1316                        message.checkpoint.as_ref()?.show.then(|| {
1317                            h_flex()
1318                                .gap_2()
1319                                .child(Divider::horizontal())
1320                                .child(
1321                                    Button::new("restore-checkpoint", "Restore Checkpoint")
1322                                        .icon(IconName::Undo)
1323                                        .icon_size(IconSize::XSmall)
1324                                        .icon_position(IconPosition::Start)
1325                                        .label_size(LabelSize::XSmall)
1326                                        .icon_color(Color::Muted)
1327                                        .color(Color::Muted)
1328                                        .on_click(cx.listener(move |this, _, _window, cx| {
1329                                            this.rewind(&message_id, cx);
1330                                        }))
1331                                )
1332                                .child(Divider::horizontal())
1333                        })
1334                    }))
1335                    .child(
1336                        div()
1337                            .relative()
1338                            .child(
1339                                div()
1340                                    .py_3()
1341                                    .px_2()
1342                                    .rounded_md()
1343                                    .shadow_md()
1344                                    .bg(cx.theme().colors().editor_background)
1345                                    .border_1()
1346                                    .when(editing && !editor_focus, |this| this.border_dashed())
1347                                    .border_color(cx.theme().colors().border)
1348                                    .map(|this|{
1349                                        if editing && editor_focus {
1350                                            this.border_color(focus_border)
1351                                        } else if message.id.is_some() {
1352                                            this.hover(|s| s.border_color(focus_border.opacity(0.8)))
1353                                        } else {
1354                                            this
1355                                        }
1356                                    })
1357                                    .text_xs()
1358                                    .child(editor.clone().into_any_element()),
1359                            )
1360                            .when(editor_focus, |this| {
1361                                let base_container = h_flex()
1362                                    .absolute()
1363                                    .top_neg_3p5()
1364                                    .right_3()
1365                                    .gap_1()
1366                                    .rounded_sm()
1367                                    .border_1()
1368                                    .border_color(cx.theme().colors().border)
1369                                    .bg(cx.theme().colors().editor_background)
1370                                    .overflow_hidden();
1371
1372                                if message.id.is_some() {
1373                                    this.child(
1374                                        base_container
1375                                            .child(
1376                                                IconButton::new("cancel", IconName::Close)
1377                                                    .disabled(self.is_loading_contents)
1378                                                    .icon_color(Color::Error)
1379                                                    .icon_size(IconSize::XSmall)
1380                                                    .on_click(cx.listener(Self::cancel_editing))
1381                                            )
1382                                            .child(
1383                                                if self.is_loading_contents {
1384                                                    div()
1385                                                        .id("loading-edited-message-content")
1386                                                        .tooltip(Tooltip::text("Loading Added Context…"))
1387                                                        .child(loading_contents_spinner(IconSize::XSmall))
1388                                                        .into_any_element()
1389                                                } else {
1390                                                    IconButton::new("regenerate", IconName::Return)
1391                                                        .icon_color(Color::Muted)
1392                                                        .icon_size(IconSize::XSmall)
1393                                                        .tooltip(Tooltip::text(
1394                                                            "Editing will restart the thread from this point."
1395                                                        ))
1396                                                        .on_click(cx.listener({
1397                                                            let editor = editor.clone();
1398                                                            move |this, _, window, cx| {
1399                                                                this.regenerate(
1400                                                                    entry_ix, &editor, window, cx,
1401                                                                );
1402                                                            }
1403                                                        })).into_any_element()
1404                                                }
1405                                            )
1406                                    )
1407                                } else {
1408                                    this.child(
1409                                        base_container
1410                                            .border_dashed()
1411                                            .child(
1412                                                IconButton::new("editing_unavailable", IconName::PencilUnavailable)
1413                                                    .icon_size(IconSize::Small)
1414                                                    .icon_color(Color::Muted)
1415                                                    .style(ButtonStyle::Transparent)
1416                                                    .tooltip(move |_window, cx| {
1417                                                        cx.new(|_| UnavailableEditingTooltip::new(agent_name.clone()))
1418                                                            .into()
1419                                                    })
1420                                            )
1421                                    )
1422                                }
1423                            }),
1424                    )
1425                    .into_any()
1426            }
1427            AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
1428                let style = default_markdown_style(false, false, window, cx);
1429                let message_body = v_flex()
1430                    .w_full()
1431                    .gap_2p5()
1432                    .children(chunks.iter().enumerate().filter_map(
1433                        |(chunk_ix, chunk)| match chunk {
1434                            AssistantMessageChunk::Message { block } => {
1435                                block.markdown().map(|md| {
1436                                    self.render_markdown(md.clone(), style.clone())
1437                                        .into_any_element()
1438                                })
1439                            }
1440                            AssistantMessageChunk::Thought { block } => {
1441                                block.markdown().map(|md| {
1442                                    self.render_thinking_block(
1443                                        entry_ix,
1444                                        chunk_ix,
1445                                        md.clone(),
1446                                        window,
1447                                        cx,
1448                                    )
1449                                    .into_any_element()
1450                                })
1451                            }
1452                        },
1453                    ))
1454                    .into_any();
1455
1456                v_flex()
1457                    .px_5()
1458                    .py_1()
1459                    .when(entry_ix + 1 == total_entries, |this| this.pb_4())
1460                    .w_full()
1461                    .text_ui(cx)
1462                    .child(message_body)
1463                    .into_any()
1464            }
1465            AgentThreadEntry::ToolCall(tool_call) => {
1466                let has_terminals = tool_call.terminals().next().is_some();
1467
1468                div().w_full().py_1().px_5().map(|this| {
1469                    if has_terminals {
1470                        this.children(tool_call.terminals().map(|terminal| {
1471                            self.render_terminal_tool_call(
1472                                entry_ix, terminal, tool_call, window, cx,
1473                            )
1474                        }))
1475                    } else {
1476                        this.child(self.render_tool_call(entry_ix, tool_call, window, cx))
1477                    }
1478                })
1479            }
1480            .into_any(),
1481        };
1482
1483        let Some(thread) = self.thread() else {
1484            return primary;
1485        };
1486
1487        let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
1488        let primary = if entry_ix == total_entries - 1 && !is_generating {
1489            v_flex()
1490                .w_full()
1491                .child(primary)
1492                .child(self.render_thread_controls(cx))
1493                .when_some(
1494                    self.thread_feedback.comments_editor.clone(),
1495                    |this, editor| {
1496                        this.child(Self::render_feedback_feedback_editor(editor, window, cx))
1497                    },
1498                )
1499                .into_any_element()
1500        } else {
1501            primary
1502        };
1503
1504        if let Some(editing_index) = self.editing_message.as_ref()
1505            && *editing_index < entry_ix
1506        {
1507            let backdrop = div()
1508                .id(("backdrop", entry_ix))
1509                .size_full()
1510                .absolute()
1511                .inset_0()
1512                .bg(cx.theme().colors().panel_background)
1513                .opacity(0.8)
1514                .block_mouse_except_scroll()
1515                .on_click(cx.listener(Self::cancel_editing));
1516
1517            div()
1518                .relative()
1519                .child(primary)
1520                .child(backdrop)
1521                .into_any_element()
1522        } else {
1523            primary
1524        }
1525    }
1526
1527    fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
1528        cx.theme()
1529            .colors()
1530            .element_background
1531            .blend(cx.theme().colors().editor_foreground.opacity(0.025))
1532    }
1533
1534    fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
1535        cx.theme().colors().border.opacity(0.8)
1536    }
1537
1538    fn tool_name_font_size(&self) -> Rems {
1539        rems_from_px(13.)
1540    }
1541
1542    fn render_thinking_block(
1543        &self,
1544        entry_ix: usize,
1545        chunk_ix: usize,
1546        chunk: Entity<Markdown>,
1547        window: &Window,
1548        cx: &Context<Self>,
1549    ) -> AnyElement {
1550        let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
1551        let card_header_id = SharedString::from("inner-card-header");
1552        let key = (entry_ix, chunk_ix);
1553        let is_open = self.expanded_thinking_blocks.contains(&key);
1554
1555        v_flex()
1556            .child(
1557                h_flex()
1558                    .id(header_id)
1559                    .group(&card_header_id)
1560                    .relative()
1561                    .w_full()
1562                    .gap_1p5()
1563                    .child(
1564                        h_flex()
1565                            .size_4()
1566                            .justify_center()
1567                            .child(
1568                                div()
1569                                    .group_hover(&card_header_id, |s| s.invisible().w_0())
1570                                    .child(
1571                                        Icon::new(IconName::ToolThink)
1572                                            .size(IconSize::Small)
1573                                            .color(Color::Muted),
1574                                    ),
1575                            )
1576                            .child(
1577                                h_flex()
1578                                    .absolute()
1579                                    .inset_0()
1580                                    .invisible()
1581                                    .justify_center()
1582                                    .group_hover(&card_header_id, |s| s.visible())
1583                                    .child(
1584                                        Disclosure::new(("expand", entry_ix), is_open)
1585                                            .opened_icon(IconName::ChevronUp)
1586                                            .closed_icon(IconName::ChevronRight)
1587                                            .on_click(cx.listener({
1588                                                move |this, _event, _window, cx| {
1589                                                    if is_open {
1590                                                        this.expanded_thinking_blocks.remove(&key);
1591                                                    } else {
1592                                                        this.expanded_thinking_blocks.insert(key);
1593                                                    }
1594                                                    cx.notify();
1595                                                }
1596                                            })),
1597                                    ),
1598                            ),
1599                    )
1600                    .child(
1601                        div()
1602                            .text_size(self.tool_name_font_size())
1603                            .text_color(cx.theme().colors().text_muted)
1604                            .child("Thinking"),
1605                    )
1606                    .on_click(cx.listener({
1607                        move |this, _event, _window, cx| {
1608                            if is_open {
1609                                this.expanded_thinking_blocks.remove(&key);
1610                            } else {
1611                                this.expanded_thinking_blocks.insert(key);
1612                            }
1613                            cx.notify();
1614                        }
1615                    })),
1616            )
1617            .when(is_open, |this| {
1618                this.child(
1619                    div()
1620                        .relative()
1621                        .mt_1p5()
1622                        .ml(px(7.))
1623                        .pl_4()
1624                        .border_l_1()
1625                        .border_color(self.tool_card_border_color(cx))
1626                        .text_ui_sm(cx)
1627                        .child(self.render_markdown(
1628                            chunk,
1629                            default_markdown_style(false, false, window, cx),
1630                        )),
1631                )
1632            })
1633            .into_any_element()
1634    }
1635
1636    fn render_tool_call_icon(
1637        &self,
1638        group_name: SharedString,
1639        entry_ix: usize,
1640        is_collapsible: bool,
1641        is_open: bool,
1642        tool_call: &ToolCall,
1643        cx: &Context<Self>,
1644    ) -> Div {
1645        let tool_icon =
1646            if tool_call.kind == acp::ToolKind::Edit && tool_call.locations.len() == 1 {
1647                FileIcons::get_icon(&tool_call.locations[0].path, cx)
1648                    .map(Icon::from_path)
1649                    .unwrap_or(Icon::new(IconName::ToolPencil))
1650            } else {
1651                Icon::new(match tool_call.kind {
1652                    acp::ToolKind::Read => IconName::ToolRead,
1653                    acp::ToolKind::Edit => IconName::ToolPencil,
1654                    acp::ToolKind::Delete => IconName::ToolDeleteFile,
1655                    acp::ToolKind::Move => IconName::ArrowRightLeft,
1656                    acp::ToolKind::Search => IconName::ToolSearch,
1657                    acp::ToolKind::Execute => IconName::ToolTerminal,
1658                    acp::ToolKind::Think => IconName::ToolThink,
1659                    acp::ToolKind::Fetch => IconName::ToolWeb,
1660                    acp::ToolKind::Other => IconName::ToolHammer,
1661                })
1662            }
1663            .size(IconSize::Small)
1664            .color(Color::Muted);
1665
1666        let base_container = h_flex().flex_shrink_0().size_4().justify_center();
1667
1668        if is_collapsible {
1669            base_container
1670                .child(
1671                    div()
1672                        .group_hover(&group_name, |s| s.invisible().w_0())
1673                        .child(tool_icon),
1674                )
1675                .child(
1676                    h_flex()
1677                        .absolute()
1678                        .inset_0()
1679                        .invisible()
1680                        .justify_center()
1681                        .group_hover(&group_name, |s| s.visible())
1682                        .child(
1683                            Disclosure::new(("expand", entry_ix), is_open)
1684                                .opened_icon(IconName::ChevronUp)
1685                                .closed_icon(IconName::ChevronRight)
1686                                .on_click(cx.listener({
1687                                    let id = tool_call.id.clone();
1688                                    move |this: &mut Self, _, _, cx: &mut Context<Self>| {
1689                                        if is_open {
1690                                            this.expanded_tool_calls.remove(&id);
1691                                        } else {
1692                                            this.expanded_tool_calls.insert(id.clone());
1693                                        }
1694                                        cx.notify();
1695                                    }
1696                                })),
1697                        ),
1698                )
1699        } else {
1700            base_container.child(tool_icon)
1701        }
1702    }
1703
1704    fn render_tool_call(
1705        &self,
1706        entry_ix: usize,
1707        tool_call: &ToolCall,
1708        window: &Window,
1709        cx: &Context<Self>,
1710    ) -> Div {
1711        let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix));
1712        let card_header_id = SharedString::from("inner-tool-call-header");
1713
1714        let in_progress = match &tool_call.status {
1715            ToolCallStatus::InProgress => true,
1716            _ => false,
1717        };
1718
1719        let failed_or_canceled = match &tool_call.status {
1720            ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true,
1721            _ => false,
1722        };
1723
1724        let failed_tool_call = matches!(
1725            tool_call.status,
1726            ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed
1727        );
1728        let needs_confirmation = matches!(
1729            tool_call.status,
1730            ToolCallStatus::WaitingForConfirmation { .. }
1731        );
1732        let is_edit =
1733            matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some();
1734        let use_card_layout = needs_confirmation || is_edit;
1735
1736        let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
1737
1738        let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
1739
1740        let gradient_overlay = |color: Hsla| {
1741            div()
1742                .absolute()
1743                .top_0()
1744                .right_0()
1745                .w_16()
1746                .h_full()
1747                .bg(linear_gradient(
1748                    90.,
1749                    linear_color_stop(color, 1.),
1750                    linear_color_stop(color.opacity(0.2), 0.),
1751                ))
1752        };
1753        let gradient_color = if use_card_layout {
1754            self.tool_card_header_bg(cx)
1755        } else {
1756            cx.theme().colors().panel_background
1757        };
1758
1759        let tool_output_display = if is_open {
1760            match &tool_call.status {
1761                ToolCallStatus::WaitingForConfirmation { options, .. } => {
1762                    v_flex()
1763                        .w_full()
1764                        .children(tool_call.content.iter().map(|content| {
1765                            div()
1766                                .child(self.render_tool_call_content(
1767                                    entry_ix, content, tool_call, window, cx,
1768                                ))
1769                                .into_any_element()
1770                        }))
1771                        .child(self.render_permission_buttons(
1772                            options,
1773                            entry_ix,
1774                            tool_call.id.clone(),
1775                            tool_call.content.is_empty(),
1776                            cx,
1777                        ))
1778                        .into_any()
1779                }
1780                ToolCallStatus::Pending | ToolCallStatus::InProgress
1781                    if is_edit
1782                        && tool_call.content.is_empty()
1783                        && self.as_native_connection(cx).is_some() =>
1784                {
1785                    self.render_diff_loading(cx).into_any()
1786                }
1787                ToolCallStatus::Pending
1788                | ToolCallStatus::InProgress
1789                | ToolCallStatus::Completed
1790                | ToolCallStatus::Failed
1791                | ToolCallStatus::Canceled => v_flex()
1792                    .w_full()
1793                    .children(tool_call.content.iter().map(|content| {
1794                        div().child(
1795                            self.render_tool_call_content(entry_ix, content, tool_call, window, cx),
1796                        )
1797                    }))
1798                    .into_any(),
1799                ToolCallStatus::Rejected => Empty.into_any(),
1800            }
1801            .into()
1802        } else {
1803            None
1804        };
1805
1806        v_flex()
1807            .when(use_card_layout, |this| {
1808                this.rounded_md()
1809                    .border_1()
1810                    .border_color(self.tool_card_border_color(cx))
1811                    .bg(cx.theme().colors().editor_background)
1812                    .overflow_hidden()
1813            })
1814            .child(
1815                h_flex()
1816                    .id(header_id)
1817                    .relative()
1818                    .w_full()
1819                    .max_w_full()
1820                    .gap_1()
1821                    .when(use_card_layout, |this| {
1822                        this.pl_1p5()
1823                            .pr_1()
1824                            .py_0p5()
1825                            .rounded_t_md()
1826                            .when(is_open && !failed_tool_call, |this| {
1827                                this.border_b_1()
1828                                    .border_color(self.tool_card_border_color(cx))
1829                            })
1830                            .bg(self.tool_card_header_bg(cx))
1831                    })
1832                    .child(
1833                        h_flex()
1834                            .group(&card_header_id)
1835                            .relative()
1836                            .w_full()
1837                            .h(window.line_height() - px(2.))
1838                            .text_size(self.tool_name_font_size())
1839                            .child(self.render_tool_call_icon(
1840                                card_header_id,
1841                                entry_ix,
1842                                is_collapsible,
1843                                is_open,
1844                                tool_call,
1845                                cx,
1846                            ))
1847                            .child(if tool_call.locations.len() == 1 {
1848                                let name = tool_call.locations[0]
1849                                    .path
1850                                    .file_name()
1851                                    .unwrap_or_default()
1852                                    .display()
1853                                    .to_string();
1854
1855                                h_flex()
1856                                    .id(("open-tool-call-location", entry_ix))
1857                                    .w_full()
1858                                    .max_w_full()
1859                                    .px_1p5()
1860                                    .rounded_sm()
1861                                    .overflow_x_scroll()
1862                                    .hover(|label| {
1863                                        label.bg(cx.theme().colors().element_hover.opacity(0.5))
1864                                    })
1865                                    .map(|this| {
1866                                        if use_card_layout {
1867                                            this.text_color(cx.theme().colors().text)
1868                                        } else {
1869                                            this.text_color(cx.theme().colors().text_muted)
1870                                        }
1871                                    })
1872                                    .child(name)
1873                                    .tooltip(Tooltip::text("Jump to File"))
1874                                    .on_click(cx.listener(move |this, _, window, cx| {
1875                                        this.open_tool_call_location(entry_ix, 0, window, cx);
1876                                    }))
1877                                    .into_any_element()
1878                            } else {
1879                                h_flex()
1880                                    .id("non-card-label-container")
1881                                    .relative()
1882                                    .w_full()
1883                                    .max_w_full()
1884                                    .ml_1p5()
1885                                    .overflow_hidden()
1886                                    .child(h_flex().pr_8().child(self.render_markdown(
1887                                        tool_call.label.clone(),
1888                                        default_markdown_style(false, true, window, cx),
1889                                    )))
1890                                    .child(gradient_overlay(gradient_color))
1891                                    .on_click(cx.listener({
1892                                        let id = tool_call.id.clone();
1893                                        move |this: &mut Self, _, _, cx: &mut Context<Self>| {
1894                                            if is_open {
1895                                                this.expanded_tool_calls.remove(&id);
1896                                            } else {
1897                                                this.expanded_tool_calls.insert(id.clone());
1898                                            }
1899                                            cx.notify();
1900                                        }
1901                                    }))
1902                                    .into_any()
1903                            }),
1904                    )
1905                    .when(in_progress && use_card_layout, |this| {
1906                        this.child(
1907                            div().absolute().right_2().child(
1908                                Icon::new(IconName::ArrowCircle)
1909                                    .color(Color::Muted)
1910                                    .size(IconSize::Small)
1911                                    .with_animation(
1912                                        "running",
1913                                        Animation::new(Duration::from_secs(3)).repeat(),
1914                                        |icon, delta| {
1915                                            icon.transform(Transformation::rotate(percentage(
1916                                                delta,
1917                                            )))
1918                                        },
1919                                    ),
1920                            ),
1921                        )
1922                    })
1923                    .when(failed_or_canceled, |this| {
1924                        this.child(
1925                            div().absolute().right_2().child(
1926                                Icon::new(IconName::Close)
1927                                    .color(Color::Error)
1928                                    .size(IconSize::Small),
1929                            ),
1930                        )
1931                    }),
1932            )
1933            .children(tool_output_display)
1934    }
1935
1936    fn render_tool_call_content(
1937        &self,
1938        entry_ix: usize,
1939        content: &ToolCallContent,
1940        tool_call: &ToolCall,
1941        window: &Window,
1942        cx: &Context<Self>,
1943    ) -> AnyElement {
1944        match content {
1945            ToolCallContent::ContentBlock(content) => {
1946                if let Some(resource_link) = content.resource_link() {
1947                    self.render_resource_link(resource_link, cx)
1948                } else if let Some(markdown) = content.markdown() {
1949                    self.render_markdown_output(markdown.clone(), tool_call.id.clone(), window, cx)
1950                } else {
1951                    Empty.into_any_element()
1952                }
1953            }
1954            ToolCallContent::Diff(diff) => self.render_diff_editor(entry_ix, diff, tool_call, cx),
1955            ToolCallContent::Terminal(terminal) => {
1956                self.render_terminal_tool_call(entry_ix, terminal, tool_call, window, cx)
1957            }
1958        }
1959    }
1960
1961    fn render_markdown_output(
1962        &self,
1963        markdown: Entity<Markdown>,
1964        tool_call_id: acp::ToolCallId,
1965        window: &Window,
1966        cx: &Context<Self>,
1967    ) -> AnyElement {
1968        let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id));
1969
1970        v_flex()
1971            .mt_1p5()
1972            .ml(px(7.))
1973            .px_3p5()
1974            .gap_2()
1975            .border_l_1()
1976            .border_color(self.tool_card_border_color(cx))
1977            .text_sm()
1978            .text_color(cx.theme().colors().text_muted)
1979            .child(self.render_markdown(markdown, default_markdown_style(false, false, window, cx)))
1980            .child(
1981                IconButton::new(button_id, IconName::ChevronUp)
1982                    .full_width()
1983                    .style(ButtonStyle::Outlined)
1984                    .icon_color(Color::Muted)
1985                    .on_click(cx.listener({
1986                        move |this: &mut Self, _, _, cx: &mut Context<Self>| {
1987                            this.expanded_tool_calls.remove(&tool_call_id);
1988                            cx.notify();
1989                        }
1990                    })),
1991            )
1992            .into_any_element()
1993    }
1994
1995    fn render_resource_link(
1996        &self,
1997        resource_link: &acp::ResourceLink,
1998        cx: &Context<Self>,
1999    ) -> AnyElement {
2000        let uri: SharedString = resource_link.uri.clone().into();
2001
2002        let label: SharedString = if let Some(path) = resource_link.uri.strip_prefix("file://") {
2003            path.to_string().into()
2004        } else {
2005            uri.clone()
2006        };
2007
2008        let button_id = SharedString::from(format!("item-{}", uri));
2009
2010        div()
2011            .ml(px(7.))
2012            .pl_2p5()
2013            .border_l_1()
2014            .border_color(self.tool_card_border_color(cx))
2015            .overflow_hidden()
2016            .child(
2017                Button::new(button_id, label)
2018                    .label_size(LabelSize::Small)
2019                    .color(Color::Muted)
2020                    .icon(IconName::ArrowUpRight)
2021                    .icon_size(IconSize::XSmall)
2022                    .icon_color(Color::Muted)
2023                    .truncate(true)
2024                    .on_click(cx.listener({
2025                        let workspace = self.workspace.clone();
2026                        move |_, _, window, cx: &mut Context<Self>| {
2027                            Self::open_link(uri.clone(), &workspace, window, cx);
2028                        }
2029                    })),
2030            )
2031            .into_any_element()
2032    }
2033
2034    fn render_permission_buttons(
2035        &self,
2036        options: &[acp::PermissionOption],
2037        entry_ix: usize,
2038        tool_call_id: acp::ToolCallId,
2039        empty_content: bool,
2040        cx: &Context<Self>,
2041    ) -> Div {
2042        h_flex()
2043            .py_1()
2044            .pl_2()
2045            .pr_1()
2046            .gap_1()
2047            .justify_between()
2048            .flex_wrap()
2049            .when(!empty_content, |this| {
2050                this.border_t_1()
2051                    .border_color(self.tool_card_border_color(cx))
2052            })
2053            .child(
2054                div()
2055                    .min_w(rems_from_px(145.))
2056                    .child(LoadingLabel::new("Waiting for Confirmation").size(LabelSize::Small)),
2057            )
2058            .child(h_flex().gap_0p5().children(options.iter().map(|option| {
2059                let option_id = SharedString::from(option.id.0.clone());
2060                Button::new((option_id, entry_ix), option.name.clone())
2061                    .map(|this| match option.kind {
2062                        acp::PermissionOptionKind::AllowOnce => {
2063                            this.icon(IconName::Check).icon_color(Color::Success)
2064                        }
2065                        acp::PermissionOptionKind::AllowAlways => {
2066                            this.icon(IconName::CheckDouble).icon_color(Color::Success)
2067                        }
2068                        acp::PermissionOptionKind::RejectOnce => {
2069                            this.icon(IconName::Close).icon_color(Color::Error)
2070                        }
2071                        acp::PermissionOptionKind::RejectAlways => {
2072                            this.icon(IconName::Close).icon_color(Color::Error)
2073                        }
2074                    })
2075                    .icon_position(IconPosition::Start)
2076                    .icon_size(IconSize::XSmall)
2077                    .label_size(LabelSize::Small)
2078                    .on_click(cx.listener({
2079                        let tool_call_id = tool_call_id.clone();
2080                        let option_id = option.id.clone();
2081                        let option_kind = option.kind;
2082                        move |this, _, _, cx| {
2083                            this.authorize_tool_call(
2084                                tool_call_id.clone(),
2085                                option_id.clone(),
2086                                option_kind,
2087                                cx,
2088                            );
2089                        }
2090                    }))
2091            })))
2092    }
2093
2094    fn render_diff_loading(&self, cx: &Context<Self>) -> AnyElement {
2095        let bar = |n: u64, width_class: &str| {
2096            let bg_color = cx.theme().colors().element_active;
2097            let base = h_flex().h_1().rounded_full();
2098
2099            let modified = match width_class {
2100                "w_4_5" => base.w_3_4(),
2101                "w_1_4" => base.w_1_4(),
2102                "w_2_4" => base.w_2_4(),
2103                "w_3_5" => base.w_3_5(),
2104                "w_2_5" => base.w_2_5(),
2105                _ => base.w_1_2(),
2106            };
2107
2108            modified.with_animation(
2109                ElementId::Integer(n),
2110                Animation::new(Duration::from_secs(2)).repeat(),
2111                move |tab, delta| {
2112                    let delta = (delta - 0.15 * n as f32) / 0.7;
2113                    let delta = 1.0 - (0.5 - delta).abs() * 2.;
2114                    let delta = ease_in_out(delta.clamp(0., 1.));
2115                    let delta = 0.1 + 0.9 * delta;
2116
2117                    tab.bg(bg_color.opacity(delta))
2118                },
2119            )
2120        };
2121
2122        v_flex()
2123            .p_3()
2124            .gap_1()
2125            .rounded_b_md()
2126            .bg(cx.theme().colors().editor_background)
2127            .child(bar(0, "w_4_5"))
2128            .child(bar(1, "w_1_4"))
2129            .child(bar(2, "w_2_4"))
2130            .child(bar(3, "w_3_5"))
2131            .child(bar(4, "w_2_5"))
2132            .into_any_element()
2133    }
2134
2135    fn render_diff_editor(
2136        &self,
2137        entry_ix: usize,
2138        diff: &Entity<acp_thread::Diff>,
2139        tool_call: &ToolCall,
2140        cx: &Context<Self>,
2141    ) -> AnyElement {
2142        let tool_progress = matches!(
2143            &tool_call.status,
2144            ToolCallStatus::InProgress | ToolCallStatus::Pending
2145        );
2146
2147        v_flex()
2148            .h_full()
2149            .child(
2150                if let Some(entry) = self.entry_view_state.read(cx).entry(entry_ix)
2151                    && let Some(editor) = entry.editor_for_diff(diff)
2152                    && diff.read(cx).has_revealed_range(cx)
2153                {
2154                    editor.into_any_element()
2155                } else if tool_progress && self.as_native_connection(cx).is_some() {
2156                    self.render_diff_loading(cx)
2157                } else {
2158                    Empty.into_any()
2159                },
2160            )
2161            .into_any()
2162    }
2163
2164    fn render_terminal_tool_call(
2165        &self,
2166        entry_ix: usize,
2167        terminal: &Entity<acp_thread::Terminal>,
2168        tool_call: &ToolCall,
2169        window: &Window,
2170        cx: &Context<Self>,
2171    ) -> AnyElement {
2172        let terminal_data = terminal.read(cx);
2173        let working_dir = terminal_data.working_dir();
2174        let command = terminal_data.command();
2175        let started_at = terminal_data.started_at();
2176
2177        let tool_failed = matches!(
2178            &tool_call.status,
2179            ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed
2180        );
2181
2182        let output = terminal_data.output();
2183        let command_finished = output.is_some();
2184        let truncated_output = output.is_some_and(|output| output.was_content_truncated);
2185        let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0);
2186
2187        let command_failed = command_finished
2188            && output.is_some_and(|o| o.exit_status.is_none_or(|status| !status.success()));
2189
2190        let time_elapsed = if let Some(output) = output {
2191            output.ended_at.duration_since(started_at)
2192        } else {
2193            started_at.elapsed()
2194        };
2195
2196        let header_bg = cx
2197            .theme()
2198            .colors()
2199            .element_background
2200            .blend(cx.theme().colors().editor_foreground.opacity(0.025));
2201        let border_color = cx.theme().colors().border.opacity(0.6);
2202
2203        let working_dir = working_dir
2204            .as_ref()
2205            .map(|path| format!("{}", path.display()))
2206            .unwrap_or_else(|| "current directory".to_string());
2207
2208        let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
2209
2210        let header = h_flex()
2211            .id(SharedString::from(format!(
2212                "terminal-tool-header-{}",
2213                terminal.entity_id()
2214            )))
2215            .flex_none()
2216            .gap_1()
2217            .justify_between()
2218            .rounded_t_md()
2219            .child(
2220                div()
2221                    .id(("command-target-path", terminal.entity_id()))
2222                    .w_full()
2223                    .max_w_full()
2224                    .overflow_x_scroll()
2225                    .child(
2226                        Label::new(working_dir)
2227                            .buffer_font(cx)
2228                            .size(LabelSize::XSmall)
2229                            .color(Color::Muted),
2230                    ),
2231            )
2232            .when(!command_finished, |header| {
2233                header
2234                    .gap_1p5()
2235                    .child(
2236                        Button::new(
2237                            SharedString::from(format!("stop-terminal-{}", terminal.entity_id())),
2238                            "Stop",
2239                        )
2240                        .icon(IconName::Stop)
2241                        .icon_position(IconPosition::Start)
2242                        .icon_size(IconSize::Small)
2243                        .icon_color(Color::Error)
2244                        .label_size(LabelSize::Small)
2245                        .tooltip(move |window, cx| {
2246                            Tooltip::with_meta(
2247                                "Stop This Command",
2248                                None,
2249                                "Also possible by placing your cursor inside the terminal and using regular terminal bindings.",
2250                                window,
2251                                cx,
2252                            )
2253                        })
2254                        .on_click({
2255                            let terminal = terminal.clone();
2256                            cx.listener(move |_this, _event, _window, cx| {
2257                                let inner_terminal = terminal.read(cx).inner().clone();
2258                                inner_terminal.update(cx, |inner_terminal, _cx| {
2259                                    inner_terminal.kill_active_task();
2260                                });
2261                            })
2262                        }),
2263                    )
2264                    .child(Divider::vertical())
2265                    .child(
2266                        Icon::new(IconName::ArrowCircle)
2267                            .size(IconSize::XSmall)
2268                            .color(Color::Info)
2269                            .with_animation(
2270                                "arrow-circle",
2271                                Animation::new(Duration::from_secs(2)).repeat(),
2272                                |icon, delta| {
2273                                    icon.transform(Transformation::rotate(percentage(delta)))
2274                                },
2275                            ),
2276                    )
2277            })
2278            .when(tool_failed || command_failed, |header| {
2279                header.child(
2280                    div()
2281                        .id(("terminal-tool-error-code-indicator", terminal.entity_id()))
2282                        .child(
2283                            Icon::new(IconName::Close)
2284                                .size(IconSize::Small)
2285                                .color(Color::Error),
2286                        )
2287                        .when_some(output.and_then(|o| o.exit_status), |this, status| {
2288                            this.tooltip(Tooltip::text(format!(
2289                                "Exited with code {}",
2290                                status.code().unwrap_or(-1),
2291                            )))
2292                        }),
2293                )
2294            })
2295            .when(truncated_output, |header| {
2296                let tooltip = if let Some(output) = output {
2297                    if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
2298                        "Output exceeded terminal max lines and was \
2299                            truncated, the model received the first 16 KB."
2300                            .to_string()
2301                    } else {
2302                        format!(
2303                            "Output is {} long, and to avoid unexpected token usage, \
2304                                only 16 KB was sent back to the model.",
2305                            format_file_size(output.original_content_len as u64, true),
2306                        )
2307                    }
2308                } else {
2309                    "Output was truncated".to_string()
2310                };
2311
2312                header.child(
2313                    h_flex()
2314                        .id(("terminal-tool-truncated-label", terminal.entity_id()))
2315                        .gap_1()
2316                        .child(
2317                            Icon::new(IconName::Info)
2318                                .size(IconSize::XSmall)
2319                                .color(Color::Ignored),
2320                        )
2321                        .child(
2322                            Label::new("Truncated")
2323                                .color(Color::Muted)
2324                                .size(LabelSize::XSmall),
2325                        )
2326                        .tooltip(Tooltip::text(tooltip)),
2327                )
2328            })
2329            .when(time_elapsed > Duration::from_secs(10), |header| {
2330                header.child(
2331                    Label::new(format!("({})", duration_alt_display(time_elapsed)))
2332                        .buffer_font(cx)
2333                        .color(Color::Muted)
2334                        .size(LabelSize::XSmall),
2335                )
2336            })
2337            .child(
2338                Disclosure::new(
2339                    SharedString::from(format!(
2340                        "terminal-tool-disclosure-{}",
2341                        terminal.entity_id()
2342                    )),
2343                    is_expanded,
2344                )
2345                .opened_icon(IconName::ChevronUp)
2346                .closed_icon(IconName::ChevronDown)
2347                .on_click(cx.listener({
2348                    let id = tool_call.id.clone();
2349                    move |this, _event, _window, _cx| {
2350                        if is_expanded {
2351                            this.expanded_tool_calls.remove(&id);
2352                        } else {
2353                            this.expanded_tool_calls.insert(id.clone());
2354                        }
2355                    }})),
2356                );
2357
2358        let terminal_view = self
2359            .entry_view_state
2360            .read(cx)
2361            .entry(entry_ix)
2362            .and_then(|entry| entry.terminal(terminal));
2363        let show_output = is_expanded && terminal_view.is_some();
2364
2365        v_flex()
2366            .mb_2()
2367            .border_1()
2368            .when(tool_failed || command_failed, |card| card.border_dashed())
2369            .border_color(border_color)
2370            .rounded_md()
2371            .overflow_hidden()
2372            .child(
2373                v_flex()
2374                    .py_1p5()
2375                    .pl_2()
2376                    .pr_1p5()
2377                    .gap_0p5()
2378                    .bg(header_bg)
2379                    .text_xs()
2380                    .child(header)
2381                    .child(
2382                        MarkdownElement::new(
2383                            command.clone(),
2384                            terminal_command_markdown_style(window, cx),
2385                        )
2386                        .code_block_renderer(
2387                            markdown::CodeBlockRenderer::Default {
2388                                copy_button: false,
2389                                copy_button_on_hover: true,
2390                                border: false,
2391                            },
2392                        ),
2393                    ),
2394            )
2395            .when(show_output, |this| {
2396                this.child(
2397                    div()
2398                        .pt_2()
2399                        .border_t_1()
2400                        .when(tool_failed || command_failed, |card| card.border_dashed())
2401                        .border_color(border_color)
2402                        .bg(cx.theme().colors().editor_background)
2403                        .rounded_b_md()
2404                        .text_ui_sm(cx)
2405                        .children(terminal_view.clone()),
2406                )
2407            })
2408            .into_any()
2409    }
2410
2411    fn render_rules_item(&self, cx: &Context<Self>) -> Option<AnyElement> {
2412        let project_context = self
2413            .as_native_thread(cx)?
2414            .read(cx)
2415            .project_context()
2416            .read(cx);
2417
2418        let user_rules_text = if project_context.user_rules.is_empty() {
2419            None
2420        } else if project_context.user_rules.len() == 1 {
2421            let user_rules = &project_context.user_rules[0];
2422
2423            match user_rules.title.as_ref() {
2424                Some(title) => Some(format!("Using \"{title}\" user rule")),
2425                None => Some("Using user rule".into()),
2426            }
2427        } else {
2428            Some(format!(
2429                "Using {} user rules",
2430                project_context.user_rules.len()
2431            ))
2432        };
2433
2434        let first_user_rules_id = project_context
2435            .user_rules
2436            .first()
2437            .map(|user_rules| user_rules.uuid.0);
2438
2439        let rules_files = project_context
2440            .worktrees
2441            .iter()
2442            .filter_map(|worktree| worktree.rules_file.as_ref())
2443            .collect::<Vec<_>>();
2444
2445        let rules_file_text = match rules_files.as_slice() {
2446            &[] => None,
2447            &[rules_file] => Some(format!(
2448                "Using project {:?} file",
2449                rules_file.path_in_worktree
2450            )),
2451            rules_files => Some(format!("Using {} project rules files", rules_files.len())),
2452        };
2453
2454        if user_rules_text.is_none() && rules_file_text.is_none() {
2455            return None;
2456        }
2457
2458        let has_both = user_rules_text.is_some() && rules_file_text.is_some();
2459
2460        Some(
2461            h_flex()
2462                .px_2p5()
2463                .pb_1()
2464                .child(
2465                    Icon::new(IconName::Attach)
2466                        .size(IconSize::XSmall)
2467                        .color(Color::Disabled),
2468                )
2469                .when_some(user_rules_text, |parent, user_rules_text| {
2470                    parent.child(
2471                        h_flex()
2472                            .id("user-rules")
2473                            .ml_1()
2474                            .mr_1p5()
2475                            .child(
2476                                Label::new(user_rules_text)
2477                                    .size(LabelSize::XSmall)
2478                                    .color(Color::Muted)
2479                                    .truncate()
2480                                    .buffer_font(cx),
2481                            )
2482                            .hover(|s| s.bg(cx.theme().colors().element_hover))
2483                            .tooltip(Tooltip::text("View User Rules"))
2484                            .on_click(move |_event, window, cx| {
2485                                window.dispatch_action(
2486                                    Box::new(OpenRulesLibrary {
2487                                        prompt_to_select: first_user_rules_id,
2488                                    }),
2489                                    cx,
2490                                )
2491                            }),
2492                    )
2493                })
2494                .when(has_both, |this| this.child(Divider::vertical()))
2495                .when_some(rules_file_text, |parent, rules_file_text| {
2496                    parent.child(
2497                        h_flex()
2498                            .id("project-rules")
2499                            .ml_1p5()
2500                            .child(
2501                                Label::new(rules_file_text)
2502                                    .size(LabelSize::XSmall)
2503                                    .color(Color::Muted)
2504                                    .buffer_font(cx),
2505                            )
2506                            .hover(|s| s.bg(cx.theme().colors().element_hover))
2507                            .tooltip(Tooltip::text("View Project Rules"))
2508                            .on_click(cx.listener(Self::handle_open_rules)),
2509                    )
2510                })
2511                .into_any(),
2512        )
2513    }
2514
2515    fn render_empty_state_section_header(
2516        &self,
2517        label: impl Into<SharedString>,
2518        action_slot: Option<AnyElement>,
2519        cx: &mut Context<Self>,
2520    ) -> impl IntoElement {
2521        div().pl_1().pr_1p5().child(
2522            h_flex()
2523                .mt_2()
2524                .pl_1p5()
2525                .pb_1()
2526                .w_full()
2527                .justify_between()
2528                .border_b_1()
2529                .border_color(cx.theme().colors().border_variant)
2530                .child(
2531                    Label::new(label.into())
2532                        .size(LabelSize::Small)
2533                        .color(Color::Muted),
2534                )
2535                .children(action_slot),
2536        )
2537    }
2538
2539    fn render_recent_history(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
2540        let render_history = self
2541            .agent
2542            .clone()
2543            .downcast::<agent2::NativeAgentServer>()
2544            .is_some()
2545            && self
2546                .history_store
2547                .update(cx, |history_store, cx| !history_store.is_empty(cx));
2548
2549        v_flex()
2550            .size_full()
2551            .when(render_history, |this| {
2552                let recent_history: Vec<_> = self.history_store.update(cx, |history_store, _| {
2553                    history_store.entries().take(3).collect()
2554                });
2555                this.justify_end().child(
2556                    v_flex()
2557                        .child(
2558                            self.render_empty_state_section_header(
2559                                "Recent",
2560                                Some(
2561                                    Button::new("view-history", "View All")
2562                                        .style(ButtonStyle::Subtle)
2563                                        .label_size(LabelSize::Small)
2564                                        .key_binding(
2565                                            KeyBinding::for_action_in(
2566                                                &OpenHistory,
2567                                                &self.focus_handle(cx),
2568                                                window,
2569                                                cx,
2570                                            )
2571                                            .map(|kb| kb.size(rems_from_px(12.))),
2572                                        )
2573                                        .on_click(move |_event, window, cx| {
2574                                            window.dispatch_action(OpenHistory.boxed_clone(), cx);
2575                                        })
2576                                        .into_any_element(),
2577                                ),
2578                                cx,
2579                            ),
2580                        )
2581                        .child(
2582                            v_flex().p_1().pr_1p5().gap_1().children(
2583                                recent_history
2584                                    .into_iter()
2585                                    .enumerate()
2586                                    .map(|(index, entry)| {
2587                                        // TODO: Add keyboard navigation.
2588                                        let is_hovered =
2589                                            self.hovered_recent_history_item == Some(index);
2590                                        crate::acp::thread_history::AcpHistoryEntryElement::new(
2591                                            entry,
2592                                            cx.entity().downgrade(),
2593                                        )
2594                                        .hovered(is_hovered)
2595                                        .on_hover(cx.listener(
2596                                            move |this, is_hovered, _window, cx| {
2597                                                if *is_hovered {
2598                                                    this.hovered_recent_history_item = Some(index);
2599                                                } else if this.hovered_recent_history_item
2600                                                    == Some(index)
2601                                                {
2602                                                    this.hovered_recent_history_item = None;
2603                                                }
2604                                                cx.notify();
2605                                            },
2606                                        ))
2607                                        .into_any_element()
2608                                    }),
2609                            ),
2610                        ),
2611                )
2612            })
2613            .into_any()
2614    }
2615
2616    fn render_auth_required_state(
2617        &self,
2618        connection: &Rc<dyn AgentConnection>,
2619        description: Option<&Entity<Markdown>>,
2620        configuration_view: Option<&AnyView>,
2621        pending_auth_method: Option<&acp::AuthMethodId>,
2622        window: &mut Window,
2623        cx: &Context<Self>,
2624    ) -> Div {
2625        let show_description =
2626            configuration_view.is_none() && description.is_none() && pending_auth_method.is_none();
2627
2628        v_flex().flex_1().size_full().justify_end().child(
2629            v_flex()
2630                .p_2()
2631                .pr_3()
2632                .w_full()
2633                .gap_1()
2634                .border_t_1()
2635                .border_color(cx.theme().colors().border)
2636                .bg(cx.theme().status().warning.opacity(0.04))
2637                .child(
2638                    h_flex()
2639                        .gap_1p5()
2640                        .child(
2641                            Icon::new(IconName::Warning)
2642                                .color(Color::Warning)
2643                                .size(IconSize::Small),
2644                        )
2645                        .child(Label::new("Authentication Required").size(LabelSize::Small)),
2646                )
2647                .children(description.map(|desc| {
2648                    div().text_ui(cx).child(self.render_markdown(
2649                        desc.clone(),
2650                        default_markdown_style(false, false, window, cx),
2651                    ))
2652                }))
2653                .children(
2654                    configuration_view
2655                        .cloned()
2656                        .map(|view| div().w_full().child(view)),
2657                )
2658                .when(
2659                    show_description,
2660                    |el| {
2661                        el.child(
2662                            Label::new(format!(
2663                                "You are not currently authenticated with {}. Please choose one of the following options:",
2664                                self.agent.name()
2665                            ))
2666                            .size(LabelSize::Small)
2667                            .color(Color::Muted)
2668                            .mb_1()
2669                            .ml_5(),
2670                        )
2671                    },
2672                )
2673                .when_some(pending_auth_method, |el, _| {
2674                    el.child(
2675                        h_flex()
2676                            .py_4()
2677                            .w_full()
2678                            .justify_center()
2679                            .gap_1()
2680                            .child(
2681                                Icon::new(IconName::ArrowCircle)
2682                                    .size(IconSize::Small)
2683                                    .color(Color::Muted)
2684                                    .with_animation(
2685                                        "arrow-circle",
2686                                        Animation::new(Duration::from_secs(2)).repeat(),
2687                                        |icon, delta| {
2688                                            icon.transform(Transformation::rotate(percentage(
2689                                                delta,
2690                                            )))
2691                                        },
2692                                    )
2693                                    .into_any_element(),
2694                            )
2695                            .child(Label::new("Authenticating…").size(LabelSize::Small)),
2696                    )
2697                })
2698                .when(!connection.auth_methods().is_empty(), |this| {
2699                    this.child(
2700                        h_flex()
2701                            .justify_end()
2702                            .flex_wrap()
2703                            .gap_1()
2704                            .when(!show_description, |this| {
2705                                this.border_t_1()
2706                                    .mt_1()
2707                                    .pt_2()
2708                                    .border_color(cx.theme().colors().border.opacity(0.8))
2709                            })
2710                            .children(
2711                                connection
2712                                    .auth_methods()
2713                                    .iter()
2714                                    .enumerate()
2715                                    .rev()
2716                                    .map(|(ix, method)| {
2717                                        Button::new(
2718                                            SharedString::from(method.id.0.clone()),
2719                                            method.name.clone(),
2720                                        )
2721                                        .when(ix == 0, |el| {
2722                                            el.style(ButtonStyle::Tinted(ui::TintColor::Warning))
2723                                        })
2724                                        .label_size(LabelSize::Small)
2725                                        .on_click({
2726                                            let method_id = method.id.clone();
2727                                            cx.listener(move |this, _, window, cx| {
2728                                                this.authenticate(method_id.clone(), window, cx)
2729                                            })
2730                                        })
2731                                    }),
2732                            ),
2733                    )
2734                })
2735
2736        )
2737    }
2738
2739    fn render_load_error(&self, e: &LoadError, cx: &Context<Self>) -> AnyElement {
2740        let (message, action_slot) = match e {
2741            LoadError::NotInstalled {
2742                error_message,
2743                install_message,
2744                install_command,
2745            } => {
2746                let install_command = install_command.clone();
2747                let button = Button::new("install", install_message)
2748                    .tooltip(Tooltip::text(install_command.clone()))
2749                    .style(ButtonStyle::Outlined)
2750                    .label_size(LabelSize::Small)
2751                    .icon(IconName::Download)
2752                    .icon_size(IconSize::Small)
2753                    .icon_color(Color::Muted)
2754                    .icon_position(IconPosition::Start)
2755                    .on_click(cx.listener(move |this, _, window, cx| {
2756                        let task = this
2757                            .workspace
2758                            .update(cx, |workspace, cx| {
2759                                let project = workspace.project().read(cx);
2760                                let cwd = project.first_project_directory(cx);
2761                                let shell = project.terminal_settings(&cwd, cx).shell.clone();
2762                                let spawn_in_terminal = task::SpawnInTerminal {
2763                                    id: task::TaskId("install".to_string()),
2764                                    full_label: install_command.clone(),
2765                                    label: install_command.clone(),
2766                                    command: Some(install_command.clone()),
2767                                    args: Vec::new(),
2768                                    command_label: install_command.clone(),
2769                                    cwd,
2770                                    env: Default::default(),
2771                                    use_new_terminal: true,
2772                                    allow_concurrent_runs: true,
2773                                    reveal: Default::default(),
2774                                    reveal_target: Default::default(),
2775                                    hide: Default::default(),
2776                                    shell,
2777                                    show_summary: true,
2778                                    show_command: true,
2779                                    show_rerun: false,
2780                                };
2781                                workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
2782                            })
2783                            .ok();
2784                        let Some(task) = task else { return };
2785                        cx.spawn_in(window, async move |this, cx| {
2786                            if let Some(Ok(_)) = task.await {
2787                                this.update_in(cx, |this, window, cx| {
2788                                    this.reset(window, cx);
2789                                })
2790                                .ok();
2791                            }
2792                        })
2793                        .detach()
2794                    }));
2795
2796                (error_message.clone(), Some(button.into_any_element()))
2797            }
2798            LoadError::Unsupported {
2799                error_message,
2800                upgrade_message,
2801                upgrade_command,
2802            } => {
2803                let upgrade_command = upgrade_command.clone();
2804                let button = Button::new("upgrade", upgrade_message)
2805                    .tooltip(Tooltip::text(upgrade_command.clone()))
2806                    .style(ButtonStyle::Outlined)
2807                    .label_size(LabelSize::Small)
2808                    .icon(IconName::Download)
2809                    .icon_size(IconSize::Small)
2810                    .icon_color(Color::Muted)
2811                    .icon_position(IconPosition::Start)
2812                    .on_click(cx.listener(move |this, _, window, cx| {
2813                        let task = this
2814                            .workspace
2815                            .update(cx, |workspace, cx| {
2816                                let project = workspace.project().read(cx);
2817                                let cwd = project.first_project_directory(cx);
2818                                let shell = project.terminal_settings(&cwd, cx).shell.clone();
2819                                let spawn_in_terminal = task::SpawnInTerminal {
2820                                    id: task::TaskId("upgrade".to_string()),
2821                                    full_label: upgrade_command.clone(),
2822                                    label: upgrade_command.clone(),
2823                                    command: Some(upgrade_command.clone()),
2824                                    args: Vec::new(),
2825                                    command_label: upgrade_command.clone(),
2826                                    cwd,
2827                                    env: Default::default(),
2828                                    use_new_terminal: true,
2829                                    allow_concurrent_runs: true,
2830                                    reveal: Default::default(),
2831                                    reveal_target: Default::default(),
2832                                    hide: Default::default(),
2833                                    shell,
2834                                    show_summary: true,
2835                                    show_command: true,
2836                                    show_rerun: false,
2837                                };
2838                                workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
2839                            })
2840                            .ok();
2841                        let Some(task) = task else { return };
2842                        cx.spawn_in(window, async move |this, cx| {
2843                            if let Some(Ok(_)) = task.await {
2844                                this.update_in(cx, |this, window, cx| {
2845                                    this.reset(window, cx);
2846                                })
2847                                .ok();
2848                            }
2849                        })
2850                        .detach()
2851                    }));
2852
2853                (error_message.clone(), Some(button.into_any_element()))
2854            }
2855            LoadError::Exited { .. } => ("Server exited with status {status}".into(), None),
2856            LoadError::Other(msg) => (
2857                msg.into(),
2858                Some(self.create_copy_button(msg.to_string()).into_any_element()),
2859            ),
2860        };
2861
2862        Callout::new()
2863            .severity(Severity::Error)
2864            .icon(IconName::XCircleFilled)
2865            .title("Failed to Launch")
2866            .description(message)
2867            .actions_slot(div().children(action_slot))
2868            .into_any_element()
2869    }
2870
2871    fn render_activity_bar(
2872        &self,
2873        thread_entity: &Entity<AcpThread>,
2874        window: &mut Window,
2875        cx: &Context<Self>,
2876    ) -> Option<AnyElement> {
2877        let thread = thread_entity.read(cx);
2878        let action_log = thread.action_log();
2879        let changed_buffers = action_log.read(cx).changed_buffers(cx);
2880        let plan = thread.plan();
2881
2882        if changed_buffers.is_empty() && plan.is_empty() {
2883            return None;
2884        }
2885
2886        let editor_bg_color = cx.theme().colors().editor_background;
2887        let active_color = cx.theme().colors().element_selected;
2888        let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
2889
2890        let pending_edits = thread.has_pending_edit_tool_calls();
2891
2892        v_flex()
2893            .mt_1()
2894            .mx_2()
2895            .bg(bg_edit_files_disclosure)
2896            .border_1()
2897            .border_b_0()
2898            .border_color(cx.theme().colors().border)
2899            .rounded_t_md()
2900            .shadow(vec![gpui::BoxShadow {
2901                color: gpui::black().opacity(0.15),
2902                offset: point(px(1.), px(-1.)),
2903                blur_radius: px(3.),
2904                spread_radius: px(0.),
2905            }])
2906            .when(!plan.is_empty(), |this| {
2907                this.child(self.render_plan_summary(plan, window, cx))
2908                    .when(self.plan_expanded, |parent| {
2909                        parent.child(self.render_plan_entries(plan, window, cx))
2910                    })
2911            })
2912            .when(!plan.is_empty() && !changed_buffers.is_empty(), |this| {
2913                this.child(Divider::horizontal().color(DividerColor::Border))
2914            })
2915            .when(!changed_buffers.is_empty(), |this| {
2916                this.child(self.render_edits_summary(
2917                    &changed_buffers,
2918                    self.edits_expanded,
2919                    pending_edits,
2920                    window,
2921                    cx,
2922                ))
2923                .when(self.edits_expanded, |parent| {
2924                    parent.child(self.render_edited_files(
2925                        action_log,
2926                        &changed_buffers,
2927                        pending_edits,
2928                        cx,
2929                    ))
2930                })
2931            })
2932            .into_any()
2933            .into()
2934    }
2935
2936    fn render_plan_summary(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
2937        let stats = plan.stats();
2938
2939        let title = if let Some(entry) = stats.in_progress_entry
2940            && !self.plan_expanded
2941        {
2942            h_flex()
2943                .w_full()
2944                .cursor_default()
2945                .gap_1()
2946                .text_xs()
2947                .text_color(cx.theme().colors().text_muted)
2948                .justify_between()
2949                .child(
2950                    h_flex()
2951                        .gap_1()
2952                        .child(
2953                            Label::new("Current:")
2954                                .size(LabelSize::Small)
2955                                .color(Color::Muted),
2956                        )
2957                        .child(MarkdownElement::new(
2958                            entry.content.clone(),
2959                            plan_label_markdown_style(&entry.status, window, cx),
2960                        )),
2961                )
2962                .when(stats.pending > 0, |this| {
2963                    this.child(
2964                        Label::new(format!("{} left", stats.pending))
2965                            .size(LabelSize::Small)
2966                            .color(Color::Muted)
2967                            .mr_1(),
2968                    )
2969                })
2970        } else {
2971            let status_label = if stats.pending == 0 {
2972                "All Done".to_string()
2973            } else if stats.completed == 0 {
2974                format!("{} Tasks", plan.entries.len())
2975            } else {
2976                format!("{}/{}", stats.completed, plan.entries.len())
2977            };
2978
2979            h_flex()
2980                .w_full()
2981                .gap_1()
2982                .justify_between()
2983                .child(
2984                    Label::new("Plan")
2985                        .size(LabelSize::Small)
2986                        .color(Color::Muted),
2987                )
2988                .child(
2989                    Label::new(status_label)
2990                        .size(LabelSize::Small)
2991                        .color(Color::Muted)
2992                        .mr_1(),
2993                )
2994        };
2995
2996        h_flex()
2997            .p_1()
2998            .justify_between()
2999            .when(self.plan_expanded, |this| {
3000                this.border_b_1().border_color(cx.theme().colors().border)
3001            })
3002            .child(
3003                h_flex()
3004                    .id("plan_summary")
3005                    .w_full()
3006                    .gap_1()
3007                    .child(Disclosure::new("plan_disclosure", self.plan_expanded))
3008                    .child(title)
3009                    .on_click(cx.listener(|this, _, _, cx| {
3010                        this.plan_expanded = !this.plan_expanded;
3011                        cx.notify();
3012                    })),
3013            )
3014    }
3015
3016    fn render_plan_entries(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
3017        v_flex().children(plan.entries.iter().enumerate().flat_map(|(index, entry)| {
3018            let element = h_flex()
3019                .py_1()
3020                .px_2()
3021                .gap_2()
3022                .justify_between()
3023                .bg(cx.theme().colors().editor_background)
3024                .when(index < plan.entries.len() - 1, |parent| {
3025                    parent.border_color(cx.theme().colors().border).border_b_1()
3026                })
3027                .child(
3028                    h_flex()
3029                        .id(("plan_entry", index))
3030                        .gap_1p5()
3031                        .max_w_full()
3032                        .overflow_x_scroll()
3033                        .text_xs()
3034                        .text_color(cx.theme().colors().text_muted)
3035                        .child(match entry.status {
3036                            acp::PlanEntryStatus::Pending => Icon::new(IconName::TodoPending)
3037                                .size(IconSize::Small)
3038                                .color(Color::Muted)
3039                                .into_any_element(),
3040                            acp::PlanEntryStatus::InProgress => Icon::new(IconName::TodoProgress)
3041                                .size(IconSize::Small)
3042                                .color(Color::Accent)
3043                                .with_animation(
3044                                    "running",
3045                                    Animation::new(Duration::from_secs(2)).repeat(),
3046                                    |icon, delta| {
3047                                        icon.transform(Transformation::rotate(percentage(delta)))
3048                                    },
3049                                )
3050                                .into_any_element(),
3051                            acp::PlanEntryStatus::Completed => Icon::new(IconName::TodoComplete)
3052                                .size(IconSize::Small)
3053                                .color(Color::Success)
3054                                .into_any_element(),
3055                        })
3056                        .child(MarkdownElement::new(
3057                            entry.content.clone(),
3058                            plan_label_markdown_style(&entry.status, window, cx),
3059                        )),
3060                );
3061
3062            Some(element)
3063        }))
3064    }
3065
3066    fn render_edits_summary(
3067        &self,
3068        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
3069        expanded: bool,
3070        pending_edits: bool,
3071        window: &mut Window,
3072        cx: &Context<Self>,
3073    ) -> Div {
3074        const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
3075
3076        let focus_handle = self.focus_handle(cx);
3077
3078        h_flex()
3079            .p_1()
3080            .justify_between()
3081            .when(expanded, |this| {
3082                this.border_b_1().border_color(cx.theme().colors().border)
3083            })
3084            .child(
3085                h_flex()
3086                    .id("edits-container")
3087                    .w_full()
3088                    .gap_1()
3089                    .child(Disclosure::new("edits-disclosure", expanded))
3090                    .map(|this| {
3091                        if pending_edits {
3092                            this.child(
3093                                Label::new(format!(
3094                                    "Editing {} {}",
3095                                    changed_buffers.len(),
3096                                    if changed_buffers.len() == 1 {
3097                                        "file"
3098                                    } else {
3099                                        "files"
3100                                    }
3101                                ))
3102                                .color(Color::Muted)
3103                                .size(LabelSize::Small)
3104                                .with_animation(
3105                                    "edit-label",
3106                                    Animation::new(Duration::from_secs(2))
3107                                        .repeat()
3108                                        .with_easing(pulsating_between(0.3, 0.7)),
3109                                    |label, delta| label.alpha(delta),
3110                                ),
3111                            )
3112                        } else {
3113                            this.child(
3114                                Label::new("Edits")
3115                                    .size(LabelSize::Small)
3116                                    .color(Color::Muted),
3117                            )
3118                            .child(Label::new("").size(LabelSize::XSmall).color(Color::Muted))
3119                            .child(
3120                                Label::new(format!(
3121                                    "{} {}",
3122                                    changed_buffers.len(),
3123                                    if changed_buffers.len() == 1 {
3124                                        "file"
3125                                    } else {
3126                                        "files"
3127                                    }
3128                                ))
3129                                .size(LabelSize::Small)
3130                                .color(Color::Muted),
3131                            )
3132                        }
3133                    })
3134                    .on_click(cx.listener(|this, _, _, cx| {
3135                        this.edits_expanded = !this.edits_expanded;
3136                        cx.notify();
3137                    })),
3138            )
3139            .child(
3140                h_flex()
3141                    .gap_1()
3142                    .child(
3143                        IconButton::new("review-changes", IconName::ListTodo)
3144                            .icon_size(IconSize::Small)
3145                            .tooltip({
3146                                let focus_handle = focus_handle.clone();
3147                                move |window, cx| {
3148                                    Tooltip::for_action_in(
3149                                        "Review Changes",
3150                                        &OpenAgentDiff,
3151                                        &focus_handle,
3152                                        window,
3153                                        cx,
3154                                    )
3155                                }
3156                            })
3157                            .on_click(cx.listener(|_, _, window, cx| {
3158                                window.dispatch_action(OpenAgentDiff.boxed_clone(), cx);
3159                            })),
3160                    )
3161                    .child(Divider::vertical().color(DividerColor::Border))
3162                    .child(
3163                        Button::new("reject-all-changes", "Reject All")
3164                            .label_size(LabelSize::Small)
3165                            .disabled(pending_edits)
3166                            .when(pending_edits, |this| {
3167                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
3168                            })
3169                            .key_binding(
3170                                KeyBinding::for_action_in(
3171                                    &RejectAll,
3172                                    &focus_handle.clone(),
3173                                    window,
3174                                    cx,
3175                                )
3176                                .map(|kb| kb.size(rems_from_px(10.))),
3177                            )
3178                            .on_click(cx.listener(move |this, _, window, cx| {
3179                                this.reject_all(&RejectAll, window, cx);
3180                            })),
3181                    )
3182                    .child(
3183                        Button::new("keep-all-changes", "Keep All")
3184                            .label_size(LabelSize::Small)
3185                            .disabled(pending_edits)
3186                            .when(pending_edits, |this| {
3187                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
3188                            })
3189                            .key_binding(
3190                                KeyBinding::for_action_in(&KeepAll, &focus_handle, window, cx)
3191                                    .map(|kb| kb.size(rems_from_px(10.))),
3192                            )
3193                            .on_click(cx.listener(move |this, _, window, cx| {
3194                                this.keep_all(&KeepAll, window, cx);
3195                            })),
3196                    ),
3197            )
3198    }
3199
3200    fn render_edited_files(
3201        &self,
3202        action_log: &Entity<ActionLog>,
3203        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
3204        pending_edits: bool,
3205        cx: &Context<Self>,
3206    ) -> Div {
3207        let editor_bg_color = cx.theme().colors().editor_background;
3208
3209        v_flex().children(changed_buffers.iter().enumerate().flat_map(
3210            |(index, (buffer, _diff))| {
3211                let file = buffer.read(cx).file()?;
3212                let path = file.path();
3213
3214                let file_path = path.parent().and_then(|parent| {
3215                    let parent_str = parent.to_string_lossy();
3216
3217                    if parent_str.is_empty() {
3218                        None
3219                    } else {
3220                        Some(
3221                            Label::new(format!("/{}{}", parent_str, std::path::MAIN_SEPARATOR_STR))
3222                                .color(Color::Muted)
3223                                .size(LabelSize::XSmall)
3224                                .buffer_font(cx),
3225                        )
3226                    }
3227                });
3228
3229                let file_name = path.file_name().map(|name| {
3230                    Label::new(name.to_string_lossy().to_string())
3231                        .size(LabelSize::XSmall)
3232                        .buffer_font(cx)
3233                });
3234
3235                let file_icon = FileIcons::get_icon(path, cx)
3236                    .map(Icon::from_path)
3237                    .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
3238                    .unwrap_or_else(|| {
3239                        Icon::new(IconName::File)
3240                            .color(Color::Muted)
3241                            .size(IconSize::Small)
3242                    });
3243
3244                let overlay_gradient = linear_gradient(
3245                    90.,
3246                    linear_color_stop(editor_bg_color, 1.),
3247                    linear_color_stop(editor_bg_color.opacity(0.2), 0.),
3248                );
3249
3250                let element = h_flex()
3251                    .group("edited-code")
3252                    .id(("file-container", index))
3253                    .relative()
3254                    .py_1()
3255                    .pl_2()
3256                    .pr_1()
3257                    .gap_2()
3258                    .justify_between()
3259                    .bg(editor_bg_color)
3260                    .when(index < changed_buffers.len() - 1, |parent| {
3261                        parent.border_color(cx.theme().colors().border).border_b_1()
3262                    })
3263                    .child(
3264                        h_flex()
3265                            .id(("file-name", index))
3266                            .pr_8()
3267                            .gap_1p5()
3268                            .max_w_full()
3269                            .overflow_x_scroll()
3270                            .child(file_icon)
3271                            .child(h_flex().gap_0p5().children(file_name).children(file_path))
3272                            .on_click({
3273                                let buffer = buffer.clone();
3274                                cx.listener(move |this, _, window, cx| {
3275                                    this.open_edited_buffer(&buffer, window, cx);
3276                                })
3277                            }),
3278                    )
3279                    .child(
3280                        h_flex()
3281                            .gap_1()
3282                            .visible_on_hover("edited-code")
3283                            .child(
3284                                Button::new("review", "Review")
3285                                    .label_size(LabelSize::Small)
3286                                    .on_click({
3287                                        let buffer = buffer.clone();
3288                                        cx.listener(move |this, _, window, cx| {
3289                                            this.open_edited_buffer(&buffer, window, cx);
3290                                        })
3291                                    }),
3292                            )
3293                            .child(Divider::vertical().color(DividerColor::BorderVariant))
3294                            .child(
3295                                Button::new("reject-file", "Reject")
3296                                    .label_size(LabelSize::Small)
3297                                    .disabled(pending_edits)
3298                                    .on_click({
3299                                        let buffer = buffer.clone();
3300                                        let action_log = action_log.clone();
3301                                        move |_, _, cx| {
3302                                            action_log.update(cx, |action_log, cx| {
3303                                                action_log
3304                                                    .reject_edits_in_ranges(
3305                                                        buffer.clone(),
3306                                                        vec![Anchor::MIN..Anchor::MAX],
3307                                                        cx,
3308                                                    )
3309                                                    .detach_and_log_err(cx);
3310                                            })
3311                                        }
3312                                    }),
3313                            )
3314                            .child(
3315                                Button::new("keep-file", "Keep")
3316                                    .label_size(LabelSize::Small)
3317                                    .disabled(pending_edits)
3318                                    .on_click({
3319                                        let buffer = buffer.clone();
3320                                        let action_log = action_log.clone();
3321                                        move |_, _, cx| {
3322                                            action_log.update(cx, |action_log, cx| {
3323                                                action_log.keep_edits_in_range(
3324                                                    buffer.clone(),
3325                                                    Anchor::MIN..Anchor::MAX,
3326                                                    cx,
3327                                                );
3328                                            })
3329                                        }
3330                                    }),
3331                            ),
3332                    )
3333                    .child(
3334                        div()
3335                            .id("gradient-overlay")
3336                            .absolute()
3337                            .h_full()
3338                            .w_12()
3339                            .top_0()
3340                            .bottom_0()
3341                            .right(px(152.))
3342                            .bg(overlay_gradient),
3343                    );
3344
3345                Some(element)
3346            },
3347        ))
3348    }
3349
3350    fn render_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
3351        let focus_handle = self.message_editor.focus_handle(cx);
3352        let editor_bg_color = cx.theme().colors().editor_background;
3353        let (expand_icon, expand_tooltip) = if self.editor_expanded {
3354            (IconName::Minimize, "Minimize Message Editor")
3355        } else {
3356            (IconName::Maximize, "Expand Message Editor")
3357        };
3358
3359        let backdrop = div()
3360            .size_full()
3361            .absolute()
3362            .inset_0()
3363            .bg(cx.theme().colors().panel_background)
3364            .opacity(0.8)
3365            .block_mouse_except_scroll();
3366
3367        let enable_editor = match self.thread_state {
3368            ThreadState::Loading { .. } | ThreadState::Ready { .. } => true,
3369            ThreadState::Unauthenticated { .. } | ThreadState::LoadError(..) => false,
3370        };
3371
3372        v_flex()
3373            .on_action(cx.listener(Self::expand_message_editor))
3374            .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
3375                if let Some(profile_selector) = this.profile_selector.as_ref() {
3376                    profile_selector.read(cx).menu_handle().toggle(window, cx);
3377                }
3378            }))
3379            .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
3380                if let Some(model_selector) = this.model_selector.as_ref() {
3381                    model_selector
3382                        .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
3383                }
3384            }))
3385            .p_2()
3386            .gap_2()
3387            .border_t_1()
3388            .border_color(cx.theme().colors().border)
3389            .bg(editor_bg_color)
3390            .when(self.editor_expanded, |this| {
3391                this.h(vh(0.8, window)).size_full().justify_between()
3392            })
3393            .child(
3394                v_flex()
3395                    .relative()
3396                    .size_full()
3397                    .pt_1()
3398                    .pr_2p5()
3399                    .child(self.message_editor.clone())
3400                    .child(
3401                        h_flex()
3402                            .absolute()
3403                            .top_0()
3404                            .right_0()
3405                            .opacity(0.5)
3406                            .hover(|this| this.opacity(1.0))
3407                            .child(
3408                                IconButton::new("toggle-height", expand_icon)
3409                                    .icon_size(IconSize::Small)
3410                                    .icon_color(Color::Muted)
3411                                    .tooltip({
3412                                        move |window, cx| {
3413                                            Tooltip::for_action_in(
3414                                                expand_tooltip,
3415                                                &ExpandMessageEditor,
3416                                                &focus_handle,
3417                                                window,
3418                                                cx,
3419                                            )
3420                                        }
3421                                    })
3422                                    .on_click(cx.listener(|_, _, window, cx| {
3423                                        window.dispatch_action(Box::new(ExpandMessageEditor), cx);
3424                                    })),
3425                            ),
3426                    ),
3427            )
3428            .child(
3429                h_flex()
3430                    .flex_none()
3431                    .flex_wrap()
3432                    .justify_between()
3433                    .child(
3434                        h_flex()
3435                            .child(self.render_follow_toggle(cx))
3436                            .children(self.render_burn_mode_toggle(cx)),
3437                    )
3438                    .child(
3439                        h_flex()
3440                            .gap_1()
3441                            .children(self.render_token_usage(cx))
3442                            .children(self.profile_selector.clone())
3443                            .children(self.model_selector.clone())
3444                            .child(self.render_send_button(cx)),
3445                    ),
3446            )
3447            .when(!enable_editor, |this| this.child(backdrop))
3448            .into_any()
3449    }
3450
3451    pub(crate) fn as_native_connection(
3452        &self,
3453        cx: &App,
3454    ) -> Option<Rc<agent2::NativeAgentConnection>> {
3455        let acp_thread = self.thread()?.read(cx);
3456        acp_thread.connection().clone().downcast()
3457    }
3458
3459    pub(crate) fn as_native_thread(&self, cx: &App) -> Option<Entity<agent2::Thread>> {
3460        let acp_thread = self.thread()?.read(cx);
3461        self.as_native_connection(cx)?
3462            .thread(acp_thread.session_id(), cx)
3463    }
3464
3465    fn is_using_zed_ai_models(&self, cx: &App) -> bool {
3466        self.as_native_thread(cx)
3467            .and_then(|thread| thread.read(cx).model())
3468            .is_some_and(|model| model.provider_id() == language_model::ZED_CLOUD_PROVIDER_ID)
3469    }
3470
3471    fn render_token_usage(&self, cx: &mut Context<Self>) -> Option<Div> {
3472        let thread = self.thread()?.read(cx);
3473        let usage = thread.token_usage()?;
3474        let is_generating = thread.status() != ThreadStatus::Idle;
3475
3476        let used = crate::text_thread_editor::humanize_token_count(usage.used_tokens);
3477        let max = crate::text_thread_editor::humanize_token_count(usage.max_tokens);
3478
3479        Some(
3480            h_flex()
3481                .flex_shrink_0()
3482                .gap_0p5()
3483                .mr_1p5()
3484                .child(
3485                    Label::new(used)
3486                        .size(LabelSize::Small)
3487                        .color(Color::Muted)
3488                        .map(|label| {
3489                            if is_generating {
3490                                label
3491                                    .with_animation(
3492                                        "used-tokens-label",
3493                                        Animation::new(Duration::from_secs(2))
3494                                            .repeat()
3495                                            .with_easing(pulsating_between(0.3, 0.8)),
3496                                        |label, delta| label.alpha(delta),
3497                                    )
3498                                    .into_any()
3499                            } else {
3500                                label.into_any_element()
3501                            }
3502                        }),
3503                )
3504                .child(
3505                    Label::new("/")
3506                        .size(LabelSize::Small)
3507                        .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.5))),
3508                )
3509                .child(Label::new(max).size(LabelSize::Small).color(Color::Muted)),
3510        )
3511    }
3512
3513    fn toggle_burn_mode(
3514        &mut self,
3515        _: &ToggleBurnMode,
3516        _window: &mut Window,
3517        cx: &mut Context<Self>,
3518    ) {
3519        let Some(thread) = self.as_native_thread(cx) else {
3520            return;
3521        };
3522
3523        thread.update(cx, |thread, cx| {
3524            let current_mode = thread.completion_mode();
3525            thread.set_completion_mode(
3526                match current_mode {
3527                    CompletionMode::Burn => CompletionMode::Normal,
3528                    CompletionMode::Normal => CompletionMode::Burn,
3529                },
3530                cx,
3531            );
3532        });
3533    }
3534
3535    fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
3536        let Some(thread) = self.thread() else {
3537            return;
3538        };
3539        let action_log = thread.read(cx).action_log().clone();
3540        action_log.update(cx, |action_log, cx| action_log.keep_all_edits(cx));
3541    }
3542
3543    fn reject_all(&mut self, _: &RejectAll, _window: &mut Window, cx: &mut Context<Self>) {
3544        let Some(thread) = self.thread() else {
3545            return;
3546        };
3547        let action_log = thread.read(cx).action_log().clone();
3548        action_log
3549            .update(cx, |action_log, cx| action_log.reject_all_edits(cx))
3550            .detach();
3551    }
3552
3553    fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
3554        let thread = self.as_native_thread(cx)?.read(cx);
3555
3556        if thread
3557            .model()
3558            .is_none_or(|model| !model.supports_burn_mode())
3559        {
3560            return None;
3561        }
3562
3563        let active_completion_mode = thread.completion_mode();
3564        let burn_mode_enabled = active_completion_mode == CompletionMode::Burn;
3565        let icon = if burn_mode_enabled {
3566            IconName::ZedBurnModeOn
3567        } else {
3568            IconName::ZedBurnMode
3569        };
3570
3571        Some(
3572            IconButton::new("burn-mode", icon)
3573                .icon_size(IconSize::Small)
3574                .icon_color(Color::Muted)
3575                .toggle_state(burn_mode_enabled)
3576                .selected_icon_color(Color::Error)
3577                .on_click(cx.listener(|this, _event, window, cx| {
3578                    this.toggle_burn_mode(&ToggleBurnMode, window, cx);
3579                }))
3580                .tooltip(move |_window, cx| {
3581                    cx.new(|_| BurnModeTooltip::new().selected(burn_mode_enabled))
3582                        .into()
3583                })
3584                .into_any_element(),
3585        )
3586    }
3587
3588    fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
3589        let is_editor_empty = self.message_editor.read(cx).is_empty(cx);
3590        let is_generating = self
3591            .thread()
3592            .is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle);
3593
3594        if self.is_loading_contents {
3595            div()
3596                .id("loading-message-content")
3597                .px_1()
3598                .tooltip(Tooltip::text("Loading Added Context…"))
3599                .child(loading_contents_spinner(IconSize::default()))
3600                .into_any_element()
3601        } else if is_generating && is_editor_empty {
3602            IconButton::new("stop-generation", IconName::Stop)
3603                .icon_color(Color::Error)
3604                .style(ButtonStyle::Tinted(ui::TintColor::Error))
3605                .tooltip(move |window, cx| {
3606                    Tooltip::for_action("Stop Generation", &editor::actions::Cancel, window, cx)
3607                })
3608                .on_click(cx.listener(|this, _event, _, cx| this.cancel_generation(cx)))
3609                .into_any_element()
3610        } else {
3611            let send_btn_tooltip = if is_editor_empty && !is_generating {
3612                "Type to Send"
3613            } else if is_generating {
3614                "Stop and Send Message"
3615            } else {
3616                "Send"
3617            };
3618
3619            IconButton::new("send-message", IconName::Send)
3620                .style(ButtonStyle::Filled)
3621                .map(|this| {
3622                    if is_editor_empty && !is_generating {
3623                        this.disabled(true).icon_color(Color::Muted)
3624                    } else {
3625                        this.icon_color(Color::Accent)
3626                    }
3627                })
3628                .tooltip(move |window, cx| Tooltip::for_action(send_btn_tooltip, &Chat, window, cx))
3629                .on_click(cx.listener(|this, _, window, cx| {
3630                    this.send(window, cx);
3631                }))
3632                .into_any_element()
3633        }
3634    }
3635
3636    fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
3637        let following = self
3638            .workspace
3639            .read_with(cx, |workspace, _| {
3640                workspace.is_being_followed(CollaboratorId::Agent)
3641            })
3642            .unwrap_or(false);
3643
3644        IconButton::new("follow-agent", IconName::Crosshair)
3645            .icon_size(IconSize::Small)
3646            .icon_color(Color::Muted)
3647            .toggle_state(following)
3648            .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
3649            .tooltip(move |window, cx| {
3650                if following {
3651                    Tooltip::for_action("Stop Following Agent", &Follow, window, cx)
3652                } else {
3653                    Tooltip::with_meta(
3654                        "Follow Agent",
3655                        Some(&Follow),
3656                        "Track the agent's location as it reads and edits files.",
3657                        window,
3658                        cx,
3659                    )
3660                }
3661            })
3662            .on_click(cx.listener(move |this, _, window, cx| {
3663                this.workspace
3664                    .update(cx, |workspace, cx| {
3665                        if following {
3666                            workspace.unfollow(CollaboratorId::Agent, window, cx);
3667                        } else {
3668                            workspace.follow(CollaboratorId::Agent, window, cx);
3669                        }
3670                    })
3671                    .ok();
3672            }))
3673    }
3674
3675    fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
3676        let workspace = self.workspace.clone();
3677        MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
3678            Self::open_link(text, &workspace, window, cx);
3679        })
3680    }
3681
3682    fn open_link(
3683        url: SharedString,
3684        workspace: &WeakEntity<Workspace>,
3685        window: &mut Window,
3686        cx: &mut App,
3687    ) {
3688        let Some(workspace) = workspace.upgrade() else {
3689            cx.open_url(&url);
3690            return;
3691        };
3692
3693        if let Some(mention) = MentionUri::parse(&url).log_err() {
3694            workspace.update(cx, |workspace, cx| match mention {
3695                MentionUri::File { abs_path } => {
3696                    let project = workspace.project();
3697                    let Some(path) =
3698                        project.update(cx, |project, cx| project.find_project_path(abs_path, cx))
3699                    else {
3700                        return;
3701                    };
3702
3703                    workspace
3704                        .open_path(path, None, true, window, cx)
3705                        .detach_and_log_err(cx);
3706                }
3707                MentionUri::PastedImage => {}
3708                MentionUri::Directory { abs_path } => {
3709                    let project = workspace.project();
3710                    let Some(entry) = project.update(cx, |project, cx| {
3711                        let path = project.find_project_path(abs_path, cx)?;
3712                        project.entry_for_path(&path, cx)
3713                    }) else {
3714                        return;
3715                    };
3716
3717                    project.update(cx, |_, cx| {
3718                        cx.emit(project::Event::RevealInProjectPanel(entry.id));
3719                    });
3720                }
3721                MentionUri::Symbol {
3722                    abs_path: path,
3723                    line_range,
3724                    ..
3725                }
3726                | MentionUri::Selection {
3727                    abs_path: Some(path),
3728                    line_range,
3729                } => {
3730                    let project = workspace.project();
3731                    let Some((path, _)) = project.update(cx, |project, cx| {
3732                        let path = project.find_project_path(path, cx)?;
3733                        let entry = project.entry_for_path(&path, cx)?;
3734                        Some((path, entry))
3735                    }) else {
3736                        return;
3737                    };
3738
3739                    let item = workspace.open_path(path, None, true, window, cx);
3740                    window
3741                        .spawn(cx, async move |cx| {
3742                            let Some(editor) = item.await?.downcast::<Editor>() else {
3743                                return Ok(());
3744                            };
3745                            let range = Point::new(*line_range.start(), 0)
3746                                ..Point::new(*line_range.start(), 0);
3747                            editor
3748                                .update_in(cx, |editor, window, cx| {
3749                                    editor.change_selections(
3750                                        SelectionEffects::scroll(Autoscroll::center()),
3751                                        window,
3752                                        cx,
3753                                        |s| s.select_ranges(vec![range]),
3754                                    );
3755                                })
3756                                .ok();
3757                            anyhow::Ok(())
3758                        })
3759                        .detach_and_log_err(cx);
3760                }
3761                MentionUri::Selection { abs_path: None, .. } => {}
3762                MentionUri::Thread { id, name } => {
3763                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
3764                        panel.update(cx, |panel, cx| {
3765                            panel.load_agent_thread(
3766                                DbThreadMetadata {
3767                                    id,
3768                                    title: name.into(),
3769                                    updated_at: Default::default(),
3770                                },
3771                                window,
3772                                cx,
3773                            )
3774                        });
3775                    }
3776                }
3777                MentionUri::TextThread { path, .. } => {
3778                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
3779                        panel.update(cx, |panel, cx| {
3780                            panel
3781                                .open_saved_prompt_editor(path.as_path().into(), window, cx)
3782                                .detach_and_log_err(cx);
3783                        });
3784                    }
3785                }
3786                MentionUri::Rule { id, .. } => {
3787                    let PromptId::User { uuid } = id else {
3788                        return;
3789                    };
3790                    window.dispatch_action(
3791                        Box::new(OpenRulesLibrary {
3792                            prompt_to_select: Some(uuid.0),
3793                        }),
3794                        cx,
3795                    )
3796                }
3797                MentionUri::Fetch { url } => {
3798                    cx.open_url(url.as_str());
3799                }
3800            })
3801        } else {
3802            cx.open_url(&url);
3803        }
3804    }
3805
3806    fn open_tool_call_location(
3807        &self,
3808        entry_ix: usize,
3809        location_ix: usize,
3810        window: &mut Window,
3811        cx: &mut Context<Self>,
3812    ) -> Option<()> {
3813        let (tool_call_location, agent_location) = self
3814            .thread()?
3815            .read(cx)
3816            .entries()
3817            .get(entry_ix)?
3818            .location(location_ix)?;
3819
3820        let project_path = self
3821            .project
3822            .read(cx)
3823            .find_project_path(&tool_call_location.path, cx)?;
3824
3825        let open_task = self
3826            .workspace
3827            .update(cx, |workspace, cx| {
3828                workspace.open_path(project_path, None, true, window, cx)
3829            })
3830            .log_err()?;
3831        window
3832            .spawn(cx, async move |cx| {
3833                let item = open_task.await?;
3834
3835                let Some(active_editor) = item.downcast::<Editor>() else {
3836                    return anyhow::Ok(());
3837                };
3838
3839                active_editor.update_in(cx, |editor, window, cx| {
3840                    let multibuffer = editor.buffer().read(cx);
3841                    let buffer = multibuffer.as_singleton();
3842                    if agent_location.buffer.upgrade() == buffer {
3843                        let excerpt_id = multibuffer.excerpt_ids().first().cloned();
3844                        let anchor = editor::Anchor::in_buffer(
3845                            excerpt_id.unwrap(),
3846                            buffer.unwrap().read(cx).remote_id(),
3847                            agent_location.position,
3848                        );
3849                        editor.change_selections(Default::default(), window, cx, |selections| {
3850                            selections.select_anchor_ranges([anchor..anchor]);
3851                        })
3852                    } else {
3853                        let row = tool_call_location.line.unwrap_or_default();
3854                        editor.change_selections(Default::default(), window, cx, |selections| {
3855                            selections.select_ranges([Point::new(row, 0)..Point::new(row, 0)]);
3856                        })
3857                    }
3858                })?;
3859
3860                anyhow::Ok(())
3861            })
3862            .detach_and_log_err(cx);
3863
3864        None
3865    }
3866
3867    pub fn open_thread_as_markdown(
3868        &self,
3869        workspace: Entity<Workspace>,
3870        window: &mut Window,
3871        cx: &mut App,
3872    ) -> Task<anyhow::Result<()>> {
3873        let markdown_language_task = workspace
3874            .read(cx)
3875            .app_state()
3876            .languages
3877            .language_for_name("Markdown");
3878
3879        let (thread_summary, markdown) = if let Some(thread) = self.thread() {
3880            let thread = thread.read(cx);
3881            (thread.title().to_string(), thread.to_markdown(cx))
3882        } else {
3883            return Task::ready(Ok(()));
3884        };
3885
3886        window.spawn(cx, async move |cx| {
3887            let markdown_language = markdown_language_task.await?;
3888
3889            workspace.update_in(cx, |workspace, window, cx| {
3890                let project = workspace.project().clone();
3891
3892                if !project.read(cx).is_local() {
3893                    bail!("failed to open active thread as markdown in remote project");
3894                }
3895
3896                let buffer = project.update(cx, |project, cx| {
3897                    project.create_local_buffer(&markdown, Some(markdown_language), cx)
3898                });
3899                let buffer = cx.new(|cx| {
3900                    MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
3901                });
3902
3903                workspace.add_item_to_active_pane(
3904                    Box::new(cx.new(|cx| {
3905                        let mut editor =
3906                            Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
3907                        editor.set_breadcrumb_header(thread_summary);
3908                        editor
3909                    })),
3910                    None,
3911                    true,
3912                    window,
3913                    cx,
3914                );
3915
3916                anyhow::Ok(())
3917            })??;
3918            anyhow::Ok(())
3919        })
3920    }
3921
3922    fn scroll_to_top(&mut self, cx: &mut Context<Self>) {
3923        self.list_state.scroll_to(ListOffset::default());
3924        cx.notify();
3925    }
3926
3927    pub fn scroll_to_bottom(&mut self, cx: &mut Context<Self>) {
3928        if let Some(thread) = self.thread() {
3929            let entry_count = thread.read(cx).entries().len();
3930            self.list_state.reset(entry_count);
3931            cx.notify();
3932        }
3933    }
3934
3935    fn notify_with_sound(
3936        &mut self,
3937        caption: impl Into<SharedString>,
3938        icon: IconName,
3939        window: &mut Window,
3940        cx: &mut Context<Self>,
3941    ) {
3942        self.play_notification_sound(window, cx);
3943        self.show_notification(caption, icon, window, cx);
3944    }
3945
3946    fn play_notification_sound(&self, window: &Window, cx: &mut App) {
3947        let settings = AgentSettings::get_global(cx);
3948        if settings.play_sound_when_agent_done && !window.is_window_active() {
3949            Audio::play_sound(Sound::AgentDone, cx);
3950        }
3951    }
3952
3953    fn show_notification(
3954        &mut self,
3955        caption: impl Into<SharedString>,
3956        icon: IconName,
3957        window: &mut Window,
3958        cx: &mut Context<Self>,
3959    ) {
3960        if window.is_window_active() || !self.notifications.is_empty() {
3961            return;
3962        }
3963
3964        // TODO: Change this once we have title summarization for external agents.
3965        let title = self.agent.name();
3966
3967        match AgentSettings::get_global(cx).notify_when_agent_waiting {
3968            NotifyWhenAgentWaiting::PrimaryScreen => {
3969                if let Some(primary) = cx.primary_display() {
3970                    self.pop_up(icon, caption.into(), title, window, primary, cx);
3971                }
3972            }
3973            NotifyWhenAgentWaiting::AllScreens => {
3974                let caption = caption.into();
3975                for screen in cx.displays() {
3976                    self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
3977                }
3978            }
3979            NotifyWhenAgentWaiting::Never => {
3980                // Don't show anything
3981            }
3982        }
3983    }
3984
3985    fn pop_up(
3986        &mut self,
3987        icon: IconName,
3988        caption: SharedString,
3989        title: SharedString,
3990        window: &mut Window,
3991        screen: Rc<dyn PlatformDisplay>,
3992        cx: &mut Context<Self>,
3993    ) {
3994        let options = AgentNotification::window_options(screen, cx);
3995
3996        let project_name = self.workspace.upgrade().and_then(|workspace| {
3997            workspace
3998                .read(cx)
3999                .project()
4000                .read(cx)
4001                .visible_worktrees(cx)
4002                .next()
4003                .map(|worktree| worktree.read(cx).root_name().to_string())
4004        });
4005
4006        if let Some(screen_window) = cx
4007            .open_window(options, |_, cx| {
4008                cx.new(|_| {
4009                    AgentNotification::new(title.clone(), caption.clone(), icon, project_name)
4010                })
4011            })
4012            .log_err()
4013            && let Some(pop_up) = screen_window.entity(cx).log_err()
4014        {
4015            self.notification_subscriptions
4016                .entry(screen_window)
4017                .or_insert_with(Vec::new)
4018                .push(cx.subscribe_in(&pop_up, window, {
4019                    |this, _, event, window, cx| match event {
4020                        AgentNotificationEvent::Accepted => {
4021                            let handle = window.window_handle();
4022                            cx.activate(true);
4023
4024                            let workspace_handle = this.workspace.clone();
4025
4026                            // If there are multiple Zed windows, activate the correct one.
4027                            cx.defer(move |cx| {
4028                                handle
4029                                    .update(cx, |_view, window, _cx| {
4030                                        window.activate_window();
4031
4032                                        if let Some(workspace) = workspace_handle.upgrade() {
4033                                            workspace.update(_cx, |workspace, cx| {
4034                                                workspace.focus_panel::<AgentPanel>(window, cx);
4035                                            });
4036                                        }
4037                                    })
4038                                    .log_err();
4039                            });
4040
4041                            this.dismiss_notifications(cx);
4042                        }
4043                        AgentNotificationEvent::Dismissed => {
4044                            this.dismiss_notifications(cx);
4045                        }
4046                    }
4047                }));
4048
4049            self.notifications.push(screen_window);
4050
4051            // If the user manually refocuses the original window, dismiss the popup.
4052            self.notification_subscriptions
4053                .entry(screen_window)
4054                .or_insert_with(Vec::new)
4055                .push({
4056                    let pop_up_weak = pop_up.downgrade();
4057
4058                    cx.observe_window_activation(window, move |_, window, cx| {
4059                        if window.is_window_active()
4060                            && let Some(pop_up) = pop_up_weak.upgrade()
4061                        {
4062                            pop_up.update(cx, |_, cx| {
4063                                cx.emit(AgentNotificationEvent::Dismissed);
4064                            });
4065                        }
4066                    })
4067                });
4068        }
4069    }
4070
4071    fn dismiss_notifications(&mut self, cx: &mut Context<Self>) {
4072        for window in self.notifications.drain(..) {
4073            window
4074                .update(cx, |_, window, _| {
4075                    window.remove_window();
4076                })
4077                .ok();
4078
4079            self.notification_subscriptions.remove(&window);
4080        }
4081    }
4082
4083    fn render_thread_controls(&self, cx: &Context<Self>) -> impl IntoElement {
4084        let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
4085            .shape(ui::IconButtonShape::Square)
4086            .icon_size(IconSize::Small)
4087            .icon_color(Color::Ignored)
4088            .tooltip(Tooltip::text("Open Thread as Markdown"))
4089            .on_click(cx.listener(move |this, _, window, cx| {
4090                if let Some(workspace) = this.workspace.upgrade() {
4091                    this.open_thread_as_markdown(workspace, window, cx)
4092                        .detach_and_log_err(cx);
4093                }
4094            }));
4095
4096        let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUp)
4097            .shape(ui::IconButtonShape::Square)
4098            .icon_size(IconSize::Small)
4099            .icon_color(Color::Ignored)
4100            .tooltip(Tooltip::text("Scroll To Top"))
4101            .on_click(cx.listener(move |this, _, _, cx| {
4102                this.scroll_to_top(cx);
4103            }));
4104
4105        let mut container = h_flex()
4106            .id("thread-controls-container")
4107            .group("thread-controls-container")
4108            .w_full()
4109            .mr_1()
4110            .pt_1()
4111            .pb_2()
4112            .px(RESPONSE_PADDING_X)
4113            .gap_px()
4114            .opacity(0.4)
4115            .hover(|style| style.opacity(1.))
4116            .flex_wrap()
4117            .justify_end();
4118
4119        if AgentSettings::get_global(cx).enable_feedback
4120            && self
4121                .thread()
4122                .is_some_and(|thread| thread.read(cx).connection().telemetry().is_some())
4123        {
4124            let feedback = self.thread_feedback.feedback;
4125            container = container.child(
4126                div().visible_on_hover("thread-controls-container").child(
4127                    Label::new(
4128                        match feedback {
4129                            Some(ThreadFeedback::Positive) => "Thanks for your feedback!",
4130                            Some(ThreadFeedback::Negative) => "We appreciate your feedback and will use it to improve.",
4131                            None => "Rating the thread sends all of your current conversation to the Zed team.",
4132                        }
4133                    )
4134                    .color(Color::Muted)
4135                    .size(LabelSize::XSmall)
4136                    .truncate(),
4137                ),
4138            ).child(
4139                h_flex()
4140                    .child(
4141                        IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
4142                            .shape(ui::IconButtonShape::Square)
4143                            .icon_size(IconSize::Small)
4144                            .icon_color(match feedback {
4145                                Some(ThreadFeedback::Positive) => Color::Accent,
4146                                _ => Color::Ignored,
4147                            })
4148                            .tooltip(Tooltip::text("Helpful Response"))
4149                            .on_click(cx.listener(move |this, _, window, cx| {
4150                                this.handle_feedback_click(
4151                                    ThreadFeedback::Positive,
4152                                    window,
4153                                    cx,
4154                                );
4155                            })),
4156                    )
4157                    .child(
4158                        IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
4159                            .shape(ui::IconButtonShape::Square)
4160                            .icon_size(IconSize::Small)
4161                            .icon_color(match feedback {
4162                                Some(ThreadFeedback::Negative) => Color::Accent,
4163                                _ => Color::Ignored,
4164                            })
4165                            .tooltip(Tooltip::text("Not Helpful"))
4166                            .on_click(cx.listener(move |this, _, window, cx| {
4167                                this.handle_feedback_click(
4168                                    ThreadFeedback::Negative,
4169                                    window,
4170                                    cx,
4171                                );
4172                            })),
4173                    )
4174            )
4175        }
4176
4177        container.child(open_as_markdown).child(scroll_to_top)
4178    }
4179
4180    fn render_feedback_feedback_editor(
4181        editor: Entity<Editor>,
4182        window: &mut Window,
4183        cx: &Context<Self>,
4184    ) -> Div {
4185        let focus_handle = editor.focus_handle(cx);
4186        v_flex()
4187            .key_context("AgentFeedbackMessageEditor")
4188            .on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| {
4189                this.thread_feedback.dismiss_comments();
4190                cx.notify();
4191            }))
4192            .on_action(cx.listener(move |this, _: &menu::Confirm, _window, cx| {
4193                this.submit_feedback_message(cx);
4194            }))
4195            .mb_2()
4196            .mx_4()
4197            .p_2()
4198            .rounded_md()
4199            .border_1()
4200            .border_color(cx.theme().colors().border)
4201            .bg(cx.theme().colors().editor_background)
4202            .child(editor)
4203            .child(
4204                h_flex()
4205                    .gap_1()
4206                    .justify_end()
4207                    .child(
4208                        Button::new("dismiss-feedback-message", "Cancel")
4209                            .label_size(LabelSize::Small)
4210                            .key_binding(
4211                                KeyBinding::for_action_in(&menu::Cancel, &focus_handle, window, cx)
4212                                    .map(|kb| kb.size(rems_from_px(10.))),
4213                            )
4214                            .on_click(cx.listener(move |this, _, _window, cx| {
4215                                this.thread_feedback.dismiss_comments();
4216                                cx.notify();
4217                            })),
4218                    )
4219                    .child(
4220                        Button::new("submit-feedback-message", "Share Feedback")
4221                            .style(ButtonStyle::Tinted(ui::TintColor::Accent))
4222                            .label_size(LabelSize::Small)
4223                            .key_binding(
4224                                KeyBinding::for_action_in(
4225                                    &menu::Confirm,
4226                                    &focus_handle,
4227                                    window,
4228                                    cx,
4229                                )
4230                                .map(|kb| kb.size(rems_from_px(10.))),
4231                            )
4232                            .on_click(cx.listener(move |this, _, _window, cx| {
4233                                this.submit_feedback_message(cx);
4234                            })),
4235                    ),
4236            )
4237    }
4238
4239    fn handle_feedback_click(
4240        &mut self,
4241        feedback: ThreadFeedback,
4242        window: &mut Window,
4243        cx: &mut Context<Self>,
4244    ) {
4245        let Some(thread) = self.thread().cloned() else {
4246            return;
4247        };
4248
4249        self.thread_feedback.submit(thread, feedback, window, cx);
4250        cx.notify();
4251    }
4252
4253    fn submit_feedback_message(&mut self, cx: &mut Context<Self>) {
4254        let Some(thread) = self.thread().cloned() else {
4255            return;
4256        };
4257
4258        self.thread_feedback.submit_comments(thread, cx);
4259        cx.notify();
4260    }
4261
4262    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
4263        div()
4264            .id("acp-thread-scrollbar")
4265            .occlude()
4266            .on_mouse_move(cx.listener(|_, _, _, cx| {
4267                cx.notify();
4268                cx.stop_propagation()
4269            }))
4270            .on_hover(|_, _, cx| {
4271                cx.stop_propagation();
4272            })
4273            .on_any_mouse_down(|_, _, cx| {
4274                cx.stop_propagation();
4275            })
4276            .on_mouse_up(
4277                MouseButton::Left,
4278                cx.listener(|_, _, _, cx| {
4279                    cx.stop_propagation();
4280                }),
4281            )
4282            .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4283                cx.notify();
4284            }))
4285            .h_full()
4286            .absolute()
4287            .right_1()
4288            .top_1()
4289            .bottom_0()
4290            .w(px(12.))
4291            .cursor_default()
4292            .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx)))
4293    }
4294
4295    fn render_token_limit_callout(
4296        &self,
4297        line_height: Pixels,
4298        cx: &mut Context<Self>,
4299    ) -> Option<Callout> {
4300        let token_usage = self.thread()?.read(cx).token_usage()?;
4301        let ratio = token_usage.ratio();
4302
4303        let (severity, title) = match ratio {
4304            acp_thread::TokenUsageRatio::Normal => return None,
4305            acp_thread::TokenUsageRatio::Warning => {
4306                (Severity::Warning, "Thread reaching the token limit soon")
4307            }
4308            acp_thread::TokenUsageRatio::Exceeded => {
4309                (Severity::Error, "Thread reached the token limit")
4310            }
4311        };
4312
4313        let burn_mode_available = self.as_native_thread(cx).is_some_and(|thread| {
4314            thread.read(cx).completion_mode() == CompletionMode::Normal
4315                && thread
4316                    .read(cx)
4317                    .model()
4318                    .is_some_and(|model| model.supports_burn_mode())
4319        });
4320
4321        let description = if burn_mode_available {
4322            "To continue, start a new thread from a summary or turn Burn Mode on."
4323        } else {
4324            "To continue, start a new thread from a summary."
4325        };
4326
4327        Some(
4328            Callout::new()
4329                .severity(severity)
4330                .line_height(line_height)
4331                .title(title)
4332                .description(description)
4333                .actions_slot(
4334                    h_flex()
4335                        .gap_0p5()
4336                        .child(
4337                            Button::new("start-new-thread", "Start New Thread")
4338                                .label_size(LabelSize::Small)
4339                                .on_click(cx.listener(|this, _, window, cx| {
4340                                    let Some(thread) = this.thread() else {
4341                                        return;
4342                                    };
4343                                    let session_id = thread.read(cx).session_id().clone();
4344                                    window.dispatch_action(
4345                                        crate::NewNativeAgentThreadFromSummary {
4346                                            from_session_id: session_id,
4347                                        }
4348                                        .boxed_clone(),
4349                                        cx,
4350                                    );
4351                                })),
4352                        )
4353                        .when(burn_mode_available, |this| {
4354                            this.child(
4355                                IconButton::new("burn-mode-callout", IconName::ZedBurnMode)
4356                                    .icon_size(IconSize::XSmall)
4357                                    .on_click(cx.listener(|this, _event, window, cx| {
4358                                        this.toggle_burn_mode(&ToggleBurnMode, window, cx);
4359                                    })),
4360                            )
4361                        }),
4362                ),
4363        )
4364    }
4365
4366    fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context<Self>) -> Option<Div> {
4367        if !self.is_using_zed_ai_models(cx) {
4368            return None;
4369        }
4370
4371        let user_store = self.project.read(cx).user_store().read(cx);
4372        if user_store.is_usage_based_billing_enabled() {
4373            return None;
4374        }
4375
4376        let plan = user_store.plan().unwrap_or(cloud_llm_client::Plan::ZedFree);
4377
4378        let usage = user_store.model_request_usage()?;
4379
4380        Some(
4381            div()
4382                .child(UsageCallout::new(plan, usage))
4383                .line_height(line_height),
4384        )
4385    }
4386
4387    fn settings_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
4388        self.entry_view_state.update(cx, |entry_view_state, cx| {
4389            entry_view_state.settings_changed(cx);
4390        });
4391    }
4392
4393    pub(crate) fn insert_dragged_files(
4394        &self,
4395        paths: Vec<project::ProjectPath>,
4396        added_worktrees: Vec<Entity<project::Worktree>>,
4397        window: &mut Window,
4398        cx: &mut Context<Self>,
4399    ) {
4400        self.message_editor.update(cx, |message_editor, cx| {
4401            message_editor.insert_dragged_files(paths, added_worktrees, window, cx);
4402        })
4403    }
4404
4405    pub(crate) fn insert_selections(&self, window: &mut Window, cx: &mut Context<Self>) {
4406        self.message_editor.update(cx, |message_editor, cx| {
4407            message_editor.insert_selections(window, cx);
4408        })
4409    }
4410
4411    fn render_thread_retry_status_callout(
4412        &self,
4413        _window: &mut Window,
4414        _cx: &mut Context<Self>,
4415    ) -> Option<Callout> {
4416        let state = self.thread_retry_status.as_ref()?;
4417
4418        let next_attempt_in = state
4419            .duration
4420            .saturating_sub(Instant::now().saturating_duration_since(state.started_at));
4421        if next_attempt_in.is_zero() {
4422            return None;
4423        }
4424
4425        let next_attempt_in_secs = next_attempt_in.as_secs() + 1;
4426
4427        let retry_message = if state.max_attempts == 1 {
4428            if next_attempt_in_secs == 1 {
4429                "Retrying. Next attempt in 1 second.".to_string()
4430            } else {
4431                format!("Retrying. Next attempt in {next_attempt_in_secs} seconds.")
4432            }
4433        } else if next_attempt_in_secs == 1 {
4434            format!(
4435                "Retrying. Next attempt in 1 second (Attempt {} of {}).",
4436                state.attempt, state.max_attempts,
4437            )
4438        } else {
4439            format!(
4440                "Retrying. Next attempt in {next_attempt_in_secs} seconds (Attempt {} of {}).",
4441                state.attempt, state.max_attempts,
4442            )
4443        };
4444
4445        Some(
4446            Callout::new()
4447                .severity(Severity::Warning)
4448                .title(state.last_error.clone())
4449                .description(retry_message),
4450        )
4451    }
4452
4453    fn render_thread_error(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
4454        let content = match self.thread_error.as_ref()? {
4455            ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx),
4456            ThreadError::AuthenticationRequired(error) => {
4457                self.render_authentication_required_error(error.clone(), cx)
4458            }
4459            ThreadError::PaymentRequired => self.render_payment_required_error(cx),
4460            ThreadError::ModelRequestLimitReached(plan) => {
4461                self.render_model_request_limit_reached_error(*plan, cx)
4462            }
4463            ThreadError::ToolUseLimitReached => {
4464                self.render_tool_use_limit_reached_error(window, cx)?
4465            }
4466        };
4467
4468        Some(div().child(content))
4469    }
4470
4471    fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout {
4472        Callout::new()
4473            .severity(Severity::Error)
4474            .title("Error")
4475            .icon(IconName::XCircle)
4476            .description(error.clone())
4477            .actions_slot(self.create_copy_button(error.to_string()))
4478            .dismiss_action(self.dismiss_error_button(cx))
4479    }
4480
4481    fn render_payment_required_error(&self, cx: &mut Context<Self>) -> Callout {
4482        const ERROR_MESSAGE: &str =
4483            "You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
4484
4485        Callout::new()
4486            .severity(Severity::Error)
4487            .icon(IconName::XCircle)
4488            .title("Free Usage Exceeded")
4489            .description(ERROR_MESSAGE)
4490            .actions_slot(
4491                h_flex()
4492                    .gap_0p5()
4493                    .child(self.upgrade_button(cx))
4494                    .child(self.create_copy_button(ERROR_MESSAGE)),
4495            )
4496            .dismiss_action(self.dismiss_error_button(cx))
4497    }
4498
4499    fn render_authentication_required_error(
4500        &self,
4501        error: SharedString,
4502        cx: &mut Context<Self>,
4503    ) -> Callout {
4504        Callout::new()
4505            .severity(Severity::Error)
4506            .title("Authentication Required")
4507            .icon(IconName::XCircle)
4508            .description(error.clone())
4509            .actions_slot(
4510                h_flex()
4511                    .gap_0p5()
4512                    .child(self.authenticate_button(cx))
4513                    .child(self.create_copy_button(error)),
4514            )
4515            .dismiss_action(self.dismiss_error_button(cx))
4516    }
4517
4518    fn render_model_request_limit_reached_error(
4519        &self,
4520        plan: cloud_llm_client::Plan,
4521        cx: &mut Context<Self>,
4522    ) -> Callout {
4523        let error_message = match plan {
4524            cloud_llm_client::Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
4525            cloud_llm_client::Plan::ZedProTrial | cloud_llm_client::Plan::ZedFree => {
4526                "Upgrade to Zed Pro for more prompts."
4527            }
4528        };
4529
4530        Callout::new()
4531            .severity(Severity::Error)
4532            .title("Model Prompt Limit Reached")
4533            .icon(IconName::XCircle)
4534            .description(error_message)
4535            .actions_slot(
4536                h_flex()
4537                    .gap_0p5()
4538                    .child(self.upgrade_button(cx))
4539                    .child(self.create_copy_button(error_message)),
4540            )
4541            .dismiss_action(self.dismiss_error_button(cx))
4542    }
4543
4544    fn render_tool_use_limit_reached_error(
4545        &self,
4546        window: &mut Window,
4547        cx: &mut Context<Self>,
4548    ) -> Option<Callout> {
4549        let thread = self.as_native_thread(cx)?;
4550        let supports_burn_mode = thread
4551            .read(cx)
4552            .model()
4553            .is_some_and(|model| model.supports_burn_mode());
4554
4555        let focus_handle = self.focus_handle(cx);
4556
4557        Some(
4558            Callout::new()
4559                .icon(IconName::Info)
4560                .title("Consecutive tool use limit reached.")
4561                .actions_slot(
4562                    h_flex()
4563                        .gap_0p5()
4564                        .when(supports_burn_mode, |this| {
4565                            this.child(
4566                                Button::new("continue-burn-mode", "Continue with Burn Mode")
4567                                    .style(ButtonStyle::Filled)
4568                                    .style(ButtonStyle::Tinted(ui::TintColor::Accent))
4569                                    .layer(ElevationIndex::ModalSurface)
4570                                    .label_size(LabelSize::Small)
4571                                    .key_binding(
4572                                        KeyBinding::for_action_in(
4573                                            &ContinueWithBurnMode,
4574                                            &focus_handle,
4575                                            window,
4576                                            cx,
4577                                        )
4578                                        .map(|kb| kb.size(rems_from_px(10.))),
4579                                    )
4580                                    .tooltip(Tooltip::text(
4581                                        "Enable Burn Mode for unlimited tool use.",
4582                                    ))
4583                                    .on_click({
4584                                        cx.listener(move |this, _, _window, cx| {
4585                                            thread.update(cx, |thread, cx| {
4586                                                thread
4587                                                    .set_completion_mode(CompletionMode::Burn, cx);
4588                                            });
4589                                            this.resume_chat(cx);
4590                                        })
4591                                    }),
4592                            )
4593                        })
4594                        .child(
4595                            Button::new("continue-conversation", "Continue")
4596                                .layer(ElevationIndex::ModalSurface)
4597                                .label_size(LabelSize::Small)
4598                                .key_binding(
4599                                    KeyBinding::for_action_in(
4600                                        &ContinueThread,
4601                                        &focus_handle,
4602                                        window,
4603                                        cx,
4604                                    )
4605                                    .map(|kb| kb.size(rems_from_px(10.))),
4606                                )
4607                                .on_click(cx.listener(|this, _, _window, cx| {
4608                                    this.resume_chat(cx);
4609                                })),
4610                        ),
4611                ),
4612        )
4613    }
4614
4615    fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
4616        let message = message.into();
4617
4618        IconButton::new("copy", IconName::Copy)
4619            .icon_size(IconSize::Small)
4620            .icon_color(Color::Muted)
4621            .tooltip(Tooltip::text("Copy Error Message"))
4622            .on_click(move |_, _, cx| {
4623                cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
4624            })
4625    }
4626
4627    fn dismiss_error_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
4628        IconButton::new("dismiss", IconName::Close)
4629            .icon_size(IconSize::Small)
4630            .icon_color(Color::Muted)
4631            .tooltip(Tooltip::text("Dismiss Error"))
4632            .on_click(cx.listener({
4633                move |this, _, _, cx| {
4634                    this.clear_thread_error(cx);
4635                    cx.notify();
4636                }
4637            }))
4638    }
4639
4640    fn authenticate_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
4641        Button::new("authenticate", "Authenticate")
4642            .label_size(LabelSize::Small)
4643            .style(ButtonStyle::Filled)
4644            .on_click(cx.listener({
4645                move |this, _, window, cx| {
4646                    let agent = this.agent.clone();
4647                    let ThreadState::Ready { thread, .. } = &this.thread_state else {
4648                        return;
4649                    };
4650
4651                    let connection = thread.read(cx).connection().clone();
4652                    let err = AuthRequired {
4653                        description: None,
4654                        provider_id: None,
4655                    };
4656                    this.clear_thread_error(cx);
4657                    let this = cx.weak_entity();
4658                    window.defer(cx, |window, cx| {
4659                        Self::handle_auth_required(this, err, agent, connection, window, cx);
4660                    })
4661                }
4662            }))
4663    }
4664
4665    fn upgrade_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
4666        Button::new("upgrade", "Upgrade")
4667            .label_size(LabelSize::Small)
4668            .style(ButtonStyle::Tinted(ui::TintColor::Accent))
4669            .on_click(cx.listener({
4670                move |this, _, _, cx| {
4671                    this.clear_thread_error(cx);
4672                    cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx));
4673                }
4674            }))
4675    }
4676
4677    fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4678        self.thread_state = Self::initial_state(
4679            self.agent.clone(),
4680            None,
4681            self.workspace.clone(),
4682            self.project.clone(),
4683            window,
4684            cx,
4685        );
4686        cx.notify();
4687    }
4688
4689    pub fn delete_history_entry(&mut self, entry: HistoryEntry, cx: &mut Context<Self>) {
4690        let task = match entry {
4691            HistoryEntry::AcpThread(thread) => self.history_store.update(cx, |history, cx| {
4692                history.delete_thread(thread.id.clone(), cx)
4693            }),
4694            HistoryEntry::TextThread(context) => self.history_store.update(cx, |history, cx| {
4695                history.delete_text_thread(context.path.clone(), cx)
4696            }),
4697        };
4698        task.detach_and_log_err(cx);
4699    }
4700}
4701
4702fn loading_contents_spinner(size: IconSize) -> AnyElement {
4703    Icon::new(IconName::LoadCircle)
4704        .size(size)
4705        .color(Color::Accent)
4706        .with_animation(
4707            "load_context_circle",
4708            Animation::new(Duration::from_secs(3)).repeat(),
4709            |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
4710        )
4711        .into_any_element()
4712}
4713
4714impl Focusable for AcpThreadView {
4715    fn focus_handle(&self, cx: &App) -> FocusHandle {
4716        match self.thread_state {
4717            ThreadState::Loading { .. } | ThreadState::Ready { .. } => {
4718                self.message_editor.focus_handle(cx)
4719            }
4720            ThreadState::LoadError(_) | ThreadState::Unauthenticated { .. } => {
4721                self.focus_handle.clone()
4722            }
4723        }
4724    }
4725}
4726
4727impl Render for AcpThreadView {
4728    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4729        let has_messages = self.list_state.item_count() > 0;
4730        let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5;
4731
4732        v_flex()
4733            .size_full()
4734            .key_context("AcpThread")
4735            .on_action(cx.listener(Self::open_agent_diff))
4736            .on_action(cx.listener(Self::toggle_burn_mode))
4737            .on_action(cx.listener(Self::keep_all))
4738            .on_action(cx.listener(Self::reject_all))
4739            .track_focus(&self.focus_handle)
4740            .bg(cx.theme().colors().panel_background)
4741            .child(match &self.thread_state {
4742                ThreadState::Unauthenticated {
4743                    connection,
4744                    description,
4745                    configuration_view,
4746                    pending_auth_method,
4747                    ..
4748                } => self.render_auth_required_state(
4749                    connection,
4750                    description.as_ref(),
4751                    configuration_view.as_ref(),
4752                    pending_auth_method.as_ref(),
4753                    window,
4754                    cx,
4755                ),
4756                ThreadState::Loading { .. } => v_flex()
4757                    .flex_1()
4758                    .child(self.render_recent_history(window, cx)),
4759                ThreadState::LoadError(e) => v_flex()
4760                    .flex_1()
4761                    .size_full()
4762                    .items_center()
4763                    .justify_end()
4764                    .child(self.render_load_error(e, cx)),
4765                ThreadState::Ready { thread, .. } => {
4766                    let thread_clone = thread.clone();
4767
4768                    v_flex().flex_1().map(|this| {
4769                        if has_messages {
4770                            this.child(
4771                                list(
4772                                    self.list_state.clone(),
4773                                    cx.processor(|this, index: usize, window, cx| {
4774                                        let Some((entry, len)) = this.thread().and_then(|thread| {
4775                                            let entries = &thread.read(cx).entries();
4776                                            Some((entries.get(index)?, entries.len()))
4777                                        }) else {
4778                                            return Empty.into_any();
4779                                        };
4780                                        this.render_entry(index, len, entry, window, cx)
4781                                    }),
4782                                )
4783                                .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
4784                                .flex_grow()
4785                                .into_any(),
4786                            )
4787                            .child(self.render_vertical_scrollbar(cx))
4788                            .children(
4789                                match thread_clone.read(cx).status() {
4790                                    ThreadStatus::Idle
4791                                    | ThreadStatus::WaitingForToolConfirmation => None,
4792                                    ThreadStatus::Generating => div()
4793                                        .py_2()
4794                                        .px(rems_from_px(22.))
4795                                        .child(SpinnerLabel::new().size(LabelSize::Small))
4796                                        .into(),
4797                                },
4798                            )
4799                        } else {
4800                            this.child(self.render_recent_history(window, cx))
4801                        }
4802                    })
4803                }
4804            })
4805            // The activity bar is intentionally rendered outside of the ThreadState::Ready match
4806            // above so that the scrollbar doesn't render behind it. The current setup allows
4807            // the scrollbar to stop exactly at the activity bar start.
4808            .when(has_messages, |this| match &self.thread_state {
4809                ThreadState::Ready { thread, .. } => {
4810                    this.children(self.render_activity_bar(thread, window, cx))
4811                }
4812                _ => this,
4813            })
4814            .children(self.render_thread_retry_status_callout(window, cx))
4815            .children(self.render_thread_error(window, cx))
4816            .children(
4817                if let Some(usage_callout) = self.render_usage_callout(line_height, cx) {
4818                    Some(usage_callout.into_any_element())
4819                } else {
4820                    self.render_token_limit_callout(line_height, cx)
4821                        .map(|token_limit_callout| token_limit_callout.into_any_element())
4822                },
4823            )
4824            .child(self.render_message_editor(window, cx))
4825    }
4826}
4827
4828fn default_markdown_style(
4829    buffer_font: bool,
4830    muted_text: bool,
4831    window: &Window,
4832    cx: &App,
4833) -> MarkdownStyle {
4834    let theme_settings = ThemeSettings::get_global(cx);
4835    let colors = cx.theme().colors();
4836
4837    let buffer_font_size = TextSize::Small.rems(cx);
4838
4839    let mut text_style = window.text_style();
4840    let line_height = buffer_font_size * 1.75;
4841
4842    let font_family = if buffer_font {
4843        theme_settings.buffer_font.family.clone()
4844    } else {
4845        theme_settings.ui_font.family.clone()
4846    };
4847
4848    let font_size = if buffer_font {
4849        TextSize::Small.rems(cx)
4850    } else {
4851        TextSize::Default.rems(cx)
4852    };
4853
4854    let text_color = if muted_text {
4855        colors.text_muted
4856    } else {
4857        colors.text
4858    };
4859
4860    text_style.refine(&TextStyleRefinement {
4861        font_family: Some(font_family),
4862        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
4863        font_features: Some(theme_settings.ui_font.features.clone()),
4864        font_size: Some(font_size.into()),
4865        line_height: Some(line_height.into()),
4866        color: Some(text_color),
4867        ..Default::default()
4868    });
4869
4870    MarkdownStyle {
4871        base_text_style: text_style.clone(),
4872        syntax: cx.theme().syntax().clone(),
4873        selection_background_color: colors.element_selection_background,
4874        code_block_overflow_x_scroll: true,
4875        table_overflow_x_scroll: true,
4876        heading_level_styles: Some(HeadingLevelStyles {
4877            h1: Some(TextStyleRefinement {
4878                font_size: Some(rems(1.15).into()),
4879                ..Default::default()
4880            }),
4881            h2: Some(TextStyleRefinement {
4882                font_size: Some(rems(1.1).into()),
4883                ..Default::default()
4884            }),
4885            h3: Some(TextStyleRefinement {
4886                font_size: Some(rems(1.05).into()),
4887                ..Default::default()
4888            }),
4889            h4: Some(TextStyleRefinement {
4890                font_size: Some(rems(1.).into()),
4891                ..Default::default()
4892            }),
4893            h5: Some(TextStyleRefinement {
4894                font_size: Some(rems(0.95).into()),
4895                ..Default::default()
4896            }),
4897            h6: Some(TextStyleRefinement {
4898                font_size: Some(rems(0.875).into()),
4899                ..Default::default()
4900            }),
4901        }),
4902        code_block: StyleRefinement {
4903            padding: EdgesRefinement {
4904                top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
4905                left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
4906                right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
4907                bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
4908            },
4909            margin: EdgesRefinement {
4910                top: Some(Length::Definite(Pixels(8.).into())),
4911                left: Some(Length::Definite(Pixels(0.).into())),
4912                right: Some(Length::Definite(Pixels(0.).into())),
4913                bottom: Some(Length::Definite(Pixels(12.).into())),
4914            },
4915            border_style: Some(BorderStyle::Solid),
4916            border_widths: EdgesRefinement {
4917                top: Some(AbsoluteLength::Pixels(Pixels(1.))),
4918                left: Some(AbsoluteLength::Pixels(Pixels(1.))),
4919                right: Some(AbsoluteLength::Pixels(Pixels(1.))),
4920                bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
4921            },
4922            border_color: Some(colors.border_variant),
4923            background: Some(colors.editor_background.into()),
4924            text: Some(TextStyleRefinement {
4925                font_family: Some(theme_settings.buffer_font.family.clone()),
4926                font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
4927                font_features: Some(theme_settings.buffer_font.features.clone()),
4928                font_size: Some(buffer_font_size.into()),
4929                ..Default::default()
4930            }),
4931            ..Default::default()
4932        },
4933        inline_code: TextStyleRefinement {
4934            font_family: Some(theme_settings.buffer_font.family.clone()),
4935            font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
4936            font_features: Some(theme_settings.buffer_font.features.clone()),
4937            font_size: Some(buffer_font_size.into()),
4938            background_color: Some(colors.editor_foreground.opacity(0.08)),
4939            ..Default::default()
4940        },
4941        link: TextStyleRefinement {
4942            background_color: Some(colors.editor_foreground.opacity(0.025)),
4943            underline: Some(UnderlineStyle {
4944                color: Some(colors.text_accent.opacity(0.5)),
4945                thickness: px(1.),
4946                ..Default::default()
4947            }),
4948            ..Default::default()
4949        },
4950        ..Default::default()
4951    }
4952}
4953
4954fn plan_label_markdown_style(
4955    status: &acp::PlanEntryStatus,
4956    window: &Window,
4957    cx: &App,
4958) -> MarkdownStyle {
4959    let default_md_style = default_markdown_style(false, false, window, cx);
4960
4961    MarkdownStyle {
4962        base_text_style: TextStyle {
4963            color: cx.theme().colors().text_muted,
4964            strikethrough: if matches!(status, acp::PlanEntryStatus::Completed) {
4965                Some(gpui::StrikethroughStyle {
4966                    thickness: px(1.),
4967                    color: Some(cx.theme().colors().text_muted.opacity(0.8)),
4968                })
4969            } else {
4970                None
4971            },
4972            ..default_md_style.base_text_style
4973        },
4974        ..default_md_style
4975    }
4976}
4977
4978fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
4979    let default_md_style = default_markdown_style(true, false, window, cx);
4980
4981    MarkdownStyle {
4982        base_text_style: TextStyle {
4983            ..default_md_style.base_text_style
4984        },
4985        selection_background_color: cx.theme().colors().element_selection_background,
4986        ..Default::default()
4987    }
4988}
4989
4990#[cfg(test)]
4991pub(crate) mod tests {
4992    use acp_thread::StubAgentConnection;
4993    use agent_client_protocol::SessionId;
4994    use assistant_context::ContextStore;
4995    use editor::EditorSettings;
4996    use fs::FakeFs;
4997    use gpui::{EventEmitter, SemanticVersion, TestAppContext, VisualTestContext};
4998    use project::Project;
4999    use serde_json::json;
5000    use settings::SettingsStore;
5001    use std::any::Any;
5002    use std::path::Path;
5003    use workspace::Item;
5004
5005    use super::*;
5006
5007    #[gpui::test]
5008    async fn test_drop(cx: &mut TestAppContext) {
5009        init_test(cx);
5010
5011        let (thread_view, _cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
5012        let weak_view = thread_view.downgrade();
5013        drop(thread_view);
5014        assert!(!weak_view.is_upgradable());
5015    }
5016
5017    #[gpui::test]
5018    async fn test_notification_for_stop_event(cx: &mut TestAppContext) {
5019        init_test(cx);
5020
5021        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
5022
5023        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
5024        message_editor.update_in(cx, |editor, window, cx| {
5025            editor.set_text("Hello", window, cx);
5026        });
5027
5028        cx.deactivate_window();
5029
5030        thread_view.update_in(cx, |thread_view, window, cx| {
5031            thread_view.send(window, cx);
5032        });
5033
5034        cx.run_until_parked();
5035
5036        assert!(
5037            cx.windows()
5038                .iter()
5039                .any(|window| window.downcast::<AgentNotification>().is_some())
5040        );
5041    }
5042
5043    #[gpui::test]
5044    async fn test_notification_for_error(cx: &mut TestAppContext) {
5045        init_test(cx);
5046
5047        let (thread_view, cx) =
5048            setup_thread_view(StubAgentServer::new(SaboteurAgentConnection), cx).await;
5049
5050        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
5051        message_editor.update_in(cx, |editor, window, cx| {
5052            editor.set_text("Hello", window, cx);
5053        });
5054
5055        cx.deactivate_window();
5056
5057        thread_view.update_in(cx, |thread_view, window, cx| {
5058            thread_view.send(window, cx);
5059        });
5060
5061        cx.run_until_parked();
5062
5063        assert!(
5064            cx.windows()
5065                .iter()
5066                .any(|window| window.downcast::<AgentNotification>().is_some())
5067        );
5068    }
5069
5070    #[gpui::test]
5071    async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
5072        init_test(cx);
5073
5074        let tool_call_id = acp::ToolCallId("1".into());
5075        let tool_call = acp::ToolCall {
5076            id: tool_call_id.clone(),
5077            title: "Label".into(),
5078            kind: acp::ToolKind::Edit,
5079            status: acp::ToolCallStatus::Pending,
5080            content: vec!["hi".into()],
5081            locations: vec![],
5082            raw_input: None,
5083            raw_output: None,
5084        };
5085        let connection =
5086            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
5087                tool_call_id,
5088                vec![acp::PermissionOption {
5089                    id: acp::PermissionOptionId("1".into()),
5090                    name: "Allow".into(),
5091                    kind: acp::PermissionOptionKind::AllowOnce,
5092                }],
5093            )]));
5094
5095        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
5096
5097        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
5098
5099        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
5100        message_editor.update_in(cx, |editor, window, cx| {
5101            editor.set_text("Hello", window, cx);
5102        });
5103
5104        cx.deactivate_window();
5105
5106        thread_view.update_in(cx, |thread_view, window, cx| {
5107            thread_view.send(window, cx);
5108        });
5109
5110        cx.run_until_parked();
5111
5112        assert!(
5113            cx.windows()
5114                .iter()
5115                .any(|window| window.downcast::<AgentNotification>().is_some())
5116        );
5117    }
5118
5119    async fn setup_thread_view(
5120        agent: impl AgentServer + 'static,
5121        cx: &mut TestAppContext,
5122    ) -> (Entity<AcpThreadView>, &mut VisualTestContext) {
5123        let fs = FakeFs::new(cx.executor());
5124        let project = Project::test(fs, [], cx).await;
5125        let (workspace, cx) =
5126            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5127
5128        let context_store =
5129            cx.update(|_window, cx| cx.new(|cx| ContextStore::fake(project.clone(), cx)));
5130        let history_store =
5131            cx.update(|_window, cx| cx.new(|cx| HistoryStore::new(context_store, cx)));
5132
5133        let thread_view = cx.update(|window, cx| {
5134            cx.new(|cx| {
5135                AcpThreadView::new(
5136                    Rc::new(agent),
5137                    None,
5138                    None,
5139                    workspace.downgrade(),
5140                    project,
5141                    history_store,
5142                    None,
5143                    window,
5144                    cx,
5145                )
5146            })
5147        });
5148        cx.run_until_parked();
5149        (thread_view, cx)
5150    }
5151
5152    fn add_to_workspace(thread_view: Entity<AcpThreadView>, cx: &mut VisualTestContext) {
5153        let workspace = thread_view.read_with(cx, |thread_view, _cx| thread_view.workspace.clone());
5154
5155        workspace
5156            .update_in(cx, |workspace, window, cx| {
5157                workspace.add_item_to_active_pane(
5158                    Box::new(cx.new(|_| ThreadViewItem(thread_view.clone()))),
5159                    None,
5160                    true,
5161                    window,
5162                    cx,
5163                );
5164            })
5165            .unwrap();
5166    }
5167
5168    struct ThreadViewItem(Entity<AcpThreadView>);
5169
5170    impl Item for ThreadViewItem {
5171        type Event = ();
5172
5173        fn include_in_nav_history() -> bool {
5174            false
5175        }
5176
5177        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
5178            "Test".into()
5179        }
5180    }
5181
5182    impl EventEmitter<()> for ThreadViewItem {}
5183
5184    impl Focusable for ThreadViewItem {
5185        fn focus_handle(&self, cx: &App) -> FocusHandle {
5186            self.0.read(cx).focus_handle(cx)
5187        }
5188    }
5189
5190    impl Render for ThreadViewItem {
5191        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
5192            self.0.clone().into_any_element()
5193        }
5194    }
5195
5196    struct StubAgentServer<C> {
5197        connection: C,
5198    }
5199
5200    impl<C> StubAgentServer<C> {
5201        fn new(connection: C) -> Self {
5202            Self { connection }
5203        }
5204    }
5205
5206    impl StubAgentServer<StubAgentConnection> {
5207        fn default_response() -> Self {
5208            let conn = StubAgentConnection::new();
5209            conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk {
5210                content: "Default response".into(),
5211            }]);
5212            Self::new(conn)
5213        }
5214    }
5215
5216    impl<C> AgentServer for StubAgentServer<C>
5217    where
5218        C: 'static + AgentConnection + Send + Clone,
5219    {
5220        fn logo(&self) -> ui::IconName {
5221            ui::IconName::Ai
5222        }
5223
5224        fn name(&self) -> SharedString {
5225            "Test".into()
5226        }
5227
5228        fn empty_state_headline(&self) -> SharedString {
5229            "Test".into()
5230        }
5231
5232        fn empty_state_message(&self) -> SharedString {
5233            "Test".into()
5234        }
5235
5236        fn connect(
5237            &self,
5238            _root_dir: &Path,
5239            _project: &Entity<Project>,
5240            _cx: &mut App,
5241        ) -> Task<gpui::Result<Rc<dyn AgentConnection>>> {
5242            Task::ready(Ok(Rc::new(self.connection.clone())))
5243        }
5244
5245        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
5246            self
5247        }
5248    }
5249
5250    #[derive(Clone)]
5251    struct SaboteurAgentConnection;
5252
5253    impl AgentConnection for SaboteurAgentConnection {
5254        fn new_thread(
5255            self: Rc<Self>,
5256            project: Entity<Project>,
5257            _cwd: &Path,
5258            cx: &mut gpui::App,
5259        ) -> Task<gpui::Result<Entity<AcpThread>>> {
5260            Task::ready(Ok(cx.new(|cx| {
5261                let action_log = cx.new(|_| ActionLog::new(project.clone()));
5262                AcpThread::new(
5263                    "SaboteurAgentConnection",
5264                    self,
5265                    project,
5266                    action_log,
5267                    SessionId("test".into()),
5268                )
5269            })))
5270        }
5271
5272        fn auth_methods(&self) -> &[acp::AuthMethod] {
5273            &[]
5274        }
5275
5276        fn prompt_capabilities(&self) -> acp::PromptCapabilities {
5277            acp::PromptCapabilities {
5278                image: true,
5279                audio: true,
5280                embedded_context: true,
5281            }
5282        }
5283
5284        fn authenticate(
5285            &self,
5286            _method_id: acp::AuthMethodId,
5287            _cx: &mut App,
5288        ) -> Task<gpui::Result<()>> {
5289            unimplemented!()
5290        }
5291
5292        fn prompt(
5293            &self,
5294            _id: Option<acp_thread::UserMessageId>,
5295            _params: acp::PromptRequest,
5296            _cx: &mut App,
5297        ) -> Task<gpui::Result<acp::PromptResponse>> {
5298            Task::ready(Err(anyhow::anyhow!("Error prompting")))
5299        }
5300
5301        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
5302            unimplemented!()
5303        }
5304
5305        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
5306            self
5307        }
5308    }
5309
5310    pub(crate) fn init_test(cx: &mut TestAppContext) {
5311        cx.update(|cx| {
5312            let settings_store = SettingsStore::test(cx);
5313            cx.set_global(settings_store);
5314            language::init(cx);
5315            Project::init_settings(cx);
5316            AgentSettings::register(cx);
5317            workspace::init_settings(cx);
5318            ThemeSettings::register(cx);
5319            release_channel::init(SemanticVersion::default(), cx);
5320            EditorSettings::register(cx);
5321            prompt_store::init(cx)
5322        });
5323    }
5324
5325    #[gpui::test]
5326    async fn test_rewind_views(cx: &mut TestAppContext) {
5327        init_test(cx);
5328
5329        let fs = FakeFs::new(cx.executor());
5330        fs.insert_tree(
5331            "/project",
5332            json!({
5333                "test1.txt": "old content 1",
5334                "test2.txt": "old content 2"
5335            }),
5336        )
5337        .await;
5338        let project = Project::test(fs, [Path::new("/project")], cx).await;
5339        let (workspace, cx) =
5340            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5341
5342        let context_store =
5343            cx.update(|_window, cx| cx.new(|cx| ContextStore::fake(project.clone(), cx)));
5344        let history_store =
5345            cx.update(|_window, cx| cx.new(|cx| HistoryStore::new(context_store, cx)));
5346
5347        let connection = Rc::new(StubAgentConnection::new());
5348        let thread_view = cx.update(|window, cx| {
5349            cx.new(|cx| {
5350                AcpThreadView::new(
5351                    Rc::new(StubAgentServer::new(connection.as_ref().clone())),
5352                    None,
5353                    None,
5354                    workspace.downgrade(),
5355                    project.clone(),
5356                    history_store.clone(),
5357                    None,
5358                    window,
5359                    cx,
5360                )
5361            })
5362        });
5363
5364        cx.run_until_parked();
5365
5366        let thread = thread_view
5367            .read_with(cx, |view, _| view.thread().cloned())
5368            .unwrap();
5369
5370        // First user message
5371        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(acp::ToolCall {
5372            id: acp::ToolCallId("tool1".into()),
5373            title: "Edit file 1".into(),
5374            kind: acp::ToolKind::Edit,
5375            status: acp::ToolCallStatus::Completed,
5376            content: vec![acp::ToolCallContent::Diff {
5377                diff: acp::Diff {
5378                    path: "/project/test1.txt".into(),
5379                    old_text: Some("old content 1".into()),
5380                    new_text: "new content 1".into(),
5381                },
5382            }],
5383            locations: vec![],
5384            raw_input: None,
5385            raw_output: None,
5386        })]);
5387
5388        thread
5389            .update(cx, |thread, cx| thread.send_raw("Give me a diff", cx))
5390            .await
5391            .unwrap();
5392        cx.run_until_parked();
5393
5394        thread.read_with(cx, |thread, _| {
5395            assert_eq!(thread.entries().len(), 2);
5396        });
5397
5398        thread_view.read_with(cx, |view, cx| {
5399            view.entry_view_state.read_with(cx, |entry_view_state, _| {
5400                assert!(
5401                    entry_view_state
5402                        .entry(0)
5403                        .unwrap()
5404                        .message_editor()
5405                        .is_some()
5406                );
5407                assert!(entry_view_state.entry(1).unwrap().has_content());
5408            });
5409        });
5410
5411        // Second user message
5412        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(acp::ToolCall {
5413            id: acp::ToolCallId("tool2".into()),
5414            title: "Edit file 2".into(),
5415            kind: acp::ToolKind::Edit,
5416            status: acp::ToolCallStatus::Completed,
5417            content: vec![acp::ToolCallContent::Diff {
5418                diff: acp::Diff {
5419                    path: "/project/test2.txt".into(),
5420                    old_text: Some("old content 2".into()),
5421                    new_text: "new content 2".into(),
5422                },
5423            }],
5424            locations: vec![],
5425            raw_input: None,
5426            raw_output: None,
5427        })]);
5428
5429        thread
5430            .update(cx, |thread, cx| thread.send_raw("Another one", cx))
5431            .await
5432            .unwrap();
5433        cx.run_until_parked();
5434
5435        let second_user_message_id = thread.read_with(cx, |thread, _| {
5436            assert_eq!(thread.entries().len(), 4);
5437            let AgentThreadEntry::UserMessage(user_message) = &thread.entries()[2] else {
5438                panic!();
5439            };
5440            user_message.id.clone().unwrap()
5441        });
5442
5443        thread_view.read_with(cx, |view, cx| {
5444            view.entry_view_state.read_with(cx, |entry_view_state, _| {
5445                assert!(
5446                    entry_view_state
5447                        .entry(0)
5448                        .unwrap()
5449                        .message_editor()
5450                        .is_some()
5451                );
5452                assert!(entry_view_state.entry(1).unwrap().has_content());
5453                assert!(
5454                    entry_view_state
5455                        .entry(2)
5456                        .unwrap()
5457                        .message_editor()
5458                        .is_some()
5459                );
5460                assert!(entry_view_state.entry(3).unwrap().has_content());
5461            });
5462        });
5463
5464        // Rewind to first message
5465        thread
5466            .update(cx, |thread, cx| thread.rewind(second_user_message_id, cx))
5467            .await
5468            .unwrap();
5469
5470        cx.run_until_parked();
5471
5472        thread.read_with(cx, |thread, _| {
5473            assert_eq!(thread.entries().len(), 2);
5474        });
5475
5476        thread_view.read_with(cx, |view, cx| {
5477            view.entry_view_state.read_with(cx, |entry_view_state, _| {
5478                assert!(
5479                    entry_view_state
5480                        .entry(0)
5481                        .unwrap()
5482                        .message_editor()
5483                        .is_some()
5484                );
5485                assert!(entry_view_state.entry(1).unwrap().has_content());
5486
5487                // Old views should be dropped
5488                assert!(entry_view_state.entry(2).is_none());
5489                assert!(entry_view_state.entry(3).is_none());
5490            });
5491        });
5492    }
5493
5494    #[gpui::test]
5495    async fn test_message_editing_cancel(cx: &mut TestAppContext) {
5496        init_test(cx);
5497
5498        let connection = StubAgentConnection::new();
5499
5500        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk {
5501            content: acp::ContentBlock::Text(acp::TextContent {
5502                text: "Response".into(),
5503                annotations: None,
5504            }),
5505        }]);
5506
5507        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
5508        add_to_workspace(thread_view.clone(), cx);
5509
5510        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
5511        message_editor.update_in(cx, |editor, window, cx| {
5512            editor.set_text("Original message to edit", window, cx);
5513        });
5514        thread_view.update_in(cx, |thread_view, window, cx| {
5515            thread_view.send(window, cx);
5516        });
5517
5518        cx.run_until_parked();
5519
5520        let user_message_editor = thread_view.read_with(cx, |view, cx| {
5521            assert_eq!(view.editing_message, None);
5522
5523            view.entry_view_state
5524                .read(cx)
5525                .entry(0)
5526                .unwrap()
5527                .message_editor()
5528                .unwrap()
5529                .clone()
5530        });
5531
5532        // Focus
5533        cx.focus(&user_message_editor);
5534        thread_view.read_with(cx, |view, _cx| {
5535            assert_eq!(view.editing_message, Some(0));
5536        });
5537
5538        // Edit
5539        user_message_editor.update_in(cx, |editor, window, cx| {
5540            editor.set_text("Edited message content", window, cx);
5541        });
5542
5543        // Cancel
5544        user_message_editor.update_in(cx, |_editor, window, cx| {
5545            window.dispatch_action(Box::new(editor::actions::Cancel), cx);
5546        });
5547
5548        thread_view.read_with(cx, |view, _cx| {
5549            assert_eq!(view.editing_message, None);
5550        });
5551
5552        user_message_editor.read_with(cx, |editor, cx| {
5553            assert_eq!(editor.text(cx), "Original message to edit");
5554        });
5555    }
5556
5557    #[gpui::test]
5558    async fn test_message_doesnt_send_if_empty(cx: &mut TestAppContext) {
5559        init_test(cx);
5560
5561        let connection = StubAgentConnection::new();
5562
5563        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
5564        add_to_workspace(thread_view.clone(), cx);
5565
5566        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
5567        let mut events = cx.events(&message_editor);
5568        message_editor.update_in(cx, |editor, window, cx| {
5569            editor.set_text("", window, cx);
5570        });
5571
5572        message_editor.update_in(cx, |_editor, window, cx| {
5573            window.dispatch_action(Box::new(Chat), cx);
5574        });
5575        cx.run_until_parked();
5576        // We shouldn't have received any messages
5577        assert!(matches!(
5578            events.try_next(),
5579            Err(futures::channel::mpsc::TryRecvError { .. })
5580        ));
5581    }
5582
5583    #[gpui::test]
5584    async fn test_message_editing_regenerate(cx: &mut TestAppContext) {
5585        init_test(cx);
5586
5587        let connection = StubAgentConnection::new();
5588
5589        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk {
5590            content: acp::ContentBlock::Text(acp::TextContent {
5591                text: "Response".into(),
5592                annotations: None,
5593            }),
5594        }]);
5595
5596        let (thread_view, cx) =
5597            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
5598        add_to_workspace(thread_view.clone(), cx);
5599
5600        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
5601        message_editor.update_in(cx, |editor, window, cx| {
5602            editor.set_text("Original message to edit", window, cx);
5603        });
5604        thread_view.update_in(cx, |thread_view, window, cx| {
5605            thread_view.send(window, cx);
5606        });
5607
5608        cx.run_until_parked();
5609
5610        let user_message_editor = thread_view.read_with(cx, |view, cx| {
5611            assert_eq!(view.editing_message, None);
5612            assert_eq!(view.thread().unwrap().read(cx).entries().len(), 2);
5613
5614            view.entry_view_state
5615                .read(cx)
5616                .entry(0)
5617                .unwrap()
5618                .message_editor()
5619                .unwrap()
5620                .clone()
5621        });
5622
5623        // Focus
5624        cx.focus(&user_message_editor);
5625
5626        // Edit
5627        user_message_editor.update_in(cx, |editor, window, cx| {
5628            editor.set_text("Edited message content", window, cx);
5629        });
5630
5631        // Send
5632        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk {
5633            content: acp::ContentBlock::Text(acp::TextContent {
5634                text: "New Response".into(),
5635                annotations: None,
5636            }),
5637        }]);
5638
5639        user_message_editor.update_in(cx, |_editor, window, cx| {
5640            window.dispatch_action(Box::new(Chat), cx);
5641        });
5642
5643        cx.run_until_parked();
5644
5645        thread_view.read_with(cx, |view, cx| {
5646            assert_eq!(view.editing_message, None);
5647
5648            let entries = view.thread().unwrap().read(cx).entries();
5649            assert_eq!(entries.len(), 2);
5650            assert_eq!(
5651                entries[0].to_markdown(cx),
5652                "## User\n\nEdited message content\n\n"
5653            );
5654            assert_eq!(
5655                entries[1].to_markdown(cx),
5656                "## Assistant\n\nNew Response\n\n"
5657            );
5658
5659            let new_editor = view.entry_view_state.read_with(cx, |state, _cx| {
5660                assert!(!state.entry(1).unwrap().has_content());
5661                state.entry(0).unwrap().message_editor().unwrap().clone()
5662            });
5663
5664            assert_eq!(new_editor.read(cx).text(cx), "Edited message content");
5665        })
5666    }
5667
5668    #[gpui::test]
5669    async fn test_message_editing_while_generating(cx: &mut TestAppContext) {
5670        init_test(cx);
5671
5672        let connection = StubAgentConnection::new();
5673
5674        let (thread_view, cx) =
5675            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
5676        add_to_workspace(thread_view.clone(), cx);
5677
5678        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
5679        message_editor.update_in(cx, |editor, window, cx| {
5680            editor.set_text("Original message to edit", window, cx);
5681        });
5682        thread_view.update_in(cx, |thread_view, window, cx| {
5683            thread_view.send(window, cx);
5684        });
5685
5686        cx.run_until_parked();
5687
5688        let (user_message_editor, session_id) = thread_view.read_with(cx, |view, cx| {
5689            let thread = view.thread().unwrap().read(cx);
5690            assert_eq!(thread.entries().len(), 1);
5691
5692            let editor = view
5693                .entry_view_state
5694                .read(cx)
5695                .entry(0)
5696                .unwrap()
5697                .message_editor()
5698                .unwrap()
5699                .clone();
5700
5701            (editor, thread.session_id().clone())
5702        });
5703
5704        // Focus
5705        cx.focus(&user_message_editor);
5706
5707        thread_view.read_with(cx, |view, _cx| {
5708            assert_eq!(view.editing_message, Some(0));
5709        });
5710
5711        // Edit
5712        user_message_editor.update_in(cx, |editor, window, cx| {
5713            editor.set_text("Edited message content", window, cx);
5714        });
5715
5716        thread_view.read_with(cx, |view, _cx| {
5717            assert_eq!(view.editing_message, Some(0));
5718        });
5719
5720        // Finish streaming response
5721        cx.update(|_, cx| {
5722            connection.send_update(
5723                session_id.clone(),
5724                acp::SessionUpdate::AgentMessageChunk {
5725                    content: acp::ContentBlock::Text(acp::TextContent {
5726                        text: "Response".into(),
5727                        annotations: None,
5728                    }),
5729                },
5730                cx,
5731            );
5732            connection.end_turn(session_id, acp::StopReason::EndTurn);
5733        });
5734
5735        thread_view.read_with(cx, |view, _cx| {
5736            assert_eq!(view.editing_message, Some(0));
5737        });
5738
5739        cx.run_until_parked();
5740
5741        // Should still be editing
5742        cx.update(|window, cx| {
5743            assert!(user_message_editor.focus_handle(cx).is_focused(window));
5744            assert_eq!(thread_view.read(cx).editing_message, Some(0));
5745            assert_eq!(
5746                user_message_editor.read(cx).text(cx),
5747                "Edited message content"
5748            );
5749        });
5750    }
5751
5752    #[gpui::test]
5753    async fn test_interrupt(cx: &mut TestAppContext) {
5754        init_test(cx);
5755
5756        let connection = StubAgentConnection::new();
5757
5758        let (thread_view, cx) =
5759            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
5760        add_to_workspace(thread_view.clone(), cx);
5761
5762        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
5763        message_editor.update_in(cx, |editor, window, cx| {
5764            editor.set_text("Message 1", window, cx);
5765        });
5766        thread_view.update_in(cx, |thread_view, window, cx| {
5767            thread_view.send(window, cx);
5768        });
5769
5770        let (thread, session_id) = thread_view.read_with(cx, |view, cx| {
5771            let thread = view.thread().unwrap();
5772
5773            (thread.clone(), thread.read(cx).session_id().clone())
5774        });
5775
5776        cx.run_until_parked();
5777
5778        cx.update(|_, cx| {
5779            connection.send_update(
5780                session_id.clone(),
5781                acp::SessionUpdate::AgentMessageChunk {
5782                    content: "Message 1 resp".into(),
5783                },
5784                cx,
5785            );
5786        });
5787
5788        cx.run_until_parked();
5789
5790        thread.read_with(cx, |thread, cx| {
5791            assert_eq!(
5792                thread.to_markdown(cx),
5793                indoc::indoc! {"
5794                    ## User
5795
5796                    Message 1
5797
5798                    ## Assistant
5799
5800                    Message 1 resp
5801
5802                "}
5803            )
5804        });
5805
5806        message_editor.update_in(cx, |editor, window, cx| {
5807            editor.set_text("Message 2", window, cx);
5808        });
5809        thread_view.update_in(cx, |thread_view, window, cx| {
5810            thread_view.send(window, cx);
5811        });
5812
5813        cx.update(|_, cx| {
5814            // Simulate a response sent after beginning to cancel
5815            connection.send_update(
5816                session_id.clone(),
5817                acp::SessionUpdate::AgentMessageChunk {
5818                    content: "onse".into(),
5819                },
5820                cx,
5821            );
5822        });
5823
5824        cx.run_until_parked();
5825
5826        // Last Message 1 response should appear before Message 2
5827        thread.read_with(cx, |thread, cx| {
5828            assert_eq!(
5829                thread.to_markdown(cx),
5830                indoc::indoc! {"
5831                    ## User
5832
5833                    Message 1
5834
5835                    ## Assistant
5836
5837                    Message 1 response
5838
5839                    ## User
5840
5841                    Message 2
5842
5843                "}
5844            )
5845        });
5846
5847        cx.update(|_, cx| {
5848            connection.send_update(
5849                session_id.clone(),
5850                acp::SessionUpdate::AgentMessageChunk {
5851                    content: "Message 2 response".into(),
5852                },
5853                cx,
5854            );
5855            connection.end_turn(session_id.clone(), acp::StopReason::EndTurn);
5856        });
5857
5858        cx.run_until_parked();
5859
5860        thread.read_with(cx, |thread, cx| {
5861            assert_eq!(
5862                thread.to_markdown(cx),
5863                indoc::indoc! {"
5864                    ## User
5865
5866                    Message 1
5867
5868                    ## Assistant
5869
5870                    Message 1 response
5871
5872                    ## User
5873
5874                    Message 2
5875
5876                    ## Assistant
5877
5878                    Message 2 response
5879
5880                "}
5881            )
5882        });
5883    }
5884}