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",
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
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(-)
@@ -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",
@@ -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",
@@ -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>))
- })
- })
-}
@@ -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
@@ -36,6 +36,7 @@ actions!(
[
ToggleFocus,
NewThread,
+ NewPromptEditor,
ToggleContextPicker,
ToggleModelSelector,
RemoveAllContext,
@@ -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()),
})
}
@@ -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)
@@ -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::*;
@@ -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
};