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