From 787c75cbda8b0ea3ad1bc036329f3826ae6b9e8f Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 5 Dec 2024 13:22:25 -0500 Subject: [PATCH] assistant2: Add thread history (#21599) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds support for thread history to the Assistant 2 panel. We also now generate summaries for the threads. Screenshot 2024-12-05 at 12 56 53 PM Screenshot 2024-12-05 at 12 56 58 PM Release Notes: - N/A --------- Co-authored-by: Piotr --- Cargo.lock | 3 + crates/assistant2/Cargo.toml | 3 + crates/assistant2/src/active_thread.rs | 9 +- crates/assistant2/src/assistant.rs | 1 + crates/assistant2/src/assistant_panel.rs | 224 ++++++++++++----------- crates/assistant2/src/thread.rs | 102 +++++++++-- crates/assistant2/src/thread_history.rs | 144 +++++++++++++++ crates/assistant2/src/thread_store.rs | 16 +- 8 files changed, 375 insertions(+), 127 deletions(-) create mode 100644 crates/assistant2/src/thread_history.rs diff --git a/Cargo.lock b/Cargo.lock index be4e11263d19189ab61ec9c386203d31dfe4f467..bd3cd06dca3e3a6b97a202037c1bc91ab8547934 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -456,6 +456,7 @@ version = "0.1.0" dependencies = [ "anyhow", "assistant_tool", + "chrono", "client", "collections", "command_palette_hooks", @@ -478,6 +479,8 @@ dependencies = [ "settings", "smol", "theme", + "time", + "time_format", "ui", "unindent", "util", diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index e5253adbce0181dbdfb286d650971d0ac8e2f599..b5f5fe8ecd93265f491e0bdc0273e22d6cf7d084 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -15,6 +15,7 @@ doctest = false [dependencies] anyhow.workspace = true assistant_tool.workspace = true +chrono.workspace = true client.workspace = true collections.workspace = true command_palette_hooks.workspace = true @@ -37,6 +38,8 @@ serde_json.workspace = true settings.workspace = true smol.workspace = true theme.workspace = true +time.workspace = true +time_format.workspace = true ui.workspace = true unindent.workspace = true util.workspace = true diff --git a/crates/assistant2/src/active_thread.rs b/crates/assistant2/src/active_thread.rs index 13b67dc437a7bd30331a030f358557c25e70c53a..d9cd8fcc4658806acaf19b7178be38133fc885b7 100644 --- a/crates/assistant2/src/active_thread.rs +++ b/crates/assistant2/src/active_thread.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use assistant_tool::ToolWorkingSet; use collections::HashMap; use gpui::{ - list, AnyElement, Empty, ListAlignment, ListState, Model, StyleRefinement, Subscription, - TextStyleRefinement, View, WeakView, + list, AnyElement, AppContext, Empty, ListAlignment, ListState, Model, StyleRefinement, + Subscription, TextStyleRefinement, View, WeakView, }; use language::LanguageRegistry; use language_model::Role; @@ -70,6 +70,10 @@ impl ActiveThread { self.messages.is_empty() } + pub fn summary(&self, cx: &AppContext) -> Option { + self.thread.read(cx).summary() + } + pub fn last_error(&self) -> Option { self.last_error.clone() } @@ -139,6 +143,7 @@ impl ActiveThread { self.last_error = Some(error.clone()); } ThreadEvent::StreamedCompletion => {} + ThreadEvent::SummaryChanged => {} ThreadEvent::StreamedAssistantText(message_id, text) => { if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) { markdown.update(cx, |markdown, cx| { diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index 13ac2d821bb8be636ce52b38c31f124ea4446160..3c8520680e49d7c4d2def1179399adefdfcbe61d 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -3,6 +3,7 @@ mod assistant_panel; mod context_picker; mod message_editor; mod thread; +mod thread_history; mod thread_store; use command_palette_hooks::CommandPaletteFilter; diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index d17480cd0e30e504c9a06301524a00b8299feb57..2d9f563c2f6817fca5318fbfa2a5c2456e154739 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -11,13 +11,15 @@ use gpui::{ use language::LanguageRegistry; use language_model::LanguageModelRegistry; use language_model_selector::LanguageModelSelector; -use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, KeyBinding, ListItem, Tab, Tooltip}; +use time::UtcOffset; +use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, KeyBinding, Tab, Tooltip}; use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::Workspace; use crate::active_thread::ActiveThread; use crate::message_editor::MessageEditor; -use crate::thread::{Thread, ThreadError, ThreadId}; +use crate::thread::{ThreadError, ThreadId}; +use crate::thread_history::{PastThread, ThreadHistory}; use crate::thread_store::ThreadStore; use crate::{NewThread, OpenHistory, ToggleFocus, ToggleModelSelector}; @@ -32,13 +34,21 @@ pub fn init(cx: &mut AppContext) { .detach(); } +enum ActiveView { + Thread, + History, +} + pub struct AssistantPanel { workspace: WeakView, language_registry: Arc, thread_store: Model, - thread: Option>, + thread: View, message_editor: View, tools: Arc, + local_timezone: UtcOffset, + active_view: ActiveView, + history: View, } impl AssistantPanel { @@ -68,14 +78,31 @@ impl AssistantPanel { cx: &mut ViewContext, ) -> Self { let thread = thread_store.update(cx, |this, cx| this.create_thread(cx)); + let language_registry = workspace.project().read(cx).languages().clone(); + let workspace = workspace.weak_handle(); + let weak_self = cx.view().downgrade(); Self { - workspace: workspace.weak_handle(), - language_registry: workspace.project().read(cx).languages().clone(), - thread_store, - thread: None, - message_editor: cx.new_view(|cx| MessageEditor::new(thread, cx)), + active_view: ActiveView::Thread, + workspace: workspace.clone(), + language_registry: language_registry.clone(), + thread_store: thread_store.clone(), + thread: cx.new_view(|cx| { + ActiveThread::new( + thread.clone(), + workspace, + language_registry, + tools.clone(), + cx, + ) + }), + message_editor: cx.new_view(|cx| MessageEditor::new(thread.clone(), cx)), tools, + local_timezone: UtcOffset::from_whole_seconds( + chrono::Local::now().offset().local_minus_utc(), + ) + .unwrap(), + history: cx.new_view(|cx| ThreadHistory::new(weak_self, thread_store, cx)), } } @@ -84,7 +111,8 @@ impl AssistantPanel { .thread_store .update(cx, |this, cx| this.create_thread(cx)); - self.thread = Some(cx.new_view(|cx| { + self.active_view = ActiveView::Thread; + self.thread = cx.new_view(|cx| { ActiveThread::new( thread.clone(), self.workspace.clone(), @@ -92,12 +120,12 @@ impl AssistantPanel { self.tools.clone(), cx, ) - })); + }); self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx)); self.message_editor.focus_handle(cx).focus(cx); } - fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext) { + pub(crate) 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)) @@ -105,7 +133,8 @@ impl AssistantPanel { return; }; - self.thread = Some(cx.new_view(|cx| { + self.active_view = ActiveView::Thread; + self.thread = cx.new_view(|cx| { ActiveThread::new( thread.clone(), self.workspace.clone(), @@ -113,15 +142,22 @@ impl AssistantPanel { self.tools.clone(), cx, ) - })); + }); self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx)); self.message_editor.focus_handle(cx).focus(cx); } + + pub(crate) fn local_timezone(&self) -> UtcOffset { + self.local_timezone + } } impl FocusableView for AssistantPanel { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - self.message_editor.focus_handle(cx) + match self.active_view { + ActiveView::Thread => self.message_editor.focus_handle(cx), + ActiveView::History => self.history.focus_handle(cx), + } } } @@ -180,7 +216,7 @@ impl AssistantPanel { .bg(cx.theme().colors().tab_bar_background) .border_b_1() .border_color(cx.theme().colors().border_variant) - .child(h_flex().child(Label::new("Thread Title Goes Here"))) + .child(h_flex().children(self.thread.read(cx).summary(cx).map(Label::new))) .child( h_flex() .gap(DynamicSpacing::Base08.rems(cx)) @@ -291,15 +327,11 @@ impl AssistantPanel { } fn render_active_thread_or_empty_state(&self, cx: &mut ViewContext) -> AnyElement { - let Some(thread) = self.thread.as_ref() else { - return self.render_thread_empty_state(cx).into_any_element(); - }; - - if thread.read(cx).is_empty() { + if self.thread.read(cx).is_empty() { return self.render_thread_empty_state(cx).into_any_element(); } - thread.clone().into_any() + self.thread.clone().into_any() } fn render_thread_empty_state(&self, cx: &mut ViewContext) -> impl IntoElement { @@ -361,63 +393,41 @@ impl AssistantPanel { .child(Label::new("/src/components").size(LabelSize::Small)), ), ) - .child( - h_flex() - .w_full() - .justify_center() - .child(Label::new("Recent Threads:").size(LabelSize::Small)), - ) - .child( - v_flex().gap_2().children( - recent_threads - .into_iter() - .map(|thread| self.render_past_thread(thread, cx)), - ), - ) - .child( - h_flex().w_full().justify_center().child( - Button::new("view-all-past-threads", "View All Past Threads") - .style(ButtonStyle::Subtle) - .label_size(LabelSize::Small) - .key_binding(KeyBinding::for_action_in( - &OpenHistory, - &self.focus_handle(cx), - cx, - )) - .on_click(move |_event, cx| { - cx.dispatch_action(OpenHistory.boxed_clone()); - }), - ), - ) - } - - 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(format!("Thread {id}"))) - .end_slot( - h_flex() - .gap_2() - .child(Label::new("1 hour ago").color(Color::Disabled)) + .when(!recent_threads.is_empty(), |parent| { + parent .child( - IconButton::new("delete", IconName::TrashAlt) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small), - ), - ) - .on_click(cx.listener(move |this, _event, cx| { - this.open_thread(&id, cx); - })) + h_flex() + .w_full() + .justify_center() + .child(Label::new("Recent Threads:").size(LabelSize::Small)), + ) + .child( + v_flex().gap_2().children( + recent_threads + .into_iter() + .map(|thread| PastThread::new(thread, cx.view().downgrade())), + ), + ) + .child( + h_flex().w_full().justify_center().child( + Button::new("view-all-past-threads", "View All Past Threads") + .style(ButtonStyle::Subtle) + .label_size(LabelSize::Small) + .key_binding(KeyBinding::for_action_in( + &OpenHistory, + &self.focus_handle(cx), + cx, + )) + .on_click(move |_event, cx| { + cx.dispatch_action(OpenHistory.boxed_clone()); + }), + ), + ) + }) } fn render_last_error(&self, cx: &mut ViewContext) -> Option { - let last_error = self.thread.as_ref()?.read(cx).last_error()?; + let last_error = self.thread.read(cx).last_error()?; Some( div() @@ -467,11 +477,9 @@ impl AssistantPanel { .mt_1() .child(Button::new("subscribe", "Subscribe").on_click(cx.listener( |this, _, cx| { - if let Some(thread) = this.thread.as_ref() { - thread.update(cx, |this, _cx| { - this.clear_last_error(); - }); - } + this.thread.update(cx, |this, _cx| { + this.clear_last_error(); + }); cx.open_url(&zed_urls::account_url(cx)); cx.notify(); @@ -479,11 +487,9 @@ impl AssistantPanel { ))) .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( |this, _, cx| { - if let Some(thread) = this.thread.as_ref() { - thread.update(cx, |this, _cx| { - this.clear_last_error(); - }); - } + this.thread.update(cx, |this, _cx| { + this.clear_last_error(); + }); cx.notify(); }, @@ -518,11 +524,9 @@ impl AssistantPanel { .child( Button::new("subscribe", "Update Monthly Spend Limit").on_click( cx.listener(|this, _, cx| { - if let Some(thread) = this.thread.as_ref() { - thread.update(cx, |this, _cx| { - this.clear_last_error(); - }); - } + this.thread.update(cx, |this, _cx| { + this.clear_last_error(); + }); cx.open_url(&zed_urls::account_url(cx)); cx.notify(); @@ -531,11 +535,9 @@ impl AssistantPanel { ) .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( |this, _, cx| { - if let Some(thread) = this.thread.as_ref() { - thread.update(cx, |this, _cx| { - this.clear_last_error(); - }); - } + this.thread.update(cx, |this, _cx| { + this.clear_last_error(); + }); cx.notify(); }, @@ -574,11 +576,9 @@ impl AssistantPanel { .mt_1() .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( |this, _, cx| { - if let Some(thread) = this.thread.as_ref() { - thread.update(cx, |this, _cx| { - this.clear_last_error(); - }); - } + this.thread.update(cx, |this, _cx| { + this.clear_last_error(); + }); cx.notify(); }, @@ -597,17 +597,23 @@ impl Render for AssistantPanel { .on_action(cx.listener(|this, _: &NewThread, cx| { this.new_thread(cx); })) - .on_action(cx.listener(|_this, _: &OpenHistory, _cx| { - println!("Open History"); + .on_action(cx.listener(|this, _: &OpenHistory, cx| { + this.active_view = ActiveView::History; + this.history.focus_handle(cx).focus(cx); + cx.notify(); })) .child(self.render_toolbar(cx)) - .child(self.render_active_thread_or_empty_state(cx)) - .child( - h_flex() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child(self.message_editor.clone()), - ) - .children(self.render_last_error(cx)) + .map(|parent| match self.active_view { + ActiveView::Thread => parent + .child(self.render_active_thread_or_empty_state(cx)) + .child( + h_flex() + .border_t_1() + .border_color(cx.theme().colors().border_variant) + .child(self.message_editor.clone()), + ) + .children(self.render_last_error(cx)), + ActiveView::History => parent.child(self.history.clone()), + }) } } diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs index 185719fa98b398d21ca250a293f6559604c05ea0..833f8c9b03327dfcdd93b582cd6cf21541e50a4d 100644 --- a/crates/assistant2/src/thread.rs +++ b/crates/assistant2/src/thread.rs @@ -2,18 +2,19 @@ use std::sync::Arc; use anyhow::Result; use assistant_tool::ToolWorkingSet; +use chrono::{DateTime, Utc}; use collections::HashMap; use futures::future::Shared; use futures::{FutureExt as _, StreamExt as _}; use gpui::{AppContext, EventEmitter, ModelContext, SharedString, Task}; use language_model::{ - LanguageModel, LanguageModelCompletionEvent, LanguageModelRequest, LanguageModelRequestMessage, - LanguageModelToolResult, LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role, - StopReason, + LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest, + LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse, + LanguageModelToolUseId, MessageContent, Role, StopReason, }; use language_models::provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError}; use serde::{Deserialize, Serialize}; -use util::post_inc; +use util::{post_inc, TryFutureExt as _}; use uuid::Uuid; #[derive(Debug, Clone, Copy)] @@ -56,6 +57,9 @@ pub struct Message { /// A thread of conversation with the LLM. pub struct Thread { id: ThreadId, + updated_at: DateTime, + summary: Option, + pending_summary: Task>, messages: Vec, next_message_id: MessageId, completion_count: usize, @@ -70,6 +74,9 @@ impl Thread { pub fn new(tools: Arc, _cx: &mut ModelContext) -> Self { Self { id: ThreadId::new(), + updated_at: Utc::now(), + summary: None, + pending_summary: Task::ready(None), messages: Vec::new(), next_message_id: MessageId(0), completion_count: 0, @@ -89,6 +96,23 @@ impl Thread { self.messages.is_empty() } + pub fn updated_at(&self) -> DateTime { + self.updated_at + } + + pub fn touch_updated_at(&mut self) { + self.updated_at = Utc::now(); + } + + pub fn summary(&self) -> Option { + self.summary.clone() + } + + pub fn set_summary(&mut self, summary: impl Into, cx: &mut ModelContext) { + self.summary = Some(summary.into()); + cx.emit(ThreadEvent::SummaryChanged); + } + pub fn message(&self, id: MessageId) -> Option<&Message> { self.messages.iter().find(|message| message.id == id) } @@ -121,6 +145,7 @@ impl Thread { role, text: text.into(), }); + self.touch_updated_at(); cx.emit(ThreadEvent::MessageAdded(id)); } @@ -191,13 +216,7 @@ impl Thread { thread.update(&mut cx, |thread, cx| { match event { LanguageModelCompletionEvent::StartMessage { .. } => { - let id = thread.next_message_id.post_inc(); - thread.messages.push(Message { - id, - role: Role::Assistant, - text: String::new(), - }); - cx.emit(ThreadEvent::MessageAdded(id)); + thread.insert_message(Role::Assistant, String::new(), cx); } LanguageModelCompletionEvent::Stop(reason) => { stop_reason = reason; @@ -239,6 +258,7 @@ impl Thread { } } + thread.touch_updated_at(); cx.emit(ThreadEvent::StreamedCompletion); cx.notify(); })?; @@ -246,10 +266,14 @@ impl Thread { smol::future::yield_now().await; } - thread.update(&mut cx, |thread, _cx| { + thread.update(&mut cx, |thread, cx| { thread .pending_completions .retain(|completion| completion.id != pending_completion_id); + + if thread.summary.is_none() && thread.messages.len() >= 2 { + thread.summarize(cx); + } })?; anyhow::Ok(stop_reason) @@ -292,6 +316,59 @@ impl Thread { }); } + pub fn summarize(&mut self, cx: &mut ModelContext) { + let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else { + return; + }; + let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else { + return; + }; + + if !provider.is_authenticated(cx) { + return; + } + + let mut request = self.to_completion_request(RequestKind::Chat, cx); + request.messages.push(LanguageModelRequestMessage { + role: Role::User, + content: vec![ + "Generate a concise 3-7 word title for this conversation, omitting punctuation. Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`" + .into(), + ], + cache: false, + }); + + self.pending_summary = cx.spawn(|this, mut cx| { + async move { + let stream = model.stream_completion_text(request, &cx); + let mut messages = stream.await?; + + let mut new_summary = String::new(); + while let Some(message) = messages.stream.next().await { + let text = message?; + let mut lines = text.lines(); + new_summary.extend(lines.next()); + + // Stop if the LLM generated multiple lines. + if lines.next().is_some() { + break; + } + } + + this.update(&mut cx, |this, cx| { + if !new_summary.is_empty() { + this.summary = Some(new_summary.into()); + } + + cx.emit(ThreadEvent::SummaryChanged); + })?; + + anyhow::Ok(()) + } + .log_err() + }); + } + pub fn insert_tool_output( &mut self, assistant_message_id: MessageId, @@ -365,6 +442,7 @@ pub enum ThreadEvent { StreamedCompletion, StreamedAssistantText(MessageId, String), MessageAdded(MessageId), + SummaryChanged, UsePendingTools, ToolFinished { #[allow(unused)] diff --git a/crates/assistant2/src/thread_history.rs b/crates/assistant2/src/thread_history.rs new file mode 100644 index 0000000000000000000000000000000000000000..7216ca695a77750eaaed31193205673673dcf418 --- /dev/null +++ b/crates/assistant2/src/thread_history.rs @@ -0,0 +1,144 @@ +use gpui::{ + uniform_list, AppContext, FocusHandle, FocusableView, Model, UniformListScrollHandle, WeakView, +}; +use time::{OffsetDateTime, UtcOffset}; +use ui::{prelude::*, IconButtonShape, ListItem}; + +use crate::thread::Thread; +use crate::thread_store::ThreadStore; +use crate::AssistantPanel; + +pub struct ThreadHistory { + focus_handle: FocusHandle, + assistant_panel: WeakView, + thread_store: Model, + scroll_handle: UniformListScrollHandle, +} + +impl ThreadHistory { + pub(crate) fn new( + assistant_panel: WeakView, + thread_store: Model, + cx: &mut ViewContext, + ) -> Self { + Self { + focus_handle: cx.focus_handle(), + assistant_panel, + thread_store, + scroll_handle: UniformListScrollHandle::default(), + } + } +} + +impl FocusableView for ThreadHistory { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for ThreadHistory { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let threads = self.thread_store.update(cx, |this, cx| this.threads(cx)); + + v_flex() + .id("thread-history-container") + .track_focus(&self.focus_handle) + .overflow_y_scroll() + .size_full() + .p_1() + .map(|history| { + if threads.is_empty() { + history + .justify_center() + .child( + h_flex().w_full().justify_center().child( + Label::new("You don't have any past threads yet.") + .size(LabelSize::Small), + ), + ) + } else { + history.child( + uniform_list( + cx.view().clone(), + "thread-history", + threads.len(), + move |history, range, _cx| { + threads[range] + .iter() + .map(|thread| { + PastThread::new( + thread.clone(), + history.assistant_panel.clone(), + ) + }) + .collect() + }, + ) + .track_scroll(self.scroll_handle.clone()) + .flex_grow(), + ) + } + }) + } +} + +#[derive(IntoElement)] +pub struct PastThread { + thread: Model, + assistant_panel: WeakView, +} + +impl PastThread { + pub fn new(thread: Model, assistant_panel: WeakView) -> Self { + Self { + thread, + assistant_panel, + } + } +} + +impl RenderOnce for PastThread { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let (id, summary) = { + const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread"); + let thread = self.thread.read(cx); + ( + thread.id().clone(), + thread.summary().unwrap_or(DEFAULT_SUMMARY), + ) + }; + + let thread_timestamp = time_format::format_localized_timestamp( + OffsetDateTime::from_unix_timestamp(self.thread.read(cx).updated_at().timestamp()) + .unwrap(), + OffsetDateTime::now_utc(), + self.assistant_panel + .update(cx, |this, _cx| this.local_timezone()) + .unwrap_or(UtcOffset::UTC), + time_format::TimestampFormat::EnhancedAbsolute, + ); + ListItem::new(("past-thread", self.thread.entity_id())) + .start_slot(Icon::new(IconName::MessageBubbles)) + .child(Label::new(summary)) + .end_slot( + h_flex() + .gap_2() + .child(Label::new(thread_timestamp).color(Color::Disabled)) + .child( + IconButton::new("delete", IconName::TrashAlt) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small), + ), + ) + .on_click({ + let assistant_panel = self.assistant_panel.clone(); + move |_event, cx| { + assistant_panel + .update(cx, |this, cx| { + this.open_thread(&id, cx); + }) + .ok(); + } + }) + } +} diff --git a/crates/assistant2/src/thread_store.rs b/crates/assistant2/src/thread_store.rs index 80e6d292659ae212dd3dff6647934246a96354a3..7ceee9306b9c05b912188a6102c9bf772794d8b8 100644 --- a/crates/assistant2/src/thread_store.rs +++ b/crates/assistant2/src/thread_store.rs @@ -52,13 +52,19 @@ impl ThreadStore { }) } - pub fn recent_threads(&self, limit: usize, cx: &ModelContext) -> Vec> { - self.threads + pub fn threads(&self, cx: &ModelContext) -> Vec> { + let mut threads = self + .threads .iter() .filter(|thread| !thread.read(cx).is_empty()) - .take(limit) .cloned() - .collect() + .collect::>(); + threads.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.read(cx).updated_at())); + threads + } + + pub fn recent_threads(&self, limit: usize, cx: &ModelContext) -> Vec> { + self.threads(cx).into_iter().take(limit).collect() } pub fn create_thread(&mut self, cx: &mut ModelContext) -> Model { @@ -148,6 +154,7 @@ impl ThreadStore { self.threads.push(cx.new_model(|cx| { let mut thread = Thread::new(self.tools.clone(), cx); + thread.set_summary("Introduction to quantum computing", 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); @@ -157,6 +164,7 @@ impl ThreadStore { self.threads.push(cx.new_model(|cx| { let mut thread = Thread::new(self.tools.clone(), cx); + thread.set_summary("Rust web development and async programming", 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: