thread_view.rs

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