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