rules_library.rs

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