thread_view.rs

   1use agent_servers::AgentServer;
   2use std::cell::RefCell;
   3use std::collections::BTreeMap;
   4use std::path::Path;
   5use std::rc::Rc;
   6use std::sync::Arc;
   7use std::time::Duration;
   8
   9use agentic_coding_protocol::{self as acp};
  10use assistant_tool::ActionLog;
  11use buffer_diff::BufferDiff;
  12use collections::{HashMap, HashSet};
  13use editor::{
  14    AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode,
  15    EditorStyle, MinimapVisibility, MultiBuffer, PathKey,
  16};
  17use file_icons::FileIcons;
  18use futures::channel::oneshot;
  19use gpui::{
  20    Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
  21    FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement,
  22    Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
  23    Window, div, linear_color_stop, linear_gradient, list, percentage, point, prelude::*,
  24    pulsating_between,
  25};
  26use language::language_settings::SoftWrap;
  27use language::{Buffer, Language};
  28use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
  29use parking_lot::Mutex;
  30use project::Project;
  31use settings::Settings as _;
  32use text::Anchor;
  33use theme::ThemeSettings;
  34use ui::{Disclosure, Divider, DividerColor, KeyBinding, Tooltip, prelude::*};
  35use util::ResultExt;
  36use workspace::{CollaboratorId, Workspace};
  37use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
  38
  39use ::acp_thread::{
  40    AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, Diff,
  41    LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallConfirmation, ToolCallContent,
  42    ToolCallId, ToolCallStatus,
  43};
  44
  45use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
  46use crate::acp::message_history::MessageHistory;
  47use crate::agent_diff::AgentDiff;
  48use crate::{AgentDiffPane, Follow, KeepAll, OpenAgentDiff, RejectAll};
  49
  50const RESPONSE_PADDING_X: Pixels = px(19.);
  51
  52pub struct AcpThreadView {
  53    agent: Rc<dyn AgentServer>,
  54    workspace: WeakEntity<Workspace>,
  55    project: Entity<Project>,
  56    thread_state: ThreadState,
  57    diff_editors: HashMap<EntityId, Entity<Editor>>,
  58    message_editor: Entity<Editor>,
  59    message_set_from_history: bool,
  60    _message_editor_subscription: Subscription,
  61    mention_set: Arc<Mutex<MentionSet>>,
  62    last_error: Option<Entity<Markdown>>,
  63    list_state: ListState,
  64    auth_task: Option<Task<()>>,
  65    expanded_tool_calls: HashSet<ToolCallId>,
  66    expanded_thinking_blocks: HashSet<(usize, usize)>,
  67    edits_expanded: bool,
  68    message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
  69}
  70
  71enum ThreadState {
  72    Loading {
  73        _task: Task<()>,
  74    },
  75    Ready {
  76        thread: Entity<AcpThread>,
  77        _subscription: [Subscription; 2],
  78    },
  79    LoadError(LoadError),
  80    Unauthenticated {
  81        thread: Entity<AcpThread>,
  82    },
  83}
  84
  85struct AlwaysAllowOption {
  86    id: &'static str,
  87    label: SharedString,
  88    outcome: acp::ToolCallConfirmationOutcome,
  89}
  90
  91impl AcpThreadView {
  92    pub fn new(
  93        agent: Rc<dyn AgentServer>,
  94        workspace: WeakEntity<Workspace>,
  95        project: Entity<Project>,
  96        message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
  97        window: &mut Window,
  98        cx: &mut Context<Self>,
  99    ) -> Self {
 100        let language = Language::new(
 101            language::LanguageConfig {
 102                completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
 103                ..Default::default()
 104            },
 105            None,
 106        );
 107
 108        let mention_set = Arc::new(Mutex::new(MentionSet::default()));
 109
 110        let message_editor = cx.new(|cx| {
 111            let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
 112            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
 113
 114            let mut editor = Editor::new(
 115                editor::EditorMode::AutoHeight {
 116                    min_lines: 4,
 117                    max_lines: None,
 118                },
 119                buffer,
 120                None,
 121                window,
 122                cx,
 123            );
 124            editor.set_placeholder_text("Message the agent - @ to include files", cx);
 125            editor.set_show_indent_guides(false, cx);
 126            editor.set_soft_wrap();
 127            editor.set_use_modal_editing(true);
 128            editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
 129                mention_set.clone(),
 130                workspace.clone(),
 131                cx.weak_entity(),
 132            ))));
 133            editor.set_context_menu_options(ContextMenuOptions {
 134                min_entries_visible: 12,
 135                max_entries_visible: 12,
 136                placement: Some(ContextMenuPlacement::Above),
 137            });
 138            editor
 139        });
 140
 141        let message_editor_subscription = cx.subscribe(&message_editor, |this, _, event, _| {
 142            if let editor::EditorEvent::BufferEdited = &event {
 143                if !this.message_set_from_history {
 144                    this.message_history.borrow_mut().reset_position();
 145                }
 146                this.message_set_from_history = false;
 147            }
 148        });
 149
 150        let mention_set = mention_set.clone();
 151
 152        let list_state = ListState::new(
 153            0,
 154            gpui::ListAlignment::Bottom,
 155            px(2048.0),
 156            cx.processor({
 157                move |this: &mut Self, index: usize, window, cx| {
 158                    let Some((entry, len)) = this.thread().and_then(|thread| {
 159                        let entries = &thread.read(cx).entries();
 160                        Some((entries.get(index)?, entries.len()))
 161                    }) else {
 162                        return Empty.into_any();
 163                    };
 164                    this.render_entry(index, len, entry, window, cx)
 165                }
 166            }),
 167        );
 168
 169        Self {
 170            agent: agent.clone(),
 171            workspace: workspace.clone(),
 172            project: project.clone(),
 173            thread_state: Self::initial_state(agent, workspace, project, window, cx),
 174            message_editor,
 175            message_set_from_history: false,
 176            _message_editor_subscription: message_editor_subscription,
 177            mention_set,
 178            diff_editors: Default::default(),
 179            list_state: list_state,
 180            last_error: None,
 181            auth_task: None,
 182            expanded_tool_calls: HashSet::default(),
 183            expanded_thinking_blocks: HashSet::default(),
 184            edits_expanded: false,
 185            message_history,
 186        }
 187    }
 188
 189    fn initial_state(
 190        agent: Rc<dyn AgentServer>,
 191        workspace: WeakEntity<Workspace>,
 192        project: Entity<Project>,
 193        window: &mut Window,
 194        cx: &mut Context<Self>,
 195    ) -> ThreadState {
 196        let root_dir = project
 197            .read(cx)
 198            .visible_worktrees(cx)
 199            .next()
 200            .map(|worktree| worktree.read(cx).abs_path())
 201            .unwrap_or_else(|| paths::home_dir().as_path().into());
 202
 203        let task = agent.new_thread(&root_dir, &project, cx);
 204        let load_task = cx.spawn_in(window, async move |this, cx| {
 205            let thread = match task.await {
 206                Ok(thread) => thread,
 207                Err(err) => {
 208                    this.update(cx, |this, cx| {
 209                        this.handle_load_error(err, cx);
 210                        cx.notify();
 211                    })
 212                    .log_err();
 213                    return;
 214                }
 215            };
 216
 217            let init_response = async {
 218                let resp = thread
 219                    .update(cx, |thread, cx| thread.initialize(cx))?
 220                    .await?;
 221                anyhow::Ok(resp)
 222            };
 223
 224            let result = match init_response.await {
 225                Err(e) => {
 226                    let mut cx = cx.clone();
 227                    if e.downcast_ref::<oneshot::Canceled>().is_some() {
 228                        let child_status = thread
 229                            .update(&mut cx, |thread, _| thread.child_status())
 230                            .ok()
 231                            .flatten();
 232                        if let Some(child_status) = child_status {
 233                            match child_status.await {
 234                                Ok(_) => Err(e),
 235                                Err(e) => Err(e),
 236                            }
 237                        } else {
 238                            Err(e)
 239                        }
 240                    } else {
 241                        Err(e)
 242                    }
 243                }
 244                Ok(response) => {
 245                    if !response.is_authenticated {
 246                        this.update(cx, |this, _| {
 247                            this.thread_state = ThreadState::Unauthenticated { thread };
 248                        })
 249                        .ok();
 250                        return;
 251                    };
 252                    Ok(())
 253                }
 254            };
 255
 256            this.update_in(cx, |this, window, cx| {
 257                match result {
 258                    Ok(()) => {
 259                        let thread_subscription =
 260                            cx.subscribe_in(&thread, window, Self::handle_thread_event);
 261
 262                        let action_log = thread.read(cx).action_log().clone();
 263                        let action_log_subscription =
 264                            cx.observe(&action_log, |_, _, cx| cx.notify());
 265
 266                        this.list_state
 267                            .splice(0..0, thread.read(cx).entries().len());
 268
 269                        AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
 270
 271                        this.thread_state = ThreadState::Ready {
 272                            thread,
 273                            _subscription: [thread_subscription, action_log_subscription],
 274                        };
 275
 276                        cx.notify();
 277                    }
 278                    Err(err) => {
 279                        this.handle_load_error(err, cx);
 280                    }
 281                };
 282            })
 283            .log_err();
 284        });
 285
 286        ThreadState::Loading { _task: load_task }
 287    }
 288
 289    fn handle_load_error(&mut self, err: anyhow::Error, cx: &mut Context<Self>) {
 290        if let Some(load_err) = err.downcast_ref::<LoadError>() {
 291            self.thread_state = ThreadState::LoadError(load_err.clone());
 292        } else {
 293            self.thread_state = ThreadState::LoadError(LoadError::Other(err.to_string().into()))
 294        }
 295        cx.notify();
 296    }
 297
 298    pub fn thread(&self) -> Option<&Entity<AcpThread>> {
 299        match &self.thread_state {
 300            ThreadState::Ready { thread, .. } | ThreadState::Unauthenticated { thread } => {
 301                Some(thread)
 302            }
 303            ThreadState::Loading { .. } | ThreadState::LoadError(..) => None,
 304        }
 305    }
 306
 307    pub fn title(&self, cx: &App) -> SharedString {
 308        match &self.thread_state {
 309            ThreadState::Ready { thread, .. } => thread.read(cx).title(),
 310            ThreadState::Loading { .. } => "Loading…".into(),
 311            ThreadState::LoadError(_) => "Failed to load".into(),
 312            ThreadState::Unauthenticated { .. } => "Not authenticated".into(),
 313        }
 314    }
 315
 316    pub fn cancel(&mut self, cx: &mut Context<Self>) {
 317        self.last_error.take();
 318
 319        if let Some(thread) = self.thread() {
 320            thread.update(cx, |thread, cx| thread.cancel(cx)).detach();
 321        }
 322    }
 323
 324    fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
 325        self.last_error.take();
 326
 327        let mut ix = 0;
 328        let mut chunks: Vec<acp::UserMessageChunk> = Vec::new();
 329        let project = self.project.clone();
 330        self.message_editor.update(cx, |editor, cx| {
 331            let text = editor.text(cx);
 332            editor.display_map.update(cx, |map, cx| {
 333                let snapshot = map.snapshot(cx);
 334                for (crease_id, crease) in snapshot.crease_snapshot.creases() {
 335                    if let Some(project_path) =
 336                        self.mention_set.lock().path_for_crease_id(crease_id)
 337                    {
 338                        let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
 339                        if crease_range.start > ix {
 340                            chunks.push(acp::UserMessageChunk::Text {
 341                                text: text[ix..crease_range.start].to_string(),
 342                            });
 343                        }
 344                        if let Some(abs_path) = project.read(cx).absolute_path(&project_path, cx) {
 345                            chunks.push(acp::UserMessageChunk::Path { path: abs_path });
 346                        }
 347                        ix = crease_range.end;
 348                    }
 349                }
 350
 351                if ix < text.len() {
 352                    let last_chunk = text[ix..].trim();
 353                    if !last_chunk.is_empty() {
 354                        chunks.push(acp::UserMessageChunk::Text {
 355                            text: last_chunk.into(),
 356                        });
 357                    }
 358                }
 359            })
 360        });
 361
 362        if chunks.is_empty() {
 363            return;
 364        }
 365
 366        let Some(thread) = self.thread() else { return };
 367        let message = acp::SendUserMessageParams { chunks };
 368        let task = thread.update(cx, |thread, cx| thread.send(message.clone(), cx));
 369
 370        cx.spawn(async move |this, cx| {
 371            let result = task.await;
 372
 373            this.update(cx, |this, cx| {
 374                if let Err(err) = result {
 375                    this.last_error =
 376                        Some(cx.new(|cx| Markdown::new(err.to_string().into(), None, None, cx)))
 377                }
 378            })
 379        })
 380        .detach();
 381
 382        let mention_set = self.mention_set.clone();
 383
 384        self.message_editor.update(cx, |editor, cx| {
 385            editor.clear(window, cx);
 386            editor.remove_creases(mention_set.lock().drain(), cx)
 387        });
 388
 389        self.message_history.borrow_mut().push(message);
 390    }
 391
 392    fn previous_history_message(
 393        &mut self,
 394        _: &PreviousHistoryMessage,
 395        window: &mut Window,
 396        cx: &mut Context<Self>,
 397    ) {
 398        self.message_set_from_history = Self::set_draft_message(
 399            self.message_editor.clone(),
 400            self.mention_set.clone(),
 401            self.project.clone(),
 402            self.message_history.borrow_mut().prev(),
 403            window,
 404            cx,
 405        );
 406    }
 407
 408    fn next_history_message(
 409        &mut self,
 410        _: &NextHistoryMessage,
 411        window: &mut Window,
 412        cx: &mut Context<Self>,
 413    ) {
 414        self.message_set_from_history = Self::set_draft_message(
 415            self.message_editor.clone(),
 416            self.mention_set.clone(),
 417            self.project.clone(),
 418            self.message_history.borrow_mut().next(),
 419            window,
 420            cx,
 421        );
 422    }
 423
 424    fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) {
 425        if let Some(thread) = self.thread() {
 426            AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err();
 427        }
 428    }
 429
 430    fn open_edited_buffer(
 431        &mut self,
 432        buffer: &Entity<Buffer>,
 433        window: &mut Window,
 434        cx: &mut Context<Self>,
 435    ) {
 436        let Some(thread) = self.thread() else {
 437            return;
 438        };
 439
 440        let Some(diff) =
 441            AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err()
 442        else {
 443            return;
 444        };
 445
 446        diff.update(cx, |diff, cx| {
 447            diff.move_to_path(PathKey::for_buffer(&buffer, cx), window, cx)
 448        })
 449    }
 450
 451    fn set_draft_message(
 452        message_editor: Entity<Editor>,
 453        mention_set: Arc<Mutex<MentionSet>>,
 454        project: Entity<Project>,
 455        message: Option<&acp::SendUserMessageParams>,
 456        window: &mut Window,
 457        cx: &mut Context<Self>,
 458    ) -> bool {
 459        cx.notify();
 460
 461        let Some(message) = message else {
 462            return false;
 463        };
 464
 465        let mut text = String::new();
 466        let mut mentions = Vec::new();
 467
 468        for chunk in &message.chunks {
 469            match chunk {
 470                acp::UserMessageChunk::Text { text: chunk } => {
 471                    text.push_str(&chunk);
 472                }
 473                acp::UserMessageChunk::Path { path } => {
 474                    let start = text.len();
 475                    let content = MentionPath::new(path).to_string();
 476                    text.push_str(&content);
 477                    let end = text.len();
 478                    if let Some(project_path) =
 479                        project.read(cx).project_path_for_absolute_path(path, cx)
 480                    {
 481                        let filename: SharedString = path
 482                            .file_name()
 483                            .unwrap_or_default()
 484                            .to_string_lossy()
 485                            .to_string()
 486                            .into();
 487                        mentions.push((start..end, project_path, filename));
 488                    }
 489                }
 490            }
 491        }
 492
 493        let snapshot = message_editor.update(cx, |editor, cx| {
 494            editor.set_text(text, window, cx);
 495            editor.buffer().read(cx).snapshot(cx)
 496        });
 497
 498        for (range, project_path, filename) in mentions {
 499            let crease_icon_path = if project_path.path.is_dir() {
 500                FileIcons::get_folder_icon(false, cx)
 501                    .unwrap_or_else(|| IconName::Folder.path().into())
 502            } else {
 503                FileIcons::get_icon(Path::new(project_path.path.as_ref()), cx)
 504                    .unwrap_or_else(|| IconName::File.path().into())
 505            };
 506
 507            let anchor = snapshot.anchor_before(range.start);
 508            let crease_id = crate::context_picker::insert_crease_for_mention(
 509                anchor.excerpt_id,
 510                anchor.text_anchor,
 511                range.end - range.start,
 512                filename,
 513                crease_icon_path,
 514                message_editor.clone(),
 515                window,
 516                cx,
 517            );
 518            if let Some(crease_id) = crease_id {
 519                mention_set.lock().insert(crease_id, project_path);
 520            }
 521        }
 522
 523        true
 524    }
 525
 526    fn handle_thread_event(
 527        &mut self,
 528        thread: &Entity<AcpThread>,
 529        event: &AcpThreadEvent,
 530        window: &mut Window,
 531        cx: &mut Context<Self>,
 532    ) {
 533        let count = self.list_state.item_count();
 534        match event {
 535            AcpThreadEvent::NewEntry => {
 536                let index = thread.read(cx).entries().len() - 1;
 537                self.sync_thread_entry_view(index, window, cx);
 538                self.list_state.splice(count..count, 1);
 539            }
 540            AcpThreadEvent::EntryUpdated(index) => {
 541                let index = *index;
 542                self.sync_thread_entry_view(index, window, cx);
 543                self.list_state.splice(index..index + 1, 1);
 544            }
 545        }
 546        cx.notify();
 547    }
 548
 549    fn sync_thread_entry_view(
 550        &mut self,
 551        entry_ix: usize,
 552        window: &mut Window,
 553        cx: &mut Context<Self>,
 554    ) {
 555        let Some(multibuffer) = self.entry_diff_multibuffer(entry_ix, cx) else {
 556            return;
 557        };
 558
 559        if self.diff_editors.contains_key(&multibuffer.entity_id()) {
 560            return;
 561        }
 562
 563        let editor = cx.new(|cx| {
 564            let mut editor = Editor::new(
 565                EditorMode::Full {
 566                    scale_ui_elements_with_buffer_font_size: false,
 567                    show_active_line_background: false,
 568                    sized_by_content: true,
 569                },
 570                multibuffer.clone(),
 571                None,
 572                window,
 573                cx,
 574            );
 575            editor.set_show_gutter(false, cx);
 576            editor.disable_inline_diagnostics();
 577            editor.disable_expand_excerpt_buttons(cx);
 578            editor.set_show_vertical_scrollbar(false, cx);
 579            editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
 580            editor.set_soft_wrap_mode(SoftWrap::None, cx);
 581            editor.scroll_manager.set_forbid_vertical_scroll(true);
 582            editor.set_show_indent_guides(false, cx);
 583            editor.set_read_only(true);
 584            editor.set_show_breakpoints(false, cx);
 585            editor.set_show_code_actions(false, cx);
 586            editor.set_show_git_diff_gutter(false, cx);
 587            editor.set_expand_all_diff_hunks(cx);
 588            editor.set_text_style_refinement(TextStyleRefinement {
 589                font_size: Some(
 590                    TextSize::Small
 591                        .rems(cx)
 592                        .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
 593                        .into(),
 594                ),
 595                ..Default::default()
 596            });
 597            editor
 598        });
 599        let entity_id = multibuffer.entity_id();
 600        cx.observe_release(&multibuffer, move |this, _, _| {
 601            this.diff_editors.remove(&entity_id);
 602        })
 603        .detach();
 604
 605        self.diff_editors.insert(entity_id, editor);
 606    }
 607
 608    fn entry_diff_multibuffer(&self, entry_ix: usize, cx: &App) -> Option<Entity<MultiBuffer>> {
 609        let entry = self.thread()?.read(cx).entries().get(entry_ix)?;
 610        entry.diff().map(|diff| diff.multibuffer.clone())
 611    }
 612
 613    fn authenticate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 614        let Some(thread) = self.thread().cloned() else {
 615            return;
 616        };
 617
 618        self.last_error.take();
 619        let authenticate = thread.read(cx).authenticate();
 620        self.auth_task = Some(cx.spawn_in(window, {
 621            let project = self.project.clone();
 622            let agent = self.agent.clone();
 623            async move |this, cx| {
 624                let result = authenticate.await;
 625
 626                this.update_in(cx, |this, window, cx| {
 627                    if let Err(err) = result {
 628                        this.last_error = Some(cx.new(|cx| {
 629                            Markdown::new(format!("Error: {err}").into(), None, None, cx)
 630                        }))
 631                    } else {
 632                        this.thread_state = Self::initial_state(
 633                            agent,
 634                            this.workspace.clone(),
 635                            project.clone(),
 636                            window,
 637                            cx,
 638                        )
 639                    }
 640                    this.auth_task.take()
 641                })
 642                .ok();
 643            }
 644        }));
 645    }
 646
 647    fn authorize_tool_call(
 648        &mut self,
 649        id: ToolCallId,
 650        outcome: acp::ToolCallConfirmationOutcome,
 651        cx: &mut Context<Self>,
 652    ) {
 653        let Some(thread) = self.thread() else {
 654            return;
 655        };
 656        thread.update(cx, |thread, cx| {
 657            thread.authorize_tool_call(id, outcome, cx);
 658        });
 659        cx.notify();
 660    }
 661
 662    fn render_entry(
 663        &self,
 664        index: usize,
 665        total_entries: usize,
 666        entry: &AgentThreadEntry,
 667        window: &mut Window,
 668        cx: &Context<Self>,
 669    ) -> AnyElement {
 670        match &entry {
 671            AgentThreadEntry::UserMessage(message) => div()
 672                .py_4()
 673                .px_2()
 674                .child(
 675                    v_flex()
 676                        .p_3()
 677                        .gap_1p5()
 678                        .rounded_lg()
 679                        .shadow_md()
 680                        .bg(cx.theme().colors().editor_background)
 681                        .border_1()
 682                        .border_color(cx.theme().colors().border)
 683                        .text_xs()
 684                        .child(self.render_markdown(
 685                            message.content.clone(),
 686                            user_message_markdown_style(window, cx),
 687                        )),
 688                )
 689                .into_any(),
 690            AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
 691                let style = default_markdown_style(false, window, cx);
 692                let message_body = v_flex()
 693                    .w_full()
 694                    .gap_2p5()
 695                    .children(chunks.iter().enumerate().map(|(chunk_ix, chunk)| {
 696                        match chunk {
 697                            AssistantMessageChunk::Text { chunk } => self
 698                                .render_markdown(chunk.clone(), style.clone())
 699                                .into_any_element(),
 700                            AssistantMessageChunk::Thought { chunk } => self.render_thinking_block(
 701                                index,
 702                                chunk_ix,
 703                                chunk.clone(),
 704                                window,
 705                                cx,
 706                            ),
 707                        }
 708                    }))
 709                    .into_any();
 710
 711                v_flex()
 712                    .px_5()
 713                    .py_1()
 714                    .when(index + 1 == total_entries, |this| this.pb_4())
 715                    .w_full()
 716                    .text_ui(cx)
 717                    .child(message_body)
 718                    .into_any()
 719            }
 720            AgentThreadEntry::ToolCall(tool_call) => div()
 721                .py_1p5()
 722                .px_5()
 723                .child(self.render_tool_call(index, tool_call, window, cx))
 724                .into_any(),
 725        }
 726    }
 727
 728    fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
 729        cx.theme()
 730            .colors()
 731            .element_background
 732            .blend(cx.theme().colors().editor_foreground.opacity(0.025))
 733    }
 734
 735    fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
 736        cx.theme().colors().border.opacity(0.6)
 737    }
 738
 739    fn tool_name_font_size(&self) -> Rems {
 740        rems_from_px(13.)
 741    }
 742
 743    fn render_thinking_block(
 744        &self,
 745        entry_ix: usize,
 746        chunk_ix: usize,
 747        chunk: Entity<Markdown>,
 748        window: &Window,
 749        cx: &Context<Self>,
 750    ) -> AnyElement {
 751        let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
 752        let key = (entry_ix, chunk_ix);
 753        let is_open = self.expanded_thinking_blocks.contains(&key);
 754
 755        v_flex()
 756            .child(
 757                h_flex()
 758                    .id(header_id)
 759                    .group("disclosure-header")
 760                    .w_full()
 761                    .justify_between()
 762                    .opacity(0.8)
 763                    .hover(|style| style.opacity(1.))
 764                    .child(
 765                        h_flex()
 766                            .gap_1p5()
 767                            .child(
 768                                Icon::new(IconName::ToolBulb)
 769                                    .size(IconSize::Small)
 770                                    .color(Color::Muted),
 771                            )
 772                            .child(
 773                                div()
 774                                    .text_size(self.tool_name_font_size())
 775                                    .child("Thinking"),
 776                            ),
 777                    )
 778                    .child(
 779                        div().visible_on_hover("disclosure-header").child(
 780                            Disclosure::new("thinking-disclosure", is_open)
 781                                .opened_icon(IconName::ChevronUp)
 782                                .closed_icon(IconName::ChevronDown)
 783                                .on_click(cx.listener({
 784                                    move |this, _event, _window, cx| {
 785                                        if is_open {
 786                                            this.expanded_thinking_blocks.remove(&key);
 787                                        } else {
 788                                            this.expanded_thinking_blocks.insert(key);
 789                                        }
 790                                        cx.notify();
 791                                    }
 792                                })),
 793                        ),
 794                    )
 795                    .on_click(cx.listener({
 796                        move |this, _event, _window, cx| {
 797                            if is_open {
 798                                this.expanded_thinking_blocks.remove(&key);
 799                            } else {
 800                                this.expanded_thinking_blocks.insert(key);
 801                            }
 802                            cx.notify();
 803                        }
 804                    })),
 805            )
 806            .when(is_open, |this| {
 807                this.child(
 808                    div()
 809                        .relative()
 810                        .mt_1p5()
 811                        .ml(px(7.))
 812                        .pl_4()
 813                        .border_l_1()
 814                        .border_color(self.tool_card_border_color(cx))
 815                        .text_ui_sm(cx)
 816                        .child(
 817                            self.render_markdown(chunk, default_markdown_style(false, window, cx)),
 818                        ),
 819                )
 820            })
 821            .into_any_element()
 822    }
 823
 824    fn render_tool_call(
 825        &self,
 826        entry_ix: usize,
 827        tool_call: &ToolCall,
 828        window: &Window,
 829        cx: &Context<Self>,
 830    ) -> Div {
 831        let header_id = SharedString::from(format!("tool-call-header-{}", entry_ix));
 832
 833        let status_icon = match &tool_call.status {
 834            ToolCallStatus::WaitingForConfirmation { .. } => None,
 835            ToolCallStatus::Allowed {
 836                status: acp::ToolCallStatus::Running,
 837                ..
 838            } => Some(
 839                Icon::new(IconName::ArrowCircle)
 840                    .color(Color::Accent)
 841                    .size(IconSize::Small)
 842                    .with_animation(
 843                        "running",
 844                        Animation::new(Duration::from_secs(2)).repeat(),
 845                        |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
 846                    )
 847                    .into_any(),
 848            ),
 849            ToolCallStatus::Allowed {
 850                status: acp::ToolCallStatus::Finished,
 851                ..
 852            } => None,
 853            ToolCallStatus::Rejected
 854            | ToolCallStatus::Canceled
 855            | ToolCallStatus::Allowed {
 856                status: acp::ToolCallStatus::Error,
 857                ..
 858            } => Some(
 859                Icon::new(IconName::X)
 860                    .color(Color::Error)
 861                    .size(IconSize::Small)
 862                    .into_any_element(),
 863            ),
 864        };
 865
 866        let needs_confirmation = match &tool_call.status {
 867            ToolCallStatus::WaitingForConfirmation { .. } => true,
 868            _ => tool_call
 869                .content
 870                .iter()
 871                .any(|content| matches!(content, ToolCallContent::Diff { .. })),
 872        };
 873
 874        let is_collapsible = tool_call.content.is_some() && !needs_confirmation;
 875        let is_open = !is_collapsible || self.expanded_tool_calls.contains(&tool_call.id);
 876
 877        let content = if is_open {
 878            match &tool_call.status {
 879                ToolCallStatus::WaitingForConfirmation { confirmation, .. } => {
 880                    Some(self.render_tool_call_confirmation(
 881                        tool_call.id,
 882                        confirmation,
 883                        tool_call.content.as_ref(),
 884                        window,
 885                        cx,
 886                    ))
 887                }
 888                ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => {
 889                    tool_call.content.as_ref().map(|content| {
 890                        div()
 891                            .py_1p5()
 892                            .child(self.render_tool_call_content(content, window, cx))
 893                            .into_any_element()
 894                    })
 895                }
 896                ToolCallStatus::Rejected => None,
 897            }
 898        } else {
 899            None
 900        };
 901
 902        v_flex()
 903            .when(needs_confirmation, |this| {
 904                this.rounded_lg()
 905                    .border_1()
 906                    .border_color(self.tool_card_border_color(cx))
 907                    .bg(cx.theme().colors().editor_background)
 908                    .overflow_hidden()
 909            })
 910            .child(
 911                h_flex()
 912                    .id(header_id)
 913                    .w_full()
 914                    .gap_1()
 915                    .justify_between()
 916                    .map(|this| {
 917                        if needs_confirmation {
 918                            this.px_2()
 919                                .py_1()
 920                                .rounded_t_md()
 921                                .bg(self.tool_card_header_bg(cx))
 922                                .border_b_1()
 923                                .border_color(self.tool_card_border_color(cx))
 924                        } else {
 925                            this.opacity(0.8).hover(|style| style.opacity(1.))
 926                        }
 927                    })
 928                    .child(
 929                        h_flex()
 930                            .id("tool-call-header")
 931                            .overflow_x_scroll()
 932                            .map(|this| {
 933                                if needs_confirmation {
 934                                    this.text_xs()
 935                                } else {
 936                                    this.text_size(self.tool_name_font_size())
 937                                }
 938                            })
 939                            .gap_1p5()
 940                            .child(
 941                                Icon::new(tool_call.icon)
 942                                    .size(IconSize::Small)
 943                                    .color(Color::Muted),
 944                            )
 945                            .child(if tool_call.locations.len() == 1 {
 946                                let name = tool_call.locations[0]
 947                                    .path
 948                                    .file_name()
 949                                    .unwrap_or_default()
 950                                    .display()
 951                                    .to_string();
 952
 953                                h_flex()
 954                                    .id(("open-tool-call-location", entry_ix))
 955                                    .child(name)
 956                                    .w_full()
 957                                    .max_w_full()
 958                                    .pr_1()
 959                                    .gap_0p5()
 960                                    .cursor_pointer()
 961                                    .rounded_sm()
 962                                    .opacity(0.8)
 963                                    .hover(|label| {
 964                                        label.opacity(1.).bg(cx
 965                                            .theme()
 966                                            .colors()
 967                                            .element_hover
 968                                            .opacity(0.5))
 969                                    })
 970                                    .tooltip(Tooltip::text("Jump to File"))
 971                                    .on_click(cx.listener(move |this, _, window, cx| {
 972                                        this.open_tool_call_location(entry_ix, 0, window, cx);
 973                                    }))
 974                                    .into_any_element()
 975                            } else {
 976                                self.render_markdown(
 977                                    tool_call.label.clone(),
 978                                    default_markdown_style(needs_confirmation, window, cx),
 979                                )
 980                                .into_any()
 981                            }),
 982                    )
 983                    .child(
 984                        h_flex()
 985                            .gap_0p5()
 986                            .when(is_collapsible, |this| {
 987                                this.child(
 988                                    Disclosure::new(("expand", tool_call.id.0), is_open)
 989                                        .opened_icon(IconName::ChevronUp)
 990                                        .closed_icon(IconName::ChevronDown)
 991                                        .on_click(cx.listener({
 992                                            let id = tool_call.id;
 993                                            move |this: &mut Self, _, _, cx: &mut Context<Self>| {
 994                                                if is_open {
 995                                                    this.expanded_tool_calls.remove(&id);
 996                                                } else {
 997                                                    this.expanded_tool_calls.insert(id);
 998                                                }
 999                                                cx.notify();
1000                                            }
1001                                        })),
1002                                )
1003                            })
1004                            .children(status_icon),
1005                    )
1006                    .on_click(cx.listener({
1007                        let id = tool_call.id;
1008                        move |this: &mut Self, _, _, cx: &mut Context<Self>| {
1009                            if is_open {
1010                                this.expanded_tool_calls.remove(&id);
1011                            } else {
1012                                this.expanded_tool_calls.insert(id);
1013                            }
1014                            cx.notify();
1015                        }
1016                    })),
1017            )
1018            .when(is_open, |this| {
1019                this.child(
1020                    div()
1021                        .text_xs()
1022                        .when(is_collapsible, |this| {
1023                            this.mt_1()
1024                                .border_1()
1025                                .border_color(self.tool_card_border_color(cx))
1026                                .bg(cx.theme().colors().editor_background)
1027                                .rounded_lg()
1028                        })
1029                        .children(content),
1030                )
1031            })
1032    }
1033
1034    fn render_tool_call_content(
1035        &self,
1036        content: &ToolCallContent,
1037        window: &Window,
1038        cx: &Context<Self>,
1039    ) -> AnyElement {
1040        match content {
1041            ToolCallContent::Markdown { markdown } => {
1042                div()
1043                    .p_2()
1044                    .child(self.render_markdown(
1045                        markdown.clone(),
1046                        default_markdown_style(false, window, cx),
1047                    ))
1048                    .into_any_element()
1049            }
1050            ToolCallContent::Diff {
1051                diff: Diff { multibuffer, .. },
1052                ..
1053            } => self.render_diff_editor(multibuffer),
1054        }
1055    }
1056
1057    fn render_tool_call_confirmation(
1058        &self,
1059        tool_call_id: ToolCallId,
1060        confirmation: &ToolCallConfirmation,
1061        content: Option<&ToolCallContent>,
1062        window: &Window,
1063        cx: &Context<Self>,
1064    ) -> AnyElement {
1065        let confirmation_container = v_flex().mt_1().py_1p5();
1066
1067        match confirmation {
1068            ToolCallConfirmation::Edit { description } => confirmation_container
1069                .child(
1070                    div()
1071                        .px_2()
1072                        .children(description.clone().map(|description| {
1073                            self.render_markdown(
1074                                description,
1075                                default_markdown_style(false, window, cx),
1076                            )
1077                        })),
1078                )
1079                .children(content.map(|content| self.render_tool_call_content(content, window, cx)))
1080                .child(self.render_confirmation_buttons(
1081                    &[AlwaysAllowOption {
1082                        id: "always_allow",
1083                        label: "Always Allow Edits".into(),
1084                        outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow,
1085                    }],
1086                    tool_call_id,
1087                    cx,
1088                ))
1089                .into_any(),
1090            ToolCallConfirmation::Execute {
1091                command,
1092                root_command,
1093                description,
1094            } => confirmation_container
1095                .child(v_flex().px_2().pb_1p5().child(command.clone()).children(
1096                    description.clone().map(|description| {
1097                        self.render_markdown(description, default_markdown_style(false, window, cx))
1098                            .on_url_click({
1099                                let workspace = self.workspace.clone();
1100                                move |text, window, cx| {
1101                                    Self::open_link(text, &workspace, window, cx);
1102                                }
1103                            })
1104                    }),
1105                ))
1106                .children(content.map(|content| self.render_tool_call_content(content, window, cx)))
1107                .child(self.render_confirmation_buttons(
1108                    &[AlwaysAllowOption {
1109                        id: "always_allow",
1110                        label: format!("Always Allow {root_command}").into(),
1111                        outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow,
1112                    }],
1113                    tool_call_id,
1114                    cx,
1115                ))
1116                .into_any(),
1117            ToolCallConfirmation::Mcp {
1118                server_name,
1119                tool_name: _,
1120                tool_display_name,
1121                description,
1122            } => confirmation_container
1123                .child(
1124                    v_flex()
1125                        .px_2()
1126                        .pb_1p5()
1127                        .child(format!("{server_name} - {tool_display_name}"))
1128                        .children(description.clone().map(|description| {
1129                            self.render_markdown(
1130                                description,
1131                                default_markdown_style(false, window, cx),
1132                            )
1133                        })),
1134                )
1135                .children(content.map(|content| self.render_tool_call_content(content, window, cx)))
1136                .child(self.render_confirmation_buttons(
1137                    &[
1138                        AlwaysAllowOption {
1139                            id: "always_allow_server",
1140                            label: format!("Always Allow {server_name}").into(),
1141                            outcome: acp::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
1142                        },
1143                        AlwaysAllowOption {
1144                            id: "always_allow_tool",
1145                            label: format!("Always Allow {tool_display_name}").into(),
1146                            outcome: acp::ToolCallConfirmationOutcome::AlwaysAllowTool,
1147                        },
1148                    ],
1149                    tool_call_id,
1150                    cx,
1151                ))
1152                .into_any(),
1153            ToolCallConfirmation::Fetch { description, urls } => confirmation_container
1154                .child(
1155                    v_flex()
1156                        .px_2()
1157                        .pb_1p5()
1158                        .gap_1()
1159                        .children(urls.iter().map(|url| {
1160                            h_flex().child(
1161                                Button::new(url.clone(), url)
1162                                    .icon(IconName::ArrowUpRight)
1163                                    .icon_color(Color::Muted)
1164                                    .icon_size(IconSize::XSmall)
1165                                    .on_click({
1166                                        let url = url.clone();
1167                                        move |_, _, cx| cx.open_url(&url)
1168                                    }),
1169                            )
1170                        }))
1171                        .children(description.clone().map(|description| {
1172                            self.render_markdown(
1173                                description,
1174                                default_markdown_style(false, window, cx),
1175                            )
1176                        })),
1177                )
1178                .children(content.map(|content| self.render_tool_call_content(content, window, cx)))
1179                .child(self.render_confirmation_buttons(
1180                    &[AlwaysAllowOption {
1181                        id: "always_allow",
1182                        label: "Always Allow".into(),
1183                        outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow,
1184                    }],
1185                    tool_call_id,
1186                    cx,
1187                ))
1188                .into_any(),
1189            ToolCallConfirmation::Other { description } => confirmation_container
1190                .child(v_flex().px_2().pb_1p5().child(self.render_markdown(
1191                    description.clone(),
1192                    default_markdown_style(false, window, cx),
1193                )))
1194                .children(content.map(|content| self.render_tool_call_content(content, window, cx)))
1195                .child(self.render_confirmation_buttons(
1196                    &[AlwaysAllowOption {
1197                        id: "always_allow",
1198                        label: "Always Allow".into(),
1199                        outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow,
1200                    }],
1201                    tool_call_id,
1202                    cx,
1203                ))
1204                .into_any(),
1205        }
1206    }
1207
1208    fn render_confirmation_buttons(
1209        &self,
1210        always_allow_options: &[AlwaysAllowOption],
1211        tool_call_id: ToolCallId,
1212        cx: &Context<Self>,
1213    ) -> Div {
1214        h_flex()
1215            .pt_1p5()
1216            .px_1p5()
1217            .gap_1()
1218            .justify_end()
1219            .border_t_1()
1220            .border_color(self.tool_card_border_color(cx))
1221            .when(self.agent.supports_always_allow(), |this| {
1222                this.children(always_allow_options.into_iter().map(|always_allow_option| {
1223                    let outcome = always_allow_option.outcome;
1224                    Button::new(
1225                        (always_allow_option.id, tool_call_id.0),
1226                        always_allow_option.label.clone(),
1227                    )
1228                    .icon(IconName::CheckDouble)
1229                    .icon_position(IconPosition::Start)
1230                    .icon_size(IconSize::XSmall)
1231                    .icon_color(Color::Success)
1232                    .on_click(cx.listener({
1233                        let id = tool_call_id;
1234                        move |this, _, _, cx| {
1235                            this.authorize_tool_call(id, outcome, cx);
1236                        }
1237                    }))
1238                }))
1239            })
1240            .child(
1241                Button::new(("allow", tool_call_id.0), "Allow")
1242                    .icon(IconName::Check)
1243                    .icon_position(IconPosition::Start)
1244                    .icon_size(IconSize::XSmall)
1245                    .icon_color(Color::Success)
1246                    .on_click(cx.listener({
1247                        let id = tool_call_id;
1248                        move |this, _, _, cx| {
1249                            this.authorize_tool_call(
1250                                id,
1251                                acp::ToolCallConfirmationOutcome::Allow,
1252                                cx,
1253                            );
1254                        }
1255                    })),
1256            )
1257            .child(
1258                Button::new(("reject", tool_call_id.0), "Reject")
1259                    .icon(IconName::X)
1260                    .icon_position(IconPosition::Start)
1261                    .icon_size(IconSize::XSmall)
1262                    .icon_color(Color::Error)
1263                    .on_click(cx.listener({
1264                        let id = tool_call_id;
1265                        move |this, _, _, cx| {
1266                            this.authorize_tool_call(
1267                                id,
1268                                acp::ToolCallConfirmationOutcome::Reject,
1269                                cx,
1270                            );
1271                        }
1272                    })),
1273            )
1274    }
1275
1276    fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>) -> AnyElement {
1277        v_flex()
1278            .h_full()
1279            .child(
1280                if let Some(editor) = self.diff_editors.get(&multibuffer.entity_id()) {
1281                    editor.clone().into_any_element()
1282                } else {
1283                    Empty.into_any()
1284                },
1285            )
1286            .into_any()
1287    }
1288
1289    fn render_agent_logo(&self) -> AnyElement {
1290        Icon::new(self.agent.logo())
1291            .color(Color::Muted)
1292            .size(IconSize::XLarge)
1293            .into_any_element()
1294    }
1295
1296    fn render_error_agent_logo(&self) -> AnyElement {
1297        let logo = Icon::new(self.agent.logo())
1298            .color(Color::Muted)
1299            .size(IconSize::XLarge)
1300            .into_any_element();
1301
1302        h_flex()
1303            .relative()
1304            .justify_center()
1305            .child(div().opacity(0.3).child(logo))
1306            .child(
1307                h_flex().absolute().right_1().bottom_0().child(
1308                    Icon::new(IconName::XCircle)
1309                        .color(Color::Error)
1310                        .size(IconSize::Small),
1311                ),
1312            )
1313            .into_any_element()
1314    }
1315
1316    fn render_empty_state(&self, cx: &App) -> AnyElement {
1317        let loading = matches!(&self.thread_state, ThreadState::Loading { .. });
1318
1319        v_flex()
1320            .size_full()
1321            .items_center()
1322            .justify_center()
1323            .child(if loading {
1324                h_flex()
1325                    .justify_center()
1326                    .child(self.render_agent_logo())
1327                    .with_animation(
1328                        "pulsating_icon",
1329                        Animation::new(Duration::from_secs(2))
1330                            .repeat()
1331                            .with_easing(pulsating_between(0.4, 1.0)),
1332                        |icon, delta| icon.opacity(delta),
1333                    )
1334                    .into_any()
1335            } else {
1336                self.render_agent_logo().into_any_element()
1337            })
1338            .child(h_flex().mt_4().mb_1().justify_center().child(if loading {
1339                div()
1340                    .child(LoadingLabel::new("").size(LabelSize::Large))
1341                    .into_any_element()
1342            } else {
1343                Headline::new(self.agent.empty_state_headline())
1344                    .size(HeadlineSize::Medium)
1345                    .into_any_element()
1346            }))
1347            .child(
1348                div()
1349                    .max_w_1_2()
1350                    .text_sm()
1351                    .text_center()
1352                    .map(|this| {
1353                        if loading {
1354                            this.invisible()
1355                        } else {
1356                            this.text_color(cx.theme().colors().text_muted)
1357                        }
1358                    })
1359                    .child(self.agent.empty_state_message()),
1360            )
1361            .into_any()
1362    }
1363
1364    fn render_pending_auth_state(&self) -> AnyElement {
1365        v_flex()
1366            .items_center()
1367            .justify_center()
1368            .child(self.render_error_agent_logo())
1369            .child(
1370                h_flex()
1371                    .mt_4()
1372                    .mb_1()
1373                    .justify_center()
1374                    .child(Headline::new("Not Authenticated").size(HeadlineSize::Medium)),
1375            )
1376            .into_any()
1377    }
1378
1379    fn render_error_state(&self, e: &LoadError, cx: &Context<Self>) -> AnyElement {
1380        let mut container = v_flex()
1381            .items_center()
1382            .justify_center()
1383            .child(self.render_error_agent_logo())
1384            .child(
1385                v_flex()
1386                    .mt_4()
1387                    .mb_2()
1388                    .gap_0p5()
1389                    .text_center()
1390                    .items_center()
1391                    .child(Headline::new("Failed to launch").size(HeadlineSize::Medium))
1392                    .child(
1393                        Label::new(e.to_string())
1394                            .size(LabelSize::Small)
1395                            .color(Color::Muted),
1396                    ),
1397            );
1398
1399        if let LoadError::Unsupported {
1400            upgrade_message,
1401            upgrade_command,
1402            ..
1403        } = &e
1404        {
1405            let upgrade_message = upgrade_message.clone();
1406            let upgrade_command = upgrade_command.clone();
1407            container = container.child(Button::new("upgrade", upgrade_message).on_click(
1408                cx.listener(move |this, _, window, cx| {
1409                    this.workspace
1410                        .update(cx, |workspace, cx| {
1411                            let project = workspace.project().read(cx);
1412                            let cwd = project.first_project_directory(cx);
1413                            let shell = project.terminal_settings(&cwd, cx).shell.clone();
1414                            let spawn_in_terminal = task::SpawnInTerminal {
1415                                id: task::TaskId("install".to_string()),
1416                                full_label: upgrade_command.clone(),
1417                                label: upgrade_command.clone(),
1418                                command: Some(upgrade_command.clone()),
1419                                args: Vec::new(),
1420                                command_label: upgrade_command.clone(),
1421                                cwd,
1422                                env: Default::default(),
1423                                use_new_terminal: true,
1424                                allow_concurrent_runs: true,
1425                                reveal: Default::default(),
1426                                reveal_target: Default::default(),
1427                                hide: Default::default(),
1428                                shell,
1429                                show_summary: true,
1430                                show_command: true,
1431                                show_rerun: false,
1432                            };
1433                            workspace
1434                                .spawn_in_terminal(spawn_in_terminal, window, cx)
1435                                .detach();
1436                        })
1437                        .ok();
1438                }),
1439            ));
1440        }
1441
1442        container.into_any()
1443    }
1444
1445    fn render_edits_bar(
1446        &self,
1447        thread_entity: &Entity<AcpThread>,
1448        window: &mut Window,
1449        cx: &Context<Self>,
1450    ) -> Option<AnyElement> {
1451        let thread = thread_entity.read(cx);
1452        let action_log = thread.action_log();
1453        let changed_buffers = action_log.read(cx).changed_buffers(cx);
1454
1455        if changed_buffers.is_empty() {
1456            return None;
1457        }
1458
1459        let editor_bg_color = cx.theme().colors().editor_background;
1460        let active_color = cx.theme().colors().element_selected;
1461        let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
1462
1463        let pending_edits = thread.has_pending_edit_tool_calls();
1464        let expanded = self.edits_expanded;
1465
1466        v_flex()
1467            .mt_1()
1468            .mx_2()
1469            .bg(bg_edit_files_disclosure)
1470            .border_1()
1471            .border_b_0()
1472            .border_color(cx.theme().colors().border)
1473            .rounded_t_md()
1474            .shadow(vec![gpui::BoxShadow {
1475                color: gpui::black().opacity(0.15),
1476                offset: point(px(1.), px(-1.)),
1477                blur_radius: px(3.),
1478                spread_radius: px(0.),
1479            }])
1480            .child(self.render_edits_bar_summary(
1481                action_log,
1482                &changed_buffers,
1483                expanded,
1484                pending_edits,
1485                window,
1486                cx,
1487            ))
1488            .when(expanded, |parent| {
1489                parent.child(self.render_edits_bar_files(
1490                    action_log,
1491                    &changed_buffers,
1492                    pending_edits,
1493                    cx,
1494                ))
1495            })
1496            .into_any()
1497            .into()
1498    }
1499
1500    fn render_edits_bar_summary(
1501        &self,
1502        action_log: &Entity<ActionLog>,
1503        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
1504        expanded: bool,
1505        pending_edits: bool,
1506        window: &mut Window,
1507        cx: &Context<Self>,
1508    ) -> Div {
1509        const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
1510
1511        let focus_handle = self.focus_handle(cx);
1512
1513        h_flex()
1514            .p_1()
1515            .justify_between()
1516            .when(expanded, |this| {
1517                this.border_b_1().border_color(cx.theme().colors().border)
1518            })
1519            .child(
1520                h_flex()
1521                    .id("edits-container")
1522                    .cursor_pointer()
1523                    .w_full()
1524                    .gap_1()
1525                    .child(Disclosure::new("edits-disclosure", expanded))
1526                    .map(|this| {
1527                        if pending_edits {
1528                            this.child(
1529                                Label::new(format!(
1530                                    "Editing {} {}",
1531                                    changed_buffers.len(),
1532                                    if changed_buffers.len() == 1 {
1533                                        "file"
1534                                    } else {
1535                                        "files"
1536                                    }
1537                                ))
1538                                .color(Color::Muted)
1539                                .size(LabelSize::Small)
1540                                .with_animation(
1541                                    "edit-label",
1542                                    Animation::new(Duration::from_secs(2))
1543                                        .repeat()
1544                                        .with_easing(pulsating_between(0.3, 0.7)),
1545                                    |label, delta| label.alpha(delta),
1546                                ),
1547                            )
1548                        } else {
1549                            this.child(
1550                                Label::new("Edits")
1551                                    .size(LabelSize::Small)
1552                                    .color(Color::Muted),
1553                            )
1554                            .child(Label::new("").size(LabelSize::XSmall).color(Color::Muted))
1555                            .child(
1556                                Label::new(format!(
1557                                    "{} {}",
1558                                    changed_buffers.len(),
1559                                    if changed_buffers.len() == 1 {
1560                                        "file"
1561                                    } else {
1562                                        "files"
1563                                    }
1564                                ))
1565                                .size(LabelSize::Small)
1566                                .color(Color::Muted),
1567                            )
1568                        }
1569                    })
1570                    .on_click(cx.listener(|this, _, _, cx| {
1571                        this.edits_expanded = !this.edits_expanded;
1572                        cx.notify();
1573                    })),
1574            )
1575            .child(
1576                h_flex()
1577                    .gap_1()
1578                    .child(
1579                        IconButton::new("review-changes", IconName::ListTodo)
1580                            .icon_size(IconSize::Small)
1581                            .tooltip({
1582                                let focus_handle = focus_handle.clone();
1583                                move |window, cx| {
1584                                    Tooltip::for_action_in(
1585                                        "Review Changes",
1586                                        &OpenAgentDiff,
1587                                        &focus_handle,
1588                                        window,
1589                                        cx,
1590                                    )
1591                                }
1592                            })
1593                            .on_click(cx.listener(|_, _, window, cx| {
1594                                window.dispatch_action(OpenAgentDiff.boxed_clone(), cx);
1595                            })),
1596                    )
1597                    .child(Divider::vertical().color(DividerColor::Border))
1598                    .child(
1599                        Button::new("reject-all-changes", "Reject All")
1600                            .label_size(LabelSize::Small)
1601                            .disabled(pending_edits)
1602                            .when(pending_edits, |this| {
1603                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
1604                            })
1605                            .key_binding(
1606                                KeyBinding::for_action_in(
1607                                    &RejectAll,
1608                                    &focus_handle.clone(),
1609                                    window,
1610                                    cx,
1611                                )
1612                                .map(|kb| kb.size(rems_from_px(10.))),
1613                            )
1614                            .on_click({
1615                                let action_log = action_log.clone();
1616                                cx.listener(move |_, _, _, cx| {
1617                                    action_log.update(cx, |action_log, cx| {
1618                                        action_log.reject_all_edits(cx).detach();
1619                                    })
1620                                })
1621                            }),
1622                    )
1623                    .child(
1624                        Button::new("keep-all-changes", "Keep All")
1625                            .label_size(LabelSize::Small)
1626                            .disabled(pending_edits)
1627                            .when(pending_edits, |this| {
1628                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
1629                            })
1630                            .key_binding(
1631                                KeyBinding::for_action_in(&KeepAll, &focus_handle, window, cx)
1632                                    .map(|kb| kb.size(rems_from_px(10.))),
1633                            )
1634                            .on_click({
1635                                let action_log = action_log.clone();
1636                                cx.listener(move |_, _, _, cx| {
1637                                    action_log.update(cx, |action_log, cx| {
1638                                        action_log.keep_all_edits(cx);
1639                                    })
1640                                })
1641                            }),
1642                    ),
1643            )
1644    }
1645
1646    fn render_edits_bar_files(
1647        &self,
1648        action_log: &Entity<ActionLog>,
1649        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
1650        pending_edits: bool,
1651        cx: &Context<Self>,
1652    ) -> Div {
1653        let editor_bg_color = cx.theme().colors().editor_background;
1654
1655        v_flex().children(changed_buffers.into_iter().enumerate().flat_map(
1656            |(index, (buffer, _diff))| {
1657                let file = buffer.read(cx).file()?;
1658                let path = file.path();
1659
1660                let file_path = path.parent().and_then(|parent| {
1661                    let parent_str = parent.to_string_lossy();
1662
1663                    if parent_str.is_empty() {
1664                        None
1665                    } else {
1666                        Some(
1667                            Label::new(format!("/{}{}", parent_str, std::path::MAIN_SEPARATOR_STR))
1668                                .color(Color::Muted)
1669                                .size(LabelSize::XSmall)
1670                                .buffer_font(cx),
1671                        )
1672                    }
1673                });
1674
1675                let file_name = path.file_name().map(|name| {
1676                    Label::new(name.to_string_lossy().to_string())
1677                        .size(LabelSize::XSmall)
1678                        .buffer_font(cx)
1679                });
1680
1681                let file_icon = FileIcons::get_icon(&path, cx)
1682                    .map(Icon::from_path)
1683                    .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
1684                    .unwrap_or_else(|| {
1685                        Icon::new(IconName::File)
1686                            .color(Color::Muted)
1687                            .size(IconSize::Small)
1688                    });
1689
1690                let overlay_gradient = linear_gradient(
1691                    90.,
1692                    linear_color_stop(editor_bg_color, 1.),
1693                    linear_color_stop(editor_bg_color.opacity(0.2), 0.),
1694                );
1695
1696                let element = h_flex()
1697                    .group("edited-code")
1698                    .id(("file-container", index))
1699                    .relative()
1700                    .py_1()
1701                    .pl_2()
1702                    .pr_1()
1703                    .gap_2()
1704                    .justify_between()
1705                    .bg(editor_bg_color)
1706                    .when(index < changed_buffers.len() - 1, |parent| {
1707                        parent.border_color(cx.theme().colors().border).border_b_1()
1708                    })
1709                    .child(
1710                        h_flex()
1711                            .id(("file-name", index))
1712                            .pr_8()
1713                            .gap_1p5()
1714                            .max_w_full()
1715                            .overflow_x_scroll()
1716                            .child(file_icon)
1717                            .child(h_flex().gap_0p5().children(file_name).children(file_path))
1718                            .on_click({
1719                                let buffer = buffer.clone();
1720                                cx.listener(move |this, _, window, cx| {
1721                                    this.open_edited_buffer(&buffer, window, cx);
1722                                })
1723                            }),
1724                    )
1725                    .child(
1726                        h_flex()
1727                            .gap_1()
1728                            .visible_on_hover("edited-code")
1729                            .child(
1730                                Button::new("review", "Review")
1731                                    .label_size(LabelSize::Small)
1732                                    .on_click({
1733                                        let buffer = buffer.clone();
1734                                        cx.listener(move |this, _, window, cx| {
1735                                            this.open_edited_buffer(&buffer, window, cx);
1736                                        })
1737                                    }),
1738                            )
1739                            .child(Divider::vertical().color(DividerColor::BorderVariant))
1740                            .child(
1741                                Button::new("reject-file", "Reject")
1742                                    .label_size(LabelSize::Small)
1743                                    .disabled(pending_edits)
1744                                    .on_click({
1745                                        let buffer = buffer.clone();
1746                                        let action_log = action_log.clone();
1747                                        move |_, _, cx| {
1748                                            action_log.update(cx, |action_log, cx| {
1749                                                action_log
1750                                                    .reject_edits_in_ranges(
1751                                                        buffer.clone(),
1752                                                        vec![Anchor::MIN..Anchor::MAX],
1753                                                        cx,
1754                                                    )
1755                                                    .detach_and_log_err(cx);
1756                                            })
1757                                        }
1758                                    }),
1759                            )
1760                            .child(
1761                                Button::new("keep-file", "Keep")
1762                                    .label_size(LabelSize::Small)
1763                                    .disabled(pending_edits)
1764                                    .on_click({
1765                                        let buffer = buffer.clone();
1766                                        let action_log = action_log.clone();
1767                                        move |_, _, cx| {
1768                                            action_log.update(cx, |action_log, cx| {
1769                                                action_log.keep_edits_in_range(
1770                                                    buffer.clone(),
1771                                                    Anchor::MIN..Anchor::MAX,
1772                                                    cx,
1773                                                );
1774                                            })
1775                                        }
1776                                    }),
1777                            ),
1778                    )
1779                    .child(
1780                        div()
1781                            .id("gradient-overlay")
1782                            .absolute()
1783                            .h_full()
1784                            .w_12()
1785                            .top_0()
1786                            .bottom_0()
1787                            .right(px(152.))
1788                            .bg(overlay_gradient),
1789                    );
1790
1791                Some(element)
1792            },
1793        ))
1794    }
1795
1796    fn render_message_editor(&mut self, cx: &mut Context<Self>) -> AnyElement {
1797        let settings = ThemeSettings::get_global(cx);
1798        let font_size = TextSize::Small
1799            .rems(cx)
1800            .to_pixels(settings.agent_font_size(cx));
1801        let line_height = settings.buffer_line_height.value() * font_size;
1802
1803        let text_style = TextStyle {
1804            color: cx.theme().colors().text,
1805            font_family: settings.buffer_font.family.clone(),
1806            font_fallbacks: settings.buffer_font.fallbacks.clone(),
1807            font_features: settings.buffer_font.features.clone(),
1808            font_size: font_size.into(),
1809            line_height: line_height.into(),
1810            ..Default::default()
1811        };
1812
1813        EditorElement::new(
1814            &self.message_editor,
1815            EditorStyle {
1816                background: cx.theme().colors().editor_background,
1817                local_player: cx.theme().players().local(),
1818                text: text_style,
1819                syntax: cx.theme().syntax().clone(),
1820                ..Default::default()
1821            },
1822        )
1823        .into_any()
1824    }
1825
1826    fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
1827        if self.thread().map_or(true, |thread| {
1828            thread.read(cx).status() == ThreadStatus::Idle
1829        }) {
1830            let is_editor_empty = self.message_editor.read(cx).is_empty(cx);
1831            IconButton::new("send-message", IconName::Send)
1832                .icon_color(Color::Accent)
1833                .style(ButtonStyle::Filled)
1834                .disabled(self.thread().is_none() || is_editor_empty)
1835                .on_click(cx.listener(|this, _, window, cx| {
1836                    this.chat(&Chat, window, cx);
1837                }))
1838                .when(!is_editor_empty, |button| {
1839                    button.tooltip(move |window, cx| Tooltip::for_action("Send", &Chat, window, cx))
1840                })
1841                .when(is_editor_empty, |button| {
1842                    button.tooltip(Tooltip::text("Type a message to submit"))
1843                })
1844                .into_any_element()
1845        } else {
1846            IconButton::new("stop-generation", IconName::StopFilled)
1847                .icon_color(Color::Error)
1848                .style(ButtonStyle::Tinted(ui::TintColor::Error))
1849                .tooltip(move |window, cx| {
1850                    Tooltip::for_action("Stop Generation", &editor::actions::Cancel, window, cx)
1851                })
1852                .on_click(cx.listener(|this, _event, _, cx| this.cancel(cx)))
1853                .into_any_element()
1854        }
1855    }
1856
1857    fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
1858        let following = self
1859            .workspace
1860            .read_with(cx, |workspace, _| {
1861                workspace.is_being_followed(CollaboratorId::Agent)
1862            })
1863            .unwrap_or(false);
1864
1865        IconButton::new("follow-agent", IconName::Crosshair)
1866            .icon_size(IconSize::Small)
1867            .icon_color(Color::Muted)
1868            .toggle_state(following)
1869            .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
1870            .tooltip(move |window, cx| {
1871                if following {
1872                    Tooltip::for_action("Stop Following Agent", &Follow, window, cx)
1873                } else {
1874                    Tooltip::with_meta(
1875                        "Follow Agent",
1876                        Some(&Follow),
1877                        "Track the agent's location as it reads and edits files.",
1878                        window,
1879                        cx,
1880                    )
1881                }
1882            })
1883            .on_click(cx.listener(move |this, _, window, cx| {
1884                this.workspace
1885                    .update(cx, |workspace, cx| {
1886                        if following {
1887                            workspace.unfollow(CollaboratorId::Agent, window, cx);
1888                        } else {
1889                            workspace.follow(CollaboratorId::Agent, window, cx);
1890                        }
1891                    })
1892                    .ok();
1893            }))
1894    }
1895
1896    fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
1897        let workspace = self.workspace.clone();
1898        MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
1899            Self::open_link(text, &workspace, window, cx);
1900        })
1901    }
1902
1903    fn open_link(
1904        url: SharedString,
1905        workspace: &WeakEntity<Workspace>,
1906        window: &mut Window,
1907        cx: &mut App,
1908    ) {
1909        let Some(workspace) = workspace.upgrade() else {
1910            cx.open_url(&url);
1911            return;
1912        };
1913
1914        if let Some(mention_path) = MentionPath::try_parse(&url) {
1915            workspace.update(cx, |workspace, cx| {
1916                let project = workspace.project();
1917                let Some((path, entry)) = project.update(cx, |project, cx| {
1918                    let path = project.find_project_path(mention_path.path(), cx)?;
1919                    let entry = project.entry_for_path(&path, cx)?;
1920                    Some((path, entry))
1921                }) else {
1922                    return;
1923                };
1924
1925                if entry.is_dir() {
1926                    project.update(cx, |_, cx| {
1927                        cx.emit(project::Event::RevealInProjectPanel(entry.id));
1928                    });
1929                } else {
1930                    workspace
1931                        .open_path(path, None, true, window, cx)
1932                        .detach_and_log_err(cx);
1933                }
1934            })
1935        } else {
1936            cx.open_url(&url);
1937        }
1938    }
1939
1940    fn open_tool_call_location(
1941        &self,
1942        entry_ix: usize,
1943        location_ix: usize,
1944        window: &mut Window,
1945        cx: &mut Context<Self>,
1946    ) -> Option<()> {
1947        let location = self
1948            .thread()?
1949            .read(cx)
1950            .entries()
1951            .get(entry_ix)?
1952            .locations()?
1953            .get(location_ix)?;
1954
1955        let project_path = self
1956            .project
1957            .read(cx)
1958            .find_project_path(&location.path, cx)?;
1959
1960        let open_task = self
1961            .workspace
1962            .update(cx, |worskpace, cx| {
1963                worskpace.open_path(project_path, None, true, window, cx)
1964            })
1965            .log_err()?;
1966
1967        window
1968            .spawn(cx, async move |cx| {
1969                let item = open_task.await?;
1970
1971                let Some(active_editor) = item.downcast::<Editor>() else {
1972                    return anyhow::Ok(());
1973                };
1974
1975                active_editor.update_in(cx, |editor, window, cx| {
1976                    let snapshot = editor.buffer().read(cx).snapshot(cx);
1977                    let first_hunk = editor
1978                        .diff_hunks_in_ranges(
1979                            &[editor::Anchor::min()..editor::Anchor::max()],
1980                            &snapshot,
1981                        )
1982                        .next();
1983                    if let Some(first_hunk) = first_hunk {
1984                        let first_hunk_start = first_hunk.multi_buffer_range().start;
1985                        editor.change_selections(Default::default(), window, cx, |selections| {
1986                            selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
1987                        })
1988                    }
1989                })?;
1990
1991                anyhow::Ok(())
1992            })
1993            .detach_and_log_err(cx);
1994
1995        None
1996    }
1997
1998    pub fn open_thread_as_markdown(
1999        &self,
2000        workspace: Entity<Workspace>,
2001        window: &mut Window,
2002        cx: &mut App,
2003    ) -> Task<anyhow::Result<()>> {
2004        let markdown_language_task = workspace
2005            .read(cx)
2006            .app_state()
2007            .languages
2008            .language_for_name("Markdown");
2009
2010        let (thread_summary, markdown) = match &self.thread_state {
2011            ThreadState::Ready { thread, .. } | ThreadState::Unauthenticated { thread } => {
2012                let thread = thread.read(cx);
2013                (thread.title().to_string(), thread.to_markdown(cx))
2014            }
2015            ThreadState::Loading { .. } | ThreadState::LoadError(..) => return Task::ready(Ok(())),
2016        };
2017
2018        window.spawn(cx, async move |cx| {
2019            let markdown_language = markdown_language_task.await?;
2020
2021            workspace.update_in(cx, |workspace, window, cx| {
2022                let project = workspace.project().clone();
2023
2024                if !project.read(cx).is_local() {
2025                    anyhow::bail!("failed to open active thread as markdown in remote project");
2026                }
2027
2028                let buffer = project.update(cx, |project, cx| {
2029                    project.create_local_buffer(&markdown, Some(markdown_language), cx)
2030                });
2031                let buffer = cx.new(|cx| {
2032                    MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
2033                });
2034
2035                workspace.add_item_to_active_pane(
2036                    Box::new(cx.new(|cx| {
2037                        let mut editor =
2038                            Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
2039                        editor.set_breadcrumb_header(thread_summary);
2040                        editor
2041                    })),
2042                    None,
2043                    true,
2044                    window,
2045                    cx,
2046                );
2047
2048                anyhow::Ok(())
2049            })??;
2050            anyhow::Ok(())
2051        })
2052    }
2053
2054    fn scroll_to_top(&mut self, cx: &mut Context<Self>) {
2055        self.list_state.scroll_to(ListOffset::default());
2056        cx.notify();
2057    }
2058}
2059
2060impl Focusable for AcpThreadView {
2061    fn focus_handle(&self, cx: &App) -> FocusHandle {
2062        self.message_editor.focus_handle(cx)
2063    }
2064}
2065
2066impl Render for AcpThreadView {
2067    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2068        let open_as_markdown = IconButton::new("open-as-markdown", IconName::DocumentText)
2069            .icon_size(IconSize::XSmall)
2070            .icon_color(Color::Ignored)
2071            .tooltip(Tooltip::text("Open Thread as Markdown"))
2072            .on_click(cx.listener(move |this, _, window, cx| {
2073                if let Some(workspace) = this.workspace.upgrade() {
2074                    this.open_thread_as_markdown(workspace, window, cx)
2075                        .detach_and_log_err(cx);
2076                }
2077            }));
2078
2079        let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUpAlt)
2080            .icon_size(IconSize::XSmall)
2081            .icon_color(Color::Ignored)
2082            .tooltip(Tooltip::text("Scroll To Top"))
2083            .on_click(cx.listener(move |this, _, _, cx| {
2084                this.scroll_to_top(cx);
2085            }));
2086
2087        v_flex()
2088            .size_full()
2089            .key_context("AcpThread")
2090            .on_action(cx.listener(Self::chat))
2091            .on_action(cx.listener(Self::previous_history_message))
2092            .on_action(cx.listener(Self::next_history_message))
2093            .on_action(cx.listener(Self::open_agent_diff))
2094            .child(match &self.thread_state {
2095                ThreadState::Unauthenticated { .. } => {
2096                    v_flex()
2097                        .p_2()
2098                        .flex_1()
2099                        .items_center()
2100                        .justify_center()
2101                        .child(self.render_pending_auth_state())
2102                        .child(
2103                            h_flex().mt_1p5().justify_center().child(
2104                                Button::new("sign-in", format!("Sign in to {}", self.agent.name()))
2105                                    .on_click(cx.listener(|this, _, window, cx| {
2106                                        this.authenticate(window, cx)
2107                                    })),
2108                            ),
2109                        )
2110                }
2111                ThreadState::Loading { .. } => v_flex().flex_1().child(self.render_empty_state(cx)),
2112                ThreadState::LoadError(e) => v_flex()
2113                    .p_2()
2114                    .flex_1()
2115                    .items_center()
2116                    .justify_center()
2117                    .child(self.render_error_state(e, cx)),
2118                ThreadState::Ready { thread, .. } => v_flex().flex_1().map(|this| {
2119                    if self.list_state.item_count() > 0 {
2120                        this.child(
2121                            list(self.list_state.clone())
2122                                .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
2123                                .flex_grow()
2124                                .into_any(),
2125                        )
2126                        .child(
2127                            h_flex()
2128                                .group("controls")
2129                                .mt_1()
2130                                .mr_1()
2131                                .py_2()
2132                                .px(RESPONSE_PADDING_X)
2133                                .opacity(0.4)
2134                                .hover(|style| style.opacity(1.))
2135                                .gap_1()
2136                                .flex_wrap()
2137                                .justify_end()
2138                                .child(open_as_markdown)
2139                                .child(scroll_to_top)
2140                                .into_any_element(),
2141                        )
2142                        .children(match thread.read(cx).status() {
2143                            ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => None,
2144                            ThreadStatus::Generating => div()
2145                                .px_5()
2146                                .py_2()
2147                                .child(LoadingLabel::new("").size(LabelSize::Small))
2148                                .into(),
2149                        })
2150                        .children(self.render_edits_bar(&thread, window, cx))
2151                    } else {
2152                        this.child(self.render_empty_state(cx))
2153                    }
2154                }),
2155            })
2156            .when_some(self.last_error.clone(), |el, error| {
2157                el.child(
2158                    div()
2159                        .p_2()
2160                        .text_xs()
2161                        .border_t_1()
2162                        .border_color(cx.theme().colors().border)
2163                        .bg(cx.theme().status().error_background)
2164                        .child(
2165                            self.render_markdown(error, default_markdown_style(false, window, cx)),
2166                        ),
2167                )
2168            })
2169            .child(
2170                v_flex()
2171                    .p_2()
2172                    .pt_3()
2173                    .gap_1()
2174                    .bg(cx.theme().colors().editor_background)
2175                    .border_t_1()
2176                    .border_color(cx.theme().colors().border)
2177                    .child(self.render_message_editor(cx))
2178                    .child(
2179                        h_flex()
2180                            .justify_between()
2181                            .child(self.render_follow_toggle(cx))
2182                            .child(self.render_send_button(cx)),
2183                    ),
2184            )
2185    }
2186}
2187
2188fn user_message_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
2189    let mut style = default_markdown_style(false, window, cx);
2190    let mut text_style = window.text_style();
2191    let theme_settings = ThemeSettings::get_global(cx);
2192
2193    let buffer_font = theme_settings.buffer_font.family.clone();
2194    let buffer_font_size = TextSize::Small.rems(cx);
2195
2196    text_style.refine(&TextStyleRefinement {
2197        font_family: Some(buffer_font),
2198        font_size: Some(buffer_font_size.into()),
2199        ..Default::default()
2200    });
2201
2202    style.base_text_style = text_style;
2203    style.link_callback = Some(Rc::new(move |url, cx| {
2204        if MentionPath::try_parse(url).is_some() {
2205            let colors = cx.theme().colors();
2206            Some(TextStyleRefinement {
2207                background_color: Some(colors.element_background),
2208                ..Default::default()
2209            })
2210        } else {
2211            None
2212        }
2213    }));
2214    style
2215}
2216
2217fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> MarkdownStyle {
2218    let theme_settings = ThemeSettings::get_global(cx);
2219    let colors = cx.theme().colors();
2220
2221    let buffer_font_size = TextSize::Small.rems(cx);
2222
2223    let mut text_style = window.text_style();
2224    let line_height = buffer_font_size * 1.75;
2225
2226    let font_family = if buffer_font {
2227        theme_settings.buffer_font.family.clone()
2228    } else {
2229        theme_settings.ui_font.family.clone()
2230    };
2231
2232    let font_size = if buffer_font {
2233        TextSize::Small.rems(cx)
2234    } else {
2235        TextSize::Default.rems(cx)
2236    };
2237
2238    text_style.refine(&TextStyleRefinement {
2239        font_family: Some(font_family),
2240        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
2241        font_features: Some(theme_settings.ui_font.features.clone()),
2242        font_size: Some(font_size.into()),
2243        line_height: Some(line_height.into()),
2244        color: Some(cx.theme().colors().text),
2245        ..Default::default()
2246    });
2247
2248    MarkdownStyle {
2249        base_text_style: text_style.clone(),
2250        syntax: cx.theme().syntax().clone(),
2251        selection_background_color: cx.theme().colors().element_selection_background,
2252        code_block_overflow_x_scroll: true,
2253        table_overflow_x_scroll: true,
2254        heading_level_styles: Some(HeadingLevelStyles {
2255            h1: Some(TextStyleRefinement {
2256                font_size: Some(rems(1.15).into()),
2257                ..Default::default()
2258            }),
2259            h2: Some(TextStyleRefinement {
2260                font_size: Some(rems(1.1).into()),
2261                ..Default::default()
2262            }),
2263            h3: Some(TextStyleRefinement {
2264                font_size: Some(rems(1.05).into()),
2265                ..Default::default()
2266            }),
2267            h4: Some(TextStyleRefinement {
2268                font_size: Some(rems(1.).into()),
2269                ..Default::default()
2270            }),
2271            h5: Some(TextStyleRefinement {
2272                font_size: Some(rems(0.95).into()),
2273                ..Default::default()
2274            }),
2275            h6: Some(TextStyleRefinement {
2276                font_size: Some(rems(0.875).into()),
2277                ..Default::default()
2278            }),
2279        }),
2280        code_block: StyleRefinement {
2281            padding: EdgesRefinement {
2282                top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
2283                left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
2284                right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
2285                bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
2286            },
2287            margin: EdgesRefinement {
2288                top: Some(Length::Definite(Pixels(8.).into())),
2289                left: Some(Length::Definite(Pixels(0.).into())),
2290                right: Some(Length::Definite(Pixels(0.).into())),
2291                bottom: Some(Length::Definite(Pixels(12.).into())),
2292            },
2293            border_style: Some(BorderStyle::Solid),
2294            border_widths: EdgesRefinement {
2295                top: Some(AbsoluteLength::Pixels(Pixels(1.))),
2296                left: Some(AbsoluteLength::Pixels(Pixels(1.))),
2297                right: Some(AbsoluteLength::Pixels(Pixels(1.))),
2298                bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
2299            },
2300            border_color: Some(colors.border_variant),
2301            background: Some(colors.editor_background.into()),
2302            text: Some(TextStyleRefinement {
2303                font_family: Some(theme_settings.buffer_font.family.clone()),
2304                font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
2305                font_features: Some(theme_settings.buffer_font.features.clone()),
2306                font_size: Some(buffer_font_size.into()),
2307                ..Default::default()
2308            }),
2309            ..Default::default()
2310        },
2311        inline_code: TextStyleRefinement {
2312            font_family: Some(theme_settings.buffer_font.family.clone()),
2313            font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
2314            font_features: Some(theme_settings.buffer_font.features.clone()),
2315            font_size: Some(buffer_font_size.into()),
2316            background_color: Some(colors.editor_foreground.opacity(0.08)),
2317            ..Default::default()
2318        },
2319        link: TextStyleRefinement {
2320            background_color: Some(colors.editor_foreground.opacity(0.025)),
2321            underline: Some(UnderlineStyle {
2322                color: Some(colors.text_accent.opacity(0.5)),
2323                thickness: px(1.),
2324                ..Default::default()
2325            }),
2326            ..Default::default()
2327        },
2328        ..Default::default()
2329    }
2330}