assistant2: Thread history keyboard navigation (#23145)
Open and delete threads via keyboard: https://github.com/user-attachments/assets/79b402ad-a49d-4c52-9d46-28a7bf32ff1f Note: this doesn't include navigation in the "recent threads" section of the empty state Release Notes: - N/A
This commit is contained in:
parent
5da67899b7
commit
938e28f871
6 changed files with 126 additions and 11 deletions
|
@ -597,6 +597,12 @@
|
|||
"enter": "assistant2::AcceptSuggestedContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ThreadHistory",
|
||||
"bindings": {
|
||||
"backspace": "assistant2::RemoveSelectedThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "PromptEditor",
|
||||
"bindings": {
|
||||
|
|
|
@ -252,6 +252,12 @@
|
|||
"enter": "assistant2::AcceptSuggestedContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ThreadHistory",
|
||||
"bindings": {
|
||||
"backspace": "assistant2::RemoveSelectedThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "PromptLibrary",
|
||||
"use_key_equivalents": true,
|
||||
|
|
|
@ -40,6 +40,7 @@ actions!(
|
|||
ToggleModelSelector,
|
||||
RemoveAllContext,
|
||||
OpenHistory,
|
||||
RemoveSelectedThread,
|
||||
Chat,
|
||||
ChatMode,
|
||||
CycleNextInlineAssist,
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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<AssistantPanel>,
|
||||
thread_store: Model<ThreadStore>,
|
||||
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<Self>) {
|
||||
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<Self>) {
|
||||
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<Self>) {
|
||||
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<Self>) {
|
||||
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>) {
|
||||
self.selected_index = index;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(index, ScrollStrategy::Top);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||
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<Self>) {
|
||||
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<Self>) -> 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<Thread>,
|
||||
assistant_panel: WeakView<AssistantPanel>,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
impl PastThread {
|
||||
pub fn new(thread: Model<Thread>, assistant_panel: WeakView<AssistantPanel>) -> Self {
|
||||
pub fn new(
|
||||
thread: Model<Thread>,
|
||||
assistant_panel: WeakView<AssistantPanel>,
|
||||
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)
|
||||
|
|
|
@ -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<Self>) -> Vec<Model<Thread>> {
|
||||
let mut threads = self
|
||||
.threads
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue