Extract `ContextEditor` to `assistant_context_editor` (#23433)

Marshall Bowers created

This PR extracts the `ContextEditor` to the `assistant_context_editor`
crate.

As part of this, we have decoupled the `ContextEditor` from the
`AssistantPanel`.

There is now an `AssistantPanelDelegate` that the `ContextEditor` uses
when it needs to interface with the Assistant panel.

Release Notes:

- N/A

Change summary

Cargo.lock                                                      |  10 
crates/assistant/Cargo.toml                                     |   1 
crates/assistant/src/assistant.rs                               |  40 
crates/assistant/src/assistant_panel.rs                         |  95 
crates/assistant/src/context_history.rs                         |   5 
crates/assistant/src/inline_assistant.rs                        |   5 
crates/assistant/src/terminal_inline_assistant.rs               |   4 
crates/assistant_context_editor/Cargo.toml                      |  14 
crates/assistant_context_editor/src/assistant_context_editor.rs |   5 
crates/assistant_context_editor/src/context_editor.rs           | 256 +-
crates/assistant_context_editor/src/slash_command.rs            |   2 
crates/assistant_context_editor/src/slash_command_picker.rs     |   0 
12 files changed, 267 insertions(+), 170 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -389,7 +389,6 @@ dependencies = [
  "feature_flags",
  "fs",
  "futures 0.3.31",
- "fuzzy",
  "gpui",
  "indexed_docs",
  "indoc",
@@ -502,6 +501,7 @@ name = "assistant_context_editor"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "assistant_settings",
  "assistant_slash_command",
  "assistant_slash_commands",
  "assistant_tool",
@@ -516,18 +516,24 @@ dependencies = [
  "futures 0.3.31",
  "fuzzy",
  "gpui",
+ "indexed_docs",
  "language",
  "language_model",
+ "language_model_selector",
  "language_models",
+ "languages",
  "log",
+ "multi_buffer",
  "open_ai",
  "parking_lot",
  "paths",
+ "picker",
  "pretty_assertions",
  "project",
  "prompt_library",
  "rand 0.8.5",
  "regex",
+ "rope",
  "rpc",
  "serde",
  "serde_json",
@@ -537,6 +543,8 @@ dependencies = [
  "strum",
  "telemetry_events",
  "text",
+ "theme",
+ "tree-sitter-md",
  "ui",
  "unindent",
  "util",

crates/assistant/Cargo.toml 🔗

@@ -37,7 +37,6 @@ editor.workspace = true
 feature_flags.workspace = true
 fs.workspace = true
 futures.workspace = true
-fuzzy.workspace = true
 gpui.workspace = true
 indexed_docs.workspace = true
 indoc.workspace = true

crates/assistant/src/assistant.rs 🔗

@@ -1,15 +1,11 @@
 #![cfg_attr(target_os = "windows", allow(unused, dead_code))]
 
 pub mod assistant_panel;
-mod context_editor;
 mod context_history;
 mod inline_assistant;
-mod slash_command;
-pub(crate) mod slash_command_picker;
 pub mod slash_command_settings;
 mod terminal_inline_assistant;
 
-use std::path::PathBuf;
 use std::sync::Arc;
 
 use assistant_settings::AssistantSettings;
@@ -19,7 +15,6 @@ use client::Client;
 use command_palette_hooks::CommandPaletteFilter;
 use feature_flags::FeatureFlagAppExt;
 use fs::Fs;
-use gpui::impl_internal_actions;
 use gpui::{actions, AppContext, Global, UpdateGlobal};
 use language_model::{
     LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
@@ -37,33 +32,16 @@ use crate::slash_command_settings::SlashCommandSettings;
 actions!(
     assistant,
     [
-        Assist,
-        Edit,
-        Split,
-        CopyCode,
-        CycleMessageRole,
-        QuoteSelection,
-        InsertIntoEditor,
         ToggleFocus,
         InsertActivePrompt,
         DeployHistory,
         DeployPromptLibrary,
-        ConfirmCommand,
         NewContext,
-        ToggleModelSelector,
         CycleNextInlineAssist,
         CyclePreviousInlineAssist
     ]
 );
 
-#[derive(PartialEq, Clone)]
-pub enum InsertDraggedFiles {
-    ProjectPaths(Vec<PathBuf>),
-    ExternalFiles(Vec<PathBuf>),
-}
-
-impl_internal_actions!(assistant, [InsertDraggedFiles]);
-
 const DEFAULT_CONTEXT_LINES: usize = 50;
 
 #[derive(Deserialize, Debug)]
@@ -334,24 +312,6 @@ fn update_slash_commands_from_settings(cx: &mut AppContext) {
     }
 }
 
-pub fn humanize_token_count(count: usize) -> String {
-    match count {
-        0..=999 => count.to_string(),
-        1000..=9999 => {
-            let thousands = count / 1000;
-            let hundreds = (count % 1000 + 50) / 100;
-            if hundreds == 0 {
-                format!("{}k", thousands)
-            } else if hundreds == 10 {
-                format!("{}k", thousands + 1)
-            } else {
-                format!("{}.{}k", thousands, hundreds)
-            }
-        }
-        _ => format!("{}k", (count + 500) / 1000),
-    }
-}
-
 #[cfg(test)]
 #[ctor::ctor]
 fn init_logger() {

crates/assistant/src/assistant_panel.rs 🔗

@@ -1,14 +1,14 @@
-use crate::context_editor::{
-    ContextEditor, ContextEditorToolbarItem, ContextEditorToolbarItemEvent, DEFAULT_TAB_TITLE,
-};
 use crate::context_history::ContextHistory;
 use crate::{
-    slash_command::SlashCommandCompletionProvider,
     terminal_inline_assistant::TerminalInlineAssistant, DeployHistory, DeployPromptLibrary,
-    InlineAssistant, InsertDraggedFiles, NewContext, ToggleFocus, ToggleModelSelector,
+    InlineAssistant, NewContext, ToggleFocus,
+};
+use anyhow::{anyhow, Result};
+use assistant_context_editor::{
+    AssistantPanelDelegate, Context, ContextEditor, ContextEditorToolbarItem,
+    ContextEditorToolbarItemEvent, ContextId, ContextStore, ContextStoreEvent, InsertDraggedFiles,
+    SlashCommandCompletionProvider, ToggleModelSelector, DEFAULT_TAB_TITLE,
 };
-use anyhow::Result;
-use assistant_context_editor::{Context, ContextId, ContextStore, ContextStoreEvent};
 use assistant_settings::{AssistantDockPosition, AssistantSettings};
 use assistant_slash_command::SlashCommandWorkingSet;
 use assistant_tool::ToolWorkingSet;
@@ -46,6 +46,8 @@ use workspace::{
 use zed_actions::InlineAssist;
 
 pub fn init(cx: &mut AppContext) {
+    <dyn AssistantPanelDelegate>::set_global(Arc::new(ConcreteAssistantPanelDelegate), cx);
+
     workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
     cx.observe_new_views(
         |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
@@ -438,7 +440,7 @@ impl AssistantPanel {
             if let Some(context_editor) = self.active_context_editor(cx) {
                 let new_summary = model_summary_editor.read(cx).text(cx);
                 context_editor.update(cx, |context_editor, cx| {
-                    context_editor.context.update(cx, |context, cx| {
+                    context_editor.context().update(cx, |context, cx| {
                         if context.summary().is_none()
                             && (new_summary == DEFAULT_TAB_TITLE || new_summary.trim().is_empty())
                         {
@@ -475,7 +477,7 @@ impl AssistantPanel {
     ) {
         if let Some(context_editor) = self.active_context_editor(cx) {
             context_editor.update(cx, |context_editor, cx| {
-                context_editor.context.update(cx, |context, cx| {
+                context_editor.context().update(cx, |context, cx| {
                     context.summarize(true, cx);
                 })
             })
@@ -501,7 +503,6 @@ impl AssistantPanel {
             .log_err()
             .flatten();
 
-        let assistant_panel = cx.view().downgrade();
         let editor = cx.new_view(|cx| {
             let mut editor = ContextEditor::for_context(
                 context,
@@ -509,7 +510,6 @@ impl AssistantPanel {
                 self.workspace.clone(),
                 self.project.clone(),
                 lsp_adapter_delegate,
-                assistant_panel,
                 cx,
             );
             editor.insert_default_prompt(cx);
@@ -523,7 +523,7 @@ impl AssistantPanel {
         if let Some(editor) = self.active_context_editor(cx) {
             editor.update(cx, |active_context, cx| {
                 active_context
-                    .context
+                    .context()
                     .update(cx, |context, cx| context.completion_provider_changed(cx))
             })
         }
@@ -716,7 +716,7 @@ impl AssistantPanel {
                 .read(cx)
                 .active_context_editor(cx)
                 .and_then(|editor| {
-                    let editor = &editor.read(cx).editor;
+                    let editor = &editor.read(cx).editor().clone();
                     if editor.read(cx).is_focused(cx) {
                         Some(editor.clone())
                     } else {
@@ -778,7 +778,6 @@ impl AssistantPanel {
 
                     let fs = this.fs.clone();
                     let project = this.project.clone();
-                    let weak_assistant_panel = cx.view().downgrade();
 
                     let editor = cx.new_view(|cx| {
                         ContextEditor::for_context(
@@ -787,7 +786,6 @@ impl AssistantPanel {
                             workspace,
                             project,
                             lsp_adapter_delegate,
-                            weak_assistant_panel,
                             cx,
                         )
                     });
@@ -808,7 +806,6 @@ impl AssistantPanel {
                 .log_err()
                 .flatten();
 
-            let assistant_panel = cx.view().downgrade();
             let editor = cx.new_view(|cx| {
                 let mut editor = ContextEditor::for_context(
                     context,
@@ -816,7 +813,6 @@ impl AssistantPanel {
                     self.workspace.clone(),
                     self.project.clone(),
                     lsp_adapter_delegate,
-                    assistant_panel,
                     cx,
                 );
                 editor.insert_default_prompt(cx);
@@ -1013,7 +1009,7 @@ impl AssistantPanel {
     }
 
     pub fn active_context(&self, cx: &AppContext) -> Option<Model<Context>> {
-        Some(self.active_context_editor(cx)?.read(cx).context.clone())
+        Some(self.active_context_editor(cx)?.read(cx).context().clone())
     }
 
     pub fn open_saved_context(
@@ -1023,7 +1019,7 @@ impl AssistantPanel {
     ) -> Task<Result<()>> {
         let existing_context = self.pane.read(cx).items().find_map(|item| {
             item.downcast::<ContextEditor>()
-                .filter(|editor| editor.read(cx).context.read(cx).path() == Some(&path))
+                .filter(|editor| editor.read(cx).context().read(cx).path() == Some(&path))
         });
         if let Some(existing_context) = existing_context {
             return cx.spawn(|this, mut cx| async move {
@@ -1042,7 +1038,6 @@ impl AssistantPanel {
 
         cx.spawn(|this, mut cx| async move {
             let context = context.await?;
-            let assistant_panel = this.clone();
             this.update(&mut cx, |this, cx| {
                 let editor = cx.new_view(|cx| {
                     ContextEditor::for_context(
@@ -1051,7 +1046,6 @@ impl AssistantPanel {
                         workspace,
                         project,
                         lsp_adapter_delegate,
-                        assistant_panel,
                         cx,
                     )
                 });
@@ -1069,7 +1063,7 @@ impl AssistantPanel {
     ) -> Task<Result<View<ContextEditor>>> {
         let existing_context = self.pane.read(cx).items().find_map(|item| {
             item.downcast::<ContextEditor>()
-                .filter(|editor| *editor.read(cx).context.read(cx).id() == id)
+                .filter(|editor| *editor.read(cx).context().read(cx).id() == id)
         });
         if let Some(existing_context) = existing_context {
             return cx.spawn(|this, mut cx| async move {
@@ -1091,7 +1085,6 @@ impl AssistantPanel {
 
         cx.spawn(|this, mut cx| async move {
             let context = context.await?;
-            let assistant_panel = this.clone();
             this.update(&mut cx, |this, cx| {
                 let editor = cx.new_view(|cx| {
                     ContextEditor::for_context(
@@ -1100,7 +1093,6 @@ impl AssistantPanel {
                         workspace,
                         this.project.clone(),
                         lsp_adapter_delegate,
-                        assistant_panel,
                         cx,
                     )
                 });
@@ -1304,6 +1296,61 @@ impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
     }
 }
 
+struct ConcreteAssistantPanelDelegate;
+
+impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
+    fn active_context_editor(
+        &self,
+        workspace: &mut Workspace,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Option<View<ContextEditor>> {
+        let panel = workspace.panel::<AssistantPanel>(cx)?;
+        panel.read(cx).active_context_editor(cx)
+    }
+
+    fn open_remote_context(
+        &self,
+        workspace: &mut Workspace,
+        context_id: ContextId,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Task<Result<View<ContextEditor>>> {
+        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
+            return Task::ready(Err(anyhow!("no Assistant panel found")));
+        };
+
+        panel.update(cx, |panel, cx| panel.open_remote_context(context_id, cx))
+    }
+
+    fn quote_selection(
+        &self,
+        workspace: &mut Workspace,
+        creases: Vec<(String, String)>,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
+            return;
+        };
+
+        // Activate the panel
+        if !panel.focus_handle(cx).contains_focused(cx) {
+            workspace.toggle_panel_focus::<AssistantPanel>(cx);
+        }
+
+        panel.update(cx, |_, cx| {
+            // Wait to create a new context until the workspace is no longer
+            // being updated.
+            cx.defer(move |panel, cx| {
+                if let Some(context) = panel
+                    .active_context_editor(cx)
+                    .or_else(|| panel.new_context(cx))
+                {
+                    context.update(cx, |context, cx| context.quote_creases(creases, cx));
+                };
+            });
+        });
+    }
+}
+
 #[derive(Debug, PartialEq, Eq, Clone, Copy)]
 pub enum WorkflowAssistStatus {
     Pending,

crates/assistant/src/context_history.rs 🔗

@@ -1,6 +1,8 @@
 use std::sync::Arc;
 
-use assistant_context_editor::{ContextStore, RemoteContextMetadata, SavedContextMetadata};
+use assistant_context_editor::{
+    ContextStore, RemoteContextMetadata, SavedContextMetadata, DEFAULT_TAB_TITLE,
+};
 use gpui::{
     AppContext, EventEmitter, FocusHandle, FocusableView, Model, Subscription, Task, View, WeakView,
 };
@@ -10,7 +12,6 @@ use ui::utils::{format_distance_from_now, DateTimeType};
 use ui::{prelude::*, Avatar, ListItem, ListItemSpacing};
 use workspace::Item;
 
-use crate::context_editor::DEFAULT_TAB_TITLE;
 use crate::AssistantPanel;
 
 #[derive(Clone)]

crates/assistant/src/inline_assistant.rs 🔗

@@ -1,9 +1,8 @@
 use crate::{
-    humanize_token_count, AssistantPanel, AssistantPanelEvent, CycleNextInlineAssist,
-    CyclePreviousInlineAssist,
+    AssistantPanel, AssistantPanelEvent, CycleNextInlineAssist, CyclePreviousInlineAssist,
 };
 use anyhow::{anyhow, Context as _, Result};
-use assistant_context_editor::RequestType;
+use assistant_context_editor::{humanize_token_count, RequestType};
 use assistant_settings::AssistantSettings;
 use client::{telemetry::Telemetry, ErrorExt};
 use collections::{hash_map, HashMap, HashSet, VecDeque};

crates/assistant/src/terminal_inline_assistant.rs 🔗

@@ -1,6 +1,6 @@
-use crate::{humanize_token_count, AssistantPanel, AssistantPanelEvent, DEFAULT_CONTEXT_LINES};
+use crate::{AssistantPanel, AssistantPanelEvent, DEFAULT_CONTEXT_LINES};
 use anyhow::{Context as _, Result};
-use assistant_context_editor::RequestType;
+use assistant_context_editor::{humanize_token_count, RequestType};
 use assistant_settings::AssistantSettings;
 use client::telemetry::Telemetry;
 use collections::{HashMap, VecDeque};

crates/assistant_context_editor/Cargo.toml 🔗

@@ -13,6 +13,7 @@ path = "src/assistant_context_editor.rs"
 
 [dependencies]
 anyhow.workspace = true
+assistant_settings.workspace = true
 assistant_slash_command.workspace = true
 assistant_slash_commands.workspace = true
 assistant_tool.workspace = true
@@ -27,32 +28,41 @@ fs.workspace = true
 futures.workspace = true
 fuzzy.workspace = true
 gpui.workspace = true
+indexed_docs.workspace = true
 language.workspace = true
 language_model.workspace = true
+language_model_selector.workspace = true
 language_models.workspace = true
 log.workspace = true
+multi_buffer.workspace = true
 open_ai.workspace = true
+parking_lot.workspace = true
 paths.workspace = true
+picker.workspace = true
 project.workspace = true
 prompt_library.workspace = true
 regex.workspace = true
+rope.workspace = true
 rpc.workspace = true
 serde.workspace = true
 serde_json.workspace = true
+settings.workspace = true
 smallvec.workspace = true
 smol.workspace = true
 strum.workspace = true
 telemetry_events.workspace = true
 text.workspace = true
+theme.workspace = true
 ui.workspace = true
 util.workspace = true
 uuid.workspace = true
+workspace.workspace = true
 
 [dev-dependencies]
 language_model = { workspace = true, features = ["test-support"] }
-parking_lot.workspace = true
+languages = { workspace = true, features = ["test-support"] }
 pretty_assertions.workspace = true
 rand.workspace = true
-settings.workspace = true
+tree-sitter-md.workspace = true
 unindent.workspace = true
 workspace = { workspace = true, features = ["test-support"] }

crates/assistant_context_editor/src/assistant_context_editor.rs 🔗

@@ -1,6 +1,9 @@
 mod context;
+mod context_editor;
 mod context_store;
 mod patch;
+mod slash_command;
+mod slash_command_picker;
 
 use std::sync::Arc;
 
@@ -8,8 +11,10 @@ use client::Client;
 use gpui::AppContext;
 
 pub use crate::context::*;
+pub use crate::context_editor::*;
 pub use crate::context_store::*;
 pub use crate::patch::*;
+pub use crate::slash_command::*;
 
 pub fn init(client: Arc<Client>, _cx: &mut AppContext) {
     context_store::init(&client.into());

crates/assistant/src/context_editor.rs → crates/assistant_context_editor/src/context_editor.rs 🔗

@@ -1,9 +1,4 @@
 use anyhow::Result;
-use assistant_context_editor::{
-    AssistantPatch, AssistantPatchStatus, CacheStatus, Content, Context, ContextEvent, ContextId,
-    InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId, MessageMetadata,
-    MessageStatus, ParsedSlashCommand, PendingSlashCommandStatus, RequestType,
-};
 use assistant_settings::AssistantSettings;
 use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
 use assistant_slash_commands::{
@@ -27,12 +22,12 @@ use editor::{display_map::CreaseId, FoldPlaceholder};
 use fs::Fs;
 use futures::FutureExt;
 use gpui::{
-    div, img, percentage, point, prelude::*, pulsating_between, size, Animation, AnimationExt,
-    AnyElement, AnyView, AppContext, AsyncWindowContext, ClipboardEntry, ClipboardItem,
-    CursorStyle, Empty, Entity, EventEmitter, FocusHandle, FocusableView, FontWeight,
-    InteractiveElement, IntoElement, Model, ParentElement, Pixels, Render, RenderImage,
-    SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
-    View, WeakModel, WeakView,
+    actions, div, img, impl_internal_actions, percentage, point, prelude::*, pulsating_between,
+    size, Animation, AnimationExt, AnyElement, AnyView, AppContext, AsyncWindowContext,
+    ClipboardEntry, ClipboardItem, CursorStyle, Empty, Entity, EventEmitter, FocusHandle,
+    FocusableView, FontWeight, Global, InteractiveElement, IntoElement, Model, ParentElement,
+    Pixels, Render, RenderImage, SharedString, Size, StatefulInteractiveElement, Styled,
+    Subscription, Task, Transformation, View, WeakModel, WeakView,
 };
 use indexed_docs::IndexedDocsStore;
 use language::{language_settings::SoftWrap, BufferSnapshot, LspAdapterDelegate, ToOffset};
@@ -61,10 +56,34 @@ use workspace::{
     Workspace,
 };
 
+actions!(
+    assistant,
+    [
+        Assist,
+        ConfirmCommand,
+        CopyCode,
+        CycleMessageRole,
+        Edit,
+        InsertIntoEditor,
+        QuoteSelection,
+        Split,
+        ToggleModelSelector,
+    ]
+);
+
+#[derive(PartialEq, Clone)]
+pub enum InsertDraggedFiles {
+    ProjectPaths(Vec<PathBuf>),
+    ExternalFiles(Vec<PathBuf>),
+}
+
+impl_internal_actions!(assistant, [InsertDraggedFiles]);
+
+use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker};
 use crate::{
-    humanize_token_count, slash_command::SlashCommandCompletionProvider, slash_command_picker,
-    Assist, AssistantPanel, ConfirmCommand, CopyCode, CycleMessageRole, Edit, InsertDraggedFiles,
-    InsertIntoEditor, QuoteSelection, Split, ToggleModelSelector,
+    AssistantPatch, AssistantPatchStatus, CacheStatus, Content, Context, ContextEvent, ContextId,
+    InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId, MessageMetadata,
+    MessageStatus, ParsedSlashCommand, PendingSlashCommandStatus, RequestType,
 };
 
 #[derive(Copy, Clone, Debug, PartialEq)]
@@ -94,15 +113,54 @@ enum AssistError {
     Message(SharedString),
 }
 
+pub trait AssistantPanelDelegate {
+    fn active_context_editor(
+        &self,
+        workspace: &mut Workspace,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Option<View<ContextEditor>>;
+
+    fn open_remote_context(
+        &self,
+        workspace: &mut Workspace,
+        context_id: ContextId,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Task<Result<View<ContextEditor>>>;
+
+    fn quote_selection(
+        &self,
+        workspace: &mut Workspace,
+        creases: Vec<(String, String)>,
+        cx: &mut ViewContext<Workspace>,
+    );
+}
+
+impl dyn AssistantPanelDelegate {
+    /// Returns the global [`AssistantPanelDelegate`], if it exists.
+    pub fn try_global(cx: &AppContext) -> Option<Arc<Self>> {
+        cx.try_global::<GlobalAssistantPanelDelegate>()
+            .map(|global| global.0.clone())
+    }
+
+    /// Sets the global [`AssistantPanelDelegate`].
+    pub fn set_global(delegate: Arc<Self>, cx: &mut AppContext) {
+        cx.set_global(GlobalAssistantPanelDelegate(delegate));
+    }
+}
+
+struct GlobalAssistantPanelDelegate(Arc<dyn AssistantPanelDelegate>);
+
+impl Global for GlobalAssistantPanelDelegate {}
+
 pub struct ContextEditor {
-    pub(crate) context: Model<Context>,
+    context: Model<Context>,
     fs: Arc<dyn Fs>,
     slash_commands: Arc<SlashCommandWorkingSet>,
     tools: Arc<ToolWorkingSet>,
     workspace: WeakView<Workspace>,
     project: Model<Project>,
     lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
-    pub(crate) editor: View<Editor>,
+    editor: View<Editor>,
     blocks: HashMap<MessageId, (MessageHeader, CustomBlockId)>,
     image_blocks: HashSet<CustomBlockId>,
     scroll_position: Option<ScrollPosition>,
@@ -113,7 +171,6 @@ pub struct ContextEditor {
     _subscriptions: Vec<Subscription>,
     patches: HashMap<Range<language::Anchor>, PatchViewState>,
     active_patch: Option<Range<language::Anchor>>,
-    assistant_panel: WeakView<AssistantPanel>,
     last_error: Option<AssistError>,
     show_accept_terms: bool,
     pub(crate) slash_menu_handle:
@@ -130,13 +187,12 @@ pub const DEFAULT_TAB_TITLE: &str = "New Chat";
 const MAX_TAB_TITLE_LEN: usize = 16;
 
 impl ContextEditor {
-    pub(crate) fn for_context(
+    pub fn for_context(
         context: Model<Context>,
         fs: Arc<dyn Fs>,
         workspace: WeakView<Workspace>,
         project: Model<Project>,
         lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
-        assistant_panel: WeakView<AssistantPanel>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let completion_provider = SlashCommandCompletionProvider::new(
@@ -190,7 +246,6 @@ impl ContextEditor {
             _subscriptions,
             patches: HashMap::default(),
             active_patch: None,
-            assistant_panel,
             last_error: None,
             show_accept_terms: false,
             slash_menu_handle: Default::default(),
@@ -203,6 +258,14 @@ impl ContextEditor {
         this
     }
 
+    pub fn context(&self) -> &Model<Context> {
+        &self.context
+    }
+
+    pub fn editor(&self) -> &View<Editor> {
+        &self.editor
+    }
+
     pub fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) {
         let command_name = DefaultSlashCommand.name();
         self.editor.update(cx, |editor, cx| {
@@ -1523,10 +1586,12 @@ impl ContextEditor {
         _: &InsertIntoEditor,
         cx: &mut ViewContext<Workspace>,
     ) {
-        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
+        let Some(assistant_panel_delegate) = <dyn AssistantPanelDelegate>::try_global(cx) else {
             return;
         };
-        let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else {
+        let Some(context_editor_view) =
+            assistant_panel_delegate.active_context_editor(workspace, cx)
+        else {
             return;
         };
         let Some(active_editor_view) = workspace
@@ -1546,8 +1611,9 @@ impl ContextEditor {
 
     pub fn copy_code(workspace: &mut Workspace, _: &CopyCode, cx: &mut ViewContext<Workspace>) {
         let result = maybe!({
-            let panel = workspace.panel::<AssistantPanel>(cx)?;
-            let context_editor_view = panel.read(cx).active_context_editor(cx)?;
+            let assistant_panel_delegate = <dyn AssistantPanelDelegate>::try_global(cx)?;
+            let context_editor_view =
+                assistant_panel_delegate.active_context_editor(workspace, cx)?;
             Self::get_selection_or_code_block(&context_editor_view, cx)
         });
         let Some((text, is_code_block)) = result else {
@@ -1579,10 +1645,12 @@ impl ContextEditor {
         action: &InsertDraggedFiles,
         cx: &mut ViewContext<Workspace>,
     ) {
-        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
+        let Some(assistant_panel_delegate) = <dyn AssistantPanelDelegate>::try_global(cx) else {
             return;
         };
-        let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else {
+        let Some(context_editor_view) =
+            assistant_panel_delegate.active_context_editor(workspace, cx)
+        else {
             return;
         };
 
@@ -1653,7 +1721,7 @@ impl ContextEditor {
         _: &QuoteSelection,
         cx: &mut ViewContext<Workspace>,
     ) {
-        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
+        let Some(assistant_panel_delegate) = <dyn AssistantPanelDelegate>::try_global(cx) else {
             return;
         };
 
@@ -1664,61 +1732,46 @@ impl ContextEditor {
         if creases.is_empty() {
             return;
         }
-        // Activate the panel
-        if !panel.focus_handle(cx).contains_focused(cx) {
-            workspace.toggle_panel_focus::<AssistantPanel>(cx);
-        }
 
-        panel.update(cx, |_, cx| {
-            // Wait to create a new context until the workspace is no longer
-            // being updated.
-            cx.defer(move |panel, cx| {
-                if let Some(context) = panel
-                    .active_context_editor(cx)
-                    .or_else(|| panel.new_context(cx))
-                {
-                    context.update(cx, |context, cx| {
-                        context.editor.update(cx, |editor, cx| {
-                            editor.insert("\n", cx);
-                            for (text, crease_title) in creases {
-                                let point = editor.selections.newest::<Point>(cx).head();
-                                let start_row = MultiBufferRow(point.row);
-
-                                editor.insert(&text, cx);
-
-                                let snapshot = editor.buffer().read(cx).snapshot(cx);
-                                let anchor_before = snapshot.anchor_after(point);
-                                let anchor_after = editor
-                                    .selections
-                                    .newest_anchor()
-                                    .head()
-                                    .bias_left(&snapshot);
-
-                                editor.insert("\n", cx);
-
-                                let fold_placeholder = quote_selection_fold_placeholder(
-                                    crease_title,
-                                    cx.view().downgrade(),
-                                );
-                                let crease = Crease::inline(
-                                    anchor_before..anchor_after,
-                                    fold_placeholder,
-                                    render_quote_selection_output_toggle,
-                                    |_, _, _| Empty.into_any(),
-                                );
-                                editor.insert_creases(vec![crease], cx);
-                                editor.fold_at(
-                                    &FoldAt {
-                                        buffer_row: start_row,
-                                    },
-                                    cx,
-                                );
-                            }
-                        })
-                    });
-                };
-            });
-        });
+        assistant_panel_delegate.quote_selection(workspace, creases, cx);
+    }
+
+    pub fn quote_creases(&mut self, creases: Vec<(String, String)>, cx: &mut ViewContext<Self>) {
+        self.editor.update(cx, |editor, cx| {
+            editor.insert("\n", cx);
+            for (text, crease_title) in creases {
+                let point = editor.selections.newest::<Point>(cx).head();
+                let start_row = MultiBufferRow(point.row);
+
+                editor.insert(&text, cx);
+
+                let snapshot = editor.buffer().read(cx).snapshot(cx);
+                let anchor_before = snapshot.anchor_after(point);
+                let anchor_after = editor
+                    .selections
+                    .newest_anchor()
+                    .head()
+                    .bias_left(&snapshot);
+
+                editor.insert("\n", cx);
+
+                let fold_placeholder =
+                    quote_selection_fold_placeholder(crease_title, cx.view().downgrade());
+                let crease = Crease::inline(
+                    anchor_before..anchor_after,
+                    fold_placeholder,
+                    render_quote_selection_output_toggle,
+                    |_, _, _| Empty.into_any(),
+                );
+                editor.insert_creases(vec![crease], cx);
+                editor.fold_at(
+                    &FoldAt {
+                        buffer_row: start_row,
+                    },
+                    cx,
+                );
+            }
+        })
     }
 
     fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
@@ -2154,10 +2207,10 @@ impl ContextEditor {
     }
 
     fn render_notice(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
-        use feature_flags::FeatureFlagAppExt;
-        let nudge = self.assistant_panel.upgrade().map(|assistant_panel| {
-            assistant_panel.read(cx).show_zed_ai_notice && cx.has_flag::<feature_flags::ZedPro>()
-        });
+        // This was previously gated behind the `zed-pro` feature flag. Since we
+        // aren't planning to ship that right now, we're just hard-coding this
+        // value to not show the nudge.
+        let nudge = Some(false);
 
         if nudge.map_or(false, |value| value) {
             Some(
@@ -3039,18 +3092,15 @@ impl FollowableItem for ContextEditor {
         let context_id = ContextId::from_proto(state.context_id);
         let editor_state = state.editor?;
 
-        let (project, panel) = workspace.update(cx, |workspace, cx| {
-            Some((
-                workspace.project().clone(),
-                workspace.panel::<AssistantPanel>(cx)?,
-            ))
-        })?;
+        let project = workspace.read(cx).project().clone();
+        let assistant_panel_delegate = <dyn AssistantPanelDelegate>::try_global(cx)?;
 
-        let context_editor =
-            panel.update(cx, |panel, cx| panel.open_remote_context(context_id, cx));
+        let context_editor_task = workspace.update(cx, |workspace, cx| {
+            assistant_panel_delegate.open_remote_context(workspace, context_id, cx)
+        });
 
         Some(cx.spawn(|mut cx| async move {
-            let context_editor = context_editor.await?;
+            let context_editor = context_editor_task.await?;
             context_editor
                 .update(&mut cx, |context_editor, cx| {
                     context_editor.remote_id = Some(id);
@@ -3466,6 +3516,24 @@ fn configuration_error(cx: &AppContext) -> Option<ConfigurationError> {
     None
 }
 
+pub fn humanize_token_count(count: usize) -> String {
+    match count {
+        0..=999 => count.to_string(),
+        1000..=9999 => {
+            let thousands = count / 1000;
+            let hundreds = (count % 1000 + 50) / 100;
+            if hundreds == 0 {
+                format!("{}k", thousands)
+            } else if hundreds == 10 {
+                format!("{}k", thousands + 1)
+            } else {
+                format!("{}.{}k", thousands, hundreds)
+            }
+        }
+        _ => format!("{}k", (count + 500) / 1000),
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

crates/assistant/src/slash_command.rs → crates/assistant_context_editor/src/slash_command.rs 🔗

@@ -20,7 +20,7 @@ use std::{
 };
 use workspace::Workspace;
 
-pub(crate) struct SlashCommandCompletionProvider {
+pub struct SlashCommandCompletionProvider {
     cancel_flag: Mutex<Arc<AtomicBool>>,
     slash_commands: Arc<SlashCommandWorkingSet>,
     editor: Option<WeakView<ContextEditor>>,