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