thread_view.rs

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