From a30ea2fc682bdaec572f3e130631e297670612a6 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 4 Dec 2024 16:39:39 -0500 Subject: [PATCH] assistant2: Factor out `ActiveThread` view (#21555) This PR factors a new `ActiveThread` view out of the `AssistantPanel` to group together the state that pertains solely to the active view. There was a bunch of related state on the `AssistantPanel` pertaining to the active thread that needed to be initialized/reset together and it makes for a clearer narrative is this state is encapsulated in its own view. Release Notes: - N/A --- crates/assistant2/src/active_thread.rs | 237 ++++++++++++ crates/assistant2/src/assistant.rs | 1 + crates/assistant2/src/assistant_panel.rs | 436 ++++++++--------------- crates/assistant2/src/thread.rs | 4 + crates/assistant2/src/thread_store.rs | 9 +- 5 files changed, 396 insertions(+), 291 deletions(-) create mode 100644 crates/assistant2/src/active_thread.rs diff --git a/crates/assistant2/src/active_thread.rs b/crates/assistant2/src/active_thread.rs new file mode 100644 index 0000000000000000000000000000000000000000..13b67dc437a7bd30331a030f358557c25e70c53a --- /dev/null +++ b/crates/assistant2/src/active_thread.rs @@ -0,0 +1,237 @@ +use std::sync::Arc; + +use assistant_tool::ToolWorkingSet; +use collections::HashMap; +use gpui::{ + list, AnyElement, Empty, ListAlignment, ListState, Model, StyleRefinement, Subscription, + TextStyleRefinement, View, WeakView, +}; +use language::LanguageRegistry; +use language_model::Role; +use markdown::{Markdown, MarkdownStyle}; +use settings::Settings as _; +use theme::ThemeSettings; +use ui::prelude::*; +use workspace::Workspace; + +use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent}; + +pub struct ActiveThread { + workspace: WeakView, + language_registry: Arc, + tools: Arc, + thread: Model, + messages: Vec, + list_state: ListState, + rendered_messages_by_id: HashMap>, + last_error: Option, + _subscriptions: Vec, +} + +impl ActiveThread { + pub fn new( + thread: Model, + workspace: WeakView, + language_registry: Arc, + tools: Arc, + cx: &mut ViewContext, + ) -> Self { + let subscriptions = vec![ + cx.observe(&thread, |_, _, cx| cx.notify()), + cx.subscribe(&thread, Self::handle_thread_event), + ]; + + let mut this = Self { + workspace, + language_registry, + tools, + thread: thread.clone(), + messages: Vec::new(), + rendered_messages_by_id: HashMap::default(), + list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), { + let this = cx.view().downgrade(); + move |ix, cx: &mut WindowContext| { + this.update(cx, |this, cx| this.render_message(ix, cx)) + .unwrap() + } + }), + last_error: None, + _subscriptions: subscriptions, + }; + + for message in thread.read(cx).messages().cloned().collect::>() { + this.push_message(&message.id, message.text.clone(), cx); + } + + this + } + + pub fn is_empty(&self) -> bool { + self.messages.is_empty() + } + + pub fn last_error(&self) -> Option { + self.last_error.clone() + } + + pub fn clear_last_error(&mut self) { + self.last_error.take(); + } + + fn push_message(&mut self, id: &MessageId, text: String, cx: &mut ViewContext) { + let old_len = self.messages.len(); + self.messages.push(*id); + self.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, + event: &ThreadEvent, + cx: &mut ViewContext, + ) { + match event { + ThreadEvent::ShowError(error) => { + self.last_error = Some(error.clone()); + } + ThreadEvent::StreamedCompletion => {} + ThreadEvent::StreamedAssistantText(message_id, text) => { + if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) { + markdown.update(cx, |markdown, cx| { + markdown.append(text, cx); + }); + } + } + ThreadEvent::MessageAdded(message_id) => { + if let Some(message_text) = self + .thread + .read(cx) + .message(*message_id) + .map(|message| message.text.clone()) + { + self.push_message(message_id, message_text, cx); + } + + cx.notify(); + } + ThreadEvent::UsePendingTools => { + let pending_tool_uses = self + .thread + .read(cx) + .pending_tool_uses() + .into_iter() + .filter(|tool_use| tool_use.status.is_idle()) + .cloned() + .collect::>(); + + for tool_use in pending_tool_uses { + if let Some(tool) = self.tools.tool(&tool_use.name, cx) { + let task = tool.run(tool_use.input, self.workspace.clone(), cx); + + self.thread.update(cx, |thread, cx| { + thread.insert_tool_output( + tool_use.assistant_message_id, + tool_use.id.clone(), + task, + cx, + ); + }); + } + } + } + ThreadEvent::ToolFinished { .. } => {} + } + } + + fn render_message(&self, ix: usize, cx: &mut ViewContext) -> AnyElement { + let message_id = self.messages[ix]; + let Some(message) = self.thread.read(cx).message(message_id) else { + return Empty.into_any(); + }; + + let Some(markdown) = self.rendered_messages_by_id.get(&message_id) else { + return Empty.into_any(); + }; + + let (role_icon, role_name) = match message.role { + Role::User => (IconName::Person, "You"), + Role::Assistant => (IconName::ZedAssistant, "Assistant"), + Role::System => (IconName::Settings, "System"), + }; + + div() + .id(("message-container", ix)) + .p_2() + .child( + v_flex() + .border_1() + .border_color(cx.theme().colors().border_variant) + .rounded_md() + .child( + h_flex() + .justify_between() + .p_1p5() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .child( + h_flex() + .gap_2() + .child(Icon::new(role_icon).size(IconSize::Small)) + .child(Label::new(role_name).size(LabelSize::Small)), + ), + ) + .child(v_flex().p_1p5().text_ui(cx).child(markdown.clone())), + ) + .into_any() + } +} + +impl Render for ActiveThread { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + list(self.list_state.clone()).flex_1() + } +} diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index aa79ce0c672cf4b3f905c7981af3ed3f85e20b48..dfa361ad8c50d106145a9e2adf201dcc4cf352a7 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -1,3 +1,4 @@ +mod active_thread; mod assistant_panel; mod message_editor; mod thread; diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 00bd15de2e220cf6f9c1b972e891a6e7fd680c77..d17480cd0e30e504c9a06301524a00b8299feb57 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -3,25 +3,21 @@ use std::sync::Arc; use anyhow::Result; use assistant_tool::ToolWorkingSet; use client::zed_urls; -use collections::HashMap; use gpui::{ - list, prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, Empty, - EventEmitter, FocusHandle, FocusableView, FontWeight, ListAlignment, ListState, Model, Pixels, - StyleRefinement, Subscription, Task, TextStyleRefinement, View, ViewContext, WeakView, + prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, EventEmitter, + FocusHandle, FocusableView, FontWeight, Model, Pixels, Task, View, ViewContext, WeakView, WindowContext, }; use language::LanguageRegistry; -use language_model::{LanguageModelRegistry, Role}; +use language_model::LanguageModelRegistry; use language_model_selector::LanguageModelSelector; -use markdown::{Markdown, MarkdownStyle}; -use settings::Settings; -use theme::ThemeSettings; use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, KeyBinding, ListItem, Tab, Tooltip}; use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::Workspace; +use crate::active_thread::ActiveThread; use crate::message_editor::MessageEditor; -use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent, ThreadId}; +use crate::thread::{Thread, ThreadError, ThreadId}; use crate::thread_store::ThreadStore; use crate::{NewThread, OpenHistory, ToggleFocus, ToggleModelSelector}; @@ -39,16 +35,10 @@ pub fn init(cx: &mut AppContext) { pub struct AssistantPanel { workspace: WeakView, language_registry: Arc, - #[allow(unused)] thread_store: Model, - thread: Model, - thread_messages: Vec, - rendered_messages_by_id: HashMap>, - thread_list_state: ListState, + thread: Option>, message_editor: View, tools: Arc, - last_error: Option, - _subscriptions: Vec, } impl AssistantPanel { @@ -78,29 +68,14 @@ impl AssistantPanel { cx: &mut ViewContext, ) -> Self { 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), - ]; Self { workspace: workspace.weak_handle(), language_registry: workspace.project().read(cx).languages().clone(), thread_store, - thread: thread.clone(), - thread_messages: Vec::new(), - rendered_messages_by_id: HashMap::default(), - thread_list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), { - let this = cx.view().downgrade(); - move |ix, cx: &mut WindowContext| { - this.update(cx, |this, cx| this.render_message(ix, cx)) - .unwrap() - } - }), + thread: None, message_editor: cx.new_view(|cx| MessageEditor::new(thread, cx)), tools, - last_error: None, - _subscriptions: subscriptions, } } @@ -108,7 +83,18 @@ impl AssistantPanel { let thread = self .thread_store .update(cx, |this, cx| this.create_thread(cx)); - self.reset_thread(thread, cx); + + self.thread = Some(cx.new_view(|cx| { + ActiveThread::new( + thread.clone(), + self.workspace.clone(), + self.language_registry.clone(), + 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) { @@ -118,136 +104,18 @@ impl AssistantPanel { 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), - ]; - - self.message_editor = cx.new_view(|cx| MessageEditor::new(thread.clone(), cx)); - self.thread = thread; - self.thread_messages.clear(); - self.thread_list_state.reset(0); - self.rendered_messages_by_id.clear(); - self._subscriptions = subscriptions; - - 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, + self.thread = Some(cx.new_view(|cx| { + ActiveThread::new( + thread.clone(), + self.workspace.clone(), + self.language_registry.clone(), + self.tools.clone(), cx, ) - }); - self.rendered_messages_by_id.insert(*id, markdown); - } - - fn handle_thread_event( - &mut self, - _: Model, - event: &ThreadEvent, - cx: &mut ViewContext, - ) { - match event { - ThreadEvent::ShowError(error) => { - self.last_error = Some(error.clone()); - } - ThreadEvent::StreamedCompletion => {} - ThreadEvent::StreamedAssistantText(message_id, text) => { - if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) { - markdown.update(cx, |markdown, cx| { - markdown.append(text, cx); - }); - } - } - ThreadEvent::MessageAdded(message_id) => { - if let Some(message_text) = self - .thread - .read(cx) - .message(*message_id) - .map(|message| message.text.clone()) - { - self.push_message(message_id, message_text, cx); - } - - cx.notify(); - } - ThreadEvent::UsePendingTools => { - let pending_tool_uses = self - .thread - .read(cx) - .pending_tool_uses() - .into_iter() - .filter(|tool_use| tool_use.status.is_idle()) - .cloned() - .collect::>(); - - for tool_use in pending_tool_uses { - if let Some(tool) = self.tools.tool(&tool_use.name, cx) { - let task = tool.run(tool_use.input, self.workspace.clone(), cx); - - self.thread.update(cx, |thread, cx| { - thread.insert_tool_output( - tool_use.assistant_message_id, - tool_use.id.clone(), - task, - cx, - ); - }); - } - } - } - ThreadEvent::ToolFinished { .. } => {} - } + })); + self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx)); + self.message_editor.focus_handle(cx).focus(cx); } } @@ -422,140 +290,105 @@ impl AssistantPanel { ) } - fn render_message_list(&self, cx: &mut ViewContext) -> AnyElement { - if self.thread_messages.is_empty() { - let recent_threads = self - .thread_store - .update(cx, |this, cx| this.recent_threads(3, cx)); + 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(); + }; - return v_flex() - .gap_2() - .mx_auto() - .child( - v_flex().w_full().child( - svg() - .path("icons/logo_96.svg") - .text_color(cx.theme().colors().text) - .w(px(40.)) - .h(px(40.)) - .mx_auto() - .mb_4(), - ), - ) - .child(v_flex()) - .child( - h_flex() - .w_full() - .justify_center() - .child(Label::new("Context Examples:").size(LabelSize::Small)), - ) - .child( - h_flex() - .gap_2() - .justify_center() - .child( - h_flex() - .gap_1() - .p_0p5() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border_variant) - .child( - Icon::new(IconName::Terminal) - .size(IconSize::Small) - .color(Color::Disabled), - ) - .child(Label::new("Terminal").size(LabelSize::Small)), - ) - .child( - h_flex() - .gap_1() - .p_0p5() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border_variant) - .child( - Icon::new(IconName::Folder) - .size(IconSize::Small) - .color(Color::Disabled), - ) - .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()); - }), - ), - ) - .into_any(); + if thread.read(cx).is_empty() { + return self.render_thread_empty_state(cx).into_any_element(); } - list(self.thread_list_state.clone()).flex_1().into_any() + thread.clone().into_any() } - fn render_message(&self, ix: usize, cx: &mut ViewContext) -> AnyElement { - let message_id = self.thread_messages[ix]; - let Some(message) = self.thread.read(cx).message(message_id) else { - return Empty.into_any(); - }; - - let Some(markdown) = self.rendered_messages_by_id.get(&message_id) else { - return Empty.into_any(); - }; - - let (role_icon, role_name) = match message.role { - Role::User => (IconName::Person, "You"), - Role::Assistant => (IconName::ZedAssistant, "Assistant"), - Role::System => (IconName::Settings, "System"), - }; + fn render_thread_empty_state(&self, cx: &mut ViewContext) -> impl IntoElement { + let recent_threads = self + .thread_store + .update(cx, |this, cx| this.recent_threads(3, cx)); - div() - .id(("message-container", ix)) - .p_2() + v_flex() + .gap_2() + .mx_auto() .child( - v_flex() - .border_1() - .border_color(cx.theme().colors().border_variant) - .rounded_md() + v_flex().w_full().child( + svg() + .path("icons/logo_96.svg") + .text_color(cx.theme().colors().text) + .w(px(40.)) + .h(px(40.)) + .mx_auto() + .mb_4(), + ), + ) + .child(v_flex()) + .child( + h_flex() + .w_full() + .justify_center() + .child(Label::new("Context Examples:").size(LabelSize::Small)), + ) + .child( + h_flex() + .gap_2() + .justify_center() .child( h_flex() - .justify_between() - .p_1p5() - .border_b_1() + .gap_1() + .p_0p5() + .rounded_md() + .border_1() .border_color(cx.theme().colors().border_variant) .child( - h_flex() - .gap_2() - .child(Icon::new(role_icon).size(IconSize::Small)) - .child(Label::new(role_name).size(LabelSize::Small)), - ), + Icon::new(IconName::Terminal) + .size(IconSize::Small) + .color(Color::Disabled), + ) + .child(Label::new("Terminal").size(LabelSize::Small)), ) - .child(v_flex().p_1p5().text_ui(cx).child(markdown.clone())), + .child( + h_flex() + .gap_1() + .p_0p5() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().border_variant) + .child( + Icon::new(IconName::Folder) + .size(IconSize::Small) + .color(Color::Disabled), + ) + .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()); + }), + ), ) - .into_any() } fn render_past_thread( @@ -584,7 +417,7 @@ impl AssistantPanel { } fn render_last_error(&self, cx: &mut ViewContext) -> Option { - let last_error = self.last_error.as_ref()?; + let last_error = self.thread.as_ref()?.read(cx).last_error()?; Some( div() @@ -602,7 +435,7 @@ impl AssistantPanel { self.render_max_monthly_spend_reached_error(cx) } ThreadError::Message(error_message) => { - self.render_error_message(error_message, cx) + self.render_error_message(&error_message, cx) } }) .into_any(), @@ -634,14 +467,24 @@ impl AssistantPanel { .mt_1() .child(Button::new("subscribe", "Subscribe").on_click(cx.listener( |this, _, cx| { - this.last_error = None; + if let Some(thread) = this.thread.as_ref() { + thread.update(cx, |this, _cx| { + this.clear_last_error(); + }); + } + cx.open_url(&zed_urls::account_url(cx)); cx.notify(); }, ))) .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( |this, _, cx| { - this.last_error = None; + if let Some(thread) = this.thread.as_ref() { + thread.update(cx, |this, _cx| { + this.clear_last_error(); + }); + } + cx.notify(); }, ))), @@ -675,7 +518,12 @@ impl AssistantPanel { .child( Button::new("subscribe", "Update Monthly Spend Limit").on_click( cx.listener(|this, _, cx| { - this.last_error = None; + if let Some(thread) = this.thread.as_ref() { + thread.update(cx, |this, _cx| { + this.clear_last_error(); + }); + } + cx.open_url(&zed_urls::account_url(cx)); cx.notify(); }), @@ -683,7 +531,12 @@ impl AssistantPanel { ) .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( |this, _, cx| { - this.last_error = None; + if let Some(thread) = this.thread.as_ref() { + thread.update(cx, |this, _cx| { + this.clear_last_error(); + }); + } + cx.notify(); }, ))), @@ -721,7 +574,12 @@ impl AssistantPanel { .mt_1() .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( |this, _, cx| { - this.last_error = None; + if let Some(thread) = this.thread.as_ref() { + thread.update(cx, |this, _cx| { + this.clear_last_error(); + }); + } + cx.notify(); }, ))), @@ -743,7 +601,7 @@ impl Render for AssistantPanel { println!("Open History"); })) .child(self.render_toolbar(cx)) - .child(self.render_message_list(cx)) + .child(self.render_active_thread_or_empty_state(cx)) .child( h_flex() .border_t_1() diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs index fc5e0d6a15f1cfa4f83f1d7295cf30be60d8888a..185719fa98b398d21ca250a293f6559604c05ea0 100644 --- a/crates/assistant2/src/thread.rs +++ b/crates/assistant2/src/thread.rs @@ -85,6 +85,10 @@ impl Thread { &self.id } + pub fn is_empty(&self) -> bool { + self.messages.is_empty() + } + pub fn message(&self, id: MessageId) -> Option<&Message> { self.messages.iter().find(|message| message.id == id) } diff --git a/crates/assistant2/src/thread_store.rs b/crates/assistant2/src/thread_store.rs index d784c842c9d2c1aac694f2aac672ff72f1f0f02b..80e6d292659ae212dd3dff6647934246a96354a3 100644 --- a/crates/assistant2/src/thread_store.rs +++ b/crates/assistant2/src/thread_store.rs @@ -52,8 +52,13 @@ impl ThreadStore { }) } - pub fn recent_threads(&self, limit: usize, _cx: &ModelContext) -> Vec> { - self.threads.iter().take(limit).cloned().collect() + pub fn recent_threads(&self, limit: usize, cx: &ModelContext) -> Vec> { + self.threads + .iter() + .filter(|thread| !thread.read(cx).is_empty()) + .take(limit) + .cloned() + .collect() } pub fn create_thread(&mut self, cx: &mut ModelContext) -> Model {