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