assistant2: Add prompt editor (#23436)

Marshall Bowers created

This PR adds the Assistant1 experience to Assistant2 as a "prompt
editor".

<img width="1309" alt="Screenshot 2025-01-21 at 7 17 26 PM"
src="https://github.com/user-attachments/assets/3ce2f32b-2b1a-48a8-8e56-4c44e3ac4ce5"
/>

Release Notes:

- N/A

Change summary

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 ++++++++++--
crates/assistant_context_editor/src/context.rs        |   5 
crates/assistant_context_editor/src/context_editor.rs |  25 ++
crates/zed/src/zed.rs                                 |  16 +
9 files changed, 149 insertions(+), 59 deletions(-)

Detailed changes

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",

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",

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<Project>,
-    cx: &mut AppContext,
-) -> Result<Option<Arc<dyn LspAdapterDelegate>>> {
-    project.update(cx, |project, cx| {
-        // TODO: Find the right worktree.
-        let Some(worktree) = project.worktrees(cx).next() else {
-            return Ok(None::<Arc<dyn LspAdapterDelegate>>);
-        };
-        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<dyn LspAdapterDelegate>))
-        })
-    })
-}

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

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::<AssistantPanel>(cx);
                         panel.update(cx, |panel, cx| panel.open_history(cx));
                     }
+                })
+                .register_action(|workspace, _: &NewPromptEditor, cx| {
+                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
+                        workspace.focus_panel::<AssistantPanel>(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<ThreadStore>,
     thread: View<ActiveThread>,
     message_editor: View<MessageEditor>,
+    context_store: Model<assistant_context_editor::ContextStore>,
+    context_editor: Option<View<ContextEditor>>,
     tools: Arc<ToolWorkingSet>,
     local_timezone: UtcOffset,
     active_view: ActiveView,
     history: View<ThreadHistory>,
+    new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
     width: Option<Pixels>,
     height: Option<Pixels>,
 }
@@ -71,6 +85,7 @@ pub struct AssistantPanel {
 impl AssistantPanel {
     pub fn load(
         workspace: WeakView<Workspace>,
+        prompt_builder: Arc<PromptBuilder>,
         cx: AsyncWindowContext,
     ) -> Task<Result<View<Self>>> {
         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<ThreadStore>,
+        context_store: Model<assistant_context_editor::ContextStore>,
         tools: Arc<ToolWorkingSet>,
         cx: &mut ViewContext<Self>,
     ) -> 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>) {
+        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>) {
         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()),
             })
     }

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::<ToolUseFeatureFlag>() {
+        // 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::<ToolUseFeatureFlag>() {
             request.tools = self
                 .tools
                 .tools(cx)

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<Project>,
+    cx: &mut AppContext,
+) -> Result<Option<Arc<dyn LspAdapterDelegate>>> {
+    project.update(cx, |project, cx| {
+        // TODO: Find the right worktree.
+        let Some(worktree) = project.worktrees(cx).next() else {
+            return Ok(None::<Arc<dyn LspAdapterDelegate>>);
+        };
+        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<dyn LspAdapterDelegate>))
+        })
+    })
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

crates/zed/src/zed.rs 🔗

@@ -362,8 +362,11 @@ fn initialize_panels(prompt_builder: Arc<PromptBuilder>, cx: &mut ViewContext<Wo
             workspace_handle.clone(),
             cx.clone(),
         );
-        let assistant_panel =
-            assistant::AssistantPanel::load(workspace_handle.clone(), prompt_builder, cx.clone());
+        let assistant_panel = assistant::AssistantPanel::load(
+            workspace_handle.clone(),
+            prompt_builder.clone(),
+            cx.clone(),
+        );
 
         let (
             project_panel,
@@ -427,7 +430,14 @@ fn initialize_panels(prompt_builder: Arc<PromptBuilder>, cx: &mut ViewContext<Wo
             }
         };
         let assistant2_panel = if is_assistant2_enabled {
-            Some(assistant2::AssistantPanel::load(workspace_handle.clone(), cx.clone()).await?)
+            Some(
+                assistant2::AssistantPanel::load(
+                    workspace_handle.clone(),
+                    prompt_builder,
+                    cx.clone(),
+                )
+                .await?,
+            )
         } else {
             None
         };