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