Add `assistant_context_editor` crate (#23429)

Marshall Bowers created

This PR adds a new `assistant_context_editor` crate.

This will ultimately house the `ContextEditor` so that it can be
consumed by both `assistant` and `assistant2`.

For the purposes of this PR, we just introduce the crate and move some
supporting constructs to it, such as the `ContextStore`.

Release Notes:

- N/A

Change summary

Cargo.lock                                                      |  58 +
Cargo.toml                                                      |   2 
crates/assistant/Cargo.toml                                     |  10 
crates/assistant/src/assistant.rs                               |  72 --
crates/assistant/src/assistant_panel.rs                         |   6 
crates/assistant/src/context_editor.rs                          |  18 
crates/assistant/src/context_history.rs                         |   3 
crates/assistant/src/inline_assistant.rs                        |   3 
crates/assistant/src/slash_command.rs                           |  64 -
crates/assistant/src/terminal_inline_assistant.rs               |   5 
crates/assistant_context_editor/Cargo.toml                      |  58 +
crates/assistant_context_editor/LICENSE-GPL                     |   1 
crates/assistant_context_editor/src/assistant_context_editor.rs |  16 
crates/assistant_context_editor/src/context.rs                  | 100 ++
crates/assistant_context_editor/src/context/context_tests.rs    |  13 
crates/assistant_context_editor/src/context_store.rs            |   8 
crates/assistant_context_editor/src/patch.rs                    |  12 
crates/assistant_slash_command/src/assistant_slash_command.rs   |  61 +
crates/collab/Cargo.toml                                        |   1 
crates/collab/src/tests/integration_tests.rs                    |   2 
crates/collab/src/tests/test_server.rs                          |   2 
21 files changed, 304 insertions(+), 211 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -372,14 +372,13 @@ name = "assistant"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "assistant_context_editor",
  "assistant_settings",
  "assistant_slash_command",
  "assistant_slash_commands",
  "assistant_tool",
  "async-watch",
- "chrono",
  "client",
- "clock",
  "collections",
  "command_palette_hooks",
  "context_server",
@@ -403,7 +402,6 @@ dependencies = [
  "lsp",
  "menu",
  "multi_buffer",
- "open_ai",
  "parking_lot",
  "paths",
  "picker",
@@ -412,21 +410,16 @@ dependencies = [
  "prompt_library",
  "proto",
  "rand 0.8.5",
- "regex",
  "rope",
- "rpc",
  "schemars",
  "search",
  "semantic_index",
  "serde",
- "serde_json",
  "serde_json_lenient",
  "settings",
  "similar",
- "smallvec",
  "smol",
  "streaming_diff",
- "strum",
  "telemetry",
  "telemetry_events",
  "terminal",
@@ -437,7 +430,6 @@ dependencies = [
  "ui",
  "unindent",
  "util",
- "uuid",
  "workspace",
  "zed_actions",
 ]
@@ -505,6 +497,53 @@ dependencies = [
  "zed_actions",
 ]
 
+[[package]]
+name = "assistant_context_editor"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "assistant_slash_command",
+ "assistant_slash_commands",
+ "assistant_tool",
+ "chrono",
+ "client",
+ "clock",
+ "collections",
+ "context_server",
+ "editor",
+ "feature_flags",
+ "fs",
+ "futures 0.3.31",
+ "fuzzy",
+ "gpui",
+ "language",
+ "language_model",
+ "language_models",
+ "log",
+ "open_ai",
+ "parking_lot",
+ "paths",
+ "pretty_assertions",
+ "project",
+ "prompt_library",
+ "rand 0.8.5",
+ "regex",
+ "rpc",
+ "serde",
+ "serde_json",
+ "settings",
+ "smallvec",
+ "smol",
+ "strum",
+ "telemetry_events",
+ "text",
+ "ui",
+ "unindent",
+ "util",
+ "uuid",
+ "workspace",
+]
+
 [[package]]
 name = "assistant_settings"
 version = "0.1.0"
@@ -2640,6 +2679,7 @@ dependencies = [
  "anthropic",
  "anyhow",
  "assistant",
+ "assistant_context_editor",
  "assistant_slash_command",
  "assistant_tool",
  "async-stripe",

Cargo.toml 🔗

@@ -7,6 +7,7 @@ members = [
     "crates/assets",
     "crates/assistant",
     "crates/assistant2",
+    "crates/assistant_context_editor",
     "crates/assistant_settings",
     "crates/assistant_slash_command",
     "crates/assistant_slash_commands",
@@ -204,6 +205,7 @@ anthropic = { path = "crates/anthropic" }
 assets = { path = "crates/assets" }
 assistant = { path = "crates/assistant" }
 assistant2 = { path = "crates/assistant2" }
+assistant_context_editor = { path = "crates/assistant_context_editor" }
 assistant_settings = { path = "crates/assistant_settings" }
 assistant_slash_command = { path = "crates/assistant_slash_command" }
 assistant_slash_commands = { path = "crates/assistant_slash_commands" }

crates/assistant/Cargo.toml 🔗

@@ -22,14 +22,13 @@ test-support = [
 
 [dependencies]
 anyhow.workspace = true
+assistant_context_editor.workspace = true
 assistant_settings.workspace = true
 assistant_slash_command.workspace = true
 assistant_slash_commands.workspace = true
 assistant_tool.workspace = true
 async-watch.workspace = true
-chrono.workspace = true
 client.workspace = true
-clock.workspace = true
 collections.workspace = true
 command_palette_hooks.workspace = true
 context_server.workspace = true
@@ -50,27 +49,21 @@ log.workspace = true
 lsp.workspace = true
 menu.workspace = true
 multi_buffer.workspace = true
-open_ai = { workspace = true, features = ["schemars"] }
 parking_lot.workspace = true
 paths.workspace = true
 picker.workspace = true
 project.workspace = true
 prompt_library.workspace = true
 proto.workspace = true
-regex.workspace = true
 rope.workspace = true
-rpc.workspace = true
 schemars.workspace = true
 search.workspace = true
 semantic_index.workspace = true
 serde.workspace = true
-serde_json.workspace = true
 settings.workspace = true
 similar.workspace = true
-smallvec.workspace = true
 smol.workspace = true
 streaming_diff.workspace = true
-strum.workspace = true
 telemetry.workspace = true
 telemetry_events.workspace = true
 terminal.workspace = true
@@ -79,7 +72,6 @@ text.workspace = true
 theme.workspace = true
 ui.workspace = true
 util.workspace = true
-uuid.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
 

crates/assistant/src/assistant.rs 🔗

@@ -1,12 +1,9 @@
 #![cfg_attr(target_os = "windows", allow(unused, dead_code))]
 
 pub mod assistant_panel;
-mod context;
 mod context_editor;
 mod context_history;
-pub mod context_store;
 mod inline_assistant;
-mod patch;
 mod slash_command;
 pub(crate) mod slash_command_picker;
 pub mod slash_command_settings;
@@ -18,26 +15,23 @@ use std::sync::Arc;
 use assistant_settings::AssistantSettings;
 use assistant_slash_command::SlashCommandRegistry;
 use assistant_slash_commands::{ProjectSlashCommandFeatureFlag, SearchSlashCommandFeatureFlag};
-use client::{proto, Client};
+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, SharedString, UpdateGlobal};
+use gpui::{actions, AppContext, Global, UpdateGlobal};
 use language_model::{
     LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
 };
 use prompt_library::{PromptBuilder, PromptLoadingParams};
 use semantic_index::{CloudEmbeddingProvider, SemanticDb};
-use serde::{Deserialize, Serialize};
+use serde::Deserialize;
 use settings::{Settings, SettingsStore};
 use util::ResultExt;
 
 pub use crate::assistant_panel::{AssistantPanel, AssistantPanelEvent};
-pub use crate::context::*;
-pub use crate::context_store::*;
 pub(crate) use crate::inline_assistant::*;
-pub use crate::patch::*;
 use crate::slash_command_settings::SlashCommandSettings;
 
 actions!(
@@ -72,15 +66,6 @@ impl_internal_actions!(assistant, [InsertDraggedFiles]);
 
 const DEFAULT_CONTEXT_LINES: usize = 50;
 
-#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
-pub struct MessageId(clock::Lamport);
-
-impl MessageId {
-    pub fn as_u64(self) -> u64 {
-        self.0.as_u64()
-    }
-}
-
 #[derive(Deserialize, Debug)]
 pub struct LanguageModelUsage {
     pub prompt_tokens: u32,
@@ -95,55 +80,6 @@ pub struct LanguageModelChoiceDelta {
     pub finish_reason: Option<String>,
 }
 
-#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
-pub enum MessageStatus {
-    Pending,
-    Done,
-    Error(SharedString),
-    Canceled,
-}
-
-impl MessageStatus {
-    pub fn from_proto(status: proto::ContextMessageStatus) -> MessageStatus {
-        match status.variant {
-            Some(proto::context_message_status::Variant::Pending(_)) => MessageStatus::Pending,
-            Some(proto::context_message_status::Variant::Done(_)) => MessageStatus::Done,
-            Some(proto::context_message_status::Variant::Error(error)) => {
-                MessageStatus::Error(error.message.into())
-            }
-            Some(proto::context_message_status::Variant::Canceled(_)) => MessageStatus::Canceled,
-            None => MessageStatus::Pending,
-        }
-    }
-
-    pub fn to_proto(&self) -> proto::ContextMessageStatus {
-        match self {
-            MessageStatus::Pending => proto::ContextMessageStatus {
-                variant: Some(proto::context_message_status::Variant::Pending(
-                    proto::context_message_status::Pending {},
-                )),
-            },
-            MessageStatus::Done => proto::ContextMessageStatus {
-                variant: Some(proto::context_message_status::Variant::Done(
-                    proto::context_message_status::Done {},
-                )),
-            },
-            MessageStatus::Error(message) => proto::ContextMessageStatus {
-                variant: Some(proto::context_message_status::Variant::Error(
-                    proto::context_message_status::Error {
-                        message: message.to_string(),
-                    },
-                )),
-            },
-            MessageStatus::Canceled => proto::ContextMessageStatus {
-                variant: Some(proto::context_message_status::Variant::Canceled(
-                    proto::context_message_status::Canceled {},
-                )),
-            },
-        }
-    }
-}
-
 /// The state pertaining to the Assistant.
 #[derive(Default)]
 struct Assistant {
@@ -214,7 +150,7 @@ pub fn init(
     })
     .detach();
 
-    context_store::init(&client.clone().into());
+    assistant_context_editor::init(client.clone(), cx);
     prompt_library::init(cx);
     init_language_model_settings(cx);
     assistant_slash_command::init(cx);

crates/assistant/src/assistant_panel.rs 🔗

@@ -4,11 +4,11 @@ use crate::context_editor::{
 use crate::context_history::ContextHistory;
 use crate::{
     slash_command::SlashCommandCompletionProvider,
-    terminal_inline_assistant::TerminalInlineAssistant, Context, ContextId, ContextStore,
-    ContextStoreEvent, DeployHistory, DeployPromptLibrary, InlineAssistant, InsertDraggedFiles,
-    NewContext, ToggleFocus, ToggleModelSelector,
+    terminal_inline_assistant::TerminalInlineAssistant, DeployHistory, DeployPromptLibrary,
+    InlineAssistant, InsertDraggedFiles, NewContext, ToggleFocus, ToggleModelSelector,
 };
 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;

crates/assistant/src/context_editor.rs 🔗

@@ -1,4 +1,9 @@
 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::{
@@ -58,11 +63,8 @@ use workspace::{
 
 use crate::{
     humanize_token_count, slash_command::SlashCommandCompletionProvider, slash_command_picker,
-    Assist, AssistantPanel, AssistantPatch, AssistantPatchStatus, CacheStatus, ConfirmCommand,
-    Content, Context, ContextEvent, ContextId, CopyCode, CycleMessageRole, Edit,
-    InsertDraggedFiles, InsertIntoEditor, InvokedSlashCommandId, InvokedSlashCommandStatus,
-    Message, MessageId, MessageMetadata, MessageStatus, ParsedSlashCommand,
-    PendingSlashCommandStatus, QuoteSelection, RequestType, Split, ToggleModelSelector,
+    Assist, AssistantPanel, ConfirmCommand, CopyCode, CycleMessageRole, Edit, InsertDraggedFiles,
+    InsertIntoEditor, QuoteSelection, Split, ToggleModelSelector,
 };
 
 #[derive(Copy, Clone, Debug, PartialEq)]
@@ -138,7 +140,7 @@ impl ContextEditor {
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let completion_provider = SlashCommandCompletionProvider::new(
-            context.read(cx).slash_commands.clone(),
+            context.read(cx).slash_commands().clone(),
             Some(cx.view().downgrade()),
             Some(workspace.clone()),
         );
@@ -167,8 +169,8 @@ impl ContextEditor {
 
         let sections = context.read(cx).slash_command_output_sections().to_vec();
         let patch_ranges = context.read(cx).patch_ranges().collect::<Vec<_>>();
-        let slash_commands = context.read(cx).slash_commands.clone();
-        let tools = context.read(cx).tools.clone();
+        let slash_commands = context.read(cx).slash_commands().clone();
+        let tools = context.read(cx).tools().clone();
         let mut this = Self {
             context,
             slash_commands,

crates/assistant/src/context_history.rs 🔗

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

crates/assistant/src/inline_assistant.rs 🔗

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

crates/assistant/src/slash_command.rs 🔗

@@ -1,8 +1,7 @@
 use crate::context_editor::ContextEditor;
 use anyhow::Result;
-use assistant_slash_command::AfterCompletion;
 pub use assistant_slash_command::SlashCommand;
-use assistant_slash_command::SlashCommandWorkingSet;
+use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWorkingSet};
 use editor::{CompletionProvider, Editor};
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{Model, Task, ViewContext, WeakView, WindowContext};
@@ -28,13 +27,6 @@ pub(crate) struct SlashCommandCompletionProvider {
     workspace: Option<WeakView<Workspace>>,
 }
 
-pub(crate) struct SlashCommandLine {
-    /// The range within the line containing the command name.
-    pub name: Range<usize>,
-    /// Ranges within the line containing the command arguments.
-    pub arguments: Vec<Range<usize>>,
-}
-
 impl SlashCommandCompletionProvider {
     pub fn new(
         slash_commands: Arc<SlashCommandWorkingSet>,
@@ -336,57 +328,3 @@ impl CompletionProvider for SlashCommandCompletionProvider {
         false
     }
 }
-
-impl SlashCommandLine {
-    pub(crate) fn parse(line: &str) -> Option<Self> {
-        let mut call: Option<Self> = None;
-        let mut ix = 0;
-        for c in line.chars() {
-            let next_ix = ix + c.len_utf8();
-            if let Some(call) = &mut call {
-                // The command arguments start at the first non-whitespace character
-                // after the command name, and continue until the end of the line.
-                if let Some(argument) = call.arguments.last_mut() {
-                    if c.is_whitespace() {
-                        if (*argument).is_empty() {
-                            argument.start = next_ix;
-                            argument.end = next_ix;
-                        } else {
-                            argument.end = ix;
-                            call.arguments.push(next_ix..next_ix);
-                        }
-                    } else {
-                        argument.end = next_ix;
-                    }
-                }
-                // The command name ends at the first whitespace character.
-                else if !call.name.is_empty() {
-                    if c.is_whitespace() {
-                        call.arguments = vec![next_ix..next_ix];
-                    } else {
-                        call.name.end = next_ix;
-                    }
-                }
-                // The command name must begin with a letter.
-                else if c.is_alphabetic() {
-                    call.name.end = next_ix;
-                } else {
-                    return None;
-                }
-            }
-            // Commands start with a slash.
-            else if c == '/' {
-                call = Some(SlashCommandLine {
-                    name: next_ix..next_ix,
-                    arguments: Vec::new(),
-                });
-            }
-            // The line can't contain anything before the slash except for whitespace.
-            else if !c.is_whitespace() {
-                return None;
-            }
-            ix = next_ix;
-        }
-        call
-    }
-}

crates/assistant/src/terminal_inline_assistant.rs 🔗

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

crates/assistant_context_editor/Cargo.toml 🔗

@@ -0,0 +1,58 @@
+[package]
+name = "assistant_context_editor"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/assistant_context_editor.rs"
+
+[dependencies]
+anyhow.workspace = true
+assistant_slash_command.workspace = true
+assistant_slash_commands.workspace = true
+assistant_tool.workspace = true
+chrono.workspace = true
+client.workspace = true
+clock.workspace = true
+collections.workspace = true
+context_server.workspace = true
+editor.workspace = true
+feature_flags.workspace = true
+fs.workspace = true
+futures.workspace = true
+fuzzy.workspace = true
+gpui.workspace = true
+language.workspace = true
+language_model.workspace = true
+language_models.workspace = true
+log.workspace = true
+open_ai.workspace = true
+paths.workspace = true
+project.workspace = true
+prompt_library.workspace = true
+regex.workspace = true
+rpc.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+smallvec.workspace = true
+smol.workspace = true
+strum.workspace = true
+telemetry_events.workspace = true
+text.workspace = true
+ui.workspace = true
+util.workspace = true
+uuid.workspace = true
+
+[dev-dependencies]
+language_model = { workspace = true, features = ["test-support"] }
+parking_lot.workspace = true
+pretty_assertions.workspace = true
+rand.workspace = true
+settings.workspace = true
+unindent.workspace = true
+workspace = { workspace = true, features = ["test-support"] }

crates/assistant_context_editor/src/assistant_context_editor.rs 🔗

@@ -0,0 +1,16 @@
+mod context;
+mod context_store;
+mod patch;
+
+use std::sync::Arc;
+
+use client::Client;
+use gpui::AppContext;
+
+pub use crate::context::*;
+pub use crate::context_store::*;
+pub use crate::patch::*;
+
+pub fn init(client: Arc<Client>, _cx: &mut AppContext) {
+    context_store::init(&client.into());
+}

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

@@ -1,14 +1,11 @@
 #[cfg(test)]
 mod context_tests;
 
-use crate::{
-    slash_command::SlashCommandLine, AssistantEdit, AssistantPatch, AssistantPatchStatus,
-    MessageId, MessageStatus,
-};
+use crate::patch::{AssistantEdit, AssistantPatch, AssistantPatchStatus};
 use anyhow::{anyhow, Context as _, Result};
 use assistant_slash_command::{
-    SlashCommandContent, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult,
-    SlashCommandWorkingSet,
+    SlashCommandContent, SlashCommandEvent, SlashCommandLine, SlashCommandOutputSection,
+    SlashCommandResult, SlashCommandWorkingSet,
 };
 use assistant_slash_commands::FileCommandMetadata;
 use assistant_tool::ToolWorkingSet;
@@ -22,8 +19,6 @@ use gpui::{
     AppContext, Context as _, EventEmitter, Model, ModelContext, RenderImage, SharedString,
     Subscription, Task,
 };
-use prompt_library::PromptBuilder;
-
 use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
 use language_model::{
     LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
@@ -38,6 +33,7 @@ use language_models::{
 use open_ai::Model as OpenAiModel;
 use paths::contexts_dir;
 use project::Project;
+use prompt_library::PromptBuilder;
 use serde::{Deserialize, Serialize};
 use smallvec::SmallVec;
 use std::{
@@ -52,9 +48,9 @@ use std::{
 };
 use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
 use text::{BufferSnapshot, ToPoint};
+use ui::IconName;
 use util::{post_inc, ResultExt, TryFutureExt};
 use uuid::Uuid;
-use workspace::ui::IconName;
 
 #[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
 pub struct ContextId(String);
@@ -73,6 +69,64 @@ impl ContextId {
     }
 }
 
+#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+pub struct MessageId(pub clock::Lamport);
+
+impl MessageId {
+    pub fn as_u64(self) -> u64 {
+        self.0.as_u64()
+    }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
+pub enum MessageStatus {
+    Pending,
+    Done,
+    Error(SharedString),
+    Canceled,
+}
+
+impl MessageStatus {
+    pub fn from_proto(status: proto::ContextMessageStatus) -> MessageStatus {
+        match status.variant {
+            Some(proto::context_message_status::Variant::Pending(_)) => MessageStatus::Pending,
+            Some(proto::context_message_status::Variant::Done(_)) => MessageStatus::Done,
+            Some(proto::context_message_status::Variant::Error(error)) => {
+                MessageStatus::Error(error.message.into())
+            }
+            Some(proto::context_message_status::Variant::Canceled(_)) => MessageStatus::Canceled,
+            None => MessageStatus::Pending,
+        }
+    }
+
+    pub fn to_proto(&self) -> proto::ContextMessageStatus {
+        match self {
+            MessageStatus::Pending => proto::ContextMessageStatus {
+                variant: Some(proto::context_message_status::Variant::Pending(
+                    proto::context_message_status::Pending {},
+                )),
+            },
+            MessageStatus::Done => proto::ContextMessageStatus {
+                variant: Some(proto::context_message_status::Variant::Done(
+                    proto::context_message_status::Done {},
+                )),
+            },
+            MessageStatus::Error(message) => proto::ContextMessageStatus {
+                variant: Some(proto::context_message_status::Variant::Error(
+                    proto::context_message_status::Error {
+                        message: message.to_string(),
+                    },
+                )),
+            },
+            MessageStatus::Canceled => proto::ContextMessageStatus {
+                variant: Some(proto::context_message_status::Variant::Canceled(
+                    proto::context_message_status::Canceled {},
+                )),
+            },
+        }
+    }
+}
+
 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 pub enum RequestType {
     /// Request a normal chat response from the model.
@@ -423,7 +477,7 @@ pub struct MessageCacheMetadata {
 pub struct MessageMetadata {
     pub role: Role,
     pub status: MessageStatus,
-    pub(crate) timestamp: clock::Lamport,
+    pub timestamp: clock::Lamport,
     #[serde(skip)]
     pub cache: Option<MessageCacheMetadata>,
 }
@@ -544,8 +598,8 @@ pub struct Context {
     parsed_slash_commands: Vec<ParsedSlashCommand>,
     invoked_slash_commands: HashMap<InvokedSlashCommandId, InvokedSlashCommand>,
     edits_since_last_parse: language::Subscription,
-    pub(crate) slash_commands: Arc<SlashCommandWorkingSet>,
-    pub(crate) tools: Arc<ToolWorkingSet>,
+    slash_commands: Arc<SlashCommandWorkingSet>,
+    tools: Arc<ToolWorkingSet>,
     slash_command_output_sections: Vec<SlashCommandOutputSection<language::Anchor>>,
     pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
     message_anchors: Vec<MessageAnchor>,
@@ -790,6 +844,14 @@ impl Context {
         }
     }
 
+    pub fn slash_commands(&self) -> &Arc<SlashCommandWorkingSet> {
+        &self.slash_commands
+    }
+
+    pub fn tools(&self) -> &Arc<ToolWorkingSet> {
+        &self.tools
+    }
+
     pub fn set_capability(
         &mut self,
         capability: language::Capability,
@@ -1048,11 +1110,7 @@ impl Context {
         self.summary.as_ref()
     }
 
-    pub(crate) fn patch_containing(
-        &self,
-        position: Point,
-        cx: &AppContext,
-    ) -> Option<&AssistantPatch> {
+    pub fn patch_containing(&self, position: Point, cx: &AppContext) -> Option<&AssistantPatch> {
         let buffer = self.buffer.read(cx);
         let index = self.patches.binary_search_by(|patch| {
             let patch_range = patch.range.to_point(&buffer);
@@ -1075,7 +1133,7 @@ impl Context {
         self.patches.iter().map(|patch| patch.range.clone())
     }
 
-    pub(crate) fn patch_for_range(
+    pub fn patch_for_range(
         &self,
         range: &Range<language::Anchor>,
         cx: &AppContext,
@@ -1165,7 +1223,7 @@ impl Context {
         }
     }
 
-    pub(crate) fn token_count(&self) -> Option<usize> {
+    pub fn token_count(&self) -> Option<usize> {
         self.token_count
     }
 
@@ -2879,7 +2937,7 @@ impl Context {
         self.message_anchors.insert(insertion_ix, new_anchor);
     }
 
-    pub(super) fn summarize(&mut self, replace_old: bool, cx: &mut ModelContext<Self>) {
+    pub fn summarize(&mut self, replace_old: bool, cx: &mut ModelContext<Self>) {
         let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
             return;
         };
@@ -3118,7 +3176,7 @@ impl Context {
         });
     }
 
-    pub(crate) fn custom_summary(&mut self, custom_summary: String, cx: &mut ModelContext<Self>) {
+    pub fn custom_summary(&mut self, custom_summary: String, cx: &mut ModelContext<Self>) {
         let timestamp = self.next_timestamp();
         let summary = self.summary.get_or_insert(ContextSummary::default());
         summary.timestamp = timestamp;

crates/assistant/src/context/context_tests.rs → crates/assistant_context_editor/src/context/context_tests.rs 🔗

@@ -1,7 +1,6 @@
-use super::{AssistantEdit, MessageCacheMetadata};
 use crate::{
-    assistant_panel, AssistantEditKind, CacheStatus, Context, ContextEvent, ContextId,
-    ContextOperation, InvokedSlashCommandId, MessageId, MessageStatus,
+    AssistantEdit, AssistantEditKind, CacheStatus, Context, ContextEvent, ContextId,
+    ContextOperation, InvokedSlashCommandId, MessageCacheMetadata, MessageId, MessageStatus,
 };
 use anyhow::Result;
 use assistant_slash_command::{
@@ -48,7 +47,6 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
     let settings_store = SettingsStore::test(cx);
     LanguageModelRegistry::test(cx);
     cx.set_global(settings_store);
-    assistant_panel::init(cx);
     let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
     let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
     let context = cx.new_model(|cx| {
@@ -189,7 +187,6 @@ fn test_message_splitting(cx: &mut AppContext) {
     let settings_store = SettingsStore::test(cx);
     cx.set_global(settings_store);
     LanguageModelRegistry::test(cx);
-    assistant_panel::init(cx);
     let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
 
     let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
@@ -294,7 +291,6 @@ fn test_messages_for_offsets(cx: &mut AppContext) {
     let settings_store = SettingsStore::test(cx);
     LanguageModelRegistry::test(cx);
     cx.set_global(settings_store);
-    assistant_panel::init(cx);
     let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
     let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
     let context = cx.new_model(|cx| {
@@ -390,7 +386,6 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
     cx.set_global(settings_store);
     cx.update(LanguageModelRegistry::test);
     cx.update(Project::init_settings);
-    cx.update(assistant_panel::init);
     let fs = FakeFs::new(cx.background_executor.clone());
 
     fs.insert_tree(
@@ -698,7 +693,6 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
     let project = Project::test(fs, [Path::new("/root")], cx).await;
     cx.update(LanguageModelRegistry::test);
 
-    cx.update(assistant_panel::init);
     let registry = Arc::new(LanguageRegistry::test(cx.executor()));
 
     // Create a new context
@@ -1081,7 +1075,6 @@ async fn test_serialization(cx: &mut TestAppContext) {
     let settings_store = cx.update(SettingsStore::test);
     cx.set_global(settings_store);
     cx.update(LanguageModelRegistry::test);
-    cx.update(assistant_panel::init);
     let registry = Arc::new(LanguageRegistry::test(cx.executor()));
     let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
     let context = cx.new_model(|cx| {
@@ -1173,7 +1166,6 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
     cx.set_global(settings_store);
     cx.update(LanguageModelRegistry::test);
 
-    cx.update(assistant_panel::init);
     let slash_commands = cx.update(SlashCommandRegistry::default_global);
     slash_commands.register_command(FakeSlashCommand("cmd-1".into()), false);
     slash_commands.register_command(FakeSlashCommand("cmd-2".into()), false);
@@ -1446,7 +1438,6 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
     let settings_store = SettingsStore::test(cx);
     LanguageModelRegistry::test(cx);
     cx.set_global(settings_store);
-    assistant_panel::init(cx);
     let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
     let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
     let context = cx.new_model(|cx| {

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

@@ -33,7 +33,7 @@ use std::{
 };
 use util::{ResultExt, TryFutureExt};
 
-pub fn init(client: &AnyProtoClient) {
+pub(crate) fn init(client: &AnyProtoClient) {
     client.add_model_message_handler(ContextStore::handle_advertise_contexts);
     client.add_model_request_handler(ContextStore::handle_open_context);
     client.add_model_request_handler(ContextStore::handle_create_context);
@@ -497,11 +497,7 @@ impl ContextStore {
         })
     }
 
-    pub(super) fn loaded_context_for_id(
-        &self,
-        id: &ContextId,
-        cx: &AppContext,
-    ) -> Option<Model<Context>> {
+    pub fn loaded_context_for_id(&self, id: &ContextId, cx: &AppContext) -> Option<Model<Context>> {
         self.contexts.iter().find_map(|context| {
             let context = context.upgrade()?;
             if context.read(cx).id() == id {

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

@@ -9,7 +9,7 @@ use std::{cmp, ops::Range, path::Path, sync::Arc};
 use text::{AnchorRangeExt as _, Bias, OffsetRangeExt as _, Point};
 
 #[derive(Clone, Debug)]
-pub(crate) struct AssistantPatch {
+pub struct AssistantPatch {
     pub range: Range<language::Anchor>,
     pub title: SharedString,
     pub edits: Arc<[Result<AssistantEdit>]>,
@@ -17,13 +17,13 @@ pub(crate) struct AssistantPatch {
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
-pub(crate) enum AssistantPatchStatus {
+pub enum AssistantPatchStatus {
     Pending,
     Ready,
 }
 
 #[derive(Clone, Debug, PartialEq, Eq)]
-pub(crate) struct AssistantEdit {
+pub struct AssistantEdit {
     pub path: String,
     pub kind: AssistantEditKind,
 }
@@ -55,7 +55,7 @@ pub enum AssistantEditKind {
 }
 
 #[derive(Clone, Debug, Eq, PartialEq)]
-pub(crate) struct ResolvedPatch {
+pub struct ResolvedPatch {
     pub edit_groups: HashMap<Model<Buffer>, Vec<ResolvedEditGroup>>,
     pub errors: Vec<AssistantPatchResolutionError>,
 }
@@ -74,7 +74,7 @@ pub struct ResolvedEdit {
 }
 
 #[derive(Clone, Debug, Eq, PartialEq)]
-pub(crate) struct AssistantPatchResolutionError {
+pub struct AssistantPatchResolutionError {
     pub edit_ix: usize,
     pub message: String,
 }
@@ -425,7 +425,7 @@ impl AssistantEditKind {
 }
 
 impl AssistantPatch {
-    pub(crate) async fn resolve(
+    pub async fn resolve(
         &self,
         project: Model<Project>,
         cx: &mut AsyncAppContext,

crates/assistant_slash_command/src/assistant_slash_command.rs 🔗

@@ -262,6 +262,67 @@ impl SlashCommandOutputSection<language::Anchor> {
     }
 }
 
+pub struct SlashCommandLine {
+    /// The range within the line containing the command name.
+    pub name: Range<usize>,
+    /// Ranges within the line containing the command arguments.
+    pub arguments: Vec<Range<usize>>,
+}
+
+impl SlashCommandLine {
+    pub fn parse(line: &str) -> Option<Self> {
+        let mut call: Option<Self> = None;
+        let mut ix = 0;
+        for c in line.chars() {
+            let next_ix = ix + c.len_utf8();
+            if let Some(call) = &mut call {
+                // The command arguments start at the first non-whitespace character
+                // after the command name, and continue until the end of the line.
+                if let Some(argument) = call.arguments.last_mut() {
+                    if c.is_whitespace() {
+                        if (*argument).is_empty() {
+                            argument.start = next_ix;
+                            argument.end = next_ix;
+                        } else {
+                            argument.end = ix;
+                            call.arguments.push(next_ix..next_ix);
+                        }
+                    } else {
+                        argument.end = next_ix;
+                    }
+                }
+                // The command name ends at the first whitespace character.
+                else if !call.name.is_empty() {
+                    if c.is_whitespace() {
+                        call.arguments = vec![next_ix..next_ix];
+                    } else {
+                        call.name.end = next_ix;
+                    }
+                }
+                // The command name must begin with a letter.
+                else if c.is_alphabetic() {
+                    call.name.end = next_ix;
+                } else {
+                    return None;
+                }
+            }
+            // Commands start with a slash.
+            else if c == '/' {
+                call = Some(SlashCommandLine {
+                    name: next_ix..next_ix,
+                    arguments: Vec::new(),
+                });
+            }
+            // The line can't contain anything before the slash except for whitespace.
+            else if !c.is_whitespace() {
+                return None;
+            }
+            ix = next_ix;
+        }
+        call
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use pretty_assertions::assert_eq;

crates/collab/Cargo.toml 🔗

@@ -79,6 +79,7 @@ uuid.workspace = true
 
 [dev-dependencies]
 assistant = { workspace = true, features = ["test-support"] }
+assistant_context_editor.workspace = true
 assistant_slash_command.workspace = true
 assistant_tool.workspace = true
 async-trait.workspace = true

crates/collab/src/tests/integration_tests.rs 🔗

@@ -6,7 +6,7 @@ use crate::{
     },
 };
 use anyhow::{anyhow, Result};
-use assistant::ContextStore;
+use assistant_context_editor::ContextStore;
 use assistant_slash_command::SlashCommandWorkingSet;
 use assistant_tool::ToolWorkingSet;
 use call::{room, ActiveCall, ParticipantLocation, Room};

crates/collab/src/tests/test_server.rs 🔗

@@ -308,7 +308,7 @@ impl TestServer {
                 settings::KeymapFile::load_asset_allow_partial_failure(os_keymap, cx).unwrap(),
             );
             language_model::LanguageModelRegistry::test(cx);
-            assistant::context_store::init(&client.clone().into());
+            assistant_context_editor::init(client.clone(), cx);
         });
 
         client