From 74c581b9f49e0bdd63d9282362f3424b86a320b5 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 20 Feb 2025 17:53:58 -0500 Subject: [PATCH] assistant2: Combine history views into one (#25293) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR combines the two history views in Assistant2 into one. Screenshot 2025-02-20 at 5 34 37 PM Screenshot 2025-02-20 at 5 34 41 PM Release Notes: - N/A --- crates/assistant2/src/assistant.rs | 2 +- crates/assistant2/src/assistant_panel.rs | 186 ++++++------------ crates/assistant2/src/history_store.rs | 61 ++++++ crates/assistant2/src/thread_history.rs | 163 ++++++++++++--- .../assistant_context_editor/src/context.rs | 2 +- .../src/context_store.rs | 6 + 6 files changed, 265 insertions(+), 155 deletions(-) create mode 100644 crates/assistant2/src/history_store.rs diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index 30127a59d5..70d4d9d100 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -7,6 +7,7 @@ mod context; mod context_picker; mod context_store; mod context_strip; +mod history_store; mod inline_assistant; mod inline_prompt_editor; mod message_editor; @@ -40,7 +41,6 @@ actions!( ToggleModelSelector, RemoveAllContext, OpenHistory, - OpenPromptEditorHistory, OpenConfiguration, RemoveSelectedThread, Chat, diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index f20436146e..f83a6dc75d 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use anyhow::{anyhow, Result}; use assistant_context_editor::{ make_lsp_adapter_delegate, render_remaining_tokens, AssistantPanelDelegate, ConfigurationError, - ContextEditor, ContextHistory, SlashCommandCompletionProvider, + ContextEditor, SlashCommandCompletionProvider, }; use assistant_settings::{AssistantDockPosition, AssistantSettings}; use assistant_slash_command::SlashCommandWorkingSet; @@ -31,14 +31,12 @@ use zed_actions::assistant::{DeployPromptLibrary, ToggleFocus}; use crate::active_thread::ActiveThread; use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent}; +use crate::history_store::{HistoryEntry, HistoryStore}; use crate::message_editor::MessageEditor; use crate::thread::{Thread, ThreadError, ThreadId}; -use crate::thread_history::{PastThread, ThreadHistory}; +use crate::thread_history::{PastContext, PastThread, ThreadHistory}; use crate::thread_store::ThreadStore; -use crate::{ - InlineAssistant, NewPromptEditor, NewThread, OpenConfiguration, OpenHistory, - OpenPromptEditorHistory, -}; +use crate::{InlineAssistant, NewPromptEditor, NewThread, OpenConfiguration, OpenHistory}; pub fn init(cx: &mut App) { cx.observe_new( @@ -62,12 +60,6 @@ pub fn init(cx: &mut App) { panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx)); } }) - .register_action(|workspace, _: &OpenPromptEditorHistory, window, cx| { - if let Some(panel) = workspace.panel::(cx) { - workspace.focus_panel::(window, cx); - panel.update(cx, |panel, cx| panel.open_prompt_editor_history(window, cx)); - } - }) .register_action(|workspace, _: &OpenConfiguration, window, cx| { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); @@ -83,7 +75,6 @@ enum ActiveView { Thread, PromptEditor, History, - PromptEditorHistory, Configuration, } @@ -97,15 +88,14 @@ pub struct AssistantPanel { message_editor: Entity, context_store: Entity, context_editor: Option>, - context_history: Option>, configuration: Option>, configuration_subscription: Option, tools: Arc, local_timezone: UtcOffset, active_view: ActiveView, + history_store: Entity, history: Entity, new_item_context_menu_handle: PopoverMenuHandle, - open_history_context_menu_handle: PopoverMenuHandle, width: Option, height: Option, } @@ -173,6 +163,9 @@ impl AssistantPanel { ) }); + let history_store = + cx.new(|cx| HistoryStore::new(thread_store.clone(), context_store.clone(), cx)); + Self { active_view: ActiveView::Thread, workspace: workspace.clone(), @@ -194,7 +187,6 @@ impl AssistantPanel { message_editor, context_store, context_editor: None, - context_history: None, configuration: None, configuration_subscription: None, tools, @@ -202,9 +194,9 @@ impl AssistantPanel { chrono::Local::now().offset().local_minus_utc(), ) .unwrap(), - history: cx.new(|cx| ThreadHistory::new(weak_self, thread_store, cx)), + history_store: history_store.clone(), + history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, cx)), new_item_context_menu_handle: PopoverMenuHandle::default(), - open_history_context_menu_handle: PopoverMenuHandle::default(), width: None, height: None, } @@ -331,26 +323,7 @@ impl AssistantPanel { cx.notify(); } - fn open_prompt_editor_history(&mut self, window: &mut Window, cx: &mut Context) { - self.active_view = ActiveView::PromptEditorHistory; - self.context_history = Some(cx.new(|cx| { - ContextHistory::new( - self.project.clone(), - self.context_store.clone(), - self.workspace.clone(), - window, - cx, - ) - })); - - if let Some(context_history) = self.context_history.as_ref() { - context_history.focus_handle(cx).focus(window); - } - - cx.notify(); - } - - fn open_saved_prompt_editor( + pub(crate) fn open_saved_prompt_editor( &mut self, path: PathBuf, window: &mut Window, @@ -499,13 +472,6 @@ impl Focusable for AssistantPanel { cx.focus_handle() } } - ActiveView::PromptEditorHistory => { - if let Some(context_history) = self.context_history.as_ref() { - context_history.focus_handle(cx) - } else { - cx.focus_handle() - } - } ActiveView::Configuration => { if let Some(configuration) = self.configuration.as_ref() { configuration.focus_handle(cx) @@ -618,18 +584,10 @@ impl AssistantPanel { SharedString::from(context_editor.read(cx).title(cx).to_string()) }) .unwrap_or_else(|| SharedString::from("Loading Summary…")), - ActiveView::History | ActiveView::PromptEditorHistory => "History".into(), + ActiveView::History => "History".into(), ActiveView::Configuration => "Assistant Settings".into(), }; - let sub_title = match self.active_view { - ActiveView::Thread => None, - ActiveView::PromptEditor => None, - ActiveView::History => Some("Thread"), - ActiveView::PromptEditorHistory => Some("Prompt Editor"), - ActiveView::Configuration => None, - }; - h_flex() .id("assistant-toolbar") .px(DynamicSpacing::Base08.rems(cx)) @@ -645,24 +603,7 @@ impl AssistantPanel { .w_full() .gap_1() .justify_between() - .child( - h_flex() - .child(Label::new(title)) - .when(sub_title.is_some(), |this| { - this.child( - h_flex() - .pl_1p5() - .gap_1p5() - .child( - Label::new("/") - .size(LabelSize::Small) - .color(Color::Disabled) - .alpha(0.5), - ) - .child(Label::new(sub_title.unwrap())), - ) - }), - ) + .child(Label::new(title)) .children(if matches!(self.active_view, ActiveView::PromptEditor) { self.context_editor .as_ref() @@ -696,23 +637,23 @@ impl AssistantPanel { }), ) .child( - PopoverMenu::new("assistant-toolbar-history-popover-menu") - .trigger_with_tooltip( - IconButton::new("open-history", IconName::HistoryRerun) - .icon_size(IconSize::Small) - .style(ButtonStyle::Subtle), - Tooltip::text("History…"), - ) - .anchor(Corner::TopRight) - .with_handle(self.open_history_context_menu_handle.clone()) - .menu(move |window, cx| { - Some(ContextMenu::build(window, cx, |menu, _window, _cx| { - menu.action("Thread History", OpenHistory.boxed_clone()) - .action( - "Prompt Editor History", - OpenPromptEditorHistory.boxed_clone(), - ) - })) + IconButton::new("open-history", IconName::HistoryRerun) + .icon_size(IconSize::Small) + .style(ButtonStyle::Subtle) + .tooltip({ + let focus_handle = self.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "History", + &OpenHistory, + &focus_handle, + window, + cx, + ) + } + }) + .on_click(move |_event, window, cx| { + window.dispatch_action(OpenHistory.boxed_clone(), cx); }), ) .child( @@ -762,9 +703,9 @@ impl AssistantPanel { window: &mut Window, cx: &mut Context, ) -> impl IntoElement { - let recent_threads = self - .thread_store - .update(cx, |this, _cx| this.recent_threads(3)); + let recent_history = self + .history_store + .update(cx, |this, cx| this.recent_entries(3, cx)); let create_welcome_heading = || { h_flex() @@ -791,7 +732,8 @@ impl AssistantPanel { ) .map(|parent| { match configuration_error { - Some(ConfigurationError::ProviderNotAuthenticated) | Some(ConfigurationError::NoProvider) => { + Some(ConfigurationError::ProviderNotAuthenticated) + | Some(ConfigurationError::NoProvider) => { parent.child( v_flex() .gap_0p5() @@ -818,34 +760,24 @@ impl AssistantPanel { ), ) } - Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => { - parent.child( - v_flex() - .gap_0p5() - .child(create_welcome_heading()) - .children(provider.render_accept_terms( - LanguageModelProviderTosView::ThreadEmptyState, - cx, - )), - ) - } + Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => parent + .child(v_flex().gap_0p5().child(create_welcome_heading()).children( + provider.render_accept_terms( + LanguageModelProviderTosView::ThreadEmptyState, + cx, + ), + )), None => parent, } }) - .when( - recent_threads.is_empty() && no_error, - |parent| { - parent.child( - v_flex().gap_0p5().child(create_welcome_heading()).child( - h_flex().w_full().justify_center().child( - Label::new("Start typing to chat with your codebase") - .color(Color::Muted), - ), - ), - ) - }, - ) - .when(!recent_threads.is_empty(), |parent| { + .when(recent_history.is_empty() && no_error, |parent| { + parent.child(v_flex().gap_0p5().child(create_welcome_heading()).child( + h_flex().w_full().justify_center().child( + Label::new("Start typing to chat with your codebase").color(Color::Muted), + ), + )) + }) + .when(!recent_history.is_empty(), |parent| { parent .child( h_flex().w_full().justify_center().child( @@ -855,9 +787,18 @@ impl AssistantPanel { ), ) .child(v_flex().mx_auto().w_4_5().gap_2().children( - recent_threads.into_iter().map(|thread| { - // TODO: keyboard navigation - PastThread::new(thread, cx.entity().downgrade(), false) + recent_history.into_iter().map(|entry| { + // TODO: Add keyboard navigation. + match entry { + HistoryEntry::Thread(thread) => { + PastThread::new(thread, cx.entity().downgrade(), false) + .into_any_element() + } + HistoryEntry::Context(context) => { + PastContext::new(context, cx.entity().downgrade(), false) + .into_any_element() + } + } }), )) .child( @@ -869,7 +810,7 @@ impl AssistantPanel { &OpenHistory, &self.focus_handle(cx), window, - cx + cx, )) .on_click(move |_event, window, cx| { window.dispatch_action(OpenHistory.boxed_clone(), cx); @@ -1068,7 +1009,6 @@ impl Render for AssistantPanel { .children(self.render_last_error(cx)), ActiveView::History => parent.child(self.history.clone()), ActiveView::PromptEditor => parent.children(self.context_editor.clone()), - ActiveView::PromptEditorHistory => parent.children(self.context_history.clone()), ActiveView::Configuration => parent.children(self.configuration.clone()), }) } diff --git a/crates/assistant2/src/history_store.rs b/crates/assistant2/src/history_store.rs new file mode 100644 index 0000000000..44d50e00e6 --- /dev/null +++ b/crates/assistant2/src/history_store.rs @@ -0,0 +1,61 @@ +use assistant_context_editor::SavedContextMetadata; +use chrono::{DateTime, Utc}; +use gpui::{prelude::*, Entity}; + +use crate::thread_store::{SavedThreadMetadata, ThreadStore}; + +pub enum HistoryEntry { + Thread(SavedThreadMetadata), + Context(SavedContextMetadata), +} + +impl HistoryEntry { + pub fn updated_at(&self) -> DateTime { + match self { + HistoryEntry::Thread(thread) => thread.updated_at, + HistoryEntry::Context(context) => context.mtime.to_utc(), + } + } +} + +pub struct HistoryStore { + thread_store: Entity, + context_store: Entity, +} + +impl HistoryStore { + pub fn new( + thread_store: Entity, + context_store: Entity, + _cx: &mut Context, + ) -> Self { + Self { + thread_store, + context_store, + } + } + + /// Returns the number of history entries. + pub fn entry_count(&self, cx: &mut Context) -> usize { + self.entries(cx).len() + } + + pub fn entries(&self, cx: &mut Context) -> Vec { + let mut history_entries = Vec::new(); + + for thread in self.thread_store.update(cx, |this, _cx| this.threads()) { + history_entries.push(HistoryEntry::Thread(thread)); + } + + for context in self.context_store.update(cx, |this, _cx| this.contexts()) { + history_entries.push(HistoryEntry::Context(context)); + } + + history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at())); + history_entries + } + + pub fn recent_entries(&self, limit: usize, cx: &mut Context) -> Vec { + self.entries(cx).into_iter().take(limit).collect() + } +} diff --git a/crates/assistant2/src/thread_history.rs b/crates/assistant2/src/thread_history.rs index 2c0870bde2..89023175e4 100644 --- a/crates/assistant2/src/thread_history.rs +++ b/crates/assistant2/src/thread_history.rs @@ -1,3 +1,4 @@ +use assistant_context_editor::SavedContextMetadata; use gpui::{ uniform_list, App, Entity, FocusHandle, Focusable, ScrollStrategy, UniformListScrollHandle, WeakEntity, @@ -5,13 +6,14 @@ use gpui::{ use time::{OffsetDateTime, UtcOffset}; use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip}; -use crate::thread_store::{SavedThreadMetadata, ThreadStore}; +use crate::history_store::{HistoryEntry, HistoryStore}; +use crate::thread_store::SavedThreadMetadata; use crate::{AssistantPanel, RemoveSelectedThread}; pub struct ThreadHistory { focus_handle: FocusHandle, assistant_panel: WeakEntity, - thread_store: Entity, + history_store: Entity, scroll_handle: UniformListScrollHandle, selected_index: usize, } @@ -19,13 +21,13 @@ pub struct ThreadHistory { impl ThreadHistory { pub(crate) fn new( assistant_panel: WeakEntity, - thread_store: Entity, + history_store: Entity, cx: &mut Context, ) -> Self { Self { focus_handle: cx.focus_handle(), assistant_panel, - thread_store, + history_store, scroll_handle: UniformListScrollHandle::default(), selected_index: 0, } @@ -37,7 +39,9 @@ impl ThreadHistory { window: &mut Window, cx: &mut Context, ) { - let count = self.thread_store.read(cx).thread_count(); + let count = self + .history_store + .update(cx, |this, cx| this.entry_count(cx)); if count > 0 { if self.selected_index == 0 { self.set_selected_index(count - 1, window, cx); @@ -53,7 +57,9 @@ impl ThreadHistory { window: &mut Window, cx: &mut Context, ) { - let count = self.thread_store.read(cx).thread_count(); + let count = self + .history_store + .update(cx, |this, cx| this.entry_count(cx)); if count > 0 { if self.selected_index == count - 1 { self.set_selected_index(0, window, cx); @@ -64,14 +70,18 @@ impl ThreadHistory { } fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context) { - let count = self.thread_store.read(cx).thread_count(); + let count = self + .history_store + .update(cx, |this, cx| this.entry_count(cx)); if count > 0 { self.set_selected_index(0, window, cx); } } fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context) { - let count = self.thread_store.read(cx).thread_count(); + let count = self + .history_store + .update(cx, |this, cx| this.entry_count(cx)); if count > 0 { self.set_selected_index(count - 1, window, cx); } @@ -85,12 +95,23 @@ impl ThreadHistory { } fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { - let threads = self.thread_store.update(cx, |this, _cx| this.threads()); + let entries = self.history_store.update(cx, |this, cx| this.entries(cx)); - if let Some(thread) = threads.get(self.selected_index) { - self.assistant_panel - .update(cx, move |this, cx| this.open_thread(&thread.id, window, cx)) - .ok(); + if let Some(entry) = entries.get(self.selected_index) { + match entry { + HistoryEntry::Thread(thread) => { + self.assistant_panel + .update(cx, move |this, cx| this.open_thread(&thread.id, window, cx)) + .ok(); + } + HistoryEntry::Context(context) => { + self.assistant_panel + .update(cx, move |this, cx| { + this.open_saved_prompt_editor(context.path.clone(), window, cx) + }) + .ok(); + } + } cx.notify(); } @@ -102,14 +123,19 @@ impl ThreadHistory { _window: &mut Window, cx: &mut Context, ) { - let threads = self.thread_store.update(cx, |this, _cx| this.threads()); + let entries = self.history_store.update(cx, |this, cx| this.entries(cx)); - if let Some(thread) = threads.get(self.selected_index) { - self.assistant_panel - .update(cx, |this, cx| { - this.delete_thread(&thread.id, cx); - }) - .ok(); + if let Some(entry) = entries.get(self.selected_index) { + match entry { + HistoryEntry::Thread(thread) => { + self.assistant_panel + .update(cx, |this, cx| { + this.delete_thread(&thread.id, cx); + }) + .ok(); + } + HistoryEntry::Context(_context) => {} + } cx.notify(); } @@ -124,7 +150,7 @@ impl Focusable for ThreadHistory { impl Render for ThreadHistory { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let threads = self.thread_store.update(cx, |this, _cx| this.threads()); + let history_entries = self.history_store.update(cx, |this, cx| this.entries(cx)); let selected_index = self.selected_index; v_flex() @@ -141,7 +167,7 @@ impl Render for ThreadHistory { .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::remove_selected_thread)) .map(|history| { - if threads.is_empty() { + if history_entries.is_empty() { history .justify_center() .child( @@ -155,17 +181,26 @@ impl Render for ThreadHistory { uniform_list( cx.entity().clone(), "thread-history", - threads.len(), + history_entries.len(), move |history, range, _window, _cx| { - threads[range] + history_entries[range] .iter() .enumerate() - .map(|(index, thread)| { - h_flex().w_full().pb_1().child(PastThread::new( - thread.clone(), - history.assistant_panel.clone(), - selected_index == index, - )) + .map(|(index, entry)| { + h_flex().w_full().pb_1().child(match entry { + HistoryEntry::Thread(thread) => PastThread::new( + thread.clone(), + history.assistant_panel.clone(), + selected_index == index, + ) + .into_any_element(), + HistoryEntry::Context(context) => PastContext::new( + context.clone(), + history.assistant_panel.clone(), + selected_index == index, + ) + .into_any_element(), + }) }) .collect() }, @@ -261,3 +296,71 @@ impl RenderOnce for PastThread { }) } } + +#[derive(IntoElement)] +pub struct PastContext { + context: SavedContextMetadata, + assistant_panel: WeakEntity, + selected: bool, +} + +impl PastContext { + pub fn new( + context: SavedContextMetadata, + assistant_panel: WeakEntity, + selected: bool, + ) -> Self { + Self { + context, + assistant_panel, + selected, + } + } +} + +impl RenderOnce for PastContext { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let summary = self.context.title; + + let context_timestamp = time_format::format_localized_timestamp( + OffsetDateTime::from_unix_timestamp(self.context.mtime.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(SharedString::from( + self.context.path.to_string_lossy().to_string(), + )) + .outlined() + .toggle_state(self.selected) + .start_slot( + Icon::new(IconName::Code) + .size(IconSize::Small) + .color(Color::Muted), + ) + .spacing(ListItemSpacing::Sparse) + .child(Label::new(summary).size(LabelSize::Small).text_ellipsis()) + .end_slot( + h_flex().gap_1p5().child( + Label::new(context_timestamp) + .color(Color::Muted) + .size(LabelSize::XSmall), + ), + ) + .on_click({ + let assistant_panel = self.assistant_panel.clone(); + let path = self.context.path.clone(); + move |_event, window, cx| { + assistant_panel + .update(cx, |this, cx| { + this.open_saved_prompt_editor(path.clone(), window, cx) + .detach_and_log_err(cx); + }) + .ok(); + } + }) + } +} diff --git a/crates/assistant_context_editor/src/context.rs b/crates/assistant_context_editor/src/context.rs index 68d001b3b1..7b72c4c04d 100644 --- a/crates/assistant_context_editor/src/context.rs +++ b/crates/assistant_context_editor/src/context.rs @@ -3366,7 +3366,7 @@ impl SavedContextV0_1_0 { } } -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct SavedContextMetadata { pub title: String, pub path: PathBuf, diff --git a/crates/assistant_context_editor/src/context_store.rs b/crates/assistant_context_editor/src/context_store.rs index c14087c968..f5424ef712 100644 --- a/crates/assistant_context_editor/src/context_store.rs +++ b/crates/assistant_context_editor/src/context_store.rs @@ -350,6 +350,12 @@ impl ContextStore { } } + pub fn contexts(&self) -> Vec { + let mut contexts = self.contexts_metadata.iter().cloned().collect::>(); + contexts.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.mtime)); + contexts + } + pub fn create(&mut self, cx: &mut Context) -> Entity { let context = cx.new(|cx| { AssistantContext::local(