@@ -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::<AssistantPanel>(cx) {
- workspace.focus_panel::<AssistantPanel>(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::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
@@ -83,7 +75,6 @@ enum ActiveView {
Thread,
PromptEditor,
History,
- PromptEditorHistory,
Configuration,
}
@@ -97,15 +88,14 @@ pub struct AssistantPanel {
message_editor: Entity<MessageEditor>,
context_store: Entity<assistant_context_editor::ContextStore>,
context_editor: Option<Entity<ContextEditor>>,
- context_history: Option<Entity<ContextHistory>>,
configuration: Option<Entity<AssistantConfiguration>>,
configuration_subscription: Option<Subscription>,
tools: Arc<ToolWorkingSet>,
local_timezone: UtcOffset,
active_view: ActiveView,
+ history_store: Entity<HistoryStore>,
history: Entity<ThreadHistory>,
new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
- open_history_context_menu_handle: PopoverMenuHandle<ContextMenu>,
width: Option<Pixels>,
height: Option<Pixels>,
}
@@ -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>) {
- 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<Self>,
) -> 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()),
})
}
@@ -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<AssistantPanel>,
- thread_store: Entity<ThreadStore>,
+ history_store: Entity<HistoryStore>,
scroll_handle: UniformListScrollHandle,
selected_index: usize,
}
@@ -19,13 +21,13 @@ pub struct ThreadHistory {
impl ThreadHistory {
pub(crate) fn new(
assistant_panel: WeakEntity<AssistantPanel>,
- thread_store: Entity<ThreadStore>,
+ history_store: Entity<HistoryStore>,
cx: &mut Context<Self>,
) -> 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<Self>,
) {
- 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<Self>,
) {
- 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<Self>) {
- 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<Self>) {
- 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<Self>) {
- 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<Self>,
) {
- 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<Self>) -> 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<AssistantPanel>,
+ selected: bool,
+}
+
+impl PastContext {
+ pub fn new(
+ context: SavedContextMetadata,
+ assistant_panel: WeakEntity<AssistantPanel>,
+ 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();
+ }
+ })
+ }
+}