thread_view.rs

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