thread_view.rs

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