assistant2: Combine history views into one (#25293)

This PR combines the two history views in Assistant2 into one.

<img width="1309" alt="Screenshot 2025-02-20 at 5 34 37 PM"
src="https://github.com/user-attachments/assets/fbb08542-58b5-4930-8a20-254234e335fa"
/>

<img width="1309" alt="Screenshot 2025-02-20 at 5 34 41 PM"
src="https://github.com/user-attachments/assets/1174849e-edad-4e02-8bf3-bb92aafba4f8"
/>


Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2025-02-20 17:53:58 -05:00 committed by GitHub
parent 3a222f0666
commit 74c581b9f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 265 additions and 155 deletions

View file

@ -7,6 +7,7 @@ mod context;
mod context_picker; mod context_picker;
mod context_store; mod context_store;
mod context_strip; mod context_strip;
mod history_store;
mod inline_assistant; mod inline_assistant;
mod inline_prompt_editor; mod inline_prompt_editor;
mod message_editor; mod message_editor;
@ -40,7 +41,6 @@ actions!(
ToggleModelSelector, ToggleModelSelector,
RemoveAllContext, RemoveAllContext,
OpenHistory, OpenHistory,
OpenPromptEditorHistory,
OpenConfiguration, OpenConfiguration,
RemoveSelectedThread, RemoveSelectedThread,
Chat, Chat,

View file

@ -4,7 +4,7 @@ use std::sync::Arc;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use assistant_context_editor::{ use assistant_context_editor::{
make_lsp_adapter_delegate, render_remaining_tokens, AssistantPanelDelegate, ConfigurationError, make_lsp_adapter_delegate, render_remaining_tokens, AssistantPanelDelegate, ConfigurationError,
ContextEditor, ContextHistory, SlashCommandCompletionProvider, ContextEditor, SlashCommandCompletionProvider,
}; };
use assistant_settings::{AssistantDockPosition, AssistantSettings}; use assistant_settings::{AssistantDockPosition, AssistantSettings};
use assistant_slash_command::SlashCommandWorkingSet; use assistant_slash_command::SlashCommandWorkingSet;
@ -31,14 +31,12 @@ use zed_actions::assistant::{DeployPromptLibrary, ToggleFocus};
use crate::active_thread::ActiveThread; use crate::active_thread::ActiveThread;
use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent}; use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
use crate::history_store::{HistoryEntry, HistoryStore};
use crate::message_editor::MessageEditor; use crate::message_editor::MessageEditor;
use crate::thread::{Thread, ThreadError, ThreadId}; 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::thread_store::ThreadStore;
use crate::{ use crate::{InlineAssistant, NewPromptEditor, NewThread, OpenConfiguration, OpenHistory};
InlineAssistant, NewPromptEditor, NewThread, OpenConfiguration, OpenHistory,
OpenPromptEditorHistory,
};
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
cx.observe_new( cx.observe_new(
@ -62,12 +60,6 @@ pub fn init(cx: &mut App) {
panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx)); 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| { .register_action(|workspace, _: &OpenConfiguration, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) { if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx); workspace.focus_panel::<AssistantPanel>(window, cx);
@ -83,7 +75,6 @@ enum ActiveView {
Thread, Thread,
PromptEditor, PromptEditor,
History, History,
PromptEditorHistory,
Configuration, Configuration,
} }
@ -97,15 +88,14 @@ pub struct AssistantPanel {
message_editor: Entity<MessageEditor>, message_editor: Entity<MessageEditor>,
context_store: Entity<assistant_context_editor::ContextStore>, context_store: Entity<assistant_context_editor::ContextStore>,
context_editor: Option<Entity<ContextEditor>>, context_editor: Option<Entity<ContextEditor>>,
context_history: Option<Entity<ContextHistory>>,
configuration: Option<Entity<AssistantConfiguration>>, configuration: Option<Entity<AssistantConfiguration>>,
configuration_subscription: Option<Subscription>, configuration_subscription: Option<Subscription>,
tools: Arc<ToolWorkingSet>, tools: Arc<ToolWorkingSet>,
local_timezone: UtcOffset, local_timezone: UtcOffset,
active_view: ActiveView, active_view: ActiveView,
history_store: Entity<HistoryStore>,
history: Entity<ThreadHistory>, history: Entity<ThreadHistory>,
new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>, new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
open_history_context_menu_handle: PopoverMenuHandle<ContextMenu>,
width: Option<Pixels>, width: Option<Pixels>,
height: 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 { Self {
active_view: ActiveView::Thread, active_view: ActiveView::Thread,
workspace: workspace.clone(), workspace: workspace.clone(),
@ -194,7 +187,6 @@ impl AssistantPanel {
message_editor, message_editor,
context_store, context_store,
context_editor: None, context_editor: None,
context_history: None,
configuration: None, configuration: None,
configuration_subscription: None, configuration_subscription: None,
tools, tools,
@ -202,9 +194,9 @@ impl AssistantPanel {
chrono::Local::now().offset().local_minus_utc(), chrono::Local::now().offset().local_minus_utc(),
) )
.unwrap(), .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(), new_item_context_menu_handle: PopoverMenuHandle::default(),
open_history_context_menu_handle: PopoverMenuHandle::default(),
width: None, width: None,
height: None, height: None,
} }
@ -331,26 +323,7 @@ impl AssistantPanel {
cx.notify(); cx.notify();
} }
fn open_prompt_editor_history(&mut self, window: &mut Window, cx: &mut Context<Self>) { pub(crate) fn open_saved_prompt_editor(
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(
&mut self, &mut self,
path: PathBuf, path: PathBuf,
window: &mut Window, window: &mut Window,
@ -499,13 +472,6 @@ impl Focusable for AssistantPanel {
cx.focus_handle() 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 => { ActiveView::Configuration => {
if let Some(configuration) = self.configuration.as_ref() { if let Some(configuration) = self.configuration.as_ref() {
configuration.focus_handle(cx) configuration.focus_handle(cx)
@ -618,18 +584,10 @@ impl AssistantPanel {
SharedString::from(context_editor.read(cx).title(cx).to_string()) SharedString::from(context_editor.read(cx).title(cx).to_string())
}) })
.unwrap_or_else(|| SharedString::from("Loading Summary…")), .unwrap_or_else(|| SharedString::from("Loading Summary…")),
ActiveView::History | ActiveView::PromptEditorHistory => "History".into(), ActiveView::History => "History".into(),
ActiveView::Configuration => "Assistant Settings".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() h_flex()
.id("assistant-toolbar") .id("assistant-toolbar")
.px(DynamicSpacing::Base08.rems(cx)) .px(DynamicSpacing::Base08.rems(cx))
@ -645,24 +603,7 @@ impl AssistantPanel {
.w_full() .w_full()
.gap_1() .gap_1()
.justify_between() .justify_between()
.child(
h_flex()
.child(Label::new(title)) .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())),
)
}),
)
.children(if matches!(self.active_view, ActiveView::PromptEditor) { .children(if matches!(self.active_view, ActiveView::PromptEditor) {
self.context_editor self.context_editor
.as_ref() .as_ref()
@ -696,23 +637,23 @@ impl AssistantPanel {
}), }),
) )
.child( .child(
PopoverMenu::new("assistant-toolbar-history-popover-menu")
.trigger_with_tooltip(
IconButton::new("open-history", IconName::HistoryRerun) IconButton::new("open-history", IconName::HistoryRerun)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.style(ButtonStyle::Subtle), .style(ButtonStyle::Subtle)
Tooltip::text("History…"), .tooltip({
let focus_handle = self.focus_handle(cx);
move |window, cx| {
Tooltip::for_action_in(
"History",
&OpenHistory,
&focus_handle,
window,
cx,
) )
.anchor(Corner::TopRight) }
.with_handle(self.open_history_context_menu_handle.clone()) })
.menu(move |window, cx| { .on_click(move |_event, window, cx| {
Some(ContextMenu::build(window, cx, |menu, _window, _cx| { window.dispatch_action(OpenHistory.boxed_clone(), cx);
menu.action("Thread History", OpenHistory.boxed_clone())
.action(
"Prompt Editor History",
OpenPromptEditorHistory.boxed_clone(),
)
}))
}), }),
) )
.child( .child(
@ -762,9 +703,9 @@ impl AssistantPanel {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> impl IntoElement { ) -> impl IntoElement {
let recent_threads = self let recent_history = self
.thread_store .history_store
.update(cx, |this, _cx| this.recent_threads(3)); .update(cx, |this, cx| this.recent_entries(3, cx));
let create_welcome_heading = || { let create_welcome_heading = || {
h_flex() h_flex()
@ -791,7 +732,8 @@ impl AssistantPanel {
) )
.map(|parent| { .map(|parent| {
match configuration_error { match configuration_error {
Some(ConfigurationError::ProviderNotAuthenticated) | Some(ConfigurationError::NoProvider) => { Some(ConfigurationError::ProviderNotAuthenticated)
| Some(ConfigurationError::NoProvider) => {
parent.child( parent.child(
v_flex() v_flex()
.gap_0p5() .gap_0p5()
@ -818,34 +760,24 @@ impl AssistantPanel {
), ),
) )
} }
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => { Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => parent
parent.child( .child(v_flex().gap_0p5().child(create_welcome_heading()).children(
v_flex() provider.render_accept_terms(
.gap_0p5()
.child(create_welcome_heading())
.children(provider.render_accept_terms(
LanguageModelProviderTosView::ThreadEmptyState, LanguageModelProviderTosView::ThreadEmptyState,
cx, cx,
),
)), )),
)
}
None => parent, None => parent,
} }
}) })
.when( .when(recent_history.is_empty() && no_error, |parent| {
recent_threads.is_empty() && no_error, parent.child(v_flex().gap_0p5().child(create_welcome_heading()).child(
|parent| {
parent.child(
v_flex().gap_0p5().child(create_welcome_heading()).child(
h_flex().w_full().justify_center().child( h_flex().w_full().justify_center().child(
Label::new("Start typing to chat with your codebase") Label::new("Start typing to chat with your codebase").color(Color::Muted),
.color(Color::Muted),
), ),
), ))
) })
}, .when(!recent_history.is_empty(), |parent| {
)
.when(!recent_threads.is_empty(), |parent| {
parent parent
.child( .child(
h_flex().w_full().justify_center().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( .child(v_flex().mx_auto().w_4_5().gap_2().children(
recent_threads.into_iter().map(|thread| { recent_history.into_iter().map(|entry| {
// TODO: keyboard navigation // TODO: Add keyboard navigation.
match entry {
HistoryEntry::Thread(thread) => {
PastThread::new(thread, cx.entity().downgrade(), false) PastThread::new(thread, cx.entity().downgrade(), false)
.into_any_element()
}
HistoryEntry::Context(context) => {
PastContext::new(context, cx.entity().downgrade(), false)
.into_any_element()
}
}
}), }),
)) ))
.child( .child(
@ -869,7 +810,7 @@ impl AssistantPanel {
&OpenHistory, &OpenHistory,
&self.focus_handle(cx), &self.focus_handle(cx),
window, window,
cx cx,
)) ))
.on_click(move |_event, window, cx| { .on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx); window.dispatch_action(OpenHistory.boxed_clone(), cx);
@ -1068,7 +1009,6 @@ impl Render for AssistantPanel {
.children(self.render_last_error(cx)), .children(self.render_last_error(cx)),
ActiveView::History => parent.child(self.history.clone()), ActiveView::History => parent.child(self.history.clone()),
ActiveView::PromptEditor => parent.children(self.context_editor.clone()), ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
ActiveView::PromptEditorHistory => parent.children(self.context_history.clone()),
ActiveView::Configuration => parent.children(self.configuration.clone()), ActiveView::Configuration => parent.children(self.configuration.clone()),
}) })
} }

View file

@ -0,0 +1,61 @@
use assistant_context_editor::SavedContextMetadata;
use chrono::{DateTime, Utc};
use gpui::{prelude::*, Entity};
use crate::thread_store::{SavedThreadMetadata, ThreadStore};
pub enum HistoryEntry {
Thread(SavedThreadMetadata),
Context(SavedContextMetadata),
}
impl HistoryEntry {
pub fn updated_at(&self) -> DateTime<Utc> {
match self {
HistoryEntry::Thread(thread) => thread.updated_at,
HistoryEntry::Context(context) => context.mtime.to_utc(),
}
}
}
pub struct HistoryStore {
thread_store: Entity<ThreadStore>,
context_store: Entity<assistant_context_editor::ContextStore>,
}
impl HistoryStore {
pub fn new(
thread_store: Entity<ThreadStore>,
context_store: Entity<assistant_context_editor::ContextStore>,
_cx: &mut Context<Self>,
) -> Self {
Self {
thread_store,
context_store,
}
}
/// 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> {
let mut history_entries = Vec::new();
for thread in self.thread_store.update(cx, |this, _cx| this.threads()) {
history_entries.push(HistoryEntry::Thread(thread));
}
for context in self.context_store.update(cx, |this, _cx| this.contexts()) {
history_entries.push(HistoryEntry::Context(context));
}
history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
history_entries
}
pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
self.entries(cx).into_iter().take(limit).collect()
}
}

View file

@ -1,3 +1,4 @@
use assistant_context_editor::SavedContextMetadata;
use gpui::{ use gpui::{
uniform_list, App, Entity, FocusHandle, Focusable, ScrollStrategy, UniformListScrollHandle, uniform_list, App, Entity, FocusHandle, Focusable, ScrollStrategy, UniformListScrollHandle,
WeakEntity, WeakEntity,
@ -5,13 +6,14 @@ use gpui::{
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_store::{SavedThreadMetadata, ThreadStore}; use crate::history_store::{HistoryEntry, HistoryStore};
use crate::thread_store::SavedThreadMetadata;
use crate::{AssistantPanel, RemoveSelectedThread}; use crate::{AssistantPanel, RemoveSelectedThread};
pub struct ThreadHistory { pub struct ThreadHistory {
focus_handle: FocusHandle, focus_handle: FocusHandle,
assistant_panel: WeakEntity<AssistantPanel>, assistant_panel: WeakEntity<AssistantPanel>,
thread_store: Entity<ThreadStore>, history_store: Entity<HistoryStore>,
scroll_handle: UniformListScrollHandle, scroll_handle: UniformListScrollHandle,
selected_index: usize, selected_index: usize,
} }
@ -19,13 +21,13 @@ pub struct ThreadHistory {
impl ThreadHistory { impl ThreadHistory {
pub(crate) fn new( pub(crate) fn new(
assistant_panel: WeakEntity<AssistantPanel>, assistant_panel: WeakEntity<AssistantPanel>,
thread_store: Entity<ThreadStore>, history_store: Entity<HistoryStore>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
Self { Self {
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
assistant_panel, assistant_panel,
thread_store, history_store,
scroll_handle: UniformListScrollHandle::default(), scroll_handle: UniformListScrollHandle::default(),
selected_index: 0, selected_index: 0,
} }
@ -37,7 +39,9 @@ impl ThreadHistory {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, 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 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, window, cx);
@ -53,7 +57,9 @@ impl ThreadHistory {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, 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 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, window, cx);
@ -64,14 +70,18 @@ impl ThreadHistory {
} }
fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context<Self>) { 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 { if count > 0 {
self.set_selected_index(0, window, cx); self.set_selected_index(0, window, 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.thread_store.read(cx).thread_count(); let count = self
.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, window, cx);
} }
@ -85,12 +95,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 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) { if let Some(entry) = entries.get(self.selected_index) {
match entry {
HistoryEntry::Thread(thread) => {
self.assistant_panel 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.assistant_panel
.update(cx, move |this, cx| {
this.open_saved_prompt_editor(context.path.clone(), window, cx)
})
.ok();
}
}
cx.notify(); cx.notify();
} }
@ -102,14 +123,19 @@ impl ThreadHistory {
_window: &mut Window, _window: &mut Window,
cx: &mut Context<Self>, 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) { if let Some(entry) = entries.get(self.selected_index) {
match entry {
HistoryEntry::Thread(thread) => {
self.assistant_panel self.assistant_panel
.update(cx, |this, cx| { .update(cx, |this, cx| {
this.delete_thread(&thread.id, cx); this.delete_thread(&thread.id, cx);
}) })
.ok(); .ok();
}
HistoryEntry::Context(_context) => {}
}
cx.notify(); cx.notify();
} }
@ -124,7 +150,7 @@ impl Focusable for ThreadHistory {
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 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; let selected_index = self.selected_index;
v_flex() v_flex()
@ -141,7 +167,7 @@ impl Render for ThreadHistory {
.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| { .map(|history| {
if threads.is_empty() { if history_entries.is_empty() {
history history
.justify_center() .justify_center()
.child( .child(
@ -155,17 +181,26 @@ impl Render for ThreadHistory {
uniform_list( uniform_list(
cx.entity().clone(), cx.entity().clone(),
"thread-history", "thread-history",
threads.len(), history_entries.len(),
move |history, range, _window, _cx| { move |history, range, _window, _cx| {
threads[range] history_entries[range]
.iter() .iter()
.enumerate() .enumerate()
.map(|(index, thread)| { .map(|(index, entry)| {
h_flex().w_full().pb_1().child(PastThread::new( h_flex().w_full().pb_1().child(match entry {
HistoryEntry::Thread(thread) => PastThread::new(
thread.clone(), thread.clone(),
history.assistant_panel.clone(), history.assistant_panel.clone(),
selected_index == index, selected_index == index,
)) )
.into_any_element(),
HistoryEntry::Context(context) => PastContext::new(
context.clone(),
history.assistant_panel.clone(),
selected_index == index,
)
.into_any_element(),
})
}) })
.collect() .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();
}
})
}
}

View file

@ -3366,7 +3366,7 @@ impl SavedContextV0_1_0 {
} }
} }
#[derive(Clone)] #[derive(Debug, Clone)]
pub struct SavedContextMetadata { pub struct SavedContextMetadata {
pub title: String, pub title: String,
pub path: PathBuf, pub path: PathBuf,

View file

@ -350,6 +350,12 @@ impl ContextStore {
} }
} }
pub fn contexts(&self) -> Vec<SavedContextMetadata> {
let mut contexts = self.contexts_metadata.iter().cloned().collect::<Vec<_>>();
contexts.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.mtime));
contexts
}
pub fn create(&mut self, cx: &mut Context<Self>) -> Entity<AssistantContext> { pub fn create(&mut self, cx: &mut Context<Self>) -> Entity<AssistantContext> {
let context = cx.new(|cx| { let context = cx.new(|cx| {
AssistantContext::local( AssistantContext::local(