rules_library.rs

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