Break ground on `assistant2` (#21109)

Marshall Bowers created

This PR breaks ground on a new `assistant2` crate.

In order to see this new version of the assistant, both of the following
must be true:
1. The `assistant2` feature flag is enabled for your user
   - It is **not** currently enabled for all staff.
2. You are running a development build of Zed

The intent here is to enable the folks working on `assistant2` to
incrementally land work onto `main` without breaking use of the current
Assistant for anyone.

<img width="1136" alt="Screenshot 2024-11-23 at 10 46 08 AM"
src="https://github.com/user-attachments/assets/5723a13f-5be1-4486-9460-ead7329ba78e">

Release Notes:

- N/A

Change summary

Cargo.lock                                |  14 ++
Cargo.toml                                |   2 
crates/assistant2/Cargo.toml              |  22 ++++
crates/assistant2/LICENSE-GPL             |   1 
crates/assistant2/src/assistant.rs        |  40 ++++++++
crates/assistant2/src/assistant_panel.rs  | 123 +++++++++++++++++++++++++
crates/feature_flags/src/feature_flags.rs |  10 ++
crates/zed/Cargo.toml                     |   1 
crates/zed/src/main.rs                    |   1 
crates/zed/src/zed.rs                     |  34 +++++-
10 files changed, 243 insertions(+), 5 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -449,6 +449,19 @@ dependencies = [
  "zed_actions",
 ]
 
+[[package]]
+name = "assistant2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "command_palette_hooks",
+ "feature_flags",
+ "gpui",
+ "proto",
+ "ui",
+ "workspace",
+]
+
 [[package]]
 name = "assistant_slash_command"
 version = "0.1.0"
@@ -15549,6 +15562,7 @@ dependencies = [
  "ashpd",
  "assets",
  "assistant",
+ "assistant2",
  "async-watch",
  "audio",
  "auto_update",

Cargo.toml 🔗

@@ -5,6 +5,7 @@ members = [
     "crates/anthropic",
     "crates/assets",
     "crates/assistant",
+    "crates/assistant2",
     "crates/assistant_slash_command",
     "crates/assistant_tool",
     "crates/audio",
@@ -186,6 +187,7 @@ ai = { path = "crates/ai" }
 anthropic = { path = "crates/anthropic" }
 assets = { path = "crates/assets" }
 assistant = { path = "crates/assistant" }
+assistant2 = { path = "crates/assistant2" }
 assistant_slash_command = { path = "crates/assistant_slash_command" }
 assistant_tool = { path = "crates/assistant_tool" }
 audio = { path = "crates/audio" }

crates/assistant2/Cargo.toml 🔗

@@ -0,0 +1,22 @@
+[package]
+name = "assistant2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/assistant.rs"
+doctest = false
+
+[dependencies]
+anyhow.workspace = true
+command_palette_hooks.workspace = true
+feature_flags.workspace = true
+gpui.workspace = true
+proto.workspace = true
+ui.workspace = true
+workspace.workspace = true

crates/assistant2/src/assistant.rs 🔗

@@ -0,0 +1,40 @@
+mod assistant_panel;
+
+use command_palette_hooks::CommandPaletteFilter;
+use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
+use gpui::{actions, AppContext};
+
+pub use crate::assistant_panel::AssistantPanel;
+
+actions!(assistant2, [ToggleFocus, NewChat]);
+
+const NAMESPACE: &str = "assistant2";
+
+/// Initializes the `assistant2` crate.
+pub fn init(cx: &mut AppContext) {
+    assistant_panel::init(cx);
+    feature_gate_assistant2_actions(cx);
+}
+
+fn feature_gate_assistant2_actions(cx: &mut AppContext) {
+    const ASSISTANT1_NAMESPACE: &str = "assistant";
+
+    CommandPaletteFilter::update_global(cx, |filter, _cx| {
+        filter.hide_namespace(NAMESPACE);
+    });
+
+    cx.observe_flag::<Assistant2FeatureFlag, _>(move |is_enabled, cx| {
+        if is_enabled {
+            CommandPaletteFilter::update_global(cx, |filter, _cx| {
+                filter.show_namespace(NAMESPACE);
+                filter.hide_namespace(ASSISTANT1_NAMESPACE);
+            });
+        } else {
+            CommandPaletteFilter::update_global(cx, |filter, _cx| {
+                filter.hide_namespace(NAMESPACE);
+                filter.show_namespace(ASSISTANT1_NAMESPACE);
+            });
+        }
+    })
+    .detach();
+}

crates/assistant2/src/assistant_panel.rs 🔗

@@ -0,0 +1,123 @@
+use anyhow::Result;
+use gpui::{
+    prelude::*, px, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle,
+    FocusableView, Pixels, Task, View, ViewContext, WeakView, WindowContext,
+};
+use ui::prelude::*;
+use workspace::dock::{DockPosition, Panel, PanelEvent};
+use workspace::{Pane, Workspace};
+
+use crate::{NewChat, ToggleFocus};
+
+pub fn init(cx: &mut AppContext) {
+    cx.observe_new_views(
+        |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
+            workspace.register_action(|workspace, _: &ToggleFocus, cx| {
+                workspace.toggle_panel_focus::<AssistantPanel>(cx);
+            });
+        },
+    )
+    .detach();
+}
+
+pub struct AssistantPanel {
+    pane: View<Pane>,
+}
+
+impl AssistantPanel {
+    pub fn load(
+        workspace: WeakView<Workspace>,
+        cx: AsyncWindowContext,
+    ) -> Task<Result<View<Self>>> {
+        cx.spawn(|mut cx| async move {
+            workspace.update(&mut cx, |workspace, cx| {
+                cx.new_view(|cx| Self::new(workspace, cx))
+            })
+        })
+    }
+
+    fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
+        let pane = cx.new_view(|cx| {
+            let mut pane = Pane::new(
+                workspace.weak_handle(),
+                workspace.project().clone(),
+                Default::default(),
+                None,
+                NewChat.boxed_clone(),
+                cx,
+            );
+            pane.set_can_split(false, cx);
+            pane.set_can_navigate(true, cx);
+
+            pane
+        });
+
+        Self { pane }
+    }
+}
+
+impl FocusableView for AssistantPanel {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.pane.focus_handle(cx)
+    }
+}
+
+impl EventEmitter<PanelEvent> for AssistantPanel {}
+
+impl Panel for AssistantPanel {
+    fn persistent_name() -> &'static str {
+        "AssistantPanel2"
+    }
+
+    fn position(&self, _cx: &WindowContext) -> DockPosition {
+        DockPosition::Right
+    }
+
+    fn position_is_valid(&self, _: DockPosition) -> bool {
+        true
+    }
+
+    fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
+
+    fn size(&self, _cx: &WindowContext) -> Pixels {
+        px(640.)
+    }
+
+    fn set_size(&mut self, _size: Option<Pixels>, _cx: &mut ViewContext<Self>) {}
+
+    fn is_zoomed(&self, cx: &WindowContext) -> bool {
+        self.pane.read(cx).is_zoomed()
+    }
+
+    fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
+        self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
+    }
+
+    fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
+
+    fn pane(&self) -> Option<View<Pane>> {
+        Some(self.pane.clone())
+    }
+
+    fn remote_id() -> Option<proto::PanelId> {
+        Some(proto::PanelId::AssistantPanel)
+    }
+
+    fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
+        Some(IconName::ZedAssistant)
+    }
+
+    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
+        Some("Assistant Panel")
+    }
+
+    fn toggle_action(&self) -> Box<dyn Action> {
+        Box::new(ToggleFocus)
+    }
+}
+
+impl Render for AssistantPanel {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+        div().child(Label::new("Assistant II"))
+    }
+}

