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