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