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 anyhow::Result;
   8use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer};
   9use gpui::{
  10    Animation, AnimationExt, App, EdgesRefinement, Empty, Entity, Focusable, ListState,
  11    SharedString, StyleRefinement, Subscription, TextStyleRefinement, Transformation,
  12    UnderlineStyle, Window, div, list, percentage, prelude::*,
  13};
  14use gpui::{FocusHandle, Task};
  15use language::Buffer;
  16use language::language_settings::SoftWrap;
  17use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
  18use project::Project;
  19use settings::Settings as _;
  20use theme::ThemeSettings;
  21use ui::prelude::*;
  22use ui::{Button, Tooltip};
  23use util::{ResultExt, paths};
  24use zed_actions::agent::Chat;
  25
  26use crate::{
  27    AcpServer, AcpThread, AcpThreadEvent, AgentThreadEntryContent, MessageChunk, Role, ThreadEntry,
  28    ToolCall, ToolCallConfirmation, ToolCallContent, ToolCallId, ToolCallStatus,
  29};
  30
  31pub struct AcpThreadView {
  32    agent: Arc<AcpServer>,
  33    thread_state: ThreadState,
  34    // todo! reconsider structure. currently pretty sparse, but easy to clean up if we need to delete entries.
  35    thread_entry_views: Vec<Option<ThreadEntryView>>,
  36    message_editor: Entity<Editor>,
  37    last_error: Option<Entity<Markdown>>,
  38    list_state: ListState,
  39    send_task: Option<Task<Result<()>>>,
  40    auth_task: Option<Task<()>>,
  41}
  42
  43#[derive(Debug)]
  44enum ThreadEntryView {
  45    Diff { editor: Entity<Editor> },
  46}
  47
  48enum ThreadState {
  49    Loading {
  50        _task: Task<()>,
  51    },
  52    Ready {
  53        thread: Entity<AcpThread>,
  54        _subscription: Subscription,
  55    },
  56    LoadError(SharedString),
  57    Unauthenticated,
  58}
  59
  60impl AcpThreadView {
  61    pub fn new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
  62        let message_editor = cx.new(|cx| {
  63            let buffer = cx.new(|cx| Buffer::local("", cx));
  64            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
  65
  66            let mut editor = Editor::new(
  67                editor::EditorMode::AutoHeight {
  68                    min_lines: 4,
  69                    max_lines: None,
  70                },
  71                buffer,
  72                None,
  73                window,
  74                cx,
  75            );
  76            editor.set_placeholder_text("Send a message", cx);
  77            editor.set_soft_wrap();
  78            editor
  79        });
  80
  81        let list_state = ListState::new(
  82            0,
  83            gpui::ListAlignment::Bottom,
  84            px(2048.0),
  85            cx.processor({
  86                move |this: &mut Self, item: usize, window, cx| {
  87                    let Some(entry) = this
  88                        .thread()
  89                        .and_then(|thread| thread.read(cx).entries.get(item))
  90                    else {
  91                        return Empty.into_any();
  92                    };
  93                    this.render_entry(item, entry, window, cx)
  94                }
  95            }),
  96        );
  97
  98        let root_dir = project
  99            .read(cx)
 100            .visible_worktrees(cx)
 101            .next()
 102            .map(|worktree| worktree.read(cx).abs_path())
 103            .unwrap_or_else(|| paths::home_dir().as_path().into());
 104
 105        let cli_path =
 106            Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../gemini-cli/packages/cli");
 107
 108        let child = util::command::new_smol_command("node")
 109            .arg(cli_path)
 110            .arg("--acp")
 111            .current_dir(root_dir)
 112            .stdin(std::process::Stdio::piped())
 113            .stdout(std::process::Stdio::piped())
 114            .stderr(std::process::Stdio::inherit())
 115            .kill_on_drop(true)
 116            .spawn()
 117            .unwrap();
 118
 119        let agent = AcpServer::stdio(child, project, cx);
 120
 121        Self {
 122            thread_state: Self::initial_state(agent.clone(), window, cx),
 123            agent,
 124            message_editor,
 125            thread_entry_views: Vec::new(),
 126            send_task: None,
 127            list_state: list_state,
 128            last_error: None,
 129            auth_task: None,
 130        }
 131    }
 132
 133    fn initial_state(
 134        agent: Arc<AcpServer>,
 135        window: &mut Window,
 136        cx: &mut Context<Self>,
 137    ) -> ThreadState {
 138        let load_task = cx.spawn_in(window, async move |this, cx| {
 139            let result = match agent.initialize().await {
 140                Err(e) => Err(e),
 141                Ok(response) => {
 142                    if !response.is_authenticated {
 143                        this.update(cx, |this, _| {
 144                            this.thread_state = ThreadState::Unauthenticated;
 145                        })
 146                        .ok();
 147                        return;
 148                    }
 149                    agent.clone().create_thread(cx).await
 150                }
 151            };
 152
 153            this.update_in(cx, |this, window, cx| {
 154                match result {
 155                    Ok(thread) => {
 156                        let subscription =
 157                            cx.subscribe_in(&thread, window, Self::handle_thread_event);
 158                        this.list_state
 159                            .splice(0..0, thread.read(cx).entries().len());
 160
 161                        this.thread_state = ThreadState::Ready {
 162                            thread,
 163                            _subscription: subscription,
 164                        };
 165                    }
 166                    Err(e) => {
 167                        dbg!(&e);
 168                        if let Some(exit_status) = agent.exit_status() {
 169                            this.thread_state = ThreadState::LoadError(
 170                                format!(
 171                                    "Gemini exited with status {}",
 172                                    exit_status.code().unwrap_or(-127)
 173                                )
 174                                .into(),
 175                            )
 176                        } else {
 177                            this.thread_state = ThreadState::LoadError(e.to_string().into())
 178                        }
 179                    }
 180                };
 181                cx.notify();
 182            })
 183            .log_err();
 184        });
 185
 186        ThreadState::Loading { _task: load_task }
 187    }
 188
 189    fn thread(&self) -> Option<&Entity<AcpThread>> {
 190        match &self.thread_state {
 191            ThreadState::Ready { thread, .. } => Some(thread),
 192            ThreadState::Loading { .. }
 193            | ThreadState::LoadError(..)
 194            | ThreadState::Unauthenticated => None,
 195        }
 196    }
 197
 198    pub fn title(&self, cx: &App) -> SharedString {
 199        match &self.thread_state {
 200            ThreadState::Ready { thread, .. } => thread.read(cx).title(),
 201            ThreadState::Loading { .. } => "Loading...".into(),
 202            ThreadState::LoadError(_) => "Failed to load".into(),
 203            ThreadState::Unauthenticated => "Not authenticated".into(),
 204        }
 205    }
 206
 207    pub fn cancel(&mut self) {
 208        self.send_task.take();
 209    }
 210
 211    fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
 212        self.last_error.take();
 213        let text = self.message_editor.read(cx).text(cx);
 214        if text.is_empty() {
 215            return;
 216        }
 217        let Some(thread) = self.thread() else { return };
 218
 219        let task = thread.update(cx, |thread, cx| thread.send(&text, cx));
 220
 221        self.send_task = Some(cx.spawn(async move |this, cx| {
 222            let result = task.await;
 223
 224            this.update(cx, |this, cx| {
 225                if let Err(err) = result {
 226                    this.last_error =
 227                        Some(cx.new(|cx| {
 228                            Markdown::new(format!("Error: {err}").into(), None, None, cx)
 229                        }))
 230                }
 231                this.send_task.take();
 232            })
 233        }));
 234
 235        self.message_editor.update(cx, |editor, cx| {
 236            editor.clear(window, cx);
 237        });
 238    }
 239
 240    fn handle_thread_event(
 241        &mut self,
 242        thread: &Entity<AcpThread>,
 243        event: &AcpThreadEvent,
 244        window: &mut Window,
 245        cx: &mut Context<Self>,
 246    ) {
 247        let count = self.list_state.item_count();
 248        match event {
 249            AcpThreadEvent::NewEntry => {
 250                self.sync_thread_entry_view(thread.read(cx).entries.len() - 1, window, cx);
 251                self.list_state.splice(count..count, 1);
 252            }
 253            AcpThreadEvent::EntryUpdated(index) => {
 254                let index = *index;
 255                self.sync_thread_entry_view(index, window, cx);
 256                self.list_state.splice(index..index + 1, 1);
 257            }
 258        }
 259        cx.notify();
 260    }
 261
 262    // todo! should we do this on the fly from render?
 263    fn sync_thread_entry_view(
 264        &mut self,
 265        entry_ix: usize,
 266        window: &mut Window,
 267        cx: &mut Context<Self>,
 268    ) {
 269        let multibuffer = match (
 270            self.entry_diff_multibuffer(entry_ix, cx),
 271            self.thread_entry_views.get(entry_ix),
 272        ) {
 273            (Some(multibuffer), Some(Some(ThreadEntryView::Diff { editor }))) => {
 274                if editor.read(cx).buffer() == &multibuffer {
 275                    // same buffer, all synced up
 276                    return;
 277                }
 278                // new buffer, replace editor
 279                multibuffer
 280            }
 281            (Some(multibuffer), _) => multibuffer,
 282            (None, Some(Some(ThreadEntryView::Diff { .. }))) => {
 283                // no longer displaying a diff, drop editor
 284                self.thread_entry_views[entry_ix] = None;
 285                return;
 286            }
 287            (None, _) => return,
 288        };
 289
 290        let editor = cx.new(|cx| {
 291            let mut editor = Editor::new(
 292                EditorMode::Full {
 293                    scale_ui_elements_with_buffer_font_size: false,
 294                    show_active_line_background: false,
 295                    sized_by_content: true,
 296                },
 297                multibuffer.clone(),
 298                None,
 299                window,
 300                cx,
 301            );
 302            editor.set_show_gutter(false, cx);
 303            editor.disable_inline_diagnostics();
 304            editor.disable_expand_excerpt_buttons(cx);
 305            editor.set_show_vertical_scrollbar(false, cx);
 306            editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
 307            editor.set_soft_wrap_mode(SoftWrap::None, cx);
 308            editor.scroll_manager.set_forbid_vertical_scroll(true);
 309            editor.set_show_indent_guides(false, cx);
 310            editor.set_read_only(true);
 311            editor.set_show_breakpoints(false, cx);
 312            editor.set_show_code_actions(false, cx);
 313            editor.set_show_git_diff_gutter(false, cx);
 314            editor.set_expand_all_diff_hunks(cx);
 315            editor.set_text_style_refinement(TextStyleRefinement {
 316                font_size: Some(
 317                    TextSize::Small
 318                        .rems(cx)
 319                        .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
 320                        .into(),
 321                ),
 322                ..Default::default()
 323            });
 324            editor
 325        });
 326
 327        if entry_ix >= self.thread_entry_views.len() {
 328            self.thread_entry_views
 329                .resize_with(entry_ix + 1, Default::default);
 330        }
 331
 332        self.thread_entry_views[entry_ix] = Some(ThreadEntryView::Diff {
 333            editor: editor.clone(),
 334        });
 335    }
 336
 337    fn entry_diff_multibuffer(&self, entry_ix: usize, cx: &App) -> Option<Entity<MultiBuffer>> {
 338        let entry = self.thread()?.read(cx).entries().get(entry_ix)?;
 339        if let AgentThreadEntryContent::ToolCall(ToolCall {
 340            content: Some(ToolCallContent::Diff { diff }),
 341            ..
 342        }) = &entry.content
 343        {
 344            Some(diff.multibuffer.clone())
 345        } else {
 346            None
 347        }
 348    }
 349
 350    fn authenticate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 351        let agent = self.agent.clone();
 352
 353        self.auth_task = Some(cx.spawn_in(window, async move |this, cx| {
 354            let result = agent.authenticate().await;
 355
 356            this.update_in(cx, |this, window, cx| {
 357                if let Err(err) = result {
 358                    this.last_error =
 359                        Some(cx.new(|cx| {
 360                            Markdown::new(format!("Error: {err}").into(), None, None, cx)
 361                        }))
 362                } else {
 363                    this.thread_state = Self::initial_state(agent, window, cx)
 364                }
 365                this.auth_task.take()
 366            })
 367            .ok();
 368        }));
 369    }
 370
 371    fn authorize_tool_call(
 372        &mut self,
 373        id: ToolCallId,
 374        outcome: acp::ToolCallConfirmationOutcome,
 375        cx: &mut Context<Self>,
 376    ) {
 377        let Some(thread) = self.thread() else {
 378            return;
 379        };
 380        thread.update(cx, |thread, cx| {
 381            thread.authorize_tool_call(id, outcome, cx);
 382        });
 383        cx.notify();
 384    }
 385
 386    fn render_entry(
 387        &self,
 388        index: usize,
 389        entry: &ThreadEntry,
 390        window: &mut Window,
 391        cx: &Context<Self>,
 392    ) -> AnyElement {
 393        match &entry.content {
 394            AgentThreadEntryContent::Message(message) => {
 395                let style = if message.role == Role::User {
 396                    user_message_markdown_style(window, cx)
 397                } else {
 398                    default_markdown_style(window, cx)
 399                };
 400                let message_body = div()
 401                    .children(message.chunks.iter().map(|chunk| match chunk {
 402                        MessageChunk::Text { chunk } => {
 403                            // todo!() open link
 404                            MarkdownElement::new(chunk.clone(), style.clone())
 405                        }
 406                        _ => todo!(),
 407                    }))
 408                    .into_any();
 409
 410                match message.role {
 411                    Role::User => div()
 412                        .p_2()
 413                        .pt_5()
 414                        .child(
 415                            div()
 416                                .text_xs()
 417                                .p_3()
 418                                .bg(cx.theme().colors().editor_background)
 419                                .rounded_lg()
 420                                .shadow_md()
 421                                .border_1()
 422                                .border_color(cx.theme().colors().border)
 423                                .child(message_body),
 424                        )
 425                        .into_any(),
 426                    Role::Assistant => div()
 427                        .text_ui(cx)
 428                        .p_5()
 429                        .pt_2()
 430                        .child(message_body)
 431                        .into_any(),
 432                }
 433            }
 434            AgentThreadEntryContent::ToolCall(tool_call) => div()
 435                .px_2()
 436                .py_4()
 437                .child(self.render_tool_call(index, tool_call, window, cx))
 438                .into_any(),
 439        }
 440    }
 441
 442    fn render_tool_call(
 443        &self,
 444        entry_ix: usize,
 445        tool_call: &ToolCall,
 446        window: &Window,
 447        cx: &Context<Self>,
 448    ) -> Div {
 449        let status_icon = match &tool_call.status {
 450            ToolCallStatus::WaitingForConfirmation { .. } => Empty.into_element().into_any(),
 451            ToolCallStatus::Allowed {
 452                status: acp::ToolCallStatus::Running,
 453                ..
 454            } => Icon::new(IconName::ArrowCircle)
 455                .color(Color::Success)
 456                .size(IconSize::Small)
 457                .with_animation(
 458                    "running",
 459                    Animation::new(Duration::from_secs(2)).repeat(),
 460                    |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
 461                )
 462                .into_any_element(),
 463            ToolCallStatus::Allowed {
 464                status: acp::ToolCallStatus::Finished,
 465                ..
 466            } => Icon::new(IconName::Check)
 467                .color(Color::Success)
 468                .size(IconSize::Small)
 469                .into_any_element(),
 470            ToolCallStatus::Rejected
 471            | ToolCallStatus::Allowed {
 472                status: acp::ToolCallStatus::Error,
 473                ..
 474            } => Icon::new(IconName::X)
 475                .color(Color::Error)
 476                .size(IconSize::Small)
 477                .into_any_element(),
 478        };
 479
 480        let content = match &tool_call.status {
 481            ToolCallStatus::WaitingForConfirmation { confirmation, .. } => {
 482                Some(self.render_tool_call_confirmation(
 483                    entry_ix,
 484                    tool_call.id,
 485                    confirmation,
 486                    tool_call.content.as_ref(),
 487                    window,
 488                    cx,
 489                ))
 490            }
 491            ToolCallStatus::Allowed { .. } => tool_call.content.as_ref().map(|content| {
 492                div()
 493                    .border_color(cx.theme().colors().border)
 494                    .border_t_1()
 495                    .px_2()
 496                    .py_1p5()
 497                    .child(self.render_tool_call_content(entry_ix, content, window, cx))
 498                    .into_any_element()
 499            }),
 500            ToolCallStatus::Rejected => None,
 501        };
 502
 503        v_flex()
 504            .text_xs()
 505            .rounded_md()
 506            .border_1()
 507            .border_color(cx.theme().colors().border)
 508            .bg(cx.theme().colors().editor_background)
 509            .child(
 510                h_flex()
 511                    .px_2()
 512                    .py_1p5()
 513                    .w_full()
 514                    .gap_1p5()
 515                    .child(
 516                        Icon::new(tool_call.icon)
 517                            .size(IconSize::Small)
 518                            .color(Color::Muted),
 519                    )
 520                    // todo! danilo please help
 521                    .child(MarkdownElement::new(
 522                        tool_call.label.clone(),
 523                        default_markdown_style(window, cx),
 524                    ))
 525                    .child(div().w_full())
 526                    .child(status_icon),
 527            )
 528            .children(content)
 529    }
 530
 531    fn render_tool_call_content(
 532        &self,
 533        entry_ix: usize,
 534        content: &ToolCallContent,
 535        window: &Window,
 536        cx: &Context<Self>,
 537    ) -> AnyElement {
 538        match content {
 539            ToolCallContent::Markdown { markdown } => {
 540                MarkdownElement::new(markdown.clone(), default_markdown_style(window, cx))
 541                    .into_any_element()
 542            }
 543            ToolCallContent::Diff { .. } => self.render_diff_editor(entry_ix),
 544        }
 545    }
 546
 547    fn render_tool_call_confirmation(
 548        &self,
 549        entry_ix: usize,
 550        tool_call_id: ToolCallId,
 551        confirmation: &ToolCallConfirmation,
 552        content: Option<&ToolCallContent>,
 553        window: &Window,
 554        cx: &Context<Self>,
 555    ) -> AnyElement {
 556        match confirmation {
 557            ToolCallConfirmation::Edit { description } => {
 558                v_flex()
 559                    .border_color(cx.theme().colors().border)
 560                    .border_t_1()
 561                    .px_2()
 562                    .py_1p5()
 563                    .children(description.clone().map(|description| {
 564                        MarkdownElement::new(description, default_markdown_style(window, cx))
 565                    }))
 566                    .children(content.map(|content| {
 567                        self.render_tool_call_content(entry_ix, content, window, cx)
 568                    }))
 569                    .child(
 570                        h_flex()
 571                            .justify_end()
 572                            .gap_1()
 573                            .child(
 574                                Button::new(
 575                                    ("always_allow", tool_call_id.as_u64()),
 576                                    "Always Allow Edits",
 577                                )
 578                                .icon(IconName::CheckDouble)
 579                                .icon_position(IconPosition::Start)
 580                                .icon_size(IconSize::Small)
 581                                .icon_color(Color::Success)
 582                                .on_click(cx.listener({
 583                                    let id = tool_call_id;
 584                                    move |this, _, _, cx| {
 585                                        this.authorize_tool_call(
 586                                            id,
 587                                            acp::ToolCallConfirmationOutcome::AlwaysAllow,
 588                                            cx,
 589                                        );
 590                                    }
 591                                })),
 592                            )
 593                            .child(
 594                                Button::new(("allow", tool_call_id.as_u64()), "Allow")
 595                                    .icon(IconName::Check)
 596                                    .icon_position(IconPosition::Start)
 597                                    .icon_size(IconSize::Small)
 598                                    .icon_color(Color::Success)
 599                                    .on_click(cx.listener({
 600                                        let id = tool_call_id;
 601                                        move |this, _, _, cx| {
 602                                            this.authorize_tool_call(
 603                                                id,
 604                                                acp::ToolCallConfirmationOutcome::Allow,
 605                                                cx,
 606                                            );
 607                                        }
 608                                    })),
 609                            )
 610                            .child(
 611                                Button::new(("reject", tool_call_id.as_u64()), "Reject")
 612                                    .icon(IconName::X)
 613                                    .icon_position(IconPosition::Start)
 614                                    .icon_size(IconSize::Small)
 615                                    .icon_color(Color::Error)
 616                                    .on_click(cx.listener({
 617                                        let id = tool_call_id;
 618                                        move |this, _, _, cx| {
 619                                            this.authorize_tool_call(
 620                                                id,
 621                                                acp::ToolCallConfirmationOutcome::Reject,
 622                                                cx,
 623                                            );
 624                                        }
 625                                    })),
 626                            ),
 627                    )
 628                    .into_any()
 629            }
 630            ToolCallConfirmation::Execute {
 631                command,
 632                root_command,
 633                description,
 634            } => {
 635                v_flex()
 636                    .border_color(cx.theme().colors().border)
 637                    .border_t_1()
 638                    .px_2()
 639                    .py_1p5()
 640                    // todo! nicer rendering
 641                    .child(command.clone())
 642                    .children(description.clone().map(|description| {
 643                        MarkdownElement::new(description, default_markdown_style(window, cx))
 644                    }))
 645                    .children(content.map(|content| {
 646                        self.render_tool_call_content(entry_ix, content, window, cx)
 647                    }))
 648                    .child(
 649                        h_flex()
 650                            .justify_end()
 651                            .gap_1()
 652                            .child(
 653                                Button::new(
 654                                    ("always_allow", tool_call_id.as_u64()),
 655                                    format!("Always Allow {root_command}"),
 656                                )
 657                                .icon(IconName::CheckDouble)
 658                                .icon_position(IconPosition::Start)
 659                                .icon_size(IconSize::Small)
 660                                .icon_color(Color::Success)
 661                                .on_click(cx.listener({
 662                                    let id = tool_call_id;
 663                                    move |this, _, _, cx| {
 664                                        this.authorize_tool_call(
 665                                            id,
 666                                            acp::ToolCallConfirmationOutcome::AlwaysAllow,
 667                                            cx,
 668                                        );
 669                                    }
 670                                })),
 671                            )
 672                            .child(
 673                                Button::new(("allow", tool_call_id.as_u64()), "Allow")
 674                                    .icon(IconName::Check)
 675                                    .icon_position(IconPosition::Start)
 676                                    .icon_size(IconSize::Small)
 677                                    .icon_color(Color::Success)
 678                                    .on_click(cx.listener({
 679                                        let id = tool_call_id;
 680                                        move |this, _, _, cx| {
 681                                            this.authorize_tool_call(
 682                                                id,
 683                                                acp::ToolCallConfirmationOutcome::Allow,
 684                                                cx,
 685                                            );
 686                                        }
 687                                    })),
 688                            )
 689                            .child(
 690                                Button::new(("reject", tool_call_id.as_u64()), "Reject")
 691                                    .icon(IconName::X)
 692                                    .icon_position(IconPosition::Start)
 693                                    .icon_size(IconSize::Small)
 694                                    .icon_color(Color::Error)
 695                                    .on_click(cx.listener({
 696                                        let id = tool_call_id;
 697                                        move |this, _, _, cx| {
 698                                            this.authorize_tool_call(
 699                                                id,
 700                                                acp::ToolCallConfirmationOutcome::Reject,
 701                                                cx,
 702                                            );
 703                                        }
 704                                    })),
 705                            ),
 706                    )
 707                    .into_any()
 708            }
 709            ToolCallConfirmation::Mcp {
 710                server_name,
 711                tool_name: _,
 712                tool_display_name,
 713                description,
 714            } => {
 715                v_flex()
 716                    .border_color(cx.theme().colors().border)
 717                    .border_t_1()
 718                    .px_2()
 719                    .py_1p5()
 720                    // todo! nicer rendering
 721                    .child(format!("{server_name} - {tool_display_name}"))
 722                    .children(description.clone().map(|description| {
 723                        MarkdownElement::new(description, default_markdown_style(window, cx))
 724                    }))
 725                    .children(content.map(|content| {
 726                        self.render_tool_call_content(entry_ix, content, window, cx)
 727                    }))
 728                    .child(
 729                        h_flex()
 730                            .justify_end()
 731                            .gap_1()
 732                            .child(
 733                                Button::new(
 734                                    ("always_allow_server", tool_call_id.as_u64()),
 735                                    format!("Always Allow {server_name}"),
 736                                )
 737                                .icon(IconName::CheckDouble)
 738                                .icon_position(IconPosition::Start)
 739                                .icon_size(IconSize::Small)
 740                                .icon_color(Color::Success)
 741                                .on_click(cx.listener({
 742                                    let id = tool_call_id;
 743                                    move |this, _, _, cx| {
 744                                        this.authorize_tool_call(
 745                                            id,
 746                                            acp::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
 747                                            cx,
 748                                        );
 749                                    }
 750                                })),
 751                            )
 752                            .child(
 753                                Button::new(
 754                                    ("always_allow_tool", tool_call_id.as_u64()),
 755                                    format!("Always Allow {tool_display_name}"),
 756                                )
 757                                .icon(IconName::CheckDouble)
 758                                .icon_position(IconPosition::Start)
 759                                .icon_size(IconSize::Small)
 760                                .icon_color(Color::Success)
 761                                .on_click(cx.listener({
 762                                    let id = tool_call_id;
 763                                    move |this, _, _, cx| {
 764                                        this.authorize_tool_call(
 765                                            id,
 766                                            acp::ToolCallConfirmationOutcome::AlwaysAllowTool,
 767                                            cx,
 768                                        );
 769                                    }
 770                                })),
 771                            )
 772                            .child(
 773                                Button::new(("allow", tool_call_id.as_u64()), "Allow")
 774                                    .icon(IconName::Check)
 775                                    .icon_position(IconPosition::Start)
 776                                    .icon_size(IconSize::Small)
 777                                    .icon_color(Color::Success)
 778                                    .on_click(cx.listener({
 779                                        let id = tool_call_id;
 780                                        move |this, _, _, cx| {
 781                                            this.authorize_tool_call(
 782                                                id,
 783                                                acp::ToolCallConfirmationOutcome::Allow,
 784                                                cx,
 785                                            );
 786                                        }
 787                                    })),
 788                            )
 789                            .child(
 790                                Button::new(("reject", tool_call_id.as_u64()), "Reject")
 791                                    .icon(IconName::X)
 792                                    .icon_position(IconPosition::Start)
 793                                    .icon_size(IconSize::Small)
 794                                    .icon_color(Color::Error)
 795                                    .on_click(cx.listener({
 796                                        let id = tool_call_id;
 797                                        move |this, _, _, cx| {
 798                                            this.authorize_tool_call(
 799                                                id,
 800                                                acp::ToolCallConfirmationOutcome::Reject,
 801                                                cx,
 802                                            );
 803                                        }
 804                                    })),
 805                            ),
 806                    )
 807                    .into_any()
 808            }
 809            ToolCallConfirmation::Fetch { description, urls } => v_flex()
 810                .border_color(cx.theme().colors().border)
 811                .border_t_1()
 812                .px_2()
 813                .py_1p5()
 814                // todo! nicer rendering
 815                .children(urls.clone())
 816                .children(description.clone().map(|description| {
 817                    MarkdownElement::new(description, default_markdown_style(window, cx))
 818                }))
 819                .children(
 820                    content.map(|content| {
 821                        self.render_tool_call_content(entry_ix, content, window, cx)
 822                    }),
 823                )
 824                .child(
 825                    h_flex()
 826                        .justify_end()
 827                        .gap_1()
 828                        .child(
 829                            Button::new(("always_allow", tool_call_id.as_u64()), "Always Allow")
 830                                .icon(IconName::CheckDouble)
 831                                .icon_position(IconPosition::Start)
 832                                .icon_size(IconSize::Small)
 833                                .icon_color(Color::Success)
 834                                .on_click(cx.listener({
 835                                    let id = tool_call_id;
 836                                    move |this, _, _, cx| {
 837                                        this.authorize_tool_call(
 838                                            id,
 839                                            acp::ToolCallConfirmationOutcome::AlwaysAllow,
 840                                            cx,
 841                                        );
 842                                    }
 843                                })),
 844                        )
 845                        .child(
 846                            Button::new(("allow", tool_call_id.as_u64()), "Allow")
 847                                .icon(IconName::Check)
 848                                .icon_position(IconPosition::Start)
 849                                .icon_size(IconSize::Small)
 850                                .icon_color(Color::Success)
 851                                .on_click(cx.listener({
 852                                    let id = tool_call_id;
 853                                    move |this, _, _, cx| {
 854                                        this.authorize_tool_call(
 855                                            id,
 856                                            acp::ToolCallConfirmationOutcome::Allow,
 857                                            cx,
 858                                        );
 859                                    }
 860                                })),
 861                        )
 862                        .child(
 863                            Button::new(("reject", tool_call_id.as_u64()), "Reject")
 864                                .icon(IconName::X)
 865                                .icon_position(IconPosition::Start)
 866                                .icon_size(IconSize::Small)
 867                                .icon_color(Color::Error)
 868                                .on_click(cx.listener({
 869                                    let id = tool_call_id;
 870                                    move |this, _, _, cx| {
 871                                        this.authorize_tool_call(
 872                                            id,
 873                                            acp::ToolCallConfirmationOutcome::Reject,
 874                                            cx,
 875                                        );
 876                                    }
 877                                })),
 878                        ),
 879                )
 880                .into_any(),
 881            ToolCallConfirmation::Other { description } => v_flex()
 882                .border_color(cx.theme().colors().border)
 883                .border_t_1()
 884                .px_2()
 885                .py_1p5()
 886                // todo! nicer rendering
 887                .child(MarkdownElement::new(
 888                    description.clone(),
 889                    default_markdown_style(window, cx),
 890                ))
 891                .children(
 892                    content.map(|content| {
 893                        self.render_tool_call_content(entry_ix, content, window, cx)
 894                    }),
 895                )
 896                .child(
 897                    h_flex()
 898                        .justify_end()
 899                        .gap_1()
 900                        .child(
 901                            Button::new(("always_allow", tool_call_id.as_u64()), "Always Allow")
 902                                .icon(IconName::CheckDouble)
 903                                .icon_position(IconPosition::Start)
 904                                .icon_size(IconSize::Small)
 905                                .icon_color(Color::Success)
 906                                .on_click(cx.listener({
 907                                    let id = tool_call_id;
 908                                    move |this, _, _, cx| {
 909                                        this.authorize_tool_call(
 910                                            id,
 911                                            acp::ToolCallConfirmationOutcome::AlwaysAllow,
 912                                            cx,
 913                                        );
 914                                    }
 915                                })),
 916                        )
 917                        .child(
 918                            Button::new(("allow", tool_call_id.as_u64()), "Allow")
 919                                .icon(IconName::Check)
 920                                .icon_position(IconPosition::Start)
 921                                .icon_size(IconSize::Small)
 922                                .icon_color(Color::Success)
 923                                .on_click(cx.listener({
 924                                    let id = tool_call_id;
 925                                    move |this, _, _, cx| {
 926                                        this.authorize_tool_call(
 927                                            id,
 928                                            acp::ToolCallConfirmationOutcome::Allow,
 929                                            cx,
 930                                        );
 931                                    }
 932                                })),
 933                        )
 934                        .child(
 935                            Button::new(("reject", tool_call_id.as_u64()), "Reject")
 936                                .icon(IconName::X)
 937                                .icon_position(IconPosition::Start)
 938                                .icon_size(IconSize::Small)
 939                                .icon_color(Color::Error)
 940                                .on_click(cx.listener({
 941                                    let id = tool_call_id;
 942                                    move |this, _, _, cx| {
 943                                        this.authorize_tool_call(
 944                                            id,
 945                                            acp::ToolCallConfirmationOutcome::Reject,
 946                                            cx,
 947                                        );
 948                                    }
 949                                })),
 950                        ),
 951                )
 952                .into_any(),
 953        }
 954    }
 955
 956    fn render_diff_editor(&self, entry_ix: usize) -> AnyElement {
 957        if let Some(Some(ThreadEntryView::Diff { editor })) = self.thread_entry_views.get(entry_ix)
 958        {
 959            editor.clone().into_any_element()
 960        } else {
 961            Empty.into_any()
 962        }
 963    }
 964}
 965
 966impl Focusable for AcpThreadView {
 967    fn focus_handle(&self, cx: &App) -> FocusHandle {
 968        self.message_editor.focus_handle(cx)
 969    }
 970}
 971
 972impl Render for AcpThreadView {
 973    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 974        let text = self.message_editor.read(cx).text(cx);
 975        let is_editor_empty = text.is_empty();
 976        let focus_handle = self.message_editor.focus_handle(cx);
 977
 978        v_flex()
 979            .key_context("MessageEditor")
 980            .on_action(cx.listener(Self::chat))
 981            .h_full()
 982            .child(match &self.thread_state {
 983                ThreadState::Unauthenticated => v_flex()
 984                    .p_2()
 985                    .flex_1()
 986                    .justify_end()
 987                    .child(Label::new("Not authenticated"))
 988                    .child(Button::new("sign-in", "Sign in via Gemini CLI").on_click(
 989                        cx.listener(|this, _, window, cx| this.authenticate(window, cx)),
 990                    )),
 991                ThreadState::Loading { .. } => v_flex()
 992                    .p_2()
 993                    .flex_1()
 994                    .justify_end()
 995                    .child(Label::new("Connecting to Gemini...")),
 996                ThreadState::LoadError(e) => div()
 997                    .p_2()
 998                    .flex_1()
 999                    .justify_end()
1000                    .child(Label::new(format!("Failed to load: {e}")).into_any_element()),
1001                ThreadState::Ready { thread, .. } => v_flex()
1002                    .flex_1()
1003                    .gap_2()
1004                    .pb_2()
1005                    .child(
1006                        list(self.list_state.clone())
1007                            .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
1008                            .flex_grow(),
1009                    )
1010                    .child(div().px_3().children(if self.send_task.is_none() {
1011                        None
1012                    } else {
1013                        Label::new(if thread.read(cx).waiting_for_tool_confirmation() {
1014                            "Waiting for tool confirmation"
1015                        } else {
1016                            "Generating..."
1017                        })
1018                        .color(Color::Muted)
1019                        .size(LabelSize::Small)
1020                        .into()
1021                    })),
1022            })
1023            .when_some(self.last_error.clone(), |el, error| {
1024                el.child(
1025                    div()
1026                        .text_xs()
1027                        .p_2()
1028                        .gap_2()
1029                        .border_t_1()
1030                        .border_color(cx.theme().status().error_border)
1031                        .bg(cx.theme().status().error_background)
1032                        .child(MarkdownElement::new(
1033                            error,
1034                            default_markdown_style(window, cx),
1035                        )),
1036                )
1037            })
1038            .child(
1039                v_flex()
1040                    .bg(cx.theme().colors().editor_background)
1041                    .border_t_1()
1042                    .border_color(cx.theme().colors().border)
1043                    .p_2()
1044                    .gap_2()
1045                    .child(self.message_editor.clone())
1046                    .child(h_flex().justify_end().child(if self.send_task.is_some() {
1047                        IconButton::new("stop-generation", IconName::StopFilled)
1048                            .icon_color(Color::Error)
1049                            .style(ButtonStyle::Tinted(ui::TintColor::Error))
1050                            .tooltip(move |window, cx| {
1051                                Tooltip::for_action(
1052                                    "Stop Generation",
1053                                    &editor::actions::Cancel,
1054                                    window,
1055                                    cx,
1056                                )
1057                            })
1058                            .disabled(is_editor_empty)
1059                            .on_click(cx.listener(|this, _event, _, _| this.cancel()))
1060                    } else {
1061                        IconButton::new("send-message", IconName::Send)
1062                            .icon_color(Color::Accent)
1063                            .style(ButtonStyle::Filled)
1064                            .disabled(is_editor_empty)
1065                            .on_click({
1066                                let focus_handle = focus_handle.clone();
1067                                move |_event, window, cx| {
1068                                    focus_handle.dispatch_action(&Chat, window, cx);
1069                                }
1070                            })
1071                            .when(!is_editor_empty, |button| {
1072                                button.tooltip(move |window, cx| {
1073                                    Tooltip::for_action("Send", &Chat, window, cx)
1074                                })
1075                            })
1076                            .when(is_editor_empty, |button| {
1077                                button.tooltip(Tooltip::text("Type a message to submit"))
1078                            })
1079                    })),
1080            )
1081    }
1082}
1083
1084fn user_message_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
1085    let mut style = default_markdown_style(window, cx);
1086    let mut text_style = window.text_style();
1087    let theme_settings = ThemeSettings::get_global(cx);
1088
1089    let buffer_font = theme_settings.buffer_font.family.clone();
1090    let buffer_font_size = TextSize::Small.rems(cx);
1091
1092    text_style.refine(&TextStyleRefinement {
1093        font_family: Some(buffer_font),
1094        font_size: Some(buffer_font_size.into()),
1095        ..Default::default()
1096    });
1097
1098    style.base_text_style = text_style;
1099    style
1100}
1101
1102fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
1103    let theme_settings = ThemeSettings::get_global(cx);
1104    let colors = cx.theme().colors();
1105    let ui_font_size = TextSize::Default.rems(cx);
1106    let buffer_font_size = TextSize::Small.rems(cx);
1107    let mut text_style = window.text_style();
1108    let line_height = buffer_font_size * 1.75;
1109
1110    text_style.refine(&TextStyleRefinement {
1111        font_family: Some(theme_settings.ui_font.family.clone()),
1112        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
1113        font_features: Some(theme_settings.ui_font.features.clone()),
1114        font_size: Some(ui_font_size.into()),
1115        line_height: Some(line_height.into()),
1116        color: Some(cx.theme().colors().text),
1117        ..Default::default()
1118    });
1119
1120    MarkdownStyle {
1121        base_text_style: text_style.clone(),
1122        syntax: cx.theme().syntax().clone(),
1123        selection_background_color: cx.theme().colors().element_selection_background,
1124        code_block_overflow_x_scroll: true,
1125        table_overflow_x_scroll: true,
1126        heading_level_styles: Some(HeadingLevelStyles {
1127            h1: Some(TextStyleRefinement {
1128                font_size: Some(rems(1.15).into()),
1129                ..Default::default()
1130            }),
1131            h2: Some(TextStyleRefinement {
1132                font_size: Some(rems(1.1).into()),
1133                ..Default::default()
1134            }),
1135            h3: Some(TextStyleRefinement {
1136                font_size: Some(rems(1.05).into()),
1137                ..Default::default()
1138            }),
1139            h4: Some(TextStyleRefinement {
1140                font_size: Some(rems(1.).into()),
1141                ..Default::default()
1142            }),
1143            h5: Some(TextStyleRefinement {
1144                font_size: Some(rems(0.95).into()),
1145                ..Default::default()
1146            }),
1147            h6: Some(TextStyleRefinement {
1148                font_size: Some(rems(0.875).into()),
1149                ..Default::default()
1150            }),
1151        }),
1152        code_block: StyleRefinement {
1153            padding: EdgesRefinement {
1154                top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
1155                left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
1156                right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
1157                bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
1158            },
1159            background: Some(colors.editor_background.into()),
1160            text: Some(TextStyleRefinement {
1161                font_family: Some(theme_settings.buffer_font.family.clone()),
1162                font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
1163                font_features: Some(theme_settings.buffer_font.features.clone()),
1164                font_size: Some(buffer_font_size.into()),
1165                ..Default::default()
1166            }),
1167            ..Default::default()
1168        },
1169        inline_code: TextStyleRefinement {
1170            font_family: Some(theme_settings.buffer_font.family.clone()),
1171            font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
1172            font_features: Some(theme_settings.buffer_font.features.clone()),
1173            font_size: Some(buffer_font_size.into()),
1174            background_color: Some(colors.editor_foreground.opacity(0.08)),
1175            ..Default::default()
1176        },
1177        link: TextStyleRefinement {
1178            background_color: Some(colors.editor_foreground.opacity(0.025)),
1179            underline: Some(UnderlineStyle {
1180                color: Some(colors.text_accent.opacity(0.5)),
1181                thickness: px(1.),
1182                ..Default::default()
1183            }),
1184            ..Default::default()
1185        },
1186        link_callback: Some(Rc::new(move |_url, _cx| {
1187            // todo!()
1188            // if MentionLink::is_valid(url) {
1189            //     let colors = cx.theme().colors();
1190            //     Some(TextStyleRefinement {
1191            //         background_color: Some(colors.element_background),
1192            //         ..Default::default()
1193            //     })
1194            // } else {
1195            None
1196            // }
1197        })),
1198        ..Default::default()
1199    }
1200}