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