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