agent: Add search to Thread History (#28085)
 Release Notes: - agent: Add search box to thread history --------- Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de> Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
This commit is contained in:
parent
277a3f8d6f
commit
3d48efad67
3 changed files with 276 additions and 98 deletions
|
@ -228,7 +228,7 @@ impl AssistantPanel {
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
history_store: history_store.clone(),
|
history_store: history_store.clone(),
|
||||||
history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, cx)),
|
history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
|
||||||
assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
|
assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
|
||||||
width: None,
|
width: None,
|
||||||
height: None,
|
height: None,
|
||||||
|
@ -1134,11 +1134,11 @@ impl AssistantPanel {
|
||||||
// TODO: Add keyboard navigation.
|
// TODO: Add keyboard navigation.
|
||||||
match entry {
|
match entry {
|
||||||
HistoryEntry::Thread(thread) => {
|
HistoryEntry::Thread(thread) => {
|
||||||
PastThread::new(thread, cx.entity().downgrade(), false)
|
PastThread::new(thread, cx.entity().downgrade(), false, vec![])
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
HistoryEntry::Context(context) => {
|
HistoryEntry::Context(context) => {
|
||||||
PastContext::new(context, cx.entity().downgrade(), false)
|
PastContext::new(context, cx.entity().downgrade(), false, vec![])
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ use gpui::{Entity, prelude::*};
|
||||||
|
|
||||||
use crate::thread_store::{SerializedThreadMetadata, ThreadStore};
|
use crate::thread_store::{SerializedThreadMetadata, ThreadStore};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum HistoryEntry {
|
pub enum HistoryEntry {
|
||||||
Thread(SerializedThreadMetadata),
|
Thread(SerializedThreadMetadata),
|
||||||
Context(SavedContextMetadata),
|
Context(SavedContextMetadata),
|
||||||
|
@ -21,25 +22,27 @@ impl HistoryEntry {
|
||||||
pub struct HistoryStore {
|
pub struct HistoryStore {
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
context_store: Entity<assistant_context_editor::ContextStore>,
|
context_store: Entity<assistant_context_editor::ContextStore>,
|
||||||
|
_subscriptions: Vec<gpui::Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HistoryStore {
|
impl HistoryStore {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
context_store: Entity<assistant_context_editor::ContextStore>,
|
context_store: Entity<assistant_context_editor::ContextStore>,
|
||||||
_cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let subscriptions = vec![
|
||||||
|
cx.observe(&thread_store, |_, _, cx| cx.notify()),
|
||||||
|
cx.observe(&context_store, |_, _, cx| cx.notify()),
|
||||||
|
];
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
thread_store,
|
thread_store,
|
||||||
context_store,
|
context_store,
|
||||||
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the number of history entries.
|
|
||||||
pub fn entry_count(&self, cx: &mut Context<Self>) -> usize {
|
|
||||||
self.entries(cx).len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
|
pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
|
||||||
let mut history_entries = Vec::new();
|
let mut history_entries = Vec::new();
|
||||||
|
|
||||||
|
|
|
@ -1,52 +1,176 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use assistant_context_editor::SavedContextMetadata;
|
use assistant_context_editor::SavedContextMetadata;
|
||||||
|
use editor::{Editor, EditorEvent};
|
||||||
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, Entity, FocusHandle, Focusable, ScrollStrategy, UniformListScrollHandle, WeakEntity,
|
App, Entity, FocusHandle, Focusable, ScrollStrategy, Task, UniformListScrollHandle, WeakEntity,
|
||||||
uniform_list,
|
Window, uniform_list,
|
||||||
};
|
};
|
||||||
use time::{OffsetDateTime, UtcOffset};
|
use time::{OffsetDateTime, UtcOffset};
|
||||||
use ui::{IconButtonShape, ListItem, ListItemSpacing, Tooltip, prelude::*};
|
use ui::{HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tooltip, prelude::*};
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
use crate::history_store::{HistoryEntry, HistoryStore};
|
use crate::history_store::{HistoryEntry, HistoryStore};
|
||||||
use crate::thread_store::SerializedThreadMetadata;
|
use crate::thread_store::SerializedThreadMetadata;
|
||||||
use crate::{AssistantPanel, RemoveSelectedThread};
|
use crate::{AssistantPanel, RemoveSelectedThread};
|
||||||
|
|
||||||
pub struct ThreadHistory {
|
pub struct ThreadHistory {
|
||||||
focus_handle: FocusHandle,
|
|
||||||
assistant_panel: WeakEntity<AssistantPanel>,
|
assistant_panel: WeakEntity<AssistantPanel>,
|
||||||
history_store: Entity<HistoryStore>,
|
|
||||||
scroll_handle: UniformListScrollHandle,
|
scroll_handle: UniformListScrollHandle,
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
|
search_query: SharedString,
|
||||||
|
search_editor: Entity<Editor>,
|
||||||
|
all_entries: Arc<Vec<HistoryEntry>>,
|
||||||
|
matches: Vec<StringMatch>,
|
||||||
|
_subscriptions: Vec<gpui::Subscription>,
|
||||||
|
_search_task: Option<Task<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ThreadHistory {
|
impl ThreadHistory {
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
assistant_panel: WeakEntity<AssistantPanel>,
|
assistant_panel: WeakEntity<AssistantPanel>,
|
||||||
history_store: Entity<HistoryStore>,
|
history_store: Entity<HistoryStore>,
|
||||||
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let search_editor = cx.new(|cx| {
|
||||||
|
let mut editor = Editor::single_line(window, cx);
|
||||||
|
editor.set_placeholder_text("Search threads...", cx);
|
||||||
|
editor
|
||||||
|
});
|
||||||
|
|
||||||
|
let search_editor_subscription = cx.subscribe_in(
|
||||||
|
&search_editor,
|
||||||
|
window,
|
||||||
|
|this, search_editor, event, window, cx| {
|
||||||
|
if let EditorEvent::BufferEdited = event {
|
||||||
|
let query = search_editor.read(cx).text(cx);
|
||||||
|
this.search_query = query.into();
|
||||||
|
this.update_search(window, cx);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let entries: Arc<Vec<_>> = history_store
|
||||||
|
.update(cx, |store, cx| store.entries(cx))
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let history_store_subscription =
|
||||||
|
cx.observe_in(&history_store, window, |this, history_store, window, cx| {
|
||||||
|
this.all_entries = history_store
|
||||||
|
.update(cx, |store, cx| store.entries(cx))
|
||||||
|
.into();
|
||||||
|
this.matches.clear();
|
||||||
|
this.update_search(window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
focus_handle: cx.focus_handle(),
|
|
||||||
assistant_panel,
|
assistant_panel,
|
||||||
history_store,
|
|
||||||
scroll_handle: UniformListScrollHandle::default(),
|
scroll_handle: UniformListScrollHandle::default(),
|
||||||
selected_index: 0,
|
selected_index: 0,
|
||||||
|
search_query: SharedString::new_static(""),
|
||||||
|
all_entries: entries,
|
||||||
|
matches: Vec::new(),
|
||||||
|
search_editor,
|
||||||
|
_subscriptions: vec![search_editor_subscription, history_store_subscription],
|
||||||
|
_search_task: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_search(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self._search_task.take();
|
||||||
|
|
||||||
|
if self.has_search_query() {
|
||||||
|
self.perform_search(cx);
|
||||||
|
} else {
|
||||||
|
self.matches.clear();
|
||||||
|
self.set_selected_index(0, cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform_search(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let query = self.search_query.clone();
|
||||||
|
let all_entries = self.all_entries.clone();
|
||||||
|
|
||||||
|
let task = cx.spawn(async move |this, cx| {
|
||||||
|
let executor = cx.background_executor().clone();
|
||||||
|
|
||||||
|
let matches = cx
|
||||||
|
.background_spawn(async move {
|
||||||
|
let mut candidates = Vec::with_capacity(all_entries.len());
|
||||||
|
|
||||||
|
for (idx, entry) in all_entries.iter().enumerate() {
|
||||||
|
match entry {
|
||||||
|
HistoryEntry::Thread(thread) => {
|
||||||
|
candidates.push(StringMatchCandidate::new(idx, &thread.summary));
|
||||||
|
}
|
||||||
|
HistoryEntry::Context(context) => {
|
||||||
|
candidates.push(StringMatchCandidate::new(idx, &context.title));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_MATCHES: usize = 100;
|
||||||
|
|
||||||
|
fuzzy::match_strings(
|
||||||
|
&candidates,
|
||||||
|
&query,
|
||||||
|
false,
|
||||||
|
MAX_MATCHES,
|
||||||
|
&Default::default(),
|
||||||
|
executor,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.matches = matches;
|
||||||
|
this.set_selected_index(0, cx);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
});
|
||||||
|
|
||||||
|
self._search_task = Some(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_search_query(&self) -> bool {
|
||||||
|
!self.search_query.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matched_count(&self) -> usize {
|
||||||
|
if self.has_search_query() {
|
||||||
|
self.matches.len()
|
||||||
|
} else {
|
||||||
|
self.all_entries.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_match(&self, ix: usize) -> Option<&HistoryEntry> {
|
||||||
|
if self.has_search_query() {
|
||||||
|
self.matches
|
||||||
|
.get(ix)
|
||||||
|
.and_then(|m| self.all_entries.get(m.candidate_id))
|
||||||
|
} else {
|
||||||
|
self.all_entries.get(ix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_previous(
|
pub fn select_previous(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &menu::SelectPrevious,
|
_: &menu::SelectPrevious,
|
||||||
window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let count = self
|
let count = self.matched_count();
|
||||||
.history_store
|
|
||||||
.update(cx, |this, cx| this.entry_count(cx));
|
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
if self.selected_index == 0 {
|
if self.selected_index == 0 {
|
||||||
self.set_selected_index(count - 1, window, cx);
|
self.set_selected_index(count - 1, cx);
|
||||||
} else {
|
} else {
|
||||||
self.set_selected_index(self.selected_index - 1, window, cx);
|
self.set_selected_index(self.selected_index - 1, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,40 +178,39 @@ impl ThreadHistory {
|
||||||
pub fn select_next(
|
pub fn select_next(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &menu::SelectNext,
|
_: &menu::SelectNext,
|
||||||
window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let count = self
|
let count = self.matched_count();
|
||||||
.history_store
|
|
||||||
.update(cx, |this, cx| this.entry_count(cx));
|
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
if self.selected_index == count - 1 {
|
if self.selected_index == count - 1 {
|
||||||
self.set_selected_index(0, window, cx);
|
self.set_selected_index(0, cx);
|
||||||
} else {
|
} else {
|
||||||
self.set_selected_index(self.selected_index + 1, window, cx);
|
self.set_selected_index(self.selected_index + 1, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
|
fn select_first(
|
||||||
let count = self
|
&mut self,
|
||||||
.history_store
|
_: &menu::SelectFirst,
|
||||||
.update(cx, |this, cx| this.entry_count(cx));
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let count = self.matched_count();
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
self.set_selected_index(0, window, cx);
|
self.set_selected_index(0, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) {
|
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let count = self
|
let count = self.matched_count();
|
||||||
.history_store
|
|
||||||
.update(cx, |this, cx| this.entry_count(cx));
|
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
self.set_selected_index(count - 1, window, cx);
|
self.set_selected_index(count - 1, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_selected_index(&mut self, index: usize, _window: &mut Window, cx: &mut Context<Self>) {
|
fn set_selected_index(&mut self, index: usize, cx: &mut Context<Self>) {
|
||||||
self.selected_index = index;
|
self.selected_index = index;
|
||||||
self.scroll_handle
|
self.scroll_handle
|
||||||
.scroll_to_item(index, ScrollStrategy::Top);
|
.scroll_to_item(index, ScrollStrategy::Top);
|
||||||
|
@ -95,23 +218,23 @@ impl ThreadHistory {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let entries = self.history_store.update(cx, |this, cx| this.entries(cx));
|
if let Some(entry) = self.get_match(self.selected_index) {
|
||||||
|
let task_result = match entry {
|
||||||
if let Some(entry) = entries.get(self.selected_index) {
|
HistoryEntry::Thread(thread) => self
|
||||||
match entry {
|
.assistant_panel
|
||||||
HistoryEntry::Thread(thread) => {
|
|
||||||
self.assistant_panel
|
|
||||||
.update(cx, move |this, cx| this.open_thread(&thread.id, window, cx))
|
.update(cx, move |this, cx| this.open_thread(&thread.id, window, cx))
|
||||||
.ok();
|
.ok(),
|
||||||
}
|
HistoryEntry::Context(context) => self
|
||||||
HistoryEntry::Context(context) => {
|
.assistant_panel
|
||||||
self.assistant_panel
|
|
||||||
.update(cx, move |this, cx| {
|
.update(cx, move |this, cx| {
|
||||||
this.open_saved_prompt_editor(context.path.clone(), window, cx)
|
this.open_saved_prompt_editor(context.path.clone(), window, cx)
|
||||||
})
|
})
|
||||||
.ok();
|
.ok(),
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
if let Some(task) = task_result {
|
||||||
|
task.detach_and_log_err(cx);
|
||||||
|
};
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
@ -120,12 +243,10 @@ impl ThreadHistory {
|
||||||
fn remove_selected_thread(
|
fn remove_selected_thread(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &RemoveSelectedThread,
|
_: &RemoveSelectedThread,
|
||||||
_window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let entries = self.history_store.update(cx, |this, cx| this.entries(cx));
|
if let Some(entry) = self.get_match(self.selected_index) {
|
||||||
|
|
||||||
if let Some(entry) = entries.get(self.selected_index) {
|
|
||||||
match entry {
|
match entry {
|
||||||
HistoryEntry::Thread(thread) => {
|
HistoryEntry::Thread(thread) => {
|
||||||
self.assistant_panel
|
self.assistant_panel
|
||||||
|
@ -143,72 +264,117 @@ impl ThreadHistory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.update_search(window, cx);
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Focusable for ThreadHistory {
|
impl Focusable for ThreadHistory {
|
||||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||||
self.focus_handle.clone()
|
self.search_editor.focus_handle(cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for ThreadHistory {
|
impl Render for ThreadHistory {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let history_entries = self.history_store.update(cx, |this, cx| this.entries(cx));
|
|
||||||
let selected_index = self.selected_index;
|
let selected_index = self.selected_index;
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.id("thread-history-container")
|
|
||||||
.key_context("ThreadHistory")
|
.key_context("ThreadHistory")
|
||||||
.track_focus(&self.focus_handle)
|
|
||||||
.overflow_y_scroll()
|
|
||||||
.size_full()
|
.size_full()
|
||||||
.p_1()
|
|
||||||
.on_action(cx.listener(Self::select_previous))
|
.on_action(cx.listener(Self::select_previous))
|
||||||
.on_action(cx.listener(Self::select_next))
|
.on_action(cx.listener(Self::select_next))
|
||||||
.on_action(cx.listener(Self::select_first))
|
.on_action(cx.listener(Self::select_first))
|
||||||
.on_action(cx.listener(Self::select_last))
|
.on_action(cx.listener(Self::select_last))
|
||||||
.on_action(cx.listener(Self::confirm))
|
.on_action(cx.listener(Self::confirm))
|
||||||
.on_action(cx.listener(Self::remove_selected_thread))
|
.on_action(cx.listener(Self::remove_selected_thread))
|
||||||
.map(|history| {
|
.when(!self.all_entries.is_empty(), |parent| {
|
||||||
if history_entries.is_empty() {
|
parent.child(
|
||||||
history
|
h_flex()
|
||||||
.justify_center()
|
.h(px(41.)) // Match the toolbar perfectly
|
||||||
|
.w_full()
|
||||||
|
.py_1()
|
||||||
|
.px_2()
|
||||||
|
.gap_2()
|
||||||
|
.justify_between()
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(cx.theme().colors().border)
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::MagnifyingGlass)
|
||||||
|
.color(Color::Muted)
|
||||||
|
.size(IconSize::Small),
|
||||||
|
)
|
||||||
|
.child(self.search_editor.clone()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.child({
|
||||||
|
let view = v_flex().overflow_hidden().flex_grow();
|
||||||
|
|
||||||
|
if self.all_entries.is_empty() {
|
||||||
|
view.justify_center()
|
||||||
.child(
|
.child(
|
||||||
h_flex().w_full().justify_center().child(
|
h_flex().w_full().justify_center().child(
|
||||||
Label::new("You don't have any past threads yet.")
|
Label::new("You don't have any past threads yet.")
|
||||||
.size(LabelSize::Small),
|
.size(LabelSize::Small),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
} else if self.has_search_query() && self.matches.is_empty() {
|
||||||
|
view.justify_center().child(
|
||||||
|
h_flex().w_full().justify_center().child(
|
||||||
|
Label::new("No threads match your search.").size(LabelSize::Small),
|
||||||
|
),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
history.child(
|
view.p_1().child(
|
||||||
uniform_list(
|
uniform_list(
|
||||||
cx.entity().clone(),
|
cx.entity().clone(),
|
||||||
"thread-history",
|
"thread-history",
|
||||||
history_entries.len(),
|
self.matched_count(),
|
||||||
move |history, range, _window, _cx| {
|
move |history, range, _window, _cx| {
|
||||||
history_entries[range]
|
let range_start = range.start;
|
||||||
.iter()
|
let assistant_panel = history.assistant_panel.clone();
|
||||||
.enumerate()
|
|
||||||
.map(|(index, entry)| {
|
let render_item = |index: usize,
|
||||||
|
entry: &HistoryEntry,
|
||||||
|
highlight_positions: Vec<usize>|
|
||||||
|
-> Div {
|
||||||
h_flex().w_full().pb_1().child(match entry {
|
h_flex().w_full().pb_1().child(match entry {
|
||||||
HistoryEntry::Thread(thread) => PastThread::new(
|
HistoryEntry::Thread(thread) => PastThread::new(
|
||||||
thread.clone(),
|
thread.clone(),
|
||||||
history.assistant_panel.clone(),
|
assistant_panel.clone(),
|
||||||
selected_index == index,
|
selected_index == index + range_start,
|
||||||
|
highlight_positions,
|
||||||
)
|
)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
HistoryEntry::Context(context) => PastContext::new(
|
HistoryEntry::Context(context) => PastContext::new(
|
||||||
context.clone(),
|
context.clone(),
|
||||||
history.assistant_panel.clone(),
|
assistant_panel.clone(),
|
||||||
selected_index == index,
|
selected_index == index + range_start,
|
||||||
|
highlight_positions,
|
||||||
)
|
)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
})
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
if history.has_search_query() {
|
||||||
|
history.matches[range]
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(index, m)| {
|
||||||
|
history.all_entries.get(m.candidate_id).map(|entry| {
|
||||||
|
render_item(index, entry, m.positions.clone())
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
} else {
|
||||||
|
history.all_entries[range]
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, entry)| render_item(index, entry, vec![]))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.track_scroll(self.scroll_handle.clone())
|
.track_scroll(self.scroll_handle.clone())
|
||||||
|
@ -224,6 +390,7 @@ pub struct PastThread {
|
||||||
thread: SerializedThreadMetadata,
|
thread: SerializedThreadMetadata,
|
||||||
assistant_panel: WeakEntity<AssistantPanel>,
|
assistant_panel: WeakEntity<AssistantPanel>,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
|
highlight_positions: Vec<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PastThread {
|
impl PastThread {
|
||||||
|
@ -231,11 +398,13 @@ impl PastThread {
|
||||||
thread: SerializedThreadMetadata,
|
thread: SerializedThreadMetadata,
|
||||||
assistant_panel: WeakEntity<AssistantPanel>,
|
assistant_panel: WeakEntity<AssistantPanel>,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
|
highlight_positions: Vec<usize>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
thread,
|
thread,
|
||||||
assistant_panel,
|
assistant_panel,
|
||||||
selected,
|
selected,
|
||||||
|
highlight_positions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -258,9 +427,11 @@ impl RenderOnce for PastThread {
|
||||||
.toggle_state(self.selected)
|
.toggle_state(self.selected)
|
||||||
.spacing(ListItemSpacing::Sparse)
|
.spacing(ListItemSpacing::Sparse)
|
||||||
.start_slot(
|
.start_slot(
|
||||||
div()
|
div().max_w_4_5().child(
|
||||||
.max_w_4_5()
|
HighlightedLabel::new(summary, self.highlight_positions)
|
||||||
.child(Label::new(summary).size(LabelSize::Small).truncate()),
|
.size(LabelSize::Small)
|
||||||
|
.truncate(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.end_slot(
|
.end_slot(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
@ -318,6 +489,7 @@ pub struct PastContext {
|
||||||
context: SavedContextMetadata,
|
context: SavedContextMetadata,
|
||||||
assistant_panel: WeakEntity<AssistantPanel>,
|
assistant_panel: WeakEntity<AssistantPanel>,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
|
highlight_positions: Vec<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PastContext {
|
impl PastContext {
|
||||||
|
@ -325,11 +497,13 @@ impl PastContext {
|
||||||
context: SavedContextMetadata,
|
context: SavedContextMetadata,
|
||||||
assistant_panel: WeakEntity<AssistantPanel>,
|
assistant_panel: WeakEntity<AssistantPanel>,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
|
highlight_positions: Vec<usize>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
context,
|
context,
|
||||||
assistant_panel,
|
assistant_panel,
|
||||||
selected,
|
selected,
|
||||||
|
highlight_positions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -337,7 +511,6 @@ impl PastContext {
|
||||||
impl RenderOnce for PastContext {
|
impl RenderOnce for PastContext {
|
||||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let summary = self.context.title;
|
let summary = self.context.title;
|
||||||
|
|
||||||
let context_timestamp = time_format::format_localized_timestamp(
|
let context_timestamp = time_format::format_localized_timestamp(
|
||||||
OffsetDateTime::from_unix_timestamp(self.context.mtime.timestamp()).unwrap(),
|
OffsetDateTime::from_unix_timestamp(self.context.mtime.timestamp()).unwrap(),
|
||||||
OffsetDateTime::now_utc(),
|
OffsetDateTime::now_utc(),
|
||||||
|
@ -354,9 +527,11 @@ impl RenderOnce for PastContext {
|
||||||
.toggle_state(self.selected)
|
.toggle_state(self.selected)
|
||||||
.spacing(ListItemSpacing::Sparse)
|
.spacing(ListItemSpacing::Sparse)
|
||||||
.start_slot(
|
.start_slot(
|
||||||
div()
|
div().max_w_4_5().child(
|
||||||
.max_w_4_5()
|
HighlightedLabel::new(summary, self.highlight_positions)
|
||||||
.child(Label::new(summary).size(LabelSize::Small).truncate()),
|
.size(LabelSize::Small)
|
||||||
|
.truncate(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.end_slot(
|
.end_slot(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue