Add a palette for prompting semantically relevant actions

Mikayla Maki , dino , and Gaauwe Rombouts created

Co-authored-by: dino <dinojoaocosta@gmail.com>
Co-authored-by: Gaauwe Rombouts <mail@grombouts.nl>

Change summary

Cargo.lock                                  |  25 ++
Cargo.toml                                  |   2 
crates/language_model/src/language_model.rs |   9 
crates/magic_palette/Cargo.toml             |  33 ++
crates/magic_palette/LICENSE-GPL            |   1 
crates/magic_palette/src/magic_palette.rs   | 259 +++++++++++++++++++++++
crates/zed/Cargo.toml                       |   1 
crates/zed/src/main.rs                      |   1 
8 files changed, 331 insertions(+)

Detailed changes

Cargo.lock 🔗

@@ -9609,6 +9609,30 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "magic_palette"
+version = "0.1.0"
+dependencies = [
+ "agent_settings",
+ "anyhow",
+ "client",
+ "cloud_llm_client",
+ "collections",
+ "futures 0.3.31",
+ "gpui",
+ "language_model",
+ "log",
+ "menu",
+ "picker",
+ "settings",
+ "telemetry",
+ "theme",
+ "ui",
+ "util",
+ "workspace",
+ "zed_actions",
+]
+
 [[package]]
 name = "malloc_buf"
 version = "0.0.6"
