thread_view.rs

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