diff --git a/Cargo.lock b/Cargo.lock index f679ced758..307ce3aaf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -385,49 +385,6 @@ dependencies = [ "workspace", ] -[[package]] -name = "assistant2" -version = "0.1.0" -dependencies = [ - "anyhow", - "assets", - "assistant_tooling", - "chrono", - "client", - "collections", - "editor", - "env_logger", - "feature_flags", - "file_icons", - "fs", - "futures 0.3.28", - "fuzzy", - "gpui", - "http 0.1.0", - "language", - "languages", - "log", - "markdown", - "node_runtime", - "open_ai", - "picker", - "project", - "rand 0.8.5", - "regex", - "release_channel", - "schemars", - "semantic_index", - "serde", - "serde_json", - "settings", - "story", - "theme", - "ui", - "unindent", - "util", - "workspace", -] - [[package]] name = "assistant_slash_command" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index c53a5db006..67bbcc5dd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,6 @@ members = [ "crates/anthropic", "crates/assets", "crates/assistant", - "crates/assistant2", "crates/assistant_slash_command", "crates/assistant_tooling", "crates/audio", @@ -148,7 +147,6 @@ 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_tooling = { path = "crates/assistant_tooling" } audio = { path = "crates/audio" } diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml deleted file mode 100644 index 8df924d607..0000000000 --- a/crates/assistant2/Cargo.toml +++ /dev/null @@ -1,66 +0,0 @@ -[package] -name = "assistant2" -version = "0.1.0" -edition = "2021" -publish = false -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/assistant2.rs" - -[features] -default = [] -stories = ["dep:story"] - -[dependencies] -anyhow.workspace = true -assistant_tooling.workspace = true -client.workspace = true -chrono.workspace = true -collections.workspace = true -editor.workspace = true -feature_flags.workspace = true -file_icons.workspace = true -fs.workspace = true -futures.workspace = true -fuzzy.workspace = true -gpui.workspace = true -language.workspace = true -log.workspace = true -markdown.workspace = true -open_ai.workspace = true -picker.workspace = true -project.workspace = true -regex.workspace = true -schemars.workspace = true -semantic_index.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -story = { workspace = true, optional = true } -theme.workspace = true -ui.workspace = true -util.workspace = true -unindent.workspace = true -workspace.workspace = true - -[dev-dependencies] -assets.workspace = true -editor = { workspace = true, features = ["test-support"] } -env_logger.workspace = true -gpui = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -languages.workspace = true -markdown = { workspace = true, features = ["test-support"] } -node_runtime.workspace = true -project = { workspace = true, features = ["test-support"] } -rand.workspace = true -release_channel.workspace = true -settings = { workspace = true, features = ["test-support"] } -theme = { workspace = true, features = ["test-support"] } -util = { workspace = true, features = ["test-support"] } -http = { workspace = true, features = ["test-support"] } -workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/assistant2/LICENSE-GPL b/crates/assistant2/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/assistant2/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/assistant2/evals/list-of-into-element.md b/crates/assistant2/evals/list-of-into-element.md deleted file mode 100644 index fca5e1afeb..0000000000 --- a/crates/assistant2/evals/list-of-into-element.md +++ /dev/null @@ -1 +0,0 @@ -> Give me a comprehensive list of all the elements defined in my project using the following query: `impl Element for {}, impl Element for {}, impl IntoElement for {})` diff --git a/crates/assistant2/evals/new-gpui-element.md b/crates/assistant2/evals/new-gpui-element.md deleted file mode 100644 index 51452cb36e..0000000000 --- a/crates/assistant2/evals/new-gpui-element.md +++ /dev/null @@ -1 +0,0 @@ -> What are all the places we define a new gpui element in my project? (impl Element for {}) diff --git a/crates/assistant2/evals/settings-file.md b/crates/assistant2/evals/settings-file.md deleted file mode 100644 index ff15f7d003..0000000000 --- a/crates/assistant2/evals/settings-file.md +++ /dev/null @@ -1,3 +0,0 @@ -Use tools frequently, especially when referring to files and code. The Zed editor we're working in can show me files directly when you add annotations. Be concise in chat, bountiful in tool calling. - -Teach me everything you can about how zed loads settings. Please annotate the code inline. diff --git a/crates/assistant2/evals/what-is-the-assistant2-crate.md b/crates/assistant2/evals/what-is-the-assistant2-crate.md deleted file mode 100644 index 5d39684c5a..0000000000 --- a/crates/assistant2/evals/what-is-the-assistant2-crate.md +++ /dev/null @@ -1 +0,0 @@ -> Can you tell me what the assistant2 crate is for in my project? Tell me in 100 words or less. diff --git a/crates/assistant2/src/assistant2.rs b/crates/assistant2/src/assistant2.rs deleted file mode 100644 index bb60a6487c..0000000000 --- a/crates/assistant2/src/assistant2.rs +++ /dev/null @@ -1,1183 +0,0 @@ -mod assistant_settings; -mod attachments; -mod completion_provider; -mod saved_conversation; -mod saved_conversations; -mod tools; -pub mod ui; - -use crate::saved_conversation::SavedConversationMetadata; -use crate::ui::UserOrAssistant; -use ::ui::{div, prelude::*, Color, Tooltip, ViewContext}; -use anyhow::{Context, Result}; -use assistant_tooling::{ - AttachmentRegistry, ProjectContext, ToolFunctionCall, ToolRegistry, UserAttachment, -}; -use attachments::ActiveEditorAttachmentTool; -use client::{proto, Client, UserStore}; -use collections::HashMap; -use completion_provider::*; -use editor::Editor; -use feature_flags::FeatureFlagAppExt as _; -use file_icons::FileIcons; -use fs::Fs; -use futures::{future::join_all, StreamExt}; -use gpui::{ - list, AnyElement, AppContext, AsyncWindowContext, ClickEvent, EventEmitter, FocusHandle, - FocusableView, ListAlignment, ListState, Model, ReadGlobal, Render, Task, UpdateGlobal, View, - WeakView, -}; -use language::{language_settings::SoftWrap, LanguageRegistry}; -use markdown::{Markdown, MarkdownStyle}; -use open_ai::{FunctionContent, ToolCall, ToolCallContent}; -use saved_conversation::{SavedAssistantMessagePart, SavedChatMessage, SavedConversation}; -use saved_conversations::SavedConversations; -use semantic_index::{CloudEmbeddingProvider, ProjectIndex, ProjectIndexDebugView, SemanticIndex}; -use serde::{Deserialize, Serialize}; -use settings::Settings; -use std::sync::Arc; -use tools::{AnnotationTool, CreateBufferTool, ProjectIndexTool}; -use ui::{ActiveFileButton, Composer, ProjectIndexButton}; -use util::paths::CONVERSATIONS_DIR; -use util::{maybe, paths::EMBEDDINGS_DIR, ResultExt}; -use workspace::{ - dock::{DockPosition, Panel, PanelEvent}, - Workspace, -}; - -pub use assistant_settings::AssistantSettings; - -const MAX_COMPLETION_CALLS_PER_SUBMISSION: usize = 5; - -#[derive(Eq, PartialEq, Copy, Clone, Deserialize)] -pub struct Submit(SubmitMode); - -/// There are multiple different ways to submit a model request, represented by this enum. -#[derive(Eq, PartialEq, Copy, Clone, Deserialize)] -pub enum SubmitMode { - /// Only include the conversation. - Simple, - /// Send the current file as context. - CurrentFile, - /// Search the codebase and send relevant excerpts. - Codebase, -} - -gpui::actions!(assistant2, [Cancel, ToggleFocus, DebugProjectIndex,]); -gpui::impl_actions!(assistant2, [Submit]); - -pub fn init(client: Arc, cx: &mut AppContext) { - AssistantSettings::register(cx); - - cx.spawn(|mut cx| { - let client = client.clone(); - async move { - let embedding_provider = CloudEmbeddingProvider::new(client.clone()); - let semantic_index = SemanticIndex::new( - EMBEDDINGS_DIR.join("semantic-index-db.0.mdb"), - Arc::new(embedding_provider), - &mut cx, - ) - .await?; - cx.update(|cx| cx.set_global(semantic_index)) - } - }) - .detach(); - - cx.set_global(CompletionProvider::new(CloudCompletionProvider::new( - client, - ))); - - cx.observe_new_views( - |workspace: &mut Workspace, _cx: &mut ViewContext| { - workspace.register_action(|workspace, _: &ToggleFocus, cx| { - workspace.toggle_panel_focus::(cx); - }); - workspace.register_action(|workspace, _: &DebugProjectIndex, cx| { - if let Some(panel) = workspace.panel::(cx) { - let index = panel.read(cx).chat.read(cx).project_index.clone(); - let view = cx.new_view(|cx| ProjectIndexDebugView::new(index, cx)); - workspace.add_item_to_center(Box::new(view), cx); - } - }); - }, - ) - .detach(); -} - -pub fn enabled(cx: &AppContext) -> bool { - cx.is_staff() -} - -pub struct AssistantPanel { - chat: View, - width: Option, -} - -impl AssistantPanel { - pub fn load( - workspace: WeakView, - cx: AsyncWindowContext, - ) -> Task>> { - cx.spawn(|mut cx| async move { - let (app_state, project) = workspace.update(&mut cx, |workspace, _| { - (workspace.app_state().clone(), workspace.project().clone()) - })?; - - cx.new_view(|cx| { - let project_index = SemanticIndex::update_global(cx, |semantic_index, cx| { - semantic_index.project_index(project.clone(), cx) - }); - - // Used in tools to render file icons - cx.observe_global::(|_, cx| { - cx.notify(); - }) - .detach(); - - let mut tool_registry = ToolRegistry::new(); - tool_registry - .register(ProjectIndexTool::new(project_index.clone())) - .unwrap(); - tool_registry - .register(CreateBufferTool::new(workspace.clone(), project.clone())) - .unwrap(); - tool_registry - .register(AnnotationTool::new(workspace.clone(), project.clone())) - .unwrap(); - - let mut attachment_registry = AttachmentRegistry::new(); - attachment_registry - .register(ActiveEditorAttachmentTool::new(workspace.clone(), cx)); - - Self::new( - project.read(cx).fs().clone(), - app_state.languages.clone(), - Arc::new(tool_registry), - Arc::new(attachment_registry), - app_state.user_store.clone(), - project_index, - workspace, - cx, - ) - }) - }) - } - - #[allow(clippy::too_many_arguments)] - pub fn new( - fs: Arc, - language_registry: Arc, - tool_registry: Arc, - attachment_registry: Arc, - user_store: Model, - project_index: Model, - workspace: WeakView, - cx: &mut ViewContext, - ) -> Self { - let chat = cx.new_view(|cx| { - AssistantChat::new( - fs, - language_registry, - tool_registry.clone(), - attachment_registry, - user_store, - project_index, - workspace, - cx, - ) - }); - - Self { width: None, chat } - } -} - -impl Render for AssistantPanel { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - div() - .size_full() - .v_flex() - .bg(cx.theme().colors().panel_background) - .child(self.chat.clone()) - } -} - -impl Panel for AssistantPanel { - fn persistent_name() -> &'static str { - "AssistantPanelv2" - } - - fn position(&self, _cx: &WindowContext) -> workspace::dock::DockPosition { - // todo!("Add a setting / use assistant settings") - DockPosition::Right - } - - fn position_is_valid(&self, position: workspace::dock::DockPosition) -> bool { - matches!(position, DockPosition::Right) - } - - fn set_position(&mut self, _: workspace::dock::DockPosition, _: &mut ViewContext) { - // Do nothing until we have a setting for this - } - - fn size(&self, _cx: &WindowContext) -> Pixels { - self.width.unwrap_or(px(400.)) - } - - fn set_size(&mut self, size: Option, cx: &mut ViewContext) { - self.width = size; - cx.notify(); - } - - fn icon(&self, _cx: &WindowContext) -> Option<::ui::IconName> { - Some(IconName::ZedAssistant) - } - - fn icon_tooltip(&self, _: &WindowContext) -> Option<&'static str> { - Some("Assistant Panel ✨") - } - - fn toggle_action(&self) -> Box { - Box::new(ToggleFocus) - } -} - -impl EventEmitter for AssistantPanel {} - -impl FocusableView for AssistantPanel { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - self.chat.read(cx).composer_editor.read(cx).focus_handle(cx) - } -} - -pub struct AssistantChat { - model: String, - messages: Vec, - list_state: ListState, - fs: Arc, - language_registry: Arc, - composer_editor: View, - saved_conversations: View, - saved_conversations_open: bool, - project_index_button: View, - active_file_button: Option>, - user_store: Model, - next_message_id: MessageId, - collapsed_messages: HashMap, - editing_message: Option, - pending_completion: Option>, - tool_registry: Arc, - attachment_registry: Arc, - project_index: Model, - markdown_style: MarkdownStyle, -} - -struct EditingMessage { - id: MessageId, - body: View, -} - -impl AssistantChat { - #[allow(clippy::too_many_arguments)] - fn new( - fs: Arc, - language_registry: Arc, - tool_registry: Arc, - attachment_registry: Arc, - user_store: Model, - project_index: Model, - workspace: WeakView, - cx: &mut ViewContext, - ) -> Self { - let model = CompletionProvider::global(cx).default_model(); - let view = cx.view().downgrade(); - let list_state = ListState::new( - 0, - ListAlignment::Bottom, - px(1024.), - move |ix, cx: &mut WindowContext| { - view.update(cx, |this, cx| this.render_message(ix, cx)) - .unwrap() - }, - ); - - let project_index_button = cx.new_view(|cx| { - ProjectIndexButton::new(project_index.clone(), tool_registry.clone(), cx) - }); - - let active_file_button = match workspace.upgrade() { - Some(workspace) => { - Some(cx.new_view( - |cx| ActiveFileButton::new(attachment_registry.clone(), workspace, cx), // - )) - } - _ => None, - }; - - let saved_conversations = cx.new_view(|cx| SavedConversations::new(cx)); - cx.spawn({ - let fs = fs.clone(); - let saved_conversations = saved_conversations.downgrade(); - |_assistant_chat, mut cx| async move { - let saved_conversation_metadata = SavedConversationMetadata::list(fs).await?; - - cx.update(|cx| { - saved_conversations.update(cx, |this, cx| { - this.init(saved_conversation_metadata, cx); - }) - })??; - - anyhow::Ok(()) - } - }) - .detach_and_log_err(cx); - - Self { - model, - messages: Vec::new(), - composer_editor: cx.new_view(|cx| { - let mut editor = Editor::auto_height(80, cx); - editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); - editor.set_placeholder_text("Send a message…", cx); - editor - }), - saved_conversations, - saved_conversations_open: false, - list_state, - user_store, - fs, - language_registry, - project_index_button, - active_file_button, - project_index, - next_message_id: MessageId(0), - editing_message: None, - collapsed_messages: HashMap::default(), - pending_completion: None, - attachment_registry, - tool_registry, - markdown_style: MarkdownStyle { - code_block: gpui::TextStyleRefinement { - font_family: Some("Zed Mono".into()), - color: Some(cx.theme().colors().editor_foreground), - background_color: Some(cx.theme().colors().editor_background), - ..Default::default() - }, - inline_code: gpui::TextStyleRefinement { - font_family: Some("Zed Mono".into()), - // @nate: Could we add inline-code specific styles to the theme? - color: Some(cx.theme().colors().editor_foreground), - background_color: Some(cx.theme().colors().editor_background), - ..Default::default() - }, - rule_color: Color::Muted.color(cx), - block_quote_border_color: Color::Muted.color(cx), - block_quote: gpui::TextStyleRefinement { - color: Some(Color::Muted.color(cx)), - ..Default::default() - }, - link: gpui::TextStyleRefinement { - color: Some(Color::Accent.color(cx)), - underline: Some(gpui::UnderlineStyle { - thickness: px(1.), - color: Some(Color::Accent.color(cx)), - wavy: false, - }), - ..Default::default() - }, - syntax: cx.theme().syntax().clone(), - selection_background_color: { - let mut selection = cx.theme().players().local().selection; - selection.fade_out(0.7); - selection - }, - }, - } - } - - fn message_for_id(&self, id: MessageId) -> Option<&ChatMessage> { - self.messages.iter().find(|message| match message { - ChatMessage::User(message) => message.id == id, - ChatMessage::Assistant(message) => message.id == id, - }) - } - - fn toggle_saved_conversations(&mut self) { - self.saved_conversations_open = !self.saved_conversations_open; - } - - fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - // If we're currently editing a message, cancel the edit. - if self.editing_message.take().is_some() { - cx.notify(); - return; - } - - if self.pending_completion.take().is_some() { - if let Some(ChatMessage::Assistant(grouping)) = self.messages.last() { - if grouping.messages.is_empty() { - self.pop_message(cx); - } - } - return; - } - - cx.propagate(); - } - - fn submit(&mut self, Submit(mode): &Submit, cx: &mut ViewContext) { - if self.composer_editor.focus_handle(cx).is_focused(cx) { - // Don't allow multiple concurrent completions. - if self.pending_completion.is_some() { - cx.propagate(); - return; - } - - let message = self.composer_editor.update(cx, |composer_editor, cx| { - let text = composer_editor.text(cx); - let id = self.next_message_id.post_inc(); - let body = cx.new_view(|cx| { - Markdown::new( - text, - self.markdown_style.clone(), - Some(self.language_registry.clone()), - cx, - ) - }); - composer_editor.clear(cx); - - ChatMessage::User(UserMessage { - id, - body, - attachments: Vec::new(), - }) - }); - self.push_message(message, cx); - } else if let Some(editing_message) = self.editing_message.as_ref() { - let focus_handle = editing_message.body.focus_handle(cx); - if focus_handle.contains_focused(cx) { - if let Some(ChatMessage::User(user_message)) = - self.message_for_id(editing_message.id) - { - user_message.body.update(cx, |body, cx| { - body.reset(editing_message.body.read(cx).text(cx), cx); - }); - } - - self.truncate_messages(editing_message.id, cx); - - self.pending_completion.take(); - self.composer_editor.focus_handle(cx).focus(cx); - self.editing_message.take(); - } else { - log::error!("unexpected state: no user message editor is focused."); - return; - } - } else { - log::error!("unexpected state: no user message editor is focused."); - return; - } - - let mode = *mode; - self.pending_completion = Some(cx.spawn(move |this, mut cx| async move { - let attachments_task = this.update(&mut cx, |this, cx| { - let attachment_registry = this.attachment_registry.clone(); - attachment_registry.call_all_attachment_tools(cx) - }); - - let attachments = maybe!(async { - let attachments_task = attachments_task?; - let attachments = attachments_task.await?; - - anyhow::Ok(attachments) - }) - .await - .log_err() - .unwrap_or_default(); - - // Set the attachments to the _last_ user message - this.update(&mut cx, |this, _cx| { - if let Some(ChatMessage::User(message)) = this.messages.last_mut() { - message.attachments = attachments; - } - }) - .log_err(); - - Self::request_completion( - this.clone(), - mode, - MAX_COMPLETION_CALLS_PER_SUBMISSION, - &mut cx, - ) - .await - .log_err(); - - this.update(&mut cx, |this, _cx| { - this.pending_completion = None; - }) - .context("Failed to push new user message") - .log_err(); - })); - } - - async fn request_completion( - this: WeakView, - mode: SubmitMode, - limit: usize, - cx: &mut AsyncWindowContext, - ) -> Result<()> { - let mut call_count = 0; - loop { - let complete = async { - let (tool_definitions, model_name, messages) = this.update(cx, |this, cx| { - this.push_new_assistant_message(cx); - - let definitions = if call_count < limit - && matches!(mode, SubmitMode::Codebase | SubmitMode::Simple) - { - this.tool_registry.definitions() - } else { - Vec::new() - }; - call_count += 1; - - ( - definitions, - this.model.clone(), - this.completion_messages(cx), - ) - })?; - - let messages = messages.await?; - - let completion = cx.update(|cx| { - CompletionProvider::global(cx).complete( - model_name, - messages, - Vec::new(), - 1.0, - tool_definitions, - ) - }); - - let mut stream = completion?.await?; - while let Some(delta) = stream.next().await { - let delta = delta?; - this.update(cx, |this, cx| { - if let Some(ChatMessage::Assistant(AssistantMessage { messages, .. })) = - this.messages.last_mut() - { - if messages.is_empty() { - messages.push(AssistantMessagePart { - body: cx.new_view(|cx| { - Markdown::new( - "".into(), - this.markdown_style.clone(), - Some(this.language_registry.clone()), - cx, - ) - }), - tool_calls: Vec::new(), - }) - } - - let message = messages.last_mut().unwrap(); - - if let Some(content) = &delta.content { - message - .body - .update(cx, |message, cx| message.append(&content, cx)); - } - - for tool_call_delta in delta.tool_calls { - let index = tool_call_delta.index as usize; - if index >= message.tool_calls.len() { - message.tool_calls.resize_with(index + 1, Default::default); - } - let tool_call = &mut message.tool_calls[index]; - - if let Some(id) = &tool_call_delta.id { - tool_call.id.push_str(id); - } - - match tool_call_delta.variant { - Some(proto::tool_call_delta::Variant::Function( - tool_call_delta, - )) => { - this.tool_registry.update_tool_call( - tool_call, - tool_call_delta.name.as_deref(), - tool_call_delta.arguments.as_deref(), - cx, - ); - } - None => {} - } - } - - cx.notify(); - } else { - unreachable!() - } - })?; - } - - anyhow::Ok(()) - } - .await; - - let mut tool_tasks = Vec::new(); - this.update(cx, |this, cx| { - if let Some(ChatMessage::Assistant(AssistantMessage { - error: message_error, - messages, - .. - })) = this.messages.last_mut() - { - if let Err(error) = complete { - message_error.replace(SharedString::from(error.to_string())); - cx.notify(); - } else { - if let Some(current_message) = messages.last_mut() { - for tool_call in current_message.tool_calls.iter_mut() { - tool_tasks - .extend(this.tool_registry.execute_tool_call(tool_call, cx)); - } - } - } - } - })?; - - // This ends recursion on calling for responses after tools - if tool_tasks.is_empty() { - return Ok(()); - } - - join_all(tool_tasks.into_iter()).await; - } - } - - fn push_new_assistant_message(&mut self, cx: &mut ViewContext) { - // If the last message is a grouped assistant message, add to the grouped message - if let Some(ChatMessage::Assistant(AssistantMessage { messages, .. })) = - self.messages.last_mut() - { - messages.push(AssistantMessagePart { - body: cx.new_view(|cx| { - Markdown::new( - "".into(), - self.markdown_style.clone(), - Some(self.language_registry.clone()), - cx, - ) - }), - tool_calls: Vec::new(), - }); - return; - } - - let message = ChatMessage::Assistant(AssistantMessage { - id: self.next_message_id.post_inc(), - messages: vec![AssistantMessagePart { - body: cx.new_view(|cx| { - Markdown::new( - "".into(), - self.markdown_style.clone(), - Some(self.language_registry.clone()), - cx, - ) - }), - tool_calls: Vec::new(), - }], - error: None, - }); - self.push_message(message, cx); - } - - fn push_message(&mut self, message: ChatMessage, cx: &mut ViewContext) { - let old_len = self.messages.len(); - let focus_handle = Some(message.focus_handle(cx)); - self.messages.push(message); - self.list_state - .splice_focusable(old_len..old_len, focus_handle); - cx.notify(); - } - - fn pop_message(&mut self, cx: &mut ViewContext) { - if self.messages.is_empty() { - return; - } - - self.messages.pop(); - self.list_state - .splice(self.messages.len()..self.messages.len() + 1, 0); - cx.notify(); - } - - fn truncate_messages(&mut self, last_message_id: MessageId, cx: &mut ViewContext) { - if let Some(index) = self.messages.iter().position(|message| match message { - ChatMessage::User(message) => message.id == last_message_id, - ChatMessage::Assistant(message) => message.id == last_message_id, - }) { - self.list_state.splice(index + 1..self.messages.len(), 0); - self.messages.truncate(index + 1); - cx.notify(); - } - } - - fn is_message_collapsed(&self, id: &MessageId) -> bool { - self.collapsed_messages.get(id).copied().unwrap_or_default() - } - - fn toggle_message_collapsed(&mut self, id: MessageId) { - let entry = self.collapsed_messages.entry(id).or_insert(false); - *entry = !*entry; - } - - fn reset(&mut self) { - self.messages.clear(); - self.list_state.reset(0); - self.editing_message.take(); - self.collapsed_messages.clear(); - } - - fn new_conversation(&mut self, cx: &mut ViewContext) { - let messages = std::mem::take(&mut self.messages) - .into_iter() - .map(|message| self.serialize_message(message, cx)) - .collect::>(); - - self.reset(); - - let title = messages - .first() - .map(|message| match message { - SavedChatMessage::User { body, .. } => body.clone(), - SavedChatMessage::Assistant { messages, .. } => messages - .first() - .map(|message| message.body.to_string()) - .unwrap_or_default(), - }) - .unwrap_or_else(|| "A conversation with the assistant.".to_string()); - - let saved_conversation = SavedConversation { - version: "0.3.0".to_string(), - title, - messages, - }; - - let discriminant = 1; - - let path = CONVERSATIONS_DIR.join(&format!( - "{title} - {discriminant}.zed.{version}.json", - title = saved_conversation.title, - version = saved_conversation.version - )); - - cx.spawn({ - let fs = self.fs.clone(); - |_this, _cx| async move { - fs.create_dir(CONVERSATIONS_DIR.as_ref()).await?; - fs.atomic_write(path, serde_json::to_string(&saved_conversation)?) - .await?; - - anyhow::Ok(()) - } - }) - .detach_and_log_err(cx); - } - - fn render_error( - &self, - error: Option, - _ix: usize, - cx: &mut ViewContext, - ) -> AnyElement { - let theme = cx.theme(); - - if let Some(error) = error { - div() - .py_1() - .px_2() - .mx_neg_1() - .rounded_md() - .border_1() - .border_color(theme.status().error_border) - // .bg(theme.status().error_background) - .text_color(theme.status().error) - .child(error.clone()) - .into_any_element() - } else { - div().into_any_element() - } - } - - fn render_message(&self, ix: usize, cx: &mut ViewContext) -> AnyElement { - let is_first = ix == 0; - let is_last = ix == self.messages.len().saturating_sub(1); - - let padding = Spacing::Large.rems(cx); - - // Whenever there's a run of assistant messages, group as one Assistant UI element - - match &self.messages[ix] { - ChatMessage::User(UserMessage { - id, - body, - attachments, - }) => div() - .id(SharedString::from(format!("message-{}-container", id.0))) - .when(is_first, |this| this.pt(padding)) - .map(|element| { - if let Some(editing_message) = self.editing_message.as_ref() { - if editing_message.id == *id { - return element.child(Composer::new( - editing_message.body.clone(), - self.project_index_button.clone(), - self.active_file_button.clone(), - crate::ui::ModelSelector::new( - cx.view().downgrade(), - self.model.clone(), - ) - .into_any_element(), - )); - } - } - - element - .on_click(cx.listener({ - let id = *id; - let body = body.clone(); - move |assistant_chat, event: &ClickEvent, cx| { - if event.up.click_count == 2 { - let body = cx.new_view(|cx| { - let mut editor = Editor::auto_height(80, cx); - let source = Arc::from(body.read(cx).source()); - editor.set_text(source, cx); - editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); - editor - }); - assistant_chat.editing_message = Some(EditingMessage { - id, - body: body.clone(), - }); - body.focus_handle(cx).focus(cx); - } - } - })) - .child( - crate::ui::ChatMessage::new( - *id, - UserOrAssistant::User(self.user_store.read(cx).current_user()), - vec![ - body.clone().into_any_element(), - h_flex() - .gap_2() - .children( - attachments - .iter() - .map(|attachment| attachment.view.clone()), - ) - .into_any_element(), - ], - self.is_message_collapsed(id), - Box::new(cx.listener({ - let id = *id; - move |assistant_chat, _event, _cx| { - assistant_chat.toggle_message_collapsed(id) - } - })), - ) - // TODO: Wire up selections. - .selected(is_last), - ) - }) - .into_any(), - ChatMessage::Assistant(AssistantMessage { - id, - messages, - error, - .. - }) => { - let mut message_elements = Vec::new(); - - for message in messages { - if !message.body.read(cx).source().is_empty() { - message_elements.push(div().child(message.body.clone()).into_any()) - } - - let tools = message - .tool_calls - .iter() - .filter_map(|tool_call| self.tool_registry.render_tool_call(tool_call, cx)) - .collect::>(); - - if !tools.is_empty() { - message_elements.push(div().children(tools).into_any()) - } - } - - if message_elements.is_empty() { - message_elements.push(::ui::Label::new("Researching...").into_any_element()) - } - - div() - .when(is_first, |this| this.pt(padding)) - .child( - crate::ui::ChatMessage::new( - *id, - UserOrAssistant::Assistant, - message_elements, - self.is_message_collapsed(id), - Box::new(cx.listener({ - let id = *id; - move |assistant_chat, _event, _cx| { - assistant_chat.toggle_message_collapsed(id) - } - })), - ) - // TODO: Wire up selections. - .selected(is_last), - ) - .child(self.render_error(error.clone(), ix, cx)) - .into_any() - } - } - } - - fn completion_messages(&self, cx: &mut WindowContext) -> Task>> { - let project_index = self.project_index.read(cx); - let project = project_index.project(); - let fs = project_index.fs(); - - let mut project_context = ProjectContext::new(project, fs); - let mut completion_messages = Vec::new(); - - for message in &self.messages { - match message { - ChatMessage::User(UserMessage { - body, attachments, .. - }) => { - for attachment in attachments { - if let Some(content) = attachment.generate(&mut project_context, cx) { - completion_messages.push(CompletionMessage::System { content }); - } - } - - // Show user's message last so that the assistant is grounded in the user's request - completion_messages.push(CompletionMessage::User { - content: body.read(cx).source().to_string(), - }); - } - ChatMessage::Assistant(AssistantMessage { messages, .. }) => { - for message in messages { - let body = message.body.clone(); - - if body.read(cx).source().is_empty() && message.tool_calls.is_empty() { - continue; - } - - let tool_calls_from_assistant = message - .tool_calls - .iter() - .map(|tool_call| ToolCall { - content: ToolCallContent::Function { - function: FunctionContent { - name: tool_call.name.clone(), - arguments: tool_call.arguments.clone(), - }, - }, - id: tool_call.id.clone(), - }) - .collect(); - - completion_messages.push(CompletionMessage::Assistant { - content: Some(body.read(cx).source().to_string()), - tool_calls: tool_calls_from_assistant, - }); - - for tool_call in &message.tool_calls { - // Every tool call _must_ have a result by ID, otherwise OpenAI will error. - let content = self.tool_registry.content_for_tool_call( - tool_call, - &mut project_context, - cx, - ); - completion_messages.push(CompletionMessage::Tool { - content, - tool_call_id: tool_call.id.clone(), - }); - } - } - } - } - } - - let system_message = project_context.generate_system_message(cx); - - cx.background_executor().spawn(async move { - let content = system_message.await?; - completion_messages.insert(0, CompletionMessage::System { content }); - Ok(completion_messages) - }) - } - - fn serialize_message( - &self, - message: ChatMessage, - cx: &mut ViewContext, - ) -> SavedChatMessage { - match message { - ChatMessage::User(message) => SavedChatMessage::User { - id: message.id, - body: message.body.read(cx).source().into(), - attachments: message - .attachments - .iter() - .map(|attachment| { - self.attachment_registry - .serialize_user_attachment(attachment) - }) - .collect(), - }, - ChatMessage::Assistant(message) => SavedChatMessage::Assistant { - id: message.id, - error: message.error, - messages: message - .messages - .iter() - .map(|message| SavedAssistantMessagePart { - body: message.body.read(cx).source().to_string().into(), - tool_calls: message - .tool_calls - .iter() - .filter_map(|tool_call| { - self.tool_registry - .serialize_tool_call(tool_call, cx) - .log_err() - }) - .collect(), - }) - .collect(), - }, - } - } -} - -impl Render for AssistantChat { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let header_height = Spacing::Small.rems(cx) * 2.0 + ButtonSize::Default.rems(); - - div() - .relative() - .flex_1() - .v_flex() - .key_context("AssistantChat") - .on_action(cx.listener(Self::submit)) - .on_action(cx.listener(Self::cancel)) - .text_color(Color::Default.color(cx)) - .child(list(self.list_state.clone()).flex_1().pt(header_height)) - .child( - h_flex() - .absolute() - .top_0() - .justify_between() - .w_full() - .h(header_height) - .p(Spacing::Small.rems(cx)) - .child( - IconButton::new( - "toggle-saved-conversations", - if self.saved_conversations_open { - IconName::ChevronRight - } else { - IconName::ChevronLeft - }, - ) - .on_click(cx.listener(|this, _event, _cx| { - this.toggle_saved_conversations(); - })) - .tooltip(move |cx| Tooltip::text("Switch Conversations", cx)), - ) - .child( - h_flex() - .gap(Spacing::Large.rems(cx)) - .child( - IconButton::new("new-conversation", IconName::Plus) - .on_click(cx.listener(move |this, _event, cx| { - this.new_conversation(cx); - })) - .tooltip(move |cx| Tooltip::text("New Context", cx)), - ) - .child( - IconButton::new("assistant-menu", IconName::Menu) - .disabled(true) - .tooltip(move |cx| { - Tooltip::text( - "Coming soon – Assistant settings & controls", - cx, - ) - }), - ), - ), - ) - .when(self.saved_conversations_open, |element| { - element.child( - h_flex() - .absolute() - .top(header_height) - .w_full() - .child(self.saved_conversations.clone()), - ) - }) - .child(Composer::new( - self.composer_editor.clone(), - self.project_index_button.clone(), - self.active_file_button.clone(), - crate::ui::ModelSelector::new(cx.view().downgrade(), self.model.clone()) - .into_any_element(), - )) - } -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)] -pub struct MessageId(usize); - -impl MessageId { - fn post_inc(&mut self) -> Self { - let id = *self; - self.0 += 1; - id - } -} - -enum ChatMessage { - User(UserMessage), - Assistant(AssistantMessage), -} - -impl ChatMessage { - fn focus_handle(&self, cx: &AppContext) -> Option { - match self { - ChatMessage::User(message) => Some(message.body.focus_handle(cx)), - ChatMessage::Assistant(_) => None, - } - } -} - -struct UserMessage { - pub id: MessageId, - pub body: View, - pub attachments: Vec, -} - -struct AssistantMessagePart { - pub body: View, - pub tool_calls: Vec, -} - -struct AssistantMessage { - pub id: MessageId, - pub messages: Vec, - pub error: Option, -} diff --git a/crates/assistant2/src/assistant_settings.rs b/crates/assistant2/src/assistant_settings.rs deleted file mode 100644 index 7d532faaeb..0000000000 --- a/crates/assistant2/src/assistant_settings.rs +++ /dev/null @@ -1,26 +0,0 @@ -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; - -#[derive(Default, Debug, Deserialize, Serialize, Clone)] -pub struct AssistantSettings { - pub enabled: bool, -} - -#[derive(Default, Debug, Deserialize, Serialize, Clone, JsonSchema)] -pub struct AssistantSettingsContent { - pub enabled: Option, -} - -impl Settings for AssistantSettings { - const KEY: Option<&'static str> = Some("assistant_v2"); - - type FileContent = AssistantSettingsContent; - - fn load( - sources: SettingsSources, - _: &mut gpui::AppContext, - ) -> anyhow::Result { - Ok(sources.json_merge().unwrap_or_else(|_| Default::default())) - } -} diff --git a/crates/assistant2/src/attachments.rs b/crates/assistant2/src/attachments.rs deleted file mode 100644 index 2187f855a4..0000000000 --- a/crates/assistant2/src/attachments.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod active_file; - -pub use active_file::*; diff --git a/crates/assistant2/src/attachments/active_file.rs b/crates/assistant2/src/attachments/active_file.rs deleted file mode 100644 index 744d92689f..0000000000 --- a/crates/assistant2/src/attachments/active_file.rs +++ /dev/null @@ -1,144 +0,0 @@ -use std::{path::PathBuf, sync::Arc}; - -use anyhow::{anyhow, Result}; -use assistant_tooling::{AttachmentOutput, LanguageModelAttachment, ProjectContext}; -use editor::Editor; -use gpui::{Render, Task, View, WeakModel, WeakView}; -use language::Buffer; -use project::ProjectPath; -use serde::{Deserialize, Serialize}; -use ui::{prelude::*, ButtonLike, Tooltip, WindowContext}; -use util::maybe; -use workspace::Workspace; - -#[derive(Serialize, Deserialize)] -pub struct ActiveEditorAttachment { - #[serde(skip)] - buffer: Option>, - path: Option, -} - -pub struct FileAttachmentView { - project_path: Option, - buffer: Option>, - error: Option, -} - -impl Render for FileAttachmentView { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - if let Some(error) = &self.error { - return div().child(error.to_string()).into_any_element(); - } - - let filename: SharedString = self - .project_path - .as_ref() - .and_then(|p| p.path.file_name()?.to_str()) - .unwrap_or("Untitled") - .to_string() - .into(); - - ButtonLike::new("file-attachment") - .child( - h_flex() - .gap_1() - .bg(cx.theme().colors().editor_background) - .rounded_md() - .child(ui::Icon::new(IconName::File)) - .child(filename.clone()), - ) - .tooltip(move |cx| Tooltip::with_meta("File Attached", None, filename.clone(), cx)) - .into_any_element() - } -} - -impl AttachmentOutput for FileAttachmentView { - fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String { - if let Some(path) = &self.project_path { - project.add_file(path.clone()); - return format!("current file: {}", path.path.display()); - } - - if let Some(buffer) = self.buffer.as_ref().and_then(|buffer| buffer.upgrade()) { - return format!("current untitled buffer text:\n{}", buffer.read(cx).text()); - } - - String::new() - } -} - -pub struct ActiveEditorAttachmentTool { - workspace: WeakView, -} - -impl ActiveEditorAttachmentTool { - pub fn new(workspace: WeakView, _cx: &mut WindowContext) -> Self { - Self { workspace } - } -} - -impl LanguageModelAttachment for ActiveEditorAttachmentTool { - type Output = ActiveEditorAttachment; - type View = FileAttachmentView; - - fn name(&self) -> Arc { - "active-editor-attachment".into() - } - - fn run(&self, cx: &mut WindowContext) -> Task> { - Task::ready(maybe!({ - let active_buffer = self - .workspace - .update(cx, |workspace, cx| { - workspace - .active_item(cx) - .and_then(|item| Some(item.act_as::(cx)?.read(cx).buffer().clone())) - })? - .ok_or_else(|| anyhow!("no active buffer"))?; - - let buffer = active_buffer.read(cx); - - if let Some(buffer) = buffer.as_singleton() { - let path = project::File::from_dyn(buffer.read(cx).file()) - .and_then(|file| file.worktree.read(cx).absolutize(&file.path).ok()); - return Ok(ActiveEditorAttachment { - buffer: Some(buffer.downgrade()), - path, - }); - } else { - Err(anyhow!("no active buffer")) - } - })) - } - - fn view( - &self, - output: Result, - cx: &mut WindowContext, - ) -> View { - let error; - let project_path; - let buffer; - match output { - Ok(output) => { - error = None; - let workspace = self.workspace.upgrade().unwrap(); - let project = workspace.read(cx).project(); - project_path = output - .path - .and_then(|path| project.read(cx).project_path_for_absolute_path(&path, cx)); - buffer = output.buffer; - } - Err(err) => { - error = Some(err); - buffer = None; - project_path = None; - } - } - cx.new_view(|_cx| FileAttachmentView { - project_path, - buffer, - error, - }) - } -} diff --git a/crates/assistant2/src/completion_provider.rs b/crates/assistant2/src/completion_provider.rs deleted file mode 100644 index deb87de868..0000000000 --- a/crates/assistant2/src/completion_provider.rs +++ /dev/null @@ -1,179 +0,0 @@ -use anyhow::Result; -use assistant_tooling::ToolFunctionDefinition; -use client::{proto, Client}; -use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt}; -use gpui::Global; -use std::sync::Arc; - -pub use open_ai::RequestMessage as CompletionMessage; - -#[derive(Clone)] -pub struct CompletionProvider(Arc); - -impl CompletionProvider { - pub fn new(backend: impl CompletionProviderBackend) -> Self { - Self(Arc::new(backend)) - } - - pub fn default_model(&self) -> String { - self.0.default_model() - } - - pub fn available_models(&self) -> Vec { - self.0.available_models() - } - - pub fn complete( - &self, - model: String, - messages: Vec, - stop: Vec, - temperature: f32, - tools: Vec, - ) -> BoxFuture<'static, Result>>> - { - self.0.complete(model, messages, stop, temperature, tools) - } -} - -impl Global for CompletionProvider {} - -pub trait CompletionProviderBackend: 'static { - fn default_model(&self) -> String; - fn available_models(&self) -> Vec; - fn complete( - &self, - model: String, - messages: Vec, - stop: Vec, - temperature: f32, - tools: Vec, - ) -> BoxFuture<'static, Result>>>; -} - -pub struct CloudCompletionProvider { - client: Arc, -} - -impl CloudCompletionProvider { - pub fn new(client: Arc) -> Self { - Self { client } - } -} - -impl CompletionProviderBackend for CloudCompletionProvider { - fn default_model(&self) -> String { - "gpt-4-turbo".into() - } - - fn available_models(&self) -> Vec { - vec!["gpt-4-turbo".into(), "gpt-4".into(), "gpt-3.5-turbo".into()] - } - - fn complete( - &self, - model: String, - messages: Vec, - stop: Vec, - temperature: f32, - tools: Vec, - ) -> BoxFuture<'static, Result>>> - { - let client = self.client.clone(); - let tools: Vec = tools - .iter() - .filter_map(|tool| { - Some(proto::ChatCompletionTool { - variant: Some(proto::chat_completion_tool::Variant::Function( - proto::chat_completion_tool::FunctionObject { - name: tool.name.clone(), - description: Some(tool.description.clone()), - parameters: Some(serde_json::to_string(&tool.parameters).ok()?), - }, - )), - }) - }) - .collect(); - - let tool_choice = match tools.is_empty() { - true => None, - false => Some("auto".into()), - }; - - async move { - let stream = client - .request_stream(proto::CompleteWithLanguageModel { - model, - messages: messages - .into_iter() - .map(|message| match message { - CompletionMessage::Assistant { - content, - tool_calls, - } => proto::LanguageModelRequestMessage { - role: proto::LanguageModelRole::LanguageModelAssistant as i32, - content: content.unwrap_or_default(), - tool_call_id: None, - tool_calls: tool_calls - .into_iter() - .map(|tool_call| match tool_call.content { - open_ai::ToolCallContent::Function { function } => { - proto::ToolCall { - id: tool_call.id, - variant: Some(proto::tool_call::Variant::Function( - proto::tool_call::FunctionCall { - name: function.name, - arguments: function.arguments, - }, - )), - } - } - }) - .collect(), - }, - CompletionMessage::User { content } => { - proto::LanguageModelRequestMessage { - role: proto::LanguageModelRole::LanguageModelUser as i32, - content, - tool_call_id: None, - tool_calls: Vec::new(), - } - } - CompletionMessage::System { content } => { - proto::LanguageModelRequestMessage { - role: proto::LanguageModelRole::LanguageModelSystem as i32, - content, - tool_calls: Vec::new(), - tool_call_id: None, - } - } - CompletionMessage::Tool { - content, - tool_call_id, - } => proto::LanguageModelRequestMessage { - role: proto::LanguageModelRole::LanguageModelTool as i32, - content, - tool_call_id: Some(tool_call_id), - tool_calls: Vec::new(), - }, - }) - .collect(), - stop, - temperature, - tool_choice, - tools, - }) - .await?; - - Ok(stream - .filter_map(|response| async move { - match response { - Ok(mut response) => Some(Ok(response.choices.pop()?.delta?)), - Err(error) => Some(Err(error)), - } - }) - .boxed()) - } - .boxed() - } -} diff --git a/crates/assistant2/src/saved_conversation.rs b/crates/assistant2/src/saved_conversation.rs deleted file mode 100644 index a46f8a54c9..0000000000 --- a/crates/assistant2/src/saved_conversation.rs +++ /dev/null @@ -1,90 +0,0 @@ -use std::cmp::Reverse; -use std::ffi::OsStr; -use std::path::PathBuf; -use std::sync::Arc; - -use anyhow::Result; -use assistant_tooling::{SavedToolFunctionCall, SavedUserAttachment}; -use fs::Fs; -use futures::StreamExt; -use gpui::SharedString; -use regex::Regex; -use serde::{Deserialize, Serialize}; -use util::paths::CONVERSATIONS_DIR; - -use crate::MessageId; - -#[derive(Serialize, Deserialize)] -pub struct SavedConversation { - /// The schema version of the conversation. - pub version: String, - /// The title of the conversation, generated by the Assistant. - pub title: String, - pub messages: Vec, -} - -#[derive(Serialize, Deserialize)] -pub enum SavedChatMessage { - User { - id: MessageId, - body: String, - attachments: Vec, - }, - Assistant { - id: MessageId, - messages: Vec, - error: Option, - }, -} - -#[derive(Serialize, Deserialize)] -pub struct SavedAssistantMessagePart { - pub body: SharedString, - pub tool_calls: Vec, -} - -pub struct SavedConversationMetadata { - pub title: String, - pub path: PathBuf, - pub mtime: chrono::DateTime, -} - -impl SavedConversationMetadata { - pub async fn list(fs: Arc) -> Result> { - fs.create_dir(&CONVERSATIONS_DIR).await?; - - let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?; - let mut conversations = Vec::new(); - while let Some(path) = paths.next().await { - let path = path?; - if path.extension() != Some(OsStr::new("json")) { - continue; - } - - let pattern = r" - \d+.zed.\d.\d.\d.json$"; - let re = Regex::new(pattern).unwrap(); - - let metadata = fs.metadata(&path).await?; - if let Some((file_name, metadata)) = path - .file_name() - .and_then(|name| name.to_str()) - .zip(metadata) - { - // This is used to filter out conversations saved by the old assistant. - if !re.is_match(file_name) { - continue; - } - - let title = re.replace(file_name, ""); - conversations.push(Self { - title: title.into_owned(), - path, - mtime: metadata.mtime.into(), - }); - } - } - conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime)); - - Ok(conversations) - } -} diff --git a/crates/assistant2/src/saved_conversations.rs b/crates/assistant2/src/saved_conversations.rs deleted file mode 100644 index 4ddb90d7e4..0000000000 --- a/crates/assistant2/src/saved_conversations.rs +++ /dev/null @@ -1,196 +0,0 @@ -use std::sync::Arc; - -use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; -use gpui::{AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, View, WeakView}; -use picker::{Picker, PickerDelegate}; -use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing}; -use util::ResultExt; - -use crate::saved_conversation::SavedConversationMetadata; - -pub struct SavedConversations { - focus_handle: FocusHandle, - picker: Option>>, -} - -impl EventEmitter for SavedConversations {} - -impl FocusableView for SavedConversations { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - if let Some(picker) = self.picker.as_ref() { - picker.focus_handle(cx) - } else { - self.focus_handle.clone() - } - } -} - -impl SavedConversations { - pub fn new(cx: &mut ViewContext) -> Self { - Self { - focus_handle: cx.focus_handle(), - picker: None, - } - } - - pub fn init( - &mut self, - saved_conversations: Vec, - cx: &mut ViewContext, - ) { - let delegate = - SavedConversationPickerDelegate::new(cx.view().downgrade(), saved_conversations); - self.picker = Some(cx.new_view(|cx| Picker::uniform_list(delegate, cx).modal(false))); - } -} - -impl Render for SavedConversations { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - v_flex() - .w_full() - .bg(cx.theme().colors().panel_background) - .children(self.picker.clone()) - } -} - -pub struct SavedConversationPickerDelegate { - view: WeakView, - saved_conversations: Vec, - selected_index: usize, - matches: Vec, -} - -impl SavedConversationPickerDelegate { - pub fn new( - weak_view: WeakView, - saved_conversations: Vec, - ) -> Self { - let matches = saved_conversations - .iter() - .map(|conversation| StringMatch { - candidate_id: 0, - score: 0.0, - positions: Default::default(), - string: conversation.title.clone(), - }) - .collect(); - - Self { - view: weak_view, - saved_conversations, - selected_index: 0, - matches, - } - } -} - -impl PickerDelegate for SavedConversationPickerDelegate { - type ListItem = ui::ListItem; - - fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { - "Select saved conversation...".into() - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext>) { - self.selected_index = ix; - } - - fn update_matches( - &mut self, - query: String, - cx: &mut ViewContext>, - ) -> gpui::Task<()> { - let background_executor = cx.background_executor().clone(); - let candidates = self - .saved_conversations - .iter() - .enumerate() - .map(|(id, conversation)| { - let text = conversation.title.clone(); - - StringMatchCandidate { - id, - char_bag: text.as_str().into(), - string: text, - } - }) - .collect::>(); - - cx.spawn(move |this, mut cx| async move { - let matches = if query.is_empty() { - candidates - .into_iter() - .enumerate() - .map(|(index, candidate)| StringMatch { - candidate_id: index, - string: candidate.string, - positions: Vec::new(), - score: 0.0, - }) - .collect() - } else { - match_strings( - &candidates, - &query, - false, - 100, - &Default::default(), - background_executor, - ) - .await - }; - - this.update(&mut cx, |this, _cx| { - this.delegate.matches = matches; - this.delegate.selected_index = this - .delegate - .selected_index - .min(this.delegate.matches.len().saturating_sub(1)); - }) - .log_err(); - }) - } - - fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext>) { - if self.matches.is_empty() { - self.dismissed(cx); - return; - } - - // TODO: Implement selecting a saved conversation. - } - - fn dismissed(&mut self, cx: &mut ui::prelude::ViewContext>) { - self.view - .update(cx, |_, cx| cx.emit(DismissEvent)) - .log_err(); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _cx: &mut ViewContext>, - ) -> Option { - let conversation_match = &self.matches[ix]; - let _conversation = &self.saved_conversations[conversation_match.candidate_id]; - - Some( - ListItem::new(ix) - .spacing(ListItemSpacing::Sparse) - .selected(selected) - .child(HighlightedLabel::new( - conversation_match.string.clone(), - conversation_match.positions.clone(), - )), - ) - } -} diff --git a/crates/assistant2/src/tools.rs b/crates/assistant2/src/tools.rs deleted file mode 100644 index f60f41c586..0000000000 --- a/crates/assistant2/src/tools.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod annotate_code; -mod create_buffer; -mod project_index; - -pub use annotate_code::*; -pub use create_buffer::*; -pub use project_index::*; diff --git a/crates/assistant2/src/tools/annotate_code.rs b/crates/assistant2/src/tools/annotate_code.rs deleted file mode 100644 index fc9e84351a..0000000000 --- a/crates/assistant2/src/tools/annotate_code.rs +++ /dev/null @@ -1,304 +0,0 @@ -use anyhow::Result; -use assistant_tooling::{LanguageModelTool, ProjectContext, ToolView}; -use editor::{ - display_map::{BlockContext, BlockDisposition, BlockProperties, BlockStyle}, - Editor, MultiBuffer, -}; -use futures::{channel::mpsc::UnboundedSender, StreamExt as _}; -use gpui::{prelude::*, AnyElement, AsyncWindowContext, Model, Task, View, WeakView}; -use language::ToPoint; -use project::{search::SearchQuery, Project, ProjectPath}; -use schemars::JsonSchema; -use serde::Deserialize; -use std::path::Path; -use ui::prelude::*; -use util::ResultExt; -use workspace::Workspace; - -pub struct AnnotationTool { - workspace: WeakView, - project: Model, -} - -impl AnnotationTool { - pub fn new(workspace: WeakView, project: Model) -> Self { - Self { workspace, project } - } -} - -#[derive(Default, Debug, Deserialize, JsonSchema, Clone)] -pub struct AnnotationInput { - /// Name for this set of annotations - #[serde(default = "default_title")] - title: String, - /// Excerpts from the file to show to the user. - excerpts: Vec, -} - -fn default_title() -> String { - "Untitled".to_string() -} - -#[derive(Debug, Deserialize, JsonSchema, Clone)] -struct Excerpt { - /// Path to the file - path: String, - /// A short, distinctive string that appears in the file, used to define a location in the file. - text_passage: String, - /// Text to display above the code excerpt - annotation: String, -} - -impl LanguageModelTool for AnnotationTool { - type View = AnnotationResultView; - - fn name(&self) -> String { - "annotate_code".to_string() - } - - fn description(&self) -> String { - "Dynamically annotate symbols in the current codebase. Opens a buffer in a panel in their editor, to the side of the conversation. The annotations are shown in the editor as a block decoration.".to_string() - } - - fn view(&self, cx: &mut WindowContext) -> View { - cx.new_view(|cx| { - let (tx, mut rx) = futures::channel::mpsc::unbounded(); - cx.spawn(|view, mut cx| async move { - while let Some(excerpt) = rx.next().await { - AnnotationResultView::add_excerpt(view.clone(), excerpt, &mut cx).await?; - } - anyhow::Ok(()) - }) - .detach(); - - AnnotationResultView { - project: self.project.clone(), - workspace: self.workspace.clone(), - tx, - pending_excerpt: None, - added_editor_to_workspace: false, - editor: None, - error: None, - rendered_excerpt_count: 0, - } - }) - } -} - -pub struct AnnotationResultView { - workspace: WeakView, - project: Model, - pending_excerpt: Option, - added_editor_to_workspace: bool, - editor: Option>, - tx: UnboundedSender, - error: Option, - rendered_excerpt_count: usize, -} - -impl AnnotationResultView { - async fn add_excerpt( - this: WeakView, - excerpt: Excerpt, - cx: &mut AsyncWindowContext, - ) -> Result<()> { - let project = this.update(cx, |this, _cx| this.project.clone())?; - - let worktree_id = project.update(cx, |project, cx| { - let worktree = project.worktrees().next()?; - let worktree_id = worktree.read(cx).id(); - Some(worktree_id) - })?; - - let worktree_id = if let Some(worktree_id) = worktree_id { - worktree_id - } else { - return Err(anyhow::anyhow!("No worktree found")); - }; - - let buffer_task = project.update(cx, |project, cx| { - project.open_buffer( - ProjectPath { - worktree_id, - path: Path::new(&excerpt.path).into(), - }, - cx, - ) - })?; - - let buffer = match buffer_task.await { - Ok(buffer) => buffer, - Err(error) => { - return this.update(cx, |this, cx| { - this.error = Some(error); - cx.notify(); - }) - } - }; - - let snapshot = buffer.update(cx, |buffer, _cx| buffer.snapshot())?; - let query = SearchQuery::text(&excerpt.text_passage, false, false, false, vec![], vec![])?; - let matches = query.search(&snapshot, None).await; - let Some(first_match) = matches.first() else { - log::warn!( - "text {:?} does not appear in '{}'", - excerpt.text_passage, - excerpt.path - ); - return Ok(()); - }; - - this.update(cx, |this, cx| { - let mut start = first_match.start.to_point(&snapshot); - start.column = 0; - - if let Some(editor) = &this.editor { - editor.update(cx, |editor, cx| { - let ranges = editor.buffer().update(cx, |multibuffer, cx| { - multibuffer.push_excerpts_with_context_lines( - buffer.clone(), - vec![start..start], - 5, - cx, - ) - }); - - let annotation = SharedString::from(excerpt.annotation); - editor.insert_blocks( - [BlockProperties { - position: ranges[0].start, - height: annotation.split('\n').count() as u8 + 1, - style: BlockStyle::Fixed, - render: Box::new(move |cx| Self::render_note_block(&annotation, cx)), - disposition: BlockDisposition::Above, - }], - None, - cx, - ); - }); - - if !this.added_editor_to_workspace { - this.added_editor_to_workspace = true; - this.workspace - .update(cx, |workspace, cx| { - workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx); - }) - .log_err(); - } - } - })?; - - Ok(()) - } - - fn render_note_block(explanation: &SharedString, cx: &mut BlockContext) -> AnyElement { - let anchor_x = cx.anchor_x; - let gutter_width = cx.gutter_dimensions.width; - - h_flex() - .w_full() - .py_2() - .border_y_1() - .border_color(cx.theme().colors().border) - .child( - h_flex() - .justify_center() - .w(gutter_width) - .child(Icon::new(IconName::Ai).color(Color::Hint)), - ) - .child( - h_flex() - .w_full() - .ml(anchor_x - gutter_width) - .child(explanation.clone()), - ) - .into_any_element() - } -} - -impl Render for AnnotationResultView { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - if let Some(error) = &self.error { - ui::Label::new(error.to_string()).into_any_element() - } else { - ui::Label::new(SharedString::from(format!( - "Opened a buffer with {} excerpts", - self.rendered_excerpt_count - ))) - .into_any_element() - } - } -} - -impl ToolView for AnnotationResultView { - type Input = AnnotationInput; - type SerializedState = Option; - - fn generate(&self, _: &mut ProjectContext, _: &mut ViewContext) -> String { - if let Some(error) = &self.error { - format!("Failed to create buffer: {error:?}") - } else { - format!( - "opened {} excerpts in a buffer", - self.rendered_excerpt_count - ) - } - } - - fn set_input(&mut self, mut input: Self::Input, cx: &mut ViewContext) { - let editor = if let Some(editor) = &self.editor { - editor.clone() - } else { - let multibuffer = cx.new_model(|_cx| { - MultiBuffer::new(0, language::Capability::ReadWrite).with_title(String::new()) - }); - let editor = cx.new_view(|cx| { - Editor::for_multibuffer(multibuffer.clone(), Some(self.project.clone()), true, cx) - }); - - self.editor = Some(editor.clone()); - editor - }; - - editor.update(cx, |editor, cx| { - editor.buffer().update(cx, |multibuffer, cx| { - if multibuffer.title(cx) != input.title { - multibuffer.set_title(input.title.clone(), cx); - } - }); - - self.pending_excerpt = input.excerpts.pop(); - for excerpt in input.excerpts.iter().skip(self.rendered_excerpt_count) { - self.tx.unbounded_send(excerpt.clone()).ok(); - } - self.rendered_excerpt_count = input.excerpts.len(); - }); - - cx.notify(); - } - - fn execute(&mut self, _cx: &mut ViewContext) -> Task> { - if let Some(excerpt) = self.pending_excerpt.take() { - self.rendered_excerpt_count += 1; - self.tx.unbounded_send(excerpt.clone()).ok(); - } - - self.tx.close_channel(); - Task::ready(Ok(())) - } - - fn serialize(&self, _cx: &mut ViewContext) -> Self::SerializedState { - self.error.as_ref().map(|error| error.to_string()) - } - - fn deserialize( - &mut self, - output: Self::SerializedState, - _cx: &mut ViewContext, - ) -> Result<()> { - if let Some(error_message) = output { - self.error = Some(anyhow::anyhow!("{}", error_message)); - } - Ok(()) - } -} diff --git a/crates/assistant2/src/tools/create_buffer.rs b/crates/assistant2/src/tools/create_buffer.rs deleted file mode 100644 index 894ee75d55..0000000000 --- a/crates/assistant2/src/tools/create_buffer.rs +++ /dev/null @@ -1,145 +0,0 @@ -use anyhow::{anyhow, Result}; -use assistant_tooling::{LanguageModelTool, ProjectContext, ToolView}; -use editor::Editor; -use gpui::{prelude::*, Model, Task, View, WeakView}; -use project::Project; -use schemars::JsonSchema; -use serde::Deserialize; -use ui::prelude::*; -use util::ResultExt; -use workspace::Workspace; - -pub struct CreateBufferTool { - workspace: WeakView, - project: Model, -} - -impl CreateBufferTool { - pub fn new(workspace: WeakView, project: Model) -> Self { - Self { workspace, project } - } -} - -#[derive(Debug, Clone, Deserialize, JsonSchema)] -pub struct CreateBufferInput { - /// The contents of the buffer. - text: String, - - /// The name of the language to use for the buffer. - /// - /// This should be a human-readable name, like "Rust", "JavaScript", or "Python". - language: String, -} - -impl LanguageModelTool for CreateBufferTool { - type View = CreateBufferView; - - fn name(&self) -> String { - "create_file".to_string() - } - - fn description(&self) -> String { - "Create a new untitled file in the current codebase. Side effect: opens it in a new pane/tab for the user to edit.".to_string() - } - - fn view(&self, cx: &mut WindowContext) -> View { - cx.new_view(|_cx| CreateBufferView { - workspace: self.workspace.clone(), - project: self.project.clone(), - input: None, - error: None, - }) - } -} - -pub struct CreateBufferView { - workspace: WeakView, - project: Model, - input: Option, - error: Option, -} - -impl Render for CreateBufferView { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - ui::Label::new("Opening a buffer") - } -} - -impl ToolView for CreateBufferView { - type Input = CreateBufferInput; - - type SerializedState = (); - - fn generate(&self, _project: &mut ProjectContext, _cx: &mut ViewContext) -> String { - let Some(input) = self.input.as_ref() else { - return "No input".to_string(); - }; - - match &self.error { - None => format!("Created a new {} buffer", input.language), - Some(err) => format!("Failed to create buffer: {err:?}"), - } - } - - fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext) { - self.input = Some(input); - cx.notify(); - } - - fn execute(&mut self, cx: &mut ViewContext) -> Task> { - cx.spawn({ - let workspace = self.workspace.clone(); - let project = self.project.clone(); - let input = self.input.clone(); - |_this, mut cx| async move { - let input = input.ok_or_else(|| anyhow!("no input"))?; - - let text = input.text.clone(); - let language_name = input.language.clone(); - let language = cx - .update(|cx| { - project - .read(cx) - .languages() - .language_for_name(&language_name) - })? - .await?; - - let buffer = cx - .update(|cx| project.update(cx, |project, cx| project.create_buffer(cx)))? - .await?; - - buffer.update(&mut cx, |buffer, cx| { - buffer.edit([(0..0, text)], None, cx); - buffer.set_language(Some(language), cx) - })?; - - workspace - .update(&mut cx, |workspace, cx| { - workspace.add_item_to_active_pane( - Box::new( - cx.new_view(|cx| Editor::for_buffer(buffer, Some(project), cx)), - ), - None, - cx, - ); - }) - .log_err(); - - Ok(()) - } - }) - } - - fn serialize(&self, _cx: &mut ViewContext) -> Self::SerializedState { - () - } - - fn deserialize( - &mut self, - _output: Self::SerializedState, - _cx: &mut ViewContext, - ) -> Result<()> { - Ok(()) - } -} diff --git a/crates/assistant2/src/tools/project_index.rs b/crates/assistant2/src/tools/project_index.rs deleted file mode 100644 index 5d28d470f3..0000000000 --- a/crates/assistant2/src/tools/project_index.rs +++ /dev/null @@ -1,428 +0,0 @@ -use anyhow::Result; -use assistant_tooling::{LanguageModelTool, ToolView}; -use collections::BTreeMap; -use file_icons::FileIcons; -use gpui::{prelude::*, AnyElement, Model, Task}; -use project::ProjectPath; -use schemars::JsonSchema; -use semantic_index::{ProjectIndex, Status}; -use serde::{Deserialize, Serialize}; -use std::{ - fmt::Write as _, - ops::Range, - path::{Path, PathBuf}, - str::FromStr as _, - sync::Arc, -}; -use ui::{prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, WindowContext}; - -const DEFAULT_SEARCH_LIMIT: usize = 20; - -pub struct ProjectIndexTool { - project_index: Model, -} - -#[derive(Default)] -enum ProjectIndexToolState { - #[default] - CollectingQuery, - Searching, - Error(anyhow::Error), - Finished { - excerpts: BTreeMap>>, - index_status: Status, - }, -} - -pub struct ProjectIndexView { - project_index: Model, - input: CodebaseQuery, - expanded_header: bool, - state: ProjectIndexToolState, -} - -#[derive(Default, Deserialize, JsonSchema)] -pub struct CodebaseQuery { - /// Semantic search query - query: String, - /// Criteria to include results - includes: Option, - /// Criteria to exclude results - excludes: Option, -} - -#[derive(Deserialize, JsonSchema, Clone, Default)] -pub struct SearchFilter { - /// Filter by file path prefix - prefix_path: Option, - /// Filter by file extension - extension: Option, - // Note: we possibly can't do content filtering very easily given the project context handling - // the final results, so we're leaving out direct string matches for now -} - -fn project_starts_with(prefix_path: Option, project_path: ProjectPath) -> bool { - if let Some(path) = &prefix_path { - if let Some(path) = PathBuf::from_str(path).ok() { - return project_path.path.starts_with(path); - } - } - - return false; -} - -impl SearchFilter { - fn matches(&self, project_path: &ProjectPath) -> bool { - let path_match = project_starts_with(self.prefix_path.clone(), project_path.clone()); - - path_match - && (if let Some(extension) = &self.extension { - project_path - .path - .extension() - .and_then(|ext| ext.to_str()) - .map(|ext| ext == extension) - .unwrap_or(false) - } else { - true - }) - } -} - -#[derive(Serialize, Deserialize)] -pub struct SerializedState { - index_status: Status, - error_message: Option, - worktrees: BTreeMap, WorktreeIndexOutput>, -} - -#[derive(Default, Serialize, Deserialize)] -struct WorktreeIndexOutput { - excerpts: BTreeMap, Vec>>, -} - -impl ProjectIndexView { - fn toggle_header(&mut self, cx: &mut ViewContext) { - self.expanded_header = !self.expanded_header; - cx.notify(); - } - - fn render_filter_section( - &mut self, - heading: &str, - filter: Option, - cx: &mut ViewContext, - ) -> Option { - let filter = match filter { - Some(filter) => filter, - None => return None, - }; - - // Any of the filter fields can be empty. We'll show nothing if they're all empty. - let path = filter.prefix_path.as_ref().map(|path| { - let icon_path = FileIcons::get_icon(Path::new(path), cx) - .map(SharedString::from) - .unwrap_or_else(|| SharedString::from("icons/file_icons/file.svg")); - - h_flex() - .gap_1() - .child("Paths: ") - .child(Icon::from_path(icon_path)) - .child(ui::Label::new(path.clone()).color(Color::Muted)) - }); - - let extension = filter.extension.as_ref().map(|extension| { - let icon_path = FileIcons::get_icon(Path::new(extension), cx) - .map(SharedString::from) - .unwrap_or_else(|| SharedString::from("icons/file_icons/file.svg")); - - h_flex() - .gap_1() - .child("Extensions: ") - .child(Icon::from_path(icon_path)) - .child(ui::Label::new(extension.clone()).color(Color::Muted)) - }); - - if path.is_none() && extension.is_none() { - return None; - } - - Some( - v_flex() - .child(ui::Label::new(heading.to_string())) - .gap_1() - .children(path) - .children(extension) - .into_any_element(), - ) - } -} - -impl Render for ProjectIndexView { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let query = self.input.query.clone(); - - let (header_text, content) = match &self.state { - ProjectIndexToolState::Error(error) => { - return format!("failed to search: {error:?}").into_any_element() - } - ProjectIndexToolState::CollectingQuery | ProjectIndexToolState::Searching => { - ("Searching...".to_string(), div()) - } - ProjectIndexToolState::Finished { excerpts, .. } => { - let file_count = excerpts.len(); - - if excerpts.is_empty() { - ("No results found".to_string(), div()) - } else { - let header_text = format!( - "Read {} {}", - file_count, - if file_count == 1 { "file" } else { "files" } - ); - - let el = v_flex().gap_2().children(excerpts.keys().map(|path| { - h_flex().gap_2().child(Icon::new(IconName::File)).child( - Label::new(path.path.to_string_lossy().to_string()).color(Color::Muted), - ) - })); - - (header_text, el) - } - } - }; - - let header = h_flex() - .gap_2() - .child(Icon::new(IconName::File)) - .child(header_text); - - v_flex() - .gap_3() - .child( - CollapsibleContainer::new("collapsible-container", self.expanded_header) - .start_slot(header) - .on_click(cx.listener(move |this, _, cx| { - this.toggle_header(cx); - })) - .child( - v_flex() - .gap_3() - .p_3() - .child( - h_flex() - .gap_2() - .child(Icon::new(IconName::MagnifyingGlass)) - .child(Label::new(format!("`{}`", query)).color(Color::Muted)), - ) - .children(self.render_filter_section( - "Includes", - self.input.includes.clone(), - cx, - )) - .children(self.render_filter_section( - "Excludes", - self.input.excludes.clone(), - cx, - )) - .child(content), - ), - ) - .into_any_element() - } -} - -impl ToolView for ProjectIndexView { - type Input = CodebaseQuery; - type SerializedState = SerializedState; - - fn generate( - &self, - context: &mut assistant_tooling::ProjectContext, - _: &mut ViewContext, - ) -> String { - match &self.state { - ProjectIndexToolState::CollectingQuery => String::new(), - ProjectIndexToolState::Searching => String::new(), - ProjectIndexToolState::Error(error) => format!("failed to search: {error:?}"), - ProjectIndexToolState::Finished { - excerpts, - index_status, - } => { - let mut body = "found results in the following paths:\n".to_string(); - - for (project_path, ranges) in excerpts { - context.add_excerpts(project_path.clone(), ranges); - writeln!(&mut body, "* {}", &project_path.path.display()).unwrap(); - } - - if *index_status != Status::Idle { - body.push_str("Still indexing. Results may be incomplete.\n"); - } - - body - } - } - } - - fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext) { - self.input = input; - cx.notify(); - } - - fn execute(&mut self, cx: &mut ViewContext) -> Task> { - self.state = ProjectIndexToolState::Searching; - cx.notify(); - - let project_index = self.project_index.read(cx); - let index_status = project_index.status(); - - // TODO: wire the filters into the search here instead of processing after. - // Otherwise we'll get zero results sometimes. - let search = project_index.search(self.input.query.clone(), DEFAULT_SEARCH_LIMIT, cx); - - let includes = self.input.includes.clone(); - let excludes = self.input.excludes.clone(); - - cx.spawn(|this, mut cx| async move { - let search_result = search.await; - this.update(&mut cx, |this, cx| { - match search_result { - Ok(search_results) => { - let mut excerpts = BTreeMap::>>::new(); - for search_result in search_results { - let project_path = ProjectPath { - worktree_id: search_result.worktree.read(cx).id(), - path: search_result.path, - }; - - if let Some(includes) = &includes { - if !includes.matches(&project_path) { - continue; - } - } else if let Some(excludes) = &excludes { - if excludes.matches(&project_path) { - continue; - } - } - - excerpts - .entry(project_path) - .or_default() - .push(search_result.range); - } - this.state = ProjectIndexToolState::Finished { - excerpts, - index_status, - }; - } - Err(error) => { - this.state = ProjectIndexToolState::Error(error); - } - } - cx.notify(); - }) - }) - } - - fn serialize(&self, cx: &mut ViewContext) -> Self::SerializedState { - let mut serialized = SerializedState { - error_message: None, - index_status: Status::Idle, - worktrees: Default::default(), - }; - match &self.state { - ProjectIndexToolState::Error(err) => serialized.error_message = Some(err.to_string()), - ProjectIndexToolState::Finished { - excerpts, - index_status, - } => { - serialized.index_status = *index_status; - if let Some(project) = self.project_index.read(cx).project().upgrade() { - let project = project.read(cx); - for (project_path, excerpts) in excerpts { - if let Some(worktree) = - project.worktree_for_id(project_path.worktree_id, cx) - { - let worktree_path = worktree.read(cx).abs_path(); - serialized - .worktrees - .entry(worktree_path) - .or_default() - .excerpts - .insert(project_path.path.clone(), excerpts.clone()); - } - } - } - } - _ => {} - } - serialized - } - - fn deserialize( - &mut self, - serialized: Self::SerializedState, - cx: &mut ViewContext, - ) -> Result<()> { - if !serialized.worktrees.is_empty() { - let mut excerpts = BTreeMap::>>::new(); - if let Some(project) = self.project_index.read(cx).project().upgrade() { - let project = project.read(cx); - for (worktree_path, worktree_state) in serialized.worktrees { - if let Some(worktree) = project - .worktrees() - .find(|worktree| worktree.read(cx).abs_path() == worktree_path) - { - let worktree_id = worktree.read(cx).id(); - for (path, serialized_excerpts) in worktree_state.excerpts { - excerpts.insert(ProjectPath { worktree_id, path }, serialized_excerpts); - } - } - } - } - self.state = ProjectIndexToolState::Finished { - excerpts, - index_status: serialized.index_status, - }; - } - cx.notify(); - Ok(()) - } -} - -impl ProjectIndexTool { - pub fn new(project_index: Model) -> Self { - Self { project_index } - } -} - -impl LanguageModelTool for ProjectIndexTool { - type View = ProjectIndexView; - - fn name(&self) -> String { - "semantic_search_codebase".to_string() - } - - fn description(&self) -> String { - unindent::unindent( - r#"This search tool uses a semantic index to perform search queries across your codebase, identifying and returning excerpts of text and code possibly related to the query. - - Ideal for: - - Discovering implementations of similar logic within the project - - Finding usage examples of functions, classes/structures, libraries, and other code elements - - Developing understanding of the codebase's architecture and design - - Note: The search's effectiveness is directly related to the current state of the codebase and the specificity of your query. It is recommended that you use snippets of code that are similar to the code you wish to find."#, - ) - } - - fn view(&self, cx: &mut WindowContext) -> gpui::View { - cx.new_view(|_| ProjectIndexView { - state: ProjectIndexToolState::CollectingQuery, - input: Default::default(), - expanded_header: false, - project_index: self.project_index.clone(), - }) - } -} diff --git a/crates/assistant2/src/ui.rs b/crates/assistant2/src/ui.rs deleted file mode 100644 index 3333620a47..0000000000 --- a/crates/assistant2/src/ui.rs +++ /dev/null @@ -1,17 +0,0 @@ -mod active_file_button; -mod chat_message; -mod chat_notice; -mod composer; -mod project_index_button; - -#[cfg(feature = "stories")] -mod stories; - -pub use active_file_button::*; -pub use chat_message::*; -pub use chat_notice::*; -pub use composer::*; -pub use project_index_button::*; - -#[cfg(feature = "stories")] -pub use stories::*; diff --git a/crates/assistant2/src/ui/active_file_button.rs b/crates/assistant2/src/ui/active_file_button.rs deleted file mode 100644 index 1041578568..0000000000 --- a/crates/assistant2/src/ui/active_file_button.rs +++ /dev/null @@ -1,134 +0,0 @@ -use crate::attachments::ActiveEditorAttachmentTool; -use assistant_tooling::AttachmentRegistry; -use editor::Editor; -use gpui::{prelude::*, Subscription, View}; -use std::sync::Arc; -use ui::{prelude::*, ButtonLike, Color, Icon, IconName, Tooltip}; -use workspace::Workspace; - -#[derive(Clone)] -enum Status { - ActiveFile(String), - #[allow(dead_code)] - NoFile, -} - -pub struct ActiveFileButton { - attachment_registry: Arc, - status: Status, - #[allow(dead_code)] - workspace_subscription: Subscription, -} - -impl ActiveFileButton { - pub fn new( - attachment_registry: Arc, - workspace: View, - cx: &mut ViewContext, - ) -> Self { - let workspace_subscription = cx.subscribe(&workspace, Self::handle_workspace_event); - - cx.defer(move |this, cx| this.update_active_buffer(workspace.clone(), cx)); - - Self { - attachment_registry, - status: Status::NoFile, - workspace_subscription, - } - } - - pub fn set_enabled(&mut self, enabled: bool) { - self.attachment_registry - .set_attachment_tool_enabled::(enabled); - } - - pub fn update_active_buffer(&mut self, workspace: View, cx: &mut ViewContext) { - let active_buffer = workspace - .read(cx) - .active_item(cx) - .and_then(|item| Some(item.act_as::(cx)?.read(cx).buffer().clone())); - - if let Some(buffer) = active_buffer { - let buffer = buffer.read(cx); - - if let Some(singleton) = buffer.as_singleton() { - let singleton = singleton.read(cx); - - let filename: String = singleton - .file() - .map(|file| file.path().to_string_lossy()) - .unwrap_or("Untitled".into()) - .into(); - - self.status = Status::ActiveFile(filename); - } - } - } - - fn handle_workspace_event( - &mut self, - workspace: View, - event: &workspace::Event, - cx: &mut ViewContext, - ) { - if let workspace::Event::ActiveItemChanged = event { - self.update_active_buffer(workspace, cx); - } - } -} - -impl Render for ActiveFileButton { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let is_enabled = self - .attachment_registry - .is_attachment_tool_enabled::(); - - let icon = if is_enabled { - Icon::new(IconName::File) - .size(IconSize::XSmall) - .color(Color::Default) - } else { - Icon::new(IconName::File) - .size(IconSize::XSmall) - .color(Color::Disabled) - }; - - let indicator = None; - - let status = self.status.clone(); - - ButtonLike::new("active-file-button") - .child( - ui::IconWithIndicator::new(icon, indicator) - .indicator_border_color(Some(gpui::transparent_black())), - ) - .tooltip({ - move |cx| { - let status = status.clone(); - let (tooltip, meta) = match (is_enabled, status) { - (false, _) => ( - "Active file disabled".to_string(), - Some("Click to enable".to_string()), - ), - (true, Status::ActiveFile(filename)) => ( - format!("Active file {filename} enabled"), - Some("Click to disable".to_string()), - ), - (true, Status::NoFile) => { - ("No file active for conversation".to_string(), None) - } - }; - - if let Some(meta) = meta { - Tooltip::with_meta(tooltip, None, meta, cx) - } else { - Tooltip::text(tooltip, cx) - } - } - }) - .on_click(cx.listener(move |this, _, cx| { - this.set_enabled(!is_enabled); - cx.notify(); - })) - } -} diff --git a/crates/assistant2/src/ui/chat_message.rs b/crates/assistant2/src/ui/chat_message.rs deleted file mode 100644 index fb07e55b5c..0000000000 --- a/crates/assistant2/src/ui/chat_message.rs +++ /dev/null @@ -1,140 +0,0 @@ -use std::sync::Arc; - -use client::User; -use gpui::{hsla, AnyElement, ClickEvent}; -use ui::{prelude::*, Avatar, Tooltip}; - -use crate::MessageId; - -pub enum UserOrAssistant { - User(Option>), - Assistant, -} - -#[derive(IntoElement)] -pub struct ChatMessage { - id: MessageId, - player: UserOrAssistant, - messages: Vec, - selected: bool, - collapsed: bool, - on_collapse_handle_click: Box, -} - -impl ChatMessage { - pub fn new( - id: MessageId, - player: UserOrAssistant, - messages: Vec, - collapsed: bool, - on_collapse_handle_click: Box, - ) -> Self { - Self { - id, - player, - messages, - selected: false, - collapsed, - on_collapse_handle_click, - } - } -} - -impl Selectable for ChatMessage { - fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } -} - -impl RenderOnce for ChatMessage { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { - let message_group = SharedString::from(format!("{}_group", self.id.0)); - - let collapse_handle_id = SharedString::from(format!("{}_collapse_handle", self.id.0)); - - let content_padding = Spacing::Small.rems(cx); - // Clamp the message height to exactly 1.5 lines when collapsed. - let collapsed_height = content_padding.to_pixels(cx.rem_size()) + cx.line_height() * 1.5; - - let background_color = if let UserOrAssistant::User(_) = &self.player { - Some(cx.theme().colors().surface_background) - } else { - None - }; - - let (username, avatar_uri) = match self.player { - UserOrAssistant::Assistant => ( - "Assistant".into(), - Some("https://zed.dev/assistant_avatar.png".into()), - ), - UserOrAssistant::User(Some(user)) => { - (user.github_login.clone(), Some(user.avatar_uri.clone())) - } - UserOrAssistant::User(None) => ("You".into(), None), - }; - - v_flex() - .group(message_group.clone()) - .gap(Spacing::XSmall.rems(cx)) - .p(Spacing::XSmall.rems(cx)) - .when(self.selected, |element| { - element.bg(hsla(0.6, 0.67, 0.46, 0.12)) - }) - .rounded_lg() - .child( - h_flex() - .justify_between() - .px(content_padding) - .child( - h_flex() - .gap_2() - .map(|this| { - let avatar_size = rems_from_px(20.); - if let Some(avatar_uri) = avatar_uri { - this.child(Avatar::new(avatar_uri).size(avatar_size)) - } else { - this.child(div().size(avatar_size)) - } - }) - .child(Label::new(username).color(Color::Muted)), - ) - .child( - h_flex().visible_on_hover(message_group).child( - // temp icons - IconButton::new( - collapse_handle_id.clone(), - if self.collapsed { - IconName::ArrowUp - } else { - IconName::ArrowDown - }, - ) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .on_click(self.on_collapse_handle_click) - .tooltip(|cx| Tooltip::text("Collapse Message", cx)), - ), - ), - ) - .when(self.messages.len() > 0, |el| { - el.child( - h_flex().w_full().child( - v_flex() - .relative() - .overflow_hidden() - .w_full() - .p(content_padding) - .gap_3() - .text_ui(cx) - .rounded_lg() - .when_some(background_color, |this, background_color| { - this.bg(background_color) - }) - .when(self.collapsed, |this| this.h(collapsed_height)) - .children(self.messages), - ), - ) - }) - } -} diff --git a/crates/assistant2/src/ui/chat_notice.rs b/crates/assistant2/src/ui/chat_notice.rs deleted file mode 100644 index 5001d2d23e..0000000000 --- a/crates/assistant2/src/ui/chat_notice.rs +++ /dev/null @@ -1,71 +0,0 @@ -use ui::{prelude::*, Avatar, IconButtonShape}; - -#[derive(IntoElement)] -pub struct ChatNotice { - message: SharedString, - meta: Option, -} - -impl ChatNotice { - pub fn new(message: impl Into) -> Self { - Self { - message: message.into(), - meta: None, - } - } - - pub fn meta(mut self, meta: impl Into) -> Self { - self.meta = Some(meta.into()); - self - } -} - -impl RenderOnce for ChatNotice { - fn render(self, _cx: &mut WindowContext) -> impl IntoElement { - h_flex() - .w_full() - .items_start() - .mt_4() - .gap_3() - .child( - // TODO: Replace with question mark. - Avatar::new("https://zed.dev/assistant_avatar.png").size(rems_from_px(20.)), - ) - .child( - v_flex() - .size_full() - .gap_1() - .pr_4() - .overflow_hidden() - .child( - h_flex() - .justify_between() - .overflow_hidden() - .child( - h_flex() - .flex_none() - .overflow_hidden() - .child(Label::new(self.message)), - ) - .child( - h_flex() - .flex_shrink_0() - .gap_1() - .child(Button::new("allow", "Allow")) - .child( - IconButton::new("deny", IconName::Close) - .shape(IconButtonShape::Square) - .icon_color(Color::Muted) - .size(ButtonSize::None) - .icon_size(IconSize::XSmall), - ), - ), - ) - .children( - self.meta.map(|meta| { - Label::new(meta).size(LabelSize::Small).color(Color::Muted) - }), - ), - ) - } -} diff --git a/crates/assistant2/src/ui/composer.rs b/crates/assistant2/src/ui/composer.rs deleted file mode 100644 index 29047b5ee2..0000000000 --- a/crates/assistant2/src/ui/composer.rs +++ /dev/null @@ -1,193 +0,0 @@ -use crate::{ - ui::{ActiveFileButton, ProjectIndexButton}, - AssistantChat, CompletionProvider, -}; -use editor::{Editor, EditorElement, EditorStyle}; -use gpui::{AnyElement, FontStyle, ReadGlobal, TextStyle, View, WeakView, WhiteSpace}; -use settings::Settings; -use theme::ThemeSettings; -use ui::{popover_menu, prelude::*, ButtonLike, ContextMenu, Divider, TextSize, Tooltip}; - -#[derive(IntoElement)] -pub struct Composer { - editor: View, - project_index_button: View, - active_file_button: Option>, - model_selector: AnyElement, -} - -impl Composer { - pub fn new( - editor: View, - project_index_button: View, - active_file_button: Option>, - model_selector: AnyElement, - ) -> Self { - Self { - editor, - project_index_button, - active_file_button, - model_selector, - } - } - - fn render_tools(&mut self, _cx: &mut WindowContext) -> impl IntoElement { - h_flex().child(self.project_index_button.clone()) - } - - fn render_attachment_tools(&mut self, _cx: &mut WindowContext) -> impl IntoElement { - h_flex().children( - self.active_file_button - .clone() - .map(|view| view.into_any_element()), - ) - } -} - -impl RenderOnce for Composer { - fn render(mut self, cx: &mut WindowContext) -> impl IntoElement { - let font_size = TextSize::Default.rems(cx); - let line_height = font_size.to_pixels(cx.rem_size()) * 1.3; - let mut editor_border = cx.theme().colors().text; - editor_border.fade_out(0.90); - - // Remove the extra 1px added by the border - let padding = Spacing::XLarge.rems(cx) - rems_from_px(1.); - - h_flex() - .p(Spacing::Small.rems(cx)) - .w_full() - .items_start() - .child( - v_flex() - .w_full() - .rounded_lg() - .p(padding) - .border_1() - .border_color(editor_border) - .bg(cx.theme().colors().editor_background) - .child( - v_flex() - .justify_between() - .w_full() - .gap_2() - .child({ - let settings = ThemeSettings::get_global(cx); - let text_style = TextStyle { - color: cx.theme().colors().editor_foreground, - font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features.clone(), - font_size: font_size.into(), - font_weight: settings.buffer_font.weight, - font_style: FontStyle::Normal, - line_height: line_height.into(), - background_color: None, - underline: None, - strikethrough: None, - white_space: WhiteSpace::Normal, - }; - - EditorElement::new( - &self.editor, - EditorStyle { - background: cx.theme().colors().editor_background, - local_player: cx.theme().players().local(), - text: text_style, - ..Default::default() - }, - ) - }) - .child( - h_flex() - .flex_none() - .gap_2() - .justify_between() - .w_full() - .child( - h_flex().gap_1().child( - h_flex() - .gap_2() - .child(self.render_tools(cx)) - .child(Divider::vertical()) - .child(self.render_attachment_tools(cx)), - ), - ) - .child(h_flex().gap_1().child(self.model_selector)), - ), - ), - ) - } -} - -#[derive(IntoElement)] -pub struct ModelSelector { - assistant_chat: WeakView, - model: String, -} - -impl ModelSelector { - pub fn new(assistant_chat: WeakView, model: String) -> Self { - Self { - assistant_chat, - model, - } - } -} - -impl RenderOnce for ModelSelector { - fn render(self, _cx: &mut WindowContext) -> impl IntoElement { - popover_menu("model-switcher") - .menu(move |cx| { - ContextMenu::build(cx, |mut menu, cx| { - for model in CompletionProvider::global(cx).available_models() { - menu = menu.custom_entry( - { - let model = model.clone(); - move |_| Label::new(model.clone()).into_any_element() - }, - { - let assistant_chat = self.assistant_chat.clone(); - move |cx| { - _ = assistant_chat.update(cx, |assistant_chat, cx| { - assistant_chat.model.clone_from(&model); - cx.notify(); - }); - } - }, - ); - } - menu - }) - .into() - }) - .trigger( - ButtonLike::new("active-model") - .child( - h_flex() - .w_full() - .gap_0p5() - .child( - div() - .overflow_x_hidden() - .flex_grow() - .whitespace_nowrap() - .child( - Label::new(self.model) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) - .child( - div().child( - Icon::new(IconName::ChevronDown) - .color(Color::Muted) - .size(IconSize::XSmall), - ), - ), - ) - .style(ButtonStyle::Subtle) - .tooltip(move |cx| Tooltip::text("Change Model", cx)), - ) - .anchor(gpui::AnchorCorner::BottomRight) - } -} diff --git a/crates/assistant2/src/ui/project_index_button.rs b/crates/assistant2/src/ui/project_index_button.rs deleted file mode 100644 index 6d7cb08187..0000000000 --- a/crates/assistant2/src/ui/project_index_button.rs +++ /dev/null @@ -1,112 +0,0 @@ -use assistant_tooling::ToolRegistry; -use gpui::{percentage, prelude::*, Animation, AnimationExt, Model, Transformation}; -use semantic_index::{ProjectIndex, Status}; -use std::{sync::Arc, time::Duration}; -use ui::{prelude::*, ButtonLike, Color, Icon, IconName, Indicator, Tooltip}; - -use crate::tools::ProjectIndexTool; - -pub struct ProjectIndexButton { - project_index: Model, - tool_registry: Arc, -} - -impl ProjectIndexButton { - pub fn new( - project_index: Model, - tool_registry: Arc, - cx: &mut ViewContext, - ) -> Self { - cx.subscribe(&project_index, |_this, _, _status: &Status, cx| { - cx.notify(); - }) - .detach(); - Self { - project_index, - tool_registry, - } - } - - pub fn set_enabled(&mut self, enabled: bool) { - self.tool_registry - .set_tool_enabled::(enabled); - } -} - -impl Render for ProjectIndexButton { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let status = self.project_index.read(cx).status(); - let is_enabled = self.tool_registry.is_tool_enabled::(); - - let icon = if is_enabled { - match status { - Status::Idle => Icon::new(IconName::Code) - .size(IconSize::XSmall) - .color(Color::Default), - Status::Loading => Icon::new(IconName::Code) - .size(IconSize::XSmall) - .color(Color::Muted), - Status::Scanning { .. } => Icon::new(IconName::Code) - .size(IconSize::XSmall) - .color(Color::Muted), - } - } else { - Icon::new(IconName::Code) - .size(IconSize::XSmall) - .color(Color::Disabled) - }; - - let indicator = if is_enabled { - match status { - Status::Idle => Some(Indicator::dot().color(Color::Success)), - Status::Scanning { .. } => Some(Indicator::dot().color(Color::Warning)), - Status::Loading => Some(Indicator::icon( - Icon::new(IconName::Spinner) - .color(Color::Accent) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ), - )), - } - } else { - None - }; - - ButtonLike::new("project-index") - .child( - ui::IconWithIndicator::new(icon, indicator) - .indicator_border_color(Some(gpui::transparent_black())), - ) - .tooltip({ - move |cx| { - let (tooltip, meta) = match (is_enabled, status) { - (false, _) => ( - "Project index disabled".to_string(), - Some("Click to enable".to_string()), - ), - (_, Status::Idle) => ( - "Project index ready".to_string(), - Some("Click to disable".to_string()), - ), - (_, Status::Loading) => ("Project index loading...".to_string(), None), - (_, Status::Scanning { remaining_count }) => ( - "Project index scanning...".to_string(), - Some(format!("{} remaining...", remaining_count)), - ), - }; - - if let Some(meta) = meta { - Tooltip::with_meta(tooltip, None, meta, cx) - } else { - Tooltip::text(tooltip, cx) - } - } - }) - .on_click(cx.listener(move |this, _, cx| { - this.set_enabled(!is_enabled); - cx.notify(); - })) - } -} diff --git a/crates/assistant2/src/ui/stories.rs b/crates/assistant2/src/ui/stories.rs deleted file mode 100644 index 8bc2b30d66..0000000000 --- a/crates/assistant2/src/ui/stories.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod chat_message; -mod chat_notice; - -pub use chat_message::*; -pub use chat_notice::*; diff --git a/crates/assistant2/src/ui/stories/chat_message.rs b/crates/assistant2/src/ui/stories/chat_message.rs deleted file mode 100644 index 1d63ae78c4..0000000000 --- a/crates/assistant2/src/ui/stories/chat_message.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::sync::Arc; - -use client::User; -use story::{StoryContainer, StoryItem, StorySection}; -use ui::prelude::*; - -use crate::ui::{ChatMessage, UserOrAssistant}; -use crate::MessageId; - -pub struct ChatMessageStory; - -impl Render for ChatMessageStory { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - let user_1 = Arc::new(User { - id: 12345, - github_login: "iamnbutler".into(), - avatar_uri: "https://avatars.githubusercontent.com/u/1714999?v=4".into(), - }); - - StoryContainer::new( - "ChatMessage Story", - "crates/assistant2/src/ui/stories/chat_message.rs", - ) - .child( - StorySection::new() - .child(StoryItem::new( - "User chat message", - ChatMessage::new( - MessageId(0), - UserOrAssistant::User(Some(user_1.clone())), - vec![div().child("What can I do here?").into_any_element()], - false, - Box::new(|_, _| {}), - ), - )) - .child(StoryItem::new( - "User chat message (collapsed)", - ChatMessage::new( - MessageId(0), - UserOrAssistant::User(Some(user_1.clone())), - vec![div().child("What can I do here?").into_any_element()], - true, - Box::new(|_, _| {}), - ), - )), - ) - .child( - StorySection::new() - .child(StoryItem::new( - "Assistant chat message", - ChatMessage::new( - MessageId(0), - UserOrAssistant::Assistant, - vec![div().child("You can talk to me!").into_any_element()], - false, - Box::new(|_, _| {}), - ), - )) - .child(StoryItem::new( - "Assistant chat message (collapsed)", - ChatMessage::new( - MessageId(0), - UserOrAssistant::Assistant, - vec![div().child(MULTI_LINE_MESSAGE).into_any_element()], - true, - Box::new(|_, _| {}), - ), - )), - ) - .child( - StorySection::new().child(StoryItem::new( - "Conversation between user and assistant", - v_flex() - .gap_2() - .child(ChatMessage::new( - MessageId(0), - UserOrAssistant::User(Some(user_1.clone())), - vec![div().child("What is Rust??").into_any_element()], - false, - Box::new(|_, _| {}), - )) - .child(ChatMessage::new( - MessageId(0), - UserOrAssistant::Assistant, - vec![div().child("Rust is a multi-paradigm programming language focused on performance and safety").into_any_element()], - false, - Box::new(|_, _| {}), - )) - .child(ChatMessage::new( - MessageId(0), - UserOrAssistant::User(Some(user_1)), - vec![div().child("Sounds pretty cool!").into_any_element()], - false, - Box::new(|_, _| {}), - )), - )), - ) - } -} - -const MULTI_LINE_MESSAGE: &str = "In 2010, the movies nominated for the 82nd Academy Awards, for films released in 2009, were as follows. Note that 2010 nominees were announced for the ceremony happening in that year, but they honor movies from the previous year"; diff --git a/crates/assistant2/src/ui/stories/chat_notice.rs b/crates/assistant2/src/ui/stories/chat_notice.rs deleted file mode 100644 index ad8eef92c7..0000000000 --- a/crates/assistant2/src/ui/stories/chat_notice.rs +++ /dev/null @@ -1,22 +0,0 @@ -use story::{StoryContainer, StoryItem, StorySection}; -use ui::prelude::*; - -use crate::ui::ChatNotice; - -pub struct ChatNoticeStory; - -impl Render for ChatNoticeStory { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - StoryContainer::new( - "ChatNotice Story", - "crates/assistant2/src/ui/stories/chat_notice.rs", - ) - .child( - StorySection::new().child(StoryItem::new( - "Project index request", - ChatNotice::new("Allow assistant to index your project?") - .meta("Enabling will allow responses more relevant to this project."), - )), - ) - } -}