diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index c29a961841..b3bf895c5d 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -597,6 +597,12 @@ "enter": "assistant2::AcceptSuggestedContext" } }, + { + "context": "ThreadHistory", + "bindings": { + "backspace": "assistant2::RemoveSelectedThread" + } + }, { "context": "PromptEditor", "bindings": { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 627dc10956..e768899ec1 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -252,6 +252,12 @@ "enter": "assistant2::AcceptSuggestedContext" } }, + { + "context": "ThreadHistory", + "bindings": { + "backspace": "assistant2::RemoveSelectedThread" + } + }, { "context": "PromptLibrary", "use_key_equivalents": true, diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index 28883f37cf..bf35d35765 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -40,6 +40,7 @@ actions!( ToggleModelSelector, RemoveAllContext, OpenHistory, + RemoveSelectedThread, Chat, ChatMode, CycleNextInlineAssist, diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 4e704be591..0d4e5a8d54 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -428,13 +428,12 @@ impl AssistantPanel { .color(Color::Muted), ), ) - .child( - v_flex().mx_auto().w_4_5().gap_2().children( - recent_threads - .into_iter() - .map(|thread| PastThread::new(thread, cx.view().downgrade())), - ), - ) + .child(v_flex().mx_auto().w_4_5().gap_2().children( + recent_threads.into_iter().map(|thread| { + // TODO: keyboard navigation + PastThread::new(thread, cx.view().downgrade(), false) + }), + )) .child( h_flex().w_full().justify_center().child( Button::new("view-all-past-threads", "View All Past Threads") diff --git a/crates/assistant2/src/thread_history.rs b/crates/assistant2/src/thread_history.rs index 0b33340d12..18619fd051 100644 --- a/crates/assistant2/src/thread_history.rs +++ b/crates/assistant2/src/thread_history.rs @@ -1,18 +1,20 @@ use gpui::{ - uniform_list, AppContext, FocusHandle, FocusableView, Model, UniformListScrollHandle, WeakView, + uniform_list, AppContext, FocusHandle, FocusableView, Model, ScrollStrategy, + UniformListScrollHandle, WeakView, }; use time::{OffsetDateTime, UtcOffset}; use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip}; use crate::thread::Thread; use crate::thread_store::ThreadStore; -use crate::AssistantPanel; +use crate::{AssistantPanel, RemoveSelectedThread}; pub struct ThreadHistory { focus_handle: FocusHandle, assistant_panel: WeakView, thread_store: Model, scroll_handle: UniformListScrollHandle, + selected_index: usize, } impl ThreadHistory { @@ -26,6 +28,82 @@ impl ThreadHistory { assistant_panel, thread_store, scroll_handle: UniformListScrollHandle::default(), + selected_index: 0, + } + } + + pub fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext) { + let count = self.thread_store.read(cx).non_empty_len(cx); + + if count > 0 { + if self.selected_index == 0 { + self.set_selected_index(count - 1, cx); + } else { + self.set_selected_index(self.selected_index - 1, cx); + } + } + } + + pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext) { + let count = self.thread_store.read(cx).non_empty_len(cx); + + if count > 0 { + if self.selected_index == count - 1 { + self.set_selected_index(0, cx); + } else { + self.set_selected_index(self.selected_index + 1, cx); + } + } + } + + fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext) { + let count = self.thread_store.read(cx).non_empty_len(cx); + if count > 0 { + self.set_selected_index(0, cx); + } + } + + fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext) { + let count = self.thread_store.read(cx).non_empty_len(cx); + if count > 0 { + self.set_selected_index(count - 1, cx); + } + } + + fn set_selected_index(&mut self, index: usize, cx: &mut ViewContext) { + self.selected_index = index; + self.scroll_handle + .scroll_to_item(index, ScrollStrategy::Top); + cx.notify(); + } + + fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + let threads = self.thread_store.update(cx, |this, cx| this.threads(cx)); + + if let Some(thread) = threads.get(self.selected_index) { + self.assistant_panel + .update(cx, move |this, cx| { + let thread_id = thread.read(cx).id().clone(); + this.open_thread(&thread_id, cx) + }) + .ok(); + + cx.notify(); + } + } + + fn remove_selected_thread(&mut self, _: &RemoveSelectedThread, cx: &mut ViewContext) { + let threads = self.thread_store.update(cx, |this, cx| this.threads(cx)); + + if let Some(thread) = threads.get(self.selected_index) { + self.assistant_panel + .update(cx, |this, cx| { + let thread_id = thread.read(cx).id().clone(); + this.delete_thread(&thread_id, cx); + }) + .ok(); + + cx.notify(); } } } @@ -39,13 +117,21 @@ impl FocusableView for ThreadHistory { 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)); + let selected_index = self.selected_index; v_flex() .id("thread-history-container") + .key_context("ThreadHistory") .track_focus(&self.focus_handle) .overflow_y_scroll() .size_full() .p_1() + .on_action(cx.listener(Self::select_prev)) + .on_action(cx.listener(Self::select_next)) + .on_action(cx.listener(Self::select_first)) + .on_action(cx.listener(Self::select_last)) + .on_action(cx.listener(Self::confirm)) + .on_action(cx.listener(Self::remove_selected_thread)) .map(|history| { if threads.is_empty() { history @@ -65,10 +151,12 @@ impl Render for ThreadHistory { move |history, range, _cx| { threads[range] .iter() - .map(|thread| { + .enumerate() + .map(|(index, thread)| { h_flex().w_full().pb_1().child(PastThread::new( thread.clone(), history.assistant_panel.clone(), + selected_index == index, )) }) .collect() @@ -86,13 +174,19 @@ impl Render for ThreadHistory { pub struct PastThread { thread: Model, assistant_panel: WeakView, + selected: bool, } impl PastThread { - pub fn new(thread: Model, assistant_panel: WeakView) -> Self { + pub fn new( + thread: Model, + assistant_panel: WeakView, + selected: bool, + ) -> Self { Self { thread, assistant_panel, + selected, } } } @@ -116,6 +210,7 @@ impl RenderOnce for PastThread { ListItem::new(("past-thread", self.thread.entity_id())) .outlined() + .toggle_state(self.selected) .start_slot( Icon::new(IconName::MessageCircle) .size(IconSize::Small) diff --git a/crates/assistant2/src/thread_store.rs b/crates/assistant2/src/thread_store.rs index 367a60a366..e07e447f79 100644 --- a/crates/assistant2/src/thread_store.rs +++ b/crates/assistant2/src/thread_store.rs @@ -52,6 +52,14 @@ impl ThreadStore { }) } + /// Returns the number of non-empty threads. + pub fn non_empty_len(&self, cx: &AppContext) -> usize { + self.threads + .iter() + .filter(|thread| !thread.read(cx).is_empty()) + .count() + } + pub fn threads(&self, cx: &ModelContext) -> Vec> { let mut threads = self .threads