magic_palette.rs

  1use agent_settings::AgentSettings;
  2use anyhow::Result;
  3use cloud_llm_client::CompletionIntent;
  4use command_palette::humanize_action_name;
  5use futures::StreamExt as _;
  6use gpui::{
  7    Action, AppContext as _, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
  8    IntoElement, Task, WeakEntity,
  9};
 10use language_model::{
 11    ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
 12};
 13use picker::{Picker, PickerDelegate};
 14use settings::Settings as _;
 15use ui::{
 16    App, Context, InteractiveElement, KeyBinding, Label, ListItem, ListItemSpacing,
 17    ParentElement as _, Render, Styled as _, Toggleable as _, Window, div, h_flex, rems,
 18};
 19use util::ResultExt;
 20use workspace::{ModalView, Workspace};
 21
 22pub fn init(cx: &mut App) {
 23    cx.observe_new(MagicPalette::register).detach();
 24}
 25
 26gpui::actions!(magic_palette, [Toggle]);
 27
 28fn format_prompt(query: &str, actions: &str) -> String {
 29    format!(
 30        "Match the query: \"{query}\" to relevant actions. Return 5-10 action names, most relevant first, one per line.
 31        Actions:
 32        {actions}"
 33    )
 34}
 35
 36struct MagicPalette {
 37    picker: Entity<Picker<MagicPaletteDelegate>>,
 38}
 39
 40impl ModalView for MagicPalette {}
 41
 42impl EventEmitter<DismissEvent> for MagicPalette {}
 43
 44impl Focusable for MagicPalette {
 45    fn focus_handle(&self, cx: &App) -> FocusHandle {
 46        self.picker.focus_handle(cx)
 47    }
 48}
 49
 50impl MagicPalette {
 51    fn register(
 52        workspace: &mut Workspace,
 53        _window: Option<&mut Window>,
 54        _cx: &mut Context<Workspace>,
 55    ) {
 56        workspace.register_action(|workspace, _: &Toggle, window, cx| {
 57            Self::toggle(workspace, window, cx)
 58        });
 59    }
 60
 61    fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
 62        let Some(previous_focus_handle) = window.focused(cx) else {
 63            return;
 64        };
 65
 66        if agent_settings::AgentSettings::get_global(cx).enabled(cx) {
 67            workspace.toggle_modal(window, cx, |window, cx| {
 68                MagicPalette::new(previous_focus_handle, window, cx)
 69            });
 70        }
 71    }
 72
 73    fn new(
 74        previous_focus_handle: FocusHandle,
 75        window: &mut Window,
 76        cx: &mut Context<Self>,
 77    ) -> Self {
 78        let this = cx.weak_entity();
 79        let delegate = MagicPaletteDelegate::new(this, previous_focus_handle);
 80        let picker = cx.new(|cx| {
 81            let picker = Picker::uniform_list(delegate, window, cx);
 82            picker
 83        });
 84        Self { picker }
 85    }
 86}
 87
 88impl Render for MagicPalette {
 89    fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
 90        div()
 91            .key_context("MagicPalette")
 92            .w(rems(34.))
 93            .child(self.picker.clone())
 94    }
 95}
 96
 97#[derive(Debug)]
 98struct Command {
 99    name: String,
100    action: Box<dyn Action>,
101}
102
103struct MagicPaletteDelegate {
104    query: String,
105    llm_generation_task: Option<Task<Result<()>>>,
106    magic_palette: WeakEntity<MagicPalette>,
107    matches: Vec<Command>,
108    selected_index: usize,
109    previous_focus_handle: FocusHandle,
110}
111
112impl MagicPaletteDelegate {
113    fn new(magic_palette: WeakEntity<MagicPalette>, previous_focus_handle: FocusHandle) -> Self {
114        Self {
115            query: String::new(),
116            llm_generation_task: None,
117            magic_palette,
118            matches: vec![],
119            selected_index: 0,
120            previous_focus_handle,
121        }
122    }
123}
124
125impl PickerDelegate for MagicPaletteDelegate {
126    type ListItem = ListItem;
127
128    fn match_count(&self) -> usize {
129        self.matches.len()
130    }
131
132    fn selected_index(&self) -> usize {
133        self.selected_index
134    }
135
136    fn set_selected_index(
137        &mut self,
138        ix: usize,
139        _window: &mut Window,
140        _cx: &mut Context<picker::Picker<Self>>,
141    ) {
142        self.selected_index = ix;
143    }
144
145    fn placeholder_text(&self, _window: &mut Window, _cx: &mut ui::App) -> std::sync::Arc<str> {
146        "Ask Zed AI what actions you want to perform...".into()
147    }
148
149    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<ui::SharedString> {
150        if self.llm_generation_task.is_some() {
151            Some("Generating...".into())
152        } else {
153            None
154        }
155    }
156
157    fn update_matches(
158        &mut self,
159        query: String,
160        _window: &mut Window,
161        _cx: &mut Context<picker::Picker<Self>>,
162    ) -> gpui::Task<()> {
163        self.query = query;
164        Task::ready(())
165    }
166
167    fn confirm(
168        &mut self,
169        _secondary: bool,
170        window: &mut Window,
171        cx: &mut Context<picker::Picker<Self>>,
172    ) {
173        if self.matches.is_empty() {
174            let Some(ConfiguredModel { provider, model }) =
175                LanguageModelRegistry::read_global(cx).commit_message_model()
176            else {
177                return;
178            };
179            let temperature = AgentSettings::temperature_for_model(&model, cx);
180            let query = self.query.clone();
181            cx.notify();
182            self.llm_generation_task = Some(cx.spawn_in(window, async move |this, cx| {
183                let actions = cx.update(|_, cx| cx.action_documentation().clone())?;
184
185                if let Some(task) = cx.update(|_, cx| {
186                    if !provider.is_authenticated(cx) {
187                        Some(provider.authenticate(cx))
188                    } else {
189                        None
190                    }
191                })? {
192                    task.await.log_err();
193                };
194
195                let actions = actions
196                    .into_iter()
197                    .filter(|(action, _)| !action.starts_with("vim") && !action.starts_with("dev"))
198                    .map(|(name, description)| {
199                        let short = description
200                            .split_whitespace()
201                            .take(5)
202                            .collect::<Vec<_>>()
203                            .join(" ");
204
205                        format!("{} | {}", name, short)
206                    })
207                    .collect::<Vec<String>>();
208                let actions = actions.join("\n");
209                let prompt = format_prompt(&query, &actions);
210                println!("{}", prompt);
211
212                let request = LanguageModelRequest {
213                    thread_id: None,
214                    prompt_id: None,
215                    intent: Some(CompletionIntent::GenerateGitCommitMessage),
216                    mode: None,
217                    messages: vec![LanguageModelRequestMessage {
218                        role: Role::User,
219                        content: vec![prompt.into()],
220                        cache: false,
221                    }],
222                    tools: Vec::new(),
223                    tool_choice: None,
224                    stop: Vec::new(),
225                    temperature,
226                    thinking_allowed: false,
227                };
228
229                let stream = model.stream_completion_text(request, cx);
230                dbg!("pinging stream");
231                let mut messages = stream.await?;
232                let mut buffer = String::new();
233                while let Some(Ok(message)) = messages.stream.next().await {
234                    buffer.push_str(&message);
235                }
236
237                // Split result by `\n` and for each string, call `cx.build_action`.
238                let commands = cx.update(move |_window, cx| {
239                    let mut commands: Vec<Command> = vec![];
240
241                    for name in buffer.lines() {
242                        dbg!(name);
243
244                        let action = cx.build_action(name, None);
245                        match action {
246                            Ok(action) => commands.push(Command {
247                                action: action,
248                                name: humanize_action_name(name),
249                            }),
250                            Err(err) => {
251                                log::error!("Failed to build action: {}", err);
252                            }
253                        }
254                    }
255
256                    commands
257                })?;
258
259                this.update(cx, |this, cx| {
260                    this.delegate.matches = commands;
261                    this.delegate.llm_generation_task = None;
262                    this.delegate.selected_index = 0;
263                    cx.notify();
264                })?;
265
266                Ok(())
267            }));
268        } else {
269            let command = self.matches.swap_remove(self.selected_index);
270            telemetry::event!(
271                "Action Invoked",
272                source = "magic palette",
273                action = command.name
274            );
275            self.matches.clear();
276            self.query.clear();
277            self.llm_generation_task.take();
278
279            let action = command.action;
280            window.focus(&self.previous_focus_handle);
281            self.dismissed(window, cx);
282            window.dispatch_action(action, cx);
283        }
284    }
285
286    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
287        self.magic_palette
288            .update(cx, |_, cx| {
289                cx.emit(DismissEvent);
290            })
291            .ok();
292    }
293
294    fn render_match(
295        &self,
296        ix: usize,
297        selected: bool,
298        _window: &mut Window,
299        cx: &mut Context<picker::Picker<Self>>,
300    ) -> Option<Self::ListItem> {
301        let command = self.matches.get(ix)?;
302
303        Some(
304            ListItem::new(ix)
305                .inset(true)
306                .spacing(ListItemSpacing::Sparse)
307                .toggle_state(selected)
308                .child(
309                    h_flex()
310                        .w_full()
311                        .py_px()
312                        .justify_between()
313                        .child(Label::new(command.name.clone()))
314                        .child(KeyBinding::for_action_in(
315                            &*command.action,
316                            &self.previous_focus_handle,
317                            cx,
318                        )),
319                ),
320        )
321    }
322}