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