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