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