From 8da927325cb1ecce97e1885508af796758a5fae0 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 30 Oct 2025 09:42:26 -0700 Subject: [PATCH] Add a palette for prompting semantically relevant actions Co-authored-by: dino Co-authored-by: Gaauwe Rombouts --- 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(+) create mode 100644 crates/magic_palette/Cargo.toml create mode 120000 crates/magic_palette/LICENSE-GPL create mode 100644 crates/magic_palette/src/magic_palette.rs diff --git a/Cargo.lock b/Cargo.lock index 78c972865a4e01ba66357142ff8737b634639b27..6a4a92b7037cdd23e021c6fe9fe6133c61223b18 100644 --- a/Cargo.lock +++ b/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", diff --git a/Cargo.toml b/Cargo.toml index 369082ff16736f9f682ad8c5bd09634c03434609..76d00dd77a81824e44dc73f7e16c4e56c3cf7863 100644 --- a/Cargo.toml +++ b/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" } diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index 24f9b84afcfa7b9a40b4a1b7684e9a9b036a5a85..72c55bea865d0f84d82f94a4b4f58a85ac7f29e9 100644 --- a/crates/language_model/src/language_model.rs +++ b/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>, } +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 { diff --git a/crates/magic_palette/Cargo.toml b/crates/magic_palette/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..a5bcdbfcc4c35c07e4738002e72aacfe6aee7712 --- /dev/null +++ b/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 diff --git a/crates/magic_palette/LICENSE-GPL b/crates/magic_palette/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..e0f9dbd5d63fef1630c297edc4ceba4790be6f02 --- /dev/null +++ b/crates/magic_palette/LICENSE-GPL @@ -0,0 +1 @@ +LICENSE-GPL \ No newline at end of file diff --git a/crates/magic_palette/src/magic_palette.rs b/crates/magic_palette/src/magic_palette.rs new file mode 100644 index 0000000000000000000000000000000000000000..2d32e4ada022e43bddd8d5d220f5fb49c94c432d --- /dev/null +++ b/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>, +} + +impl ModalView for MagicPalette {} + +impl EventEmitter 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.register_action(|workspace, _: &Toggle, window, cx| { + Self::toggle(workspace, window, cx) + }); + } + + fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { + 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 { + 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) -> impl IntoElement { + div() + .key_context("MagicPalette") + .w(rems(34.)) + .child(self.picker.clone()) + } +} + +struct Command { + name: String, + action: Box, +} + +enum MagicPaletteMode { + WriteQuery, + SelectResult(Vec), +} + +struct MagicPaletteDelegate { + query: String, + llm_generation_task: Task>, + magic_palette: WeakEntity, + mode: MagicPaletteMode, + selected_index: usize, +} + +impl MagicPaletteDelegate { + fn new(magic_palette: WeakEntity) -> 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>, + ) { + self.selected_index = ix; + } + + fn placeholder_text(&self, _window: &mut Window, _cx: &mut ui::App) -> std::sync::Arc { + "Ask Zed AI what actions you want to perform...".into() + } + + fn update_matches( + &mut self, + query: String, + _window: &mut Window, + _cx: &mut Context>, + ) -> gpui::Task<()> { + self.query = query; + Task::ready(()) + } + + fn confirm( + &mut self, + _secondary: bool, + window: &mut Window, + cx: &mut Context>, + ) { + 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::>(); + 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>) { + self.magic_palette + .update(cx, |_, cx| { + cx.emit(DismissEvent); + }) + .ok(); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + window: &mut Window, + cx: &mut Context>, + ) -> Option { + None + } + + fn confirm_input( + &mut self, + _secondary: bool, + _window: &mut Window, + _: &mut Context>, + ) { + } +} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 9f6196c1482bcff2db9b7812dfb75b1471fec273..901d500c0efdb175e2d65fb64fecc2bd6e23191d 100644 --- a/crates/zed/Cargo.toml +++ b/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 diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index b873a58d3b61338b25c5908c2f87b62acb95d6f6..5f107610c6397b6229b9a75c6eb080ff3735e5d7 100644 --- a/crates/zed/src/main.rs +++ b/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,