thread_view.rs

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