Compare commits
5 Commits
fix-linux-
...
add-magic-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b6ef5eaab | ||
|
|
cba3f0d80f | ||
|
|
5d5f2df9e2 | ||
|
|
706061cdde | ||
|
|
8da927325c |
26
Cargo.lock
generated
26
Cargo.lock
generated
@@ -9609,6 +9609,31 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "magic_palette"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"agent_settings",
|
||||
"anyhow",
|
||||
"client",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"command_palette",
|
||||
"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 +21223,7 @@ dependencies = [
|
||||
"languages",
|
||||
"line_ending_selector",
|
||||
"log",
|
||||
"magic_palette",
|
||||
"markdown",
|
||||
"markdown_preview",
|
||||
"menu",
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
34
crates/magic_palette/Cargo.toml
Normal file
34
crates/magic_palette/Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
[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
|
||||
command_palette.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
|
||||
1
crates/magic_palette/LICENSE-GPL
Symbolic link
1
crates/magic_palette/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
LICENSE-GPL
|
||||
322
crates/magic_palette/src/magic_palette.rs
Normal file
322
crates/magic_palette/src/magic_palette.rs
Normal file
@@ -0,0 +1,322 @@
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use command_palette::humanize_action_name;
|
||||
use futures::StreamExt as _;
|
||||
use gpui::{
|
||||
Action, AppContext as _, DismissEvent, Entity, EventEmitter, FocusHandle, 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, KeyBinding, Label, ListItem, ListItemSpacing,
|
||||
ParentElement as _, Render, Styled as _, Toggleable as _, Window, div, h_flex, 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]);
|
||||
|
||||
fn format_prompt(query: &str, actions: &str) -> String {
|
||||
format!(
|
||||
"Match the query: \"{query}\" to relevant actions. Return 5-10 action names, most relevant first, one per line.
|
||||
Actions:
|
||||
{actions}"
|
||||
)
|
||||
}
|
||||
|
||||
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) -> 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>) {
|
||||
let Some(previous_focus_handle) = window.focused(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if agent_settings::AgentSettings::get_global(cx).enabled(cx) {
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
MagicPalette::new(previous_focus_handle, window, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn new(
|
||||
previous_focus_handle: FocusHandle,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let this = cx.weak_entity();
|
||||
let delegate = MagicPaletteDelegate::new(this, previous_focus_handle);
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Command {
|
||||
name: String,
|
||||
action: Box<dyn Action>,
|
||||
}
|
||||
|
||||
struct MagicPaletteDelegate {
|
||||
query: String,
|
||||
llm_generation_task: Option<Task<Result<()>>>,
|
||||
magic_palette: WeakEntity<MagicPalette>,
|
||||
matches: Vec<Command>,
|
||||
selected_index: usize,
|
||||
previous_focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl MagicPaletteDelegate {
|
||||
fn new(magic_palette: WeakEntity<MagicPalette>, previous_focus_handle: FocusHandle) -> Self {
|
||||
Self {
|
||||
query: String::new(),
|
||||
llm_generation_task: None,
|
||||
magic_palette,
|
||||
matches: vec![],
|
||||
selected_index: 0,
|
||||
previous_focus_handle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for MagicPaletteDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.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 no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<ui::SharedString> {
|
||||
if self.llm_generation_task.is_some() {
|
||||
Some("Generating...".into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
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>>,
|
||||
) {
|
||||
if self.matches.is_empty() {
|
||||
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();
|
||||
cx.notify();
|
||||
self.llm_generation_task = Some(cx.spawn_in(window, async move |this, cx| {
|
||||
let actions = cx.update(|_, cx| cx.action_documentation().clone())?;
|
||||
|
||||
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()
|
||||
.filter(|(action, _)| !action.starts_with("vim") && !action.starts_with("dev"))
|
||||
.map(|(name, description)| {
|
||||
let short = description
|
||||
.split_whitespace()
|
||||
.take(5)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
format!("{} | {}", name, short)
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
let actions = actions.join("\n");
|
||||
let prompt = format_prompt(&query, &actions);
|
||||
println!("{}", 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);
|
||||
}
|
||||
|
||||
// Split result by `\n` and for each string, call `cx.build_action`.
|
||||
let commands = cx.update(move |_window, cx| {
|
||||
let mut commands: Vec<Command> = vec![];
|
||||
|
||||
for name in buffer.lines() {
|
||||
dbg!(name);
|
||||
|
||||
let action = cx.build_action(name, None);
|
||||
match action {
|
||||
Ok(action) => commands.push(Command {
|
||||
action: action,
|
||||
name: humanize_action_name(name),
|
||||
}),
|
||||
Err(err) => {
|
||||
log::error!("Failed to build action: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
commands
|
||||
})?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.delegate.matches = commands;
|
||||
this.delegate.llm_generation_task = None;
|
||||
this.delegate.selected_index = 0;
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
} else {
|
||||
let command = self.matches.swap_remove(self.selected_index);
|
||||
telemetry::event!(
|
||||
"Action Invoked",
|
||||
source = "magic palette",
|
||||
action = command.name
|
||||
);
|
||||
self.matches.clear();
|
||||
self.query.clear();
|
||||
self.llm_generation_task.take();
|
||||
|
||||
let action = command.action;
|
||||
window.focus(&self.previous_focus_handle);
|
||||
self.dismissed(window, cx);
|
||||
window.dispatch_action(action, cx);
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
let command = self.matches.get(ix)?;
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.py_px()
|
||||
.justify_between()
|
||||
.child(Label::new(command.name.clone()))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&*command.action,
|
||||
&self.previous_focus_handle,
|
||||
cx,
|
||||
)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user