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::Text { chunk } => self
 736                                .render_markdown(chunk.clone(), style.clone())
 737                                .into_any_element(),
 738                            AssistantMessageChunk::Thought { chunk } => self.render_thinking_block(
 739                                index,
 740                                chunk_ix,
 741                                chunk.clone(),
 742                                window,
 743                                cx,
 744                            ),
 745                        }
 746                    }))
 747                    .into_any();
 748
 749                v_flex()
 750                    .px_5()
 751                    .py_1()
 752                    .when(index + 1 == total_entries, |this| this.pb_4())
 753                    .w_full()
 754                    .text_ui(cx)
 755                    .child(message_body)
 756                    .into_any()
 757            }
 758            AgentThreadEntry::ToolCall(tool_call) => div()
 759                .py_1p5()
 760                .px_5()
 761                .child(self.render_tool_call(index, tool_call, window, cx))
 762                .into_any(),
 763        }
 764    }
 765
 766    fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
 767        cx.theme()
 768            .colors()
 769            .element_background
 770            .blend(cx.theme().colors().editor_foreground.opacity(0.025))
 771    }
 772
 773    fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
 774        cx.theme().colors().border.opacity(0.6)
 775    }
 776
 777    fn tool_name_font_size(&self) -> Rems {
 778        rems_from_px(13.)
 779    }
 780
 781    fn render_thinking_block(
 782        &self,
 783        entry_ix: usize,
 784        chunk_ix: usize,
 785        chunk: Entity<Markdown>,
 786        window: &Window,
 787        cx: &Context<Self>,
 788    ) -> AnyElement {
 789        let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
 790        let key = (entry_ix, chunk_ix);
 791        let is_open = self.expanded_thinking_blocks.contains(&key);
 792
 793        v_flex()
 794            .child(
 795                h_flex()
 796                    .id(header_id)
 797                    .group("disclosure-header")
 798                    .w_full()
 799                    .justify_between()
 800                    .opacity(0.8)
 801                    .hover(|style| style.opacity(1.))
 802                    .child(
 803                        h_flex()
 804                            .gap_1p5()
 805                            .child(
 806                                Icon::new(IconName::ToolBulb)
 807                                    .size(IconSize::Small)
 808                                    .color(Color::Muted),
 809                            )
 810                            .child(
 811                                div()
 812                                    .text_size(self.tool_name_font_size())
 813                                    .child("Thinking"),
 814                            ),
 815                    )
 816                    .child(
 817                        div().visible_on_hover("disclosure-header").child(
 818                            Disclosure::new("thinking-disclosure", is_open)
 819                                .opened_icon(IconName::ChevronUp)
 820                                .closed_icon(IconName::ChevronDown)
 821                                .on_click(cx.listener({
 822                                    move |this, _event, _window, cx| {
 823                                        if is_open {
 824                                            this.expanded_thinking_blocks.remove(&key);
 825                                        } else {
 826                                            this.expanded_thinking_blocks.insert(key);
 827                                        }
 828                                        cx.notify();
 829                                    }
 830                                })),
 831                        ),
 832                    )
 833                    .on_click(cx.listener({
 834                        move |this, _event, _window, cx| {
 835                            if is_open {
 836                                this.expanded_thinking_blocks.remove(&key);
 837                            } else {
 838                                this.expanded_thinking_blocks.insert(key);
 839                            }
 840                            cx.notify();
 841                        }
 842                    })),
 843            )
 844            .when(is_open, |this| {
 845                this.child(
 846                    div()
 847                        .relative()
 848                        .mt_1p5()
 849                        .ml(px(7.))
 850                        .pl_4()
 851                        .border_l_1()
 852                        .border_color(self.tool_card_border_color(cx))
 853                        .text_ui_sm(cx)
 854                        .child(
 855                            self.render_markdown(chunk, default_markdown_style(false, window, cx)),
 856                        ),
 857                )
 858            })
 859            .into_any_element()
 860    }
 861
 862    fn render_tool_call(
 863        &self,
 864        entry_ix: usize,
 865        tool_call: &ToolCall,
 866        window: &Window,
 867        cx: &Context<Self>,
 868    ) -> Div {
 869        let header_id = SharedString::from(format!("tool-call-header-{}", entry_ix));
 870
 871        let status_icon = match &tool_call.status {
 872            ToolCallStatus::WaitingForConfirmation { .. } => None,
 873            ToolCallStatus::Allowed {
 874                status: acp::ToolCallStatus::Running,
 875                ..
 876            } => Some(
 877                Icon::new(IconName::ArrowCircle)
 878                    .color(Color::Accent)
 879                    .size(IconSize::Small)
 880                    .with_animation(
 881                        "running",
 882                        Animation::new(Duration::from_secs(2)).repeat(),
 883                        |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
 884                    )
 885                    .into_any(),
 886            ),
 887            ToolCallStatus::Allowed {
 888                status: acp::ToolCallStatus::Finished,
 889                ..
 890            } => None,
 891            ToolCallStatus::Rejected
 892            | ToolCallStatus::Canceled
 893            | ToolCallStatus::Allowed {
 894                status: acp::ToolCallStatus::Error,
 895                ..
 896            } => Some(
 897                Icon::new(IconName::X)
 898                    .color(Color::Error)
 899                    .size(IconSize::Small)
 900                    .into_any_element(),
 901            ),
 902        };
 903
 904        let needs_confirmation = match &tool_call.status {
 905            ToolCallStatus::WaitingForConfirmation { .. } => true,
 906            _ => tool_call
 907                .content
 908                .iter()
 909                .any(|content| matches!(content, ToolCallContent::Diff { .. })),
 910        };
 911
 912        let is_collapsible = tool_call.content.is_some() && !needs_confirmation;
 913        let is_open = !is_collapsible || self.expanded_tool_calls.contains(&tool_call.id);
 914
 915        let content = if is_open {
 916            match &tool_call.status {
 917                ToolCallStatus::WaitingForConfirmation { confirmation, .. } => {
 918                    Some(self.render_tool_call_confirmation(
 919                        tool_call.id,
 920                        confirmation,
 921                        tool_call.content.as_ref(),
 922                        window,
 923                        cx,
 924                    ))
 925                }
 926                ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => {
 927                    tool_call.content.as_ref().map(|content| {
 928                        div()
 929                            .py_1p5()
 930                            .child(self.render_tool_call_content(content, window, cx))
 931                            .into_any_element()
 932                    })
 933                }
 934                ToolCallStatus::Rejected => None,
 935            }
 936        } else {
 937            None
 938        };
 939
 940        v_flex()
 941            .when(needs_confirmation, |this| {
 942                this.rounded_lg()
 943                    .border_1()
 944                    .border_color(self.tool_card_border_color(cx))
 945                    .bg(cx.theme().colors().editor_background)
 946                    .overflow_hidden()
 947            })
 948            .child(
 949                h_flex()
 950                    .id(header_id)
 951                    .w_full()
 952                    .gap_1()
 953                    .justify_between()
 954                    .map(|this| {
 955                        if needs_confirmation {
 956                            this.px_2()
 957                                .py_1()
 958                                .rounded_t_md()
 959                                .bg(self.tool_card_header_bg(cx))
 960                                .border_b_1()
 961                                .border_color(self.tool_card_border_color(cx))
 962                        } else {
 963                            this.opacity(0.8).hover(|style| style.opacity(1.))
 964                        }
 965                    })
 966                    .child(
 967                        h_flex()
 968                            .id("tool-call-header")
 969                            .overflow_x_scroll()
 970                            .map(|this| {
 971                                if needs_confirmation {
 972                                    this.text_xs()
 973                                } else {
 974                                    this.text_size(self.tool_name_font_size())
 975                                }
 976                            })
 977                            .gap_1p5()
 978                            .child(
 979                                Icon::new(tool_call.icon)
 980                                    .size(IconSize::Small)
 981                                    .color(Color::Muted),
 982                            )
 983                            .child(if tool_call.locations.len() == 1 {
 984                                let name = tool_call.locations[0]
 985                                    .path
 986                                    .file_name()
 987                                    .unwrap_or_default()
 988                                    .display()
 989                                    .to_string();
 990
 991                                h_flex()
 992                                    .id(("open-tool-call-location", entry_ix))
 993                                    .child(name)
 994                                    .w_full()
 995                                    .max_w_full()
 996                                    .pr_1()
 997                                    .gap_0p5()
 998                                    .cursor_pointer()
 999                                    .rounded_sm()
1000                                    .opacity(0.8)
1001                                    .hover(|label| {
1002                                        label.opacity(1.).bg(cx
1003                                            .theme()
1004                                            .colors()
1005                                            .element_hover
1006                                            .opacity(0.5))
1007                                    })
1008                                    .tooltip(Tooltip::text("Jump to File"))
1009                                    .on_click(cx.listener(move |this, _, window, cx| {
1010                                        this.open_tool_call_location(entry_ix, 0, window, cx);
1011                                    }))
1012                                    .into_any_element()
1013                            } else {
1014                                self.render_markdown(
1015                                    tool_call.label.clone(),
1016                                    default_markdown_style(needs_confirmation, window, cx),
1017                                )
1018                                .into_any()
1019                            }),
1020                    )
1021                    .child(
1022                        h_flex()
1023                            .gap_0p5()
1024                            .when(is_collapsible, |this| {
1025                                this.child(
1026                                    Disclosure::new(("expand", tool_call.id.0), is_open)
1027                                        .opened_icon(IconName::ChevronUp)
1028                                        .closed_icon(IconName::ChevronDown)
1029                                        .on_click(cx.listener({
1030                                            let id = tool_call.id;
1031                                            move |this: &mut Self, _, _, cx: &mut Context<Self>| {
1032                                                if is_open {
1033                                                    this.expanded_tool_calls.remove(&id);
1034                                                } else {
1035                                                    this.expanded_tool_calls.insert(id);
1036                                                }
1037                                                cx.notify();
1038                                            }
1039                                        })),
1040                                )
1041                            })
1042                            .children(status_icon),
1043                    )
1044                    .on_click(cx.listener({
1045                        let id = tool_call.id;
1046                        move |this: &mut Self, _, _, cx: &mut Context<Self>| {
1047                            if is_open {
1048                                this.expanded_tool_calls.remove(&id);
1049                            } else {
1050                                this.expanded_tool_calls.insert(id);
1051                            }
1052                            cx.notify();
1053                        }
1054                    })),
1055            )
1056            .when(is_open, |this| {
1057                this.child(
1058                    div()
1059                        .text_xs()
1060                        .when(is_collapsible, |this| {
1061                            this.mt_1()
1062                                .border_1()
1063                                .border_color(self.tool_card_border_color(cx))
1064                                .bg(cx.theme().colors().editor_background)
1065                                .rounded_lg()
1066                        })
1067                        .children(content),
1068                )
1069            })
1070    }
1071
1072    fn render_tool_call_content(
1073        &self,
1074        content: &ToolCallContent,
1075        window: &Window,
1076        cx: &Context<Self>,
1077    ) -> AnyElement {
1078        match content {
1079            ToolCallContent::Markdown { markdown } => {
1080                div()
1081                    .p_2()
1082                    .child(self.render_markdown(
1083                        markdown.clone(),
1084                        default_markdown_style(false, window, cx),
1085                    ))
1086                    .into_any_element()
1087            }
1088            ToolCallContent::Diff {
1089                diff: Diff { multibuffer, .. },
1090                ..
1091            } => self.render_diff_editor(multibuffer),
1092        }
1093    }
1094
1095    fn render_tool_call_confirmation(
1096        &self,
1097        tool_call_id: ToolCallId,
1098        confirmation: &ToolCallConfirmation,
1099        content: Option<&ToolCallContent>,
1100        window: &Window,
1101        cx: &Context<Self>,
1102    ) -> AnyElement {
1103        let confirmation_container = v_flex().mt_1().py_1p5();
1104
1105        match confirmation {
1106            ToolCallConfirmation::Edit { description } => confirmation_container
1107                .child(
1108                    div()
1109                        .px_2()
1110                        .children(description.clone().map(|description| {
1111                            self.render_markdown(
1112                                description,
1113                                default_markdown_style(false, window, cx),
1114                            )
1115                        })),
1116                )
1117                .children(content.map(|content| self.render_tool_call_content(content, window, cx)))
1118                .child(self.render_confirmation_buttons(
1119                    &[AlwaysAllowOption {
1120                        id: "always_allow",
1121                        label: "Always Allow Edits".into(),
1122                        outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow,
1123                    }],
1124                    tool_call_id,
1125                    cx,
1126                ))
1127                .into_any(),
1128            ToolCallConfirmation::Execute {
1129                command,
1130                root_command,
1131                description,
1132            } => confirmation_container
1133                .child(v_flex().px_2().pb_1p5().child(command.clone()).children(
1134                    description.clone().map(|description| {
1135                        self.render_markdown(description, default_markdown_style(false, window, cx))
1136                            .on_url_click({
1137                                let workspace = self.workspace.clone();
1138                                move |text, window, cx| {
1139                                    Self::open_link(text, &workspace, window, cx);
1140                                }
1141                            })
1142                    }),
1143                ))
1144                .children(content.map(|content| self.render_tool_call_content(content, window, cx)))
1145                .child(self.render_confirmation_buttons(
1146                    &[AlwaysAllowOption {
1147                        id: "always_allow",
1148                        label: format!("Always Allow {root_command}").into(),
1149                        outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow,
1150                    }],
1151                    tool_call_id,
1152                    cx,
1153                ))
1154                .into_any(),
1155            ToolCallConfirmation::Mcp {
1156                server_name,
1157                tool_name: _,
1158                tool_display_name,
1159                description,
1160            } => confirmation_container
1161                .child(
1162                    v_flex()
1163                        .px_2()
1164                        .pb_1p5()
1165                        .child(format!("{server_name} - {tool_display_name}"))
1166                        .children(description.clone().map(|description| {
1167                            self.render_markdown(
1168                                description,
1169                                default_markdown_style(false, window, cx),
1170                            )
1171                        })),
1172                )
1173                .children(content.map(|content| self.render_tool_call_content(content, window, cx)))
1174                .child(self.render_confirmation_buttons(
1175                    &[
1176                        AlwaysAllowOption {
1177                            id: "always_allow_server",
1178                            label: format!("Always Allow {server_name}").into(),
1179                            outcome: acp::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
1180                        },
1181                        AlwaysAllowOption {
1182                            id: "always_allow_tool",
1183                            label: format!("Always Allow {tool_display_name}").into(),
1184                            outcome: acp::ToolCallConfirmationOutcome::AlwaysAllowTool,
1185                        },
1186                    ],
1187                    tool_call_id,
1188                    cx,
1189                ))
1190                .into_any(),
1191            ToolCallConfirmation::Fetch { description, urls } => confirmation_container
1192                .child(
1193                    v_flex()
1194                        .px_2()
1195                        .pb_1p5()
1196                        .gap_1()
1197                        .children(urls.iter().map(|url| {
1198                            h_flex().child(
1199                                Button::new(url.clone(), url)
1200                                    .icon(IconName::ArrowUpRight)
1201                                    .icon_color(Color::Muted)
1202                                    .icon_size(IconSize::XSmall)
1203                                    .on_click({
1204                                        let url = url.clone();
1205                                        move |_, _, cx| cx.open_url(&url)
1206                                    }),
1207                            )
1208                        }))
1209                        .children(description.clone().map(|description| {
1210                            self.render_markdown(
1211                                description,
1212                                default_markdown_style(false, window, cx),
1213                            )
1214                        })),
1215                )
1216                .children(content.map(|content| self.render_tool_call_content(content, window, cx)))
1217                .child(self.render_confirmation_buttons(
1218                    &[AlwaysAllowOption {
1219                        id: "always_allow",
1220                        label: "Always Allow".into(),
1221                        outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow,
1222                    }],
1223                    tool_call_id,
1224                    cx,
1225                ))
1226                .into_any(),
1227            ToolCallConfirmation::Other { description } => confirmation_container
1228                .child(v_flex().px_2().pb_1p5().child(self.render_markdown(
1229                    description.clone(),
1230                    default_markdown_style(false, window, cx),
1231                )))
1232                .children(content.map(|content| self.render_tool_call_content(content, window, cx)))
1233                .child(self.render_confirmation_buttons(
1234                    &[AlwaysAllowOption {
1235                        id: "always_allow",
1236                        label: "Always Allow".into(),
1237                        outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow,
1238                    }],
1239                    tool_call_id,
1240                    cx,
1241                ))
1242                .into_any(),
1243        }
1244    }
1245
1246    fn render_confirmation_buttons(
1247        &self,
1248        always_allow_options: &[AlwaysAllowOption],
1249        tool_call_id: ToolCallId,
1250        cx: &Context<Self>,
1251    ) -> Div {
1252        h_flex()
1253            .pt_1p5()
1254            .px_1p5()
1255            .gap_1()
1256            .justify_end()
1257            .border_t_1()
1258            .border_color(self.tool_card_border_color(cx))
1259            .when(self.agent.supports_always_allow(), |this| {
1260                this.children(always_allow_options.into_iter().map(|always_allow_option| {
1261                    let outcome = always_allow_option.outcome;
1262                    Button::new(
1263                        (always_allow_option.id, tool_call_id.0),
1264                        always_allow_option.label.clone(),
1265                    )
1266                    .icon(IconName::CheckDouble)
1267                    .icon_position(IconPosition::Start)
1268                    .icon_size(IconSize::XSmall)
1269                    .icon_color(Color::Success)
1270                    .on_click(cx.listener({
1271                        let id = tool_call_id;
1272                        move |this, _, _, cx| {
1273                            this.authorize_tool_call(id, outcome, cx);
1274                        }
1275                    }))
1276                }))
1277            })
1278            .child(
1279                Button::new(("allow", tool_call_id.0), "Allow")
1280                    .icon(IconName::Check)
1281                    .icon_position(IconPosition::Start)
1282                    .icon_size(IconSize::XSmall)
1283                    .icon_color(Color::Success)
1284                    .on_click(cx.listener({
1285                        let id = tool_call_id;
1286                        move |this, _, _, cx| {
1287                            this.authorize_tool_call(
1288                                id,
1289                                acp::ToolCallConfirmationOutcome::Allow,
1290                                cx,
1291                            );
1292                        }
1293                    })),
1294            )
1295            .child(
1296                Button::new(("reject", tool_call_id.0), "Reject")
1297                    .icon(IconName::X)
1298                    .icon_position(IconPosition::Start)
1299                    .icon_size(IconSize::XSmall)
1300                    .icon_color(Color::Error)
1301                    .on_click(cx.listener({
1302                        let id = tool_call_id;
1303                        move |this, _, _, cx| {
1304                            this.authorize_tool_call(
1305                                id,
1306                                acp::ToolCallConfirmationOutcome::Reject,
1307                                cx,
1308                            );
1309                        }
1310                    })),
1311            )
1312    }
1313
1314    fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>) -> AnyElement {
1315        v_flex()
1316            .h_full()
1317            .child(
1318                if let Some(editor) = self.diff_editors.get(&multibuffer.entity_id()) {
1319                    editor.clone().into_any_element()
1320                } else {
1321                    Empty.into_any()
1322                },
1323            )
1324            .into_any()
1325    }
1326
1327    fn render_agent_logo(&self) -> AnyElement {
1328        Icon::new(self.agent.logo())
1329            .color(Color::Muted)
1330            .size(IconSize::XLarge)
1331            .into_any_element()
1332    }
1333
1334    fn render_error_agent_logo(&self) -> AnyElement {
1335        let logo = Icon::new(self.agent.logo())
1336            .color(Color::Muted)
1337            .size(IconSize::XLarge)
1338            .into_any_element();
1339
1340        h_flex()
1341            .relative()
1342            .justify_center()
1343            .child(div().opacity(0.3).child(logo))
1344            .child(
1345                h_flex().absolute().right_1().bottom_0().child(
1346                    Icon::new(IconName::XCircle)
1347                        .color(Color::Error)
1348                        .size(IconSize::Small),
1349                ),
1350            )
1351            .into_any_element()
1352    }
1353
1354    fn render_empty_state(&self, cx: &App) -> AnyElement {
1355        let loading = matches!(&self.thread_state, ThreadState::Loading { .. });
1356
1357        v_flex()
1358            .size_full()
1359            .items_center()
1360            .justify_center()
1361            .child(if loading {
1362                h_flex()
1363                    .justify_center()
1364                    .child(self.render_agent_logo())
1365                    .with_animation(
1366                        "pulsating_icon",
1367                        Animation::new(Duration::from_secs(2))
1368                            .repeat()
1369                            .with_easing(pulsating_between(0.4, 1.0)),
1370                        |icon, delta| icon.opacity(delta),
1371                    )
1372                    .into_any()
1373            } else {
1374                self.render_agent_logo().into_any_element()
1375            })
1376            .child(h_flex().mt_4().mb_1().justify_center().child(if loading {
1377                div()
1378                    .child(LoadingLabel::new("").size(LabelSize::Large))
1379                    .into_any_element()
1380            } else {
1381                Headline::new(self.agent.empty_state_headline())
1382                    .size(HeadlineSize::Medium)
1383                    .into_any_element()
1384            }))
1385            .child(
1386                div()
1387                    .max_w_1_2()
1388                    .text_sm()
1389                    .text_center()
1390                    .map(|this| {
1391                        if loading {
1392                            this.invisible()
1393                        } else {
1394                            this.text_color(cx.theme().colors().text_muted)
1395                        }
1396                    })
1397                    .child(self.agent.empty_state_message()),
1398            )
1399            .into_any()
1400    }
1401
1402    fn render_pending_auth_state(&self) -> AnyElement {
1403        v_flex()
1404            .items_center()
1405            .justify_center()
1406            .child(self.render_error_agent_logo())
1407            .child(
1408                h_flex()
1409                    .mt_4()
1410                    .mb_1()
1411                    .justify_center()
1412                    .child(Headline::new("Not Authenticated").size(HeadlineSize::Medium)),
1413            )
1414            .into_any()
1415    }
1416
1417    fn render_error_state(&self, e: &LoadError, cx: &Context<Self>) -> AnyElement {
1418        let mut container = v_flex()
1419            .items_center()
1420            .justify_center()
1421            .child(self.render_error_agent_logo())
1422            .child(
1423                v_flex()
1424                    .mt_4()
1425                    .mb_2()
1426                    .gap_0p5()
1427                    .text_center()
1428                    .items_center()
1429                    .child(Headline::new("Failed to launch").size(HeadlineSize::Medium))
1430                    .child(
1431                        Label::new(e.to_string())
1432                            .size(LabelSize::Small)
1433                            .color(Color::Muted),
1434                    ),
1435            );
1436
1437        if let LoadError::Unsupported {
1438            upgrade_message,
1439            upgrade_command,
1440            ..
1441        } = &e
1442        {
1443            let upgrade_message = upgrade_message.clone();
1444            let upgrade_command = upgrade_command.clone();
1445            container = container.child(Button::new("upgrade", upgrade_message).on_click(
1446                cx.listener(move |this, _, window, cx| {
1447                    this.workspace
1448                        .update(cx, |workspace, cx| {
1449                            let project = workspace.project().read(cx);
1450                            let cwd = project.first_project_directory(cx);
1451                            let shell = project.terminal_settings(&cwd, cx).shell.clone();
1452                            let spawn_in_terminal = task::SpawnInTerminal {
1453                                id: task::TaskId("install".to_string()),
1454                                full_label: upgrade_command.clone(),
1455                                label: upgrade_command.clone(),
1456                                command: Some(upgrade_command.clone()),
1457                                args: Vec::new(),
1458                                command_label: upgrade_command.clone(),
1459                                cwd,
1460                                env: Default::default(),
1461                                use_new_terminal: true,
1462                                allow_concurrent_runs: true,
1463                                reveal: Default::default(),
1464                                reveal_target: Default::default(),
1465                                hide: Default::default(),
1466                                shell,
1467                                show_summary: true,
1468                                show_command: true,
1469                                show_rerun: false,
1470                            };
1471                            workspace
1472                                .spawn_in_terminal(spawn_in_terminal, window, cx)
1473                                .detach();
1474                        })
1475                        .ok();
1476                }),
1477            ));
1478        }
1479
1480        container.into_any()
1481    }
1482
1483    fn render_activity_bar(
1484        &self,
1485        thread_entity: &Entity<AcpThread>,
1486        window: &mut Window,
1487        cx: &Context<Self>,
1488    ) -> Option<AnyElement> {
1489        let thread = thread_entity.read(cx);
1490        let action_log = thread.action_log();
1491        let changed_buffers = action_log.read(cx).changed_buffers(cx);
1492        let plan = thread.plan();
1493
1494        if changed_buffers.is_empty() && plan.is_empty() {
1495            return None;
1496        }
1497
1498        let editor_bg_color = cx.theme().colors().editor_background;
1499        let active_color = cx.theme().colors().element_selected;
1500        let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
1501
1502        let pending_edits = thread.has_pending_edit_tool_calls();
1503
1504        v_flex()
1505            .mt_1()
1506            .mx_2()
1507            .bg(bg_edit_files_disclosure)
1508            .border_1()
1509            .border_b_0()
1510            .border_color(cx.theme().colors().border)
1511            .rounded_t_md()
1512            .shadow(vec![gpui::BoxShadow {
1513                color: gpui::black().opacity(0.15),
1514                offset: point(px(1.), px(-1.)),
1515                blur_radius: px(3.),
1516                spread_radius: px(0.),
1517            }])
1518            .when(!plan.is_empty(), |this| {
1519                this.child(self.render_plan_summary(plan, window, cx))
1520                    .when(self.plan_expanded, |parent| {
1521                        parent.child(self.render_plan_entries(plan, window, cx))
1522                    })
1523            })
1524            .when(!changed_buffers.is_empty(), |this| {
1525                this.child(Divider::horizontal())
1526                    .child(self.render_edits_summary(
1527                        action_log,
1528                        &changed_buffers,
1529                        self.edits_expanded,
1530                        pending_edits,
1531                        window,
1532                        cx,
1533                    ))
1534                    .when(self.edits_expanded, |parent| {
1535                        parent.child(self.render_edited_files(
1536                            action_log,
1537                            &changed_buffers,
1538                            pending_edits,
1539                            cx,
1540                        ))
1541                    })
1542            })
1543            .into_any()
1544            .into()
1545    }
1546
1547    fn render_plan_summary(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
1548        let stats = plan.stats();
1549
1550        let title = if let Some(entry) = stats.in_progress_entry
1551            && !self.plan_expanded
1552        {
1553            h_flex()
1554                .w_full()
1555                .gap_1()
1556                .text_xs()
1557                .text_color(cx.theme().colors().text_muted)
1558                .justify_between()
1559                .child(
1560                    h_flex()
1561                        .gap_1()
1562                        .child(
1563                            Label::new("Current:")
1564                                .size(LabelSize::Small)
1565                                .color(Color::Muted),
1566                        )
1567                        .child(MarkdownElement::new(
1568                            entry.content.clone(),
1569                            plan_label_markdown_style(&entry.status, window, cx),
1570                        )),
1571                )
1572                .when(stats.pending > 0, |this| {
1573                    this.child(
1574                        Label::new(format!("{} left", stats.pending))
1575                            .size(LabelSize::Small)
1576                            .color(Color::Muted)
1577                            .mr_1(),
1578                    )
1579                })
1580        } else {
1581            let status_label = if stats.pending == 0 {
1582                "All Done".to_string()
1583            } else if stats.completed == 0 {
1584                format!("{}", plan.entries.len())
1585            } else {
1586                format!("{}/{}", stats.completed, plan.entries.len())
1587            };
1588
1589            h_flex()
1590                .w_full()
1591                .gap_1()
1592                .justify_between()
1593                .child(
1594                    Label::new("Plan")
1595                        .size(LabelSize::Small)
1596                        .color(Color::Muted),
1597                )
1598                .child(
1599                    Label::new(status_label)
1600                        .size(LabelSize::Small)
1601                        .color(Color::Muted)
1602                        .mr_1(),
1603                )
1604        };
1605
1606        h_flex()
1607            .p_1()
1608            .justify_between()
1609            .when(self.plan_expanded, |this| {
1610                this.border_b_1().border_color(cx.theme().colors().border)
1611            })
1612            .child(
1613                h_flex()
1614                    .id("plan_summary")
1615                    .w_full()
1616                    .gap_1()
1617                    .child(Disclosure::new("plan_disclosure", self.plan_expanded))
1618                    .child(title)
1619                    .on_click(cx.listener(|this, _, _, cx| {
1620                        this.plan_expanded = !this.plan_expanded;
1621                        cx.notify();
1622                    })),
1623            )
1624    }
1625
1626    fn render_plan_entries(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
1627        v_flex().children(plan.entries.iter().enumerate().flat_map(|(index, entry)| {
1628            let element = h_flex()
1629                .py_1()
1630                .px_2()
1631                .gap_2()
1632                .justify_between()
1633                .bg(cx.theme().colors().editor_background)
1634                .when(index < plan.entries.len() - 1, |parent| {
1635                    parent.border_color(cx.theme().colors().border).border_b_1()
1636                })
1637                .child(
1638                    h_flex()
1639                        .id(("plan_entry", index))
1640                        .gap_1p5()
1641                        .max_w_full()
1642                        .overflow_x_scroll()
1643                        .text_xs()
1644                        .text_color(cx.theme().colors().text_muted)
1645                        .child(match entry.status {
1646                            acp::PlanEntryStatus::Pending => Icon::new(IconName::TodoPending)
1647                                .size(IconSize::Small)
1648                                .color(Color::Muted)
1649                                .into_any_element(),
1650                            acp::PlanEntryStatus::InProgress => Icon::new(IconName::TodoProgress)
1651                                .size(IconSize::Small)
1652                                .color(Color::Accent)
1653                                .with_animation(
1654                                    "running",
1655                                    Animation::new(Duration::from_secs(2)).repeat(),
1656                                    |icon, delta| {
1657                                        icon.transform(Transformation::rotate(percentage(delta)))
1658                                    },
1659                                )
1660                                .into_any_element(),
1661                            acp::PlanEntryStatus::Completed => Icon::new(IconName::TodoComplete)
1662                                .size(IconSize::Small)
1663                                .color(Color::Success)
1664                                .into_any_element(),
1665                        })
1666                        .child(MarkdownElement::new(
1667                            entry.content.clone(),
1668                            plan_label_markdown_style(&entry.status, window, cx),
1669                        )),
1670                );
1671
1672            Some(element)
1673        }))
1674    }
1675
1676    fn render_edits_summary(
1677        &self,
1678        action_log: &Entity<ActionLog>,
1679        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
1680        expanded: bool,
1681        pending_edits: bool,
1682        window: &mut Window,
1683        cx: &Context<Self>,
1684    ) -> Div {
1685        const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
1686
1687        let focus_handle = self.focus_handle(cx);
1688
1689        h_flex()
1690            .p_1()
1691            .justify_between()
1692            .when(expanded, |this| {
1693                this.border_b_1().border_color(cx.theme().colors().border)
1694            })
1695            .child(
1696                h_flex()
1697                    .id("edits-container")
1698                    .cursor_pointer()
1699                    .w_full()
1700                    .gap_1()
1701                    .child(Disclosure::new("edits-disclosure", expanded))
1702                    .map(|this| {
1703                        if pending_edits {
1704                            this.child(
1705                                Label::new(format!(
1706                                    "Editing {} {}",
1707                                    changed_buffers.len(),
1708                                    if changed_buffers.len() == 1 {
1709                                        "file"
1710                                    } else {
1711                                        "files"
1712                                    }
1713                                ))
1714                                .color(Color::Muted)
1715                                .size(LabelSize::Small)
1716                                .with_animation(
1717                                    "edit-label",
1718                                    Animation::new(Duration::from_secs(2))
1719                                        .repeat()
1720                                        .with_easing(pulsating_between(0.3, 0.7)),
1721                                    |label, delta| label.alpha(delta),
1722                                ),
1723                            )
1724                        } else {
1725                            this.child(
1726                                Label::new("Edits")
1727                                    .size(LabelSize::Small)
1728                                    .color(Color::Muted),
1729                            )
1730                            .child(Label::new("").size(LabelSize::XSmall).color(Color::Muted))
1731                            .child(
1732                                Label::new(format!(
1733                                    "{} {}",
1734                                    changed_buffers.len(),
1735                                    if changed_buffers.len() == 1 {
1736                                        "file"
1737                                    } else {
1738                                        "files"
1739                                    }
1740                                ))
1741                                .size(LabelSize::Small)
1742                                .color(Color::Muted),
1743                            )
1744                        }
1745                    })
1746                    .on_click(cx.listener(|this, _, _, cx| {
1747                        this.edits_expanded = !this.edits_expanded;
1748                        cx.notify();
1749                    })),
1750            )
1751            .child(
1752                h_flex()
1753                    .gap_1()
1754                    .child(
1755                        IconButton::new("review-changes", IconName::ListTodo)
1756                            .icon_size(IconSize::Small)
1757                            .tooltip({
1758                                let focus_handle = focus_handle.clone();
1759                                move |window, cx| {
1760                                    Tooltip::for_action_in(
1761                                        "Review Changes",
1762                                        &OpenAgentDiff,
1763                                        &focus_handle,
1764                                        window,
1765                                        cx,
1766                                    )
1767                                }
1768                            })
1769                            .on_click(cx.listener(|_, _, window, cx| {
1770                                window.dispatch_action(OpenAgentDiff.boxed_clone(), cx);
1771                            })),
1772                    )
1773                    .child(Divider::vertical().color(DividerColor::Border))
1774                    .child(
1775                        Button::new("reject-all-changes", "Reject All")
1776                            .label_size(LabelSize::Small)
1777                            .disabled(pending_edits)
1778                            .when(pending_edits, |this| {
1779                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
1780                            })
1781                            .key_binding(
1782                                KeyBinding::for_action_in(
1783                                    &RejectAll,
1784                                    &focus_handle.clone(),
1785                                    window,
1786                                    cx,
1787                                )
1788                                .map(|kb| kb.size(rems_from_px(10.))),
1789                            )
1790                            .on_click({
1791                                let action_log = action_log.clone();
1792                                cx.listener(move |_, _, _, cx| {
1793                                    action_log.update(cx, |action_log, cx| {
1794                                        action_log.reject_all_edits(cx).detach();
1795                                    })
1796                                })
1797                            }),
1798                    )
1799                    .child(
1800                        Button::new("keep-all-changes", "Keep All")
1801                            .label_size(LabelSize::Small)
1802                            .disabled(pending_edits)
1803                            .when(pending_edits, |this| {
1804                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
1805                            })
1806                            .key_binding(
1807                                KeyBinding::for_action_in(&KeepAll, &focus_handle, window, cx)
1808                                    .map(|kb| kb.size(rems_from_px(10.))),
1809                            )
1810                            .on_click({
1811                                let action_log = action_log.clone();
1812                                cx.listener(move |_, _, _, cx| {
1813                                    action_log.update(cx, |action_log, cx| {
1814                                        action_log.keep_all_edits(cx);
1815                                    })
1816                                })
1817                            }),
1818                    ),
1819            )
1820    }
1821
1822    fn render_edited_files(
1823        &self,
1824        action_log: &Entity<ActionLog>,
1825        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
1826        pending_edits: bool,
1827        cx: &Context<Self>,
1828    ) -> Div {
1829        let editor_bg_color = cx.theme().colors().editor_background;
1830
1831        v_flex().children(changed_buffers.into_iter().enumerate().flat_map(
1832            |(index, (buffer, _diff))| {
1833                let file = buffer.read(cx).file()?;
1834                let path = file.path();
1835
1836                let file_path = path.parent().and_then(|parent| {
1837                    let parent_str = parent.to_string_lossy();
1838
1839                    if parent_str.is_empty() {
1840                        None
1841                    } else {
1842                        Some(
1843                            Label::new(format!("/{}{}", parent_str, std::path::MAIN_SEPARATOR_STR))
1844                                .color(Color::Muted)
1845                                .size(LabelSize::XSmall)
1846                                .buffer_font(cx),
1847                        )
1848                    }
1849                });
1850
1851                let file_name = path.file_name().map(|name| {
1852                    Label::new(name.to_string_lossy().to_string())
1853                        .size(LabelSize::XSmall)
1854                        .buffer_font(cx)
1855                });
1856
1857                let file_icon = FileIcons::get_icon(&path, cx)
1858                    .map(Icon::from_path)
1859                    .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
1860                    .unwrap_or_else(|| {
1861                        Icon::new(IconName::File)
1862                            .color(Color::Muted)
1863                            .size(IconSize::Small)
1864                    });
1865
1866                let overlay_gradient = linear_gradient(
1867                    90.,
1868                    linear_color_stop(editor_bg_color, 1.),
1869                    linear_color_stop(editor_bg_color.opacity(0.2), 0.),
1870                );
1871
1872                let element = h_flex()
1873                    .group("edited-code")
1874                    .id(("file-container", index))
1875                    .relative()
1876                    .py_1()
1877                    .pl_2()
1878                    .pr_1()
1879                    .gap_2()
1880                    .justify_between()
1881                    .bg(editor_bg_color)
1882                    .when(index < changed_buffers.len() - 1, |parent| {
1883                        parent.border_color(cx.theme().colors().border).border_b_1()
1884                    })
1885                    .child(
1886                        h_flex()
1887                            .id(("file-name", index))
1888                            .pr_8()
1889                            .gap_1p5()
1890                            .max_w_full()
1891                            .overflow_x_scroll()
1892                            .child(file_icon)
1893                            .child(h_flex().gap_0p5().children(file_name).children(file_path))
1894                            .on_click({
1895                                let buffer = buffer.clone();
1896                                cx.listener(move |this, _, window, cx| {
1897                                    this.open_edited_buffer(&buffer, window, cx);
1898                                })
1899                            }),
1900                    )
1901                    .child(
1902                        h_flex()
1903                            .gap_1()
1904                            .visible_on_hover("edited-code")
1905                            .child(
1906                                Button::new("review", "Review")
1907                                    .label_size(LabelSize::Small)
1908                                    .on_click({
1909                                        let buffer = buffer.clone();
1910                                        cx.listener(move |this, _, window, cx| {
1911                                            this.open_edited_buffer(&buffer, window, cx);
1912                                        })
1913                                    }),
1914                            )
1915                            .child(Divider::vertical().color(DividerColor::BorderVariant))
1916                            .child(
1917                                Button::new("reject-file", "Reject")
1918                                    .label_size(LabelSize::Small)
1919                                    .disabled(pending_edits)
1920                                    .on_click({
1921                                        let buffer = buffer.clone();
1922                                        let action_log = action_log.clone();
1923                                        move |_, _, cx| {
1924                                            action_log.update(cx, |action_log, cx| {
1925                                                action_log
1926                                                    .reject_edits_in_ranges(
1927                                                        buffer.clone(),
1928                                                        vec![Anchor::MIN..Anchor::MAX],
1929                                                        cx,
1930                                                    )
1931                                                    .detach_and_log_err(cx);
1932                                            })
1933                                        }
1934                                    }),
1935                            )
1936                            .child(
1937                                Button::new("keep-file", "Keep")
1938                                    .label_size(LabelSize::Small)
1939                                    .disabled(pending_edits)
1940                                    .on_click({
1941                                        let buffer = buffer.clone();
1942                                        let action_log = action_log.clone();
1943                                        move |_, _, cx| {
1944                                            action_log.update(cx, |action_log, cx| {
1945                                                action_log.keep_edits_in_range(
1946                                                    buffer.clone(),
1947                                                    Anchor::MIN..Anchor::MAX,
1948                                                    cx,
1949                                                );
1950                                            })
1951                                        }
1952                                    }),
1953                            ),
1954                    )
1955                    .child(
1956                        div()
1957                            .id("gradient-overlay")
1958                            .absolute()
1959                            .h_full()
1960                            .w_12()
1961                            .top_0()
1962                            .bottom_0()
1963                            .right(px(152.))
1964                            .bg(overlay_gradient),
1965                    );
1966
1967                Some(element)
1968            },
1969        ))
1970    }
1971
1972    fn render_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
1973        let focus_handle = self.message_editor.focus_handle(cx);
1974        let editor_bg_color = cx.theme().colors().editor_background;
1975        let (expand_icon, expand_tooltip) = if self.editor_expanded {
1976            (IconName::Minimize, "Minimize Message Editor")
1977        } else {
1978            (IconName::Maximize, "Expand Message Editor")
1979        };
1980
1981        v_flex()
1982            .on_action(cx.listener(Self::expand_message_editor))
1983            .p_2()
1984            .gap_2()
1985            .border_t_1()
1986            .border_color(cx.theme().colors().border)
1987            .bg(editor_bg_color)
1988            .when(self.editor_expanded, |this| {
1989                this.h(vh(0.8, window)).size_full().justify_between()
1990            })
1991            .child(
1992                v_flex()
1993                    .relative()
1994                    .size_full()
1995                    .pt_1()
1996                    .pr_2p5()
1997                    .child(div().flex_1().child({
1998                        let settings = ThemeSettings::get_global(cx);
1999                        let font_size = TextSize::Small
2000                            .rems(cx)
2001                            .to_pixels(settings.agent_font_size(cx));
2002                        let line_height = settings.buffer_line_height.value() * font_size;
2003
2004                        let text_style = TextStyle {
2005                            color: cx.theme().colors().text,
2006                            font_family: settings.buffer_font.family.clone(),
2007                            font_fallbacks: settings.buffer_font.fallbacks.clone(),
2008                            font_features: settings.buffer_font.features.clone(),
2009                            font_size: font_size.into(),
2010                            line_height: line_height.into(),
2011                            ..Default::default()
2012                        };
2013
2014                        EditorElement::new(
2015                            &self.message_editor,
2016                            EditorStyle {
2017                                background: editor_bg_color,
2018                                local_player: cx.theme().players().local(),
2019                                text: text_style,
2020                                syntax: cx.theme().syntax().clone(),
2021                                ..Default::default()
2022                            },
2023                        )
2024                    }))
2025                    .child(
2026                        h_flex()
2027                            .absolute()
2028                            .top_0()
2029                            .right_0()
2030                            .opacity(0.5)
2031                            .hover(|this| this.opacity(1.0))
2032                            .child(
2033                                IconButton::new("toggle-height", expand_icon)
2034                                    .icon_size(IconSize::XSmall)
2035                                    .icon_color(Color::Muted)
2036                                    .tooltip({
2037                                        let focus_handle = focus_handle.clone();
2038                                        move |window, cx| {
2039                                            Tooltip::for_action_in(
2040                                                expand_tooltip,
2041                                                &ExpandMessageEditor,
2042                                                &focus_handle,
2043                                                window,
2044                                                cx,
2045                                            )
2046                                        }
2047                                    })
2048                                    .on_click(cx.listener(|_, _, window, cx| {
2049                                        window.dispatch_action(Box::new(ExpandMessageEditor), cx);
2050                                    })),
2051                            ),
2052                    ),
2053            )
2054            .child(
2055                h_flex()
2056                    .flex_none()
2057                    .justify_between()
2058                    .child(self.render_follow_toggle(cx))
2059                    .child(self.render_send_button(cx)),
2060            )
2061            .into_any()
2062    }
2063
2064    fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
2065        if self.thread().map_or(true, |thread| {
2066            thread.read(cx).status() == ThreadStatus::Idle
2067        }) {
2068            let is_editor_empty = self.message_editor.read(cx).is_empty(cx);
2069            IconButton::new("send-message", IconName::Send)
2070                .icon_color(Color::Accent)
2071                .style(ButtonStyle::Filled)
2072                .disabled(self.thread().is_none() || is_editor_empty)
2073                .on_click(cx.listener(|this, _, window, cx| {
2074                    this.chat(&Chat, window, cx);
2075                }))
2076                .when(!is_editor_empty, |button| {
2077                    button.tooltip(move |window, cx| Tooltip::for_action("Send", &Chat, window, cx))
2078                })
2079                .when(is_editor_empty, |button| {
2080                    button.tooltip(Tooltip::text("Type a message to submit"))
2081                })
2082                .into_any_element()
2083        } else {
2084            IconButton::new("stop-generation", IconName::StopFilled)
2085                .icon_color(Color::Error)
2086                .style(ButtonStyle::Tinted(ui::TintColor::Error))
2087                .tooltip(move |window, cx| {
2088                    Tooltip::for_action("Stop Generation", &editor::actions::Cancel, window, cx)
2089                })
2090                .on_click(cx.listener(|this, _event, _, cx| this.cancel(cx)))
2091                .into_any_element()
2092        }
2093    }
2094
2095    fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
2096        let following = self
2097            .workspace
2098            .read_with(cx, |workspace, _| {
2099                workspace.is_being_followed(CollaboratorId::Agent)
2100            })
2101            .unwrap_or(false);
2102
2103        IconButton::new("follow-agent", IconName::Crosshair)
2104            .icon_size(IconSize::Small)
2105            .icon_color(Color::Muted)
2106            .toggle_state(following)
2107            .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
2108            .tooltip(move |window, cx| {
2109                if following {
2110                    Tooltip::for_action("Stop Following Agent", &Follow, window, cx)
2111                } else {
2112                    Tooltip::with_meta(
2113                        "Follow Agent",
2114                        Some(&Follow),
2115                        "Track the agent's location as it reads and edits files.",
2116                        window,
2117                        cx,
2118                    )
2119                }
2120            })
2121            .on_click(cx.listener(move |this, _, window, cx| {
2122                this.workspace
2123                    .update(cx, |workspace, cx| {
2124                        if following {
2125                            workspace.unfollow(CollaboratorId::Agent, window, cx);
2126                        } else {
2127                            workspace.follow(CollaboratorId::Agent, window, cx);
2128                        }
2129                    })
2130                    .ok();
2131            }))
2132    }
2133
2134    fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
2135        let workspace = self.workspace.clone();
2136        MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
2137            Self::open_link(text, &workspace, window, cx);
2138        })
2139    }
2140
2141    fn open_link(
2142        url: SharedString,
2143        workspace: &WeakEntity<Workspace>,
2144        window: &mut Window,
2145        cx: &mut App,
2146    ) {
2147        let Some(workspace) = workspace.upgrade() else {
2148            cx.open_url(&url);
2149            return;
2150        };
2151
2152        if let Some(mention_path) = MentionPath::try_parse(&url) {
2153            workspace.update(cx, |workspace, cx| {
2154                let project = workspace.project();
2155                let Some((path, entry)) = project.update(cx, |project, cx| {
2156                    let path = project.find_project_path(mention_path.path(), cx)?;
2157                    let entry = project.entry_for_path(&path, cx)?;
2158                    Some((path, entry))
2159                }) else {
2160                    return;
2161                };
2162
2163                if entry.is_dir() {
2164                    project.update(cx, |_, cx| {
2165                        cx.emit(project::Event::RevealInProjectPanel(entry.id));
2166                    });
2167                } else {
2168                    workspace
2169                        .open_path(path, None, true, window, cx)
2170                        .detach_and_log_err(cx);
2171                }
2172            })
2173        } else {
2174            cx.open_url(&url);
2175        }
2176    }
2177
2178    fn open_tool_call_location(
2179        &self,
2180        entry_ix: usize,
2181        location_ix: usize,
2182        window: &mut Window,
2183        cx: &mut Context<Self>,
2184    ) -> Option<()> {
2185        let location = self
2186            .thread()?
2187            .read(cx)
2188            .entries()
2189            .get(entry_ix)?
2190            .locations()?
2191            .get(location_ix)?;
2192
2193        let project_path = self
2194            .project
2195            .read(cx)
2196            .find_project_path(&location.path, cx)?;
2197
2198        let open_task = self
2199            .workspace
2200            .update(cx, |worskpace, cx| {
2201                worskpace.open_path(project_path, None, true, window, cx)
2202            })
2203            .log_err()?;
2204
2205        window
2206            .spawn(cx, async move |cx| {
2207                let item = open_task.await?;
2208
2209                let Some(active_editor) = item.downcast::<Editor>() else {
2210                    return anyhow::Ok(());
2211                };
2212
2213                active_editor.update_in(cx, |editor, window, cx| {
2214                    let snapshot = editor.buffer().read(cx).snapshot(cx);
2215                    let first_hunk = editor
2216                        .diff_hunks_in_ranges(
2217                            &[editor::Anchor::min()..editor::Anchor::max()],
2218                            &snapshot,
2219                        )
2220                        .next();
2221                    if let Some(first_hunk) = first_hunk {
2222                        let first_hunk_start = first_hunk.multi_buffer_range().start;
2223                        editor.change_selections(Default::default(), window, cx, |selections| {
2224                            selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
2225                        })
2226                    }
2227                })?;
2228
2229                anyhow::Ok(())
2230            })
2231            .detach_and_log_err(cx);
2232
2233        None
2234    }
2235
2236    pub fn open_thread_as_markdown(
2237        &self,
2238        workspace: Entity<Workspace>,
2239        window: &mut Window,
2240        cx: &mut App,
2241    ) -> Task<anyhow::Result<()>> {
2242        let markdown_language_task = workspace
2243            .read(cx)
2244            .app_state()
2245            .languages
2246            .language_for_name("Markdown");
2247
2248        let (thread_summary, markdown) = match &self.thread_state {
2249            ThreadState::Ready { thread, .. } | ThreadState::Unauthenticated { thread } => {
2250                let thread = thread.read(cx);
2251                (thread.title().to_string(), thread.to_markdown(cx))
2252            }
2253            ThreadState::Loading { .. } | ThreadState::LoadError(..) => return Task::ready(Ok(())),
2254        };
2255
2256        window.spawn(cx, async move |cx| {
2257            let markdown_language = markdown_language_task.await?;
2258
2259            workspace.update_in(cx, |workspace, window, cx| {
2260                let project = workspace.project().clone();
2261
2262                if !project.read(cx).is_local() {
2263                    anyhow::bail!("failed to open active thread as markdown in remote project");
2264                }
2265
2266                let buffer = project.update(cx, |project, cx| {
2267                    project.create_local_buffer(&markdown, Some(markdown_language), cx)
2268                });
2269                let buffer = cx.new(|cx| {
2270                    MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
2271                });
2272
2273                workspace.add_item_to_active_pane(
2274                    Box::new(cx.new(|cx| {
2275                        let mut editor =
2276                            Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
2277                        editor.set_breadcrumb_header(thread_summary);
2278                        editor
2279                    })),
2280                    None,
2281                    true,
2282                    window,
2283                    cx,
2284                );
2285
2286                anyhow::Ok(())
2287            })??;
2288            anyhow::Ok(())
2289        })
2290    }
2291
2292    fn scroll_to_top(&mut self, cx: &mut Context<Self>) {
2293        self.list_state.scroll_to(ListOffset::default());
2294        cx.notify();
2295    }
2296}
2297
2298impl Focusable for AcpThreadView {
2299    fn focus_handle(&self, cx: &App) -> FocusHandle {
2300        self.message_editor.focus_handle(cx)
2301    }
2302}
2303
2304impl Render for AcpThreadView {
2305    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2306        let open_as_markdown = IconButton::new("open-as-markdown", IconName::DocumentText)
2307            .icon_size(IconSize::XSmall)
2308            .icon_color(Color::Ignored)
2309            .tooltip(Tooltip::text("Open Thread as Markdown"))
2310            .on_click(cx.listener(move |this, _, window, cx| {
2311                if let Some(workspace) = this.workspace.upgrade() {
2312                    this.open_thread_as_markdown(workspace, window, cx)
2313                        .detach_and_log_err(cx);
2314                }
2315            }));
2316
2317        let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUpAlt)
2318            .icon_size(IconSize::XSmall)
2319            .icon_color(Color::Ignored)
2320            .tooltip(Tooltip::text("Scroll To Top"))
2321            .on_click(cx.listener(move |this, _, _, cx| {
2322                this.scroll_to_top(cx);
2323            }));
2324
2325        v_flex()
2326            .size_full()
2327            .key_context("AcpThread")
2328            .on_action(cx.listener(Self::chat))
2329            .on_action(cx.listener(Self::previous_history_message))
2330            .on_action(cx.listener(Self::next_history_message))
2331            .on_action(cx.listener(Self::open_agent_diff))
2332            .child(match &self.thread_state {
2333                ThreadState::Unauthenticated { .. } => {
2334                    v_flex()
2335                        .p_2()
2336                        .flex_1()
2337                        .items_center()
2338                        .justify_center()
2339                        .child(self.render_pending_auth_state())
2340                        .child(
2341                            h_flex().mt_1p5().justify_center().child(
2342                                Button::new("sign-in", format!("Sign in to {}", self.agent.name()))
2343                                    .on_click(cx.listener(|this, _, window, cx| {
2344                                        this.authenticate(window, cx)
2345                                    })),
2346                            ),
2347                        )
2348                }
2349                ThreadState::Loading { .. } => v_flex().flex_1().child(self.render_empty_state(cx)),
2350                ThreadState::LoadError(e) => v_flex()
2351                    .p_2()
2352                    .flex_1()
2353                    .items_center()
2354                    .justify_center()
2355                    .child(self.render_error_state(e, cx)),
2356                ThreadState::Ready { thread, .. } => v_flex().flex_1().map(|this| {
2357                    if self.list_state.item_count() > 0 {
2358                        this.child(
2359                            list(self.list_state.clone())
2360                                .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
2361                                .flex_grow()
2362                                .into_any(),
2363                        )
2364                        .child(
2365                            h_flex()
2366                                .group("controls")
2367                                .mt_1()
2368                                .mr_1()
2369                                .py_2()
2370                                .px(RESPONSE_PADDING_X)
2371                                .opacity(0.4)
2372                                .hover(|style| style.opacity(1.))
2373                                .flex_wrap()
2374                                .justify_end()
2375                                .child(open_as_markdown)
2376                                .child(scroll_to_top)
2377                                .into_any_element(),
2378                        )
2379                        .children(match thread.read(cx).status() {
2380                            ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => None,
2381                            ThreadStatus::Generating => div()
2382                                .px_5()
2383                                .py_2()
2384                                .child(LoadingLabel::new("").size(LabelSize::Small))
2385                                .into(),
2386                        })
2387                        .children(self.render_activity_bar(&thread, window, cx))
2388                    } else {
2389                        this.child(self.render_empty_state(cx))
2390                    }
2391                }),
2392            })
2393            .when_some(self.last_error.clone(), |el, error| {
2394                el.child(
2395                    div()
2396                        .p_2()
2397                        .text_xs()
2398                        .border_t_1()
2399                        .border_color(cx.theme().colors().border)
2400                        .bg(cx.theme().status().error_background)
2401                        .child(
2402                            self.render_markdown(error, default_markdown_style(false, window, cx)),
2403                        ),
2404                )
2405            })
2406            .child(self.render_message_editor(window, cx))
2407    }
2408}
2409
2410fn user_message_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
2411    let mut style = default_markdown_style(false, window, cx);
2412    let mut text_style = window.text_style();
2413    let theme_settings = ThemeSettings::get_global(cx);
2414
2415    let buffer_font = theme_settings.buffer_font.family.clone();
2416    let buffer_font_size = TextSize::Small.rems(cx);
2417
2418    text_style.refine(&TextStyleRefinement {
2419        font_family: Some(buffer_font),
2420        font_size: Some(buffer_font_size.into()),
2421        ..Default::default()
2422    });
2423
2424    style.base_text_style = text_style;
2425    style.link_callback = Some(Rc::new(move |url, cx| {
2426        if MentionPath::try_parse(url).is_some() {
2427            let colors = cx.theme().colors();
2428            Some(TextStyleRefinement {
2429                background_color: Some(colors.element_background),
2430                ..Default::default()
2431            })
2432        } else {
2433            None
2434        }
2435    }));
2436    style
2437}
2438
2439fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> MarkdownStyle {
2440    let theme_settings = ThemeSettings::get_global(cx);
2441    let colors = cx.theme().colors();
2442
2443    let buffer_font_size = TextSize::Small.rems(cx);
2444
2445    let mut text_style = window.text_style();
2446    let line_height = buffer_font_size * 1.75;
2447
2448    let font_family = if buffer_font {
2449        theme_settings.buffer_font.family.clone()
2450    } else {
2451        theme_settings.ui_font.family.clone()
2452    };
2453
2454    let font_size = if buffer_font {
2455        TextSize::Small.rems(cx)
2456    } else {
2457        TextSize::Default.rems(cx)
2458    };
2459
2460    text_style.refine(&TextStyleRefinement {
2461        font_family: Some(font_family),
2462        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
2463        font_features: Some(theme_settings.ui_font.features.clone()),
2464        font_size: Some(font_size.into()),
2465        line_height: Some(line_height.into()),
2466        color: Some(cx.theme().colors().text),
2467        ..Default::default()
2468    });
2469
2470    MarkdownStyle {
2471        base_text_style: text_style.clone(),
2472        syntax: cx.theme().syntax().clone(),
2473        selection_background_color: cx.theme().colors().element_selection_background,
2474        code_block_overflow_x_scroll: true,
2475        table_overflow_x_scroll: true,
2476        heading_level_styles: Some(HeadingLevelStyles {
2477            h1: Some(TextStyleRefinement {
2478                font_size: Some(rems(1.15).into()),
2479                ..Default::default()
2480            }),
2481            h2: Some(TextStyleRefinement {
2482                font_size: Some(rems(1.1).into()),
2483                ..Default::default()
2484            }),
2485            h3: Some(TextStyleRefinement {
2486                font_size: Some(rems(1.05).into()),
2487                ..Default::default()
2488            }),
2489            h4: Some(TextStyleRefinement {
2490                font_size: Some(rems(1.).into()),
2491                ..Default::default()
2492            }),
2493            h5: Some(TextStyleRefinement {
2494                font_size: Some(rems(0.95).into()),
2495                ..Default::default()
2496            }),
2497            h6: Some(TextStyleRefinement {
2498                font_size: Some(rems(0.875).into()),
2499                ..Default::default()
2500            }),
2501        }),
2502        code_block: StyleRefinement {
2503            padding: EdgesRefinement {
2504                top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
2505                left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
2506                right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
2507                bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
2508            },
2509            margin: EdgesRefinement {
2510                top: Some(Length::Definite(Pixels(8.).into())),
2511                left: Some(Length::Definite(Pixels(0.).into())),
2512                right: Some(Length::Definite(Pixels(0.).into())),
2513                bottom: Some(Length::Definite(Pixels(12.).into())),
2514            },
2515            border_style: Some(BorderStyle::Solid),
2516            border_widths: EdgesRefinement {
2517                top: Some(AbsoluteLength::Pixels(Pixels(1.))),
2518                left: Some(AbsoluteLength::Pixels(Pixels(1.))),
2519                right: Some(AbsoluteLength::Pixels(Pixels(1.))),
2520                bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
2521            },
2522            border_color: Some(colors.border_variant),
2523            background: Some(colors.editor_background.into()),
2524            text: Some(TextStyleRefinement {
2525                font_family: Some(theme_settings.buffer_font.family.clone()),
2526                font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
2527                font_features: Some(theme_settings.buffer_font.features.clone()),
2528                font_size: Some(buffer_font_size.into()),
2529                ..Default::default()
2530            }),
2531            ..Default::default()
2532        },
2533        inline_code: TextStyleRefinement {
2534            font_family: Some(theme_settings.buffer_font.family.clone()),
2535            font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
2536            font_features: Some(theme_settings.buffer_font.features.clone()),
2537            font_size: Some(buffer_font_size.into()),
2538            background_color: Some(colors.editor_foreground.opacity(0.08)),
2539            ..Default::default()
2540        },
2541        link: TextStyleRefinement {
2542            background_color: Some(colors.editor_foreground.opacity(0.025)),
2543            underline: Some(UnderlineStyle {
2544                color: Some(colors.text_accent.opacity(0.5)),
2545                thickness: px(1.),
2546                ..Default::default()
2547            }),
2548            ..Default::default()
2549        },
2550        ..Default::default()
2551    }
2552}
2553
2554fn plan_label_markdown_style(
2555    status: &acp::PlanEntryStatus,
2556    window: &Window,
2557    cx: &App,
2558) -> MarkdownStyle {
2559    let default_md_style = default_markdown_style(false, window, cx);
2560
2561    MarkdownStyle {
2562        base_text_style: TextStyle {
2563            color: cx.theme().colors().text_muted,
2564            strikethrough: if matches!(status, acp::PlanEntryStatus::Completed) {
2565                Some(gpui::StrikethroughStyle {
2566                    thickness: px(1.),
2567                    color: Some(cx.theme().colors().text_muted.opacity(0.8)),
2568                })
2569            } else {
2570                None
2571            },
2572            ..default_md_style.base_text_style
2573        },
2574        ..default_md_style
2575    }
2576}