thread_view.rs

   1use acp_thread::{
   2    AcpThread, AcpThreadEvent, AcpThreadMetadata, AgentThreadEntry, AssistantMessage,
   3    AssistantMessageChunk, LoadError, MentionUri, ThreadStatus, ToolCall, ToolCallContent,
   4    ToolCallStatus, UserMessageId,
   5};
   6use acp_thread::{AgentConnection, Plan};
   7use action_log::ActionLog;
   8use agent::{TextThreadStore, ThreadStore};
   9use agent_client_protocol::{self as acp};
  10use agent_servers::AgentServer;
  11use agent_settings::{AgentSettings, CompletionMode, NotifyWhenAgentWaiting};
  12use anyhow::bail;
  13use audio::{Audio, Sound};
  14use buffer_diff::BufferDiff;
  15use client::zed_urls;
  16use collections::{HashMap, HashSet};
  17use editor::scroll::Autoscroll;
  18use editor::{Editor, EditorMode, MultiBuffer, PathKey, SelectionEffects};
  19use file_icons::FileIcons;
  20use gpui::{
  21    Action, Animation, AnimationExt, App, BorderStyle, ClickEvent, ClipboardItem, EdgesRefinement,
  22    Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton,
  23    PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle,
  24    TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div,
  25    linear_color_stop, linear_gradient, list, percentage, point, prelude::*, pulsating_between,
  26};
  27use language::Buffer;
  28use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
  29use project::Project;
  30use prompt_store::PromptId;
  31use rope::Point;
  32use settings::{Settings as _, SettingsStore};
  33use std::{collections::BTreeMap, process::ExitStatus, rc::Rc, time::Duration};
  34use text::Anchor;
  35use theme::ThemeSettings;
  36use ui::{
  37    Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle,
  38    Scrollbar, ScrollbarState, Tooltip, prelude::*,
  39};
  40use util::{ResultExt, size::format_file_size, time::duration_alt_display};
  41use workspace::{CollaboratorId, Workspace};
  42use zed_actions::agent::{Chat, ToggleModelSelector};
  43use zed_actions::assistant::OpenRulesLibrary;
  44
  45use super::entry_view_state::EntryViewState;
  46use crate::acp::AcpModelSelectorPopover;
  47use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
  48use crate::agent_diff::AgentDiff;
  49use crate::ui::{AgentNotification, AgentNotificationEvent, BurnModeTooltip};
  50use crate::{
  51    AgentDiffPane, AgentPanel, ContinueThread, ContinueWithBurnMode, ExpandMessageEditor, Follow,
  52    KeepAll, OpenAgentDiff, RejectAll, ToggleBurnMode,
  53};
  54
  55const RESPONSE_PADDING_X: Pixels = px(19.);
  56pub const MIN_EDITOR_LINES: usize = 4;
  57pub const MAX_EDITOR_LINES: usize = 8;
  58
  59enum ThreadError {
  60    PaymentRequired,
  61    ModelRequestLimitReached(cloud_llm_client::Plan),
  62    ToolUseLimitReached,
  63    Other(SharedString),
  64}
  65
  66impl ThreadError {
  67    fn from_err(error: anyhow::Error) -> Self {
  68        if error.is::<language_model::PaymentRequiredError>() {
  69            Self::PaymentRequired
  70        } else if error.is::<language_model::ToolUseLimitReachedError>() {
  71            Self::ToolUseLimitReached
  72        } else if let Some(error) =
  73            error.downcast_ref::<language_model::ModelRequestLimitReachedError>()
  74        {
  75            Self::ModelRequestLimitReached(error.plan)
  76        } else {
  77            Self::Other(error.to_string().into())
  78        }
  79    }
  80}
  81
  82pub struct AcpThreadView {
  83    agent: Rc<dyn AgentServer>,
  84    workspace: WeakEntity<Workspace>,
  85    project: Entity<Project>,
  86    thread_store: Entity<ThreadStore>,
  87    text_thread_store: Entity<TextThreadStore>,
  88    thread_state: ThreadState,
  89    entry_view_state: EntryViewState,
  90    message_editor: Entity<MessageEditor>,
  91    model_selector: Option<Entity<AcpModelSelectorPopover>>,
  92    notifications: Vec<WindowHandle<AgentNotification>>,
  93    notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
  94    thread_error: Option<ThreadError>,
  95    list_state: ListState,
  96    scrollbar_state: ScrollbarState,
  97    auth_task: Option<Task<()>>,
  98    expanded_tool_calls: HashSet<acp::ToolCallId>,
  99    expanded_thinking_blocks: HashSet<(usize, usize)>,
 100    edits_expanded: bool,
 101    plan_expanded: bool,
 102    editor_expanded: bool,
 103    terminal_expanded: bool,
 104    editing_message: Option<EditingMessage>,
 105    _cancel_task: Option<Task<()>>,
 106    _subscriptions: [Subscription; 2],
 107}
 108
 109struct EditingMessage {
 110    index: usize,
 111    message_id: UserMessageId,
 112    editor: Entity<MessageEditor>,
 113    _subscription: Subscription,
 114}
 115
 116enum ThreadState {
 117    Loading {
 118        _task: Task<()>,
 119    },
 120    Ready {
 121        thread: Entity<AcpThread>,
 122        _subscription: [Subscription; 2],
 123    },
 124    LoadError(LoadError),
 125    Unauthenticated {
 126        connection: Rc<dyn AgentConnection>,
 127    },
 128    ServerExited {
 129        status: ExitStatus,
 130    },
 131}
 132
 133impl AcpThreadView {
 134    pub fn new(
 135        agent: Rc<dyn AgentServer>,
 136        workspace: WeakEntity<Workspace>,
 137        project: Entity<Project>,
 138        thread_store: Entity<ThreadStore>,
 139        text_thread_store: Entity<TextThreadStore>,
 140        restore_thread: Option<AcpThreadMetadata>,
 141        window: &mut Window,
 142        cx: &mut Context<Self>,
 143    ) -> Self {
 144        let message_editor = cx.new(|cx| {
 145            MessageEditor::new(
 146                workspace.clone(),
 147                project.clone(),
 148                thread_store.clone(),
 149                text_thread_store.clone(),
 150                editor::EditorMode::AutoHeight {
 151                    min_lines: MIN_EDITOR_LINES,
 152                    max_lines: Some(MAX_EDITOR_LINES),
 153                },
 154                window,
 155                cx,
 156            )
 157        });
 158
 159        let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0));
 160
 161        let subscriptions = [
 162            cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
 163            cx.subscribe_in(&message_editor, window, Self::on_message_editor_event),
 164        ];
 165
 166        Self {
 167            agent: agent.clone(),
 168            workspace: workspace.clone(),
 169            project: project.clone(),
 170            thread_store,
 171            text_thread_store,
 172            thread_state: Self::initial_state(
 173                agent,
 174                restore_thread,
 175                workspace,
 176                project,
 177                window,
 178                cx,
 179            ),
 180            message_editor,
 181            model_selector: None,
 182            notifications: Vec::new(),
 183            notification_subscriptions: HashMap::default(),
 184            entry_view_state: EntryViewState::default(),
 185            list_state: list_state.clone(),
 186            scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()),
 187            thread_error: None,
 188            auth_task: None,
 189            expanded_tool_calls: HashSet::default(),
 190            expanded_thinking_blocks: HashSet::default(),
 191            editing_message: None,
 192            edits_expanded: false,
 193            plan_expanded: false,
 194            editor_expanded: false,
 195            terminal_expanded: true,
 196            _subscriptions: subscriptions,
 197            _cancel_task: None,
 198        }
 199    }
 200
 201    fn initial_state(
 202        agent: Rc<dyn AgentServer>,
 203        restore_thread: Option<AcpThreadMetadata>,
 204        workspace: WeakEntity<Workspace>,
 205        project: Entity<Project>,
 206        window: &mut Window,
 207        cx: &mut Context<Self>,
 208    ) -> ThreadState {
 209        let root_dir = project
 210            .read(cx)
 211            .visible_worktrees(cx)
 212            .next()
 213            .map(|worktree| worktree.read(cx).abs_path())
 214            .unwrap_or_else(|| paths::home_dir().as_path().into());
 215
 216        let connect_task = agent.connect(&root_dir, &project, cx);
 217        let load_task = cx.spawn_in(window, async move |this, cx| {
 218            let connection = match connect_task.await {
 219                Ok(connection) => connection,
 220                Err(err) => {
 221                    this.update(cx, |this, cx| {
 222                        this.handle_load_error(err, cx);
 223                        cx.notify();
 224                    })
 225                    .log_err();
 226                    return;
 227                }
 228            };
 229
 230            // this.update_in(cx, |_this, _window, cx| {
 231            //     let status = connection.exit_status(cx);
 232            //     cx.spawn(async move |this, cx| {
 233            //         let status = status.await.ok();
 234            //         this.update(cx, |this, cx| {
 235            //             this.thread_state = ThreadState::ServerExited { status };
 236            //             cx.notify();
 237            //         })
 238            //         .ok();
 239            //     })
 240            //     .detach();
 241            // })
 242            // .ok();
 243            //
 244            let task = cx.update(|_, cx| {
 245                if let Some(restore_thread) = restore_thread {
 246                    connection.clone().load_thread(
 247                        project.clone(),
 248                        &root_dir,
 249                        restore_thread.id,
 250                        cx,
 251                    )
 252                } else {
 253                    connection
 254                        .clone()
 255                        .new_thread(project.clone(), &root_dir, cx)
 256                }
 257            });
 258
 259            let Ok(task) = task else {
 260                return;
 261            };
 262
 263            let result = match task.await {
 264                Err(e) => {
 265                    let mut cx = cx.clone();
 266                    if e.is::<acp_thread::AuthRequired>() {
 267                        this.update(&mut cx, |this, cx| {
 268                            this.thread_state = ThreadState::Unauthenticated { connection };
 269                            cx.notify();
 270                        })
 271                        .ok();
 272                        return;
 273                    } else {
 274                        Err(e)
 275                    }
 276                }
 277                Ok(thread) => Ok(thread),
 278            };
 279
 280            this.update_in(cx, |this, window, cx| {
 281                match result {
 282                    Ok(thread) => {
 283                        let thread_subscription =
 284                            cx.subscribe_in(&thread, window, Self::handle_thread_event);
 285
 286                        let action_log = thread.read(cx).action_log().clone();
 287                        let action_log_subscription =
 288                            cx.observe(&action_log, |_, _, cx| cx.notify());
 289
 290                        this.list_state
 291                            .splice(0..0, thread.read(cx).entries().len());
 292
 293                        AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
 294
 295                        this.model_selector =
 296                            thread
 297                                .read(cx)
 298                                .connection()
 299                                .model_selector()
 300                                .map(|selector| {
 301                                    cx.new(|cx| {
 302                                        AcpModelSelectorPopover::new(
 303                                            thread.read(cx).session_id().clone(),
 304                                            selector,
 305                                            PopoverMenuHandle::default(),
 306                                            this.focus_handle(cx),
 307                                            window,
 308                                            cx,
 309                                        )
 310                                    })
 311                                });
 312
 313                        this.thread_state = ThreadState::Ready {
 314                            thread,
 315                            _subscription: [thread_subscription, action_log_subscription],
 316                        };
 317
 318                        cx.notify();
 319                    }
 320                    Err(err) => {
 321                        this.handle_load_error(err, cx);
 322                    }
 323                };
 324            })
 325            .log_err();
 326        });
 327
 328        ThreadState::Loading { _task: load_task }
 329    }
 330
 331    fn handle_load_error(&mut self, err: anyhow::Error, cx: &mut Context<Self>) {
 332        if let Some(load_err) = err.downcast_ref::<LoadError>() {
 333            self.thread_state = ThreadState::LoadError(load_err.clone());
 334        } else {
 335            self.thread_state = ThreadState::LoadError(LoadError::Other(err.to_string().into()))
 336        }
 337        cx.notify();
 338    }
 339
 340    pub fn thread(&self) -> Option<&Entity<AcpThread>> {
 341        match &self.thread_state {
 342            ThreadState::Ready { thread, .. } => Some(thread),
 343            ThreadState::Unauthenticated { .. }
 344            | ThreadState::Loading { .. }
 345            | ThreadState::LoadError(..)
 346            | ThreadState::ServerExited { .. } => None,
 347        }
 348    }
 349
 350    pub fn title(&self, cx: &App) -> SharedString {
 351        match &self.thread_state {
 352            ThreadState::Ready { thread, .. } => thread.read(cx).title(),
 353            ThreadState::Loading { .. } => "Loading…".into(),
 354            ThreadState::LoadError(_) => "Failed to load".into(),
 355            ThreadState::Unauthenticated { .. } => "Not authenticated".into(),
 356            ThreadState::ServerExited { .. } => "Server exited unexpectedly".into(),
 357        }
 358    }
 359
 360    pub fn cancel_generation(&mut self, cx: &mut Context<Self>) {
 361        self.thread_error.take();
 362
 363        if let Some(thread) = self.thread() {
 364            self._cancel_task = Some(thread.update(cx, |thread, cx| thread.cancel(cx)));
 365        }
 366    }
 367
 368    pub fn expand_message_editor(
 369        &mut self,
 370        _: &ExpandMessageEditor,
 371        _window: &mut Window,
 372        cx: &mut Context<Self>,
 373    ) {
 374        self.set_editor_is_expanded(!self.editor_expanded, cx);
 375        cx.notify();
 376    }
 377
 378    fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
 379        self.editor_expanded = is_expanded;
 380        self.message_editor.update(cx, |editor, cx| {
 381            if is_expanded {
 382                editor.set_mode(
 383                    EditorMode::Full {
 384                        scale_ui_elements_with_buffer_font_size: false,
 385                        show_active_line_background: false,
 386                        sized_by_content: false,
 387                    },
 388                    cx,
 389                )
 390            } else {
 391                editor.set_mode(
 392                    EditorMode::AutoHeight {
 393                        min_lines: MIN_EDITOR_LINES,
 394                        max_lines: Some(MAX_EDITOR_LINES),
 395                    },
 396                    cx,
 397                )
 398            }
 399        });
 400        cx.notify();
 401    }
 402
 403    pub fn on_message_editor_event(
 404        &mut self,
 405        _: &Entity<MessageEditor>,
 406        event: &MessageEditorEvent,
 407        window: &mut Window,
 408        cx: &mut Context<Self>,
 409    ) {
 410        match event {
 411            MessageEditorEvent::Send => self.send(window, cx),
 412            MessageEditorEvent::Cancel => self.cancel_generation(cx),
 413        }
 414    }
 415
 416    fn resume_chat(&mut self, cx: &mut Context<Self>) {
 417        self.thread_error.take();
 418        let Some(thread) = self.thread() else {
 419            return;
 420        };
 421
 422        let task = thread.update(cx, |thread, cx| thread.resume(cx));
 423        cx.spawn(async move |this, cx| {
 424            let result = task.await;
 425
 426            this.update(cx, |this, cx| {
 427                if let Err(err) = result {
 428                    this.handle_thread_error(err, cx);
 429                }
 430            })
 431        })
 432        .detach();
 433    }
 434
 435    fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 436        let contents = self
 437            .message_editor
 438            .update(cx, |message_editor, cx| message_editor.contents(window, cx));
 439        self.send_impl(contents, window, cx)
 440    }
 441
 442    fn send_impl(
 443        &mut self,
 444        contents: Task<anyhow::Result<Vec<acp::ContentBlock>>>,
 445        window: &mut Window,
 446        cx: &mut Context<Self>,
 447    ) {
 448        self.thread_error.take();
 449        self.editing_message.take();
 450
 451        let Some(thread) = self.thread().cloned() else {
 452            return;
 453        };
 454        let task = cx.spawn_in(window, async move |this, cx| {
 455            let contents = contents.await?;
 456
 457            if contents.is_empty() {
 458                return Ok(());
 459            }
 460
 461            this.update_in(cx, |this, window, cx| {
 462                this.set_editor_is_expanded(false, cx);
 463                this.scroll_to_bottom(cx);
 464                this.message_editor.update(cx, |message_editor, cx| {
 465                    message_editor.clear(window, cx);
 466                });
 467            })?;
 468            let send = thread.update(cx, |thread, cx| thread.send(contents, cx))?;
 469            send.await
 470        });
 471
 472        cx.spawn(async move |this, cx| {
 473            if let Err(err) = task.await {
 474                this.update(cx, |this, cx| {
 475                    this.handle_thread_error(err, cx);
 476                })
 477                .ok();
 478            }
 479        })
 480        .detach();
 481    }
 482
 483    fn cancel_editing(&mut self, _: &ClickEvent, _window: &mut Window, cx: &mut Context<Self>) {
 484        self.editing_message.take();
 485        cx.notify();
 486    }
 487
 488    fn regenerate(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
 489        let Some(editing_message) = self.editing_message.take() else {
 490            return;
 491        };
 492
 493        let Some(thread) = self.thread().cloned() else {
 494            return;
 495        };
 496
 497        let rewind = thread.update(cx, |thread, cx| {
 498            thread.rewind(editing_message.message_id, cx)
 499        });
 500
 501        let contents = editing_message
 502            .editor
 503            .update(cx, |message_editor, cx| message_editor.contents(window, cx));
 504        let task = cx.foreground_executor().spawn(async move {
 505            rewind.await?;
 506            contents.await
 507        });
 508        self.send_impl(task, window, cx);
 509    }
 510
 511    fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) {
 512        if let Some(thread) = self.thread() {
 513            AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err();
 514        }
 515    }
 516
 517    fn open_edited_buffer(
 518        &mut self,
 519        buffer: &Entity<Buffer>,
 520        window: &mut Window,
 521        cx: &mut Context<Self>,
 522    ) {
 523        let Some(thread) = self.thread() else {
 524            return;
 525        };
 526
 527        let Some(diff) =
 528            AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err()
 529        else {
 530            return;
 531        };
 532
 533        diff.update(cx, |diff, cx| {
 534            diff.move_to_path(PathKey::for_buffer(&buffer, cx), window, cx)
 535        })
 536    }
 537
 538    fn handle_thread_error(&mut self, error: anyhow::Error, cx: &mut Context<Self>) {
 539        self.thread_error = Some(ThreadError::from_err(error));
 540        cx.notify();
 541    }
 542
 543    fn clear_thread_error(&mut self, cx: &mut Context<Self>) {
 544        self.thread_error = None;
 545        cx.notify();
 546    }
 547
 548    fn handle_thread_event(
 549        &mut self,
 550        thread: &Entity<AcpThread>,
 551        event: &AcpThreadEvent,
 552        window: &mut Window,
 553        cx: &mut Context<Self>,
 554    ) {
 555        match event {
 556            AcpThreadEvent::NewEntry => {
 557                let len = thread.read(cx).entries().len();
 558                let index = len - 1;
 559                self.entry_view_state.sync_entry(
 560                    self.workspace.clone(),
 561                    thread.clone(),
 562                    index,
 563                    window,
 564                    cx,
 565                );
 566                self.list_state.splice(index..index, 1);
 567            }
 568            AcpThreadEvent::EntryUpdated(index) => {
 569                self.entry_view_state.sync_entry(
 570                    self.workspace.clone(),
 571                    thread.clone(),
 572                    *index,
 573                    window,
 574                    cx,
 575                );
 576                self.list_state.splice(*index..index + 1, 1);
 577            }
 578            AcpThreadEvent::EntriesRemoved(range) => {
 579                self.entry_view_state.remove(range.clone());
 580                self.list_state.splice(range.clone(), 0);
 581            }
 582            AcpThreadEvent::ToolAuthorizationRequired => {
 583                self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx);
 584            }
 585            AcpThreadEvent::Stopped => {
 586                let used_tools = thread.read(cx).used_tools_since_last_user_message();
 587                self.notify_with_sound(
 588                    if used_tools {
 589                        "Finished running tools"
 590                    } else {
 591                        "New message"
 592                    },
 593                    IconName::ZedAssistant,
 594                    window,
 595                    cx,
 596                );
 597            }
 598            AcpThreadEvent::Error => {
 599                self.notify_with_sound(
 600                    "Agent stopped due to an error",
 601                    IconName::Warning,
 602                    window,
 603                    cx,
 604                );
 605            }
 606            AcpThreadEvent::ServerExited(status) => {
 607                self.thread_state = ThreadState::ServerExited { status: *status };
 608            }
 609        }
 610        cx.notify();
 611    }
 612
 613    fn authenticate(
 614        &mut self,
 615        method: acp::AuthMethodId,
 616        window: &mut Window,
 617        cx: &mut Context<Self>,
 618    ) {
 619        let ThreadState::Unauthenticated { ref connection } = self.thread_state else {
 620            return;
 621        };
 622
 623        self.thread_error.take();
 624        let authenticate = connection.authenticate(method, cx);
 625        self.auth_task = Some(cx.spawn_in(window, {
 626            let project = self.project.clone();
 627            let agent = self.agent.clone();
 628            async move |this, cx| {
 629                let result = authenticate.await;
 630
 631                this.update_in(cx, |this, window, cx| {
 632                    if let Err(err) = result {
 633                        this.handle_thread_error(err, cx);
 634                    } else {
 635                        this.thread_state = Self::initial_state(
 636                            agent,
 637                            None, // todo!()
 638                            this.workspace.clone(),
 639                            project.clone(),
 640                            window,
 641                            cx,
 642                        )
 643                    }
 644                    this.auth_task.take()
 645                })
 646                .ok();
 647            }
 648        }));
 649    }
 650
 651    fn authorize_tool_call(
 652        &mut self,
 653        tool_call_id: acp::ToolCallId,
 654        option_id: acp::PermissionOptionId,
 655        option_kind: acp::PermissionOptionKind,
 656        cx: &mut Context<Self>,
 657    ) {
 658        let Some(thread) = self.thread() else {
 659            return;
 660        };
 661        thread.update(cx, |thread, cx| {
 662            thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx);
 663        });
 664        cx.notify();
 665    }
 666
 667    fn rewind(&mut self, message_id: &UserMessageId, cx: &mut Context<Self>) {
 668        let Some(thread) = self.thread() else {
 669            return;
 670        };
 671        thread
 672            .update(cx, |thread, cx| thread.rewind(message_id.clone(), cx))
 673            .detach_and_log_err(cx);
 674        cx.notify();
 675    }
 676
 677    fn render_entry(
 678        &self,
 679        entry_ix: usize,
 680        total_entries: usize,
 681        entry: &AgentThreadEntry,
 682        window: &mut Window,
 683        cx: &Context<Self>,
 684    ) -> AnyElement {
 685        let primary = match &entry {
 686            AgentThreadEntry::UserMessage(message) => div()
 687                .id(("user_message", entry_ix))
 688                .py_4()
 689                .px_2()
 690                .children(message.id.clone().and_then(|message_id| {
 691                    message.checkpoint.as_ref()?.show.then(|| {
 692                        Button::new("restore-checkpoint", "Restore Checkpoint")
 693                            .icon(IconName::Undo)
 694                            .icon_size(IconSize::XSmall)
 695                            .icon_position(IconPosition::Start)
 696                            .label_size(LabelSize::XSmall)
 697                            .on_click(cx.listener(move |this, _, _window, cx| {
 698                                this.rewind(&message_id, cx);
 699                            }))
 700                    })
 701                }))
 702                .child(
 703                    v_flex()
 704                        .p_3()
 705                        .gap_1p5()
 706                        .rounded_lg()
 707                        .shadow_md()
 708                        .bg(cx.theme().colors().editor_background)
 709                        .border_1()
 710                        .border_color(cx.theme().colors().border)
 711                        .text_xs()
 712                        .id("message")
 713                        .on_click(cx.listener({
 714                            move |this, _, window, cx| {
 715                                this.start_editing_message(entry_ix, window, cx)
 716                            }
 717                        }))
 718                        .children(
 719                            if let Some(editing) = self.editing_message.as_ref()
 720                                && Some(&editing.message_id) == message.id.as_ref()
 721                            {
 722                                Some(
 723                                    self.render_edit_message_editor(editing, cx)
 724                                        .into_any_element(),
 725                                )
 726                            } else {
 727                                message.content.markdown().map(|md| {
 728                                    self.render_markdown(
 729                                        md.clone(),
 730                                        user_message_markdown_style(window, cx),
 731                                    )
 732                                    .into_any_element()
 733                                })
 734                            },
 735                        ),
 736                )
 737                .into_any(),
 738            AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
 739                let style = default_markdown_style(false, window, cx);
 740                let message_body = v_flex()
 741                    .w_full()
 742                    .gap_2p5()
 743                    .children(chunks.iter().enumerate().filter_map(
 744                        |(chunk_ix, chunk)| match chunk {
 745                            AssistantMessageChunk::Message { block } => {
 746                                block.markdown().map(|md| {
 747                                    self.render_markdown(md.clone(), style.clone())
 748                                        .into_any_element()
 749                                })
 750                            }
 751                            AssistantMessageChunk::Thought { block } => {
 752                                block.markdown().map(|md| {
 753                                    self.render_thinking_block(
 754                                        entry_ix,
 755                                        chunk_ix,
 756                                        md.clone(),
 757                                        window,
 758                                        cx,
 759                                    )
 760                                    .into_any_element()
 761                                })
 762                            }
 763                        },
 764                    ))
 765                    .into_any();
 766
 767                v_flex()
 768                    .px_5()
 769                    .py_1()
 770                    .when(entry_ix + 1 == total_entries, |this| this.pb_4())
 771                    .w_full()
 772                    .text_ui(cx)
 773                    .child(message_body)
 774                    .into_any()
 775            }
 776            AgentThreadEntry::ToolCall(tool_call) => {
 777                let has_terminals = tool_call.terminals().next().is_some();
 778
 779                div().w_full().py_1p5().px_5().map(|this| {
 780                    if has_terminals {
 781                        this.children(tool_call.terminals().map(|terminal| {
 782                            self.render_terminal_tool_call(
 783                                entry_ix, terminal, tool_call, window, cx,
 784                            )
 785                        }))
 786                    } else {
 787                        this.child(self.render_tool_call(entry_ix, tool_call, window, cx))
 788                    }
 789                })
 790            }
 791            .into_any(),
 792        };
 793
 794        let Some(thread) = self.thread() else {
 795            return primary;
 796        };
 797
 798        let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
 799        let primary = if entry_ix == total_entries - 1 && !is_generating {
 800            v_flex()
 801                .w_full()
 802                .child(primary)
 803                .child(self.render_thread_controls(cx))
 804                .into_any_element()
 805        } else {
 806            primary
 807        };
 808
 809        if let Some(editing) = self.editing_message.as_ref()
 810            && editing.index < entry_ix
 811        {
 812            let backdrop = div()
 813                .id(("backdrop", entry_ix))
 814                .size_full()
 815                .absolute()
 816                .inset_0()
 817                .bg(cx.theme().colors().panel_background)
 818                .opacity(0.8)
 819                .block_mouse_except_scroll()
 820                .on_click(cx.listener(Self::cancel_editing));
 821
 822            div()
 823                .relative()
 824                .child(backdrop)
 825                .child(primary)
 826                .into_any_element()
 827        } else {
 828            primary
 829        }
 830    }
 831
 832    fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
 833        cx.theme()
 834            .colors()
 835            .element_background
 836            .blend(cx.theme().colors().editor_foreground.opacity(0.025))
 837    }
 838
 839    fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
 840        cx.theme().colors().border.opacity(0.6)
 841    }
 842
 843    fn tool_name_font_size(&self) -> Rems {
 844        rems_from_px(13.)
 845    }
 846
 847    fn render_thinking_block(
 848        &self,
 849        entry_ix: usize,
 850        chunk_ix: usize,
 851        chunk: Entity<Markdown>,
 852        window: &Window,
 853        cx: &Context<Self>,
 854    ) -> AnyElement {
 855        let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
 856        let card_header_id = SharedString::from("inner-card-header");
 857        let key = (entry_ix, chunk_ix);
 858        let is_open = self.expanded_thinking_blocks.contains(&key);
 859
 860        v_flex()
 861            .child(
 862                h_flex()
 863                    .id(header_id)
 864                    .group(&card_header_id)
 865                    .relative()
 866                    .w_full()
 867                    .gap_1p5()
 868                    .opacity(0.8)
 869                    .hover(|style| style.opacity(1.))
 870                    .child(
 871                        h_flex()
 872                            .size_4()
 873                            .justify_center()
 874                            .child(
 875                                div()
 876                                    .group_hover(&card_header_id, |s| s.invisible().w_0())
 877                                    .child(
 878                                        Icon::new(IconName::ToolThink)
 879                                            .size(IconSize::Small)
 880                                            .color(Color::Muted),
 881                                    ),
 882                            )
 883                            .child(
 884                                h_flex()
 885                                    .absolute()
 886                                    .inset_0()
 887                                    .invisible()
 888                                    .justify_center()
 889                                    .group_hover(&card_header_id, |s| s.visible())
 890                                    .child(
 891                                        Disclosure::new(("expand", entry_ix), is_open)
 892                                            .opened_icon(IconName::ChevronUp)
 893                                            .closed_icon(IconName::ChevronRight)
 894                                            .on_click(cx.listener({
 895                                                move |this, _event, _window, cx| {
 896                                                    if is_open {
 897                                                        this.expanded_thinking_blocks.remove(&key);
 898                                                    } else {
 899                                                        this.expanded_thinking_blocks.insert(key);
 900                                                    }
 901                                                    cx.notify();
 902                                                }
 903                                            })),
 904                                    ),
 905                            ),
 906                    )
 907                    .child(
 908                        div()
 909                            .text_size(self.tool_name_font_size())
 910                            .child("Thinking"),
 911                    )
 912                    .on_click(cx.listener({
 913                        move |this, _event, _window, cx| {
 914                            if is_open {
 915                                this.expanded_thinking_blocks.remove(&key);
 916                            } else {
 917                                this.expanded_thinking_blocks.insert(key);
 918                            }
 919                            cx.notify();
 920                        }
 921                    })),
 922            )
 923            .when(is_open, |this| {
 924                this.child(
 925                    div()
 926                        .relative()
 927                        .mt_1p5()
 928                        .ml(px(7.))
 929                        .pl_4()
 930                        .border_l_1()
 931                        .border_color(self.tool_card_border_color(cx))
 932                        .text_ui_sm(cx)
 933                        .child(
 934                            self.render_markdown(chunk, default_markdown_style(false, window, cx)),
 935                        ),
 936                )
 937            })
 938            .into_any_element()
 939    }
 940
 941    fn render_tool_call_icon(
 942        &self,
 943        group_name: SharedString,
 944        entry_ix: usize,
 945        is_collapsible: bool,
 946        is_open: bool,
 947        tool_call: &ToolCall,
 948        cx: &Context<Self>,
 949    ) -> Div {
 950        let tool_icon = Icon::new(match tool_call.kind {
 951            acp::ToolKind::Read => IconName::ToolRead,
 952            acp::ToolKind::Edit => IconName::ToolPencil,
 953            acp::ToolKind::Delete => IconName::ToolDeleteFile,
 954            acp::ToolKind::Move => IconName::ArrowRightLeft,
 955            acp::ToolKind::Search => IconName::ToolSearch,
 956            acp::ToolKind::Execute => IconName::ToolTerminal,
 957            acp::ToolKind::Think => IconName::ToolThink,
 958            acp::ToolKind::Fetch => IconName::ToolWeb,
 959            acp::ToolKind::Other => IconName::ToolHammer,
 960        })
 961        .size(IconSize::Small)
 962        .color(Color::Muted);
 963
 964        let base_container = h_flex().size_4().justify_center();
 965
 966        if is_collapsible {
 967            base_container
 968                .child(
 969                    div()
 970                        .group_hover(&group_name, |s| s.invisible().w_0())
 971                        .child(tool_icon),
 972                )
 973                .child(
 974                    h_flex()
 975                        .absolute()
 976                        .inset_0()
 977                        .invisible()
 978                        .justify_center()
 979                        .group_hover(&group_name, |s| s.visible())
 980                        .child(
 981                            Disclosure::new(("expand", entry_ix), is_open)
 982                                .opened_icon(IconName::ChevronUp)
 983                                .closed_icon(IconName::ChevronRight)
 984                                .on_click(cx.listener({
 985                                    let id = tool_call.id.clone();
 986                                    move |this: &mut Self, _, _, cx: &mut Context<Self>| {
 987                                        if is_open {
 988                                            this.expanded_tool_calls.remove(&id);
 989                                        } else {
 990                                            this.expanded_tool_calls.insert(id.clone());
 991                                        }
 992                                        cx.notify();
 993                                    }
 994                                })),
 995                        ),
 996                )
 997        } else {
 998            base_container.child(tool_icon)
 999        }
1000    }
1001
1002    fn render_tool_call(
1003        &self,
1004        entry_ix: usize,
1005        tool_call: &ToolCall,
1006        window: &Window,
1007        cx: &Context<Self>,
1008    ) -> Div {
1009        let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix));
1010        let card_header_id = SharedString::from("inner-tool-call-header");
1011
1012        let status_icon = match &tool_call.status {
1013            ToolCallStatus::Allowed {
1014                status: acp::ToolCallStatus::Pending,
1015            }
1016            | ToolCallStatus::WaitingForConfirmation { .. } => None,
1017            ToolCallStatus::Allowed {
1018                status: acp::ToolCallStatus::InProgress,
1019                ..
1020            } => Some(
1021                Icon::new(IconName::ArrowCircle)
1022                    .color(Color::Accent)
1023                    .size(IconSize::Small)
1024                    .with_animation(
1025                        "running",
1026                        Animation::new(Duration::from_secs(2)).repeat(),
1027                        |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
1028                    )
1029                    .into_any(),
1030            ),
1031            ToolCallStatus::Allowed {
1032                status: acp::ToolCallStatus::Completed,
1033                ..
1034            } => None,
1035            ToolCallStatus::Rejected
1036            | ToolCallStatus::Canceled
1037            | ToolCallStatus::Allowed {
1038                status: acp::ToolCallStatus::Failed,
1039                ..
1040            } => Some(
1041                Icon::new(IconName::Close)
1042                    .color(Color::Error)
1043                    .size(IconSize::Small)
1044                    .into_any_element(),
1045            ),
1046        };
1047
1048        let needs_confirmation = matches!(
1049            tool_call.status,
1050            ToolCallStatus::WaitingForConfirmation { .. }
1051        );
1052        let is_edit = matches!(tool_call.kind, acp::ToolKind::Edit);
1053        let has_diff = tool_call
1054            .content
1055            .iter()
1056            .any(|content| matches!(content, ToolCallContent::Diff { .. }));
1057        let has_nonempty_diff = tool_call.content.iter().any(|content| match content {
1058            ToolCallContent::Diff(diff) => diff.read(cx).has_revealed_range(cx),
1059            _ => false,
1060        });
1061        let use_card_layout = needs_confirmation || is_edit || has_diff;
1062
1063        let is_collapsible = !tool_call.content.is_empty() && !use_card_layout;
1064
1065        let is_open = tool_call.content.is_empty()
1066            || needs_confirmation
1067            || has_nonempty_diff
1068            || self.expanded_tool_calls.contains(&tool_call.id);
1069
1070        let gradient_overlay = |color: Hsla| {
1071            div()
1072                .absolute()
1073                .top_0()
1074                .right_0()
1075                .w_12()
1076                .h_full()
1077                .bg(linear_gradient(
1078                    90.,
1079                    linear_color_stop(color, 1.),
1080                    linear_color_stop(color.opacity(0.2), 0.),
1081                ))
1082        };
1083        let gradient_color = if use_card_layout {
1084            self.tool_card_header_bg(cx)
1085        } else {
1086            cx.theme().colors().panel_background
1087        };
1088
1089        let tool_output_display = match &tool_call.status {
1090            ToolCallStatus::WaitingForConfirmation { options, .. } => v_flex()
1091                .w_full()
1092                .children(tool_call.content.iter().map(|content| {
1093                    div()
1094                        .child(
1095                            self.render_tool_call_content(entry_ix, content, tool_call, window, cx),
1096                        )
1097                        .into_any_element()
1098                }))
1099                .child(self.render_permission_buttons(
1100                    options,
1101                    entry_ix,
1102                    tool_call.id.clone(),
1103                    tool_call.content.is_empty(),
1104                    cx,
1105                )),
1106            ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => v_flex()
1107                .w_full()
1108                .children(tool_call.content.iter().map(|content| {
1109                    div()
1110                        .child(
1111                            self.render_tool_call_content(entry_ix, content, tool_call, window, cx),
1112                        )
1113                        .into_any_element()
1114                })),
1115            ToolCallStatus::Rejected => v_flex().size_0(),
1116        };
1117
1118        v_flex()
1119            .when(use_card_layout, |this| {
1120                this.rounded_lg()
1121                    .border_1()
1122                    .border_color(self.tool_card_border_color(cx))
1123                    .bg(cx.theme().colors().editor_background)
1124                    .overflow_hidden()
1125            })
1126            .child(
1127                h_flex()
1128                    .id(header_id)
1129                    .w_full()
1130                    .gap_1()
1131                    .justify_between()
1132                    .map(|this| {
1133                        if use_card_layout {
1134                            this.pl_2()
1135                                .pr_1()
1136                                .py_1()
1137                                .rounded_t_md()
1138                                .bg(self.tool_card_header_bg(cx))
1139                        } else {
1140                            this.opacity(0.8).hover(|style| style.opacity(1.))
1141                        }
1142                    })
1143                    .child(
1144                        h_flex()
1145                            .group(&card_header_id)
1146                            .relative()
1147                            .w_full()
1148                            .text_size(self.tool_name_font_size())
1149                            .child(self.render_tool_call_icon(
1150                                card_header_id,
1151                                entry_ix,
1152                                is_collapsible,
1153                                is_open,
1154                                tool_call,
1155                                cx,
1156                            ))
1157                            .child(if tool_call.locations.len() == 1 {
1158                                let name = tool_call.locations[0]
1159                                    .path
1160                                    .file_name()
1161                                    .unwrap_or_default()
1162                                    .display()
1163                                    .to_string();
1164
1165                                h_flex()
1166                                    .id(("open-tool-call-location", entry_ix))
1167                                    .w_full()
1168                                    .max_w_full()
1169                                    .px_1p5()
1170                                    .rounded_sm()
1171                                    .overflow_x_scroll()
1172                                    .opacity(0.8)
1173                                    .hover(|label| {
1174                                        label.opacity(1.).bg(cx
1175                                            .theme()
1176                                            .colors()
1177                                            .element_hover
1178                                            .opacity(0.5))
1179                                    })
1180                                    .child(name)
1181                                    .tooltip(Tooltip::text("Jump to File"))
1182                                    .on_click(cx.listener(move |this, _, window, cx| {
1183                                        this.open_tool_call_location(entry_ix, 0, window, cx);
1184                                    }))
1185                                    .into_any_element()
1186                            } else {
1187                                h_flex()
1188                                    .id("non-card-label-container")
1189                                    .w_full()
1190                                    .relative()
1191                                    .ml_1p5()
1192                                    .overflow_hidden()
1193                                    .child(
1194                                        h_flex()
1195                                            .id("non-card-label")
1196                                            .pr_8()
1197                                            .w_full()
1198                                            .overflow_x_scroll()
1199                                            .child(self.render_markdown(
1200                                                tool_call.label.clone(),
1201                                                default_markdown_style(
1202                                                    needs_confirmation || is_edit || has_diff,
1203                                                    window,
1204                                                    cx,
1205                                                ),
1206                                            )),
1207                                    )
1208                                    .child(gradient_overlay(gradient_color))
1209                                    .on_click(cx.listener({
1210                                        let id = tool_call.id.clone();
1211                                        move |this: &mut Self, _, _, cx: &mut Context<Self>| {
1212                                            if is_open {
1213                                                this.expanded_tool_calls.remove(&id);
1214                                            } else {
1215                                                this.expanded_tool_calls.insert(id.clone());
1216                                            }
1217                                            cx.notify();
1218                                        }
1219                                    }))
1220                                    .into_any()
1221                            }),
1222                    )
1223                    .children(status_icon),
1224            )
1225            .when(is_open, |this| this.child(tool_output_display))
1226    }
1227
1228    fn render_tool_call_content(
1229        &self,
1230        entry_ix: usize,
1231        content: &ToolCallContent,
1232        tool_call: &ToolCall,
1233        window: &Window,
1234        cx: &Context<Self>,
1235    ) -> AnyElement {
1236        match content {
1237            ToolCallContent::ContentBlock(content) => {
1238                if let Some(resource_link) = content.resource_link() {
1239                    self.render_resource_link(resource_link, cx)
1240                } else if let Some(markdown) = content.markdown() {
1241                    self.render_markdown_output(markdown.clone(), tool_call.id.clone(), window, cx)
1242                } else {
1243                    Empty.into_any_element()
1244                }
1245            }
1246            ToolCallContent::Diff(diff) => {
1247                self.render_diff_editor(entry_ix, &diff.read(cx).multibuffer(), cx)
1248            }
1249            ToolCallContent::Terminal(terminal) => {
1250                self.render_terminal_tool_call(entry_ix, terminal, tool_call, window, cx)
1251            }
1252        }
1253    }
1254
1255    fn render_markdown_output(
1256        &self,
1257        markdown: Entity<Markdown>,
1258        tool_call_id: acp::ToolCallId,
1259        window: &Window,
1260        cx: &Context<Self>,
1261    ) -> AnyElement {
1262        let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id.clone()));
1263
1264        v_flex()
1265            .mt_1p5()
1266            .ml(px(7.))
1267            .px_3p5()
1268            .gap_2()
1269            .border_l_1()
1270            .border_color(self.tool_card_border_color(cx))
1271            .text_sm()
1272            .text_color(cx.theme().colors().text_muted)
1273            .child(self.render_markdown(markdown, default_markdown_style(false, window, cx)))
1274            .child(
1275                Button::new(button_id, "Collapse Output")
1276                    .full_width()
1277                    .style(ButtonStyle::Outlined)
1278                    .label_size(LabelSize::Small)
1279                    .icon(IconName::ChevronUp)
1280                    .icon_color(Color::Muted)
1281                    .icon_position(IconPosition::Start)
1282                    .on_click(cx.listener({
1283                        let id = tool_call_id.clone();
1284                        move |this: &mut Self, _, _, cx: &mut Context<Self>| {
1285                            this.expanded_tool_calls.remove(&id);
1286                            cx.notify();
1287                        }
1288                    })),
1289            )
1290            .into_any_element()
1291    }
1292
1293    fn render_resource_link(
1294        &self,
1295        resource_link: &acp::ResourceLink,
1296        cx: &Context<Self>,
1297    ) -> AnyElement {
1298        let uri: SharedString = resource_link.uri.clone().into();
1299
1300        let label: SharedString = if let Some(path) = resource_link.uri.strip_prefix("file://") {
1301            path.to_string().into()
1302        } else {
1303            uri.clone()
1304        };
1305
1306        let button_id = SharedString::from(format!("item-{}", uri.clone()));
1307
1308        div()
1309            .ml(px(7.))
1310            .pl_2p5()
1311            .border_l_1()
1312            .border_color(self.tool_card_border_color(cx))
1313            .overflow_hidden()
1314            .child(
1315                Button::new(button_id, label)
1316                    .label_size(LabelSize::Small)
1317                    .color(Color::Muted)
1318                    .icon(IconName::ArrowUpRight)
1319                    .icon_size(IconSize::XSmall)
1320                    .icon_color(Color::Muted)
1321                    .truncate(true)
1322                    .on_click(cx.listener({
1323                        let workspace = self.workspace.clone();
1324                        move |_, _, window, cx: &mut Context<Self>| {
1325                            Self::open_link(uri.clone(), &workspace, window, cx);
1326                        }
1327                    })),
1328            )
1329            .into_any_element()
1330    }
1331
1332    fn render_permission_buttons(
1333        &self,
1334        options: &[acp::PermissionOption],
1335        entry_ix: usize,
1336        tool_call_id: acp::ToolCallId,
1337        empty_content: bool,
1338        cx: &Context<Self>,
1339    ) -> Div {
1340        h_flex()
1341            .py_1()
1342            .pl_2()
1343            .pr_1()
1344            .gap_1()
1345            .justify_between()
1346            .flex_wrap()
1347            .when(!empty_content, |this| {
1348                this.border_t_1()
1349                    .border_color(self.tool_card_border_color(cx))
1350            })
1351            .child(
1352                div()
1353                    .min_w(rems_from_px(145.))
1354                    .child(LoadingLabel::new("Waiting for Confirmation").size(LabelSize::Small)),
1355            )
1356            .child(h_flex().gap_0p5().children(options.iter().map(|option| {
1357                let option_id = SharedString::from(option.id.0.clone());
1358                Button::new((option_id, entry_ix), option.name.clone())
1359                    .map(|this| match option.kind {
1360                        acp::PermissionOptionKind::AllowOnce => {
1361                            this.icon(IconName::Check).icon_color(Color::Success)
1362                        }
1363                        acp::PermissionOptionKind::AllowAlways => {
1364                            this.icon(IconName::CheckDouble).icon_color(Color::Success)
1365                        }
1366                        acp::PermissionOptionKind::RejectOnce => {
1367                            this.icon(IconName::Close).icon_color(Color::Error)
1368                        }
1369                        acp::PermissionOptionKind::RejectAlways => {
1370                            this.icon(IconName::Close).icon_color(Color::Error)
1371                        }
1372                    })
1373                    .icon_position(IconPosition::Start)
1374                    .icon_size(IconSize::XSmall)
1375                    .label_size(LabelSize::Small)
1376                    .on_click(cx.listener({
1377                        let tool_call_id = tool_call_id.clone();
1378                        let option_id = option.id.clone();
1379                        let option_kind = option.kind;
1380                        move |this, _, _, cx| {
1381                            this.authorize_tool_call(
1382                                tool_call_id.clone(),
1383                                option_id.clone(),
1384                                option_kind,
1385                                cx,
1386                            );
1387                        }
1388                    }))
1389            })))
1390    }
1391
1392    fn render_diff_editor(
1393        &self,
1394        entry_ix: usize,
1395        multibuffer: &Entity<MultiBuffer>,
1396        cx: &Context<Self>,
1397    ) -> AnyElement {
1398        v_flex()
1399            .h_full()
1400            .border_t_1()
1401            .border_color(self.tool_card_border_color(cx))
1402            .child(
1403                if let Some(entry) = self.entry_view_state.entry(entry_ix)
1404                    && let Some(editor) = entry.editor_for_diff(&multibuffer)
1405                {
1406                    editor.clone().into_any_element()
1407                } else {
1408                    Empty.into_any()
1409                },
1410            )
1411            .into_any()
1412    }
1413
1414    fn render_terminal_tool_call(
1415        &self,
1416        entry_ix: usize,
1417        terminal: &Entity<acp_thread::Terminal>,
1418        tool_call: &ToolCall,
1419        window: &Window,
1420        cx: &Context<Self>,
1421    ) -> AnyElement {
1422        let terminal_data = terminal.read(cx);
1423        let working_dir = terminal_data.working_dir();
1424        let command = terminal_data.command();
1425        let started_at = terminal_data.started_at();
1426
1427        let tool_failed = matches!(
1428            &tool_call.status,
1429            ToolCallStatus::Rejected
1430                | ToolCallStatus::Canceled
1431                | ToolCallStatus::Allowed {
1432                    status: acp::ToolCallStatus::Failed,
1433                    ..
1434                }
1435        );
1436
1437        let output = terminal_data.output();
1438        let command_finished = output.is_some();
1439        let truncated_output = output.is_some_and(|output| output.was_content_truncated);
1440        let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0);
1441
1442        let command_failed = command_finished
1443            && output.is_some_and(|o| o.exit_status.is_none_or(|status| !status.success()));
1444
1445        let time_elapsed = if let Some(output) = output {
1446            output.ended_at.duration_since(started_at)
1447        } else {
1448            started_at.elapsed()
1449        };
1450
1451        let header_bg = cx
1452            .theme()
1453            .colors()
1454            .element_background
1455            .blend(cx.theme().colors().editor_foreground.opacity(0.025));
1456        let border_color = cx.theme().colors().border.opacity(0.6);
1457
1458        let working_dir = working_dir
1459            .as_ref()
1460            .map(|path| format!("{}", path.display()))
1461            .unwrap_or_else(|| "current directory".to_string());
1462
1463        let header = h_flex()
1464            .id(SharedString::from(format!(
1465                "terminal-tool-header-{}",
1466                terminal.entity_id()
1467            )))
1468            .flex_none()
1469            .gap_1()
1470            .justify_between()
1471            .rounded_t_md()
1472            .child(
1473                div()
1474                    .id(("command-target-path", terminal.entity_id()))
1475                    .w_full()
1476                    .max_w_full()
1477                    .overflow_x_scroll()
1478                    .child(
1479                        Label::new(working_dir)
1480                            .buffer_font(cx)
1481                            .size(LabelSize::XSmall)
1482                            .color(Color::Muted),
1483                    ),
1484            )
1485            .when(!command_finished, |header| {
1486                header
1487                    .gap_1p5()
1488                    .child(
1489                        Button::new(
1490                            SharedString::from(format!("stop-terminal-{}", terminal.entity_id())),
1491                            "Stop",
1492                        )
1493                        .icon(IconName::Stop)
1494                        .icon_position(IconPosition::Start)
1495                        .icon_size(IconSize::Small)
1496                        .icon_color(Color::Error)
1497                        .label_size(LabelSize::Small)
1498                        .tooltip(move |window, cx| {
1499                            Tooltip::with_meta(
1500                                "Stop This Command",
1501                                None,
1502                                "Also possible by placing your cursor inside the terminal and using regular terminal bindings.",
1503                                window,
1504                                cx,
1505                            )
1506                        })
1507                        .on_click({
1508                            let terminal = terminal.clone();
1509                            cx.listener(move |_this, _event, _window, cx| {
1510                                let inner_terminal = terminal.read(cx).inner().clone();
1511                                inner_terminal.update(cx, |inner_terminal, _cx| {
1512                                    inner_terminal.kill_active_task();
1513                                });
1514                            })
1515                        }),
1516                    )
1517                    .child(Divider::vertical())
1518                    .child(
1519                        Icon::new(IconName::ArrowCircle)
1520                            .size(IconSize::XSmall)
1521                            .color(Color::Info)
1522                            .with_animation(
1523                                "arrow-circle",
1524                                Animation::new(Duration::from_secs(2)).repeat(),
1525                                |icon, delta| {
1526                                    icon.transform(Transformation::rotate(percentage(delta)))
1527                                },
1528                            ),
1529                    )
1530            })
1531            .when(tool_failed || command_failed, |header| {
1532                header.child(
1533                    div()
1534                        .id(("terminal-tool-error-code-indicator", terminal.entity_id()))
1535                        .child(
1536                            Icon::new(IconName::Close)
1537                                .size(IconSize::Small)
1538                                .color(Color::Error),
1539                        )
1540                        .when_some(output.and_then(|o| o.exit_status), |this, status| {
1541                            this.tooltip(Tooltip::text(format!(
1542                                "Exited with code {}",
1543                                status.code().unwrap_or(-1),
1544                            )))
1545                        }),
1546                )
1547            })
1548            .when(truncated_output, |header| {
1549                let tooltip = if let Some(output) = output {
1550                    if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
1551                        "Output exceeded terminal max lines and was \
1552                            truncated, the model received the first 16 KB."
1553                            .to_string()
1554                    } else {
1555                        format!(
1556                            "Output is {} long—to avoid unexpected token usage, \
1557                                only 16 KB was sent back to the model.",
1558                            format_file_size(output.original_content_len as u64, true),
1559                        )
1560                    }
1561                } else {
1562                    "Output was truncated".to_string()
1563                };
1564
1565                header.child(
1566                    h_flex()
1567                        .id(("terminal-tool-truncated-label", terminal.entity_id()))
1568                        .gap_1()
1569                        .child(
1570                            Icon::new(IconName::Info)
1571                                .size(IconSize::XSmall)
1572                                .color(Color::Ignored),
1573                        )
1574                        .child(
1575                            Label::new("Truncated")
1576                                .color(Color::Muted)
1577                                .size(LabelSize::XSmall),
1578                        )
1579                        .tooltip(Tooltip::text(tooltip)),
1580                )
1581            })
1582            .when(time_elapsed > Duration::from_secs(10), |header| {
1583                header.child(
1584                    Label::new(format!("({})", duration_alt_display(time_elapsed)))
1585                        .buffer_font(cx)
1586                        .color(Color::Muted)
1587                        .size(LabelSize::XSmall),
1588                )
1589            })
1590            .child(
1591                Disclosure::new(
1592                    SharedString::from(format!(
1593                        "terminal-tool-disclosure-{}",
1594                        terminal.entity_id()
1595                    )),
1596                    self.terminal_expanded,
1597                )
1598                .opened_icon(IconName::ChevronUp)
1599                .closed_icon(IconName::ChevronDown)
1600                .on_click(cx.listener(move |this, _event, _window, _cx| {
1601                    this.terminal_expanded = !this.terminal_expanded;
1602                })),
1603            );
1604
1605        let terminal_view = self
1606            .entry_view_state
1607            .entry(entry_ix)
1608            .and_then(|entry| entry.terminal(&terminal));
1609        let show_output = self.terminal_expanded && terminal_view.is_some();
1610
1611        v_flex()
1612            .mb_2()
1613            .border_1()
1614            .when(tool_failed || command_failed, |card| card.border_dashed())
1615            .border_color(border_color)
1616            .rounded_lg()
1617            .overflow_hidden()
1618            .child(
1619                v_flex()
1620                    .py_1p5()
1621                    .pl_2()
1622                    .pr_1p5()
1623                    .gap_0p5()
1624                    .bg(header_bg)
1625                    .text_xs()
1626                    .child(header)
1627                    .child(
1628                        MarkdownElement::new(
1629                            command.clone(),
1630                            terminal_command_markdown_style(window, cx),
1631                        )
1632                        .code_block_renderer(
1633                            markdown::CodeBlockRenderer::Default {
1634                                copy_button: false,
1635                                copy_button_on_hover: true,
1636                                border: false,
1637                            },
1638                        ),
1639                    ),
1640            )
1641            .when(show_output, |this| {
1642                this.child(
1643                    div()
1644                        .pt_2()
1645                        .border_t_1()
1646                        .when(tool_failed || command_failed, |card| card.border_dashed())
1647                        .border_color(border_color)
1648                        .bg(cx.theme().colors().editor_background)
1649                        .rounded_b_md()
1650                        .text_ui_sm(cx)
1651                        .children(terminal_view.clone()),
1652                )
1653            })
1654            .into_any()
1655    }
1656
1657    fn render_agent_logo(&self) -> AnyElement {
1658        Icon::new(self.agent.logo())
1659            .color(Color::Muted)
1660            .size(IconSize::XLarge)
1661            .into_any_element()
1662    }
1663
1664    fn render_error_agent_logo(&self) -> AnyElement {
1665        let logo = Icon::new(self.agent.logo())
1666            .color(Color::Muted)
1667            .size(IconSize::XLarge)
1668            .into_any_element();
1669
1670        h_flex()
1671            .relative()
1672            .justify_center()
1673            .child(div().opacity(0.3).child(logo))
1674            .child(
1675                h_flex().absolute().right_1().bottom_0().child(
1676                    Icon::new(IconName::XCircle)
1677                        .color(Color::Error)
1678                        .size(IconSize::Small),
1679                ),
1680            )
1681            .into_any_element()
1682    }
1683
1684    fn render_empty_state(&self, cx: &App) -> AnyElement {
1685        let loading = matches!(&self.thread_state, ThreadState::Loading { .. });
1686
1687        v_flex()
1688            .size_full()
1689            .items_center()
1690            .justify_center()
1691            .child(if loading {
1692                h_flex()
1693                    .justify_center()
1694                    .child(self.render_agent_logo())
1695                    .with_animation(
1696                        "pulsating_icon",
1697                        Animation::new(Duration::from_secs(2))
1698                            .repeat()
1699                            .with_easing(pulsating_between(0.4, 1.0)),
1700                        |icon, delta| icon.opacity(delta),
1701                    )
1702                    .into_any()
1703            } else {
1704                self.render_agent_logo().into_any_element()
1705            })
1706            .child(h_flex().mt_4().mb_1().justify_center().child(if loading {
1707                div()
1708                    .child(LoadingLabel::new("").size(LabelSize::Large))
1709                    .into_any_element()
1710            } else {
1711                Headline::new(self.agent.empty_state_headline())
1712                    .size(HeadlineSize::Medium)
1713                    .into_any_element()
1714            }))
1715            .child(
1716                div()
1717                    .max_w_1_2()
1718                    .text_sm()
1719                    .text_center()
1720                    .map(|this| {
1721                        if loading {
1722                            this.invisible()
1723                        } else {
1724                            this.text_color(cx.theme().colors().text_muted)
1725                        }
1726                    })
1727                    .child(self.agent.empty_state_message()),
1728            )
1729            .into_any()
1730    }
1731
1732    fn render_pending_auth_state(&self) -> AnyElement {
1733        v_flex()
1734            .items_center()
1735            .justify_center()
1736            .child(self.render_error_agent_logo())
1737            .child(
1738                h_flex()
1739                    .mt_4()
1740                    .mb_1()
1741                    .justify_center()
1742                    .child(Headline::new("Not Authenticated").size(HeadlineSize::Medium)),
1743            )
1744            .into_any()
1745    }
1746
1747    fn render_server_exited(&self, status: ExitStatus, _cx: &Context<Self>) -> AnyElement {
1748        v_flex()
1749            .items_center()
1750            .justify_center()
1751            .child(self.render_error_agent_logo())
1752            .child(
1753                v_flex()
1754                    .mt_4()
1755                    .mb_2()
1756                    .gap_0p5()
1757                    .text_center()
1758                    .items_center()
1759                    .child(Headline::new("Server exited unexpectedly").size(HeadlineSize::Medium))
1760                    .child(
1761                        Label::new(format!("Exit status: {}", status.code().unwrap_or(-127)))
1762                            .size(LabelSize::Small)
1763                            .color(Color::Muted),
1764                    ),
1765            )
1766            .into_any_element()
1767    }
1768
1769    fn render_load_error(&self, e: &LoadError, cx: &Context<Self>) -> AnyElement {
1770        let mut container = v_flex()
1771            .items_center()
1772            .justify_center()
1773            .child(self.render_error_agent_logo())
1774            .child(
1775                v_flex()
1776                    .mt_4()
1777                    .mb_2()
1778                    .gap_0p5()
1779                    .text_center()
1780                    .items_center()
1781                    .child(Headline::new("Failed to launch").size(HeadlineSize::Medium))
1782                    .child(
1783                        Label::new(e.to_string())
1784                            .size(LabelSize::Small)
1785                            .color(Color::Muted),
1786                    ),
1787            );
1788
1789        if let LoadError::Unsupported {
1790            upgrade_message,
1791            upgrade_command,
1792            ..
1793        } = &e
1794        {
1795            let upgrade_message = upgrade_message.clone();
1796            let upgrade_command = upgrade_command.clone();
1797            container = container.child(Button::new("upgrade", upgrade_message).on_click(
1798                cx.listener(move |this, _, window, cx| {
1799                    this.workspace
1800                        .update(cx, |workspace, cx| {
1801                            let project = workspace.project().read(cx);
1802                            let cwd = project.first_project_directory(cx);
1803                            let shell = project.terminal_settings(&cwd, cx).shell.clone();
1804                            let spawn_in_terminal = task::SpawnInTerminal {
1805                                id: task::TaskId("install".to_string()),
1806                                full_label: upgrade_command.clone(),
1807                                label: upgrade_command.clone(),
1808                                command: Some(upgrade_command.clone()),
1809                                args: Vec::new(),
1810                                command_label: upgrade_command.clone(),
1811                                cwd,
1812                                env: Default::default(),
1813                                use_new_terminal: true,
1814                                allow_concurrent_runs: true,
1815                                reveal: Default::default(),
1816                                reveal_target: Default::default(),
1817                                hide: Default::default(),
1818                                shell,
1819                                show_summary: true,
1820                                show_command: true,
1821                                show_rerun: false,
1822                            };
1823                            workspace
1824                                .spawn_in_terminal(spawn_in_terminal, window, cx)
1825                                .detach();
1826                        })
1827                        .ok();
1828                }),
1829            ));
1830        }
1831
1832        container.into_any()
1833    }
1834
1835    fn render_activity_bar(
1836        &self,
1837        thread_entity: &Entity<AcpThread>,
1838        window: &mut Window,
1839        cx: &Context<Self>,
1840    ) -> Option<AnyElement> {
1841        let thread = thread_entity.read(cx);
1842        let action_log = thread.action_log();
1843        let changed_buffers = action_log.read(cx).changed_buffers(cx);
1844        let plan = thread.plan();
1845
1846        if changed_buffers.is_empty() && plan.is_empty() {
1847            return None;
1848        }
1849
1850        let editor_bg_color = cx.theme().colors().editor_background;
1851        let active_color = cx.theme().colors().element_selected;
1852        let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
1853
1854        let pending_edits = thread.has_pending_edit_tool_calls();
1855
1856        v_flex()
1857            .mt_1()
1858            .mx_2()
1859            .bg(bg_edit_files_disclosure)
1860            .border_1()
1861            .border_b_0()
1862            .border_color(cx.theme().colors().border)
1863            .rounded_t_md()
1864            .shadow(vec![gpui::BoxShadow {
1865                color: gpui::black().opacity(0.15),
1866                offset: point(px(1.), px(-1.)),
1867                blur_radius: px(3.),
1868                spread_radius: px(0.),
1869            }])
1870            .when(!plan.is_empty(), |this| {
1871                this.child(self.render_plan_summary(plan, window, cx))
1872                    .when(self.plan_expanded, |parent| {
1873                        parent.child(self.render_plan_entries(plan, window, cx))
1874                    })
1875            })
1876            .when(!plan.is_empty() && !changed_buffers.is_empty(), |this| {
1877                this.child(Divider::horizontal().color(DividerColor::Border))
1878            })
1879            .when(!changed_buffers.is_empty(), |this| {
1880                this.child(self.render_edits_summary(
1881                    action_log,
1882                    &changed_buffers,
1883                    self.edits_expanded,
1884                    pending_edits,
1885                    window,
1886                    cx,
1887                ))
1888                .when(self.edits_expanded, |parent| {
1889                    parent.child(self.render_edited_files(
1890                        action_log,
1891                        &changed_buffers,
1892                        pending_edits,
1893                        cx,
1894                    ))
1895                })
1896            })
1897            .into_any()
1898            .into()
1899    }
1900
1901    fn render_plan_summary(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
1902        let stats = plan.stats();
1903
1904        let title = if let Some(entry) = stats.in_progress_entry
1905            && !self.plan_expanded
1906        {
1907            h_flex()
1908                .w_full()
1909                .cursor_default()
1910                .gap_1()
1911                .text_xs()
1912                .text_color(cx.theme().colors().text_muted)
1913                .justify_between()
1914                .child(
1915                    h_flex()
1916                        .gap_1()
1917                        .child(
1918                            Label::new("Current:")
1919                                .size(LabelSize::Small)
1920                                .color(Color::Muted),
1921                        )
1922                        .child(MarkdownElement::new(
1923                            entry.content.clone(),
1924                            plan_label_markdown_style(&entry.status, window, cx),
1925                        )),
1926                )
1927                .when(stats.pending > 0, |this| {
1928                    this.child(
1929                        Label::new(format!("{} left", stats.pending))
1930                            .size(LabelSize::Small)
1931                            .color(Color::Muted)
1932                            .mr_1(),
1933                    )
1934                })
1935        } else {
1936            let status_label = if stats.pending == 0 {
1937                "All Done".to_string()
1938            } else if stats.completed == 0 {
1939                format!("{} Tasks", plan.entries.len())
1940            } else {
1941                format!("{}/{}", stats.completed, plan.entries.len())
1942            };
1943
1944            h_flex()
1945                .w_full()
1946                .gap_1()
1947                .justify_between()
1948                .child(
1949                    Label::new("Plan")
1950                        .size(LabelSize::Small)
1951                        .color(Color::Muted),
1952                )
1953                .child(
1954                    Label::new(status_label)
1955                        .size(LabelSize::Small)
1956                        .color(Color::Muted)
1957                        .mr_1(),
1958                )
1959        };
1960
1961        h_flex()
1962            .p_1()
1963            .justify_between()
1964            .when(self.plan_expanded, |this| {
1965                this.border_b_1().border_color(cx.theme().colors().border)
1966            })
1967            .child(
1968                h_flex()
1969                    .id("plan_summary")
1970                    .w_full()
1971                    .gap_1()
1972                    .child(Disclosure::new("plan_disclosure", self.plan_expanded))
1973                    .child(title)
1974                    .on_click(cx.listener(|this, _, _, cx| {
1975                        this.plan_expanded = !this.plan_expanded;
1976                        cx.notify();
1977                    })),
1978            )
1979    }
1980
1981    fn render_plan_entries(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
1982        v_flex().children(plan.entries.iter().enumerate().flat_map(|(index, entry)| {
1983            let element = h_flex()
1984                .py_1()
1985                .px_2()
1986                .gap_2()
1987                .justify_between()
1988                .bg(cx.theme().colors().editor_background)
1989                .when(index < plan.entries.len() - 1, |parent| {
1990                    parent.border_color(cx.theme().colors().border).border_b_1()
1991                })
1992                .child(
1993                    h_flex()
1994                        .id(("plan_entry", index))
1995                        .gap_1p5()
1996                        .max_w_full()
1997                        .overflow_x_scroll()
1998                        .text_xs()
1999                        .text_color(cx.theme().colors().text_muted)
2000                        .child(match entry.status {
2001                            acp::PlanEntryStatus::Pending => Icon::new(IconName::TodoPending)
2002                                .size(IconSize::Small)
2003                                .color(Color::Muted)
2004                                .into_any_element(),
2005                            acp::PlanEntryStatus::InProgress => Icon::new(IconName::TodoProgress)
2006                                .size(IconSize::Small)
2007                                .color(Color::Accent)
2008                                .with_animation(
2009                                    "running",
2010                                    Animation::new(Duration::from_secs(2)).repeat(),
2011                                    |icon, delta| {
2012                                        icon.transform(Transformation::rotate(percentage(delta)))
2013                                    },
2014                                )
2015                                .into_any_element(),
2016                            acp::PlanEntryStatus::Completed => Icon::new(IconName::TodoComplete)
2017                                .size(IconSize::Small)
2018                                .color(Color::Success)
2019                                .into_any_element(),
2020                        })
2021                        .child(MarkdownElement::new(
2022                            entry.content.clone(),
2023                            plan_label_markdown_style(&entry.status, window, cx),
2024                        )),
2025                );
2026
2027            Some(element)
2028        }))
2029    }
2030
2031    fn render_edits_summary(
2032        &self,
2033        action_log: &Entity<ActionLog>,
2034        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
2035        expanded: bool,
2036        pending_edits: bool,
2037        window: &mut Window,
2038        cx: &Context<Self>,
2039    ) -> Div {
2040        const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
2041
2042        let focus_handle = self.focus_handle(cx);
2043
2044        h_flex()
2045            .p_1()
2046            .justify_between()
2047            .when(expanded, |this| {
2048                this.border_b_1().border_color(cx.theme().colors().border)
2049            })
2050            .child(
2051                h_flex()
2052                    .id("edits-container")
2053                    .w_full()
2054                    .gap_1()
2055                    .child(Disclosure::new("edits-disclosure", expanded))
2056                    .map(|this| {
2057                        if pending_edits {
2058                            this.child(
2059                                Label::new(format!(
2060                                    "Editing {} {}",
2061                                    changed_buffers.len(),
2062                                    if changed_buffers.len() == 1 {
2063                                        "file"
2064                                    } else {
2065                                        "files"
2066                                    }
2067                                ))
2068                                .color(Color::Muted)
2069                                .size(LabelSize::Small)
2070                                .with_animation(
2071                                    "edit-label",
2072                                    Animation::new(Duration::from_secs(2))
2073                                        .repeat()
2074                                        .with_easing(pulsating_between(0.3, 0.7)),
2075                                    |label, delta| label.alpha(delta),
2076                                ),
2077                            )
2078                        } else {
2079                            this.child(
2080                                Label::new("Edits")
2081                                    .size(LabelSize::Small)
2082                                    .color(Color::Muted),
2083                            )
2084                            .child(Label::new("").size(LabelSize::XSmall).color(Color::Muted))
2085                            .child(
2086                                Label::new(format!(
2087                                    "{} {}",
2088                                    changed_buffers.len(),
2089                                    if changed_buffers.len() == 1 {
2090                                        "file"
2091                                    } else {
2092                                        "files"
2093                                    }
2094                                ))
2095                                .size(LabelSize::Small)
2096                                .color(Color::Muted),
2097                            )
2098                        }
2099                    })
2100                    .on_click(cx.listener(|this, _, _, cx| {
2101                        this.edits_expanded = !this.edits_expanded;
2102                        cx.notify();
2103                    })),
2104            )
2105            .child(
2106                h_flex()
2107                    .gap_1()
2108                    .child(
2109                        IconButton::new("review-changes", IconName::ListTodo)
2110                            .icon_size(IconSize::Small)
2111                            .tooltip({
2112                                let focus_handle = focus_handle.clone();
2113                                move |window, cx| {
2114                                    Tooltip::for_action_in(
2115                                        "Review Changes",
2116                                        &OpenAgentDiff,
2117                                        &focus_handle,
2118                                        window,
2119                                        cx,
2120                                    )
2121                                }
2122                            })
2123                            .on_click(cx.listener(|_, _, window, cx| {
2124                                window.dispatch_action(OpenAgentDiff.boxed_clone(), cx);
2125                            })),
2126                    )
2127                    .child(Divider::vertical().color(DividerColor::Border))
2128                    .child(
2129                        Button::new("reject-all-changes", "Reject All")
2130                            .label_size(LabelSize::Small)
2131                            .disabled(pending_edits)
2132                            .when(pending_edits, |this| {
2133                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
2134                            })
2135                            .key_binding(
2136                                KeyBinding::for_action_in(
2137                                    &RejectAll,
2138                                    &focus_handle.clone(),
2139                                    window,
2140                                    cx,
2141                                )
2142                                .map(|kb| kb.size(rems_from_px(10.))),
2143                            )
2144                            .on_click({
2145                                let action_log = action_log.clone();
2146                                cx.listener(move |_, _, _, cx| {
2147                                    action_log.update(cx, |action_log, cx| {
2148                                        action_log.reject_all_edits(cx).detach();
2149                                    })
2150                                })
2151                            }),
2152                    )
2153                    .child(
2154                        Button::new("keep-all-changes", "Keep All")
2155                            .label_size(LabelSize::Small)
2156                            .disabled(pending_edits)
2157                            .when(pending_edits, |this| {
2158                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
2159                            })
2160                            .key_binding(
2161                                KeyBinding::for_action_in(&KeepAll, &focus_handle, window, cx)
2162                                    .map(|kb| kb.size(rems_from_px(10.))),
2163                            )
2164                            .on_click({
2165                                let action_log = action_log.clone();
2166                                cx.listener(move |_, _, _, cx| {
2167                                    action_log.update(cx, |action_log, cx| {
2168                                        action_log.keep_all_edits(cx);
2169                                    })
2170                                })
2171                            }),
2172                    ),
2173            )
2174    }
2175
2176    fn render_edited_files(
2177        &self,
2178        action_log: &Entity<ActionLog>,
2179        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
2180        pending_edits: bool,
2181        cx: &Context<Self>,
2182    ) -> Div {
2183        let editor_bg_color = cx.theme().colors().editor_background;
2184
2185        v_flex().children(changed_buffers.into_iter().enumerate().flat_map(
2186            |(index, (buffer, _diff))| {
2187                let file = buffer.read(cx).file()?;
2188                let path = file.path();
2189
2190                let file_path = path.parent().and_then(|parent| {
2191                    let parent_str = parent.to_string_lossy();
2192
2193                    if parent_str.is_empty() {
2194                        None
2195                    } else {
2196                        Some(
2197                            Label::new(format!("/{}{}", parent_str, std::path::MAIN_SEPARATOR_STR))
2198                                .color(Color::Muted)
2199                                .size(LabelSize::XSmall)
2200                                .buffer_font(cx),
2201                        )
2202                    }
2203                });
2204
2205                let file_name = path.file_name().map(|name| {
2206                    Label::new(name.to_string_lossy().to_string())
2207                        .size(LabelSize::XSmall)
2208                        .buffer_font(cx)
2209                });
2210
2211                let file_icon = FileIcons::get_icon(&path, cx)
2212                    .map(Icon::from_path)
2213                    .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
2214                    .unwrap_or_else(|| {
2215                        Icon::new(IconName::File)
2216                            .color(Color::Muted)
2217                            .size(IconSize::Small)
2218                    });
2219
2220                let overlay_gradient = linear_gradient(
2221                    90.,
2222                    linear_color_stop(editor_bg_color, 1.),
2223                    linear_color_stop(editor_bg_color.opacity(0.2), 0.),
2224                );
2225
2226                let element = h_flex()
2227                    .group("edited-code")
2228                    .id(("file-container", index))
2229                    .relative()
2230                    .py_1()
2231                    .pl_2()
2232                    .pr_1()
2233                    .gap_2()
2234                    .justify_between()
2235                    .bg(editor_bg_color)
2236                    .when(index < changed_buffers.len() - 1, |parent| {
2237                        parent.border_color(cx.theme().colors().border).border_b_1()
2238                    })
2239                    .child(
2240                        h_flex()
2241                            .id(("file-name", index))
2242                            .pr_8()
2243                            .gap_1p5()
2244                            .max_w_full()
2245                            .overflow_x_scroll()
2246                            .child(file_icon)
2247                            .child(h_flex().gap_0p5().children(file_name).children(file_path))
2248                            .on_click({
2249                                let buffer = buffer.clone();
2250                                cx.listener(move |this, _, window, cx| {
2251                                    this.open_edited_buffer(&buffer, window, cx);
2252                                })
2253                            }),
2254                    )
2255                    .child(
2256                        h_flex()
2257                            .gap_1()
2258                            .visible_on_hover("edited-code")
2259                            .child(
2260                                Button::new("review", "Review")
2261                                    .label_size(LabelSize::Small)
2262                                    .on_click({
2263                                        let buffer = buffer.clone();
2264                                        cx.listener(move |this, _, window, cx| {
2265                                            this.open_edited_buffer(&buffer, window, cx);
2266                                        })
2267                                    }),
2268                            )
2269                            .child(Divider::vertical().color(DividerColor::BorderVariant))
2270                            .child(
2271                                Button::new("reject-file", "Reject")
2272                                    .label_size(LabelSize::Small)
2273                                    .disabled(pending_edits)
2274                                    .on_click({
2275                                        let buffer = buffer.clone();
2276                                        let action_log = action_log.clone();
2277                                        move |_, _, cx| {
2278                                            action_log.update(cx, |action_log, cx| {
2279                                                action_log
2280                                                    .reject_edits_in_ranges(
2281                                                        buffer.clone(),
2282                                                        vec![Anchor::MIN..Anchor::MAX],
2283                                                        cx,
2284                                                    )
2285                                                    .detach_and_log_err(cx);
2286                                            })
2287                                        }
2288                                    }),
2289                            )
2290                            .child(
2291                                Button::new("keep-file", "Keep")
2292                                    .label_size(LabelSize::Small)
2293                                    .disabled(pending_edits)
2294                                    .on_click({
2295                                        let buffer = buffer.clone();
2296                                        let action_log = action_log.clone();
2297                                        move |_, _, cx| {
2298                                            action_log.update(cx, |action_log, cx| {
2299                                                action_log.keep_edits_in_range(
2300                                                    buffer.clone(),
2301                                                    Anchor::MIN..Anchor::MAX,
2302                                                    cx,
2303                                                );
2304                                            })
2305                                        }
2306                                    }),
2307                            ),
2308                    )
2309                    .child(
2310                        div()
2311                            .id("gradient-overlay")
2312                            .absolute()
2313                            .h_full()
2314                            .w_12()
2315                            .top_0()
2316                            .bottom_0()
2317                            .right(px(152.))
2318                            .bg(overlay_gradient),
2319                    );
2320
2321                Some(element)
2322            },
2323        ))
2324    }
2325
2326    fn render_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
2327        let focus_handle = self.message_editor.focus_handle(cx);
2328        let editor_bg_color = cx.theme().colors().editor_background;
2329        let (expand_icon, expand_tooltip) = if self.editor_expanded {
2330            (IconName::Minimize, "Minimize Message Editor")
2331        } else {
2332            (IconName::Maximize, "Expand Message Editor")
2333        };
2334
2335        v_flex()
2336            .on_action(cx.listener(Self::expand_message_editor))
2337            .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
2338                if let Some(model_selector) = this.model_selector.as_ref() {
2339                    model_selector
2340                        .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
2341                }
2342            }))
2343            .p_2()
2344            .gap_2()
2345            .border_t_1()
2346            .border_color(cx.theme().colors().border)
2347            .bg(editor_bg_color)
2348            .when(self.editor_expanded, |this| {
2349                this.h(vh(0.8, window)).size_full().justify_between()
2350            })
2351            .child(
2352                v_flex()
2353                    .relative()
2354                    .size_full()
2355                    .pt_1()
2356                    .pr_2p5()
2357                    .child(self.message_editor.clone())
2358                    .child(
2359                        h_flex()
2360                            .absolute()
2361                            .top_0()
2362                            .right_0()
2363                            .opacity(0.5)
2364                            .hover(|this| this.opacity(1.0))
2365                            .child(
2366                                IconButton::new("toggle-height", expand_icon)
2367                                    .icon_size(IconSize::Small)
2368                                    .icon_color(Color::Muted)
2369                                    .tooltip({
2370                                        let focus_handle = focus_handle.clone();
2371                                        move |window, cx| {
2372                                            Tooltip::for_action_in(
2373                                                expand_tooltip,
2374                                                &ExpandMessageEditor,
2375                                                &focus_handle,
2376                                                window,
2377                                                cx,
2378                                            )
2379                                        }
2380                                    })
2381                                    .on_click(cx.listener(|_, _, window, cx| {
2382                                        window.dispatch_action(Box::new(ExpandMessageEditor), cx);
2383                                    })),
2384                            ),
2385                    ),
2386            )
2387            .child(
2388                h_flex()
2389                    .flex_none()
2390                    .justify_between()
2391                    .child(
2392                        h_flex()
2393                            .gap_1()
2394                            .child(self.render_follow_toggle(cx))
2395                            .children(self.render_burn_mode_toggle(cx)),
2396                    )
2397                    .child(
2398                        h_flex()
2399                            .gap_1()
2400                            .children(self.model_selector.clone())
2401                            .child(self.render_send_button(cx)),
2402                    ),
2403            )
2404            .into_any()
2405    }
2406
2407    fn as_native_connection(&self, cx: &App) -> Option<Rc<agent2::NativeAgentConnection>> {
2408        let acp_thread = self.thread()?.read(cx);
2409        acp_thread.connection().clone().downcast()
2410    }
2411
2412    fn as_native_thread(&self, cx: &App) -> Option<Entity<agent2::Thread>> {
2413        let acp_thread = self.thread()?.read(cx);
2414        self.as_native_connection(cx)?
2415            .thread(acp_thread.session_id(), cx)
2416    }
2417
2418    fn toggle_burn_mode(
2419        &mut self,
2420        _: &ToggleBurnMode,
2421        _window: &mut Window,
2422        cx: &mut Context<Self>,
2423    ) {
2424        let Some(thread) = self.as_native_thread(cx) else {
2425            return;
2426        };
2427
2428        thread.update(cx, |thread, _cx| {
2429            let current_mode = thread.completion_mode();
2430            thread.set_completion_mode(match current_mode {
2431                CompletionMode::Burn => CompletionMode::Normal,
2432                CompletionMode::Normal => CompletionMode::Burn,
2433            });
2434        });
2435    }
2436
2437    fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
2438        let thread = self.as_native_thread(cx)?.read(cx);
2439
2440        if !thread.model().supports_burn_mode() {
2441            return None;
2442        }
2443
2444        let active_completion_mode = thread.completion_mode();
2445        let burn_mode_enabled = active_completion_mode == CompletionMode::Burn;
2446        let icon = if burn_mode_enabled {
2447            IconName::ZedBurnModeOn
2448        } else {
2449            IconName::ZedBurnMode
2450        };
2451
2452        Some(
2453            IconButton::new("burn-mode", icon)
2454                .icon_size(IconSize::Small)
2455                .icon_color(Color::Muted)
2456                .toggle_state(burn_mode_enabled)
2457                .selected_icon_color(Color::Error)
2458                .on_click(cx.listener(|this, _event, window, cx| {
2459                    this.toggle_burn_mode(&ToggleBurnMode, window, cx);
2460                }))
2461                .tooltip(move |_window, cx| {
2462                    cx.new(|_| BurnModeTooltip::new().selected(burn_mode_enabled))
2463                        .into()
2464                })
2465                .into_any_element(),
2466        )
2467    }
2468
2469    fn start_editing_message(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
2470        let Some(thread) = self.thread() else {
2471            return;
2472        };
2473        let Some(AgentThreadEntry::UserMessage(message)) = thread.read(cx).entries().get(index)
2474        else {
2475            return;
2476        };
2477        let Some(message_id) = message.id.clone() else {
2478            return;
2479        };
2480
2481        self.list_state.scroll_to_reveal_item(index);
2482
2483        let chunks = message.chunks.clone();
2484        let editor = cx.new(|cx| {
2485            let mut editor = MessageEditor::new(
2486                self.workspace.clone(),
2487                self.project.clone(),
2488                self.thread_store.clone(),
2489                self.text_thread_store.clone(),
2490                editor::EditorMode::AutoHeight {
2491                    min_lines: 1,
2492                    max_lines: None,
2493                },
2494                window,
2495                cx,
2496            );
2497            editor.set_message(chunks, window, cx);
2498            editor
2499        });
2500        let subscription =
2501            cx.subscribe_in(&editor, window, |this, _, event, window, cx| match event {
2502                MessageEditorEvent::Send => {
2503                    this.regenerate(&Default::default(), window, cx);
2504                }
2505                MessageEditorEvent::Cancel => {
2506                    this.cancel_editing(&Default::default(), window, cx);
2507                }
2508            });
2509        editor.focus_handle(cx).focus(window);
2510
2511        self.editing_message.replace(EditingMessage {
2512            index: index,
2513            message_id: message_id.clone(),
2514            editor,
2515            _subscription: subscription,
2516        });
2517        cx.notify();
2518    }
2519
2520    fn render_edit_message_editor(&self, editing: &EditingMessage, cx: &Context<Self>) -> Div {
2521        v_flex()
2522            .w_full()
2523            .gap_2()
2524            .child(editing.editor.clone())
2525            .child(
2526                h_flex()
2527                    .gap_1()
2528                    .child(
2529                        Icon::new(IconName::Warning)
2530                            .color(Color::Warning)
2531                            .size(IconSize::XSmall),
2532                    )
2533                    .child(
2534                        Label::new("Editing will restart the thread from this point.")
2535                            .color(Color::Muted)
2536                            .size(LabelSize::XSmall),
2537                    )
2538                    .child(self.render_editing_message_editor_buttons(editing, cx)),
2539            )
2540    }
2541
2542    fn render_editing_message_editor_buttons(
2543        &self,
2544        editing: &EditingMessage,
2545        cx: &Context<Self>,
2546    ) -> Div {
2547        h_flex()
2548            .gap_0p5()
2549            .flex_1()
2550            .justify_end()
2551            .child(
2552                IconButton::new("cancel-edit-message", IconName::Close)
2553                    .shape(ui::IconButtonShape::Square)
2554                    .icon_color(Color::Error)
2555                    .icon_size(IconSize::Small)
2556                    .tooltip({
2557                        let focus_handle = editing.editor.focus_handle(cx);
2558                        move |window, cx| {
2559                            Tooltip::for_action_in(
2560                                "Cancel Edit",
2561                                &menu::Cancel,
2562                                &focus_handle,
2563                                window,
2564                                cx,
2565                            )
2566                        }
2567                    })
2568                    .on_click(cx.listener(Self::cancel_editing)),
2569            )
2570            .child(
2571                IconButton::new("confirm-edit-message", IconName::Return)
2572                    .disabled(editing.editor.read(cx).is_empty(cx))
2573                    .shape(ui::IconButtonShape::Square)
2574                    .icon_color(Color::Muted)
2575                    .icon_size(IconSize::Small)
2576                    .tooltip({
2577                        let focus_handle = editing.editor.focus_handle(cx);
2578                        move |window, cx| {
2579                            Tooltip::for_action_in(
2580                                "Regenerate",
2581                                &menu::Confirm,
2582                                &focus_handle,
2583                                window,
2584                                cx,
2585                            )
2586                        }
2587                    })
2588                    .on_click(cx.listener(Self::regenerate)),
2589            )
2590    }
2591
2592    fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
2593        if self.thread().map_or(true, |thread| {
2594            thread.read(cx).status() == ThreadStatus::Idle
2595        }) {
2596            let is_editor_empty = self.message_editor.read(cx).is_empty(cx);
2597            IconButton::new("send-message", IconName::Send)
2598                .icon_color(Color::Accent)
2599                .style(ButtonStyle::Filled)
2600                .disabled(self.thread().is_none() || is_editor_empty)
2601                .when(!is_editor_empty, |button| {
2602                    button.tooltip(move |window, cx| Tooltip::for_action("Send", &Chat, window, cx))
2603                })
2604                .when(is_editor_empty, |button| {
2605                    button.tooltip(Tooltip::text("Type a message to submit"))
2606                })
2607                .on_click(cx.listener(|this, _, window, cx| {
2608                    this.send(window, cx);
2609                }))
2610                .into_any_element()
2611        } else {
2612            IconButton::new("stop-generation", IconName::Stop)
2613                .icon_color(Color::Error)
2614                .style(ButtonStyle::Tinted(ui::TintColor::Error))
2615                .tooltip(move |window, cx| {
2616                    Tooltip::for_action("Stop Generation", &editor::actions::Cancel, window, cx)
2617                })
2618                .on_click(cx.listener(|this, _event, _, cx| this.cancel_generation(cx)))
2619                .into_any_element()
2620        }
2621    }
2622
2623    fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
2624        let following = self
2625            .workspace
2626            .read_with(cx, |workspace, _| {
2627                workspace.is_being_followed(CollaboratorId::Agent)
2628            })
2629            .unwrap_or(false);
2630
2631        IconButton::new("follow-agent", IconName::Crosshair)
2632            .icon_size(IconSize::Small)
2633            .icon_color(Color::Muted)
2634            .toggle_state(following)
2635            .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
2636            .tooltip(move |window, cx| {
2637                if following {
2638                    Tooltip::for_action("Stop Following Agent", &Follow, window, cx)
2639                } else {
2640                    Tooltip::with_meta(
2641                        "Follow Agent",
2642                        Some(&Follow),
2643                        "Track the agent's location as it reads and edits files.",
2644                        window,
2645                        cx,
2646                    )
2647                }
2648            })
2649            .on_click(cx.listener(move |this, _, window, cx| {
2650                this.workspace
2651                    .update(cx, |workspace, cx| {
2652                        if following {
2653                            workspace.unfollow(CollaboratorId::Agent, window, cx);
2654                        } else {
2655                            workspace.follow(CollaboratorId::Agent, window, cx);
2656                        }
2657                    })
2658                    .ok();
2659            }))
2660    }
2661
2662    fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
2663        let workspace = self.workspace.clone();
2664        MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
2665            Self::open_link(text, &workspace, window, cx);
2666        })
2667    }
2668
2669    fn open_link(
2670        url: SharedString,
2671        workspace: &WeakEntity<Workspace>,
2672        window: &mut Window,
2673        cx: &mut App,
2674    ) {
2675        let Some(workspace) = workspace.upgrade() else {
2676            cx.open_url(&url);
2677            return;
2678        };
2679
2680        if let Some(mention) = MentionUri::parse(&url).log_err() {
2681            workspace.update(cx, |workspace, cx| match mention {
2682                MentionUri::File { abs_path, .. } => {
2683                    let project = workspace.project();
2684                    let Some((path, entry)) = project.update(cx, |project, cx| {
2685                        let path = project.find_project_path(abs_path, cx)?;
2686                        let entry = project.entry_for_path(&path, cx)?;
2687                        Some((path, entry))
2688                    }) else {
2689                        return;
2690                    };
2691
2692                    if entry.is_dir() {
2693                        project.update(cx, |_, cx| {
2694                            cx.emit(project::Event::RevealInProjectPanel(entry.id));
2695                        });
2696                    } else {
2697                        workspace
2698                            .open_path(path, None, true, window, cx)
2699                            .detach_and_log_err(cx);
2700                    }
2701                }
2702                MentionUri::Symbol {
2703                    path, line_range, ..
2704                }
2705                | MentionUri::Selection { path, line_range } => {
2706                    let project = workspace.project();
2707                    let Some((path, _)) = project.update(cx, |project, cx| {
2708                        let path = project.find_project_path(path, cx)?;
2709                        let entry = project.entry_for_path(&path, cx)?;
2710                        Some((path, entry))
2711                    }) else {
2712                        return;
2713                    };
2714
2715                    let item = workspace.open_path(path, None, true, window, cx);
2716                    window
2717                        .spawn(cx, async move |cx| {
2718                            let Some(editor) = item.await?.downcast::<Editor>() else {
2719                                return Ok(());
2720                            };
2721                            let range =
2722                                Point::new(line_range.start, 0)..Point::new(line_range.start, 0);
2723                            editor
2724                                .update_in(cx, |editor, window, cx| {
2725                                    editor.change_selections(
2726                                        SelectionEffects::scroll(Autoscroll::center()),
2727                                        window,
2728                                        cx,
2729                                        |s| s.select_ranges(vec![range]),
2730                                    );
2731                                })
2732                                .ok();
2733                            anyhow::Ok(())
2734                        })
2735                        .detach_and_log_err(cx);
2736                }
2737                MentionUri::Thread { id, .. } => {
2738                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
2739                        panel.update(cx, |panel, cx| {
2740                            panel
2741                                .open_thread_by_id(&id, window, cx)
2742                                .detach_and_log_err(cx)
2743                        });
2744                    }
2745                }
2746                MentionUri::TextThread { path, .. } => {
2747                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
2748                        panel.update(cx, |panel, cx| {
2749                            panel
2750                                .open_saved_prompt_editor(path.as_path().into(), window, cx)
2751                                .detach_and_log_err(cx);
2752                        });
2753                    }
2754                }
2755                MentionUri::Rule { id, .. } => {
2756                    let PromptId::User { uuid } = id else {
2757                        return;
2758                    };
2759                    window.dispatch_action(
2760                        Box::new(OpenRulesLibrary {
2761                            prompt_to_select: Some(uuid.0),
2762                        }),
2763                        cx,
2764                    )
2765                }
2766                MentionUri::Fetch { url } => {
2767                    cx.open_url(url.as_str());
2768                }
2769            })
2770        } else {
2771            cx.open_url(&url);
2772        }
2773    }
2774
2775    fn open_tool_call_location(
2776        &self,
2777        entry_ix: usize,
2778        location_ix: usize,
2779        window: &mut Window,
2780        cx: &mut Context<Self>,
2781    ) -> Option<()> {
2782        let (tool_call_location, agent_location) = self
2783            .thread()?
2784            .read(cx)
2785            .entries()
2786            .get(entry_ix)?
2787            .location(location_ix)?;
2788
2789        let project_path = self
2790            .project
2791            .read(cx)
2792            .find_project_path(&tool_call_location.path, cx)?;
2793
2794        let open_task = self
2795            .workspace
2796            .update(cx, |workspace, cx| {
2797                workspace.open_path(project_path, None, true, window, cx)
2798            })
2799            .log_err()?;
2800        window
2801            .spawn(cx, async move |cx| {
2802                let item = open_task.await?;
2803
2804                let Some(active_editor) = item.downcast::<Editor>() else {
2805                    return anyhow::Ok(());
2806                };
2807
2808                active_editor.update_in(cx, |editor, window, cx| {
2809                    let multibuffer = editor.buffer().read(cx);
2810                    let buffer = multibuffer.as_singleton();
2811                    if agent_location.buffer.upgrade() == buffer {
2812                        let excerpt_id = multibuffer.excerpt_ids().first().cloned();
2813                        let anchor = editor::Anchor::in_buffer(
2814                            excerpt_id.unwrap(),
2815                            buffer.unwrap().read(cx).remote_id(),
2816                            agent_location.position,
2817                        );
2818                        editor.change_selections(Default::default(), window, cx, |selections| {
2819                            selections.select_anchor_ranges([anchor..anchor]);
2820                        })
2821                    } else {
2822                        let row = tool_call_location.line.unwrap_or_default();
2823                        editor.change_selections(Default::default(), window, cx, |selections| {
2824                            selections.select_ranges([Point::new(row, 0)..Point::new(row, 0)]);
2825                        })
2826                    }
2827                })?;
2828
2829                anyhow::Ok(())
2830            })
2831            .detach_and_log_err(cx);
2832
2833        None
2834    }
2835
2836    pub fn open_thread_as_markdown(
2837        &self,
2838        workspace: Entity<Workspace>,
2839        window: &mut Window,
2840        cx: &mut App,
2841    ) -> Task<anyhow::Result<()>> {
2842        let markdown_language_task = workspace
2843            .read(cx)
2844            .app_state()
2845            .languages
2846            .language_for_name("Markdown");
2847
2848        let (thread_summary, markdown) = if let Some(thread) = self.thread() {
2849            let thread = thread.read(cx);
2850            (thread.title().to_string(), thread.to_markdown(cx))
2851        } else {
2852            return Task::ready(Ok(()));
2853        };
2854
2855        window.spawn(cx, async move |cx| {
2856            let markdown_language = markdown_language_task.await?;
2857
2858            workspace.update_in(cx, |workspace, window, cx| {
2859                let project = workspace.project().clone();
2860
2861                if !project.read(cx).is_local() {
2862                    bail!("failed to open active thread as markdown in remote project");
2863                }
2864
2865                let buffer = project.update(cx, |project, cx| {
2866                    project.create_local_buffer(&markdown, Some(markdown_language), cx)
2867                });
2868                let buffer = cx.new(|cx| {
2869                    MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
2870                });
2871
2872                workspace.add_item_to_active_pane(
2873                    Box::new(cx.new(|cx| {
2874                        let mut editor =
2875                            Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
2876                        editor.set_breadcrumb_header(thread_summary);
2877                        editor
2878                    })),
2879                    None,
2880                    true,
2881                    window,
2882                    cx,
2883                );
2884
2885                anyhow::Ok(())
2886            })??;
2887            anyhow::Ok(())
2888        })
2889    }
2890
2891    fn scroll_to_top(&mut self, cx: &mut Context<Self>) {
2892        self.list_state.scroll_to(ListOffset::default());
2893        cx.notify();
2894    }
2895
2896    pub fn scroll_to_bottom(&mut self, cx: &mut Context<Self>) {
2897        if let Some(thread) = self.thread() {
2898            let entry_count = thread.read(cx).entries().len();
2899            self.list_state.reset(entry_count);
2900            cx.notify();
2901        }
2902    }
2903
2904    fn notify_with_sound(
2905        &mut self,
2906        caption: impl Into<SharedString>,
2907        icon: IconName,
2908        window: &mut Window,
2909        cx: &mut Context<Self>,
2910    ) {
2911        self.play_notification_sound(window, cx);
2912        self.show_notification(caption, icon, window, cx);
2913    }
2914
2915    fn play_notification_sound(&self, window: &Window, cx: &mut App) {
2916        let settings = AgentSettings::get_global(cx);
2917        if settings.play_sound_when_agent_done && !window.is_window_active() {
2918            Audio::play_sound(Sound::AgentDone, cx);
2919        }
2920    }
2921
2922    fn show_notification(
2923        &mut self,
2924        caption: impl Into<SharedString>,
2925        icon: IconName,
2926        window: &mut Window,
2927        cx: &mut Context<Self>,
2928    ) {
2929        if window.is_window_active() || !self.notifications.is_empty() {
2930            return;
2931        }
2932
2933        let title = self.title(cx);
2934
2935        match AgentSettings::get_global(cx).notify_when_agent_waiting {
2936            NotifyWhenAgentWaiting::PrimaryScreen => {
2937                if let Some(primary) = cx.primary_display() {
2938                    self.pop_up(icon, caption.into(), title, window, primary, cx);
2939                }
2940            }
2941            NotifyWhenAgentWaiting::AllScreens => {
2942                let caption = caption.into();
2943                for screen in cx.displays() {
2944                    self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
2945                }
2946            }
2947            NotifyWhenAgentWaiting::Never => {
2948                // Don't show anything
2949            }
2950        }
2951    }
2952
2953    fn pop_up(
2954        &mut self,
2955        icon: IconName,
2956        caption: SharedString,
2957        title: SharedString,
2958        window: &mut Window,
2959        screen: Rc<dyn PlatformDisplay>,
2960        cx: &mut Context<Self>,
2961    ) {
2962        let options = AgentNotification::window_options(screen, cx);
2963
2964        let project_name = self.workspace.upgrade().and_then(|workspace| {
2965            workspace
2966                .read(cx)
2967                .project()
2968                .read(cx)
2969                .visible_worktrees(cx)
2970                .next()
2971                .map(|worktree| worktree.read(cx).root_name().to_string())
2972        });
2973
2974        if let Some(screen_window) = cx
2975            .open_window(options, |_, cx| {
2976                cx.new(|_| {
2977                    AgentNotification::new(title.clone(), caption.clone(), icon, project_name)
2978                })
2979            })
2980            .log_err()
2981        {
2982            if let Some(pop_up) = screen_window.entity(cx).log_err() {
2983                self.notification_subscriptions
2984                    .entry(screen_window)
2985                    .or_insert_with(Vec::new)
2986                    .push(cx.subscribe_in(&pop_up, window, {
2987                        |this, _, event, window, cx| match event {
2988                            AgentNotificationEvent::Accepted => {
2989                                let handle = window.window_handle();
2990                                cx.activate(true);
2991
2992                                let workspace_handle = this.workspace.clone();
2993
2994                                // If there are multiple Zed windows, activate the correct one.
2995                                cx.defer(move |cx| {
2996                                    handle
2997                                        .update(cx, |_view, window, _cx| {
2998                                            window.activate_window();
2999
3000                                            if let Some(workspace) = workspace_handle.upgrade() {
3001                                                workspace.update(_cx, |workspace, cx| {
3002                                                    workspace.focus_panel::<AgentPanel>(window, cx);
3003                                                });
3004                                            }
3005                                        })
3006                                        .log_err();
3007                                });
3008
3009                                this.dismiss_notifications(cx);
3010                            }
3011                            AgentNotificationEvent::Dismissed => {
3012                                this.dismiss_notifications(cx);
3013                            }
3014                        }
3015                    }));
3016
3017                self.notifications.push(screen_window);
3018
3019                // If the user manually refocuses the original window, dismiss the popup.
3020                self.notification_subscriptions
3021                    .entry(screen_window)
3022                    .or_insert_with(Vec::new)
3023                    .push({
3024                        let pop_up_weak = pop_up.downgrade();
3025
3026                        cx.observe_window_activation(window, move |_, window, cx| {
3027                            if window.is_window_active() {
3028                                if let Some(pop_up) = pop_up_weak.upgrade() {
3029                                    pop_up.update(cx, |_, cx| {
3030                                        cx.emit(AgentNotificationEvent::Dismissed);
3031                                    });
3032                                }
3033                            }
3034                        })
3035                    });
3036            }
3037        }
3038    }
3039
3040    fn dismiss_notifications(&mut self, cx: &mut Context<Self>) {
3041        for window in self.notifications.drain(..) {
3042            window
3043                .update(cx, |_, window, _| {
3044                    window.remove_window();
3045                })
3046                .ok();
3047
3048            self.notification_subscriptions.remove(&window);
3049        }
3050    }
3051
3052    fn render_thread_controls(&self, cx: &Context<Self>) -> impl IntoElement {
3053        let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
3054            .shape(ui::IconButtonShape::Square)
3055            .icon_size(IconSize::Small)
3056            .icon_color(Color::Ignored)
3057            .tooltip(Tooltip::text("Open Thread as Markdown"))
3058            .on_click(cx.listener(move |this, _, window, cx| {
3059                if let Some(workspace) = this.workspace.upgrade() {
3060                    this.open_thread_as_markdown(workspace, window, cx)
3061                        .detach_and_log_err(cx);
3062                }
3063            }));
3064
3065        let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUp)
3066            .shape(ui::IconButtonShape::Square)
3067            .icon_size(IconSize::Small)
3068            .icon_color(Color::Ignored)
3069            .tooltip(Tooltip::text("Scroll To Top"))
3070            .on_click(cx.listener(move |this, _, _, cx| {
3071                this.scroll_to_top(cx);
3072            }));
3073
3074        h_flex()
3075            .w_full()
3076            .mr_1()
3077            .pb_2()
3078            .px(RESPONSE_PADDING_X)
3079            .opacity(0.4)
3080            .hover(|style| style.opacity(1.))
3081            .flex_wrap()
3082            .justify_end()
3083            .child(open_as_markdown)
3084            .child(scroll_to_top)
3085    }
3086
3087    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
3088        div()
3089            .id("acp-thread-scrollbar")
3090            .occlude()
3091            .on_mouse_move(cx.listener(|_, _, _, cx| {
3092                cx.notify();
3093                cx.stop_propagation()
3094            }))
3095            .on_hover(|_, _, cx| {
3096                cx.stop_propagation();
3097            })
3098            .on_any_mouse_down(|_, _, cx| {
3099                cx.stop_propagation();
3100            })
3101            .on_mouse_up(
3102                MouseButton::Left,
3103                cx.listener(|_, _, _, cx| {
3104                    cx.stop_propagation();
3105                }),
3106            )
3107            .on_scroll_wheel(cx.listener(|_, _, _, cx| {
3108                cx.notify();
3109            }))
3110            .h_full()
3111            .absolute()
3112            .right_1()
3113            .top_1()
3114            .bottom_0()
3115            .w(px(12.))
3116            .cursor_default()
3117            .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx)))
3118    }
3119
3120    fn settings_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
3121        self.entry_view_state.settings_changed(cx);
3122    }
3123
3124    pub(crate) fn insert_dragged_files(
3125        &self,
3126        paths: Vec<project::ProjectPath>,
3127        added_worktrees: Vec<Entity<project::Worktree>>,
3128        window: &mut Window,
3129        cx: &mut Context<Self>,
3130    ) {
3131        self.message_editor.update(cx, |message_editor, cx| {
3132            message_editor.insert_dragged_files(paths, window, cx);
3133            drop(added_worktrees);
3134        })
3135    }
3136}
3137
3138impl AcpThreadView {
3139    fn render_thread_error(&self, window: &mut Window, cx: &mut Context<'_, Self>) -> Option<Div> {
3140        let content = match self.thread_error.as_ref()? {
3141            ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx),
3142            ThreadError::PaymentRequired => self.render_payment_required_error(cx),
3143            ThreadError::ModelRequestLimitReached(plan) => {
3144                self.render_model_request_limit_reached_error(*plan, cx)
3145            }
3146            ThreadError::ToolUseLimitReached => {
3147                self.render_tool_use_limit_reached_error(window, cx)?
3148            }
3149        };
3150
3151        Some(
3152            div()
3153                .border_t_1()
3154                .border_color(cx.theme().colors().border)
3155                .child(content),
3156        )
3157    }
3158
3159    fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout {
3160        let icon = Icon::new(IconName::XCircle)
3161            .size(IconSize::Small)
3162            .color(Color::Error);
3163
3164        Callout::new()
3165            .icon(icon)
3166            .title("Error")
3167            .description(error.clone())
3168            .secondary_action(self.create_copy_button(error.to_string()))
3169            .primary_action(self.dismiss_error_button(cx))
3170            .bg_color(self.error_callout_bg(cx))
3171    }
3172
3173    fn render_payment_required_error(&self, cx: &mut Context<Self>) -> Callout {
3174        const ERROR_MESSAGE: &str =
3175            "You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
3176
3177        let icon = Icon::new(IconName::XCircle)
3178            .size(IconSize::Small)
3179            .color(Color::Error);
3180
3181        Callout::new()
3182            .icon(icon)
3183            .title("Free Usage Exceeded")
3184            .description(ERROR_MESSAGE)
3185            .tertiary_action(self.upgrade_button(cx))
3186            .secondary_action(self.create_copy_button(ERROR_MESSAGE))
3187            .primary_action(self.dismiss_error_button(cx))
3188            .bg_color(self.error_callout_bg(cx))
3189    }
3190
3191    fn render_model_request_limit_reached_error(
3192        &self,
3193        plan: cloud_llm_client::Plan,
3194        cx: &mut Context<Self>,
3195    ) -> Callout {
3196        let error_message = match plan {
3197            cloud_llm_client::Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
3198            cloud_llm_client::Plan::ZedProTrial | cloud_llm_client::Plan::ZedFree => {
3199                "Upgrade to Zed Pro for more prompts."
3200            }
3201        };
3202
3203        let icon = Icon::new(IconName::XCircle)
3204            .size(IconSize::Small)
3205            .color(Color::Error);
3206
3207        Callout::new()
3208            .icon(icon)
3209            .title("Model Prompt Limit Reached")
3210            .description(error_message)
3211            .tertiary_action(self.upgrade_button(cx))
3212            .secondary_action(self.create_copy_button(error_message))
3213            .primary_action(self.dismiss_error_button(cx))
3214            .bg_color(self.error_callout_bg(cx))
3215    }
3216
3217    fn render_tool_use_limit_reached_error(
3218        &self,
3219        window: &mut Window,
3220        cx: &mut Context<Self>,
3221    ) -> Option<Callout> {
3222        let thread = self.as_native_thread(cx)?;
3223        let supports_burn_mode = thread.read(cx).model().supports_burn_mode();
3224
3225        let focus_handle = self.focus_handle(cx);
3226
3227        let icon = Icon::new(IconName::Info)
3228            .size(IconSize::Small)
3229            .color(Color::Info);
3230
3231        Some(
3232            Callout::new()
3233                .icon(icon)
3234                .title("Consecutive tool use limit reached.")
3235                .when(supports_burn_mode, |this| {
3236                    this.secondary_action(
3237                        Button::new("continue-burn-mode", "Continue with Burn Mode")
3238                            .style(ButtonStyle::Filled)
3239                            .style(ButtonStyle::Tinted(ui::TintColor::Accent))
3240                            .layer(ElevationIndex::ModalSurface)
3241                            .label_size(LabelSize::Small)
3242                            .key_binding(
3243                                KeyBinding::for_action_in(
3244                                    &ContinueWithBurnMode,
3245                                    &focus_handle,
3246                                    window,
3247                                    cx,
3248                                )
3249                                .map(|kb| kb.size(rems_from_px(10.))),
3250                            )
3251                            .tooltip(Tooltip::text("Enable Burn Mode for unlimited tool use."))
3252                            .on_click({
3253                                cx.listener(move |this, _, _window, cx| {
3254                                    thread.update(cx, |thread, _cx| {
3255                                        thread.set_completion_mode(CompletionMode::Burn);
3256                                    });
3257                                    this.resume_chat(cx);
3258                                })
3259                            }),
3260                    )
3261                })
3262                .primary_action(
3263                    Button::new("continue-conversation", "Continue")
3264                        .layer(ElevationIndex::ModalSurface)
3265                        .label_size(LabelSize::Small)
3266                        .key_binding(
3267                            KeyBinding::for_action_in(&ContinueThread, &focus_handle, window, cx)
3268                                .map(|kb| kb.size(rems_from_px(10.))),
3269                        )
3270                        .on_click(cx.listener(|this, _, _window, cx| {
3271                            this.resume_chat(cx);
3272                        })),
3273                ),
3274        )
3275    }
3276
3277    fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
3278        let message = message.into();
3279
3280        IconButton::new("copy", IconName::Copy)
3281            .icon_size(IconSize::Small)
3282            .icon_color(Color::Muted)
3283            .tooltip(Tooltip::text("Copy Error Message"))
3284            .on_click(move |_, _, cx| {
3285                cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
3286            })
3287    }
3288
3289    fn dismiss_error_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
3290        IconButton::new("dismiss", IconName::Close)
3291            .icon_size(IconSize::Small)
3292            .icon_color(Color::Muted)
3293            .tooltip(Tooltip::text("Dismiss Error"))
3294            .on_click(cx.listener({
3295                move |this, _, _, cx| {
3296                    this.clear_thread_error(cx);
3297                    cx.notify();
3298                }
3299            }))
3300    }
3301
3302    fn upgrade_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
3303        Button::new("upgrade", "Upgrade")
3304            .label_size(LabelSize::Small)
3305            .style(ButtonStyle::Tinted(ui::TintColor::Accent))
3306            .on_click(cx.listener({
3307                move |this, _, _, cx| {
3308                    this.clear_thread_error(cx);
3309                    cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx));
3310                }
3311            }))
3312    }
3313
3314    fn error_callout_bg(&self, cx: &Context<Self>) -> Hsla {
3315        cx.theme().status().error.opacity(0.08)
3316    }
3317}
3318
3319impl Focusable for AcpThreadView {
3320    fn focus_handle(&self, cx: &App) -> FocusHandle {
3321        self.message_editor.focus_handle(cx)
3322    }
3323}
3324
3325impl Render for AcpThreadView {
3326    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3327        let has_messages = self.list_state.item_count() > 0;
3328
3329        v_flex()
3330            .size_full()
3331            .key_context("AcpThread")
3332            .on_action(cx.listener(Self::open_agent_diff))
3333            .on_action(cx.listener(Self::toggle_burn_mode))
3334            .bg(cx.theme().colors().panel_background)
3335            .child(match &self.thread_state {
3336                ThreadState::Unauthenticated { connection } => v_flex()
3337                    .p_2()
3338                    .flex_1()
3339                    .items_center()
3340                    .justify_center()
3341                    .child(self.render_pending_auth_state())
3342                    .child(h_flex().mt_1p5().justify_center().children(
3343                        connection.auth_methods().into_iter().map(|method| {
3344                            Button::new(
3345                                SharedString::from(method.id.0.clone()),
3346                                method.name.clone(),
3347                            )
3348                            .on_click({
3349                                let method_id = method.id.clone();
3350                                cx.listener(move |this, _, window, cx| {
3351                                    this.authenticate(method_id.clone(), window, cx)
3352                                })
3353                            })
3354                        }),
3355                    )),
3356                ThreadState::Loading { .. } => v_flex().flex_1().child(self.render_empty_state(cx)),
3357                ThreadState::LoadError(e) => v_flex()
3358                    .p_2()
3359                    .flex_1()
3360                    .items_center()
3361                    .justify_center()
3362                    .child(self.render_load_error(e, cx)),
3363                ThreadState::ServerExited { status } => v_flex()
3364                    .p_2()
3365                    .flex_1()
3366                    .items_center()
3367                    .justify_center()
3368                    .child(self.render_server_exited(*status, cx)),
3369                ThreadState::Ready { thread, .. } => {
3370                    let thread_clone = thread.clone();
3371
3372                    v_flex().flex_1().map(|this| {
3373                        if has_messages {
3374                            this.child(
3375                                list(
3376                                    self.list_state.clone(),
3377                                    cx.processor(|this, index: usize, window, cx| {
3378                                        let Some((entry, len)) = this.thread().and_then(|thread| {
3379                                            let entries = &thread.read(cx).entries();
3380                                            Some((entries.get(index)?, entries.len()))
3381                                        }) else {
3382                                            return Empty.into_any();
3383                                        };
3384                                        this.render_entry(index, len, entry, window, cx)
3385                                    }),
3386                                )
3387                                .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
3388                                .flex_grow()
3389                                .into_any(),
3390                            )
3391                            .child(self.render_vertical_scrollbar(cx))
3392                            .children(
3393                                match thread_clone.read(cx).status() {
3394                                    ThreadStatus::Idle
3395                                    | ThreadStatus::WaitingForToolConfirmation => None,
3396                                    ThreadStatus::Generating => div()
3397                                        .px_5()
3398                                        .py_2()
3399                                        .child(LoadingLabel::new("").size(LabelSize::Small))
3400                                        .into(),
3401                                },
3402                            )
3403                        } else {
3404                            this.child(self.render_empty_state(cx))
3405                        }
3406                    })
3407                }
3408            })
3409            // The activity bar is intentionally rendered outside of the ThreadState::Ready match
3410            // above so that the scrollbar doesn't render behind it. The current setup allows
3411            // the scrollbar to stop exactly at the activity bar start.
3412            .when(has_messages, |this| match &self.thread_state {
3413                ThreadState::Ready { thread, .. } => {
3414                    this.children(self.render_activity_bar(thread, window, cx))
3415                }
3416                _ => this,
3417            })
3418            .children(self.render_thread_error(window, cx))
3419            .child(self.render_message_editor(window, cx))
3420    }
3421}
3422
3423fn user_message_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
3424    let mut style = default_markdown_style(false, window, cx);
3425    let mut text_style = window.text_style();
3426    let theme_settings = ThemeSettings::get_global(cx);
3427
3428    let buffer_font = theme_settings.buffer_font.family.clone();
3429    let buffer_font_size = TextSize::Small.rems(cx);
3430
3431    text_style.refine(&TextStyleRefinement {
3432        font_family: Some(buffer_font),
3433        font_size: Some(buffer_font_size.into()),
3434        ..Default::default()
3435    });
3436
3437    style.base_text_style = text_style;
3438    style.link_callback = Some(Rc::new(move |url, cx| {
3439        if MentionUri::parse(url).is_ok() {
3440            let colors = cx.theme().colors();
3441            Some(TextStyleRefinement {
3442                background_color: Some(colors.element_background),
3443                ..Default::default()
3444            })
3445        } else {
3446            None
3447        }
3448    }));
3449    style
3450}
3451
3452fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> MarkdownStyle {
3453    let theme_settings = ThemeSettings::get_global(cx);
3454    let colors = cx.theme().colors();
3455
3456    let buffer_font_size = TextSize::Small.rems(cx);
3457
3458    let mut text_style = window.text_style();
3459    let line_height = buffer_font_size * 1.75;
3460
3461    let font_family = if buffer_font {
3462        theme_settings.buffer_font.family.clone()
3463    } else {
3464        theme_settings.ui_font.family.clone()
3465    };
3466
3467    let font_size = if buffer_font {
3468        TextSize::Small.rems(cx)
3469    } else {
3470        TextSize::Default.rems(cx)
3471    };
3472
3473    text_style.refine(&TextStyleRefinement {
3474        font_family: Some(font_family),
3475        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
3476        font_features: Some(theme_settings.ui_font.features.clone()),
3477        font_size: Some(font_size.into()),
3478        line_height: Some(line_height.into()),
3479        color: Some(cx.theme().colors().text),
3480        ..Default::default()
3481    });
3482
3483    MarkdownStyle {
3484        base_text_style: text_style.clone(),
3485        syntax: cx.theme().syntax().clone(),
3486        selection_background_color: cx.theme().colors().element_selection_background,
3487        code_block_overflow_x_scroll: true,
3488        table_overflow_x_scroll: true,
3489        heading_level_styles: Some(HeadingLevelStyles {
3490            h1: Some(TextStyleRefinement {
3491                font_size: Some(rems(1.15).into()),
3492                ..Default::default()
3493            }),
3494            h2: Some(TextStyleRefinement {
3495                font_size: Some(rems(1.1).into()),
3496                ..Default::default()
3497            }),
3498            h3: Some(TextStyleRefinement {
3499                font_size: Some(rems(1.05).into()),
3500                ..Default::default()
3501            }),
3502            h4: Some(TextStyleRefinement {
3503                font_size: Some(rems(1.).into()),
3504                ..Default::default()
3505            }),
3506            h5: Some(TextStyleRefinement {
3507                font_size: Some(rems(0.95).into()),
3508                ..Default::default()
3509            }),
3510            h6: Some(TextStyleRefinement {
3511                font_size: Some(rems(0.875).into()),
3512                ..Default::default()
3513            }),
3514        }),
3515        code_block: StyleRefinement {
3516            padding: EdgesRefinement {
3517                top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
3518                left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
3519                right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
3520                bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
3521            },
3522            margin: EdgesRefinement {
3523                top: Some(Length::Definite(Pixels(8.).into())),
3524                left: Some(Length::Definite(Pixels(0.).into())),
3525                right: Some(Length::Definite(Pixels(0.).into())),
3526                bottom: Some(Length::Definite(Pixels(12.).into())),
3527            },
3528            border_style: Some(BorderStyle::Solid),
3529            border_widths: EdgesRefinement {
3530                top: Some(AbsoluteLength::Pixels(Pixels(1.))),
3531                left: Some(AbsoluteLength::Pixels(Pixels(1.))),
3532                right: Some(AbsoluteLength::Pixels(Pixels(1.))),
3533                bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
3534            },
3535            border_color: Some(colors.border_variant),
3536            background: Some(colors.editor_background.into()),
3537            text: Some(TextStyleRefinement {
3538                font_family: Some(theme_settings.buffer_font.family.clone()),
3539                font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
3540                font_features: Some(theme_settings.buffer_font.features.clone()),
3541                font_size: Some(buffer_font_size.into()),
3542                ..Default::default()
3543            }),
3544            ..Default::default()
3545        },
3546        inline_code: TextStyleRefinement {
3547            font_family: Some(theme_settings.buffer_font.family.clone()),
3548            font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
3549            font_features: Some(theme_settings.buffer_font.features.clone()),
3550            font_size: Some(buffer_font_size.into()),
3551            background_color: Some(colors.editor_foreground.opacity(0.08)),
3552            ..Default::default()
3553        },
3554        link: TextStyleRefinement {
3555            background_color: Some(colors.editor_foreground.opacity(0.025)),
3556            underline: Some(UnderlineStyle {
3557                color: Some(colors.text_accent.opacity(0.5)),
3558                thickness: px(1.),
3559                ..Default::default()
3560            }),
3561            ..Default::default()
3562        },
3563        ..Default::default()
3564    }
3565}
3566
3567fn plan_label_markdown_style(
3568    status: &acp::PlanEntryStatus,
3569    window: &Window,
3570    cx: &App,
3571) -> MarkdownStyle {
3572    let default_md_style = default_markdown_style(false, window, cx);
3573
3574    MarkdownStyle {
3575        base_text_style: TextStyle {
3576            color: cx.theme().colors().text_muted,
3577            strikethrough: if matches!(status, acp::PlanEntryStatus::Completed) {
3578                Some(gpui::StrikethroughStyle {
3579                    thickness: px(1.),
3580                    color: Some(cx.theme().colors().text_muted.opacity(0.8)),
3581                })
3582            } else {
3583                None
3584            },
3585            ..default_md_style.base_text_style
3586        },
3587        ..default_md_style
3588    }
3589}
3590
3591fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
3592    let default_md_style = default_markdown_style(true, window, cx);
3593
3594    MarkdownStyle {
3595        base_text_style: TextStyle {
3596            ..default_md_style.base_text_style
3597        },
3598        selection_background_color: cx.theme().colors().element_selection_background,
3599        ..Default::default()
3600    }
3601}
3602
3603#[cfg(test)]
3604pub(crate) mod tests {
3605    use acp_thread::{AgentServerName, StubAgentConnection};
3606    use agent::{TextThreadStore, ThreadStore};
3607    use agent_client_protocol::SessionId;
3608    use editor::EditorSettings;
3609    use fs::FakeFs;
3610    use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
3611    use project::Project;
3612    use serde_json::json;
3613    use settings::SettingsStore;
3614    use std::any::Any;
3615    use std::path::Path;
3616
3617    use super::*;
3618
3619    #[gpui::test]
3620    async fn test_drop(cx: &mut TestAppContext) {
3621        init_test(cx);
3622
3623        let (thread_view, _cx) = setup_thread_view(StubAgentServer::default(), cx).await;
3624        let weak_view = thread_view.downgrade();
3625        drop(thread_view);
3626        assert!(!weak_view.is_upgradable());
3627    }
3628
3629    #[gpui::test]
3630    async fn test_notification_for_stop_event(cx: &mut TestAppContext) {
3631        init_test(cx);
3632
3633        let (thread_view, cx) = setup_thread_view(StubAgentServer::default(), cx).await;
3634
3635        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
3636        message_editor.update_in(cx, |editor, window, cx| {
3637            editor.set_text("Hello", window, cx);
3638        });
3639
3640        cx.deactivate_window();
3641
3642        thread_view.update_in(cx, |thread_view, window, cx| {
3643            thread_view.send(window, cx);
3644        });
3645
3646        cx.run_until_parked();
3647
3648        assert!(
3649            cx.windows()
3650                .iter()
3651                .any(|window| window.downcast::<AgentNotification>().is_some())
3652        );
3653    }
3654
3655    #[gpui::test]
3656    async fn test_notification_for_error(cx: &mut TestAppContext) {
3657        init_test(cx);
3658
3659        let (thread_view, cx) =
3660            setup_thread_view(StubAgentServer::new(SaboteurAgentConnection), cx).await;
3661
3662        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
3663        message_editor.update_in(cx, |editor, window, cx| {
3664            editor.set_text("Hello", window, cx);
3665        });
3666
3667        cx.deactivate_window();
3668
3669        thread_view.update_in(cx, |thread_view, window, cx| {
3670            thread_view.send(window, cx);
3671        });
3672
3673        cx.run_until_parked();
3674
3675        assert!(
3676            cx.windows()
3677                .iter()
3678                .any(|window| window.downcast::<AgentNotification>().is_some())
3679        );
3680    }
3681
3682    #[gpui::test]
3683    async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
3684        init_test(cx);
3685
3686        let tool_call_id = acp::ToolCallId("1".into());
3687        let tool_call = acp::ToolCall {
3688            id: tool_call_id.clone(),
3689            title: "Label".into(),
3690            kind: acp::ToolKind::Edit,
3691            status: acp::ToolCallStatus::Pending,
3692            content: vec!["hi".into()],
3693            locations: vec![],
3694            raw_input: None,
3695            raw_output: None,
3696        };
3697        let connection =
3698            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
3699                tool_call_id,
3700                vec![acp::PermissionOption {
3701                    id: acp::PermissionOptionId("1".into()),
3702                    name: "Allow".into(),
3703                    kind: acp::PermissionOptionKind::AllowOnce,
3704                }],
3705            )]));
3706
3707        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
3708
3709        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
3710
3711        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
3712        message_editor.update_in(cx, |editor, window, cx| {
3713            editor.set_text("Hello", window, cx);
3714        });
3715
3716        cx.deactivate_window();
3717
3718        thread_view.update_in(cx, |thread_view, window, cx| {
3719            thread_view.send(window, cx);
3720        });
3721
3722        cx.run_until_parked();
3723
3724        assert!(
3725            cx.windows()
3726                .iter()
3727                .any(|window| window.downcast::<AgentNotification>().is_some())
3728        );
3729    }
3730
3731    async fn setup_thread_view(
3732        agent: impl AgentServer + 'static,
3733        cx: &mut TestAppContext,
3734    ) -> (Entity<AcpThreadView>, &mut VisualTestContext) {
3735        let fs = FakeFs::new(cx.executor());
3736        let project = Project::test(fs, [], cx).await;
3737        let (workspace, cx) =
3738            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3739
3740        let thread_store =
3741            cx.update(|_window, cx| cx.new(|cx| ThreadStore::fake(project.clone(), cx)));
3742        let text_thread_store =
3743            cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx)));
3744
3745        let thread_view = cx.update(|window, cx| {
3746            cx.new(|cx| {
3747                AcpThreadView::new(
3748                    Rc::new(agent),
3749                    workspace.downgrade(),
3750                    project,
3751                    thread_store.clone(),
3752                    text_thread_store.clone(),
3753                    None,
3754                    window,
3755                    cx,
3756                )
3757            })
3758        });
3759        cx.run_until_parked();
3760        (thread_view, cx)
3761    }
3762
3763    struct StubAgentServer<C> {
3764        connection: C,
3765    }
3766
3767    impl<C> StubAgentServer<C> {
3768        fn new(connection: C) -> Self {
3769            Self { connection }
3770        }
3771    }
3772
3773    impl StubAgentServer<StubAgentConnection> {
3774        fn default() -> Self {
3775            Self::new(StubAgentConnection::default())
3776        }
3777    }
3778
3779    impl<C> AgentServer for StubAgentServer<C>
3780    where
3781        C: 'static + AgentConnection + Send + Clone,
3782    {
3783        fn logo(&self) -> ui::IconName {
3784            unimplemented!()
3785        }
3786
3787        fn name(&self) -> AgentServerName {
3788            unimplemented!()
3789        }
3790
3791        fn empty_state_headline(&self) -> &'static str {
3792            unimplemented!()
3793        }
3794
3795        fn empty_state_message(&self) -> &'static str {
3796            unimplemented!()
3797        }
3798
3799        fn connect(
3800            &self,
3801            _root_dir: &Path,
3802            _project: &Entity<Project>,
3803            _cx: &mut App,
3804        ) -> Task<gpui::Result<Rc<dyn AgentConnection>>> {
3805            Task::ready(Ok(Rc::new(self.connection.clone())))
3806        }
3807    }
3808
3809    #[derive(Clone)]
3810    struct SaboteurAgentConnection;
3811
3812    impl AgentConnection for SaboteurAgentConnection {
3813        fn new_thread(
3814            self: Rc<Self>,
3815            project: Entity<Project>,
3816            _cwd: &Path,
3817            cx: &mut gpui::App,
3818        ) -> Task<gpui::Result<Entity<AcpThread>>> {
3819            Task::ready(Ok(cx.new(|cx| {
3820                AcpThread::new(
3821                    "SaboteurAgentConnection",
3822                    self,
3823                    project,
3824                    SessionId("test".into()),
3825                    cx,
3826                )
3827            })))
3828        }
3829
3830        fn auth_methods(&self) -> &[acp::AuthMethod] {
3831            &[]
3832        }
3833
3834        fn authenticate(
3835            &self,
3836            _method_id: acp::AuthMethodId,
3837            _cx: &mut App,
3838        ) -> Task<gpui::Result<()>> {
3839            unimplemented!()
3840        }
3841
3842        fn prompt(
3843            &self,
3844            _id: Option<acp_thread::UserMessageId>,
3845            _params: acp::PromptRequest,
3846            _cx: &mut App,
3847        ) -> Task<gpui::Result<acp::PromptResponse>> {
3848            Task::ready(Err(anyhow::anyhow!("Error prompting")))
3849        }
3850
3851        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
3852            unimplemented!()
3853        }
3854
3855        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
3856            self
3857        }
3858    }
3859
3860    pub(crate) fn init_test(cx: &mut TestAppContext) {
3861        cx.update(|cx| {
3862            let settings_store = SettingsStore::test(cx);
3863            cx.set_global(settings_store);
3864            language::init(cx);
3865            Project::init_settings(cx);
3866            AgentSettings::register(cx);
3867            workspace::init_settings(cx);
3868            ThemeSettings::register(cx);
3869            release_channel::init(SemanticVersion::default(), cx);
3870            EditorSettings::register(cx);
3871        });
3872    }
3873
3874    #[gpui::test]
3875    async fn test_rewind_views(cx: &mut TestAppContext) {
3876        init_test(cx);
3877
3878        let fs = FakeFs::new(cx.executor());
3879        fs.insert_tree(
3880            "/project",
3881            json!({
3882                "test1.txt": "old content 1",
3883                "test2.txt": "old content 2"
3884            }),
3885        )
3886        .await;
3887        let project = Project::test(fs, [Path::new("/project")], cx).await;
3888        let (workspace, cx) =
3889            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
3890
3891        let thread_store =
3892            cx.update(|_window, cx| cx.new(|cx| ThreadStore::fake(project.clone(), cx)));
3893        let text_thread_store =
3894            cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx)));
3895
3896        let connection = Rc::new(StubAgentConnection::new());
3897        let thread_view = cx.update(|window, cx| {
3898            cx.new(|cx| {
3899                AcpThreadView::new(
3900                    Rc::new(StubAgentServer::new(connection.as_ref().clone())),
3901                    workspace.downgrade(),
3902                    project.clone(),
3903                    thread_store.clone(),
3904                    text_thread_store.clone(),
3905                    None,
3906                    window,
3907                    cx,
3908                )
3909            })
3910        });
3911
3912        cx.run_until_parked();
3913
3914        let thread = thread_view
3915            .read_with(cx, |view, _| view.thread().cloned())
3916            .unwrap();
3917
3918        // First user message
3919        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(acp::ToolCall {
3920            id: acp::ToolCallId("tool1".into()),
3921            title: "Edit file 1".into(),
3922            kind: acp::ToolKind::Edit,
3923            status: acp::ToolCallStatus::Completed,
3924            content: vec![acp::ToolCallContent::Diff {
3925                diff: acp::Diff {
3926                    path: "/project/test1.txt".into(),
3927                    old_text: Some("old content 1".into()),
3928                    new_text: "new content 1".into(),
3929                },
3930            }],
3931            locations: vec![],
3932            raw_input: None,
3933            raw_output: None,
3934        })]);
3935
3936        thread
3937            .update(cx, |thread, cx| thread.send_raw("Give me a diff", cx))
3938            .await
3939            .unwrap();
3940        cx.run_until_parked();
3941
3942        thread.read_with(cx, |thread, _| {
3943            assert_eq!(thread.entries().len(), 2);
3944        });
3945
3946        thread_view.read_with(cx, |view, _| {
3947            assert_eq!(view.entry_view_state.entry(0).unwrap().len(), 0);
3948            assert_eq!(view.entry_view_state.entry(1).unwrap().len(), 1);
3949        });
3950
3951        // Second user message
3952        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(acp::ToolCall {
3953            id: acp::ToolCallId("tool2".into()),
3954            title: "Edit file 2".into(),
3955            kind: acp::ToolKind::Edit,
3956            status: acp::ToolCallStatus::Completed,
3957            content: vec![acp::ToolCallContent::Diff {
3958                diff: acp::Diff {
3959                    path: "/project/test2.txt".into(),
3960                    old_text: Some("old content 2".into()),
3961                    new_text: "new content 2".into(),
3962                },
3963            }],
3964            locations: vec![],
3965            raw_input: None,
3966            raw_output: None,
3967        })]);
3968
3969        thread
3970            .update(cx, |thread, cx| thread.send_raw("Another one", cx))
3971            .await
3972            .unwrap();
3973        cx.run_until_parked();
3974
3975        let second_user_message_id = thread.read_with(cx, |thread, _| {
3976            assert_eq!(thread.entries().len(), 4);
3977            let AgentThreadEntry::UserMessage(user_message) = thread.entries().get(2).unwrap()
3978            else {
3979                panic!();
3980            };
3981            user_message.id.clone().unwrap()
3982        });
3983
3984        thread_view.read_with(cx, |view, _| {
3985            assert_eq!(view.entry_view_state.entry(0).unwrap().len(), 0);
3986            assert_eq!(view.entry_view_state.entry(1).unwrap().len(), 1);
3987            assert_eq!(view.entry_view_state.entry(2).unwrap().len(), 0);
3988            assert_eq!(view.entry_view_state.entry(3).unwrap().len(), 1);
3989        });
3990
3991        // Rewind to first message
3992        thread
3993            .update(cx, |thread, cx| thread.rewind(second_user_message_id, cx))
3994            .await
3995            .unwrap();
3996
3997        cx.run_until_parked();
3998
3999        thread.read_with(cx, |thread, _| {
4000            assert_eq!(thread.entries().len(), 2);
4001        });
4002
4003        thread_view.read_with(cx, |view, _| {
4004            assert_eq!(view.entry_view_state.entry(0).unwrap().len(), 0);
4005            assert_eq!(view.entry_view_state.entry(1).unwrap().len(), 1);
4006
4007            // Old views should be dropped
4008            assert!(view.entry_view_state.entry(2).is_none());
4009            assert!(view.entry_view_state.entry(3).is_none());
4010        });
4011    }
4012}