thread_view.rs

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