thread_view.rs

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