assistant2: Combine history views into one (#25293)

Marshall Bowers created

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

Change summary

crates/assistant2/src/assistant.rs                   |   2 
crates/assistant2/src/assistant_panel.rs             | 186 ++++---------
crates/assistant2/src/history_store.rs               |  61 ++++
crates/assistant2/src/thread_history.rs              | 163 ++++++++++--
crates/assistant_context_editor/src/context.rs       |   2 
crates/assistant_context_editor/src/context_store.rs |   6 
6 files changed, 265 insertions(+), 155 deletions(-)

Detailed changes

crates/assistant2/src/assistant.rs 🔗

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

crates/assistant2/src/assistant_panel.rs 🔗

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

crates/assistant2/src/history_store.rs 🔗

@@ -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()
+    }
+}

crates/assistant2/src/thread_history.rs 🔗

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

crates/assistant_context_editor/src/context_store.rs 🔗

@@ -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> {
         let context = cx.new(|cx| {
             AssistantContext::local(