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