From e59c9108455fa4a51ed3eb2ed6b4cf3fe268b122 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 21 Jan 2025 19:36:55 -0500 Subject: [PATCH] assistant2: Add prompt editor (#23436) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds the Assistant1 experience to Assistant2 as a "prompt editor". Screenshot 2025-01-21 at 7 17 26 PM Release Notes: - N/A --- Cargo.lock | 2 + assets/keymaps/default-macos.json | 1 + crates/assistant/src/assistant_panel.rs | 34 +---- crates/assistant2/Cargo.toml | 2 + crates/assistant2/src/assistant.rs | 1 + crates/assistant2/src/assistant_panel.rs | 122 ++++++++++++++---- .../assistant_context_editor/src/context.rs | 5 +- .../src/context_editor.rs | 25 ++++ crates/zed/src/zed.rs | 16 ++- 9 files changed, 149 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a885c50968a734c678da9cec1eb5eca446b183a2..4fad1dece80500e1b27bb16b700ac462cecc8ad5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -438,7 +438,9 @@ name = "assistant2" version = "0.1.0" dependencies = [ "anyhow", + "assistant_context_editor", "assistant_settings", + "assistant_slash_command", "assistant_tool", "async-watch", "chrono", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index ec792c28569db2874b3fe9cfcdffb118b07cec3b..4987d14ea638854a21236a74c0e169b87cc4336f 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -227,6 +227,7 @@ "use_key_equivalents": true, "bindings": { "cmd-n": "assistant2::NewThread", + "cmd-alt-p": "assistant2::NewPromptEditor", "cmd-shift-h": "assistant2::OpenHistory", "cmd-alt-/": "assistant2::ToggleModelSelector", "cmd-shift-a": "assistant2::ToggleContextPicker", diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 4d79d15efd24c4794a5ec35ba764762ed66ff556..fb14eeab078527d1f0a2186f503de4f91a01445c 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -5,9 +5,10 @@ use crate::{ }; use anyhow::{anyhow, Result}; use assistant_context_editor::{ - AssistantPanelDelegate, Context, ContextEditor, ContextEditorToolbarItem, - ContextEditorToolbarItemEvent, ContextId, ContextStore, ContextStoreEvent, InsertDraggedFiles, - SlashCommandCompletionProvider, ToggleModelSelector, DEFAULT_TAB_TITLE, + make_lsp_adapter_delegate, AssistantPanelDelegate, Context, ContextEditor, + ContextEditorToolbarItem, ContextEditorToolbarItemEvent, ContextId, ContextStore, + ContextStoreEvent, InsertDraggedFiles, SlashCommandCompletionProvider, ToggleModelSelector, + DEFAULT_TAB_TITLE, }; use assistant_settings::{AssistantDockPosition, AssistantSettings}; use assistant_slash_command::SlashCommandWorkingSet; @@ -22,12 +23,11 @@ use gpui::{ ParentElement, Pixels, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, UpdateGlobal, View, WeakView, }; -use language::{LanguageRegistry, LspAdapterDelegate}; +use language::LanguageRegistry; use language_model::{ LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID, }; use language_model_selector::LanguageModelSelector; -use project::lsp_store::LocalLspAdapterDelegate; use project::Project; use prompt_library::{open_prompt_library, PromptBuilder, PromptLibrary}; use search::{buffer_search::DivRegistrar, BufferSearchBar}; @@ -1548,27 +1548,3 @@ impl Item for ConfigurationView { Some("Configuration".into()) } } - -fn make_lsp_adapter_delegate( - project: &Model, - cx: &mut AppContext, -) -> Result>> { - project.update(cx, |project, cx| { - // TODO: Find the right worktree. - let Some(worktree) = project.worktrees(cx).next() else { - return Ok(None::>); - }; - let http_client = project.client().http_client().clone(); - project.lsp_store().update(cx, |_, cx| { - Ok(Some(LocalLspAdapterDelegate::new( - project.languages().clone(), - project.environment(), - cx.weak_model(), - &worktree, - http_client, - project.fs().clone(), - cx, - ) as Arc)) - }) - }) -} diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index e28526bcea050c99465495e728253860cd916647..5d213f5b435474956639fd37494620ab7d0d6f04 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -20,7 +20,9 @@ test-support = [ [dependencies] anyhow.workspace = true +assistant_context_editor.workspace = true assistant_settings.workspace = true +assistant_slash_command.workspace = true assistant_tool.workspace = true async-watch.workspace = true chrono.workspace = true diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index bf35d35765eb04b244e25b8a50cf32fc86692ff6..9308d7a7b7743351ec75a2f2b3eeadcbcd24a9e7 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -36,6 +36,7 @@ actions!( [ ToggleFocus, NewThread, + NewPromptEditor, ToggleContextPicker, ToggleModelSelector, RemoveAllContext, diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index e2f14264d3c2daa7eaaf8d09befd595e31d7518d..ed96de01e5ab2e8d74988becb028944d7725f90b 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -1,19 +1,23 @@ use std::sync::Arc; use anyhow::Result; +use assistant_context_editor::{make_lsp_adapter_delegate, ContextEditor}; use assistant_settings::{AssistantDockPosition, AssistantSettings}; +use assistant_slash_command::SlashCommandWorkingSet; use assistant_tool::ToolWorkingSet; use client::zed_urls; use fs::Fs; use gpui::{ - prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, EventEmitter, + prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, Corner, EventEmitter, FocusHandle, FocusableView, FontWeight, Model, Pixels, Task, View, ViewContext, WeakView, WindowContext, }; use language::LanguageRegistry; +use prompt_library::PromptBuilder; use settings::Settings; use time::UtcOffset; -use ui::{prelude::*, KeyBinding, Tab, Tooltip}; +use ui::{prelude::*, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip}; +use util::ResultExt as _; use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::Workspace; @@ -22,7 +26,7 @@ use crate::message_editor::MessageEditor; use crate::thread::{Thread, ThreadError, ThreadId}; use crate::thread_history::{PastThread, ThreadHistory}; use crate::thread_store::ThreadStore; -use crate::{NewThread, OpenHistory, ToggleFocus}; +use crate::{NewPromptEditor, NewThread, OpenHistory, ToggleFocus}; pub fn init(cx: &mut AppContext) { cx.observe_new_views( @@ -42,6 +46,12 @@ pub fn init(cx: &mut AppContext) { workspace.focus_panel::(cx); panel.update(cx, |panel, cx| panel.open_history(cx)); } + }) + .register_action(|workspace, _: &NewPromptEditor, cx| { + if let Some(panel) = workspace.panel::(cx) { + workspace.focus_panel::(cx); + panel.update(cx, |panel, cx| panel.new_prompt_editor(workspace, cx)); + } }); }, ) @@ -50,6 +60,7 @@ pub fn init(cx: &mut AppContext) { enum ActiveView { Thread, + PromptEditor, History, } @@ -60,10 +71,13 @@ pub struct AssistantPanel { thread_store: Model, thread: View, message_editor: View, + context_store: Model, + context_editor: Option>, tools: Arc, local_timezone: UtcOffset, active_view: ActiveView, history: View, + new_item_context_menu_handle: PopoverMenuHandle, width: Option, height: Option, } @@ -71,6 +85,7 @@ pub struct AssistantPanel { impl AssistantPanel { pub fn load( workspace: WeakView, + prompt_builder: Arc, cx: AsyncWindowContext, ) -> Task>> { cx.spawn(|mut cx| async move { @@ -82,8 +97,22 @@ impl AssistantPanel { })? .await?; + let slash_commands = Arc::new(SlashCommandWorkingSet::default()); + let context_store = workspace + .update(&mut cx, |workspace, cx| { + let project = workspace.project().clone(); + assistant_context_editor::ContextStore::new( + project, + prompt_builder.clone(), + slash_commands, + tools.clone(), + cx, + ) + })? + .await?; + workspace.update(&mut cx, |workspace, cx| { - cx.new_view(|cx| Self::new(workspace, thread_store, tools, cx)) + cx.new_view(|cx| Self::new(workspace, thread_store, context_store, tools, cx)) }) }) } @@ -91,6 +120,7 @@ impl AssistantPanel { fn new( workspace: &Workspace, thread_store: Model, + context_store: Model, tools: Arc, cx: &mut ViewContext, ) -> Self { @@ -126,12 +156,15 @@ impl AssistantPanel { ) }), message_editor, + context_store, + context_editor: None, tools, local_timezone: UtcOffset::from_whole_seconds( chrono::Local::now().offset().local_minus_utc(), ) .unwrap(), history: cx.new_view(|cx| ThreadHistory::new(weak_self, thread_store, cx)), + new_item_context_menu_handle: PopoverMenuHandle::default(), width: None, height: None, } @@ -177,6 +210,31 @@ impl AssistantPanel { self.message_editor.focus_handle(cx).focus(cx); } + fn new_prompt_editor(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { + self.active_view = ActiveView::PromptEditor; + + let project = workspace.project().clone(); + let context = self + .context_store + .update(cx, |context_store, cx| context_store.create(cx)); + let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten(); + + self.context_editor = Some(cx.new_view(|cx| { + ContextEditor::for_context( + context, + self.fs.clone(), + self.workspace.clone(), + project, + lsp_adapter_delegate, + cx, + ) + })); + + if let Some(context_editor) = self.context_editor.as_ref() { + context_editor.focus_handle(cx).focus(cx); + } + } + fn open_history(&mut self, cx: &mut ViewContext) { self.active_view = ActiveView::History; self.history.focus_handle(cx).focus(cx); @@ -227,6 +285,7 @@ impl FocusableView for AssistantPanel { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { match self.active_view { ActiveView::Thread => self.message_editor.focus_handle(cx), + ActiveView::PromptEditor => self.context_editor.as_ref().unwrap().focus_handle(cx), ActiveView::History => self.history.focus_handle(cx), } } @@ -314,12 +373,24 @@ impl AssistantPanel { let thread = self.thread.read(cx); - let title = if thread.is_empty() { - thread.summary_or_default(cx) - } else { - thread - .summary(cx) - .unwrap_or_else(|| SharedString::from("Loading Summary…")) + let title = match self.active_view { + ActiveView::Thread => { + if thread.is_empty() { + thread.summary_or_default(cx) + } else { + thread + .summary(cx) + .unwrap_or_else(|| SharedString::from("Loading Summary…")) + } + } + ActiveView::PromptEditor => self + .context_editor + .as_ref() + .map(|context_editor| { + SharedString::from(context_editor.read(cx).title(cx).to_string()) + }) + .unwrap_or_else(|| SharedString::from("Loading Summary…")), + ActiveView::History => "History".into(), }; h_flex() @@ -341,22 +412,20 @@ impl AssistantPanel { .border_color(cx.theme().colors().border) .gap(DynamicSpacing::Base02.rems(cx)) .child( - IconButton::new("new-thread", IconName::Plus) - .icon_size(IconSize::Small) - .style(ButtonStyle::Subtle) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |cx| { - Tooltip::for_action_in( - "New Thread", - &NewThread, - &focus_handle, - cx, - ) - } - }) - .on_click(move |_event, cx| { - cx.dispatch_action(NewThread.boxed_clone()); + PopoverMenu::new("assistant-toolbar-popover-menu") + .trigger( + IconButton::new("new", IconName::Plus) + .icon_size(IconSize::Small) + .style(ButtonStyle::Subtle) + .tooltip(|cx| Tooltip::text("New…", cx)), + ) + .anchor(Corner::TopRight) + .with_handle(self.new_item_context_menu_handle.clone()) + .menu(move |cx| { + Some(ContextMenu::build(cx, |menu, _| { + menu.action("New Thread", NewThread.boxed_clone()) + .action("New Prompt Editor", NewPromptEditor.boxed_clone()) + })) }), ) .child( @@ -635,6 +704,7 @@ impl Render for AssistantPanel { .child(self.message_editor.clone()), ) .children(self.render_last_error(cx)), + ActiveView::PromptEditor => parent.children(self.context_editor.clone()), ActiveView::History => parent.child(self.history.clone()), }) } diff --git a/crates/assistant_context_editor/src/context.rs b/crates/assistant_context_editor/src/context.rs index f2709feb4142854d3b02dc2841c315193aca7268..edd17a5287f01a5a766ea302b1bfdc4f43cee25d 100644 --- a/crates/assistant_context_editor/src/context.rs +++ b/crates/assistant_context_editor/src/context.rs @@ -2304,7 +2304,10 @@ impl Context { let mut request = self.to_completion_request(request_type, cx); - if cx.has_flag::() { + // Don't attach tools for now; we'll be removing tool use from + // Assistant1 shortly. + #[allow(clippy::overly_complex_bool_expr)] + if false && cx.has_flag::() { request.tools = self .tools .tools(cx) diff --git a/crates/assistant_context_editor/src/context_editor.rs b/crates/assistant_context_editor/src/context_editor.rs index 318c72fa7ae0c361a21079bfe64d68eaef8d8e3c..5fc8da16332e8648e16c017e4d49fae6173ce009 100644 --- a/crates/assistant_context_editor/src/context_editor.rs +++ b/crates/assistant_context_editor/src/context_editor.rs @@ -35,6 +35,7 @@ use language_model::{LanguageModelImage, LanguageModelRegistry, LanguageModelToo use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu}; use multi_buffer::MultiBufferRow; use picker::Picker; +use project::lsp_store::LocalLspAdapterDelegate; use project::{Project, Worktree}; use rope::Point; use serde::{Deserialize, Serialize}; @@ -3534,6 +3535,30 @@ pub fn humanize_token_count(count: usize) -> String { } } +pub fn make_lsp_adapter_delegate( + project: &Model, + cx: &mut AppContext, +) -> Result>> { + project.update(cx, |project, cx| { + // TODO: Find the right worktree. + let Some(worktree) = project.worktrees(cx).next() else { + return Ok(None::>); + }; + let http_client = project.client().http_client().clone(); + project.lsp_store().update(cx, |_, cx| { + Ok(Some(LocalLspAdapterDelegate::new( + project.languages().clone(), + project.environment(), + cx.weak_model(), + &worktree, + http_client, + project.fs().clone(), + cx, + ) as Arc)) + }) + }) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index aa4f08ba699ead268159f5147a4509f3e883fd4e..b2394f22d6aef72170f8693fc347a9735238a80a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -362,8 +362,11 @@ fn initialize_panels(prompt_builder: Arc, cx: &mut ViewContext, cx: &mut ViewContext