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