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