thread_view.rs

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