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}