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