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