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, AssistantMessage,
  27    AssistantMessageChunk, Diff, ThreadEntry, ThreadStatus, ToolCall, ToolCallConfirmation,
  28    ToolCallContent, ToolCallId, ToolCallStatus, UserMessageChunk,
  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::UserMessage(message) => {
 396                let style = user_message_markdown_style(window, cx);
 397                let message_body = div().children(message.chunks.iter().map(|chunk| match chunk {
 398                    UserMessageChunk::Text { chunk } => {
 399                        // todo!() open link
 400                        MarkdownElement::new(chunk.clone(), style.clone())
 401                    }
 402                    _ => todo!(),
 403                }));
 404                div()
 405                    .p_2()
 406                    .pt_5()
 407                    .child(
 408                        div()
 409                            .text_xs()
 410                            .p_3()
 411                            .bg(cx.theme().colors().editor_background)
 412                            .rounded_lg()
 413                            .shadow_md()
 414                            .border_1()
 415                            .border_color(cx.theme().colors().border)
 416                            .child(message_body),
 417                    )
 418                    .into_any()
 419            }
 420            AgentThreadEntryContent::AssistantMessage(AssistantMessage { chunks }) => {
 421                let style = default_markdown_style(window, cx);
 422                let message_body = div()
 423                    .children(chunks.iter().map(|chunk| match chunk {
 424                        AssistantMessageChunk::Text { chunk } => {
 425                            // todo!() open link
 426                            MarkdownElement::new(chunk.clone(), style.clone()).into_any_element()
 427                        }
 428                        AssistantMessageChunk::Thought { chunk } => {
 429                            self.render_thinking_block(chunk.clone(), window, cx)
 430                        }
 431                    }))
 432                    .into_any();
 433
 434                div()
 435                    .text_ui(cx)
 436                    .p_5()
 437                    .pt_2()
 438                    .child(message_body)
 439                    .into_any()
 440            }
 441            AgentThreadEntryContent::ToolCall(tool_call) => div()
 442                .px_2()
 443                .py_4()
 444                .child(self.render_tool_call(index, tool_call, window, cx))
 445                .into_any(),
 446        }
 447    }
 448
 449    fn render_thinking_block(
 450        &self,
 451        chunk: Entity<Markdown>,
 452        window: &Window,
 453        cx: &Context<Self>,
 454    ) -> AnyElement {
 455        v_flex()
 456            .mt_neg_2()
 457            .mb_1p5()
 458            .child(
 459                h_flex().group("disclosure-header").justify_between().child(
 460                    h_flex()
 461                        .gap_1p5()
 462                        .child(
 463                            Icon::new(IconName::LightBulb)
 464                                .size(IconSize::XSmall)
 465                                .color(Color::Muted),
 466                        )
 467                        .child(Label::new("Thinking").size(LabelSize::Small)),
 468                ),
 469            )
 470            .child(div().relative().rounded_b_lg().mt_2().pl_4().child(
 471                div().max_h_20().text_ui_sm(cx).overflow_hidden().child(
 472                    // todo! url click
 473                    MarkdownElement::new(chunk, default_markdown_style(window, cx)),
 474                    // .on_url_click({
 475                    //     let workspace = self.workspace.clone();
 476                    //     move |text, window, cx| {
 477                    //         open_markdown_link(text, workspace.clone(), window, cx);
 478                    //     }
 479                    // }),
 480                ),
 481            ))
 482            .into_any_element()
 483    }
 484
 485    fn render_tool_call(
 486        &self,
 487        entry_ix: usize,
 488        tool_call: &ToolCall,
 489        window: &Window,
 490        cx: &Context<Self>,
 491    ) -> Div {
 492        let status_icon = match &tool_call.status {
 493            ToolCallStatus::WaitingForConfirmation { .. } => Empty.into_element().into_any(),
 494            ToolCallStatus::Allowed {
 495                status: acp::ToolCallStatus::Running,
 496                ..
 497            } => Icon::new(IconName::ArrowCircle)
 498                .color(Color::Success)
 499                .size(IconSize::Small)
 500                .with_animation(
 501                    "running",
 502                    Animation::new(Duration::from_secs(2)).repeat(),
 503                    |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
 504                )
 505                .into_any_element(),
 506            ToolCallStatus::Allowed {
 507                status: acp::ToolCallStatus::Finished,
 508                ..
 509            } => Icon::new(IconName::Check)
 510                .color(Color::Success)
 511                .size(IconSize::Small)
 512                .into_any_element(),
 513            ToolCallStatus::Rejected
 514            | ToolCallStatus::Canceled
 515            | ToolCallStatus::Allowed {
 516                status: acp::ToolCallStatus::Error,
 517                ..
 518            } => Icon::new(IconName::X)
 519                .color(Color::Error)
 520                .size(IconSize::Small)
 521                .into_any_element(),
 522        };
 523
 524        let content = match &tool_call.status {
 525            ToolCallStatus::WaitingForConfirmation { confirmation, .. } => {
 526                Some(self.render_tool_call_confirmation(
 527                    entry_ix,
 528                    tool_call.id,
 529                    confirmation,
 530                    tool_call.content.as_ref(),
 531                    window,
 532                    cx,
 533                ))
 534            }
 535            ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => {
 536                tool_call.content.as_ref().map(|content| {
 537                    div()
 538                        .border_color(cx.theme().colors().border)
 539                        .border_t_1()
 540                        .px_2()
 541                        .py_1p5()
 542                        .child(self.render_tool_call_content(entry_ix, content, window, cx))
 543                        .into_any_element()
 544                })
 545            }
 546            ToolCallStatus::Rejected => None,
 547        };
 548
 549        v_flex()
 550            .text_xs()
 551            .rounded_md()
 552            .border_1()
 553            .border_color(cx.theme().colors().border)
 554            .bg(cx.theme().colors().editor_background)
 555            .child(
 556                h_flex()
 557                    .px_2()
 558                    .py_1p5()
 559                    .w_full()
 560                    .gap_1p5()
 561                    .child(
 562                        Icon::new(tool_call.icon)
 563                            .size(IconSize::Small)
 564                            .color(Color::Muted),
 565                    )
 566                    // todo! danilo please help
 567                    .child(MarkdownElement::new(
 568                        tool_call.label.clone(),
 569                        default_markdown_style(window, cx),
 570                    ))
 571                    .child(div().w_full())
 572                    .child(status_icon),
 573            )
 574            .children(content)
 575    }
 576
 577    fn render_tool_call_content(
 578        &self,
 579        entry_ix: usize,
 580        content: &ToolCallContent,
 581        window: &Window,
 582        cx: &Context<Self>,
 583    ) -> AnyElement {
 584        match content {
 585            ToolCallContent::Markdown { markdown } => {
 586                MarkdownElement::new(markdown.clone(), default_markdown_style(window, cx))
 587                    .into_any_element()
 588            }
 589            ToolCallContent::Diff {
 590                diff: Diff { path, .. },
 591                ..
 592            } => self.render_diff_editor(entry_ix, path),
 593        }
 594    }
 595
 596    fn render_tool_call_confirmation(
 597        &self,
 598        entry_ix: usize,
 599        tool_call_id: ToolCallId,
 600        confirmation: &ToolCallConfirmation,
 601        content: Option<&ToolCallContent>,
 602        window: &Window,
 603        cx: &Context<Self>,
 604    ) -> AnyElement {
 605        match confirmation {
 606            ToolCallConfirmation::Edit { description } => {
 607                v_flex()
 608                    .border_color(cx.theme().colors().border)
 609                    .border_t_1()
 610                    .px_2()
 611                    .py_1p5()
 612                    .children(description.clone().map(|description| {
 613                        MarkdownElement::new(description, default_markdown_style(window, cx))
 614                    }))
 615                    .children(content.map(|content| {
 616                        self.render_tool_call_content(entry_ix, content, window, cx)
 617                    }))
 618                    .child(
 619                        h_flex()
 620                            .justify_end()
 621                            .gap_1()
 622                            .child(
 623                                Button::new(
 624                                    ("always_allow", tool_call_id.as_u64()),
 625                                    "Always Allow Edits",
 626                                )
 627                                .icon(IconName::CheckDouble)
 628                                .icon_position(IconPosition::Start)
 629                                .icon_size(IconSize::Small)
 630                                .icon_color(Color::Success)
 631                                .on_click(cx.listener({
 632                                    let id = tool_call_id;
 633                                    move |this, _, _, cx| {
 634                                        this.authorize_tool_call(
 635                                            id,
 636                                            acp::ToolCallConfirmationOutcome::AlwaysAllow,
 637                                            cx,
 638                                        );
 639                                    }
 640                                })),
 641                            )
 642                            .child(
 643                                Button::new(("allow", tool_call_id.as_u64()), "Allow")
 644                                    .icon(IconName::Check)
 645                                    .icon_position(IconPosition::Start)
 646                                    .icon_size(IconSize::Small)
 647                                    .icon_color(Color::Success)
 648                                    .on_click(cx.listener({
 649                                        let id = tool_call_id;
 650                                        move |this, _, _, cx| {
 651                                            this.authorize_tool_call(
 652                                                id,
 653                                                acp::ToolCallConfirmationOutcome::Allow,
 654                                                cx,
 655                                            );
 656                                        }
 657                                    })),
 658                            )
 659                            .child(
 660                                Button::new(("reject", tool_call_id.as_u64()), "Reject")
 661                                    .icon(IconName::X)
 662                                    .icon_position(IconPosition::Start)
 663                                    .icon_size(IconSize::Small)
 664                                    .icon_color(Color::Error)
 665                                    .on_click(cx.listener({
 666                                        let id = tool_call_id;
 667                                        move |this, _, _, cx| {
 668                                            this.authorize_tool_call(
 669                                                id,
 670                                                acp::ToolCallConfirmationOutcome::Reject,
 671                                                cx,
 672                                            );
 673                                        }
 674                                    })),
 675                            ),
 676                    )
 677                    .into_any()
 678            }
 679            ToolCallConfirmation::Execute {
 680                command,
 681                root_command,
 682                description,
 683            } => {
 684                v_flex()
 685                    .border_color(cx.theme().colors().border)
 686                    .border_t_1()
 687                    .px_2()
 688                    .py_1p5()
 689                    // todo! nicer rendering
 690                    .child(command.clone())
 691                    .children(description.clone().map(|description| {
 692                        MarkdownElement::new(description, default_markdown_style(window, cx))
 693                    }))
 694                    .children(content.map(|content| {
 695                        self.render_tool_call_content(entry_ix, content, window, cx)
 696                    }))
 697                    .child(
 698                        h_flex()
 699                            .justify_end()
 700                            .gap_1()
 701                            .child(
 702                                Button::new(
 703                                    ("always_allow", tool_call_id.as_u64()),
 704                                    format!("Always Allow {root_command}"),
 705                                )
 706                                .icon(IconName::CheckDouble)
 707                                .icon_position(IconPosition::Start)
 708                                .icon_size(IconSize::Small)
 709                                .icon_color(Color::Success)
 710                                .on_click(cx.listener({
 711                                    let id = tool_call_id;
 712                                    move |this, _, _, cx| {
 713                                        this.authorize_tool_call(
 714                                            id,
 715                                            acp::ToolCallConfirmationOutcome::AlwaysAllow,
 716                                            cx,
 717                                        );
 718                                    }
 719                                })),
 720                            )
 721                            .child(
 722                                Button::new(("allow", tool_call_id.as_u64()), "Allow")
 723                                    .icon(IconName::Check)
 724                                    .icon_position(IconPosition::Start)
 725                                    .icon_size(IconSize::Small)
 726                                    .icon_color(Color::Success)
 727                                    .on_click(cx.listener({
 728                                        let id = tool_call_id;
 729                                        move |this, _, _, cx| {
 730                                            this.authorize_tool_call(
 731                                                id,
 732                                                acp::ToolCallConfirmationOutcome::Allow,
 733                                                cx,
 734                                            );
 735                                        }
 736                                    })),
 737                            )
 738                            .child(
 739                                Button::new(("reject", tool_call_id.as_u64()), "Reject")
 740                                    .icon(IconName::X)
 741                                    .icon_position(IconPosition::Start)
 742                                    .icon_size(IconSize::Small)
 743                                    .icon_color(Color::Error)
 744                                    .on_click(cx.listener({
 745                                        let id = tool_call_id;
 746                                        move |this, _, _, cx| {
 747                                            this.authorize_tool_call(
 748                                                id,
 749                                                acp::ToolCallConfirmationOutcome::Reject,
 750                                                cx,
 751                                            );
 752                                        }
 753                                    })),
 754                            ),
 755                    )
 756                    .into_any()
 757            }
 758            ToolCallConfirmation::Mcp {
 759                server_name,
 760                tool_name: _,
 761                tool_display_name,
 762                description,
 763            } => {
 764                v_flex()
 765                    .border_color(cx.theme().colors().border)
 766                    .border_t_1()
 767                    .px_2()
 768                    .py_1p5()
 769                    // todo! nicer rendering
 770                    .child(format!("{server_name} - {tool_display_name}"))
 771                    .children(description.clone().map(|description| {
 772                        MarkdownElement::new(description, default_markdown_style(window, cx))
 773                    }))
 774                    .children(content.map(|content| {
 775                        self.render_tool_call_content(entry_ix, content, window, cx)
 776                    }))
 777                    .child(
 778                        h_flex()
 779                            .justify_end()
 780                            .gap_1()
 781                            .child(
 782                                Button::new(
 783                                    ("always_allow_server", tool_call_id.as_u64()),
 784                                    format!("Always Allow {server_name}"),
 785                                )
 786                                .icon(IconName::CheckDouble)
 787                                .icon_position(IconPosition::Start)
 788                                .icon_size(IconSize::Small)
 789                                .icon_color(Color::Success)
 790                                .on_click(cx.listener({
 791                                    let id = tool_call_id;
 792                                    move |this, _, _, cx| {
 793                                        this.authorize_tool_call(
 794                                            id,
 795                                            acp::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
 796                                            cx,
 797                                        );
 798                                    }
 799                                })),
 800                            )
 801                            .child(
 802                                Button::new(
 803                                    ("always_allow_tool", tool_call_id.as_u64()),
 804                                    format!("Always Allow {tool_display_name}"),
 805                                )
 806                                .icon(IconName::CheckDouble)
 807                                .icon_position(IconPosition::Start)
 808                                .icon_size(IconSize::Small)
 809                                .icon_color(Color::Success)
 810                                .on_click(cx.listener({
 811                                    let id = tool_call_id;
 812                                    move |this, _, _, cx| {
 813                                        this.authorize_tool_call(
 814                                            id,
 815                                            acp::ToolCallConfirmationOutcome::AlwaysAllowTool,
 816                                            cx,
 817                                        );
 818                                    }
 819                                })),
 820                            )
 821                            .child(
 822                                Button::new(("allow", tool_call_id.as_u64()), "Allow")
 823                                    .icon(IconName::Check)
 824                                    .icon_position(IconPosition::Start)
 825                                    .icon_size(IconSize::Small)
 826                                    .icon_color(Color::Success)
 827                                    .on_click(cx.listener({
 828                                        let id = tool_call_id;
 829                                        move |this, _, _, cx| {
 830                                            this.authorize_tool_call(
 831                                                id,
 832                                                acp::ToolCallConfirmationOutcome::Allow,
 833                                                cx,
 834                                            );
 835                                        }
 836                                    })),
 837                            )
 838                            .child(
 839                                Button::new(("reject", tool_call_id.as_u64()), "Reject")
 840                                    .icon(IconName::X)
 841                                    .icon_position(IconPosition::Start)
 842                                    .icon_size(IconSize::Small)
 843                                    .icon_color(Color::Error)
 844                                    .on_click(cx.listener({
 845                                        let id = tool_call_id;
 846                                        move |this, _, _, cx| {
 847                                            this.authorize_tool_call(
 848                                                id,
 849                                                acp::ToolCallConfirmationOutcome::Reject,
 850                                                cx,
 851                                            );
 852                                        }
 853                                    })),
 854                            ),
 855                    )
 856                    .into_any()
 857            }
 858            ToolCallConfirmation::Fetch { description, urls } => v_flex()
 859                .border_color(cx.theme().colors().border)
 860                .border_t_1()
 861                .px_2()
 862                .py_1p5()
 863                // todo! nicer rendering
 864                .children(urls.clone())
 865                .children(description.clone().map(|description| {
 866                    MarkdownElement::new(description, default_markdown_style(window, cx))
 867                }))
 868                .children(
 869                    content.map(|content| {
 870                        self.render_tool_call_content(entry_ix, content, window, cx)
 871                    }),
 872                )
 873                .child(
 874                    h_flex()
 875                        .justify_end()
 876                        .gap_1()
 877                        .child(
 878                            Button::new(("always_allow", tool_call_id.as_u64()), "Always Allow")
 879                                .icon(IconName::CheckDouble)
 880                                .icon_position(IconPosition::Start)
 881                                .icon_size(IconSize::Small)
 882                                .icon_color(Color::Success)
 883                                .on_click(cx.listener({
 884                                    let id = tool_call_id;
 885                                    move |this, _, _, cx| {
 886                                        this.authorize_tool_call(
 887                                            id,
 888                                            acp::ToolCallConfirmationOutcome::AlwaysAllow,
 889                                            cx,
 890                                        );
 891                                    }
 892                                })),
 893                        )
 894                        .child(
 895                            Button::new(("allow", tool_call_id.as_u64()), "Allow")
 896                                .icon(IconName::Check)
 897                                .icon_position(IconPosition::Start)
 898                                .icon_size(IconSize::Small)
 899                                .icon_color(Color::Success)
 900                                .on_click(cx.listener({
 901                                    let id = tool_call_id;
 902                                    move |this, _, _, cx| {
 903                                        this.authorize_tool_call(
 904                                            id,
 905                                            acp::ToolCallConfirmationOutcome::Allow,
 906                                            cx,
 907                                        );
 908                                    }
 909                                })),
 910                        )
 911                        .child(
 912                            Button::new(("reject", tool_call_id.as_u64()), "Reject")
 913                                .icon(IconName::X)
 914                                .icon_position(IconPosition::Start)
 915                                .icon_size(IconSize::Small)
 916                                .icon_color(Color::Error)
 917                                .on_click(cx.listener({
 918                                    let id = tool_call_id;
 919                                    move |this, _, _, cx| {
 920                                        this.authorize_tool_call(
 921                                            id,
 922                                            acp::ToolCallConfirmationOutcome::Reject,
 923                                            cx,
 924                                        );
 925                                    }
 926                                })),
 927                        ),
 928                )
 929                .into_any(),
 930            ToolCallConfirmation::Other { description } => v_flex()
 931                .border_color(cx.theme().colors().border)
 932                .border_t_1()
 933                .px_2()
 934                .py_1p5()
 935                // todo! nicer rendering
 936                .child(MarkdownElement::new(
 937                    description.clone(),
 938                    default_markdown_style(window, cx),
 939                ))
 940                .children(
 941                    content.map(|content| {
 942                        self.render_tool_call_content(entry_ix, content, window, cx)
 943                    }),
 944                )
 945                .child(
 946                    h_flex()
 947                        .justify_end()
 948                        .gap_1()
 949                        .child(
 950                            Button::new(("always_allow", tool_call_id.as_u64()), "Always Allow")
 951                                .icon(IconName::CheckDouble)
 952                                .icon_position(IconPosition::Start)
 953                                .icon_size(IconSize::Small)
 954                                .icon_color(Color::Success)
 955                                .on_click(cx.listener({
 956                                    let id = tool_call_id;
 957                                    move |this, _, _, cx| {
 958                                        this.authorize_tool_call(
 959                                            id,
 960                                            acp::ToolCallConfirmationOutcome::AlwaysAllow,
 961                                            cx,
 962                                        );
 963                                    }
 964                                })),
 965                        )
 966                        .child(
 967                            Button::new(("allow", tool_call_id.as_u64()), "Allow")
 968                                .icon(IconName::Check)
 969                                .icon_position(IconPosition::Start)
 970                                .icon_size(IconSize::Small)
 971                                .icon_color(Color::Success)
 972                                .on_click(cx.listener({
 973                                    let id = tool_call_id;
 974                                    move |this, _, _, cx| {
 975                                        this.authorize_tool_call(
 976                                            id,
 977                                            acp::ToolCallConfirmationOutcome::Allow,
 978                                            cx,
 979                                        );
 980                                    }
 981                                })),
 982                        )
 983                        .child(
 984                            Button::new(("reject", tool_call_id.as_u64()), "Reject")
 985                                .icon(IconName::X)
 986                                .icon_position(IconPosition::Start)
 987                                .icon_size(IconSize::Small)
 988                                .icon_color(Color::Error)
 989                                .on_click(cx.listener({
 990                                    let id = tool_call_id;
 991                                    move |this, _, _, cx| {
 992                                        this.authorize_tool_call(
 993                                            id,
 994                                            acp::ToolCallConfirmationOutcome::Reject,
 995                                            cx,
 996                                        );
 997                                    }
 998                                })),
 999                        ),
1000                )
1001                .into_any(),
1002        }
1003    }
1004
1005    fn render_diff_editor(&self, entry_ix: usize, path: &Path) -> AnyElement {
1006        v_flex()
1007            .h_full()
1008            .child(path.to_string_lossy().to_string())
1009            .child(
1010                if let Some(Some(ThreadEntryView::Diff { editor })) =
1011                    self.thread_entry_views.get(entry_ix)
1012                {
1013                    editor.clone().into_any_element()
1014                } else {
1015                    Empty.into_any()
1016                },
1017            )
1018            .into_any()
1019    }
1020}
1021
1022impl Focusable for AcpThreadView {
1023    fn focus_handle(&self, cx: &App) -> FocusHandle {
1024        self.message_editor.focus_handle(cx)
1025    }
1026}
1027
1028impl Render for AcpThreadView {
1029    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1030        let text = self.message_editor.read(cx).text(cx);
1031        let is_editor_empty = text.is_empty();
1032        let focus_handle = self.message_editor.focus_handle(cx);
1033
1034        v_flex()
1035            .key_context("MessageEditor")
1036            .on_action(cx.listener(Self::chat))
1037            .h_full()
1038            .child(match &self.thread_state {
1039                ThreadState::Unauthenticated => v_flex()
1040                    .p_2()
1041                    .flex_1()
1042                    .justify_end()
1043                    .child(Label::new("Not authenticated"))
1044                    .child(Button::new("sign-in", "Sign in via Gemini CLI").on_click(
1045                        cx.listener(|this, _, window, cx| this.authenticate(window, cx)),
1046                    )),
1047                ThreadState::Loading { .. } => v_flex()
1048                    .p_2()
1049                    .flex_1()
1050                    .justify_end()
1051                    .child(Label::new("Connecting to Gemini...")),
1052                ThreadState::LoadError(e) => div()
1053                    .p_2()
1054                    .flex_1()
1055                    .justify_end()
1056                    .child(Label::new(format!("Failed to load: {e}")).into_any_element()),
1057                ThreadState::Ready { thread, .. } => v_flex()
1058                    .flex_1()
1059                    .gap_2()
1060                    .pb_2()
1061                    .child(
1062                        list(self.list_state.clone())
1063                            .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
1064                            .flex_grow(),
1065                    )
1066                    .child(
1067                        div().px_3().children(match thread.read(cx).status() {
1068                            ThreadStatus::Idle => None,
1069                            ThreadStatus::WaitingForToolConfirmation => {
1070                                Label::new("Waiting for tool confirmation")
1071                                    .color(Color::Muted)
1072                                    .size(LabelSize::Small)
1073                                    .into()
1074                            }
1075                            ThreadStatus::Generating => Label::new("Generating...")
1076                                .color(Color::Muted)
1077                                .size(LabelSize::Small)
1078                                .into(),
1079                        }),
1080                    ),
1081            })
1082            .when_some(self.last_error.clone(), |el, error| {
1083                el.child(
1084                    div()
1085                        .text_xs()
1086                        .p_2()
1087                        .gap_2()
1088                        .border_t_1()
1089                        .border_color(cx.theme().status().error_border)
1090                        .bg(cx.theme().status().error_background)
1091                        .child(MarkdownElement::new(
1092                            error,
1093                            default_markdown_style(window, cx),
1094                        )),
1095                )
1096            })
1097            .child(
1098                v_flex()
1099                    .bg(cx.theme().colors().editor_background)
1100                    .border_t_1()
1101                    .border_color(cx.theme().colors().border)
1102                    .p_2()
1103                    .gap_2()
1104                    .child(self.message_editor.clone())
1105                    .child({
1106                        let thread = self.thread();
1107
1108                        h_flex().justify_end().child(
1109                            if thread.map_or(true, |thread| {
1110                                thread.read(cx).status() == ThreadStatus::Idle
1111                            }) {
1112                                IconButton::new("send-message", IconName::Send)
1113                                    .icon_color(Color::Accent)
1114                                    .style(ButtonStyle::Filled)
1115                                    .disabled(thread.is_none() || is_editor_empty)
1116                                    .on_click({
1117                                        let focus_handle = focus_handle.clone();
1118                                        move |_event, window, cx| {
1119                                            focus_handle.dispatch_action(&Chat, window, cx);
1120                                        }
1121                                    })
1122                                    .when(!is_editor_empty, |button| {
1123                                        button.tooltip(move |window, cx| {
1124                                            Tooltip::for_action("Send", &Chat, window, cx)
1125                                        })
1126                                    })
1127                                    .when(is_editor_empty, |button| {
1128                                        button.tooltip(Tooltip::text("Type a message to submit"))
1129                                    })
1130                            } else {
1131                                IconButton::new("stop-generation", IconName::StopFilled)
1132                                    .icon_color(Color::Error)
1133                                    .style(ButtonStyle::Tinted(ui::TintColor::Error))
1134                                    .tooltip(move |window, cx| {
1135                                        Tooltip::for_action(
1136                                            "Stop Generation",
1137                                            &editor::actions::Cancel,
1138                                            window,
1139                                            cx,
1140                                        )
1141                                    })
1142                                    .on_click(cx.listener(|this, _event, _, cx| this.cancel(cx)))
1143                            },
1144                        )
1145                    }),
1146            )
1147    }
1148}
1149
1150fn user_message_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
1151    let mut style = default_markdown_style(window, cx);
1152    let mut text_style = window.text_style();
1153    let theme_settings = ThemeSettings::get_global(cx);
1154
1155    let buffer_font = theme_settings.buffer_font.family.clone();
1156    let buffer_font_size = TextSize::Small.rems(cx);
1157
1158    text_style.refine(&TextStyleRefinement {
1159        font_family: Some(buffer_font),
1160        font_size: Some(buffer_font_size.into()),
1161        ..Default::default()
1162    });
1163
1164    style.base_text_style = text_style;
1165    style
1166}
1167
1168fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
1169    let theme_settings = ThemeSettings::get_global(cx);
1170    let colors = cx.theme().colors();
1171    let ui_font_size = TextSize::Default.rems(cx);
1172    let buffer_font_size = TextSize::Small.rems(cx);
1173    let mut text_style = window.text_style();
1174    let line_height = buffer_font_size * 1.75;
1175
1176    text_style.refine(&TextStyleRefinement {
1177        font_family: Some(theme_settings.ui_font.family.clone()),
1178        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
1179        font_features: Some(theme_settings.ui_font.features.clone()),
1180        font_size: Some(ui_font_size.into()),
1181        line_height: Some(line_height.into()),
1182        color: Some(cx.theme().colors().text),
1183        ..Default::default()
1184    });
1185
1186    MarkdownStyle {
1187        base_text_style: text_style.clone(),
1188        syntax: cx.theme().syntax().clone(),
1189        selection_background_color: cx.theme().colors().element_selection_background,
1190        code_block_overflow_x_scroll: true,
1191        table_overflow_x_scroll: true,
1192        heading_level_styles: Some(HeadingLevelStyles {
1193            h1: Some(TextStyleRefinement {
1194                font_size: Some(rems(1.15).into()),
1195                ..Default::default()
1196            }),
1197            h2: Some(TextStyleRefinement {
1198                font_size: Some(rems(1.1).into()),
1199                ..Default::default()
1200            }),
1201            h3: Some(TextStyleRefinement {
1202                font_size: Some(rems(1.05).into()),
1203                ..Default::default()
1204            }),
1205            h4: Some(TextStyleRefinement {
1206                font_size: Some(rems(1.).into()),
1207                ..Default::default()
1208            }),
1209            h5: Some(TextStyleRefinement {
1210                font_size: Some(rems(0.95).into()),
1211                ..Default::default()
1212            }),
1213            h6: Some(TextStyleRefinement {
1214                font_size: Some(rems(0.875).into()),
1215                ..Default::default()
1216            }),
1217        }),
1218        code_block: StyleRefinement {
1219            padding: EdgesRefinement {
1220                top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
1221                left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
1222                right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
1223                bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
1224            },
1225            background: Some(colors.editor_background.into()),
1226            text: Some(TextStyleRefinement {
1227                font_family: Some(theme_settings.buffer_font.family.clone()),
1228                font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
1229                font_features: Some(theme_settings.buffer_font.features.clone()),
1230                font_size: Some(buffer_font_size.into()),
1231                ..Default::default()
1232            }),
1233            ..Default::default()
1234        },
1235        inline_code: TextStyleRefinement {
1236            font_family: Some(theme_settings.buffer_font.family.clone()),
1237            font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
1238            font_features: Some(theme_settings.buffer_font.features.clone()),
1239            font_size: Some(buffer_font_size.into()),
1240            background_color: Some(colors.editor_foreground.opacity(0.08)),
1241            ..Default::default()
1242        },
1243        link: TextStyleRefinement {
1244            background_color: Some(colors.editor_foreground.opacity(0.025)),
1245            underline: Some(UnderlineStyle {
1246                color: Some(colors.text_accent.opacity(0.5)),
1247                thickness: px(1.),
1248                ..Default::default()
1249            }),
1250            ..Default::default()
1251        },
1252        link_callback: Some(Rc::new(move |_url, _cx| {
1253            // todo!()
1254            // if MentionLink::is_valid(url) {
1255            //     let colors = cx.theme().colors();
1256            //     Some(TextStyleRefinement {
1257            //         background_color: Some(colors.element_background),
1258            //         ..Default::default()
1259            //     })
1260            // } else {
1261            None
1262            // }
1263        })),
1264        ..Default::default()
1265    }
1266}