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