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