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