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