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