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"
|
"enter": "assistant2::AcceptSuggestedContext"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "ThreadHistory",
|
||||||
|
"bindings": {
|
||||||
|
"backspace": "assistant2::RemoveSelectedThread"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "PromptEditor",
|
"context": "PromptEditor",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
|
|
@ -252,6 +252,12 @@
|
||||||
"enter": "assistant2::AcceptSuggestedContext"
|
"enter": "assistant2::AcceptSuggestedContext"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "ThreadHistory",
|
||||||
|
"bindings": {
|
||||||
|
"backspace": "assistant2::RemoveSelectedThread"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "PromptLibrary",
|
"context": "PromptLibrary",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
|
|
|
@ -40,6 +40,7 @@ actions!(
|
||||||
ToggleModelSelector,
|
ToggleModelSelector,
|
||||||
RemoveAllContext,
|
RemoveAllContext,
|
||||||
OpenHistory,
|
OpenHistory,
|
||||||
|
RemoveSelectedThread,
|
||||||
Chat,
|
Chat,
|
||||||
ChatMode,
|
ChatMode,
|
||||||
CycleNextInlineAssist,
|
CycleNextInlineAssist,
|
||||||
|
|
|
@ -428,13 +428,12 @@ impl AssistantPanel {
|
||||||
.color(Color::Muted),
|
.color(Color::Muted),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(v_flex().mx_auto().w_4_5().gap_2().children(
|
||||||
v_flex().mx_auto().w_4_5().gap_2().children(
|
recent_threads.into_iter().map(|thread| {
|
||||||
recent_threads
|
// TODO: keyboard navigation
|
||||||
.into_iter()
|
PastThread::new(thread, cx.view().downgrade(), false)
|
||||||
.map(|thread| PastThread::new(thread, cx.view().downgrade())),
|
}),
|
||||||
),
|
))
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
h_flex().w_full().justify_center().child(
|
h_flex().w_full().justify_center().child(
|
||||||
Button::new("view-all-past-threads", "View All Past Threads")
|
Button::new("view-all-past-threads", "View All Past Threads")
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
use gpui::{
|
use gpui::{
|
||||||
uniform_list, AppContext, FocusHandle, FocusableView, Model, UniformListScrollHandle, WeakView,
|
uniform_list, AppContext, FocusHandle, FocusableView, Model, ScrollStrategy,
|
||||||
|
UniformListScrollHandle, WeakView,
|
||||||
};
|
};
|
||||||
use time::{OffsetDateTime, UtcOffset};
|
use time::{OffsetDateTime, UtcOffset};
|
||||||
use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip};
|
use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip};
|
||||||
|
|
||||||
use crate::thread::Thread;
|
use crate::thread::Thread;
|
||||||
use crate::thread_store::ThreadStore;
|
use crate::thread_store::ThreadStore;
|
||||||
use crate::AssistantPanel;
|
use crate::{AssistantPanel, RemoveSelectedThread};
|
||||||
|
|
||||||
pub struct ThreadHistory {
|
pub struct ThreadHistory {
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
assistant_panel: WeakView<AssistantPanel>,
|
assistant_panel: WeakView<AssistantPanel>,
|
||||||
thread_store: Model<ThreadStore>,
|
thread_store: Model<ThreadStore>,
|
||||||
scroll_handle: UniformListScrollHandle,
|
scroll_handle: UniformListScrollHandle,
|
||||||
|
selected_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ThreadHistory {
|
impl ThreadHistory {
|
||||||
|
@ -26,6 +28,82 @@ impl ThreadHistory {
|
||||||
assistant_panel,
|
assistant_panel,
|
||||||
thread_store,
|
thread_store,
|
||||||
scroll_handle: UniformListScrollHandle::default(),
|
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 {
|
impl Render for ThreadHistory {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
let threads = self.thread_store.update(cx, |this, cx| this.threads(cx));
|
let threads = self.thread_store.update(cx, |this, cx| this.threads(cx));
|
||||||
|
let selected_index = self.selected_index;
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.id("thread-history-container")
|
.id("thread-history-container")
|
||||||
|
.key_context("ThreadHistory")
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
.overflow_y_scroll()
|
.overflow_y_scroll()
|
||||||
.size_full()
|
.size_full()
|
||||||
.p_1()
|
.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| {
|
.map(|history| {
|
||||||
if threads.is_empty() {
|
if threads.is_empty() {
|
||||||
history
|
history
|
||||||
|
@ -65,10 +151,12 @@ impl Render for ThreadHistory {
|
||||||
move |history, range, _cx| {
|
move |history, range, _cx| {
|
||||||
threads[range]
|
threads[range]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|thread| {
|
.enumerate()
|
||||||
|
.map(|(index, thread)| {
|
||||||
h_flex().w_full().pb_1().child(PastThread::new(
|
h_flex().w_full().pb_1().child(PastThread::new(
|
||||||
thread.clone(),
|
thread.clone(),
|
||||||
history.assistant_panel.clone(),
|
history.assistant_panel.clone(),
|
||||||
|
selected_index == index,
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
@ -86,13 +174,19 @@ impl Render for ThreadHistory {
|
||||||
pub struct PastThread {
|
pub struct PastThread {
|
||||||
thread: Model<Thread>,
|
thread: Model<Thread>,
|
||||||
assistant_panel: WeakView<AssistantPanel>,
|
assistant_panel: WeakView<AssistantPanel>,
|
||||||
|
selected: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PastThread {
|
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 {
|
Self {
|
||||||
thread,
|
thread,
|
||||||
assistant_panel,
|
assistant_panel,
|
||||||
|
selected,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,6 +210,7 @@ impl RenderOnce for PastThread {
|
||||||
|
|
||||||
ListItem::new(("past-thread", self.thread.entity_id()))
|
ListItem::new(("past-thread", self.thread.entity_id()))
|
||||||
.outlined()
|
.outlined()
|
||||||
|
.toggle_state(self.selected)
|
||||||
.start_slot(
|
.start_slot(
|
||||||
Icon::new(IconName::MessageCircle)
|
Icon::new(IconName::MessageCircle)
|
||||||
.size(IconSize::Small)
|
.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>> {
|
pub fn threads(&self, cx: &ModelContext<Self>) -> Vec<Model<Thread>> {
|
||||||
let mut threads = self
|
let mut threads = self
|
||||||
.threads
|
.threads
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue