From 0bde0f8e2f3879a8f304716ba570375955bfe9b2 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 4 Dec 2024 14:35:44 -0500 Subject: [PATCH] assistant2: Add ability to open past threads (#21548) This PR adds the ability to open past threads in Assistant 2. There are also some mocked threads in the history for testing purposes. Release Notes: - N/A --- Cargo.lock | 2 + crates/assistant2/Cargo.toml | 2 + crates/assistant2/src/assistant_panel.rs | 149 ++++++++++++++--------- crates/assistant2/src/thread.rs | 37 +++++- crates/assistant2/src/thread_store.rs | 113 ++++++++++++++++- 5 files changed, 243 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fc64ca4093714f..72bace069b5918 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -478,7 +478,9 @@ dependencies = [ "smol", "theme", "ui", + "unindent", "util", + "uuid", "workspace", ] diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index 257183a4ac1843..fb7dcbe520b2aa 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -37,5 +37,7 @@ settings.workspace = true smol.workspace = true theme.workspace = true ui.workspace = true +unindent.workspace = true util.workspace = true +uuid.workspace = true workspace.workspace = true diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 2dc4582eee57b5..00bd15de2e220c 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -21,7 +21,7 @@ use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::Workspace; use crate::message_editor::MessageEditor; -use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent}; +use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent, ThreadId}; use crate::thread_store::ThreadStore; use crate::{NewThread, OpenHistory, ToggleFocus, ToggleModelSelector}; @@ -77,7 +77,7 @@ impl AssistantPanel { tools: Arc, cx: &mut ViewContext, ) -> Self { - let thread = cx.new_model(|cx| Thread::new(tools.clone(), cx)); + let thread = thread_store.update(cx, |this, cx| this.create_thread(cx)); let subscriptions = vec![ cx.observe(&thread, |_, _, cx| cx.notify()), cx.subscribe(&thread, Self::handle_thread_event), @@ -105,8 +105,27 @@ impl AssistantPanel { } fn new_thread(&mut self, cx: &mut ViewContext) { - let tools = self.thread.read(cx).tools().clone(); - let thread = cx.new_model(|cx| Thread::new(tools, cx)); + let thread = self + .thread_store + .update(cx, |this, cx| this.create_thread(cx)); + self.reset_thread(thread, cx); + } + + fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext) { + let Some(thread) = self + .thread_store + .update(cx, |this, cx| this.open_thread(thread_id, cx)) + else { + return; + }; + self.reset_thread(thread.clone(), cx); + + for message in thread.read(cx).messages().cloned().collect::>() { + self.push_message(&message.id, message.text.clone(), cx); + } + } + + fn reset_thread(&mut self, thread: Model, cx: &mut ViewContext) { let subscriptions = vec![ cx.observe(&thread, |_, _, cx| cx.notify()), cx.subscribe(&thread, Self::handle_thread_event), @@ -122,6 +141,56 @@ impl AssistantPanel { self.message_editor.focus_handle(cx).focus(cx); } + fn push_message(&mut self, id: &MessageId, text: String, cx: &mut ViewContext) { + let old_len = self.thread_messages.len(); + self.thread_messages.push(*id); + self.thread_list_state.splice(old_len..old_len, 1); + + let theme_settings = ThemeSettings::get_global(cx); + let ui_font_size = TextSize::Default.rems(cx); + let buffer_font_size = theme_settings.buffer_font_size; + + let mut text_style = cx.text_style(); + text_style.refine(&TextStyleRefinement { + font_family: Some(theme_settings.ui_font.family.clone()), + font_size: Some(ui_font_size.into()), + color: Some(cx.theme().colors().text), + ..Default::default() + }); + + let markdown_style = MarkdownStyle { + base_text_style: text_style, + syntax: cx.theme().syntax().clone(), + selection_background_color: cx.theme().players().local().selection, + code_block: StyleRefinement { + text: Some(TextStyleRefinement { + font_family: Some(theme_settings.buffer_font.family.clone()), + font_size: Some(buffer_font_size.into()), + ..Default::default() + }), + ..Default::default() + }, + inline_code: TextStyleRefinement { + font_family: Some(theme_settings.buffer_font.family.clone()), + font_size: Some(ui_font_size.into()), + background_color: Some(cx.theme().colors().editor_background), + ..Default::default() + }, + ..Default::default() + }; + + let markdown = cx.new_view(|cx| { + Markdown::new( + text, + markdown_style, + Some(self.language_registry.clone()), + None, + cx, + ) + }); + self.rendered_messages_by_id.insert(*id, markdown); + } + fn handle_thread_event( &mut self, _: Model, @@ -141,59 +210,13 @@ impl AssistantPanel { } } ThreadEvent::MessageAdded(message_id) => { - let old_len = self.thread_messages.len(); - self.thread_messages.push(*message_id); - self.thread_list_state.splice(old_len..old_len, 1); - if let Some(message_text) = self .thread .read(cx) .message(*message_id) .map(|message| message.text.clone()) { - let theme_settings = ThemeSettings::get_global(cx); - let ui_font_size = TextSize::Default.rems(cx); - let buffer_font_size = theme_settings.buffer_font_size; - - let mut text_style = cx.text_style(); - text_style.refine(&TextStyleRefinement { - font_family: Some(theme_settings.ui_font.family.clone()), - font_size: Some(ui_font_size.into()), - color: Some(cx.theme().colors().text), - ..Default::default() - }); - - let markdown_style = MarkdownStyle { - base_text_style: text_style, - syntax: cx.theme().syntax().clone(), - selection_background_color: cx.theme().players().local().selection, - code_block: StyleRefinement { - text: Some(TextStyleRefinement { - font_family: Some(theme_settings.buffer_font.family.clone()), - font_size: Some(buffer_font_size.into()), - ..Default::default() - }), - ..Default::default() - }, - inline_code: TextStyleRefinement { - font_family: Some(theme_settings.buffer_font.family.clone()), - font_size: Some(ui_font_size.into()), - background_color: Some(cx.theme().colors().editor_background), - ..Default::default() - }, - ..Default::default() - }; - - let markdown = cx.new_view(|cx| { - Markdown::new( - message_text, - markdown_style, - Some(self.language_registry.clone()), - None, - cx, - ) - }); - self.rendered_messages_by_id.insert(*message_id, markdown); + self.push_message(message_id, message_text, cx); } cx.notify(); @@ -401,8 +424,9 @@ impl AssistantPanel { fn render_message_list(&self, cx: &mut ViewContext) -> AnyElement { if self.thread_messages.is_empty() { - #[allow(clippy::useless_vec)] - let recent_threads = vec![1, 2, 3]; + let recent_threads = self + .thread_store + .update(cx, |this, cx| this.recent_threads(3, cx)); return v_flex() .gap_2() @@ -467,8 +491,8 @@ impl AssistantPanel { .child( v_flex().gap_2().children( recent_threads - .iter() - .map(|_thread| self.render_past_thread(cx)), + .into_iter() + .map(|thread| self.render_past_thread(thread, cx)), ), ) .child( @@ -534,10 +558,16 @@ impl AssistantPanel { .into_any() } - fn render_past_thread(&self, _cx: &mut ViewContext) -> impl IntoElement { - ListItem::new("temp") + fn render_past_thread( + &self, + thread: Model, + cx: &mut ViewContext, + ) -> impl IntoElement { + let id = thread.read(cx).id().clone(); + + ListItem::new(("past-thread", thread.entity_id())) .start_slot(Icon::new(IconName::MessageBubbles)) - .child(Label::new("Some Thread Title")) + .child(Label::new(format!("Thread {id}"))) .end_slot( h_flex() .gap_2() @@ -548,6 +578,9 @@ impl AssistantPanel { .icon_size(IconSize::Small), ), ) + .on_click(cx.listener(move |this, _event, cx| { + this.open_thread(&id, cx); + })) } fn render_last_error(&self, cx: &mut ViewContext) -> Option { diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs index a8413258843022..fc5e0d6a15f1cf 100644 --- a/crates/assistant2/src/thread.rs +++ b/crates/assistant2/src/thread.rs @@ -14,12 +14,28 @@ use language_model::{ use language_models::provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError}; use serde::{Deserialize, Serialize}; use util::post_inc; +use uuid::Uuid; #[derive(Debug, Clone, Copy)] pub enum RequestKind { Chat, } +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)] +pub struct ThreadId(Arc); + +impl ThreadId { + pub fn new() -> Self { + Self(Uuid::new_v4().to_string().into()) + } +} + +impl std::fmt::Display for ThreadId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)] pub struct MessageId(usize); @@ -39,6 +55,7 @@ pub struct Message { /// A thread of conversation with the LLM. pub struct Thread { + id: ThreadId, messages: Vec, next_message_id: MessageId, completion_count: usize, @@ -52,6 +69,7 @@ pub struct Thread { impl Thread { pub fn new(tools: Arc, _cx: &mut ModelContext) -> Self { Self { + id: ThreadId::new(), messages: Vec::new(), next_message_id: MessageId(0), completion_count: 0, @@ -63,10 +81,18 @@ impl Thread { } } + pub fn id(&self) -> &ThreadId { + &self.id + } + pub fn message(&self, id: MessageId) -> Option<&Message> { self.messages.iter().find(|message| message.id == id) } + pub fn messages(&self) -> impl Iterator { + self.messages.iter() + } + pub fn tools(&self) -> &Arc { &self.tools } @@ -76,10 +102,19 @@ impl Thread { } pub fn insert_user_message(&mut self, text: impl Into, cx: &mut ModelContext) { + self.insert_message(Role::User, text, cx) + } + + pub fn insert_message( + &mut self, + role: Role, + text: impl Into, + cx: &mut ModelContext, + ) { let id = self.next_message_id.post_inc(); self.messages.push(Message { id, - role: Role::User, + role, text: text.into(), }); cx.emit(ThreadEvent::MessageAdded(id)); diff --git a/crates/assistant2/src/thread_store.rs b/crates/assistant2/src/thread_store.rs index 99f90eace8304e..d784c842c9d2c1 100644 --- a/crates/assistant2/src/thread_store.rs +++ b/crates/assistant2/src/thread_store.rs @@ -7,14 +7,18 @@ use context_server::manager::ContextServerManager; use context_server::{ContextServerFactoryRegistry, ContextServerTool}; use gpui::{prelude::*, AppContext, Model, ModelContext, Task}; use project::Project; +use unindent::Unindent; use util::ResultExt as _; +use crate::thread::{Thread, ThreadId}; + pub struct ThreadStore { #[allow(unused)] project: Model, tools: Arc, context_server_manager: Model, context_server_tool_ids: HashMap, Vec>, + threads: Vec>, } impl ThreadStore { @@ -31,12 +35,14 @@ impl ThreadStore { ContextServerManager::new(context_server_factory_registry, project.clone(), cx) }); - let this = Self { + let mut this = Self { project, tools, context_server_manager, context_server_tool_ids: HashMap::default(), + threads: Vec::new(), }; + this.mock_recent_threads(cx); this.register_context_server_handlers(cx); this @@ -46,6 +52,23 @@ impl ThreadStore { }) } + pub fn recent_threads(&self, limit: usize, _cx: &ModelContext) -> Vec> { + self.threads.iter().take(limit).cloned().collect() + } + + pub fn create_thread(&mut self, cx: &mut ModelContext) -> Model { + let thread = cx.new_model(|cx| Thread::new(self.tools.clone(), cx)); + self.threads.push(thread.clone()); + thread + } + + pub fn open_thread(&self, id: &ThreadId, cx: &mut ModelContext) -> Option> { + self.threads + .iter() + .find(|thread| thread.read(cx).id() == id) + .cloned() + } + fn register_context_server_handlers(&self, cx: &mut ModelContext) { cx.subscribe( &self.context_server_manager.clone(), @@ -112,3 +135,91 @@ impl ThreadStore { } } } + +impl ThreadStore { + /// Creates some mocked recent threads for testing purposes. + fn mock_recent_threads(&mut self, cx: &mut ModelContext) { + use language_model::Role; + + self.threads.push(cx.new_model(|cx| { + let mut thread = Thread::new(self.tools.clone(), cx); + thread.insert_user_message("Hello! Can you help me understand quantum computing?", cx); + thread.insert_message(Role::Assistant, "Of course! I'd be happy to help you understand quantum computing. Quantum computing is a fascinating field that uses the principles of quantum mechanics to process information. Unlike classical computers that use bits (0s and 1s), quantum computers use quantum bits or 'qubits'. These qubits can exist in multiple states simultaneously, a property called superposition. This allows quantum computers to perform certain calculations much faster than classical computers. What specific aspect of quantum computing would you like to know more about?", cx); + thread.insert_user_message("That's interesting! Can you explain how quantum entanglement is used in quantum computing?", cx); + thread.insert_message(Role::Assistant, "Certainly! Quantum entanglement is a key principle used in quantum computing. When two qubits become entangled, the state of one qubit is directly related to the state of the other, regardless of the distance between them. This property is used in quantum computing to create complex quantum states and to perform operations on multiple qubits simultaneously. Entanglement allows quantum computers to process information in ways that classical computers cannot, potentially solving certain problems much more efficiently. For example, it's crucial in quantum error correction and in algorithms like quantum teleportation, which is important for quantum communication.", cx); + thread + })); + + self.threads.push(cx.new_model(|cx| { + let mut thread = Thread::new(self.tools.clone(), cx); + thread.insert_user_message("Can you show me an example of Rust code for a simple web server?", cx); + thread.insert_message(Role::Assistant, "Certainly! Here's an example of a simple web server in Rust using the `actix-web` framework: + + ```rust + use actix_web::{web, App, HttpResponse, HttpServer, Responder}; + + async fn hello() -> impl Responder { + HttpResponse::Ok().body(\"Hello, World!\") + } + + #[actix_web::main] + async fn main() -> std::io::Result<()> { + HttpServer::new(|| { + App::new() + .route(\"/\", web::get().to(hello)) + }) + .bind(\"127.0.0.1:8080\")? + .run() + .await + } + ``` + + This code creates a basic web server that responds with 'Hello, World!' when you access the root URL. Here's a breakdown of what's happening: + + 1. We import necessary items from the `actix-web` crate. + 2. We define an async `hello` function that returns a simple HTTP response. + 3. In the `main` function, we set up the server to listen on `127.0.0.1:8080`. + 4. We configure the app to respond to GET requests on the root path with our `hello` function. + + To run this, you'd need to add `actix-web` to your `Cargo.toml` dependencies: + + ```toml + [dependencies] + actix-web = \"4.0\" + ``` + + Then you can run the server with `cargo run` and access it at `http://localhost:8080`.".unindent(), cx); + thread.insert_user_message("That's great! Can you explain more about async functions in Rust?", cx); + thread.insert_message(Role::Assistant, "Certainly! Async functions are a key feature in Rust for writing efficient, non-blocking code, especially for I/O-bound operations. Here's an overview: + + 1. **Syntax**: Async functions are declared using the `async` keyword: + + ```rust + async fn my_async_function() -> Result<(), Error> { + // Asynchronous code here + } + ``` + + 2. **Futures**: Async functions return a `Future`. A `Future` represents a value that may not be available yet but will be at some point. + + 3. **Await**: Inside an async function, you can use the `.await` syntax to wait for other async operations to complete: + + ```rust + async fn fetch_data() -> Result { + let response = make_http_request().await?; + let data = process_response(response).await?; + Ok(data) + } + ``` + + 4. **Non-blocking**: Async functions allow the runtime to work on other tasks while waiting for I/O or other operations to complete, making efficient use of system resources. + + 5. **Runtime**: To execute async code, you need a runtime like `tokio` or `async-std`. Actix-web, which we used in the previous example, includes its own runtime. + + 6. **Error Handling**: Async functions work well with Rust's `?` operator for error handling. + + Async programming in Rust provides a powerful way to write concurrent code that's both safe and efficient. It's particularly useful for servers, network programming, and any application that deals with many concurrent operations.".unindent(), cx); + thread + })); + } +}