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