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