rules_library.rs

   1use anyhow::Result;
   2use collections::{HashMap, HashSet};
   3use editor::{CompletionProvider, SelectionEffects};
   4use editor::{CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle, actions::Tab};
   5use gpui::{
   6    App, Bounds, DEFAULT_ADDITIONAL_WINDOW_SIZE, Entity, EventEmitter, Focusable, MouseButton,
   7    PromptLevel, Subscription, Task, TextStyle, Tiling, TitlebarOptions, WindowBounds,
   8    WindowHandle, WindowOptions, actions, point, size, transparent_black,
   9};
  10use language::{Buffer, LanguageRegistry, language_settings::SoftWrap};
  11use language_model::{
  12    ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
  13};
  14use picker::{Picker, PickerDelegate};
  15use platform_title_bar::PlatformTitleBar;
  16use release_channel::ReleaseChannel;
  17use rope::Rope;
  18use settings::Settings;
  19use std::rc::Rc;
  20use std::sync::Arc;
  21use std::sync::atomic::AtomicBool;
  22use std::time::Duration;
  23use theme::ThemeSettings;
  24use ui::{Divider, ListItem, ListItemSpacing, ListSubHeader, Tooltip, prelude::*};
  25use ui_input::ErasedEditor;
  26use util::{ResultExt, TryFutureExt};
  27use workspace::{MultiWorkspace, Workspace, WorkspaceSettings, client_side_decorations};
  28use zed_actions::assistant::InlineAssist;
  29
  30use prompt_store::*;
  31
  32pub fn init(cx: &mut App) {
  33    prompt_store::init(cx);
  34}
  35
  36actions!(
  37    rules_library,
  38    [
  39        /// Creates a new rule in the rules library.
  40        NewRule,
  41        /// Deletes the selected rule.
  42        DeleteRule,
  43        /// Duplicates the selected rule.
  44        DuplicateRule,
  45        /// Toggles whether the selected rule is a default rule.
  46        ToggleDefaultRule,
  47        /// Restores a built-in rule to its default content.
  48        RestoreDefaultContent
  49    ]
  50);
  51
  52pub trait InlineAssistDelegate {
  53    fn assist(
  54        &self,
  55        prompt_editor: &Entity<Editor>,
  56        initial_prompt: Option<String>,
  57        window: &mut Window,
  58        cx: &mut Context<RulesLibrary>,
  59    );
  60
  61    /// Returns whether the Agent panel was focused.
  62    fn focus_agent_panel(
  63        &self,
  64        workspace: &mut Workspace,
  65        window: &mut Window,
  66        cx: &mut Context<Workspace>,
  67    ) -> bool;
  68}
  69
  70/// This function opens a new rules library window if one doesn't exist already.
  71/// If one exists, it brings it to the foreground.
  72///
  73/// Note that, when opening a new window, this waits for the PromptStore to be
  74/// initialized. If it was initialized successfully, it returns a window handle
  75/// to a rules library.
  76pub fn open_rules_library(
  77    language_registry: Arc<LanguageRegistry>,
  78    inline_assist_delegate: Box<dyn InlineAssistDelegate>,
  79    make_completion_provider: Rc<dyn Fn() -> Rc<dyn CompletionProvider>>,
  80    prompt_to_select: Option<PromptId>,
  81    cx: &mut App,
  82) -> Task<Result<WindowHandle<RulesLibrary>>> {
  83    let store = PromptStore::global(cx);
  84    cx.spawn(async move |cx| {
  85        // We query windows in spawn so that all windows have been returned to GPUI
  86        let existing_window = cx.update(|cx| {
  87            let existing_window = cx
  88                .windows()
  89                .into_iter()
  90                .find_map(|window| window.downcast::<RulesLibrary>());
  91            if let Some(existing_window) = existing_window {
  92                existing_window
  93                    .update(cx, |rules_library, window, cx| {
  94                        if let Some(prompt_to_select) = prompt_to_select {
  95                            rules_library.load_rule(prompt_to_select, true, window, cx);
  96                        }
  97                        window.activate_window()
  98                    })
  99                    .ok();
 100
 101                Some(existing_window)
 102            } else {
 103                None
 104            }
 105        });
 106
 107        if let Some(existing_window) = existing_window {
 108            return Ok(existing_window);
 109        }
 110
 111        let store = store.await?;
 112        cx.update(|cx| {
 113            let app_id = ReleaseChannel::global(cx).app_id();
 114            let bounds = Bounds::centered(None, size(px(1024.0), px(768.0)), cx);
 115            let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") {
 116                Ok(val) if val == "server" => gpui::WindowDecorations::Server,
 117                Ok(val) if val == "client" => gpui::WindowDecorations::Client,
 118                _ => match WorkspaceSettings::get_global(cx).window_decorations {
 119                    settings::WindowDecorations::Server => gpui::WindowDecorations::Server,
 120                    settings::WindowDecorations::Client => gpui::WindowDecorations::Client,
 121                },
 122            };
 123            cx.open_window(
 124                WindowOptions {
 125                    titlebar: Some(TitlebarOptions {
 126                        title: Some("Rules Library".into()),
 127                        appears_transparent: true,
 128                        traffic_light_position: Some(point(px(12.0), px(12.0))),
 129                    }),
 130                    app_id: Some(app_id.to_owned()),
 131                    window_bounds: Some(WindowBounds::Windowed(bounds)),
 132                    window_background: cx.theme().window_background_appearance(),
 133                    window_decorations: Some(window_decorations),
 134                    window_min_size: Some(DEFAULT_ADDITIONAL_WINDOW_SIZE),
 135                    kind: gpui::WindowKind::Floating,
 136                    is_movable: !cfg!(target_os = "macos"),
 137                    ..Default::default()
 138                },
 139                |window, cx| {
 140                    cx.new(|cx| {
 141                        RulesLibrary::new(
 142                            store,
 143                            language_registry,
 144                            inline_assist_delegate,
 145                            make_completion_provider,
 146                            prompt_to_select,
 147                            window,
 148                            cx,
 149                        )
 150                    })
 151                },
 152            )
 153        })
 154    })
 155}
 156
 157pub struct RulesLibrary {
 158    title_bar: Option<Entity<PlatformTitleBar>>,
 159    store: Entity<PromptStore>,
 160    language_registry: Arc<LanguageRegistry>,
 161    rule_editors: HashMap<PromptId, RuleEditor>,
 162    active_rule_id: Option<PromptId>,
 163    picker: Entity<Picker<RulePickerDelegate>>,
 164    pending_load: Task<()>,
 165    inline_assist_delegate: Box<dyn InlineAssistDelegate>,
 166    make_completion_provider: Rc<dyn Fn() -> Rc<dyn CompletionProvider>>,
 167    _subscriptions: Vec<Subscription>,
 168}
 169
 170struct RuleEditor {
 171    title_editor: Entity<Editor>,
 172    body_editor: Entity<Editor>,
 173    token_count: Option<u64>,
 174    pending_token_count: Task<Option<()>>,
 175    next_title_and_body_to_save: Option<(String, Rope)>,
 176    pending_save: Option<Task<Option<()>>>,
 177    _subscriptions: Vec<Subscription>,
 178}
 179
 180enum RulePickerEntry {
 181    Header(SharedString),
 182    Rule(PromptMetadata),
 183    Separator,
 184}
 185
 186struct RulePickerDelegate {
 187    store: Entity<PromptStore>,
 188    selected_index: usize,
 189    filtered_entries: Vec<RulePickerEntry>,
 190}
 191
 192enum RulePickerEvent {
 193    Selected { prompt_id: PromptId },
 194    Confirmed { prompt_id: PromptId },
 195    Deleted { prompt_id: PromptId },
 196    ToggledDefault { prompt_id: PromptId },
 197}
 198
 199impl EventEmitter<RulePickerEvent> for Picker<RulePickerDelegate> {}
 200
 201impl PickerDelegate for RulePickerDelegate {
 202    type ListItem = AnyElement;
 203
 204    fn match_count(&self) -> usize {
 205        self.filtered_entries.len()
 206    }
 207
 208    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
 209        Some("No rules found matching your search.".into())
 210    }
 211
 212    fn selected_index(&self) -> usize {
 213        self.selected_index
 214    }
 215
 216    fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
 217        self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1));
 218
 219        if let Some(RulePickerEntry::Rule(rule)) = self.filtered_entries.get(self.selected_index) {
 220            cx.emit(RulePickerEvent::Selected { prompt_id: rule.id });
 221        }
 222
 223        cx.notify();
 224    }
 225
 226    fn can_select(&self, ix: usize, _: &mut Window, _: &mut Context<Picker<Self>>) -> bool {
 227        match self.filtered_entries.get(ix) {
 228            Some(RulePickerEntry::Rule(_)) => true,
 229            Some(RulePickerEntry::Header(_)) | Some(RulePickerEntry::Separator) | None => false,
 230        }
 231    }
 232
 233    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
 234        "Search…".into()
 235    }
 236
 237    fn update_matches(
 238        &mut self,
 239        query: String,
 240        window: &mut Window,
 241        cx: &mut Context<Picker<Self>>,
 242    ) -> Task<()> {
 243        let cancellation_flag = Arc::new(AtomicBool::default());
 244        let search = self.store.read(cx).search(query, cancellation_flag, cx);
 245
 246        let prev_prompt_id = self
 247            .filtered_entries
 248            .get(self.selected_index)
 249            .and_then(|entry| {
 250                if let RulePickerEntry::Rule(rule) = entry {
 251                    Some(rule.id)
 252                } else {
 253                    None
 254                }
 255            });
 256
 257        cx.spawn_in(window, async move |this, cx| {
 258            let (filtered_entries, selected_index) = cx
 259                .background_spawn(async move {
 260                    let matches = search.await;
 261
 262                    let (built_in_rules, user_rules): (Vec<_>, Vec<_>) =
 263                        matches.into_iter().partition(|rule| rule.id.is_built_in());
 264                    let (default_rules, other_rules): (Vec<_>, Vec<_>) =
 265                        user_rules.into_iter().partition(|rule| rule.default);
 266
 267                    let mut filtered_entries = Vec::new();
 268
 269                    if !built_in_rules.is_empty() {
 270                        filtered_entries.push(RulePickerEntry::Header("Built-in Rules".into()));
 271
 272                        for rule in built_in_rules {
 273                            filtered_entries.push(RulePickerEntry::Rule(rule));
 274                        }
 275
 276                        filtered_entries.push(RulePickerEntry::Separator);
 277                    }
 278
 279                    if !default_rules.is_empty() {
 280                        filtered_entries.push(RulePickerEntry::Header("Default Rules".into()));
 281
 282                        for rule in default_rules {
 283                            filtered_entries.push(RulePickerEntry::Rule(rule));
 284                        }
 285
 286                        filtered_entries.push(RulePickerEntry::Separator);
 287                    }
 288
 289                    for rule in other_rules {
 290                        filtered_entries.push(RulePickerEntry::Rule(rule));
 291                    }
 292
 293                    let selected_index = prev_prompt_id
 294                        .and_then(|prev_prompt_id| {
 295                            filtered_entries.iter().position(|entry| {
 296                                if let RulePickerEntry::Rule(rule) = entry {
 297                                    rule.id == prev_prompt_id
 298                                } else {
 299                                    false
 300                                }
 301                            })
 302                        })
 303                        .unwrap_or_else(|| {
 304                            filtered_entries
 305                                .iter()
 306                                .position(|entry| matches!(entry, RulePickerEntry::Rule(_)))
 307                                .unwrap_or(0)
 308                        });
 309
 310                    (filtered_entries, selected_index)
 311                })
 312                .await;
 313
 314            this.update_in(cx, |this, window, cx| {
 315                this.delegate.filtered_entries = filtered_entries;
 316                this.set_selected_index(
 317                    selected_index,
 318                    Some(picker::Direction::Down),
 319                    true,
 320                    window,
 321                    cx,
 322                );
 323                cx.notify();
 324            })
 325            .ok();
 326        })
 327    }
 328
 329    fn confirm(&mut self, _secondary: bool, _: &mut Window, cx: &mut Context<Picker<Self>>) {
 330        if let Some(RulePickerEntry::Rule(rule)) = self.filtered_entries.get(self.selected_index) {
 331            cx.emit(RulePickerEvent::Confirmed { prompt_id: rule.id });
 332        }
 333    }
 334
 335    fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {}
 336
 337    fn render_match(
 338        &self,
 339        ix: usize,
 340        selected: bool,
 341        _: &mut Window,
 342        cx: &mut Context<Picker<Self>>,
 343    ) -> Option<Self::ListItem> {
 344        match self.filtered_entries.get(ix)? {
 345            RulePickerEntry::Header(title) => {
 346                let tooltip_text = if title.as_ref() == "Built-in Rules" {
 347                    "Built-in rules are those included out of the box with Zed."
 348                } else {
 349                    "Default Rules are attached by default with every new thread."
 350                };
 351
 352                Some(
 353                    ListSubHeader::new(title.clone())
 354                        .end_slot(
 355                            IconButton::new("info", IconName::Info)
 356                                .style(ButtonStyle::Transparent)
 357                                .icon_size(IconSize::Small)
 358                                .icon_color(Color::Muted)
 359                                .tooltip(Tooltip::text(tooltip_text))
 360                                .into_any_element(),
 361                        )
 362                        .inset(true)
 363                        .into_any_element(),
 364                )
 365            }
 366            RulePickerEntry::Separator => Some(
 367                h_flex()
 368                    .py_1()
 369                    .child(Divider::horizontal())
 370                    .into_any_element(),
 371            ),
 372            RulePickerEntry::Rule(rule) => {
 373                let default = rule.default;
 374                let prompt_id = rule.id;
 375
 376                Some(
 377                    ListItem::new(ix)
 378                        .inset(true)
 379                        .spacing(ListItemSpacing::Sparse)
 380                        .toggle_state(selected)
 381                        .child(
 382                            Label::new(rule.title.clone().unwrap_or("Untitled".into()))
 383                                .truncate()
 384                                .mr_10(),
 385                        )
 386                        .end_slot::<IconButton>((default && !prompt_id.is_built_in()).then(|| {
 387                            IconButton::new("toggle-default-rule", IconName::Paperclip)
 388                                .toggle_state(true)
 389                                .icon_color(Color::Accent)
 390                                .icon_size(IconSize::Small)
 391                                .tooltip(Tooltip::text("Remove from Default Rules"))
 392                                .on_click(cx.listener(move |_, _, _, cx| {
 393                                    cx.emit(RulePickerEvent::ToggledDefault { prompt_id })
 394                                }))
 395                        }))
 396                        .when(!prompt_id.is_built_in(), |this| {
 397                            this.end_hover_slot(
 398                                h_flex()
 399                                    .child(
 400                                        IconButton::new("delete-rule", IconName::Trash)
 401                                            .icon_color(Color::Muted)
 402                                            .icon_size(IconSize::Small)
 403                                            .tooltip(Tooltip::text("Delete Rule"))
 404                                            .on_click(cx.listener(move |_, _, _, cx| {
 405                                                cx.emit(RulePickerEvent::Deleted { prompt_id })
 406                                            })),
 407                                    )
 408                                    .child(
 409                                        IconButton::new("toggle-default-rule", IconName::Plus)
 410                                            .selected_icon(IconName::Dash)
 411                                            .toggle_state(default)
 412                                            .icon_size(IconSize::Small)
 413                                            .icon_color(if default {
 414                                                Color::Accent
 415                                            } else {
 416                                                Color::Muted
 417                                            })
 418                                            .map(|this| {
 419                                                if default {
 420                                                    this.tooltip(Tooltip::text(
 421                                                        "Remove from Default Rules",
 422                                                    ))
 423                                                } else {
 424                                                    this.tooltip(move |_window, cx| {
 425                                                        Tooltip::with_meta(
 426                                                            "Add to Default Rules",
 427                                                            None,
 428                                                            "Always included in every thread.",
 429                                                            cx,
 430                                                        )
 431                                                    })
 432                                                }
 433                                            })
 434                                            .on_click(cx.listener(move |_, _, _, cx| {
 435                                                cx.emit(RulePickerEvent::ToggledDefault {
 436                                                    prompt_id,
 437                                                })
 438                                            })),
 439                                    ),
 440                            )
 441                        })
 442                        .into_any_element(),
 443                )
 444            }
 445        }
 446    }
 447
 448    fn render_editor(
 449        &self,
 450        editor: &Arc<dyn ErasedEditor>,
 451        _: &mut Window,
 452        cx: &mut Context<Picker<Self>>,
 453    ) -> Div {
 454        let editor = editor.as_any().downcast_ref::<Entity<Editor>>().unwrap();
 455
 456        h_flex()
 457            .py_1()
 458            .px_1p5()
 459            .mx_1()
 460            .gap_1p5()
 461            .rounded_sm()
 462            .bg(cx.theme().colors().editor_background)
 463            .border_1()
 464            .border_color(cx.theme().colors().border)
 465            .child(Icon::new(IconName::MagnifyingGlass).color(Color::Muted))
 466            .child(editor.clone())
 467    }
 468}
 469
 470impl RulesLibrary {
 471    fn new(
 472        store: Entity<PromptStore>,
 473        language_registry: Arc<LanguageRegistry>,
 474        inline_assist_delegate: Box<dyn InlineAssistDelegate>,
 475        make_completion_provider: Rc<dyn Fn() -> Rc<dyn CompletionProvider>>,
 476        rule_to_select: Option<PromptId>,
 477        window: &mut Window,
 478        cx: &mut Context<Self>,
 479    ) -> Self {
 480        let (_selected_index, _matches) = if let Some(rule_to_select) = rule_to_select {
 481            let matches = store.read(cx).all_prompt_metadata();
 482            let selected_index = matches
 483                .iter()
 484                .enumerate()
 485                .find(|(_, metadata)| metadata.id == rule_to_select)
 486                .map_or(0, |(ix, _)| ix);
 487            (selected_index, matches)
 488        } else {
 489            (0, vec![])
 490        };
 491
 492        let picker_delegate = RulePickerDelegate {
 493            store: store.clone(),
 494            selected_index: 0,
 495            filtered_entries: Vec::new(),
 496        };
 497
 498        let picker = cx.new(|cx| {
 499            let picker = Picker::list(picker_delegate, window, cx)
 500                .modal(false)
 501                .max_height(None);
 502            picker.focus(window, cx);
 503            picker
 504        });
 505
 506        Self {
 507            title_bar: Some(cx.new(|cx| PlatformTitleBar::new("rules-library-title-bar", cx))),
 508            store,
 509            language_registry,
 510            rule_editors: HashMap::default(),
 511            active_rule_id: None,
 512            pending_load: Task::ready(()),
 513            inline_assist_delegate,
 514            make_completion_provider,
 515            _subscriptions: vec![cx.subscribe_in(&picker, window, Self::handle_picker_event)],
 516            picker,
 517        }
 518    }
 519
 520    fn handle_picker_event(
 521        &mut self,
 522        _: &Entity<Picker<RulePickerDelegate>>,
 523        event: &RulePickerEvent,
 524        window: &mut Window,
 525        cx: &mut Context<Self>,
 526    ) {
 527        match event {
 528            RulePickerEvent::Selected { prompt_id } => {
 529                self.load_rule(*prompt_id, false, window, cx);
 530            }
 531            RulePickerEvent::Confirmed { prompt_id } => {
 532                self.load_rule(*prompt_id, true, window, cx);
 533            }
 534            RulePickerEvent::ToggledDefault { prompt_id } => {
 535                self.toggle_default_for_rule(*prompt_id, window, cx);
 536            }
 537            RulePickerEvent::Deleted { prompt_id } => {
 538                self.delete_rule(*prompt_id, window, cx);
 539            }
 540        }
 541    }
 542
 543    pub fn new_rule(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 544        // If we already have an untitled rule, use that instead
 545        // of creating a new one.
 546        if let Some(metadata) = self.store.read(cx).first()
 547            && metadata.title.is_none()
 548        {
 549            self.load_rule(metadata.id, true, window, cx);
 550            return;
 551        }
 552
 553        let prompt_id = PromptId::new();
 554        let save = self.store.update(cx, |store, cx| {
 555            store.save(prompt_id, None, false, "".into(), cx)
 556        });
 557        self.picker
 558            .update(cx, |picker, cx| picker.refresh(window, cx));
 559        cx.spawn_in(window, async move |this, cx| {
 560            save.await?;
 561            this.update_in(cx, |this, window, cx| {
 562                this.load_rule(prompt_id, true, window, cx)
 563            })
 564        })
 565        .detach_and_log_err(cx);
 566    }
 567
 568    pub fn save_rule(&mut self, prompt_id: PromptId, window: &mut Window, cx: &mut Context<Self>) {
 569        const SAVE_THROTTLE: Duration = Duration::from_millis(500);
 570
 571        if !prompt_id.can_edit() {
 572            return;
 573        }
 574
 575        let rule_metadata = self.store.read(cx).metadata(prompt_id).unwrap();
 576        let rule_editor = self.rule_editors.get_mut(&prompt_id).unwrap();
 577        let title = rule_editor.title_editor.read(cx).text(cx);
 578        let body = rule_editor.body_editor.update(cx, |editor, cx| {
 579            editor
 580                .buffer()
 581                .read(cx)
 582                .as_singleton()
 583                .unwrap()
 584                .read(cx)
 585                .as_rope()
 586                .clone()
 587        });
 588
 589        let store = self.store.clone();
 590        let executor = cx.background_executor().clone();
 591
 592        rule_editor.next_title_and_body_to_save = Some((title, body));
 593        if rule_editor.pending_save.is_none() {
 594            rule_editor.pending_save = Some(cx.spawn_in(window, async move |this, cx| {
 595                async move {
 596                    loop {
 597                        let title_and_body = this.update(cx, |this, _| {
 598                            this.rule_editors
 599                                .get_mut(&prompt_id)?
 600                                .next_title_and_body_to_save
 601                                .take()
 602                        })?;
 603
 604                        if let Some((title, body)) = title_and_body {
 605                            let title = if title.trim().is_empty() {
 606                                None
 607                            } else {
 608                                Some(SharedString::from(title))
 609                            };
 610                            cx.update(|_window, cx| {
 611                                store.update(cx, |store, cx| {
 612                                    store.save(prompt_id, title, rule_metadata.default, body, cx)
 613                                })
 614                            })?
 615                            .await
 616                            .log_err();
 617                            this.update_in(cx, |this, window, cx| {
 618                                this.picker
 619                                    .update(cx, |picker, cx| picker.refresh(window, cx));
 620                                cx.notify();
 621                            })?;
 622
 623                            executor.timer(SAVE_THROTTLE).await;
 624                        } else {
 625                            break;
 626                        }
 627                    }
 628
 629                    this.update(cx, |this, _cx| {
 630                        if let Some(rule_editor) = this.rule_editors.get_mut(&prompt_id) {
 631                            rule_editor.pending_save = None;
 632                        }
 633                    })
 634                }
 635                .log_err()
 636                .await
 637            }));
 638        }
 639    }
 640
 641    pub fn delete_active_rule(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 642        if let Some(active_rule_id) = self.active_rule_id {
 643            self.delete_rule(active_rule_id, window, cx);
 644        }
 645    }
 646
 647    pub fn duplicate_active_rule(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 648        if let Some(active_rule_id) = self.active_rule_id {
 649            self.duplicate_rule(active_rule_id, window, cx);
 650        }
 651    }
 652
 653    pub fn toggle_default_for_active_rule(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 654        if let Some(active_rule_id) = self.active_rule_id {
 655            self.toggle_default_for_rule(active_rule_id, window, cx);
 656        }
 657    }
 658
 659    pub fn restore_default_content_for_active_rule(
 660        &mut self,
 661        window: &mut Window,
 662        cx: &mut Context<Self>,
 663    ) {
 664        if let Some(active_rule_id) = self.active_rule_id {
 665            self.restore_default_content(active_rule_id, window, cx);
 666        }
 667    }
 668
 669    pub fn restore_default_content(
 670        &mut self,
 671        prompt_id: PromptId,
 672        window: &mut Window,
 673        cx: &mut Context<Self>,
 674    ) {
 675        let Some(built_in) = prompt_id.as_built_in() else {
 676            return;
 677        };
 678
 679        if let Some(rule_editor) = self.rule_editors.get(&prompt_id) {
 680            rule_editor.body_editor.update(cx, |editor, cx| {
 681                editor.set_text(built_in.default_content(), window, cx);
 682            });
 683        }
 684    }
 685
 686    pub fn toggle_default_for_rule(
 687        &mut self,
 688        prompt_id: PromptId,
 689        window: &mut Window,
 690        cx: &mut Context<Self>,
 691    ) {
 692        self.store.update(cx, move |store, cx| {
 693            if let Some(rule_metadata) = store.metadata(prompt_id) {
 694                store
 695                    .save_metadata(prompt_id, rule_metadata.title, !rule_metadata.default, cx)
 696                    .detach_and_log_err(cx);
 697            }
 698        });
 699        self.picker
 700            .update(cx, |picker, cx| picker.refresh(window, cx));
 701        cx.notify();
 702    }
 703
 704    pub fn load_rule(
 705        &mut self,
 706        prompt_id: PromptId,
 707        focus: bool,
 708        window: &mut Window,
 709        cx: &mut Context<Self>,
 710    ) {
 711        if let Some(rule_editor) = self.rule_editors.get(&prompt_id) {
 712            if focus {
 713                rule_editor
 714                    .body_editor
 715                    .update(cx, |editor, cx| window.focus(&editor.focus_handle(cx), cx));
 716            }
 717            self.set_active_rule(Some(prompt_id), window, cx);
 718        } else if let Some(rule_metadata) = self.store.read(cx).metadata(prompt_id) {
 719            let language_registry = self.language_registry.clone();
 720            let rule = self.store.read(cx).load(prompt_id, cx);
 721            let make_completion_provider = self.make_completion_provider.clone();
 722            self.pending_load = cx.spawn_in(window, async move |this, cx| {
 723                let rule = rule.await;
 724                let markdown = language_registry.language_for_name("Markdown").await;
 725                this.update_in(cx, |this, window, cx| match rule {
 726                    Ok(rule) => {
 727                        let title_editor = cx.new(|cx| {
 728                            let mut editor = Editor::single_line(window, cx);
 729                            editor.set_placeholder_text("Untitled", window, cx);
 730                            editor.set_text(rule_metadata.title.unwrap_or_default(), window, cx);
 731                            if prompt_id.is_built_in() {
 732                                editor.set_read_only(true);
 733                                editor.set_show_edit_predictions(Some(false), window, cx);
 734                            }
 735                            editor
 736                        });
 737                        let body_editor = cx.new(|cx| {
 738                            let buffer = cx.new(|cx| {
 739                                let mut buffer = Buffer::local(rule, cx);
 740                                buffer.set_language(markdown.log_err(), cx);
 741                                buffer.set_language_registry(language_registry);
 742                                buffer
 743                            });
 744
 745                            let mut editor = Editor::for_buffer(buffer, None, window, cx);
 746                            if !prompt_id.can_edit() {
 747                                editor.set_read_only(true);
 748                                editor.set_show_edit_predictions(Some(false), window, cx);
 749                            }
 750                            editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
 751                            editor.set_show_gutter(false, cx);
 752                            editor.set_show_wrap_guides(false, cx);
 753                            editor.set_show_indent_guides(false, cx);
 754                            editor.set_use_modal_editing(true);
 755                            editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
 756                            editor.set_completion_provider(Some(make_completion_provider()));
 757                            if focus {
 758                                window.focus(&editor.focus_handle(cx), cx);
 759                            }
 760                            editor
 761                        });
 762                        let _subscriptions = vec![
 763                            cx.subscribe_in(
 764                                &title_editor,
 765                                window,
 766                                move |this, editor, event, window, cx| {
 767                                    this.handle_rule_title_editor_event(
 768                                        prompt_id, editor, event, window, cx,
 769                                    )
 770                                },
 771                            ),
 772                            cx.subscribe_in(
 773                                &body_editor,
 774                                window,
 775                                move |this, editor, event, window, cx| {
 776                                    this.handle_rule_body_editor_event(
 777                                        prompt_id, editor, event, window, cx,
 778                                    )
 779                                },
 780                            ),
 781                        ];
 782                        this.rule_editors.insert(
 783                            prompt_id,
 784                            RuleEditor {
 785                                title_editor,
 786                                body_editor,
 787                                next_title_and_body_to_save: None,
 788                                pending_save: None,
 789                                token_count: None,
 790                                pending_token_count: Task::ready(None),
 791                                _subscriptions,
 792                            },
 793                        );
 794                        this.set_active_rule(Some(prompt_id), window, cx);
 795                        this.count_tokens(prompt_id, window, cx);
 796                    }
 797                    Err(error) => {
 798                        // TODO: we should show the error in the UI.
 799                        log::error!("error while loading rule: {:?}", error);
 800                    }
 801                })
 802                .ok();
 803            });
 804        }
 805    }
 806
 807    fn set_active_rule(
 808        &mut self,
 809        prompt_id: Option<PromptId>,
 810        window: &mut Window,
 811        cx: &mut Context<Self>,
 812    ) {
 813        self.active_rule_id = prompt_id;
 814        self.picker.update(cx, |picker, cx| {
 815            if let Some(prompt_id) = prompt_id {
 816                if picker
 817                    .delegate
 818                    .filtered_entries
 819                    .get(picker.delegate.selected_index())
 820                    .is_none_or(|old_selected_prompt| {
 821                        if let RulePickerEntry::Rule(rule) = old_selected_prompt {
 822                            rule.id != prompt_id
 823                        } else {
 824                            true
 825                        }
 826                    })
 827                    && let Some(ix) = picker.delegate.filtered_entries.iter().position(|mat| {
 828                        if let RulePickerEntry::Rule(rule) = mat {
 829                            rule.id == prompt_id
 830                        } else {
 831                            false
 832                        }
 833                    })
 834                {
 835                    picker.set_selected_index(ix, None, true, window, cx);
 836                }
 837            } else {
 838                picker.focus(window, cx);
 839            }
 840        });
 841        cx.notify();
 842    }
 843
 844    pub fn delete_rule(
 845        &mut self,
 846        prompt_id: PromptId,
 847        window: &mut Window,
 848        cx: &mut Context<Self>,
 849    ) {
 850        if let Some(metadata) = self.store.read(cx).metadata(prompt_id) {
 851            let confirmation = window.prompt(
 852                PromptLevel::Warning,
 853                &format!(
 854                    "Are you sure you want to delete {}",
 855                    metadata.title.unwrap_or("Untitled".into())
 856                ),
 857                None,
 858                &["Delete", "Cancel"],
 859                cx,
 860            );
 861
 862            cx.spawn_in(window, async move |this, cx| {
 863                if confirmation.await.ok() == Some(0) {
 864                    this.update_in(cx, |this, window, cx| {
 865                        if this.active_rule_id == Some(prompt_id) {
 866                            this.set_active_rule(None, window, cx);
 867                        }
 868                        this.rule_editors.remove(&prompt_id);
 869                        this.store
 870                            .update(cx, |store, cx| store.delete(prompt_id, cx))
 871                            .detach_and_log_err(cx);
 872                        this.picker
 873                            .update(cx, |picker, cx| picker.refresh(window, cx));
 874                        cx.notify();
 875                    })?;
 876                }
 877                anyhow::Ok(())
 878            })
 879            .detach_and_log_err(cx);
 880        }
 881    }
 882
 883    pub fn duplicate_rule(
 884        &mut self,
 885        prompt_id: PromptId,
 886        window: &mut Window,
 887        cx: &mut Context<Self>,
 888    ) {
 889        if let Some(rule) = self.rule_editors.get(&prompt_id) {
 890            const DUPLICATE_SUFFIX: &str = " copy";
 891            let title_to_duplicate = rule.title_editor.read(cx).text(cx);
 892            let existing_titles = self
 893                .rule_editors
 894                .iter()
 895                .filter(|&(&id, _)| id != prompt_id)
 896                .map(|(_, rule_editor)| rule_editor.title_editor.read(cx).text(cx))
 897                .filter(|title| title.starts_with(&title_to_duplicate))
 898                .collect::<HashSet<_>>();
 899
 900            let title = if existing_titles.is_empty() {
 901                title_to_duplicate + DUPLICATE_SUFFIX
 902            } else {
 903                let mut i = 1;
 904                loop {
 905                    let new_title = format!("{title_to_duplicate}{DUPLICATE_SUFFIX} {i}");
 906                    if !existing_titles.contains(&new_title) {
 907                        break new_title;
 908                    }
 909                    i += 1;
 910                }
 911            };
 912
 913            let new_id = PromptId::new();
 914            let body = rule.body_editor.read(cx).text(cx);
 915            let save = self.store.update(cx, |store, cx| {
 916                store.save(new_id, Some(title.into()), false, body.into(), cx)
 917            });
 918            self.picker
 919                .update(cx, |picker, cx| picker.refresh(window, cx));
 920            cx.spawn_in(window, async move |this, cx| {
 921                save.await?;
 922                this.update_in(cx, |rules_library, window, cx| {
 923                    rules_library.load_rule(new_id, true, window, cx)
 924                })
 925            })
 926            .detach_and_log_err(cx);
 927        }
 928    }
 929
 930    fn focus_active_rule(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
 931        if let Some(active_rule) = self.active_rule_id {
 932            self.rule_editors[&active_rule]
 933                .body_editor
 934                .update(cx, |editor, cx| window.focus(&editor.focus_handle(cx), cx));
 935            cx.stop_propagation();
 936        }
 937    }
 938
 939    fn focus_picker(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
 940        self.picker
 941            .update(cx, |picker, cx| picker.focus(window, cx));
 942    }
 943
 944    pub fn inline_assist(
 945        &mut self,
 946        action: &InlineAssist,
 947        window: &mut Window,
 948        cx: &mut Context<Self>,
 949    ) {
 950        let Some(active_rule_id) = self.active_rule_id else {
 951            cx.propagate();
 952            return;
 953        };
 954
 955        let rule_editor = &self.rule_editors[&active_rule_id].body_editor;
 956        let Some(ConfiguredModel { provider, .. }) =
 957            LanguageModelRegistry::read_global(cx).inline_assistant_model()
 958        else {
 959            return;
 960        };
 961
 962        let initial_prompt = action.prompt.clone();
 963        if provider.is_authenticated(cx) {
 964            self.inline_assist_delegate
 965                .assist(rule_editor, initial_prompt, window, cx);
 966        } else {
 967            for window in cx.windows() {
 968                if let Some(multi_workspace) = window.downcast::<MultiWorkspace>() {
 969                    let panel = multi_workspace
 970                        .update(cx, |multi_workspace, window, cx| {
 971                            window.activate_window();
 972                            multi_workspace.workspace().update(cx, |workspace, cx| {
 973                                self.inline_assist_delegate
 974                                    .focus_agent_panel(workspace, window, cx)
 975                            })
 976                        })
 977                        .ok();
 978                    if panel == Some(true) {
 979                        return;
 980                    }
 981                }
 982            }
 983        }
 984    }
 985
 986    fn move_down_from_title(
 987        &mut self,
 988        _: &zed_actions::editor::MoveDown,
 989        window: &mut Window,
 990        cx: &mut Context<Self>,
 991    ) {
 992        if let Some(rule_id) = self.active_rule_id
 993            && let Some(rule_editor) = self.rule_editors.get(&rule_id)
 994        {
 995            window.focus(&rule_editor.body_editor.focus_handle(cx), cx);
 996        }
 997    }
 998
 999    fn move_up_from_body(
1000        &mut self,
1001        _: &zed_actions::editor::MoveUp,
1002        window: &mut Window,
1003        cx: &mut Context<Self>,
1004    ) {
1005        if let Some(rule_id) = self.active_rule_id
1006            && let Some(rule_editor) = self.rule_editors.get(&rule_id)
1007        {
1008            window.focus(&rule_editor.title_editor.focus_handle(cx), cx);
1009        }
1010    }
1011
1012    fn handle_rule_title_editor_event(
1013        &mut self,
1014        prompt_id: PromptId,
1015        title_editor: &Entity<Editor>,
1016        event: &EditorEvent,
1017        window: &mut Window,
1018        cx: &mut Context<Self>,
1019    ) {
1020        match event {
1021            EditorEvent::BufferEdited => {
1022                self.save_rule(prompt_id, window, cx);
1023                self.count_tokens(prompt_id, window, cx);
1024            }
1025            EditorEvent::Blurred => {
1026                title_editor.update(cx, |title_editor, cx| {
1027                    title_editor.change_selections(
1028                        SelectionEffects::no_scroll(),
1029                        window,
1030                        cx,
1031                        |selections| {
1032                            let cursor = selections.oldest_anchor().head();
1033                            selections.select_anchor_ranges([cursor..cursor]);
1034                        },
1035                    );
1036                });
1037            }
1038            _ => {}
1039        }
1040    }
1041
1042    fn handle_rule_body_editor_event(
1043        &mut self,
1044        prompt_id: PromptId,
1045        body_editor: &Entity<Editor>,
1046        event: &EditorEvent,
1047        window: &mut Window,
1048        cx: &mut Context<Self>,
1049    ) {
1050        match event {
1051            EditorEvent::BufferEdited => {
1052                self.save_rule(prompt_id, window, cx);
1053                self.count_tokens(prompt_id, window, cx);
1054            }
1055            EditorEvent::Blurred => {
1056                body_editor.update(cx, |body_editor, cx| {
1057                    body_editor.change_selections(
1058                        SelectionEffects::no_scroll(),
1059                        window,
1060                        cx,
1061                        |selections| {
1062                            let cursor = selections.oldest_anchor().head();
1063                            selections.select_anchor_ranges([cursor..cursor]);
1064                        },
1065                    );
1066                });
1067            }
1068            _ => {}
1069        }
1070    }
1071
1072    fn count_tokens(&mut self, prompt_id: PromptId, window: &mut Window, cx: &mut Context<Self>) {
1073        let Some(ConfiguredModel { model, .. }) =
1074            LanguageModelRegistry::read_global(cx).default_model()
1075        else {
1076            return;
1077        };
1078        if let Some(rule) = self.rule_editors.get_mut(&prompt_id) {
1079            let editor = &rule.body_editor.read(cx);
1080            let buffer = &editor.buffer().read(cx).as_singleton().unwrap().read(cx);
1081            let body = buffer.as_rope().clone();
1082            rule.pending_token_count = cx.spawn_in(window, async move |this, cx| {
1083                async move {
1084                    const DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
1085
1086                    cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
1087                    let token_count = cx
1088                        .update(|_, cx| {
1089                            model.count_tokens(
1090                                LanguageModelRequest {
1091                                    thread_id: None,
1092                                    prompt_id: None,
1093                                    intent: None,
1094                                    messages: vec![LanguageModelRequestMessage {
1095                                        role: Role::System,
1096                                        content: vec![body.to_string().into()],
1097                                        cache: false,
1098                                        reasoning_details: None,
1099                                    }],
1100                                    tools: Vec::new(),
1101                                    tool_choice: None,
1102                                    stop: Vec::new(),
1103                                    temperature: None,
1104                                    thinking_allowed: true,
1105                                    thinking_effort: None,
1106                                    speed: None,
1107                                },
1108                                cx,
1109                            )
1110                        })?
1111                        .await?;
1112
1113                    this.update(cx, |this, cx| {
1114                        let rule_editor = this.rule_editors.get_mut(&prompt_id).unwrap();
1115                        rule_editor.token_count = Some(token_count);
1116                        cx.notify();
1117                    })
1118                }
1119                .log_err()
1120                .await
1121            });
1122        }
1123    }
1124
1125    fn render_rule_list(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
1126        v_flex()
1127            .id("rule-list")
1128            .capture_action(cx.listener(Self::focus_active_rule))
1129            .h_full()
1130            .w_64()
1131            .overflow_x_hidden()
1132            .bg(cx.theme().colors().panel_background)
1133            .when(!cfg!(target_os = "macos"), |this| this.px_1p5())
1134            .map(|this| {
1135                if cfg!(target_os = "macos") {
1136                    let Some(title_bar) = self.title_bar.as_ref() else {
1137                        return this;
1138                    };
1139                    let button_padding = DynamicSpacing::Base08.rems(cx);
1140                    let panel_background = cx.theme().colors().panel_background;
1141                    title_bar.update(cx, |title_bar, _cx| {
1142                        title_bar.set_background_color(Some(panel_background));
1143                        title_bar.set_children(Some(
1144                            h_flex()
1145                                .w_full()
1146                                .pr(button_padding)
1147                                .justify_end()
1148                                .child(
1149                                    div()
1150                                        .on_mouse_down(MouseButton::Left, |_, _, cx| {
1151                                            cx.stop_propagation();
1152                                        })
1153                                        .child(
1154                                            IconButton::new("new-rule", IconName::Plus)
1155                                                .tooltip(move |_window, cx| {
1156                                                    Tooltip::for_action("New Rule", &NewRule, cx)
1157                                                })
1158                                                .on_click(|_, window, cx| {
1159                                                    window.dispatch_action(Box::new(NewRule), cx);
1160                                                }),
1161                                        ),
1162                                )
1163                                .into_any_element(),
1164                        ));
1165                    });
1166                    this.child(title_bar.clone())
1167                } else {
1168                    this.child(
1169                        h_flex().p_1().w_full().child(
1170                            Button::new("new-rule", "New Rule")
1171                                .full_width()
1172                                .style(ButtonStyle::Outlined)
1173                                .start_icon(
1174                                    Icon::new(IconName::Plus)
1175                                        .size(IconSize::Small)
1176                                        .color(Color::Muted),
1177                                )
1178                                .on_click(|_, window, cx| {
1179                                    window.dispatch_action(Box::new(NewRule), cx);
1180                                }),
1181                        ),
1182                    )
1183                }
1184            })
1185            .child(
1186                div()
1187                    .flex_grow()
1188                    .when(cfg!(target_os = "macos"), |this| this.px_1p5())
1189                    .child(self.picker.clone()),
1190            )
1191    }
1192
1193    fn render_active_rule_editor(
1194        &self,
1195        editor: &Entity<Editor>,
1196        read_only: bool,
1197        cx: &mut Context<Self>,
1198    ) -> impl IntoElement {
1199        let settings = ThemeSettings::get_global(cx);
1200        let text_color = if read_only {
1201            cx.theme().colors().text_muted
1202        } else {
1203            cx.theme().colors().text
1204        };
1205
1206        div()
1207            .w_full()
1208            .pl_1()
1209            .border_1()
1210            .border_color(transparent_black())
1211            .rounded_sm()
1212            .when(!read_only, |this| {
1213                this.group_hover("active-editor-header", |this| {
1214                    this.border_color(cx.theme().colors().border_variant)
1215                })
1216            })
1217            .on_action(cx.listener(Self::move_down_from_title))
1218            .child(EditorElement::new(
1219                &editor,
1220                EditorStyle {
1221                    background: cx.theme().system().transparent,
1222                    local_player: cx.theme().players().local(),
1223                    text: TextStyle {
1224                        color: text_color,
1225                        font_family: settings.ui_font.family.clone(),
1226                        font_features: settings.ui_font.features.clone(),
1227                        font_size: HeadlineSize::Medium.rems().into(),
1228                        font_weight: settings.ui_font.weight,
1229                        line_height: relative(settings.buffer_line_height.value()),
1230                        ..Default::default()
1231                    },
1232                    scrollbar_width: Pixels::ZERO,
1233                    syntax: cx.theme().syntax().clone(),
1234                    status: cx.theme().status().clone(),
1235                    inlay_hints_style: editor::make_inlay_hints_style(cx),
1236                    edit_prediction_styles: editor::make_suggestion_styles(cx),
1237                    ..EditorStyle::default()
1238                },
1239            ))
1240    }
1241
1242    fn render_duplicate_rule_button(&self) -> impl IntoElement {
1243        IconButton::new("duplicate-rule", IconName::BookCopy)
1244            .tooltip(move |_window, cx| Tooltip::for_action("Duplicate Rule", &DuplicateRule, cx))
1245            .on_click(|_, window, cx| {
1246                window.dispatch_action(Box::new(DuplicateRule), cx);
1247            })
1248    }
1249
1250    fn render_built_in_rule_controls(&self) -> impl IntoElement {
1251        h_flex()
1252            .gap_1()
1253            .child(self.render_duplicate_rule_button())
1254            .child(
1255                IconButton::new("restore-default", IconName::RotateCcw)
1256                    .tooltip(move |_window, cx| {
1257                        Tooltip::for_action(
1258                            "Restore to Default Content",
1259                            &RestoreDefaultContent,
1260                            cx,
1261                        )
1262                    })
1263                    .on_click(|_, window, cx| {
1264                        window.dispatch_action(Box::new(RestoreDefaultContent), cx);
1265                    }),
1266            )
1267    }
1268
1269    fn render_regular_rule_controls(&self, default: bool) -> impl IntoElement {
1270        h_flex()
1271            .gap_1()
1272            .child(
1273                IconButton::new("toggle-default-rule", IconName::Paperclip)
1274                    .toggle_state(default)
1275                    .when(default, |this| this.icon_color(Color::Accent))
1276                    .map(|this| {
1277                        if default {
1278                            this.tooltip(Tooltip::text("Remove from Default Rules"))
1279                        } else {
1280                            this.tooltip(move |_window, cx| {
1281                                Tooltip::with_meta(
1282                                    "Add to Default Rules",
1283                                    None,
1284                                    "Always included in every thread.",
1285                                    cx,
1286                                )
1287                            })
1288                        }
1289                    })
1290                    .on_click(|_, window, cx| {
1291                        window.dispatch_action(Box::new(ToggleDefaultRule), cx);
1292                    }),
1293            )
1294            .child(self.render_duplicate_rule_button())
1295            .child(
1296                IconButton::new("delete-rule", IconName::Trash)
1297                    .tooltip(move |_window, cx| Tooltip::for_action("Delete Rule", &DeleteRule, cx))
1298                    .on_click(|_, window, cx| {
1299                        window.dispatch_action(Box::new(DeleteRule), cx);
1300                    }),
1301            )
1302    }
1303
1304    fn render_active_rule(&mut self, cx: &mut Context<RulesLibrary>) -> gpui::Stateful<Div> {
1305        div()
1306            .id("rule-editor")
1307            .h_full()
1308            .flex_grow()
1309            .border_l_1()
1310            .border_color(cx.theme().colors().border)
1311            .bg(cx.theme().colors().editor_background)
1312            .children(self.active_rule_id.and_then(|prompt_id| {
1313                let rule_metadata = self.store.read(cx).metadata(prompt_id)?;
1314                let rule_editor = &self.rule_editors[&prompt_id];
1315                let focus_handle = rule_editor.body_editor.focus_handle(cx);
1316                let registry = LanguageModelRegistry::read_global(cx);
1317                let model = registry.default_model().map(|default| default.model);
1318                let built_in = prompt_id.is_built_in();
1319
1320                Some(
1321                    v_flex()
1322                        .id("rule-editor-inner")
1323                        .size_full()
1324                        .relative()
1325                        .overflow_hidden()
1326                        .on_click(cx.listener(move |_, _, window, cx| {
1327                            window.focus(&focus_handle, cx);
1328                        }))
1329                        .child(
1330                            h_flex()
1331                                .group("active-editor-header")
1332                                .h_12()
1333                                .px_2()
1334                                .gap_2()
1335                                .justify_between()
1336                                .child(self.render_active_rule_editor(
1337                                    &rule_editor.title_editor,
1338                                    built_in,
1339                                    cx,
1340                                ))
1341                                .child(
1342                                    h_flex()
1343                                        .h_full()
1344                                        .flex_shrink_0()
1345                                        .children(rule_editor.token_count.map(|token_count| {
1346                                            let token_count: SharedString =
1347                                                token_count.to_string().into();
1348                                            let label_token_count: SharedString =
1349                                                token_count.to_string().into();
1350
1351                                            div()
1352                                                .id("token_count")
1353                                                .mr_1()
1354                                                .flex_shrink_0()
1355                                                .tooltip(move |_window, cx| {
1356                                                    Tooltip::with_meta(
1357                                                        "Token Estimation",
1358                                                        None,
1359                                                        format!(
1360                                                            "Model: {}",
1361                                                            model
1362                                                                .as_ref()
1363                                                                .map(|model| model.name().0)
1364                                                                .unwrap_or_default()
1365                                                        ),
1366                                                        cx,
1367                                                    )
1368                                                })
1369                                                .child(
1370                                                    Label::new(format!(
1371                                                        "{} tokens",
1372                                                        label_token_count
1373                                                    ))
1374                                                    .color(Color::Muted),
1375                                                )
1376                                        }))
1377                                        .map(|this| {
1378                                            if built_in {
1379                                                this.child(self.render_built_in_rule_controls())
1380                                            } else {
1381                                                this.child(self.render_regular_rule_controls(
1382                                                    rule_metadata.default,
1383                                                ))
1384                                            }
1385                                        }),
1386                                ),
1387                        )
1388                        .child(
1389                            div()
1390                                .on_action(cx.listener(Self::focus_picker))
1391                                .on_action(cx.listener(Self::inline_assist))
1392                                .on_action(cx.listener(Self::move_up_from_body))
1393                                .h_full()
1394                                .flex_grow()
1395                                .child(
1396                                    h_flex()
1397                                        .py_2()
1398                                        .pl_2p5()
1399                                        .h_full()
1400                                        .flex_1()
1401                                        .child(rule_editor.body_editor.clone()),
1402                                ),
1403                        ),
1404                )
1405            }))
1406    }
1407}
1408
1409impl Render for RulesLibrary {
1410    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1411        let ui_font = theme::setup_ui_font(window, cx);
1412        let theme = cx.theme().clone();
1413
1414        client_side_decorations(
1415            v_flex()
1416                .id("rules-library")
1417                .key_context("RulesLibrary")
1418                .on_action(cx.listener(|this, &NewRule, window, cx| this.new_rule(window, cx)))
1419                .on_action(
1420                    cx.listener(|this, &DeleteRule, window, cx| {
1421                        this.delete_active_rule(window, cx)
1422                    }),
1423                )
1424                .on_action(cx.listener(|this, &DuplicateRule, window, cx| {
1425                    this.duplicate_active_rule(window, cx)
1426                }))
1427                .on_action(cx.listener(|this, &ToggleDefaultRule, window, cx| {
1428                    this.toggle_default_for_active_rule(window, cx)
1429                }))
1430                .on_action(cx.listener(|this, &RestoreDefaultContent, window, cx| {
1431                    this.restore_default_content_for_active_rule(window, cx)
1432                }))
1433                .size_full()
1434                .overflow_hidden()
1435                .font(ui_font)
1436                .text_color(theme.colors().text)
1437                .when(!cfg!(target_os = "macos"), |this| {
1438                    this.children(self.title_bar.clone())
1439                })
1440                .bg(theme.colors().background)
1441                .child(
1442                    h_flex()
1443                        .flex_1()
1444                        .when(!cfg!(target_os = "macos"), |this| {
1445                            this.border_t_1().border_color(cx.theme().colors().border)
1446                        })
1447                        .child(self.render_rule_list(cx))
1448                        .child(self.render_active_rule(cx)),
1449                ),
1450            window,
1451            cx,
1452            Tiling::default(),
1453        )
1454    }
1455}