thread_view.rs

   1use acp_thread::{AgentConnection, Plan};
   2use agent_servers::AgentServer;
   3use std::cell::RefCell;
   4use std::collections::BTreeMap;
   5use std::path::Path;
   6use std::rc::Rc;
   7use std::sync::Arc;
   8use std::time::Duration;
   9
  10use agent_client_protocol as acp;
  11use assistant_tool::ActionLog;
  12use buffer_diff::BufferDiff;
  13use collections::{HashMap, HashSet};
  14use editor::{
  15    AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode,
  16    EditorStyle, MinimapVisibility, MultiBuffer, PathKey,
  17};
  18use file_icons::FileIcons;
  19use gpui::{
  20    Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
  21    FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement,
  22    Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
  23    Window, div, linear_color_stop, linear_gradient, list, percentage, point, prelude::*,
  24    pulsating_between,
  25};
  26use language::language_settings::SoftWrap;
  27use language::{Buffer, Language};
  28use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
  29use parking_lot::Mutex;
  30use project::Project;
  31use settings::Settings as _;
  32use text::Anchor;
  33use theme::ThemeSettings;
  34use ui::{Disclosure, Divider, DividerColor, KeyBinding, Tooltip, prelude::*};
  35use util::ResultExt;
  36use workspace::{CollaboratorId, Workspace};
  37use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
  38
  39use ::acp_thread::{
  40    AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, Diff,
  41    LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus,
  42};
  43
  44use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
  45use crate::acp::message_history::MessageHistory;
  46use crate::agent_diff::AgentDiff;
  47use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
  48use crate::{AgentDiffPane, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll};
  49
  50const RESPONSE_PADDING_X: Pixels = px(19.);
  51
  52pub struct AcpThreadView {
  53    agent: Rc<dyn AgentServer>,
  54    workspace: WeakEntity<Workspace>,
  55    project: Entity<Project>,
  56    thread_state: ThreadState,
  57    diff_editors: HashMap<EntityId, Entity<Editor>>,
  58    message_editor: Entity<Editor>,
  59    message_set_from_history: bool,
  60    _message_editor_subscription: Subscription,
  61    mention_set: Arc<Mutex<MentionSet>>,
  62    last_error: Option<Entity<Markdown>>,
  63    list_state: ListState,
  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    message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
  71    _cancel_task: Option<Task<()>>,
  72}
  73
  74enum ThreadState {
  75    Loading {
  76        _task: Task<()>,
  77    },
  78    Ready {
  79        thread: Entity<AcpThread>,
  80        _subscription: [Subscription; 2],
  81    },
  82    LoadError(LoadError),
  83    Unauthenticated {
  84        connection: Rc<dyn AgentConnection>,
  85    },
  86}
  87
  88impl AcpThreadView {
  89    pub fn new(
  90        agent: Rc<dyn AgentServer>,
  91        workspace: WeakEntity<Workspace>,
  92        project: Entity<Project>,
  93        message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
  94        min_lines: usize,
  95        max_lines: Option<usize>,
  96        window: &mut Window,
  97        cx: &mut Context<Self>,
  98    ) -> Self {
  99        let language = Language::new(
 100            language::LanguageConfig {
 101                completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
 102                ..Default::default()
 103            },
 104            None,
 105        );
 106
 107        let mention_set = Arc::new(Mutex::new(MentionSet::default()));
 108
 109        let message_editor = cx.new(|cx| {
 110            let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
 111            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
 112
 113            let mut editor = Editor::new(
 114                editor::EditorMode::AutoHeight {
 115                    min_lines,
 116                    max_lines: max_lines,
 117                },
 118                buffer,
 119                None,
 120                window,
 121                cx,
 122            );
 123            editor.set_placeholder_text("Message the agent - @ to include files", cx);
 124            editor.set_show_indent_guides(false, cx);
 125            editor.set_soft_wrap();
 126            editor.set_use_modal_editing(true);
 127            editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
 128                mention_set.clone(),
 129                workspace.clone(),
 130                cx.weak_entity(),
 131            ))));
 132            editor.set_context_menu_options(ContextMenuOptions {
 133                min_entries_visible: 12,
 134                max_entries_visible: 12,
 135                placement: Some(ContextMenuPlacement::Above),
 136            });
 137            editor
 138        });
 139
 140        let message_editor_subscription = cx.subscribe(&message_editor, |this, _, event, _| {
 141            if let editor::EditorEvent::BufferEdited = &event {
 142                if !this.message_set_from_history {
 143                    this.message_history.borrow_mut().reset_position();
 144                }
 145                this.message_set_from_history = false;
 146            }
 147        });
 148
 149        let mention_set = mention_set.clone();
 150
 151        let list_state = ListState::new(
 152            0,
 153            gpui::ListAlignment::Bottom,
 154            px(2048.0),
 155            cx.processor({
 156                move |this: &mut Self, index: usize, window, cx| {
 157                    let Some((entry, len)) = this.thread().and_then(|thread| {
 158                        let entries = &thread.read(cx).entries();
 159                        Some((entries.get(index)?, entries.len()))
 160                    }) else {
 161                        return Empty.into_any();
 162                    };
 163                    this.render_entry(index, len, entry, window, cx)
 164                }
 165            }),
 166        );
 167
 168        Self {
 169            agent: agent.clone(),
 170            workspace: workspace.clone(),
 171            project: project.clone(),
 172            thread_state: Self::initial_state(agent, workspace, project, window, cx),
 173            message_editor,
 174            message_set_from_history: false,
 175            _message_editor_subscription: message_editor_subscription,
 176            mention_set,
 177            diff_editors: Default::default(),
 178            list_state: list_state,
 179            last_error: None,
 180            auth_task: None,
 181            expanded_tool_calls: HashSet::default(),
 182            expanded_thinking_blocks: HashSet::default(),
 183            edits_expanded: false,
 184            plan_expanded: false,
 185            editor_expanded: false,
 186            message_history,
 187            _cancel_task: None,
 188        }
 189    }
 190
 191    fn initial_state(
 192        agent: Rc<dyn AgentServer>,
 193        workspace: WeakEntity<Workspace>,
 194        project: Entity<Project>,
 195        window: &mut Window,
 196        cx: &mut Context<Self>,
 197    ) -> ThreadState {
 198        let root_dir = project
 199            .read(cx)
 200            .visible_worktrees(cx)
 201            .next()
 202            .map(|worktree| worktree.read(cx).abs_path())
 203            .unwrap_or_else(|| paths::home_dir().as_path().into());
 204
 205        let connect_task = agent.connect(&root_dir, &project, cx);
 206        let load_task = cx.spawn_in(window, async move |this, cx| {
 207            let connection = match connect_task.await {
 208                Ok(thread) => thread,
 209                Err(err) => {
 210                    this.update(cx, |this, cx| {
 211                        this.handle_load_error(err, cx);
 212                        cx.notify();
 213                    })
 214                    .log_err();
 215                    return;
 216                }
 217            };
 218
 219            let result = match connection
 220                .clone()
 221                .new_thread(project.clone(), &root_dir, cx)
 222                .await
 223            {
 224                Err(e) => {
 225                    let mut cx = cx.clone();
 226                    if e.downcast_ref::<acp_thread::Unauthenticated>().is_some() {
 227                        this.update(&mut cx, |this, cx| {
 228                            this.thread_state = ThreadState::Unauthenticated { connection };
 229                            cx.notify();
 230                        })
 231                        .ok();
 232                        return;
 233                    } else {
 234                        Err(e)
 235                    }
 236                }
 237                Ok(session_id) => Ok(session_id),
 238            };
 239
 240            this.update_in(cx, |this, window, cx| {
 241                match result {
 242                    Ok(thread) => {
 243                        let thread_subscription =
 244                            cx.subscribe_in(&thread, window, Self::handle_thread_event);
 245
 246                        let action_log = thread.read(cx).action_log().clone();
 247                        let action_log_subscription =
 248                            cx.observe(&action_log, |_, _, cx| cx.notify());
 249
 250                        this.list_state
 251                            .splice(0..0, thread.read(cx).entries().len());
 252
 253                        AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
 254
 255                        this.thread_state = ThreadState::Ready {
 256                            thread,
 257                            _subscription: [thread_subscription, action_log_subscription],
 258                        };
 259
 260                        cx.notify();
 261                    }
 262                    Err(err) => {
 263                        this.handle_load_error(err, cx);
 264                    }
 265                };
 266            })
 267            .log_err();
 268        });
 269
 270        ThreadState::Loading { _task: load_task }
 271    }
 272
 273    fn handle_load_error(&mut self, err: anyhow::Error, cx: &mut Context<Self>) {
 274        if let Some(load_err) = err.downcast_ref::<LoadError>() {
 275            self.thread_state = ThreadState::LoadError(load_err.clone());
 276        } else {
 277            self.thread_state = ThreadState::LoadError(LoadError::Other(err.to_string().into()))
 278        }
 279        cx.notify();
 280    }
 281
 282    pub fn thread(&self) -> Option<&Entity<AcpThread>> {
 283        match &self.thread_state {
 284            ThreadState::Ready { thread, .. } => Some(thread),
 285            ThreadState::Unauthenticated { .. }
 286            | ThreadState::Loading { .. }
 287            | ThreadState::LoadError(..) => None,
 288        }
 289    }
 290
 291    pub fn title(&self, cx: &App) -> SharedString {
 292        match &self.thread_state {
 293            ThreadState::Ready { thread, .. } => thread.read(cx).title(),
 294            ThreadState::Loading { .. } => "Loading…".into(),
 295            ThreadState::LoadError(_) => "Failed to load".into(),
 296            ThreadState::Unauthenticated { .. } => "Not authenticated".into(),
 297        }
 298    }
 299
 300    pub fn cancel(&mut self, cx: &mut Context<Self>) {
 301        self.last_error.take();
 302
 303        if let Some(thread) = self.thread() {
 304            self._cancel_task = Some(thread.update(cx, |thread, cx| thread.cancel(cx)));
 305        }
 306    }
 307
 308    pub fn expand_message_editor(
 309        &mut self,
 310        _: &ExpandMessageEditor,
 311        _window: &mut Window,
 312        cx: &mut Context<Self>,
 313    ) {
 314        self.set_editor_is_expanded(!self.editor_expanded, cx);
 315        cx.notify();
 316    }
 317
 318    fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
 319        self.editor_expanded = is_expanded;
 320        self.message_editor.update(cx, |editor, _| {
 321            if self.editor_expanded {
 322                editor.set_mode(EditorMode::Full {
 323                    scale_ui_elements_with_buffer_font_size: false,
 324                    show_active_line_background: false,
 325                    sized_by_content: false,
 326                })
 327            } else {
 328                editor.set_mode(EditorMode::AutoHeight {
 329                    min_lines: MIN_EDITOR_LINES,
 330                    max_lines: Some(MAX_EDITOR_LINES),
 331                })
 332            }
 333        });
 334        cx.notify();
 335    }
 336
 337    fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
 338        self.last_error.take();
 339
 340        let mut ix = 0;
 341        let mut chunks: Vec<acp::ContentBlock> = Vec::new();
 342        let project = self.project.clone();
 343        self.message_editor.update(cx, |editor, cx| {
 344            let text = editor.text(cx);
 345            editor.display_map.update(cx, |map, cx| {
 346                let snapshot = map.snapshot(cx);
 347                for (crease_id, crease) in snapshot.crease_snapshot.creases() {
 348                    if let Some(project_path) =
 349                        self.mention_set.lock().path_for_crease_id(crease_id)
 350                    {
 351                        let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
 352                        if crease_range.start > ix {
 353                            chunks.push(text[ix..crease_range.start].into());
 354                        }
 355                        if let Some(abs_path) = project.read(cx).absolute_path(&project_path, cx) {
 356                            let path_str = abs_path.display().to_string();
 357                            chunks.push(acp::ContentBlock::ResourceLink(acp::ResourceLink {
 358                                uri: path_str.clone(),
 359                                name: path_str,
 360                                annotations: None,
 361                                description: None,
 362                                mime_type: None,
 363                                size: None,
 364                                title: None,
 365                            }));
 366                        }
 367                        ix = crease_range.end;
 368                    }
 369                }
 370
 371                if ix < text.len() {
 372                    let last_chunk = text[ix..].trim();
 373                    if !last_chunk.is_empty() {
 374                        chunks.push(last_chunk.into());
 375                    }
 376                }
 377            })
 378        });
 379
 380        if chunks.is_empty() {
 381            return;
 382        }
 383
 384        let Some(thread) = self.thread() else { return };
 385        let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx));
 386
 387        cx.spawn(async move |this, cx| {
 388            let result = task.await;
 389
 390            this.update(cx, |this, cx| {
 391                if let Err(err) = result {
 392                    this.last_error =
 393                        Some(cx.new(|cx| Markdown::new(err.to_string().into(), None, None, cx)))
 394                }
 395            })
 396        })
 397        .detach();
 398
 399        let mention_set = self.mention_set.clone();
 400
 401        self.set_editor_is_expanded(false, cx);
 402        self.message_editor.update(cx, |editor, cx| {
 403            editor.clear(window, cx);
 404            editor.remove_creases(mention_set.lock().drain(), cx)
 405        });
 406
 407        self.message_history.borrow_mut().push(chunks);
 408    }
 409
 410    fn previous_history_message(
 411        &mut self,
 412        _: &PreviousHistoryMessage,
 413        window: &mut Window,
 414        cx: &mut Context<Self>,
 415    ) {
 416        self.message_set_from_history = Self::set_draft_message(
 417            self.message_editor.clone(),
 418            self.mention_set.clone(),
 419            self.project.clone(),
 420            self.message_history.borrow_mut().prev(),
 421            window,
 422            cx,
 423        );
 424    }
 425
 426    fn next_history_message(
 427        &mut self,
 428        _: &NextHistoryMessage,
 429        window: &mut Window,
 430        cx: &mut Context<Self>,
 431    ) {
 432        self.message_set_from_history = Self::set_draft_message(
 433            self.message_editor.clone(),
 434            self.mention_set.clone(),
 435            self.project.clone(),
 436            self.message_history.borrow_mut().next(),
 437            window,
 438            cx,
 439        );
 440    }
 441
 442    fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) {
 443        if let Some(thread) = self.thread() {
 444            AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err();
 445        }
 446    }
 447
 448    fn open_edited_buffer(
 449        &mut self,
 450        buffer: &Entity<Buffer>,
 451        window: &mut Window,
 452        cx: &mut Context<Self>,
 453    ) {
 454        let Some(thread) = self.thread() else {
 455            return;
 456        };
 457
 458        let Some(diff) =
 459            AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err()
 460        else {
 461            return;
 462        };
 463
 464        diff.update(cx, |diff, cx| {
 465            diff.move_to_path(PathKey::for_buffer(&buffer, cx), window, cx)
 466        })
 467    }
 468
 469    fn set_draft_message(
 470        message_editor: Entity<Editor>,
 471        mention_set: Arc<Mutex<MentionSet>>,
 472        project: Entity<Project>,
 473        message: Option<&Vec<acp::ContentBlock>>,
 474        window: &mut Window,
 475        cx: &mut Context<Self>,
 476    ) -> bool {
 477        cx.notify();
 478
 479        let Some(message) = message else {
 480            return false;
 481        };
 482
 483        let mut text = String::new();
 484        let mut mentions = Vec::new();
 485
 486        for chunk in message {
 487            match chunk {
 488                acp::ContentBlock::Text(text_content) => {
 489                    text.push_str(&text_content.text);
 490                }
 491                acp::ContentBlock::ResourceLink(resource_link) => {
 492                    let path = Path::new(&resource_link.uri);
 493                    let start = text.len();
 494                    let content = MentionPath::new(&path).to_string();
 495                    text.push_str(&content);
 496                    let end = text.len();
 497                    if let Some(project_path) =
 498                        project.read(cx).project_path_for_absolute_path(&path, cx)
 499                    {
 500                        let filename: SharedString = path
 501                            .file_name()
 502                            .unwrap_or_default()
 503                            .to_string_lossy()
 504                            .to_string()
 505                            .into();
 506                        mentions.push((start..end, project_path, filename));
 507                    }
 508                }
 509                acp::ContentBlock::Image(_)
 510                | acp::ContentBlock::Audio(_)
 511                | acp::ContentBlock::Resource(_) => {}
 512            }
 513        }
 514
 515        let snapshot = message_editor.update(cx, |editor, cx| {
 516            editor.set_text(text, window, cx);
 517            editor.buffer().read(cx).snapshot(cx)
 518        });
 519
 520        for (range, project_path, filename) in mentions {
 521            let crease_icon_path = if project_path.path.is_dir() {
 522                FileIcons::get_folder_icon(false, cx)
 523                    .unwrap_or_else(|| IconName::Folder.path().into())
 524            } else {
 525                FileIcons::get_icon(Path::new(project_path.path.as_ref()), cx)
 526                    .unwrap_or_else(|| IconName::File.path().into())
 527            };
 528
 529            let anchor = snapshot.anchor_before(range.start);
 530            let crease_id = crate::context_picker::insert_crease_for_mention(
 531                anchor.excerpt_id,
 532                anchor.text_anchor,
 533                range.end - range.start,
 534                filename,
 535                crease_icon_path,
 536                message_editor.clone(),
 537                window,
 538                cx,
 539            );
 540            if let Some(crease_id) = crease_id {
 541                mention_set.lock().insert(crease_id, project_path);
 542            }
 543        }
 544
 545        true
 546    }
 547
 548    fn handle_thread_event(
 549        &mut self,
 550        thread: &Entity<AcpThread>,
 551        event: &AcpThreadEvent,
 552        window: &mut Window,
 553        cx: &mut Context<Self>,
 554    ) {
 555        let count = self.list_state.item_count();
 556        match event {
 557            AcpThreadEvent::NewEntry => {
 558                let index = thread.read(cx).entries().len() - 1;
 559                self.sync_thread_entry_view(index, window, cx);
 560                self.list_state.splice(count..count, 1);
 561            }
 562            AcpThreadEvent::EntryUpdated(index) => {
 563                let index = *index;
 564                self.sync_thread_entry_view(index, window, cx);
 565                self.list_state.splice(index..index + 1, 1);
 566            }
 567        }
 568        cx.notify();
 569    }
 570
 571    fn sync_thread_entry_view(
 572        &mut self,
 573        entry_ix: usize,
 574        window: &mut Window,
 575        cx: &mut Context<Self>,
 576    ) {
 577        let Some(multibuffers) = self.entry_diff_multibuffers(entry_ix, cx) else {
 578            return;
 579        };
 580
 581        let multibuffers = multibuffers.collect::<Vec<_>>();
 582
 583        for multibuffer in multibuffers {
 584            if self.diff_editors.contains_key(&multibuffer.entity_id()) {
 585                return;
 586            }
 587
 588            let editor = cx.new(|cx| {
 589                let mut editor = Editor::new(
 590                    EditorMode::Full {
 591                        scale_ui_elements_with_buffer_font_size: false,
 592                        show_active_line_background: false,
 593                        sized_by_content: true,
 594                    },
 595                    multibuffer.clone(),
 596                    None,
 597                    window,
 598                    cx,
 599                );
 600                editor.set_show_gutter(false, cx);
 601                editor.disable_inline_diagnostics();
 602                editor.disable_expand_excerpt_buttons(cx);
 603                editor.set_show_vertical_scrollbar(false, cx);
 604                editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
 605                editor.set_soft_wrap_mode(SoftWrap::None, cx);
 606                editor.scroll_manager.set_forbid_vertical_scroll(true);
 607                editor.set_show_indent_guides(false, cx);
 608                editor.set_read_only(true);
 609                editor.set_show_breakpoints(false, cx);
 610                editor.set_show_code_actions(false, cx);
 611                editor.set_show_git_diff_gutter(false, cx);
 612                editor.set_expand_all_diff_hunks(cx);
 613                editor.set_text_style_refinement(TextStyleRefinement {
 614                    font_size: Some(
 615                        TextSize::Small
 616                            .rems(cx)
 617                            .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
 618                            .into(),
 619                    ),
 620                    ..Default::default()
 621                });
 622                editor
 623            });
 624            let entity_id = multibuffer.entity_id();
 625            cx.observe_release(&multibuffer, move |this, _, _| {
 626                this.diff_editors.remove(&entity_id);
 627            })
 628            .detach();
 629
 630            self.diff_editors.insert(entity_id, editor);
 631        }
 632    }
 633
 634    fn entry_diff_multibuffers(
 635        &self,
 636        entry_ix: usize,
 637        cx: &App,
 638    ) -> Option<impl Iterator<Item = Entity<MultiBuffer>>> {
 639        let entry = self.thread()?.read(cx).entries().get(entry_ix)?;
 640        Some(entry.diffs().map(|diff| diff.multibuffer.clone()))
 641    }
 642
 643    fn authenticate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 644        let ThreadState::Unauthenticated { ref connection } = self.thread_state else {
 645            return;
 646        };
 647
 648        self.last_error.take();
 649        let authenticate = connection.authenticate(cx);
 650        self.auth_task = Some(cx.spawn_in(window, {
 651            let project = self.project.clone();
 652            let agent = self.agent.clone();
 653            async move |this, cx| {
 654                let result = authenticate.await;
 655
 656                this.update_in(cx, |this, window, cx| {
 657                    if let Err(err) = result {
 658                        this.last_error = Some(cx.new(|cx| {
 659                            Markdown::new(format!("Error: {err}").into(), None, None, cx)
 660                        }))
 661                    } else {
 662                        this.thread_state = Self::initial_state(
 663                            agent,
 664                            this.workspace.clone(),
 665                            project.clone(),
 666                            window,
 667                            cx,
 668                        )
 669                    }
 670                    this.auth_task.take()
 671                })
 672                .ok();
 673            }
 674        }));
 675    }
 676
 677    fn authorize_tool_call(
 678        &mut self,
 679        tool_call_id: acp::ToolCallId,
 680        option_id: acp::PermissionOptionId,
 681        option_kind: acp::PermissionOptionKind,
 682        cx: &mut Context<Self>,
 683    ) {
 684        let Some(thread) = self.thread() else {
 685            return;
 686        };
 687        thread.update(cx, |thread, cx| {
 688            thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx);
 689        });
 690        cx.notify();
 691    }
 692
 693    fn render_entry(
 694        &self,
 695        index: usize,
 696        total_entries: usize,
 697        entry: &AgentThreadEntry,
 698        window: &mut Window,
 699        cx: &Context<Self>,
 700    ) -> AnyElement {
 701        match &entry {
 702            AgentThreadEntry::UserMessage(message) => div()
 703                .py_4()
 704                .px_2()
 705                .child(
 706                    v_flex()
 707                        .p_3()
 708                        .gap_1p5()
 709                        .rounded_lg()
 710                        .shadow_md()
 711                        .bg(cx.theme().colors().editor_background)
 712                        .border_1()
 713                        .border_color(cx.theme().colors().border)
 714                        .text_xs()
 715                        .children(message.content.markdown().map(|md| {
 716                            self.render_markdown(
 717                                md.clone(),
 718                                user_message_markdown_style(window, cx),
 719                            )
 720                        })),
 721                )
 722                .into_any(),
 723            AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
 724                let style = default_markdown_style(false, window, cx);
 725                let message_body = v_flex()
 726                    .w_full()
 727                    .gap_2p5()
 728                    .children(chunks.iter().enumerate().filter_map(
 729                        |(chunk_ix, chunk)| match chunk {
 730                            AssistantMessageChunk::Message { block } => {
 731                                block.markdown().map(|md| {
 732                                    self.render_markdown(md.clone(), style.clone())
 733                                        .into_any_element()
 734                                })
 735                            }
 736                            AssistantMessageChunk::Thought { block } => {
 737                                block.markdown().map(|md| {
 738                                    self.render_thinking_block(
 739                                        index,
 740                                        chunk_ix,
 741                                        md.clone(),
 742                                        window,
 743                                        cx,
 744                                    )
 745                                    .into_any_element()
 746                                })
 747                            }
 748                        },
 749                    ))
 750                    .into_any();
 751
 752                v_flex()
 753                    .px_5()
 754                    .py_1()
 755                    .when(index + 1 == total_entries, |this| this.pb_4())
 756                    .w_full()
 757                    .text_ui(cx)
 758                    .child(message_body)
 759                    .into_any()
 760            }
 761            AgentThreadEntry::ToolCall(tool_call) => div()
 762                .py_1p5()
 763                .px_5()
 764                .child(self.render_tool_call(index, tool_call, window, cx))
 765                .into_any(),
 766        }
 767    }
 768
 769    fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
 770        cx.theme()
 771            .colors()
 772            .element_background
 773            .blend(cx.theme().colors().editor_foreground.opacity(0.025))
 774    }
 775
 776    fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
 777        cx.theme().colors().border.opacity(0.6)
 778    }
 779
 780    fn tool_name_font_size(&self) -> Rems {
 781        rems_from_px(13.)
 782    }
 783
 784    fn render_thinking_block(
 785        &self,
 786        entry_ix: usize,
 787        chunk_ix: usize,
 788        chunk: Entity<Markdown>,
 789        window: &Window,
 790        cx: &Context<Self>,
 791    ) -> AnyElement {
 792        let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
 793        let key = (entry_ix, chunk_ix);
 794        let is_open = self.expanded_thinking_blocks.contains(&key);
 795
 796        v_flex()
 797            .child(
 798                h_flex()
 799                    .id(header_id)
 800                    .group("disclosure-header")
 801                    .w_full()
 802                    .justify_between()
 803                    .opacity(0.8)
 804                    .hover(|style| style.opacity(1.))
 805                    .child(
 806                        h_flex()
 807                            .gap_1p5()
 808                            .child(
 809                                Icon::new(IconName::ToolBulb)
 810                                    .size(IconSize::Small)
 811                                    .color(Color::Muted),
 812                            )
 813                            .child(
 814                                div()
 815                                    .text_size(self.tool_name_font_size())
 816                                    .child("Thinking"),
 817                            ),
 818                    )
 819                    .child(
 820                        div().visible_on_hover("disclosure-header").child(
 821                            Disclosure::new("thinking-disclosure", is_open)
 822                                .opened_icon(IconName::ChevronUp)
 823                                .closed_icon(IconName::ChevronDown)
 824                                .on_click(cx.listener({
 825                                    move |this, _event, _window, cx| {
 826                                        if is_open {
 827                                            this.expanded_thinking_blocks.remove(&key);
 828                                        } else {
 829                                            this.expanded_thinking_blocks.insert(key);
 830                                        }
 831                                        cx.notify();
 832                                    }
 833                                })),
 834                        ),
 835                    )
 836                    .on_click(cx.listener({
 837                        move |this, _event, _window, cx| {
 838                            if is_open {
 839                                this.expanded_thinking_blocks.remove(&key);
 840                            } else {
 841                                this.expanded_thinking_blocks.insert(key);
 842                            }
 843                            cx.notify();
 844                        }
 845                    })),
 846            )
 847            .when(is_open, |this| {
 848                this.child(
 849                    div()
 850                        .relative()
 851                        .mt_1p5()
 852                        .ml(px(7.))
 853                        .pl_4()
 854                        .border_l_1()
 855                        .border_color(self.tool_card_border_color(cx))
 856                        .text_ui_sm(cx)
 857                        .child(
 858                            self.render_markdown(chunk, default_markdown_style(false, window, cx)),
 859                        ),
 860                )
 861            })
 862            .into_any_element()
 863    }
 864
 865    fn render_tool_call(
 866        &self,
 867        entry_ix: usize,
 868        tool_call: &ToolCall,
 869        window: &Window,
 870        cx: &Context<Self>,
 871    ) -> Div {
 872        let header_id = SharedString::from(format!("tool-call-header-{}", entry_ix));
 873
 874        let status_icon = match &tool_call.status {
 875            ToolCallStatus::WaitingForConfirmation { .. } => None,
 876            ToolCallStatus::Allowed {
 877                status: acp::ToolCallStatus::InProgress,
 878                ..
 879            } => Some(
 880                Icon::new(IconName::ArrowCircle)
 881                    .color(Color::Accent)
 882                    .size(IconSize::Small)
 883                    .with_animation(
 884                        "running",
 885                        Animation::new(Duration::from_secs(2)).repeat(),
 886                        |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
 887                    )
 888                    .into_any(),
 889            ),
 890            ToolCallStatus::Allowed {
 891                status: acp::ToolCallStatus::Completed,
 892                ..
 893            } => None,
 894            ToolCallStatus::Rejected
 895            | ToolCallStatus::Canceled
 896            | ToolCallStatus::Allowed {
 897                status: acp::ToolCallStatus::Failed,
 898                ..
 899            } => Some(
 900                Icon::new(IconName::X)
 901                    .color(Color::Error)
 902                    .size(IconSize::Small)
 903                    .into_any_element(),
 904            ),
 905        };
 906
 907        let needs_confirmation = match &tool_call.status {
 908            ToolCallStatus::WaitingForConfirmation { .. } => true,
 909            _ => tool_call
 910                .content
 911                .iter()
 912                .any(|content| matches!(content, ToolCallContent::Diff { .. })),
 913        };
 914
 915        let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
 916        let is_open = !is_collapsible || self.expanded_tool_calls.contains(&tool_call.id);
 917
 918        v_flex()
 919            .when(needs_confirmation, |this| {
 920                this.rounded_lg()
 921                    .border_1()
 922                    .border_color(self.tool_card_border_color(cx))
 923                    .bg(cx.theme().colors().editor_background)
 924                    .overflow_hidden()
 925            })
 926            .child(
 927                h_flex()
 928                    .id(header_id)
 929                    .w_full()
 930                    .gap_1()
 931                    .justify_between()
 932                    .map(|this| {
 933                        if needs_confirmation {
 934                            this.px_2()
 935                                .py_1()
 936                                .rounded_t_md()
 937                                .bg(self.tool_card_header_bg(cx))
 938                                .border_b_1()
 939                                .border_color(self.tool_card_border_color(cx))
 940                        } else {
 941                            this.opacity(0.8).hover(|style| style.opacity(1.))
 942                        }
 943                    })
 944                    .child(
 945                        h_flex()
 946                            .id("tool-call-header")
 947                            .overflow_x_scroll()
 948                            .map(|this| {
 949                                if needs_confirmation {
 950                                    this.text_xs()
 951                                } else {
 952                                    this.text_size(self.tool_name_font_size())
 953                                }
 954                            })
 955                            .gap_1p5()
 956                            .child(
 957                                Icon::new(match tool_call.kind {
 958                                    acp::ToolKind::Read => IconName::ToolRead,
 959                                    acp::ToolKind::Edit => IconName::ToolPencil,
 960                                    acp::ToolKind::Search => IconName::ToolSearch,
 961                                    acp::ToolKind::Execute => IconName::ToolTerminal,
 962                                    acp::ToolKind::Think => IconName::ToolBulb,
 963                                    acp::ToolKind::Fetch => IconName::ToolWeb,
 964                                    acp::ToolKind::Other => IconName::ToolHammer,
 965                                })
 966                                .size(IconSize::Small)
 967                                .color(Color::Muted),
 968                            )
 969                            .child(if tool_call.locations.len() == 1 {
 970                                let name = tool_call.locations[0]
 971                                    .path
 972                                    .file_name()
 973                                    .unwrap_or_default()
 974                                    .display()
 975                                    .to_string();
 976
 977                                h_flex()
 978                                    .id(("open-tool-call-location", entry_ix))
 979                                    .child(name)
 980                                    .w_full()
 981                                    .max_w_full()
 982                                    .pr_1()
 983                                    .gap_0p5()
 984                                    .cursor_pointer()
 985                                    .rounded_sm()
 986                                    .opacity(0.8)
 987                                    .hover(|label| {
 988                                        label.opacity(1.).bg(cx
 989                                            .theme()
 990                                            .colors()
 991                                            .element_hover
 992                                            .opacity(0.5))
 993                                    })
 994                                    .tooltip(Tooltip::text("Jump to File"))
 995                                    .on_click(cx.listener(move |this, _, window, cx| {
 996                                        this.open_tool_call_location(entry_ix, 0, window, cx);
 997                                    }))
 998                                    .into_any_element()
 999                            } else {
1000                                self.render_markdown(
1001                                    tool_call.label.clone(),
1002                                    default_markdown_style(needs_confirmation, window, cx),
1003                                )
1004                                .into_any()
1005                            }),
1006                    )
1007                    .child(
1008                        h_flex()
1009                            .gap_0p5()
1010                            .when(is_collapsible, |this| {
1011                                this.child(
1012                                    Disclosure::new(("expand", entry_ix), is_open)
1013                                        .opened_icon(IconName::ChevronUp)
1014                                        .closed_icon(IconName::ChevronDown)
1015                                        .on_click(cx.listener({
1016                                            let id = tool_call.id.clone();
1017                                            move |this: &mut Self, _, _, cx: &mut Context<Self>| {
1018                                                if is_open {
1019                                                    this.expanded_tool_calls.remove(&id);
1020                                                } else {
1021                                                    this.expanded_tool_calls.insert(id.clone());
1022                                                }
1023                                                cx.notify();
1024                                            }
1025                                        })),
1026                                )
1027                            })
1028                            .children(status_icon),
1029                    )
1030                    .on_click(cx.listener({
1031                        let id = tool_call.id.clone();
1032                        move |this: &mut Self, _, _, cx: &mut Context<Self>| {
1033                            if is_open {
1034                                this.expanded_tool_calls.remove(&id);
1035                            } else {
1036                                this.expanded_tool_calls.insert(id.clone());
1037                            }
1038                            cx.notify();
1039                        }
1040                    })),
1041            )
1042            .when(is_open, |this| {
1043                this.child(
1044                    v_flex()
1045                        .text_xs()
1046                        .when(is_collapsible, |this| {
1047                            this.mt_1()
1048                                .border_1()
1049                                .border_color(self.tool_card_border_color(cx))
1050                                .bg(cx.theme().colors().editor_background)
1051                                .rounded_lg()
1052                        })
1053                        .map(|this| {
1054                            if is_open {
1055                                match &tool_call.status {
1056                                    ToolCallStatus::WaitingForConfirmation { options, .. } => this
1057                                        .children(tool_call.content.iter().map(|content| {
1058                                            div()
1059                                                .py_1p5()
1060                                                .child(
1061                                                    self.render_tool_call_content(
1062                                                        content, window, cx,
1063                                                    ),
1064                                                )
1065                                                .into_any_element()
1066                                        }))
1067                                        .child(self.render_permission_buttons(
1068                                            options,
1069                                            entry_ix,
1070                                            tool_call.id.clone(),
1071                                            cx,
1072                                        )),
1073                                    ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => {
1074                                        this.children(tool_call.content.iter().map(|content| {
1075                                            div()
1076                                                .py_1p5()
1077                                                .child(
1078                                                    self.render_tool_call_content(
1079                                                        content, window, cx,
1080                                                    ),
1081                                                )
1082                                                .into_any_element()
1083                                        }))
1084                                    }
1085                                    ToolCallStatus::Rejected => this,
1086                                }
1087                            } else {
1088                                this
1089                            }
1090                        }),
1091                )
1092            })
1093    }
1094
1095    fn render_tool_call_content(
1096        &self,
1097        content: &ToolCallContent,
1098        window: &Window,
1099        cx: &Context<Self>,
1100    ) -> AnyElement {
1101        match content {
1102            ToolCallContent::ContentBlock { content } => {
1103                if let Some(md) = content.markdown() {
1104                    div()
1105                        .p_2()
1106                        .child(
1107                            self.render_markdown(
1108                                md.clone(),
1109                                default_markdown_style(false, window, cx),
1110                            ),
1111                        )
1112                        .into_any_element()
1113                } else {
1114                    Empty.into_any_element()
1115                }
1116            }
1117            ToolCallContent::Diff {
1118                diff: Diff { multibuffer, .. },
1119                ..
1120            } => self.render_diff_editor(multibuffer),
1121        }
1122    }
1123
1124    fn render_permission_buttons(
1125        &self,
1126        options: &[acp::PermissionOption],
1127        entry_ix: usize,
1128        tool_call_id: acp::ToolCallId,
1129        cx: &Context<Self>,
1130    ) -> Div {
1131        h_flex()
1132            .py_1p5()
1133            .px_1p5()
1134            .gap_1()
1135            .justify_end()
1136            .border_t_1()
1137            .border_color(self.tool_card_border_color(cx))
1138            .children(options.iter().map(|option| {
1139                let option_id = SharedString::from(option.id.0.clone());
1140                Button::new((option_id, entry_ix), option.label.clone())
1141                    .map(|this| match option.kind {
1142                        acp::PermissionOptionKind::AllowOnce => {
1143                            this.icon(IconName::Check).icon_color(Color::Success)
1144                        }
1145                        acp::PermissionOptionKind::AllowAlways => {
1146                            this.icon(IconName::CheckDouble).icon_color(Color::Success)
1147                        }
1148                        acp::PermissionOptionKind::RejectOnce => {
1149                            this.icon(IconName::X).icon_color(Color::Error)
1150                        }
1151                        acp::PermissionOptionKind::RejectAlways => {
1152                            this.icon(IconName::X).icon_color(Color::Error)
1153                        }
1154                    })
1155                    .icon_position(IconPosition::Start)
1156                    .icon_size(IconSize::XSmall)
1157                    .on_click(cx.listener({
1158                        let tool_call_id = tool_call_id.clone();
1159                        let option_id = option.id.clone();
1160                        let option_kind = option.kind;
1161                        move |this, _, _, cx| {
1162                            this.authorize_tool_call(
1163                                tool_call_id.clone(),
1164                                option_id.clone(),
1165                                option_kind,
1166                                cx,
1167                            );
1168                        }
1169                    }))
1170            }))
1171    }
1172
1173    fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>) -> AnyElement {
1174        v_flex()
1175            .h_full()
1176            .child(
1177                if let Some(editor) = self.diff_editors.get(&multibuffer.entity_id()) {
1178                    editor.clone().into_any_element()
1179                } else {
1180                    Empty.into_any()
1181                },
1182            )
1183            .into_any()
1184    }
1185
1186    fn render_agent_logo(&self) -> AnyElement {
1187        Icon::new(self.agent.logo())
1188            .color(Color::Muted)
1189            .size(IconSize::XLarge)
1190            .into_any_element()
1191    }
1192
1193    fn render_error_agent_logo(&self) -> AnyElement {
1194        let logo = Icon::new(self.agent.logo())
1195            .color(Color::Muted)
1196            .size(IconSize::XLarge)
1197            .into_any_element();
1198
1199        h_flex()
1200            .relative()
1201            .justify_center()
1202            .child(div().opacity(0.3).child(logo))
1203            .child(
1204                h_flex().absolute().right_1().bottom_0().child(
1205                    Icon::new(IconName::XCircle)
1206                        .color(Color::Error)
1207                        .size(IconSize::Small),
1208                ),
1209            )
1210            .into_any_element()
1211    }
1212
1213    fn render_empty_state(&self, cx: &App) -> AnyElement {
1214        let loading = matches!(&self.thread_state, ThreadState::Loading { .. });
1215
1216        v_flex()
1217            .size_full()
1218            .items_center()
1219            .justify_center()
1220            .child(if loading {
1221                h_flex()
1222                    .justify_center()
1223                    .child(self.render_agent_logo())
1224                    .with_animation(
1225                        "pulsating_icon",
1226                        Animation::new(Duration::from_secs(2))
1227                            .repeat()
1228                            .with_easing(pulsating_between(0.4, 1.0)),
1229                        |icon, delta| icon.opacity(delta),
1230                    )
1231                    .into_any()
1232            } else {
1233                self.render_agent_logo().into_any_element()
1234            })
1235            .child(h_flex().mt_4().mb_1().justify_center().child(if loading {
1236                div()
1237                    .child(LoadingLabel::new("").size(LabelSize::Large))
1238                    .into_any_element()
1239            } else {
1240                Headline::new(self.agent.empty_state_headline())
1241                    .size(HeadlineSize::Medium)
1242                    .into_any_element()
1243            }))
1244            .child(
1245                div()
1246                    .max_w_1_2()
1247                    .text_sm()
1248                    .text_center()
1249                    .map(|this| {
1250                        if loading {
1251                            this.invisible()
1252                        } else {
1253                            this.text_color(cx.theme().colors().text_muted)
1254                        }
1255                    })
1256                    .child(self.agent.empty_state_message()),
1257            )
1258            .into_any()
1259    }
1260
1261    fn render_pending_auth_state(&self) -> AnyElement {
1262        v_flex()
1263            .items_center()
1264            .justify_center()
1265            .child(self.render_error_agent_logo())
1266            .child(
1267                h_flex()
1268                    .mt_4()
1269                    .mb_1()
1270                    .justify_center()
1271                    .child(Headline::new("Not Authenticated").size(HeadlineSize::Medium)),
1272            )
1273            .into_any()
1274    }
1275
1276    fn render_error_state(&self, e: &LoadError, cx: &Context<Self>) -> AnyElement {
1277        let mut container = v_flex()
1278            .items_center()
1279            .justify_center()
1280            .child(self.render_error_agent_logo())
1281            .child(
1282                v_flex()
1283                    .mt_4()
1284                    .mb_2()
1285                    .gap_0p5()
1286                    .text_center()
1287                    .items_center()
1288                    .child(Headline::new("Failed to launch").size(HeadlineSize::Medium))
1289                    .child(
1290                        Label::new(e.to_string())
1291                            .size(LabelSize::Small)
1292                            .color(Color::Muted),
1293                    ),
1294            );
1295
1296        if let LoadError::Unsupported {
1297            upgrade_message,
1298            upgrade_command,
1299            ..
1300        } = &e
1301        {
1302            let upgrade_message = upgrade_message.clone();
1303            let upgrade_command = upgrade_command.clone();
1304            container = container.child(Button::new("upgrade", upgrade_message).on_click(
1305                cx.listener(move |this, _, window, cx| {
1306                    this.workspace
1307                        .update(cx, |workspace, cx| {
1308                            let project = workspace.project().read(cx);
1309                            let cwd = project.first_project_directory(cx);
1310                            let shell = project.terminal_settings(&cwd, cx).shell.clone();
1311                            let spawn_in_terminal = task::SpawnInTerminal {
1312                                id: task::TaskId("install".to_string()),
1313                                full_label: upgrade_command.clone(),
1314                                label: upgrade_command.clone(),
1315                                command: Some(upgrade_command.clone()),
1316                                args: Vec::new(),
1317                                command_label: upgrade_command.clone(),
1318                                cwd,
1319                                env: Default::default(),
1320                                use_new_terminal: true,
1321                                allow_concurrent_runs: true,
1322                                reveal: Default::default(),
1323                                reveal_target: Default::default(),
1324                                hide: Default::default(),
1325                                shell,
1326                                show_summary: true,
1327                                show_command: true,
1328                                show_rerun: false,
1329                            };
1330                            workspace
1331                                .spawn_in_terminal(spawn_in_terminal, window, cx)
1332                                .detach();
1333                        })
1334                        .ok();
1335                }),
1336            ));
1337        }
1338
1339        container.into_any()
1340    }
1341
1342    fn render_activity_bar(
1343        &self,
1344        thread_entity: &Entity<AcpThread>,
1345        window: &mut Window,
1346        cx: &Context<Self>,
1347    ) -> Option<AnyElement> {
1348        let thread = thread_entity.read(cx);
1349        let action_log = thread.action_log();
1350        let changed_buffers = action_log.read(cx).changed_buffers(cx);
1351        let plan = thread.plan();
1352
1353        if changed_buffers.is_empty() && plan.is_empty() {
1354            return None;
1355        }
1356
1357        let editor_bg_color = cx.theme().colors().editor_background;
1358        let active_color = cx.theme().colors().element_selected;
1359        let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
1360
1361        let pending_edits = thread.has_pending_edit_tool_calls();
1362
1363        v_flex()
1364            .mt_1()
1365            .mx_2()
1366            .bg(bg_edit_files_disclosure)
1367            .border_1()
1368            .border_b_0()
1369            .border_color(cx.theme().colors().border)
1370            .rounded_t_md()
1371            .shadow(vec![gpui::BoxShadow {
1372                color: gpui::black().opacity(0.15),
1373                offset: point(px(1.), px(-1.)),
1374                blur_radius: px(3.),
1375                spread_radius: px(0.),
1376            }])
1377            .when(!plan.is_empty(), |this| {
1378                this.child(self.render_plan_summary(plan, window, cx))
1379                    .when(self.plan_expanded, |parent| {
1380                        parent.child(self.render_plan_entries(plan, window, cx))
1381                    })
1382            })
1383            .when(!changed_buffers.is_empty(), |this| {
1384                this.child(Divider::horizontal())
1385                    .child(self.render_edits_summary(
1386                        action_log,
1387                        &changed_buffers,
1388                        self.edits_expanded,
1389                        pending_edits,
1390                        window,
1391                        cx,
1392                    ))
1393                    .when(self.edits_expanded, |parent| {
1394                        parent.child(self.render_edited_files(
1395                            action_log,
1396                            &changed_buffers,
1397                            pending_edits,
1398                            cx,
1399                        ))
1400                    })
1401            })
1402            .into_any()
1403            .into()
1404    }
1405
1406    fn render_plan_summary(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
1407        let stats = plan.stats();
1408
1409        let title = if let Some(entry) = stats.in_progress_entry
1410            && !self.plan_expanded
1411        {
1412            h_flex()
1413                .w_full()
1414                .gap_1()
1415                .text_xs()
1416                .text_color(cx.theme().colors().text_muted)
1417                .justify_between()
1418                .child(
1419                    h_flex()
1420                        .gap_1()
1421                        .child(
1422                            Label::new("Current:")
1423                                .size(LabelSize::Small)
1424                                .color(Color::Muted),
1425                        )
1426                        .child(MarkdownElement::new(
1427                            entry.content.clone(),
1428                            plan_label_markdown_style(&entry.status, window, cx),
1429                        )),
1430                )
1431                .when(stats.pending > 0, |this| {
1432                    this.child(
1433                        Label::new(format!("{} left", stats.pending))
1434                            .size(LabelSize::Small)
1435                            .color(Color::Muted)
1436                            .mr_1(),
1437                    )
1438                })
1439        } else {
1440            let status_label = if stats.pending == 0 {
1441                "All Done".to_string()
1442            } else if stats.completed == 0 {
1443                format!("{}", plan.entries.len())
1444            } else {
1445                format!("{}/{}", stats.completed, plan.entries.len())
1446            };
1447
1448            h_flex()
1449                .w_full()
1450                .gap_1()
1451                .justify_between()
1452                .child(
1453                    Label::new("Plan")
1454                        .size(LabelSize::Small)
1455                        .color(Color::Muted),
1456                )
1457                .child(
1458                    Label::new(status_label)
1459                        .size(LabelSize::Small)
1460                        .color(Color::Muted)
1461                        .mr_1(),
1462                )
1463        };
1464
1465        h_flex()
1466            .p_1()
1467            .justify_between()
1468            .when(self.plan_expanded, |this| {
1469                this.border_b_1().border_color(cx.theme().colors().border)
1470            })
1471            .child(
1472                h_flex()
1473                    .id("plan_summary")
1474                    .w_full()
1475                    .gap_1()
1476                    .child(Disclosure::new("plan_disclosure", self.plan_expanded))
1477                    .child(title)
1478                    .on_click(cx.listener(|this, _, _, cx| {
1479                        this.plan_expanded = !this.plan_expanded;
1480                        cx.notify();
1481                    })),
1482            )
1483    }
1484
1485    fn render_plan_entries(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
1486        v_flex().children(plan.entries.iter().enumerate().flat_map(|(index, entry)| {
1487            let element = h_flex()
1488                .py_1()
1489                .px_2()
1490                .gap_2()
1491                .justify_between()
1492                .bg(cx.theme().colors().editor_background)
1493                .when(index < plan.entries.len() - 1, |parent| {
1494                    parent.border_color(cx.theme().colors().border).border_b_1()
1495                })
1496                .child(
1497                    h_flex()
1498                        .id(("plan_entry", index))
1499                        .gap_1p5()
1500                        .max_w_full()
1501                        .overflow_x_scroll()
1502                        .text_xs()
1503                        .text_color(cx.theme().colors().text_muted)
1504                        .child(match entry.status {
1505                            acp::PlanEntryStatus::Pending => Icon::new(IconName::TodoPending)
1506                                .size(IconSize::Small)
1507                                .color(Color::Muted)
1508                                .into_any_element(),
1509                            acp::PlanEntryStatus::InProgress => Icon::new(IconName::TodoProgress)
1510                                .size(IconSize::Small)
1511                                .color(Color::Accent)
1512                                .with_animation(
1513                                    "running",
1514                                    Animation::new(Duration::from_secs(2)).repeat(),
1515                                    |icon, delta| {
1516                                        icon.transform(Transformation::rotate(percentage(delta)))
1517                                    },
1518                                )
1519                                .into_any_element(),
1520                            acp::PlanEntryStatus::Completed => Icon::new(IconName::TodoComplete)
1521                                .size(IconSize::Small)
1522                                .color(Color::Success)
1523                                .into_any_element(),
1524                        })
1525                        .child(MarkdownElement::new(
1526                            entry.content.clone(),
1527                            plan_label_markdown_style(&entry.status, window, cx),
1528                        )),
1529                );
1530
1531            Some(element)
1532        }))
1533    }
1534
1535    fn render_edits_summary(
1536        &self,
1537        action_log: &Entity<ActionLog>,
1538        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
1539        expanded: bool,
1540        pending_edits: bool,
1541        window: &mut Window,
1542        cx: &Context<Self>,
1543    ) -> Div {
1544        const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
1545
1546        let focus_handle = self.focus_handle(cx);
1547
1548        h_flex()
1549            .p_1()
1550            .justify_between()
1551            .when(expanded, |this| {
1552                this.border_b_1().border_color(cx.theme().colors().border)
1553            })
1554            .child(
1555                h_flex()
1556                    .id("edits-container")
1557                    .cursor_pointer()
1558                    .w_full()
1559                    .gap_1()
1560                    .child(Disclosure::new("edits-disclosure", expanded))
1561                    .map(|this| {
1562                        if pending_edits {
1563                            this.child(
1564                                Label::new(format!(
1565                                    "Editing {} {}",
1566                                    changed_buffers.len(),
1567                                    if changed_buffers.len() == 1 {
1568                                        "file"
1569                                    } else {
1570                                        "files"
1571                                    }
1572                                ))
1573                                .color(Color::Muted)
1574                                .size(LabelSize::Small)
1575                                .with_animation(
1576                                    "edit-label",
1577                                    Animation::new(Duration::from_secs(2))
1578                                        .repeat()
1579                                        .with_easing(pulsating_between(0.3, 0.7)),
1580                                    |label, delta| label.alpha(delta),
1581                                ),
1582                            )
1583                        } else {
1584                            this.child(
1585                                Label::new("Edits")
1586                                    .size(LabelSize::Small)
1587                                    .color(Color::Muted),
1588                            )
1589                            .child(Label::new("").size(LabelSize::XSmall).color(Color::Muted))
1590                            .child(
1591                                Label::new(format!(
1592                                    "{} {}",
1593                                    changed_buffers.len(),
1594                                    if changed_buffers.len() == 1 {
1595                                        "file"
1596                                    } else {
1597                                        "files"
1598                                    }
1599                                ))
1600                                .size(LabelSize::Small)
1601                                .color(Color::Muted),
1602                            )
1603                        }
1604                    })
1605                    .on_click(cx.listener(|this, _, _, cx| {
1606                        this.edits_expanded = !this.edits_expanded;
1607                        cx.notify();
1608                    })),
1609            )
1610            .child(
1611                h_flex()
1612                    .gap_1()
1613                    .child(
1614                        IconButton::new("review-changes", IconName::ListTodo)
1615                            .icon_size(IconSize::Small)
1616                            .tooltip({
1617                                let focus_handle = focus_handle.clone();
1618                                move |window, cx| {
1619                                    Tooltip::for_action_in(
1620                                        "Review Changes",
1621                                        &OpenAgentDiff,
1622                                        &focus_handle,
1623                                        window,
1624                                        cx,
1625                                    )
1626                                }
1627                            })
1628                            .on_click(cx.listener(|_, _, window, cx| {
1629                                window.dispatch_action(OpenAgentDiff.boxed_clone(), cx);
1630                            })),
1631                    )
1632                    .child(Divider::vertical().color(DividerColor::Border))
1633                    .child(
1634                        Button::new("reject-all-changes", "Reject All")
1635                            .label_size(LabelSize::Small)
1636                            .disabled(pending_edits)
1637                            .when(pending_edits, |this| {
1638                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
1639                            })
1640                            .key_binding(
1641                                KeyBinding::for_action_in(
1642                                    &RejectAll,
1643                                    &focus_handle.clone(),
1644                                    window,
1645                                    cx,
1646                                )
1647                                .map(|kb| kb.size(rems_from_px(10.))),
1648                            )
1649                            .on_click({
1650                                let action_log = action_log.clone();
1651                                cx.listener(move |_, _, _, cx| {
1652                                    action_log.update(cx, |action_log, cx| {
1653                                        action_log.reject_all_edits(cx).detach();
1654                                    })
1655                                })
1656                            }),
1657                    )
1658                    .child(
1659                        Button::new("keep-all-changes", "Keep All")
1660                            .label_size(LabelSize::Small)
1661                            .disabled(pending_edits)
1662                            .when(pending_edits, |this| {
1663                                this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
1664                            })
1665                            .key_binding(
1666                                KeyBinding::for_action_in(&KeepAll, &focus_handle, window, cx)
1667                                    .map(|kb| kb.size(rems_from_px(10.))),
1668                            )
1669                            .on_click({
1670                                let action_log = action_log.clone();
1671                                cx.listener(move |_, _, _, cx| {
1672                                    action_log.update(cx, |action_log, cx| {
1673                                        action_log.keep_all_edits(cx);
1674                                    })
1675                                })
1676                            }),
1677                    ),
1678            )
1679    }
1680
1681    fn render_edited_files(
1682        &self,
1683        action_log: &Entity<ActionLog>,
1684        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
1685        pending_edits: bool,
1686        cx: &Context<Self>,
1687    ) -> Div {
1688        let editor_bg_color = cx.theme().colors().editor_background;
1689
1690        v_flex().children(changed_buffers.into_iter().enumerate().flat_map(
1691            |(index, (buffer, _diff))| {
1692                let file = buffer.read(cx).file()?;
1693                let path = file.path();
1694
1695                let file_path = path.parent().and_then(|parent| {
1696                    let parent_str = parent.to_string_lossy();
1697
1698                    if parent_str.is_empty() {
1699                        None
1700                    } else {
1701                        Some(
1702                            Label::new(format!("/{}{}", parent_str, std::path::MAIN_SEPARATOR_STR))
1703                                .color(Color::Muted)
1704                                .size(LabelSize::XSmall)
1705                                .buffer_font(cx),
1706                        )
1707                    }
1708                });
1709
1710                let file_name = path.file_name().map(|name| {
1711                    Label::new(name.to_string_lossy().to_string())
1712                        .size(LabelSize::XSmall)
1713                        .buffer_font(cx)
1714                });
1715
1716                let file_icon = FileIcons::get_icon(&path, cx)
1717                    .map(Icon::from_path)
1718                    .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
1719                    .unwrap_or_else(|| {
1720                        Icon::new(IconName::File)
1721                            .color(Color::Muted)
1722                            .size(IconSize::Small)
1723                    });
1724
1725                let overlay_gradient = linear_gradient(
1726                    90.,
1727                    linear_color_stop(editor_bg_color, 1.),
1728                    linear_color_stop(editor_bg_color.opacity(0.2), 0.),
1729                );
1730
1731                let element = h_flex()
1732                    .group("edited-code")
1733                    .id(("file-container", index))
1734                    .relative()
1735                    .py_1()
1736                    .pl_2()
1737                    .pr_1()
1738                    .gap_2()
1739                    .justify_between()
1740                    .bg(editor_bg_color)
1741                    .when(index < changed_buffers.len() - 1, |parent| {
1742                        parent.border_color(cx.theme().colors().border).border_b_1()
1743                    })
1744                    .child(
1745                        h_flex()
1746                            .id(("file-name", index))
1747                            .pr_8()
1748                            .gap_1p5()
1749                            .max_w_full()
1750                            .overflow_x_scroll()
1751                            .child(file_icon)
1752                            .child(h_flex().gap_0p5().children(file_name).children(file_path))
1753                            .on_click({
1754                                let buffer = buffer.clone();
1755                                cx.listener(move |this, _, window, cx| {
1756                                    this.open_edited_buffer(&buffer, window, cx);
1757                                })
1758                            }),
1759                    )
1760                    .child(
1761                        h_flex()
1762                            .gap_1()
1763                            .visible_on_hover("edited-code")
1764                            .child(
1765                                Button::new("review", "Review")
1766                                    .label_size(LabelSize::Small)
1767                                    .on_click({
1768                                        let buffer = buffer.clone();
1769                                        cx.listener(move |this, _, window, cx| {
1770                                            this.open_edited_buffer(&buffer, window, cx);
1771                                        })
1772                                    }),
1773                            )
1774                            .child(Divider::vertical().color(DividerColor::BorderVariant))
1775                            .child(
1776                                Button::new("reject-file", "Reject")
1777                                    .label_size(LabelSize::Small)
1778                                    .disabled(pending_edits)
1779                                    .on_click({
1780                                        let buffer = buffer.clone();
1781                                        let action_log = action_log.clone();
1782                                        move |_, _, cx| {
1783                                            action_log.update(cx, |action_log, cx| {
1784                                                action_log
1785                                                    .reject_edits_in_ranges(
1786                                                        buffer.clone(),
1787                                                        vec![Anchor::MIN..Anchor::MAX],
1788                                                        cx,
1789                                                    )
1790                                                    .detach_and_log_err(cx);
1791                                            })
1792                                        }
1793                                    }),
1794                            )
1795                            .child(
1796                                Button::new("keep-file", "Keep")
1797                                    .label_size(LabelSize::Small)
1798                                    .disabled(pending_edits)
1799                                    .on_click({
1800                                        let buffer = buffer.clone();
1801                                        let action_log = action_log.clone();
1802                                        move |_, _, cx| {
1803                                            action_log.update(cx, |action_log, cx| {
1804                                                action_log.keep_edits_in_range(
1805                                                    buffer.clone(),
1806                                                    Anchor::MIN..Anchor::MAX,
1807                                                    cx,
1808                                                );
1809                                            })
1810                                        }
1811                                    }),
1812                            ),
1813                    )
1814                    .child(
1815                        div()
1816                            .id("gradient-overlay")
1817                            .absolute()
1818                            .h_full()
1819                            .w_12()
1820                            .top_0()
1821                            .bottom_0()
1822                            .right(px(152.))
1823                            .bg(overlay_gradient),
1824                    );
1825
1826                Some(element)
1827            },
1828        ))
1829    }
1830
1831    fn render_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
1832        let focus_handle = self.message_editor.focus_handle(cx);
1833        let editor_bg_color = cx.theme().colors().editor_background;
1834        let (expand_icon, expand_tooltip) = if self.editor_expanded {
1835            (IconName::Minimize, "Minimize Message Editor")
1836        } else {
1837            (IconName::Maximize, "Expand Message Editor")
1838        };
1839
1840        v_flex()
1841            .on_action(cx.listener(Self::expand_message_editor))
1842            .p_2()
1843            .gap_2()
1844            .border_t_1()
1845            .border_color(cx.theme().colors().border)
1846            .bg(editor_bg_color)
1847            .when(self.editor_expanded, |this| {
1848                this.h(vh(0.8, window)).size_full().justify_between()
1849            })
1850            .child(
1851                v_flex()
1852                    .relative()
1853                    .size_full()
1854                    .pt_1()
1855                    .pr_2p5()
1856                    .child(div().flex_1().child({
1857                        let settings = ThemeSettings::get_global(cx);
1858                        let font_size = TextSize::Small
1859                            .rems(cx)
1860                            .to_pixels(settings.agent_font_size(cx));
1861                        let line_height = settings.buffer_line_height.value() * font_size;
1862
1863                        let text_style = TextStyle {
1864                            color: cx.theme().colors().text,
1865                            font_family: settings.buffer_font.family.clone(),
1866                            font_fallbacks: settings.buffer_font.fallbacks.clone(),
1867                            font_features: settings.buffer_font.features.clone(),
1868                            font_size: font_size.into(),
1869                            line_height: line_height.into(),
1870                            ..Default::default()
1871                        };
1872
1873                        EditorElement::new(
1874                            &self.message_editor,
1875                            EditorStyle {
1876                                background: editor_bg_color,
1877                                local_player: cx.theme().players().local(),
1878                                text: text_style,
1879                                syntax: cx.theme().syntax().clone(),
1880                                ..Default::default()
1881                            },
1882                        )
1883                    }))
1884                    .child(
1885                        h_flex()
1886                            .absolute()
1887                            .top_0()
1888                            .right_0()
1889                            .opacity(0.5)
1890                            .hover(|this| this.opacity(1.0))
1891                            .child(
1892                                IconButton::new("toggle-height", expand_icon)
1893                                    .icon_size(IconSize::XSmall)
1894                                    .icon_color(Color::Muted)
1895                                    .tooltip({
1896                                        let focus_handle = focus_handle.clone();
1897                                        move |window, cx| {
1898                                            Tooltip::for_action_in(
1899                                                expand_tooltip,
1900                                                &ExpandMessageEditor,
1901                                                &focus_handle,
1902                                                window,
1903                                                cx,
1904                                            )
1905                                        }
1906                                    })
1907                                    .on_click(cx.listener(|_, _, window, cx| {
1908                                        window.dispatch_action(Box::new(ExpandMessageEditor), cx);
1909                                    })),
1910                            ),
1911                    ),
1912            )
1913            .child(
1914                h_flex()
1915                    .flex_none()
1916                    .justify_between()
1917                    .child(self.render_follow_toggle(cx))
1918                    .child(self.render_send_button(cx)),
1919            )
1920            .into_any()
1921    }
1922
1923    fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
1924        if self.thread().map_or(true, |thread| {
1925            thread.read(cx).status() == ThreadStatus::Idle
1926        }) {
1927            let is_editor_empty = self.message_editor.read(cx).is_empty(cx);
1928            IconButton::new("send-message", IconName::Send)
1929                .icon_color(Color::Accent)
1930                .style(ButtonStyle::Filled)
1931                .disabled(self.thread().is_none() || is_editor_empty)
1932                .on_click(cx.listener(|this, _, window, cx| {
1933                    this.chat(&Chat, window, cx);
1934                }))
1935                .when(!is_editor_empty, |button| {
1936                    button.tooltip(move |window, cx| Tooltip::for_action("Send", &Chat, window, cx))
1937                })
1938                .when(is_editor_empty, |button| {
1939                    button.tooltip(Tooltip::text("Type a message to submit"))
1940                })
1941                .into_any_element()
1942        } else {
1943            IconButton::new("stop-generation", IconName::StopFilled)
1944                .icon_color(Color::Error)
1945                .style(ButtonStyle::Tinted(ui::TintColor::Error))
1946                .tooltip(move |window, cx| {
1947                    Tooltip::for_action("Stop Generation", &editor::actions::Cancel, window, cx)
1948                })
1949                .on_click(cx.listener(|this, _event, _, cx| this.cancel(cx)))
1950                .into_any_element()
1951        }
1952    }
1953
1954    fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
1955        let following = self
1956            .workspace
1957            .read_with(cx, |workspace, _| {
1958                workspace.is_being_followed(CollaboratorId::Agent)
1959            })
1960            .unwrap_or(false);
1961
1962        IconButton::new("follow-agent", IconName::Crosshair)
1963            .icon_size(IconSize::Small)
1964            .icon_color(Color::Muted)
1965            .toggle_state(following)
1966            .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
1967            .tooltip(move |window, cx| {
1968                if following {
1969                    Tooltip::for_action("Stop Following Agent", &Follow, window, cx)
1970                } else {
1971                    Tooltip::with_meta(
1972                        "Follow Agent",
1973                        Some(&Follow),
1974                        "Track the agent's location as it reads and edits files.",
1975                        window,
1976                        cx,
1977                    )
1978                }
1979            })
1980            .on_click(cx.listener(move |this, _, window, cx| {
1981                this.workspace
1982                    .update(cx, |workspace, cx| {
1983                        if following {
1984                            workspace.unfollow(CollaboratorId::Agent, window, cx);
1985                        } else {
1986                            workspace.follow(CollaboratorId::Agent, window, cx);
1987                        }
1988                    })
1989                    .ok();
1990            }))
1991    }
1992
1993    fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
1994        let workspace = self.workspace.clone();
1995        MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
1996            Self::open_link(text, &workspace, window, cx);
1997        })
1998    }
1999
2000    fn open_link(
2001        url: SharedString,
2002        workspace: &WeakEntity<Workspace>,
2003        window: &mut Window,
2004        cx: &mut App,
2005    ) {
2006        let Some(workspace) = workspace.upgrade() else {
2007            cx.open_url(&url);
2008            return;
2009        };
2010
2011        if let Some(mention_path) = MentionPath::try_parse(&url) {
2012            workspace.update(cx, |workspace, cx| {
2013                let project = workspace.project();
2014                let Some((path, entry)) = project.update(cx, |project, cx| {
2015                    let path = project.find_project_path(mention_path.path(), cx)?;
2016                    let entry = project.entry_for_path(&path, cx)?;
2017                    Some((path, entry))
2018                }) else {
2019                    return;
2020                };
2021
2022                if entry.is_dir() {
2023                    project.update(cx, |_, cx| {
2024                        cx.emit(project::Event::RevealInProjectPanel(entry.id));
2025                    });
2026                } else {
2027                    workspace
2028                        .open_path(path, None, true, window, cx)
2029                        .detach_and_log_err(cx);
2030                }
2031            })
2032        } else {
2033            cx.open_url(&url);
2034        }
2035    }
2036
2037    fn open_tool_call_location(
2038        &self,
2039        entry_ix: usize,
2040        location_ix: usize,
2041        window: &mut Window,
2042        cx: &mut Context<Self>,
2043    ) -> Option<()> {
2044        let location = self
2045            .thread()?
2046            .read(cx)
2047            .entries()
2048            .get(entry_ix)?
2049            .locations()?
2050            .get(location_ix)?;
2051
2052        let project_path = self
2053            .project
2054            .read(cx)
2055            .find_project_path(&location.path, cx)?;
2056
2057        let open_task = self
2058            .workspace
2059            .update(cx, |worskpace, cx| {
2060                worskpace.open_path(project_path, None, true, window, cx)
2061            })
2062            .log_err()?;
2063
2064        window
2065            .spawn(cx, async move |cx| {
2066                let item = open_task.await?;
2067
2068                let Some(active_editor) = item.downcast::<Editor>() else {
2069                    return anyhow::Ok(());
2070                };
2071
2072                active_editor.update_in(cx, |editor, window, cx| {
2073                    let snapshot = editor.buffer().read(cx).snapshot(cx);
2074                    let first_hunk = editor
2075                        .diff_hunks_in_ranges(
2076                            &[editor::Anchor::min()..editor::Anchor::max()],
2077                            &snapshot,
2078                        )
2079                        .next();
2080                    if let Some(first_hunk) = first_hunk {
2081                        let first_hunk_start = first_hunk.multi_buffer_range().start;
2082                        editor.change_selections(Default::default(), window, cx, |selections| {
2083                            selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
2084                        })
2085                    }
2086                })?;
2087
2088                anyhow::Ok(())
2089            })
2090            .detach_and_log_err(cx);
2091
2092        None
2093    }
2094
2095    pub fn open_thread_as_markdown(
2096        &self,
2097        workspace: Entity<Workspace>,
2098        window: &mut Window,
2099        cx: &mut App,
2100    ) -> Task<anyhow::Result<()>> {
2101        let markdown_language_task = workspace
2102            .read(cx)
2103            .app_state()
2104            .languages
2105            .language_for_name("Markdown");
2106
2107        let (thread_summary, markdown) = if let Some(thread) = self.thread() {
2108            let thread = thread.read(cx);
2109            (thread.title().to_string(), thread.to_markdown(cx))
2110        } else {
2111            return Task::ready(Ok(()));
2112        };
2113
2114        window.spawn(cx, async move |cx| {
2115            let markdown_language = markdown_language_task.await?;
2116
2117            workspace.update_in(cx, |workspace, window, cx| {
2118                let project = workspace.project().clone();
2119
2120                if !project.read(cx).is_local() {
2121                    anyhow::bail!("failed to open active thread as markdown in remote project");
2122                }
2123
2124                let buffer = project.update(cx, |project, cx| {
2125                    project.create_local_buffer(&markdown, Some(markdown_language), cx)
2126                });
2127                let buffer = cx.new(|cx| {
2128                    MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
2129                });
2130
2131                workspace.add_item_to_active_pane(
2132                    Box::new(cx.new(|cx| {
2133                        let mut editor =
2134                            Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
2135                        editor.set_breadcrumb_header(thread_summary);
2136                        editor
2137                    })),
2138                    None,
2139                    true,
2140                    window,
2141                    cx,
2142                );
2143
2144                anyhow::Ok(())
2145            })??;
2146            anyhow::Ok(())
2147        })
2148    }
2149
2150    fn scroll_to_top(&mut self, cx: &mut Context<Self>) {
2151        self.list_state.scroll_to(ListOffset::default());
2152        cx.notify();
2153    }
2154}
2155
2156impl Focusable for AcpThreadView {
2157    fn focus_handle(&self, cx: &App) -> FocusHandle {
2158        self.message_editor.focus_handle(cx)
2159    }
2160}
2161
2162impl Render for AcpThreadView {
2163    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2164        let open_as_markdown = IconButton::new("open-as-markdown", IconName::DocumentText)
2165            .icon_size(IconSize::XSmall)
2166            .icon_color(Color::Ignored)
2167            .tooltip(Tooltip::text("Open Thread as Markdown"))
2168            .on_click(cx.listener(move |this, _, window, cx| {
2169                if let Some(workspace) = this.workspace.upgrade() {
2170                    this.open_thread_as_markdown(workspace, window, cx)
2171                        .detach_and_log_err(cx);
2172                }
2173            }));
2174
2175        let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUpAlt)
2176            .icon_size(IconSize::XSmall)
2177            .icon_color(Color::Ignored)
2178            .tooltip(Tooltip::text("Scroll To Top"))
2179            .on_click(cx.listener(move |this, _, _, cx| {
2180                this.scroll_to_top(cx);
2181            }));
2182
2183        v_flex()
2184            .size_full()
2185            .key_context("AcpThread")
2186            .on_action(cx.listener(Self::chat))
2187            .on_action(cx.listener(Self::previous_history_message))
2188            .on_action(cx.listener(Self::next_history_message))
2189            .on_action(cx.listener(Self::open_agent_diff))
2190            .child(match &self.thread_state {
2191                ThreadState::Unauthenticated { .. } => {
2192                    v_flex()
2193                        .p_2()
2194                        .flex_1()
2195                        .items_center()
2196                        .justify_center()
2197                        .child(self.render_pending_auth_state())
2198                        .child(
2199                            h_flex().mt_1p5().justify_center().child(
2200                                Button::new("sign-in", format!("Sign in to {}", self.agent.name()))
2201                                    .on_click(cx.listener(|this, _, window, cx| {
2202                                        this.authenticate(window, cx)
2203                                    })),
2204                            ),
2205                        )
2206                }
2207                ThreadState::Loading { .. } => v_flex().flex_1().child(self.render_empty_state(cx)),
2208                ThreadState::LoadError(e) => v_flex()
2209                    .p_2()
2210                    .flex_1()
2211                    .items_center()
2212                    .justify_center()
2213                    .child(self.render_error_state(e, cx)),
2214                ThreadState::Ready { thread, .. } => v_flex().flex_1().map(|this| {
2215                    if self.list_state.item_count() > 0 {
2216                        this.child(
2217                            list(self.list_state.clone())
2218                                .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
2219                                .flex_grow()
2220                                .into_any(),
2221                        )
2222                        .child(
2223                            h_flex()
2224                                .group("controls")
2225                                .mt_1()
2226                                .mr_1()
2227                                .py_2()
2228                                .px(RESPONSE_PADDING_X)
2229                                .opacity(0.4)
2230                                .hover(|style| style.opacity(1.))
2231                                .flex_wrap()
2232                                .justify_end()
2233                                .child(open_as_markdown)
2234                                .child(scroll_to_top)
2235                                .into_any_element(),
2236                        )
2237                        .children(match thread.read(cx).status() {
2238                            ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => None,
2239                            ThreadStatus::Generating => div()
2240                                .px_5()
2241                                .py_2()
2242                                .child(LoadingLabel::new("").size(LabelSize::Small))
2243                                .into(),
2244                        })
2245                        .children(self.render_activity_bar(&thread, window, cx))
2246                    } else {
2247                        this.child(self.render_empty_state(cx))
2248                    }
2249                }),
2250            })
2251            .when_some(self.last_error.clone(), |el, error| {
2252                el.child(
2253                    div()
2254                        .p_2()
2255                        .text_xs()
2256                        .border_t_1()
2257                        .border_color(cx.theme().colors().border)
2258                        .bg(cx.theme().status().error_background)
2259                        .child(
2260                            self.render_markdown(error, default_markdown_style(false, window, cx)),
2261                        ),
2262                )
2263            })
2264            .child(self.render_message_editor(window, cx))
2265    }
2266}
2267
2268fn user_message_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
2269    let mut style = default_markdown_style(false, window, cx);
2270    let mut text_style = window.text_style();
2271    let theme_settings = ThemeSettings::get_global(cx);
2272
2273    let buffer_font = theme_settings.buffer_font.family.clone();
2274    let buffer_font_size = TextSize::Small.rems(cx);
2275
2276    text_style.refine(&TextStyleRefinement {
2277        font_family: Some(buffer_font),
2278        font_size: Some(buffer_font_size.into()),
2279        ..Default::default()
2280    });
2281
2282    style.base_text_style = text_style;
2283    style.link_callback = Some(Rc::new(move |url, cx| {
2284        if MentionPath::try_parse(url).is_some() {
2285            let colors = cx.theme().colors();
2286            Some(TextStyleRefinement {
2287                background_color: Some(colors.element_background),
2288                ..Default::default()
2289            })
2290        } else {
2291            None
2292        }
2293    }));
2294    style
2295}
2296
2297fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> MarkdownStyle {
2298    let theme_settings = ThemeSettings::get_global(cx);
2299    let colors = cx.theme().colors();
2300
2301    let buffer_font_size = TextSize::Small.rems(cx);
2302
2303    let mut text_style = window.text_style();
2304    let line_height = buffer_font_size * 1.75;
2305
2306    let font_family = if buffer_font {
2307        theme_settings.buffer_font.family.clone()
2308    } else {
2309        theme_settings.ui_font.family.clone()
2310    };
2311
2312    let font_size = if buffer_font {
2313        TextSize::Small.rems(cx)
2314    } else {
2315        TextSize::Default.rems(cx)
2316    };
2317
2318    text_style.refine(&TextStyleRefinement {
2319        font_family: Some(font_family),
2320        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
2321        font_features: Some(theme_settings.ui_font.features.clone()),
2322        font_size: Some(font_size.into()),
2323        line_height: Some(line_height.into()),
2324        color: Some(cx.theme().colors().text),
2325        ..Default::default()
2326    });
2327
2328    MarkdownStyle {
2329        base_text_style: text_style.clone(),
2330        syntax: cx.theme().syntax().clone(),
2331        selection_background_color: cx.theme().colors().element_selection_background,
2332        code_block_overflow_x_scroll: true,
2333        table_overflow_x_scroll: true,
2334        heading_level_styles: Some(HeadingLevelStyles {
2335            h1: Some(TextStyleRefinement {
2336                font_size: Some(rems(1.15).into()),
2337                ..Default::default()
2338            }),
2339            h2: Some(TextStyleRefinement {
2340                font_size: Some(rems(1.1).into()),
2341                ..Default::default()
2342            }),
2343            h3: Some(TextStyleRefinement {
2344                font_size: Some(rems(1.05).into()),
2345                ..Default::default()
2346            }),
2347            h4: Some(TextStyleRefinement {
2348                font_size: Some(rems(1.).into()),
2349                ..Default::default()
2350            }),
2351            h5: Some(TextStyleRefinement {
2352                font_size: Some(rems(0.95).into()),
2353                ..Default::default()
2354            }),
2355            h6: Some(TextStyleRefinement {
2356                font_size: Some(rems(0.875).into()),
2357                ..Default::default()
2358            }),
2359        }),
2360        code_block: StyleRefinement {
2361            padding: EdgesRefinement {
2362                top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
2363                left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
2364                right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
2365                bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
2366            },
2367            margin: EdgesRefinement {
2368                top: Some(Length::Definite(Pixels(8.).into())),
2369                left: Some(Length::Definite(Pixels(0.).into())),
2370                right: Some(Length::Definite(Pixels(0.).into())),
2371                bottom: Some(Length::Definite(Pixels(12.).into())),
2372            },
2373            border_style: Some(BorderStyle::Solid),
2374            border_widths: EdgesRefinement {
2375                top: Some(AbsoluteLength::Pixels(Pixels(1.))),
2376                left: Some(AbsoluteLength::Pixels(Pixels(1.))),
2377                right: Some(AbsoluteLength::Pixels(Pixels(1.))),
2378                bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
2379            },
2380            border_color: Some(colors.border_variant),
2381            background: Some(colors.editor_background.into()),
2382            text: Some(TextStyleRefinement {
2383                font_family: Some(theme_settings.buffer_font.family.clone()),
2384                font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
2385                font_features: Some(theme_settings.buffer_font.features.clone()),
2386                font_size: Some(buffer_font_size.into()),
2387                ..Default::default()
2388            }),
2389            ..Default::default()
2390        },
2391        inline_code: TextStyleRefinement {
2392            font_family: Some(theme_settings.buffer_font.family.clone()),
2393            font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
2394            font_features: Some(theme_settings.buffer_font.features.clone()),
2395            font_size: Some(buffer_font_size.into()),
2396            background_color: Some(colors.editor_foreground.opacity(0.08)),
2397            ..Default::default()
2398        },
2399        link: TextStyleRefinement {
2400            background_color: Some(colors.editor_foreground.opacity(0.025)),
2401            underline: Some(UnderlineStyle {
2402                color: Some(colors.text_accent.opacity(0.5)),
2403                thickness: px(1.),
2404                ..Default::default()
2405            }),
2406            ..Default::default()
2407        },
2408        ..Default::default()
2409    }
2410}
2411
2412fn plan_label_markdown_style(
2413    status: &acp::PlanEntryStatus,
2414    window: &Window,
2415    cx: &App,
2416) -> MarkdownStyle {
2417    let default_md_style = default_markdown_style(false, window, cx);
2418
2419    MarkdownStyle {
2420        base_text_style: TextStyle {
2421            color: cx.theme().colors().text_muted,
2422            strikethrough: if matches!(status, acp::PlanEntryStatus::Completed) {
2423                Some(gpui::StrikethroughStyle {
2424                    thickness: px(1.),
2425                    color: Some(cx.theme().colors().text_muted.opacity(0.8)),
2426                })
2427            } else {
2428                None
2429            },
2430            ..default_md_style.base_text_style
2431        },
2432        ..default_md_style
2433    }
2434}