active_thread.rs

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