thread_view.rs

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