rules_library.rs

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