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