active_thread.rs

   1use crate::thread::{
   2    LastRestoreCheckpoint, MessageId, MessageSegment, RequestKind, Thread, ThreadError,
   3    ThreadEvent, ThreadFeedback,
   4};
   5use crate::thread_store::ThreadStore;
   6use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus};
   7use crate::ui::{ContextPill, ToolReadyPopUp, ToolReadyPopupEvent};
   8use crate::AssistantPanel;
   9use assistant_settings::AssistantSettings;
  10use collections::HashMap;
  11use editor::{Editor, MultiBuffer};
  12use gpui::{
  13    linear_color_stop, linear_gradient, list, percentage, pulsating_between, AbsoluteLength,
  14    Animation, AnimationExt, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
  15    Entity, Focusable, Hsla, Length, ListAlignment, ListState, MouseButton, ScrollHandle, Stateful,
  16    StyleRefinement, Subscription, Task, TextStyleRefinement, Transformation, UnderlineStyle,
  17    WeakEntity, WindowHandle,
  18};
  19use language::{Buffer, LanguageRegistry};
  20use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
  21use markdown::{Markdown, MarkdownStyle};
  22use settings::Settings as _;
  23use std::sync::Arc;
  24use std::time::Duration;
  25use theme::ThemeSettings;
  26use ui::{prelude::*, Disclosure, IconButton, KeyBinding, Scrollbar, ScrollbarState, Tooltip};
  27use util::ResultExt as _;
  28use workspace::{OpenOptions, Workspace};
  29
  30use crate::context_store::{refresh_context_store_text, ContextStore};
  31
  32pub struct ActiveThread {
  33    language_registry: Arc<LanguageRegistry>,
  34    thread_store: Entity<ThreadStore>,
  35    thread: Entity<Thread>,
  36    context_store: Entity<ContextStore>,
  37    workspace: WeakEntity<Workspace>,
  38    save_thread_task: Option<Task<()>>,
  39    messages: Vec<MessageId>,
  40    list_state: ListState,
  41    scrollbar_state: ScrollbarState,
  42    rendered_messages_by_id: HashMap<MessageId, RenderedMessage>,
  43    rendered_tool_use_labels: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
  44    editing_message: Option<(MessageId, EditMessageState)>,
  45    expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
  46    expanded_thinking_segments: HashMap<(MessageId, usize), bool>,
  47    last_error: Option<ThreadError>,
  48    pop_ups: Vec<WindowHandle<ToolReadyPopUp>>,
  49    _subscriptions: Vec<Subscription>,
  50    pop_up_subscriptions: HashMap<WindowHandle<ToolReadyPopUp>, Vec<Subscription>>,
  51}
  52
  53struct RenderedMessage {
  54    language_registry: Arc<LanguageRegistry>,
  55    segments: Vec<RenderedMessageSegment>,
  56}
  57
  58impl RenderedMessage {
  59    fn from_segments(
  60        segments: &[MessageSegment],
  61        language_registry: Arc<LanguageRegistry>,
  62        window: &Window,
  63        cx: &mut App,
  64    ) -> Self {
  65        let mut this = Self {
  66            language_registry,
  67            segments: Vec::with_capacity(segments.len()),
  68        };
  69        for segment in segments {
  70            this.push_segment(segment, window, cx);
  71        }
  72        this
  73    }
  74
  75    fn append_thinking(&mut self, text: &String, window: &Window, cx: &mut App) {
  76        if let Some(RenderedMessageSegment::Thinking {
  77            content,
  78            scroll_handle,
  79        }) = self.segments.last_mut()
  80        {
  81            content.update(cx, |markdown, cx| {
  82                markdown.append(text, cx);
  83            });
  84            scroll_handle.scroll_to_bottom();
  85        } else {
  86            self.segments.push(RenderedMessageSegment::Thinking {
  87                content: render_markdown(text.into(), self.language_registry.clone(), window, cx),
  88                scroll_handle: ScrollHandle::default(),
  89            });
  90        }
  91    }
  92
  93    fn append_text(&mut self, text: &String, window: &Window, cx: &mut App) {
  94        if let Some(RenderedMessageSegment::Text(markdown)) = self.segments.last_mut() {
  95            markdown.update(cx, |markdown, cx| markdown.append(text, cx));
  96        } else {
  97            self.segments
  98                .push(RenderedMessageSegment::Text(render_markdown(
  99                    SharedString::from(text),
 100                    self.language_registry.clone(),
 101                    window,
 102                    cx,
 103                )));
 104        }
 105    }
 106
 107    fn push_segment(&mut self, segment: &MessageSegment, window: &Window, cx: &mut App) {
 108        let rendered_segment = match segment {
 109            MessageSegment::Thinking(text) => RenderedMessageSegment::Thinking {
 110                content: render_markdown(text.into(), self.language_registry.clone(), window, cx),
 111                scroll_handle: ScrollHandle::default(),
 112            },
 113            MessageSegment::Text(text) => RenderedMessageSegment::Text(render_markdown(
 114                text.into(),
 115                self.language_registry.clone(),
 116                window,
 117                cx,
 118            )),
 119        };
 120        self.segments.push(rendered_segment);
 121    }
 122}
 123
 124enum RenderedMessageSegment {
 125    Thinking {
 126        content: Entity<Markdown>,
 127        scroll_handle: ScrollHandle,
 128    },
 129    Text(Entity<Markdown>),
 130}
 131
 132fn render_markdown(
 133    text: SharedString,
 134    language_registry: Arc<LanguageRegistry>,
 135    window: &Window,
 136    cx: &mut App,
 137) -> Entity<Markdown> {
 138    let theme_settings = ThemeSettings::get_global(cx);
 139    let colors = cx.theme().colors();
 140    let ui_font_size = TextSize::Default.rems(cx);
 141    let buffer_font_size = TextSize::Small.rems(cx);
 142    let mut text_style = window.text_style();
 143
 144    text_style.refine(&TextStyleRefinement {
 145        font_family: Some(theme_settings.ui_font.family.clone()),
 146        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
 147        font_features: Some(theme_settings.ui_font.features.clone()),
 148        font_size: Some(ui_font_size.into()),
 149        color: Some(cx.theme().colors().text),
 150        ..Default::default()
 151    });
 152
 153    let markdown_style = MarkdownStyle {
 154        base_text_style: text_style,
 155        syntax: cx.theme().syntax().clone(),
 156        selection_background_color: cx.theme().players().local().selection,
 157        code_block_overflow_x_scroll: true,
 158        table_overflow_x_scroll: true,
 159        code_block: StyleRefinement {
 160            margin: EdgesRefinement {
 161                top: Some(Length::Definite(rems(0.).into())),
 162                left: Some(Length::Definite(rems(0.).into())),
 163                right: Some(Length::Definite(rems(0.).into())),
 164                bottom: Some(Length::Definite(rems(0.5).into())),
 165            },
 166            padding: EdgesRefinement {
 167                top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
 168                left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
 169                right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
 170                bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
 171            },
 172            background: Some(colors.editor_background.into()),
 173            border_color: Some(colors.border_variant),
 174            border_widths: EdgesRefinement {
 175                top: Some(AbsoluteLength::Pixels(Pixels(1.))),
 176                left: Some(AbsoluteLength::Pixels(Pixels(1.))),
 177                right: Some(AbsoluteLength::Pixels(Pixels(1.))),
 178                bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
 179            },
 180            text: Some(TextStyleRefinement {
 181                font_family: Some(theme_settings.buffer_font.family.clone()),
 182                font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
 183                font_features: Some(theme_settings.buffer_font.features.clone()),
 184                font_size: Some(buffer_font_size.into()),
 185                ..Default::default()
 186            }),
 187            ..Default::default()
 188        },
 189        inline_code: TextStyleRefinement {
 190            font_family: Some(theme_settings.buffer_font.family.clone()),
 191            font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
 192            font_features: Some(theme_settings.buffer_font.features.clone()),
 193            font_size: Some(buffer_font_size.into()),
 194            background_color: Some(colors.editor_foreground.opacity(0.1)),
 195            ..Default::default()
 196        },
 197        link: TextStyleRefinement {
 198            background_color: Some(colors.editor_foreground.opacity(0.025)),
 199            underline: Some(UnderlineStyle {
 200                color: Some(colors.text_accent.opacity(0.5)),
 201                thickness: px(1.),
 202                ..Default::default()
 203            }),
 204            ..Default::default()
 205        },
 206        ..Default::default()
 207    };
 208
 209    cx.new(|cx| Markdown::new(text, markdown_style, Some(language_registry), None, cx))
 210}
 211
 212struct EditMessageState {
 213    editor: Entity<Editor>,
 214}
 215
 216impl ActiveThread {
 217    pub fn new(
 218        thread: Entity<Thread>,
 219        thread_store: Entity<ThreadStore>,
 220        language_registry: Arc<LanguageRegistry>,
 221        context_store: Entity<ContextStore>,
 222        workspace: WeakEntity<Workspace>,
 223        window: &mut Window,
 224        cx: &mut Context<Self>,
 225    ) -> Self {
 226        let subscriptions = vec![
 227            cx.observe(&thread, |_, _, cx| cx.notify()),
 228            cx.subscribe_in(&thread, window, Self::handle_thread_event),
 229        ];
 230
 231        let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.), {
 232            let this = cx.entity().downgrade();
 233            move |ix, window: &mut Window, cx: &mut App| {
 234                this.update(cx, |this, cx| this.render_message(ix, window, cx))
 235                    .unwrap()
 236            }
 237        });
 238
 239        let mut this = Self {
 240            language_registry,
 241            thread_store,
 242            thread: thread.clone(),
 243            context_store,
 244            workspace,
 245            save_thread_task: None,
 246            messages: Vec::new(),
 247            rendered_messages_by_id: HashMap::default(),
 248            rendered_tool_use_labels: HashMap::default(),
 249            expanded_tool_uses: HashMap::default(),
 250            expanded_thinking_segments: HashMap::default(),
 251            list_state: list_state.clone(),
 252            scrollbar_state: ScrollbarState::new(list_state),
 253            editing_message: None,
 254            last_error: None,
 255            pop_ups: Vec::new(),
 256            _subscriptions: subscriptions,
 257            pop_up_subscriptions: HashMap::default(),
 258        };
 259
 260        for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
 261            this.push_message(&message.id, &message.segments, window, cx);
 262
 263            for tool_use in thread.read(cx).tool_uses_for_message(message.id, cx) {
 264                this.render_tool_use_label_markdown(
 265                    tool_use.id.clone(),
 266                    tool_use.ui_text.clone(),
 267                    window,
 268                    cx,
 269                );
 270            }
 271        }
 272
 273        this
 274    }
 275
 276    pub fn thread(&self) -> &Entity<Thread> {
 277        &self.thread
 278    }
 279
 280    pub fn is_empty(&self) -> bool {
 281        self.messages.is_empty()
 282    }
 283
 284    pub fn summary(&self, cx: &App) -> Option<SharedString> {
 285        self.thread.read(cx).summary()
 286    }
 287
 288    pub fn summary_or_default(&self, cx: &App) -> SharedString {
 289        self.thread.read(cx).summary_or_default()
 290    }
 291
 292    pub fn cancel_last_completion(&mut self, cx: &mut App) -> bool {
 293        self.last_error.take();
 294        self.thread
 295            .update(cx, |thread, cx| thread.cancel_last_completion(cx))
 296    }
 297
 298    pub fn last_error(&self) -> Option<ThreadError> {
 299        self.last_error.clone()
 300    }
 301
 302    pub fn clear_last_error(&mut self) {
 303        self.last_error.take();
 304    }
 305
 306    fn push_message(
 307        &mut self,
 308        id: &MessageId,
 309        segments: &[MessageSegment],
 310        window: &mut Window,
 311        cx: &mut Context<Self>,
 312    ) {
 313        let old_len = self.messages.len();
 314        self.messages.push(*id);
 315        self.list_state.splice(old_len..old_len, 1);
 316
 317        let rendered_message =
 318            RenderedMessage::from_segments(segments, self.language_registry.clone(), window, cx);
 319        self.rendered_messages_by_id.insert(*id, rendered_message);
 320    }
 321
 322    fn edited_message(
 323        &mut self,
 324        id: &MessageId,
 325        segments: &[MessageSegment],
 326        window: &mut Window,
 327        cx: &mut Context<Self>,
 328    ) {
 329        let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
 330            return;
 331        };
 332        self.list_state.splice(index..index + 1, 1);
 333        let rendered_message =
 334            RenderedMessage::from_segments(segments, self.language_registry.clone(), window, cx);
 335        self.rendered_messages_by_id.insert(*id, rendered_message);
 336    }
 337
 338    fn deleted_message(&mut self, id: &MessageId) {
 339        let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
 340            return;
 341        };
 342        self.messages.remove(index);
 343        self.list_state.splice(index..index + 1, 0);
 344        self.rendered_messages_by_id.remove(id);
 345    }
 346
 347    fn render_tool_use_label_markdown(
 348        &mut self,
 349        tool_use_id: LanguageModelToolUseId,
 350        tool_label: impl Into<SharedString>,
 351        window: &mut Window,
 352        cx: &mut Context<Self>,
 353    ) {
 354        self.rendered_tool_use_labels.insert(
 355            tool_use_id,
 356            render_markdown(
 357                tool_label.into(),
 358                self.language_registry.clone(),
 359                window,
 360                cx,
 361            ),
 362        );
 363    }
 364
 365    fn handle_thread_event(
 366        &mut self,
 367        _thread: &Entity<Thread>,
 368        event: &ThreadEvent,
 369        window: &mut Window,
 370        cx: &mut Context<Self>,
 371    ) {
 372        match event {
 373            ThreadEvent::ShowError(error) => {
 374                self.last_error = Some(error.clone());
 375            }
 376            ThreadEvent::StreamedCompletion | ThreadEvent::SummaryChanged => {
 377                self.save_thread(cx);
 378            }
 379            ThreadEvent::DoneStreaming => {
 380                if !self.thread().read(cx).is_generating() {
 381                    self.show_notification(
 382                        "The assistant response has concluded.",
 383                        IconName::Check,
 384                        Color::Success,
 385                        window,
 386                        cx,
 387                    );
 388                }
 389            }
 390            ThreadEvent::ToolConfirmationNeeded => {
 391                self.show_notification(
 392                    "There's a tool confirmation needed.",
 393                    IconName::Info,
 394                    Color::Muted,
 395                    window,
 396                    cx,
 397                );
 398            }
 399            ThreadEvent::StreamedAssistantText(message_id, text) => {
 400                if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
 401                    rendered_message.append_text(text, window, cx);
 402                }
 403            }
 404            ThreadEvent::StreamedAssistantThinking(message_id, text) => {
 405                if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
 406                    rendered_message.append_thinking(text, window, cx);
 407                }
 408            }
 409            ThreadEvent::MessageAdded(message_id) => {
 410                if let Some(message_segments) = self
 411                    .thread
 412                    .read(cx)
 413                    .message(*message_id)
 414                    .map(|message| message.segments.clone())
 415                {
 416                    self.push_message(message_id, &message_segments, window, cx);
 417                }
 418
 419                self.save_thread(cx);
 420                cx.notify();
 421            }
 422            ThreadEvent::MessageEdited(message_id) => {
 423                if let Some(message_segments) = self
 424                    .thread
 425                    .read(cx)
 426                    .message(*message_id)
 427                    .map(|message| message.segments.clone())
 428                {
 429                    self.edited_message(message_id, &message_segments, window, cx);
 430                }
 431
 432                self.save_thread(cx);
 433                cx.notify();
 434            }
 435            ThreadEvent::MessageDeleted(message_id) => {
 436                self.deleted_message(message_id);
 437                self.save_thread(cx);
 438                cx.notify();
 439            }
 440            ThreadEvent::UsePendingTools => {
 441                let tool_uses = self
 442                    .thread
 443                    .update(cx, |thread, cx| thread.use_pending_tools(cx));
 444
 445                for tool_use in tool_uses {
 446                    self.render_tool_use_label_markdown(
 447                        tool_use.id.clone(),
 448                        tool_use.ui_text.clone(),
 449                        window,
 450                        cx,
 451                    );
 452                }
 453            }
 454            ThreadEvent::ToolFinished {
 455                pending_tool_use,
 456                canceled,
 457                ..
 458            } => {
 459                let canceled = *canceled;
 460                if let Some(tool_use) = pending_tool_use {
 461                    self.render_tool_use_label_markdown(
 462                        tool_use.id.clone(),
 463                        SharedString::from(tool_use.ui_text.clone()),
 464                        window,
 465                        cx,
 466                    );
 467                }
 468
 469                if self.thread.read(cx).all_tools_finished() {
 470                    let pending_refresh_buffers = self.thread.update(cx, |thread, cx| {
 471                        thread.action_log().update(cx, |action_log, _cx| {
 472                            action_log.take_stale_buffers_in_context()
 473                        })
 474                    });
 475
 476                    let context_update_task = if !pending_refresh_buffers.is_empty() {
 477                        let refresh_task = refresh_context_store_text(
 478                            self.context_store.clone(),
 479                            &pending_refresh_buffers,
 480                            cx,
 481                        );
 482
 483                        cx.spawn(async move |this, cx| {
 484                            let updated_context_ids = refresh_task.await;
 485
 486                            this.update(cx, |this, cx| {
 487                                this.context_store.read_with(cx, |context_store, cx| {
 488                                    context_store
 489                                        .context()
 490                                        .iter()
 491                                        .filter(|context| {
 492                                            updated_context_ids.contains(&context.id())
 493                                        })
 494                                        .flat_map(|context| context.snapshot(cx))
 495                                        .collect()
 496                                })
 497                            })
 498                        })
 499                    } else {
 500                        Task::ready(anyhow::Ok(Vec::new()))
 501                    };
 502
 503                    let model_registry = LanguageModelRegistry::read_global(cx);
 504                    if let Some(model) = model_registry.active_model() {
 505                        cx.spawn(async move |this, cx| {
 506                            let updated_context = context_update_task.await?;
 507
 508                            this.update(cx, |this, cx| {
 509                                this.thread.update(cx, |thread, cx| {
 510                                    thread.attach_tool_results(updated_context, cx);
 511                                    if !canceled {
 512                                        thread.send_to_model(model, RequestKind::Chat, cx);
 513                                    }
 514                                });
 515                            })
 516                        })
 517                        .detach();
 518                    }
 519                }
 520            }
 521            ThreadEvent::CheckpointChanged => cx.notify(),
 522        }
 523    }
 524
 525    fn show_notification(
 526        &mut self,
 527        caption: impl Into<SharedString>,
 528        icon: IconName,
 529        icon_color: Color,
 530        window: &mut Window,
 531        cx: &mut Context<'_, ActiveThread>,
 532    ) {
 533        if !window.is_window_active()
 534            && self.pop_ups.is_empty()
 535            && AssistantSettings::get_global(cx).notify_when_agent_waiting
 536        {
 537            let caption = caption.into();
 538
 539            for screen in cx.displays() {
 540                let options = ToolReadyPopUp::window_options(screen, cx);
 541
 542                if let Some(screen_window) = cx
 543                    .open_window(options, |_, cx| {
 544                        cx.new(|_| ToolReadyPopUp::new(caption.clone(), icon, icon_color))
 545                    })
 546                    .log_err()
 547                {
 548                    if let Some(pop_up) = screen_window.entity(cx).log_err() {
 549                        self.pop_up_subscriptions
 550                            .entry(screen_window)
 551                            .or_insert_with(Vec::new)
 552                            .push(cx.subscribe_in(&pop_up, window, {
 553                                |this, _, event, window, cx| match event {
 554                                    ToolReadyPopupEvent::Accepted => {
 555                                        let handle = window.window_handle();
 556                                        cx.activate(true); // Switch back to the Zed application
 557
 558                                        let workspace_handle = this.workspace.clone();
 559
 560                                        // If there are multiple Zed windows, activate the correct one.
 561                                        cx.defer(move |cx| {
 562                                            handle
 563                                                .update(cx, |_view, window, _cx| {
 564                                                    window.activate_window();
 565
 566                                                    if let Some(workspace) =
 567                                                        workspace_handle.upgrade()
 568                                                    {
 569                                                        workspace.update(_cx, |workspace, cx| {
 570                                                            workspace
 571                                                                .focus_panel::<AssistantPanel>(
 572                                                                    window, cx,
 573                                                                );
 574                                                        });
 575                                                    }
 576                                                })
 577                                                .log_err();
 578                                        });
 579
 580                                        this.dismiss_notifications(cx);
 581                                    }
 582                                    ToolReadyPopupEvent::Dismissed => {
 583                                        this.dismiss_notifications(cx);
 584                                    }
 585                                }
 586                            }));
 587
 588                        self.pop_ups.push(screen_window);
 589
 590                        // If the user manually refocuses the original window, dismiss the popup.
 591                        self.pop_up_subscriptions
 592                            .entry(screen_window)
 593                            .or_insert_with(Vec::new)
 594                            .push({
 595                                let pop_up_weak = pop_up.downgrade();
 596
 597                                cx.observe_window_activation(window, move |_, window, cx| {
 598                                    if window.is_window_active() {
 599                                        if let Some(pop_up) = pop_up_weak.upgrade() {
 600                                            pop_up.update(cx, |_, cx| {
 601                                                cx.emit(ToolReadyPopupEvent::Dismissed);
 602                                            });
 603                                        }
 604                                    }
 605                                })
 606                            });
 607                    }
 608                }
 609            }
 610        }
 611    }
 612
 613    /// Spawns a task to save the active thread.
 614    ///
 615    /// Only one task to save the thread will be in flight at a time.
 616    fn save_thread(&mut self, cx: &mut Context<Self>) {
 617        let thread = self.thread.clone();
 618        self.save_thread_task = Some(cx.spawn(async move |this, cx| {
 619            let task = this
 620                .update(cx, |this, cx| {
 621                    this.thread_store
 622                        .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
 623                })
 624                .ok();
 625
 626            if let Some(task) = task {
 627                task.await.log_err();
 628            }
 629        }));
 630    }
 631
 632    fn start_editing_message(
 633        &mut self,
 634        message_id: MessageId,
 635        message_segments: &[MessageSegment],
 636        window: &mut Window,
 637        cx: &mut Context<Self>,
 638    ) {
 639        // User message should always consist of a single text segment,
 640        // therefore we can skip returning early if it's not a text segment.
 641        let Some(MessageSegment::Text(message_text)) = message_segments.first() else {
 642            return;
 643        };
 644
 645        let buffer = cx.new(|cx| {
 646            MultiBuffer::singleton(cx.new(|cx| Buffer::local(message_text.clone(), cx)), cx)
 647        });
 648        let editor = cx.new(|cx| {
 649            let mut editor = Editor::new(
 650                editor::EditorMode::AutoHeight { max_lines: 8 },
 651                buffer,
 652                None,
 653                window,
 654                cx,
 655            );
 656            editor.focus_handle(cx).focus(window);
 657            editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
 658            editor
 659        });
 660        self.editing_message = Some((
 661            message_id,
 662            EditMessageState {
 663                editor: editor.clone(),
 664            },
 665        ));
 666        cx.notify();
 667    }
 668
 669    fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
 670        self.editing_message.take();
 671        cx.notify();
 672    }
 673
 674    fn confirm_editing_message(
 675        &mut self,
 676        _: &menu::Confirm,
 677        _: &mut Window,
 678        cx: &mut Context<Self>,
 679    ) {
 680        let Some((message_id, state)) = self.editing_message.take() else {
 681            return;
 682        };
 683        let edited_text = state.editor.read(cx).text(cx);
 684        self.thread.update(cx, |thread, cx| {
 685            thread.edit_message(
 686                message_id,
 687                Role::User,
 688                vec![MessageSegment::Text(edited_text)],
 689                cx,
 690            );
 691            for message_id in self.messages_after(message_id) {
 692                thread.delete_message(*message_id, cx);
 693            }
 694        });
 695
 696        let provider = LanguageModelRegistry::read_global(cx).active_provider();
 697        if provider
 698            .as_ref()
 699            .map_or(false, |provider| provider.must_accept_terms(cx))
 700        {
 701            cx.notify();
 702            return;
 703        }
 704        let model_registry = LanguageModelRegistry::read_global(cx);
 705        let Some(model) = model_registry.active_model() else {
 706            return;
 707        };
 708
 709        self.thread.update(cx, |thread, cx| {
 710            thread.send_to_model(model, RequestKind::Chat, cx)
 711        });
 712        cx.notify();
 713    }
 714
 715    fn last_user_message(&self, cx: &Context<Self>) -> Option<MessageId> {
 716        self.messages
 717            .iter()
 718            .rev()
 719            .find(|message_id| {
 720                self.thread
 721                    .read(cx)
 722                    .message(**message_id)
 723                    .map_or(false, |message| message.role == Role::User)
 724            })
 725            .cloned()
 726    }
 727
 728    fn messages_after(&self, message_id: MessageId) -> &[MessageId] {
 729        self.messages
 730            .iter()
 731            .position(|id| *id == message_id)
 732            .map(|index| &self.messages[index + 1..])
 733            .unwrap_or(&[])
 734    }
 735
 736    fn handle_cancel_click(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
 737        self.cancel_editing_message(&menu::Cancel, window, cx);
 738    }
 739
 740    fn handle_regenerate_click(
 741        &mut self,
 742        _: &ClickEvent,
 743        window: &mut Window,
 744        cx: &mut Context<Self>,
 745    ) {
 746        self.confirm_editing_message(&menu::Confirm, window, cx);
 747    }
 748
 749    fn handle_feedback_click(
 750        &mut self,
 751        feedback: ThreadFeedback,
 752        _window: &mut Window,
 753        cx: &mut Context<Self>,
 754    ) {
 755        let report = self
 756            .thread
 757            .update(cx, |thread, cx| thread.report_feedback(feedback, cx));
 758
 759        let this = cx.entity().downgrade();
 760        cx.spawn(async move |_, cx| {
 761            report.await?;
 762            this.update(cx, |_this, cx| cx.notify())
 763        })
 764        .detach_and_log_err(cx);
 765    }
 766
 767    fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
 768        let message_id = self.messages[ix];
 769        let Some(message) = self.thread.read(cx).message(message_id) else {
 770            return Empty.into_any();
 771        };
 772
 773        let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else {
 774            return Empty.into_any();
 775        };
 776
 777        let thread = self.thread.read(cx);
 778        // Get all the data we need from thread before we start using it in closures
 779        let checkpoint = thread.checkpoint_for_message(message_id);
 780        let context = thread.context_for_message(message_id);
 781        let tool_uses = thread.tool_uses_for_message(message_id, cx);
 782
 783        // Don't render user messages that are just there for returning tool results.
 784        if message.role == Role::User && thread.message_has_tool_results(message_id) {
 785            return Empty.into_any();
 786        }
 787
 788        let allow_editing_message =
 789            message.role == Role::User && self.last_user_message(cx) == Some(message_id);
 790
 791        let edit_message_editor = self
 792            .editing_message
 793            .as_ref()
 794            .filter(|(id, _)| *id == message_id)
 795            .map(|(_, state)| state.editor.clone());
 796
 797        let first_message = ix == 0;
 798        let is_last_message = ix == self.messages.len() - 1;
 799
 800        let colors = cx.theme().colors();
 801        let active_color = colors.element_active;
 802        let editor_bg_color = colors.editor_background;
 803        let bg_user_message_header = editor_bg_color.blend(active_color.opacity(0.25));
 804
 805        let feedback_container = h_flex().pt_2().pb_4().px_4().gap_1().justify_between();
 806        let feedback_items = match self.thread.read(cx).feedback() {
 807            Some(feedback) => feedback_container
 808                .child(
 809                    Label::new(match feedback {
 810                        ThreadFeedback::Positive => "Thanks for your feedback!",
 811                        ThreadFeedback::Negative => {
 812                            "We appreciate your feedback and will use it to improve."
 813                        }
 814                    })
 815                    .color(Color::Muted)
 816                    .size(LabelSize::XSmall),
 817                )
 818                .child(
 819                    h_flex()
 820                        .gap_1()
 821                        .child(
 822                            IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
 823                                .icon_size(IconSize::XSmall)
 824                                .icon_color(match feedback {
 825                                    ThreadFeedback::Positive => Color::Accent,
 826                                    ThreadFeedback::Negative => Color::Ignored,
 827                                })
 828                                .shape(ui::IconButtonShape::Square)
 829                                .tooltip(Tooltip::text("Helpful Response"))
 830                                .on_click(cx.listener(move |this, _, window, cx| {
 831                                    this.handle_feedback_click(
 832                                        ThreadFeedback::Positive,
 833                                        window,
 834                                        cx,
 835                                    );
 836                                })),
 837                        )
 838                        .child(
 839                            IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
 840                                .icon_size(IconSize::XSmall)
 841                                .icon_color(match feedback {
 842                                    ThreadFeedback::Positive => Color::Ignored,
 843                                    ThreadFeedback::Negative => Color::Accent,
 844                                })
 845                                .shape(ui::IconButtonShape::Square)
 846                                .tooltip(Tooltip::text("Not Helpful"))
 847                                .on_click(cx.listener(move |this, _, window, cx| {
 848                                    this.handle_feedback_click(
 849                                        ThreadFeedback::Negative,
 850                                        window,
 851                                        cx,
 852                                    );
 853                                })),
 854                        ),
 855                )
 856                .into_any_element(),
 857            None => feedback_container
 858                .child(
 859                    Label::new(
 860                        "Rating the thread sends all of your current conversation to the Zed team.",
 861                    )
 862                    .color(Color::Muted)
 863                    .size(LabelSize::XSmall),
 864                )
 865                .child(
 866                    h_flex()
 867                        .gap_1()
 868                        .child(
 869                            IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
 870                                .icon_size(IconSize::XSmall)
 871                                .icon_color(Color::Ignored)
 872                                .shape(ui::IconButtonShape::Square)
 873                                .tooltip(Tooltip::text("Helpful Response"))
 874                                .on_click(cx.listener(move |this, _, window, cx| {
 875                                    this.handle_feedback_click(
 876                                        ThreadFeedback::Positive,
 877                                        window,
 878                                        cx,
 879                                    );
 880                                })),
 881                        )
 882                        .child(
 883                            IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
 884                                .icon_size(IconSize::XSmall)
 885                                .icon_color(Color::Ignored)
 886                                .shape(ui::IconButtonShape::Square)
 887                                .tooltip(Tooltip::text("Not Helpful"))
 888                                .on_click(cx.listener(move |this, _, window, cx| {
 889                                    this.handle_feedback_click(
 890                                        ThreadFeedback::Negative,
 891                                        window,
 892                                        cx,
 893                                    );
 894                                })),
 895                        ),
 896                )
 897                .into_any_element(),
 898        };
 899
 900        let message_content = v_flex()
 901            .gap_1p5()
 902            .child(
 903                if let Some(edit_message_editor) = edit_message_editor.clone() {
 904                    div()
 905                        .key_context("EditMessageEditor")
 906                        .on_action(cx.listener(Self::cancel_editing_message))
 907                        .on_action(cx.listener(Self::confirm_editing_message))
 908                        .min_h_6()
 909                        .child(edit_message_editor)
 910                } else {
 911                    div()
 912                        .min_h_6()
 913                        .text_ui(cx)
 914                        .child(self.render_message_content(message_id, rendered_message, cx))
 915                },
 916            )
 917            .when_some(context, |parent, context| {
 918                if !context.is_empty() {
 919                    parent.child(
 920                        h_flex().flex_wrap().gap_1().children(
 921                            context
 922                                .into_iter()
 923                                .map(|context| ContextPill::added(context, false, false, None)),
 924                        ),
 925                    )
 926                } else {
 927                    parent
 928                }
 929            });
 930
 931        let styled_message = match message.role {
 932            Role::User => v_flex()
 933                .id(("message-container", ix))
 934                .map(|this| {
 935                    if first_message {
 936                        this.pt_2()
 937                    } else {
 938                        this.pt_4()
 939                    }
 940                })
 941                .pb_4()
 942                .pl_2()
 943                .pr_2p5()
 944                .child(
 945                    v_flex()
 946                        .bg(colors.editor_background)
 947                        .rounded_lg()
 948                        .border_1()
 949                        .border_color(colors.border)
 950                        .shadow_md()
 951                        .child(
 952                            h_flex()
 953                                .py_1()
 954                                .pl_2()
 955                                .pr_1()
 956                                .bg(bg_user_message_header)
 957                                .border_b_1()
 958                                .border_color(colors.border)
 959                                .justify_between()
 960                                .rounded_t_md()
 961                                .child(
 962                                    h_flex()
 963                                        .gap_1p5()
 964                                        .child(
 965                                            Icon::new(IconName::PersonCircle)
 966                                                .size(IconSize::XSmall)
 967                                                .color(Color::Muted),
 968                                        )
 969                                        .child(
 970                                            Label::new("You")
 971                                                .size(LabelSize::Small)
 972                                                .color(Color::Muted),
 973                                        ),
 974                                )
 975                                .child(
 976                                    h_flex()
 977                                        // DL: To double-check whether we want to fully remove
 978                                        // the editing feature from meassages. Checkpoint sort of
 979                                        // solve the same problem.
 980                                        .invisible()
 981                                        .gap_1()
 982                                        .when_some(
 983                                            edit_message_editor.clone(),
 984                                            |this, edit_message_editor| {
 985                                                let focus_handle =
 986                                                    edit_message_editor.focus_handle(cx);
 987                                                this.child(
 988                                                    Button::new("cancel-edit-message", "Cancel")
 989                                                        .label_size(LabelSize::Small)
 990                                                        .key_binding(
 991                                                            KeyBinding::for_action_in(
 992                                                                &menu::Cancel,
 993                                                                &focus_handle,
 994                                                                window,
 995                                                                cx,
 996                                                            )
 997                                                            .map(|kb| kb.size(rems_from_px(12.))),
 998                                                        )
 999                                                        .on_click(
1000                                                            cx.listener(Self::handle_cancel_click),
1001                                                        ),
1002                                                )
1003                                                .child(
1004                                                    Button::new(
1005                                                        "confirm-edit-message",
1006                                                        "Regenerate",
1007                                                    )
1008                                                    .label_size(LabelSize::Small)
1009                                                    .key_binding(
1010                                                        KeyBinding::for_action_in(
1011                                                            &menu::Confirm,
1012                                                            &focus_handle,
1013                                                            window,
1014                                                            cx,
1015                                                        )
1016                                                        .map(|kb| kb.size(rems_from_px(12.))),
1017                                                    )
1018                                                    .on_click(
1019                                                        cx.listener(Self::handle_regenerate_click),
1020                                                    ),
1021                                                )
1022                                            },
1023                                        )
1024                                        .when(
1025                                            edit_message_editor.is_none() && allow_editing_message,
1026                                            |this| {
1027                                                this.child(
1028                                                    Button::new("edit-message", "Edit")
1029                                                        .label_size(LabelSize::Small)
1030                                                        .on_click(cx.listener({
1031                                                            let message_segments =
1032                                                                message.segments.clone();
1033                                                            move |this, _, window, cx| {
1034                                                                this.start_editing_message(
1035                                                                    message_id,
1036                                                                    &message_segments,
1037                                                                    window,
1038                                                                    cx,
1039                                                                );
1040                                                            }
1041                                                        })),
1042                                                )
1043                                            },
1044                                        ),
1045                                ),
1046                        )
1047                        .child(div().p_2().child(message_content)),
1048                ),
1049            Role::Assistant => v_flex()
1050                .id(("message-container", ix))
1051                .ml_2()
1052                .pl_2()
1053                .pr_4()
1054                .border_l_1()
1055                .border_color(cx.theme().colors().border_variant)
1056                .child(message_content)
1057                .when(!tool_uses.is_empty(), |parent| {
1058                    parent.child(
1059                        v_flex().children(
1060                            tool_uses
1061                                .into_iter()
1062                                .map(|tool_use| self.render_tool_use(tool_use, cx)),
1063                        ),
1064                    )
1065                }),
1066            Role::System => div().id(("message-container", ix)).py_1().px_2().child(
1067                v_flex()
1068                    .bg(colors.editor_background)
1069                    .rounded_sm()
1070                    .child(div().p_4().child(message_content)),
1071            ),
1072        };
1073
1074        v_flex()
1075            .w_full()
1076            .when(first_message, |parent| {
1077                parent.child(self.render_rules_item(cx))
1078            })
1079            .when_some(checkpoint, |parent, checkpoint| {
1080                let mut is_pending = false;
1081                let mut error = None;
1082                if let Some(last_restore_checkpoint) =
1083                    self.thread.read(cx).last_restore_checkpoint()
1084                {
1085                    if last_restore_checkpoint.message_id() == message_id {
1086                        match last_restore_checkpoint {
1087                            LastRestoreCheckpoint::Pending { .. } => is_pending = true,
1088                            LastRestoreCheckpoint::Error { error: err, .. } => {
1089                                error = Some(err.clone());
1090                            }
1091                        }
1092                    }
1093                }
1094
1095                let restore_checkpoint_button =
1096                    Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
1097                        .icon(if error.is_some() {
1098                            IconName::XCircle
1099                        } else {
1100                            IconName::Undo
1101                        })
1102                        .icon_size(IconSize::XSmall)
1103                        .icon_position(IconPosition::Start)
1104                        .icon_color(if error.is_some() {
1105                            Some(Color::Error)
1106                        } else {
1107                            None
1108                        })
1109                        .label_size(LabelSize::XSmall)
1110                        .disabled(is_pending)
1111                        .on_click(cx.listener(move |this, _, _window, cx| {
1112                            this.thread.update(cx, |thread, cx| {
1113                                thread
1114                                    .restore_checkpoint(checkpoint.clone(), cx)
1115                                    .detach_and_log_err(cx);
1116                            });
1117                        }));
1118
1119                let restore_checkpoint_button = if is_pending {
1120                    restore_checkpoint_button
1121                        .with_animation(
1122                            ("pulsating-restore-checkpoint-button", ix),
1123                            Animation::new(Duration::from_secs(2))
1124                                .repeat()
1125                                .with_easing(pulsating_between(0.6, 1.)),
1126                            |label, delta| label.alpha(delta),
1127                        )
1128                        .into_any_element()
1129                } else if let Some(error) = error {
1130                    restore_checkpoint_button
1131                        .tooltip(Tooltip::text(error.to_string()))
1132                        .into_any_element()
1133                } else {
1134                    restore_checkpoint_button.into_any_element()
1135                };
1136
1137                parent.child(
1138                    h_flex()
1139                        .pt_2p5()
1140                        .px_2p5()
1141                        .w_full()
1142                        .gap_1()
1143                        .child(ui::Divider::horizontal())
1144                        .child(restore_checkpoint_button)
1145                        .child(ui::Divider::horizontal()),
1146                )
1147            })
1148            .child(styled_message)
1149            .when(
1150                is_last_message && !self.thread.read(cx).is_generating(),
1151                |parent| parent.child(feedback_items),
1152            )
1153            .into_any()
1154    }
1155
1156    fn render_message_content(
1157        &self,
1158        message_id: MessageId,
1159        rendered_message: &RenderedMessage,
1160        cx: &Context<Self>,
1161    ) -> impl IntoElement {
1162        let pending_thinking_segment_index = rendered_message
1163            .segments
1164            .iter()
1165            .enumerate()
1166            .last()
1167            .filter(|(_, segment)| matches!(segment, RenderedMessageSegment::Thinking { .. }))
1168            .map(|(index, _)| index);
1169
1170        div()
1171            .text_ui(cx)
1172            .gap_2()
1173            .children(
1174                rendered_message.segments.iter().enumerate().map(
1175                    |(index, segment)| match segment {
1176                        RenderedMessageSegment::Thinking {
1177                            content,
1178                            scroll_handle,
1179                        } => self
1180                            .render_message_thinking_segment(
1181                                message_id,
1182                                index,
1183                                content.clone(),
1184                                &scroll_handle,
1185                                Some(index) == pending_thinking_segment_index,
1186                                cx,
1187                            )
1188                            .into_any_element(),
1189                        RenderedMessageSegment::Text(markdown) => {
1190                            div().child(markdown.clone()).into_any_element()
1191                        }
1192                    },
1193                ),
1194            )
1195    }
1196
1197    fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
1198        cx.theme().colors().border.opacity(0.5)
1199    }
1200
1201    fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
1202        cx.theme()
1203            .colors()
1204            .element_background
1205            .blend(cx.theme().colors().editor_foreground.opacity(0.025))
1206    }
1207
1208    fn render_message_thinking_segment(
1209        &self,
1210        message_id: MessageId,
1211        ix: usize,
1212        markdown: Entity<Markdown>,
1213        scroll_handle: &ScrollHandle,
1214        pending: bool,
1215        cx: &Context<Self>,
1216    ) -> impl IntoElement {
1217        let is_open = self
1218            .expanded_thinking_segments
1219            .get(&(message_id, ix))
1220            .copied()
1221            .unwrap_or_default();
1222
1223        let editor_bg = cx.theme().colors().editor_background;
1224
1225        div().py_2().child(
1226            v_flex()
1227                .rounded_lg()
1228                .border_1()
1229                .border_color(self.tool_card_border_color(cx))
1230                .child(
1231                    h_flex()
1232                        .group("disclosure-header")
1233                        .justify_between()
1234                        .py_1()
1235                        .px_2()
1236                        .bg(self.tool_card_header_bg(cx))
1237                        .map(|this| {
1238                            if pending || is_open {
1239                                this.rounded_t_md()
1240                                    .border_b_1()
1241                                    .border_color(self.tool_card_border_color(cx))
1242                            } else {
1243                                this.rounded_md()
1244                            }
1245                        })
1246                        .child(
1247                            h_flex()
1248                                .gap_1p5()
1249                                .child(
1250                                    Icon::new(IconName::Brain)
1251                                        .size(IconSize::XSmall)
1252                                        .color(Color::Muted),
1253                                )
1254                                .child({
1255                                    if pending {
1256                                        Label::new("Thinking…")
1257                                            .size(LabelSize::Small)
1258                                            .buffer_font(cx)
1259                                            .with_animation(
1260                                                "pulsating-label",
1261                                                Animation::new(Duration::from_secs(2))
1262                                                    .repeat()
1263                                                    .with_easing(pulsating_between(0.4, 0.8)),
1264                                                |label, delta| label.alpha(delta),
1265                                            )
1266                                            .into_any_element()
1267                                    } else {
1268                                        Label::new("Thought Process")
1269                                            .size(LabelSize::Small)
1270                                            .buffer_font(cx)
1271                                            .into_any_element()
1272                                    }
1273                                }),
1274                        )
1275                        .child(
1276                            h_flex()
1277                                .gap_1()
1278                                .child(
1279                                    div().visible_on_hover("disclosure-header").child(
1280                                        Disclosure::new("thinking-disclosure", is_open)
1281                                            .opened_icon(IconName::ChevronUp)
1282                                            .closed_icon(IconName::ChevronDown)
1283                                            .on_click(cx.listener({
1284                                                move |this, _event, _window, _cx| {
1285                                                    let is_open = this
1286                                                        .expanded_thinking_segments
1287                                                        .entry((message_id, ix))
1288                                                        .or_insert(false);
1289
1290                                                    *is_open = !*is_open;
1291                                                }
1292                                            })),
1293                                    ),
1294                                )
1295                                .child({
1296                                    let (icon_name, color, animated) = if pending {
1297                                        (IconName::ArrowCircle, Color::Accent, true)
1298                                    } else {
1299                                        (IconName::Check, Color::Success, false)
1300                                    };
1301
1302                                    let icon =
1303                                        Icon::new(icon_name).color(color).size(IconSize::Small);
1304
1305                                    if animated {
1306                                        icon.with_animation(
1307                                            "arrow-circle",
1308                                            Animation::new(Duration::from_secs(2)).repeat(),
1309                                            |icon, delta| {
1310                                                icon.transform(Transformation::rotate(percentage(
1311                                                    delta,
1312                                                )))
1313                                            },
1314                                        )
1315                                        .into_any_element()
1316                                    } else {
1317                                        icon.into_any_element()
1318                                    }
1319                                }),
1320                        ),
1321                )
1322                .when(pending && !is_open, |this| {
1323                    let gradient_overlay = div()
1324                        .rounded_b_lg()
1325                        .h_20()
1326                        .absolute()
1327                        .w_full()
1328                        .bottom_0()
1329                        .left_0()
1330                        .bg(linear_gradient(
1331                            180.,
1332                            linear_color_stop(editor_bg, 1.),
1333                            linear_color_stop(editor_bg.opacity(0.2), 0.),
1334                        ));
1335
1336                    this.child(
1337                        div()
1338                            .relative()
1339                            .bg(editor_bg)
1340                            .rounded_b_lg()
1341                            .child(
1342                                div()
1343                                    .id(("thinking-content", ix))
1344                                    .p_2()
1345                                    .h_20()
1346                                    .track_scroll(scroll_handle)
1347                                    .text_ui_sm(cx)
1348                                    .child(markdown.clone())
1349                                    .overflow_hidden(),
1350                            )
1351                            .child(gradient_overlay),
1352                    )
1353                })
1354                .when(is_open, |this| {
1355                    this.child(
1356                        div()
1357                            .id(("thinking-content", ix))
1358                            .h_full()
1359                            .p_2()
1360                            .rounded_b_lg()
1361                            .bg(editor_bg)
1362                            .text_ui_sm(cx)
1363                            .child(markdown.clone()),
1364                    )
1365                }),
1366        )
1367    }
1368
1369    fn render_tool_use(&self, tool_use: ToolUse, cx: &mut Context<Self>) -> impl IntoElement {
1370        let is_open = self
1371            .expanded_tool_uses
1372            .get(&tool_use.id)
1373            .copied()
1374            .unwrap_or_default();
1375
1376        div().py_2().child(
1377            v_flex()
1378                .rounded_lg()
1379                .border_1()
1380                .border_color(self.tool_card_border_color(cx))
1381                .overflow_hidden()
1382                .child(
1383                    h_flex()
1384                        .group("disclosure-header")
1385                        .relative()
1386                        .gap_1p5()
1387                        .justify_between()
1388                        .py_1()
1389                        .px_2()
1390                        .bg(self.tool_card_header_bg(cx))
1391                        .map(|element| {
1392                            if is_open {
1393                                element.border_b_1().rounded_t_md()
1394                            } else {
1395                                element.rounded_md()
1396                            }
1397                        })
1398                        .border_color(self.tool_card_border_color(cx))
1399                        .child(
1400                            h_flex()
1401                                .id("tool-label-container")
1402                                .relative()
1403                                .gap_1p5()
1404                                .max_w_full()
1405                                .overflow_x_scroll()
1406                                .child(
1407                                    Icon::new(tool_use.icon)
1408                                        .size(IconSize::XSmall)
1409                                        .color(Color::Muted),
1410                                )
1411                                .child(h_flex().pr_8().text_ui_sm(cx).children(
1412                                    self.rendered_tool_use_labels.get(&tool_use.id).cloned(),
1413                                )),
1414                        )
1415                        .child(
1416                            h_flex()
1417                                .gap_1()
1418                                .child(
1419                                    div().visible_on_hover("disclosure-header").child(
1420                                        Disclosure::new("tool-use-disclosure", is_open)
1421                                            .opened_icon(IconName::ChevronUp)
1422                                            .closed_icon(IconName::ChevronDown)
1423                                            .on_click(cx.listener({
1424                                                let tool_use_id = tool_use.id.clone();
1425                                                move |this, _event, _window, _cx| {
1426                                                    let is_open = this
1427                                                        .expanded_tool_uses
1428                                                        .entry(tool_use_id.clone())
1429                                                        .or_insert(false);
1430
1431                                                    *is_open = !*is_open;
1432                                                }
1433                                            })),
1434                                    ),
1435                                )
1436                                .child({
1437                                    let (icon_name, color, animated) = match &tool_use.status {
1438                                        ToolUseStatus::Pending
1439                                        | ToolUseStatus::NeedsConfirmation => {
1440                                            (IconName::Warning, Color::Warning, false)
1441                                        }
1442                                        ToolUseStatus::Running => {
1443                                            (IconName::ArrowCircle, Color::Accent, true)
1444                                        }
1445                                        ToolUseStatus::Finished(_) => {
1446                                            (IconName::Check, Color::Success, false)
1447                                        }
1448                                        ToolUseStatus::Error(_) => {
1449                                            (IconName::Close, Color::Error, false)
1450                                        }
1451                                    };
1452
1453                                    let icon =
1454                                        Icon::new(icon_name).color(color).size(IconSize::Small);
1455
1456                                    if animated {
1457                                        icon.with_animation(
1458                                            "arrow-circle",
1459                                            Animation::new(Duration::from_secs(2)).repeat(),
1460                                            |icon, delta| {
1461                                                icon.transform(Transformation::rotate(percentage(
1462                                                    delta,
1463                                                )))
1464                                            },
1465                                        )
1466                                        .into_any_element()
1467                                    } else {
1468                                        icon.into_any_element()
1469                                    }
1470                                }),
1471                        )
1472                        .child(div().h_full().absolute().w_8().bottom_0().right_12().bg(
1473                            linear_gradient(
1474                                90.,
1475                                linear_color_stop(self.tool_card_header_bg(cx), 1.),
1476                                linear_color_stop(self.tool_card_header_bg(cx).opacity(0.2), 0.),
1477                            ),
1478                        )),
1479                )
1480                .map(|parent| {
1481                    if !is_open {
1482                        return parent;
1483                    }
1484
1485                    let content_container = || v_flex().py_1().gap_0p5().px_2p5();
1486
1487                    parent.child(
1488                        v_flex()
1489                            .gap_1()
1490                            .bg(cx.theme().colors().editor_background)
1491                            .rounded_b_lg()
1492                            .child(
1493                                content_container()
1494                                    .border_b_1()
1495                                    .border_color(self.tool_card_border_color(cx))
1496                                    .child(
1497                                        Label::new("Input")
1498                                            .size(LabelSize::XSmall)
1499                                            .color(Color::Muted)
1500                                            .buffer_font(cx),
1501                                    )
1502                                    .child(
1503                                        Label::new(
1504                                            serde_json::to_string_pretty(&tool_use.input)
1505                                                .unwrap_or_default(),
1506                                        )
1507                                        .size(LabelSize::Small)
1508                                        .buffer_font(cx),
1509                                    ),
1510                            )
1511                            .map(|container| match tool_use.status {
1512                                ToolUseStatus::Finished(output) => container.child(
1513                                    content_container()
1514                                        .child(
1515                                            Label::new("Result")
1516                                                .size(LabelSize::XSmall)
1517                                                .color(Color::Muted)
1518                                                .buffer_font(cx),
1519                                        )
1520                                        .child(
1521                                            Label::new(output)
1522                                                .size(LabelSize::Small)
1523                                                .buffer_font(cx),
1524                                        ),
1525                                ),
1526                                ToolUseStatus::Running => container.child(
1527                                    content_container().child(
1528                                        h_flex()
1529                                            .gap_1()
1530                                            .pb_1()
1531                                            .child(
1532                                                Icon::new(IconName::ArrowCircle)
1533                                                    .size(IconSize::Small)
1534                                                    .color(Color::Accent)
1535                                                    .with_animation(
1536                                                        "arrow-circle",
1537                                                        Animation::new(Duration::from_secs(2))
1538                                                            .repeat(),
1539                                                        |icon, delta| {
1540                                                            icon.transform(Transformation::rotate(
1541                                                                percentage(delta),
1542                                                            ))
1543                                                        },
1544                                                    ),
1545                                            )
1546                                            .child(
1547                                                Label::new("Running…")
1548                                                    .size(LabelSize::XSmall)
1549                                                    .color(Color::Muted)
1550                                                    .buffer_font(cx),
1551                                            ),
1552                                    ),
1553                                ),
1554                                ToolUseStatus::Error(err) => container.child(
1555                                    content_container()
1556                                        .child(
1557                                            Label::new("Error")
1558                                                .size(LabelSize::XSmall)
1559                                                .color(Color::Muted)
1560                                                .buffer_font(cx),
1561                                        )
1562                                        .child(
1563                                            Label::new(err).size(LabelSize::Small).buffer_font(cx),
1564                                        ),
1565                                ),
1566                                ToolUseStatus::Pending => container,
1567                                ToolUseStatus::NeedsConfirmation => container.child(
1568                                    content_container().child(
1569                                        Label::new("Asking Permission")
1570                                            .size(LabelSize::Small)
1571                                            .color(Color::Muted)
1572                                            .buffer_font(cx),
1573                                    ),
1574                                ),
1575                            }),
1576                    )
1577                }),
1578        )
1579    }
1580
1581    fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
1582        let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
1583        else {
1584            return div().into_any();
1585        };
1586
1587        let rules_files = system_prompt_context
1588            .worktrees
1589            .iter()
1590            .filter_map(|worktree| worktree.rules_file.as_ref())
1591            .collect::<Vec<_>>();
1592
1593        let label_text = match rules_files.as_slice() {
1594            &[] => return div().into_any(),
1595            &[rules_file] => {
1596                format!("Using {:?} file", rules_file.rel_path)
1597            }
1598            rules_files => {
1599                format!("Using {} rules files", rules_files.len())
1600            }
1601        };
1602
1603        div()
1604            .pt_1()
1605            .px_2p5()
1606            .child(
1607                h_flex()
1608                    .w_full()
1609                    .gap_0p5()
1610                    .child(
1611                        h_flex()
1612                            .gap_1p5()
1613                            .child(
1614                                Icon::new(IconName::File)
1615                                    .size(IconSize::XSmall)
1616                                    .color(Color::Disabled),
1617                            )
1618                            .child(
1619                                Label::new(label_text)
1620                                    .size(LabelSize::XSmall)
1621                                    .color(Color::Muted)
1622                                    .buffer_font(cx),
1623                            ),
1624                    )
1625                    .child(
1626                        IconButton::new("open-rule", IconName::ArrowUpRightAlt)
1627                            .shape(ui::IconButtonShape::Square)
1628                            .icon_size(IconSize::XSmall)
1629                            .icon_color(Color::Ignored)
1630                            .on_click(cx.listener(Self::handle_open_rules))
1631                            .tooltip(Tooltip::text("View Rules")),
1632                    ),
1633            )
1634            .into_any()
1635    }
1636
1637    fn handle_allow_tool(
1638        &mut self,
1639        tool_use_id: LanguageModelToolUseId,
1640        _: &ClickEvent,
1641        _window: &mut Window,
1642        cx: &mut Context<Self>,
1643    ) {
1644        if let Some(PendingToolUseStatus::NeedsConfirmation(c)) = self
1645            .thread
1646            .read(cx)
1647            .pending_tool(&tool_use_id)
1648            .map(|tool_use| tool_use.status.clone())
1649        {
1650            self.thread.update(cx, |thread, cx| {
1651                thread.run_tool(
1652                    c.tool_use_id.clone(),
1653                    c.ui_text.clone(),
1654                    c.input.clone(),
1655                    &c.messages,
1656                    c.tool.clone(),
1657                    cx,
1658                );
1659            });
1660        }
1661    }
1662
1663    fn handle_deny_tool(
1664        &mut self,
1665        tool_use_id: LanguageModelToolUseId,
1666        _: &ClickEvent,
1667        _window: &mut Window,
1668        cx: &mut Context<Self>,
1669    ) {
1670        self.thread.update(cx, |thread, cx| {
1671            thread.deny_tool_use(tool_use_id, cx);
1672        });
1673    }
1674
1675    fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
1676        let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
1677        else {
1678            return;
1679        };
1680
1681        let abs_paths = system_prompt_context
1682            .worktrees
1683            .iter()
1684            .flat_map(|worktree| worktree.rules_file.as_ref())
1685            .map(|rules_file| rules_file.abs_path.to_path_buf())
1686            .collect::<Vec<_>>();
1687
1688        if let Ok(task) = self.workspace.update(cx, move |workspace, cx| {
1689            // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
1690            // files clear. For example, if rules file 1 is already open but rules file 2 is not,
1691            // this would open and focus rules file 2 in a tab that is not next to rules file 1.
1692            workspace.open_paths(abs_paths, OpenOptions::default(), None, window, cx)
1693        }) {
1694            task.detach();
1695        }
1696    }
1697
1698    fn render_confirmations<'a>(
1699        &'a mut self,
1700        cx: &'a mut Context<Self>,
1701    ) -> impl Iterator<Item = AnyElement> + 'a {
1702        let thread = self.thread.read(cx);
1703
1704        thread
1705            .tools_needing_confirmation()
1706            .map(|tool| {
1707                div()
1708                    .m_3()
1709                    .p_2()
1710                    .bg(cx.theme().colors().editor_background)
1711                    .border_1()
1712                    .border_color(cx.theme().colors().border)
1713                    .rounded_lg()
1714                    .child(
1715                        v_flex()
1716                            .gap_1()
1717                            .child(
1718                                v_flex()
1719                                    .gap_0p5()
1720                                    .child(
1721                                        Label::new("The agent wants to run this action:")
1722                                            .color(Color::Muted),
1723                                    )
1724                                    .child(div().p_3().child(Label::new(&tool.ui_text))),
1725                            )
1726                            .child(
1727                                h_flex()
1728                                    .gap_1()
1729                                    .child({
1730                                        let tool_id = tool.id.clone();
1731                                        Button::new("allow-tool-action", "Allow").on_click(
1732                                            cx.listener(move |this, event, window, cx| {
1733                                                this.handle_allow_tool(
1734                                                    tool_id.clone(),
1735                                                    event,
1736                                                    window,
1737                                                    cx,
1738                                                )
1739                                            }),
1740                                        )
1741                                    })
1742                                    .child({
1743                                        let tool_id = tool.id.clone();
1744                                        Button::new("deny-tool", "Deny").on_click(cx.listener(
1745                                            move |this, event, window, cx| {
1746                                                this.handle_deny_tool(
1747                                                    tool_id.clone(),
1748                                                    event,
1749                                                    window,
1750                                                    cx,
1751                                                )
1752                                            },
1753                                        ))
1754                                    }),
1755                            )
1756                            .child(
1757                                Label::new("Note: A future release will introduce a way to remember your answers to these. In the meantime, you can avoid these prompts by adding \"assistant\": { \"always_allow_tool_actions\": true } to your settings.json.")
1758                                    .color(Color::Muted)
1759                                    .size(LabelSize::Small),
1760                            ),
1761                    )
1762                    .into_any()
1763            })
1764    }
1765
1766    fn dismiss_notifications(&mut self, cx: &mut Context<'_, ActiveThread>) {
1767        for window in self.pop_ups.drain(..) {
1768            window
1769                .update(cx, |_, window, _| {
1770                    window.remove_window();
1771                })
1772                .ok();
1773
1774            self.pop_up_subscriptions.remove(&window);
1775        }
1776    }
1777
1778    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
1779        div()
1780            .occlude()
1781            .id("active-thread-scrollbar")
1782            .on_mouse_move(cx.listener(|_, _, _, cx| {
1783                cx.notify();
1784                cx.stop_propagation()
1785            }))
1786            .on_hover(|_, _, cx| {
1787                cx.stop_propagation();
1788            })
1789            .on_any_mouse_down(|_, _, cx| {
1790                cx.stop_propagation();
1791            })
1792            .on_mouse_up(
1793                MouseButton::Left,
1794                cx.listener(|_, _, _, cx| {
1795                    cx.stop_propagation();
1796                }),
1797            )
1798            .on_scroll_wheel(cx.listener(|_, _, _, cx| {
1799                cx.notify();
1800            }))
1801            .h_full()
1802            .absolute()
1803            .right_1()
1804            .top_1()
1805            .bottom_0()
1806            .w(px(12.))
1807            .cursor_default()
1808            .children(Scrollbar::vertical(self.scrollbar_state.clone()))
1809    }
1810}
1811
1812impl Render for ActiveThread {
1813    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1814        v_flex()
1815            .size_full()
1816            .relative()
1817            .child(list(self.list_state.clone()).flex_grow())
1818            .children(self.render_confirmations(cx))
1819            .child(self.render_vertical_scrollbar(cx))
1820    }
1821}