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