thread_view.rs

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