crates/feature_flags/src/feature_flags.rs 🔗

@@ -39,6 +39,16 @@ pub trait FeatureFlag {
     }
 }
 
+pub struct Assistant2FeatureFlag;
+
+impl FeatureFlag for Assistant2FeatureFlag {
+    const NAME: &'static str = "assistant2";
+
+    fn enabled_for_staff() -> bool {
+        false
+    }
+}
+
 pub struct Remoting {}
 impl FeatureFlag for Remoting {
     const NAME: &'static str = "remoting";

crates/zed/Cargo.toml 🔗

@@ -19,6 +19,7 @@ activity_indicator.workspace = true
 anyhow.workspace = true
 assets.workspace = true
 assistant.workspace = true
+assistant2.workspace = true
 async-watch.workspace = true
 audio.workspace = true
 auto_update.workspace = true

crates/zed/src/main.rs 🔗

@@ -406,6 +406,7 @@ fn main() {
             stdout_is_a_pty(),
             cx,
         );
+        assistant2::init(cx);
         assistant_hints::init(cx);
         repl::init(
             app_state.fs.clone(),

crates/zed/src/zed.rs 🔗

@@ -236,10 +236,29 @@ pub fn initialize_workspace(
                 .unwrap_or(true)
         });
 
+        let release_channel = ReleaseChannel::global(cx);
+        let assistant2_feature_flag = cx.wait_for_flag::<feature_flags::Assistant2FeatureFlag>();
+
         let prompt_builder = prompt_builder.clone();
         cx.spawn(|workspace_handle, mut cx| async move {
-            let assistant_panel =
-                assistant::AssistantPanel::load(workspace_handle.clone(), prompt_builder, cx.clone());
+            let is_assistant2_enabled = if cfg!(test) {
+                false
+            } else {
+                let is_assistant2_feature_flag_enabled = assistant2_feature_flag.await;
+                release_channel == ReleaseChannel::Dev && is_assistant2_feature_flag_enabled
+            };
+
+            let (assistant_panel, assistant2_panel) = if is_assistant2_enabled {
+                let assistant2_panel =
+                    assistant2::AssistantPanel::load(workspace_handle.clone(), cx.clone()).await?;
+
+                (None, Some(assistant2_panel))
+            } else {
+                let assistant_panel =
+                    assistant::AssistantPanel::load(workspace_handle.clone(), prompt_builder, cx.clone()).await?;
+
+                (Some(assistant_panel), None)
+            };
 
             let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
             let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
@@ -257,7 +276,6 @@ pub fn initialize_workspace(
                 project_panel,
                 outline_panel,
                 terminal_panel,
-                assistant_panel,
                 channels_panel,
                 chat_panel,
                 notification_panel,
@@ -265,14 +283,20 @@ pub fn initialize_workspace(
                 project_panel,
                 outline_panel,
                 terminal_panel,
-                assistant_panel,
                 channels_panel,
                 chat_panel,
                 notification_panel,
             )?;
 
             workspace_handle.update(&mut cx, |workspace, cx| {
-                workspace.add_panel(assistant_panel, cx);
+                if let Some(assistant_panel) = assistant_panel {
+                    workspace.add_panel(assistant_panel, cx);
+                }
+
+                if let Some(assistant2_panel) = assistant2_panel {
+                    workspace.add_panel(assistant2_panel, cx);
+                }
+
                 workspace.add_panel(project_panel, cx);
                 workspace.add_panel(outline_panel, cx);
                 workspace.add_panel(terminal_panel, cx);