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