thread_view.rs

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