@@ -21198,6 +21222,7 @@ dependencies = [
  "languages",
  "line_ending_selector",
  "log",
+ "magic_palette",
  "markdown",
  "markdown_preview",
  "menu",

Cargo.toml 🔗

@@ -104,6 +104,7 @@ members = [
     "crates/livekit_client",
     "crates/lmstudio",
     "crates/lsp",
+    "crates/magic_palette",
     "crates/markdown",
     "crates/markdown_preview",
     "crates/media",
@@ -333,6 +334,7 @@ livekit_api = { path = "crates/livekit_api" }
 livekit_client = { path = "crates/livekit_client" }
 lmstudio = { path = "crates/lmstudio" }
 lsp = { path = "crates/lsp" }
+magic_palette = { path = "crates/magic_palette" }
 markdown = { path = "crates/markdown" }
 markdown_preview = { path = "crates/markdown_preview" }
 svg_preview = { path = "crates/svg_preview" }

crates/language_model/src/language_model.rs 🔗

@@ -21,6 +21,7 @@ use open_router::OpenRouterError;
 use parking_lot::Mutex;
 use serde::{Deserialize, Serialize};
 pub use settings::LanguageModelCacheConfiguration;
+use std::fmt::Debug;
 use std::ops::{Add, Sub};
 use std::str::FromStr;
 use std::sync::Arc;
@@ -510,6 +511,14 @@ pub struct LanguageModelTextStream {
     pub last_token_usage: Arc<Mutex<TokenUsage>>,
 }
 
+impl Debug for LanguageModelTextStream {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("LanguageModelTextStream")
+            .field("message_id", &self.message_id)
+            .finish()
+    }
+}
+
 impl Default for LanguageModelTextStream {
     fn default() -> Self {
         Self {

crates/magic_palette/Cargo.toml 🔗

@@ -0,0 +1,33 @@
+[package]
+name = "magic_palette"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/magic_palette.rs"
+doctest = false
+
+[dependencies]
+anyhow.workspace = true
+agent_settings.workspace = true
+cloud_llm_client.workspace = true
+language_model.workspace = true
+client.workspace = true
+collections.workspace = true
+gpui.workspace = true
+menu.workspace = true
+log.workspace = true
+picker.workspace = true
+settings.workspace = true
+theme.workspace = true
+futures.workspace = true
+ui.workspace = true
+util.workspace = true
+telemetry.workspace = true
+workspace.workspace = true
+zed_actions.workspace = true

crates/magic_palette/src/magic_palette.rs 🔗

@@ -0,0 +1,259 @@
+use agent_settings::AgentSettings;
+use anyhow::Result;
+use cloud_llm_client::CompletionIntent;
+use futures::StreamExt as _;
+use gpui::{
+    Action, AppContext as _, DismissEvent, Entity, EventEmitter, Focusable, IntoElement, Task,
+    WeakEntity,
+};
+use language_model::{
+    ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
+};
+use picker::{Picker, PickerDelegate};
+use settings::Settings as _;
+use ui::{
+    App, Context, InteractiveElement, ListItem, ParentElement as _, Render, Styled as _, Window,
+    div, rems,
+};
+use util::ResultExt;
+use workspace::{ModalView, Workspace};
+
+pub fn init(cx: &mut App) {
+    cx.observe_new(MagicPalette::register).detach();
+}
+
+gpui::actions!(magic_palette, [Toggle]);
+
+struct MagicPalette {
+    picker: Entity<Picker<MagicPaletteDelegate>>,
+}
+
+impl ModalView for MagicPalette {}
+
+impl EventEmitter<DismissEvent> for MagicPalette {}
+
+impl Focusable for MagicPalette {
+    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
+        self.picker.focus_handle(cx)
+    }
+}
+
+impl MagicPalette {
+    fn register(
+        workspace: &mut Workspace,
+        _window: Option<&mut Window>,
+        _cx: &mut Context<Workspace>,
+    ) {
+        workspace.register_action(|workspace, _: &Toggle, window, cx| {
+            Self::toggle(workspace, window, cx)
+        });
+    }
+
+    fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
+        if agent_settings::AgentSettings::get_global(cx).enabled(cx) {
+            workspace.toggle_modal(window, cx, |window, cx| MagicPalette::new(window, cx));
+        }
+    }
+
+    fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
+        let this = cx.weak_entity();
+        let delegate = MagicPaletteDelegate::new(this);
+        let picker = cx.new(|cx| {
+            let picker = Picker::uniform_list(delegate, window, cx);
+            picker
+        });
+        Self { picker }
+    }
+}
+
+impl Render for MagicPalette {
+    fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+        div()
+            .key_context("MagicPalette")
+            .w(rems(34.))
+            .child(self.picker.clone())
+    }
+}
+
+struct Command {
+    name: String,
+    action: Box<dyn Action>,
+}
+
+enum MagicPaletteMode {
+    WriteQuery,
+    SelectResult(Vec<Command>),
+}
+
+struct MagicPaletteDelegate {
+    query: String,
+    llm_generation_task: Task<Result<()>>,
+    magic_palette: WeakEntity<MagicPalette>,
+    mode: MagicPaletteMode,
+    selected_index: usize,
+}
+
+impl MagicPaletteDelegate {
+    fn new(magic_palette: WeakEntity<MagicPalette>) -> Self {
+        Self {
+            query: String::new(),
+            llm_generation_task: Task::ready(Ok(())),
+            magic_palette,
+            mode: MagicPaletteMode::WriteQuery,
+            selected_index: 0,
+        }
+    }
+}
+
+impl PickerDelegate for MagicPaletteDelegate {
+    type ListItem = ListItem;
+
+    fn match_count(&self) -> usize {
+        match &self.mode {
+            MagicPaletteMode::WriteQuery => 0,
+            MagicPaletteMode::SelectResult(commands) => commands.len(),
+        }
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_index
+    }
+
+    fn set_selected_index(
+        &mut self,
+        ix: usize,
+        _window: &mut Window,
+        _cx: &mut Context<picker::Picker<Self>>,
+    ) {
+        self.selected_index = ix;
+    }
+
+    fn placeholder_text(&self, _window: &mut Window, _cx: &mut ui::App) -> std::sync::Arc<str> {
+        "Ask Zed AI what actions you want to perform...".into()
+    }
+
+    fn update_matches(
+        &mut self,
+        query: String,
+        _window: &mut Window,
+        _cx: &mut Context<picker::Picker<Self>>,
+    ) -> gpui::Task<()> {
+        self.query = query;
+        Task::ready(())
+    }
+
+    fn confirm(
+        &mut self,
+        _secondary: bool,
+        window: &mut Window,
+        cx: &mut Context<picker::Picker<Self>>,
+    ) {
+        match &self.mode {
+            MagicPaletteMode::WriteQuery => {
+                let Some(ConfiguredModel { provider, model }) =
+                    LanguageModelRegistry::read_global(cx).commit_message_model()
+                else {
+                    return;
+                };
+                let temperature = AgentSettings::temperature_for_model(&model, cx);
+                let query = self.query.clone();
+                let actions = window.available_actions(cx);
+                self.llm_generation_task = cx.spawn_in(window, async move |this, cx| {
+                    if let Some(task) = cx.update(|_, cx| {
+                        if !provider.is_authenticated(cx) {
+                            Some(provider.authenticate(cx))
+                        } else {
+                            None
+                        }
+                    })? {
+                        task.await.log_err();
+                    };
+
+                    let actions = actions
+                        .into_iter()
+                        .map(|actions| actions.name())
+                        .collect::<Vec<&'static str>>();
+                    let actions = actions.join("\n");
+                    let prompt = format!(
+                        "You are helping a user find the most relevant actions in Zed editor based on their natural language query.
+
+User query: \"{query}\"
+
+Available actions in Zed:
+{actions}
+
+Instructions:
+1. Analyze the user's query to understand their intent
+2. Match the query against the available actions, considering:
+   - Exact keyword matches
+   - Semantic similarity (e.g., \"open file\" matches \"workspace::Open\")
+   - Common synonyms and alternative phrasings
+   - Partial matches where relevant
+3. Return the top 5-10 most relevant actions in order of relevance
+4. Return each action name exactly as shown in the list above
+5. If no good matches exist, return the closest alternatives
+
+Format your response as a simple list of action names, one per line, with no additional text or explanation."
+                    );
+                    dbg!(&prompt);
+
+                    let request = LanguageModelRequest {
+                        thread_id: None,
+                        prompt_id: None,
+                        intent: Some(CompletionIntent::GenerateGitCommitMessage),
+                        mode: None,
+                        messages: vec![LanguageModelRequestMessage {
+                            role: Role::User,
+                            content: vec![prompt.into()],
+                            cache: false,
+                        }],
+                        tools: Vec::new(),
+                        tool_choice: None,
+                        stop: Vec::new(),
+                        temperature,
+                        thinking_allowed: false,
+                    };
+
+                    let stream = model.stream_completion_text(request, cx);
+                    dbg!("pinging stream");
+                    let mut messages = stream.await?;
+                    let mut buffer = String::new();
+                    while let Some(Ok(message)) = messages.stream.next().await {
+                        buffer.push_str(&message);
+                    }
+
+                    dbg!(buffer);
+                    //
+                    Ok(())
+                });
+            }
+            MagicPaletteMode::SelectResult(commands) => todo!(),
+        }
+    }
+
+    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
+        self.magic_palette
+            .update(cx, |_, cx| {
+                cx.emit(DismissEvent);
+            })
+            .ok();
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        selected: bool,
+        window: &mut Window,
+        cx: &mut Context<picker::Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        None
+    }
+
+    fn confirm_input(
+        &mut self,
+        _secondary: bool,
+        _window: &mut Window,
+        _: &mut Context<picker::Picker<Self>>,
+    ) {
+    }
+}

crates/zed/Cargo.toml 🔗

@@ -92,6 +92,7 @@ language_tools.workspace = true
 languages = { workspace = true, features = ["load-grammars"] }
 line_ending_selector.workspace = true
 log.workspace = true
+magic_palette.workspace = true
 markdown.workspace = true
 markdown_preview.workspace = true
 menu.workspace = true

crates/zed/src/main.rs 🔗

@@ -555,6 +555,7 @@ pub fn main() {
             cx.background_executor().clone(),
         );
         command_palette::init(cx);
+        magic_palette::init(cx);
         let copilot_language_server_id = app_state.languages.next_language_server_id();
         copilot::init(
             copilot_language_server_id,