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