Allow attaching text threads as context (#29947)

Max Brunsfeld and Michael Sloan created

Release Notes:

- N/A

---------

Co-authored-by: Michael Sloan <mgsloan@gmail.com>

Change summary

Cargo.lock                                                    |   3 
crates/agent/Cargo.toml                                       |   1 
crates/agent/src/active_thread.rs                             |  49 +
crates/agent/src/assistant.rs                                 |   2 
crates/agent/src/assistant_panel.rs                           |  90 +-
crates/agent/src/context.rs                                   |  78 ++
crates/agent/src/context_picker.rs                            | 147 +++-
crates/agent/src/context_picker/completion_provider.rs        |  91 +
crates/agent/src/context_picker/thread_context_picker.rs      | 174 +++-
crates/agent/src/context_store.rs                             |  58 +
crates/agent/src/context_strip.rs                             |  63 +
crates/agent/src/history_store.rs                             |   5 
crates/agent/src/inline_assistant.rs                          |  22 
crates/agent/src/inline_prompt_editor.rs                      |   8 
crates/agent/src/message_editor.rs                            |  19 
crates/agent/src/terminal_inline_assistant.rs                 |   4 
crates/agent/src/thread_store.rs                              |   6 
crates/agent/src/ui/context_pill.rs                           |  42 +
crates/agent/src/ui/preview/agent_preview.rs                  |  15 
crates/assistant_context_editor/src/context.rs                |  22 
crates/assistant_context_editor/src/context_store.rs          |   6 
crates/component_preview/Cargo.toml                           |   2 
crates/component_preview/src/component_preview.rs             |  72 +
crates/component_preview/src/preview_support/active_thread.rs |  50 
24 files changed, 784 insertions(+), 245 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -125,6 +125,7 @@ dependencies = [
  "time_format",
  "ui",
  "ui_input",
+ "urlencoding",
  "util",
  "uuid",
  "workspace",
@@ -3251,6 +3252,7 @@ dependencies = [
  "collections",
  "component",
  "db",
+ "futures 0.3.31",
  "gpui",
  "languages",
  "log",
@@ -3260,6 +3262,7 @@ dependencies = [
  "serde",
  "ui",
  "ui_input",
+ "util",
  "workspace",
  "workspace-hack",
 ]

crates/agent/Cargo.toml 🔗

@@ -90,6 +90,7 @@ time.workspace = true
 time_format.workspace = true
 ui.workspace = true
 ui_input.workspace = true
+urlencoding.workspace = true
 util.workspace = true
 uuid.workspace = true
 workspace-hack.workspace = true

crates/agent/src/active_thread.rs 🔗

@@ -6,7 +6,7 @@ use crate::thread::{
     LastRestoreCheckpoint, MessageId, MessageSegment, Thread, ThreadError, ThreadEvent,
     ThreadFeedback,
 };
-use crate::thread_store::{RulesLoadingError, ThreadStore};
+use crate::thread_store::{RulesLoadingError, TextThreadStore, ThreadStore};
 use crate::tool_use::{PendingToolUseStatus, ToolUse};
 use crate::ui::{
     AddedContext, AgentNotification, AgentNotificationEvent, AnimatedLabel, ContextPill,
@@ -56,6 +56,7 @@ pub struct ActiveThread {
     context_store: Entity<ContextStore>,
     language_registry: Arc<LanguageRegistry>,
     thread_store: Entity<ThreadStore>,
+    text_thread_store: Entity<TextThreadStore>,
     thread: Entity<Thread>,
     workspace: WeakEntity<Workspace>,
     save_thread_task: Option<Task<()>>,
@@ -719,6 +720,15 @@ fn open_markdown_link(
                 });
             }
         }),
+        Some(MentionLink::TextThread(path)) => workspace.update(cx, |workspace, cx| {
+            if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
+                panel.update(cx, |panel, cx| {
+                    panel
+                        .open_saved_prompt_editor(path, window, cx)
+                        .detach_and_log_err(cx);
+                });
+            }
+        }),
         Some(MentionLink::Fetch(url)) => cx.open_url(&url),
         Some(MentionLink::Rule(prompt_id)) => window.dispatch_action(
             Box::new(OpenRulesLibrary {
@@ -743,6 +753,7 @@ impl ActiveThread {
     pub fn new(
         thread: Entity<Thread>,
         thread_store: Entity<ThreadStore>,
+        text_thread_store: Entity<TextThreadStore>,
         context_store: Entity<ContextStore>,
         language_registry: Arc<LanguageRegistry>,
         workspace: WeakEntity<Workspace>,
@@ -765,6 +776,7 @@ impl ActiveThread {
         let mut this = Self {
             language_registry,
             thread_store,
+            text_thread_store,
             context_store,
             thread: thread.clone(),
             workspace,
@@ -844,6 +856,14 @@ impl ActiveThread {
             .map(|(id, state)| (*id, state.last_estimated_token_count.unwrap_or(0)))
     }
 
+    pub fn thread_store(&self) -> &Entity<ThreadStore> {
+        &self.thread_store
+    }
+
+    pub fn text_thread_store(&self) -> &Entity<TextThreadStore> {
+        &self.text_thread_store
+    }
+
     fn push_message(
         &mut self,
         id: &MessageId,
@@ -1264,6 +1284,7 @@ impl ActiveThread {
             self.workspace.clone(),
             self.context_store.downgrade(),
             self.thread_store.downgrade(),
+            self.text_thread_store.downgrade(),
             window,
             cx,
         );
@@ -1285,6 +1306,7 @@ impl ActiveThread {
                 self.context_store.clone(),
                 self.workspace.clone(),
                 Some(self.thread_store.downgrade()),
+                Some(self.text_thread_store.downgrade()),
                 context_picker_menu_handle.clone(),
                 SuggestContextKind::File,
                 window,
@@ -3439,14 +3461,21 @@ pub(crate) fn open_context(
         AgentContextHandle::Thread(thread_context) => workspace.update(cx, |workspace, cx| {
             if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
                 panel.update(cx, |panel, cx| {
-                    let thread_id = thread_context.thread.read(cx).id().clone();
-                    panel
-                        .open_thread_by_id(&thread_id, window, cx)
-                        .detach_and_log_err(cx)
+                    panel.open_thread(thread_context.thread.clone(), window, cx);
                 });
             }
         }),
 
+        AgentContextHandle::TextThread(text_thread_context) => {
+            workspace.update(cx, |workspace, cx| {
+                if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
+                    panel.update(cx, |panel, cx| {
+                        panel.open_prompt_editor(text_thread_context.context.clone(), window, cx)
+                    });
+                }
+            })
+        }
+
         AgentContextHandle::Rules(rules_context) => window.dispatch_action(
             Box::new(OpenRulesLibrary {
                 prompt_to_select: Some(rules_context.prompt_id.0),
@@ -3585,18 +3614,25 @@ mod tests {
         let (workspace, cx) =
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
+        let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
         let thread_store = cx
             .update(|_, cx| {
                 ThreadStore::load(
                     project.clone(),
                     cx.new(|_| ToolWorkingSet::default()),
                     None,
-                    Arc::new(PromptBuilder::new(None).unwrap()),
+                    prompt_builder.clone(),
                     cx,
                 )
             })
             .await
             .unwrap();
+        let text_thread_store = cx
+            .update(|_, cx| {
+                TextThreadStore::new(project.clone(), prompt_builder, Default::default(), cx)
+            })
+            .await
+            .unwrap();
 
         let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
         let context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), None));
@@ -3612,6 +3648,7 @@ mod tests {
                 ActiveThread::new(
                     thread.clone(),
                     thread_store.clone(),
+                    text_thread_store.clone(),
                     context_store.clone(),
                     language_registry.clone(),
                     workspace.downgrade(),

crates/agent/src/assistant.rs 🔗

@@ -46,7 +46,7 @@ pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate}
 pub use crate::context::{ContextLoadResult, LoadedContext};
 pub use crate::inline_assistant::InlineAssistant;
 pub use crate::thread::{Message, MessageSegment, Thread, ThreadEvent};
-pub use crate::thread_store::ThreadStore;
+pub use crate::thread_store::{TextThreadStore, ThreadStore};
 pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
 pub use context_store::ContextStore;
 pub use ui::preview::{all_agent_previews, get_agent_preview};

crates/agent/src/assistant_panel.rs 🔗

@@ -52,7 +52,7 @@ use crate::history_store::{HistoryEntry, HistoryStore, RecentEntry};
 use crate::message_editor::{MessageEditor, MessageEditorEvent};
 use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
 use crate::thread_history::{PastContext, PastThread, ThreadHistory};
-use crate::thread_store::ThreadStore;
+use crate::thread_store::{TextThreadStore, ThreadStore};
 use crate::{
     AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow,
     InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
@@ -313,7 +313,7 @@ pub struct AssistantPanel {
     message_editor: Entity<MessageEditor>,
     _active_thread_subscriptions: Vec<Subscription>,
     _default_model_subscription: Subscription,
-    context_store: Entity<assistant_context_editor::ContextStore>,
+    context_store: Entity<TextThreadStore>,
     prompt_store: Option<Entity<PromptStore>>,
     configuration: Option<Entity<AssistantConfiguration>>,
     configuration_subscription: Option<Subscription>,
@@ -419,7 +419,7 @@ impl AssistantPanel {
     fn new(
         workspace: &Workspace,
         thread_store: Entity<ThreadStore>,
-        context_store: Entity<assistant_context_editor::ContextStore>,
+        context_store: Entity<TextThreadStore>,
         prompt_store: Option<Entity<PromptStore>>,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -447,6 +447,7 @@ impl AssistantPanel {
                 message_editor_context_store.clone(),
                 prompt_store.clone(),
                 thread_store.downgrade(),
+                context_store.downgrade(),
                 thread.clone(),
                 window,
                 cx,
@@ -483,6 +484,7 @@ impl AssistantPanel {
             ActiveThread::new(
                 thread.clone(),
                 thread_store.clone(),
+                context_store.clone(),
                 message_editor_context_store.clone(),
                 language_registry.clone(),
                 workspace.clone(),
@@ -676,6 +678,10 @@ impl AssistantPanel {
         &self.thread_store
     }
 
+    pub(crate) fn text_thread_store(&self) -> &Entity<TextThreadStore> {
+        &self.context_store
+    }
+
     fn cancel(&mut self, _: &editor::actions::Cancel, window: &mut Window, cx: &mut Context<Self>) {
         self.thread
             .update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
@@ -727,6 +733,7 @@ impl AssistantPanel {
             ActiveThread::new(
                 thread.clone(),
                 self.thread_store.clone(),
+                self.context_store.clone(),
                 context_store.clone(),
                 self.language_registry.clone(),
                 self.workspace.clone(),
@@ -751,6 +758,7 @@ impl AssistantPanel {
                 context_store,
                 self.prompt_store.clone(),
                 self.thread_store.downgrade(),
+                self.context_store.downgrade(),
                 thread,
                 window,
                 cx,
@@ -854,44 +862,41 @@ impl AssistantPanel {
         let context = self
             .context_store
             .update(cx, |store, cx| store.open_local_context(path, cx));
-        let fs = self.fs.clone();
-        let project = self.project.clone();
-        let workspace = self.workspace.clone();
-
-        let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten();
-
         cx.spawn_in(window, async move |this, cx| {
             let context = context.await?;
             this.update_in(cx, |this, window, cx| {
-                let editor = cx.new(|cx| {
-                    ContextEditor::for_context(
-                        context,
-                        fs,
-                        workspace,
-                        project,
-                        lsp_adapter_delegate,
-                        window,
-                        cx,
-                    )
-                });
-
-                this.set_active_view(
-                    ActiveView::prompt_editor(
-                        editor.clone(),
-                        this.language_registry.clone(),
-                        window,
-                        cx,
-                    ),
-                    window,
-                    cx,
-                );
-
-                anyhow::Ok(())
-            })??;
-            Ok(())
+                this.open_prompt_editor(context, window, cx);
+            })
         })
     }
 
+    pub(crate) fn open_prompt_editor(
+        &mut self,
+        context: Entity<AssistantContext>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project.clone(), cx)
+            .log_err()
+            .flatten();
+        let editor = cx.new(|cx| {
+            ContextEditor::for_context(
+                context,
+                self.fs.clone(),
+                self.workspace.clone(),
+                self.project.clone(),
+                lsp_adapter_delegate,
+                window,
+                cx,
+            )
+        });
+        self.set_active_view(
+            ActiveView::prompt_editor(editor.clone(), self.language_registry.clone(), window, cx),
+            window,
+            cx,
+        );
+    }
+
     pub(crate) fn open_thread_by_id(
         &mut self,
         thread_id: &ThreadId,
@@ -936,6 +941,7 @@ impl AssistantPanel {
             ActiveThread::new(
                 thread.clone(),
                 self.thread_store.clone(),
+                self.context_store.clone(),
                 context_store.clone(),
                 self.language_registry.clone(),
                 self.workspace.clone(),
@@ -960,6 +966,7 @@ impl AssistantPanel {
                 context_store,
                 self.prompt_store.clone(),
                 self.thread_store.downgrade(),
+                self.context_store.downgrade(),
                 thread,
                 window,
                 cx,
@@ -1067,7 +1074,9 @@ impl AssistantPanel {
             .app_state()
             .languages
             .language_for_name("Markdown");
-        let thread = self.active_thread(cx);
+        let Some(thread) = self.active_thread() else {
+            return;
+        };
         cx.spawn_in(window, async move |_this, cx| {
             let markdown_language = markdown_language_task.await?;
 
@@ -1133,8 +1142,11 @@ impl AssistantPanel {
         }
     }
 
-    pub(crate) fn active_thread(&self, cx: &App) -> Entity<Thread> {
-        self.thread.read(cx).thread().clone()
+    pub(crate) fn active_thread(&self) -> Option<Entity<Thread>> {
+        match &self.active_view {
+            ActiveView::Thread { thread, .. } => thread.upgrade(),
+            _ => None,
+        }
     }
 
     pub(crate) fn delete_thread(
@@ -2423,12 +2435,14 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
             };
             let prompt_store = None;
             let thread_store = None;
+            let text_thread_store = None;
             assistant.assist(
                 &prompt_editor,
                 self.workspace.clone(),
                 project,
                 prompt_store,
                 thread_store,
+                text_thread_store,
                 window,
                 cx,
             )

crates/agent/src/context.rs 🔗

@@ -3,6 +3,7 @@ use std::hash::{Hash, Hasher};
 use std::path::PathBuf;
 use std::{ops::Range, path::Path, sync::Arc};
 
+use assistant_context_editor::AssistantContext;
 use assistant_tool::outline;
 use collections::{HashMap, HashSet};
 use editor::display_map::CreaseId;
@@ -33,6 +34,7 @@ pub enum ContextKind {
     Selection,
     FetchedUrl,
     Thread,
+    TextThread,
     Rules,
     Image,
 }
@@ -46,6 +48,7 @@ impl ContextKind {
             ContextKind::Selection => IconName::Context,
             ContextKind::FetchedUrl => IconName::Globe,
             ContextKind::Thread => IconName::MessageBubbles,
+            ContextKind::TextThread => IconName::MessageBubbles,
             ContextKind::Rules => RULES_ICON,
             ContextKind::Image => IconName::Image,
         }
@@ -65,6 +68,7 @@ pub enum AgentContextHandle {
     Selection(SelectionContextHandle),
     FetchedUrl(FetchedUrlContext),
     Thread(ThreadContextHandle),
+    TextThread(TextThreadContextHandle),
     Rules(RulesContextHandle),
     Image(ImageContext),
 }
@@ -78,6 +82,7 @@ impl AgentContextHandle {
             Self::Selection(context) => context.context_id,
             Self::FetchedUrl(context) => context.context_id,
             Self::Thread(context) => context.context_id,
+            Self::TextThread(context) => context.context_id,
             Self::Rules(context) => context.context_id,
             Self::Image(context) => context.context_id,
         }
@@ -98,6 +103,7 @@ pub enum AgentContext {
     Selection(SelectionContext),
     FetchedUrl(FetchedUrlContext),
     Thread(ThreadContext),
+    TextThread(TextThreadContext),
     Rules(RulesContext),
     Image(ImageContext),
 }
@@ -115,6 +121,9 @@ impl AgentContext {
             }
             AgentContext::FetchedUrl(context) => AgentContextHandle::FetchedUrl(context.clone()),
             AgentContext::Thread(context) => AgentContextHandle::Thread(context.handle.clone()),
+            AgentContext::TextThread(context) => {
+                AgentContextHandle::TextThread(context.handle.clone())
+            }
             AgentContext::Rules(context) => AgentContextHandle::Rules(context.handle.clone()),
             AgentContext::Image(context) => AgentContextHandle::Image(context.clone()),
         }
@@ -609,6 +618,54 @@ impl Display for ThreadContext {
     }
 }
 
+#[derive(Debug, Clone)]
+pub struct TextThreadContextHandle {
+    pub context: Entity<AssistantContext>,
+    pub context_id: ContextId,
+}
+
+#[derive(Debug, Clone)]
+pub struct TextThreadContext {
+    pub handle: TextThreadContextHandle,
+    pub title: SharedString,
+    pub text: SharedString,
+}
+
+impl TextThreadContextHandle {
+    // pub fn lookup_key() ->
+    pub fn eq_for_key(&self, other: &Self) -> bool {
+        self.context == other.context
+    }
+
+    pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
+        self.context.hash(state)
+    }
+
+    pub fn title(&self, cx: &App) -> SharedString {
+        self.context.read(cx).summary_or_default()
+    }
+
+    fn load(self, cx: &App) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> {
+        let title = self.title(cx);
+        let text = self.context.read(cx).to_xml(cx);
+        let context = AgentContext::TextThread(TextThreadContext {
+            title,
+            text: text.into(),
+            handle: self,
+        });
+        Task::ready(Some((context, vec![])))
+    }
+}
+
+impl Display for TextThreadContext {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        // TODO: escape title?
+        write!(f, "<text_thread title=\"{}\">\n", self.title)?;
+        write!(f, "{}", self.text.trim())?;
+        write!(f, "\n</text_thread>")
+    }
+}
+
 #[derive(Debug, Clone)]
 pub struct RulesContextHandle {
     pub prompt_id: UserPromptId,
@@ -785,6 +842,7 @@ pub fn load_context(
             AgentContextHandle::Selection(context) => load_tasks.push(context.load(cx)),
             AgentContextHandle::FetchedUrl(context) => load_tasks.push(context.load()),
             AgentContextHandle::Thread(context) => load_tasks.push(context.load(cx)),
+            AgentContextHandle::TextThread(context) => load_tasks.push(context.load(cx)),
             AgentContextHandle::Rules(context) => load_tasks.push(context.load(prompt_store, cx)),
             AgentContextHandle::Image(context) => load_tasks.push(context.load(cx)),
         }
@@ -810,6 +868,7 @@ pub fn load_context(
         let mut selection_context = Vec::new();
         let mut fetched_url_context = Vec::new();
         let mut thread_context = Vec::new();
+        let mut text_thread_context = Vec::new();
         let mut rules_context = Vec::new();
         let mut images = Vec::new();
         for context in &contexts {
@@ -820,17 +879,21 @@ pub fn load_context(
                 AgentContext::Selection(context) => selection_context.push(context),
                 AgentContext::FetchedUrl(context) => fetched_url_context.push(context),
                 AgentContext::Thread(context) => thread_context.push(context),
+                AgentContext::TextThread(context) => text_thread_context.push(context),
                 AgentContext::Rules(context) => rules_context.push(context),
                 AgentContext::Image(context) => images.extend(context.image()),
             }
         }
 
+        // Use empty text if there are no contexts that contribute to text (everything but image
+        // context).
         if file_context.is_empty()
             && directory_context.is_empty()
             && symbol_context.is_empty()
             && selection_context.is_empty()
             && fetched_url_context.is_empty()
             && thread_context.is_empty()
+            && text_thread_context.is_empty()
             && rules_context.is_empty()
         {
             return ContextLoadResult {
@@ -903,6 +966,15 @@ pub fn load_context(
             text.push_str("</conversation_threads>\n");
         }
 
+        if !text_thread_context.is_empty() {
+            text.push_str("<text_threads>");
+            for context in text_thread_context {
+                text.push('\n');
+                let _ = writeln!(text, "{context}");
+            }
+            text.push_str("<text_threads>");
+        }
+
         if !rules_context.is_empty() {
             text.push_str(
                 "<user_rules>\n\
@@ -1019,6 +1091,11 @@ impl PartialEq for AgentContextKey {
                     return context.eq_for_key(other_context);
                 }
             }
+            AgentContextHandle::TextThread(context) => {
+                if let AgentContextHandle::TextThread(other_context) = &other.0 {
+                    return context.eq_for_key(other_context);
+                }
+            }
         }
         false
     }
@@ -1033,6 +1110,7 @@ impl Hash for AgentContextKey {
             AgentContextHandle::Selection(context) => context.hash_for_key(state),
             AgentContextHandle::FetchedUrl(context) => context.hash_for_key(state),
             AgentContextHandle::Thread(context) => context.hash_for_key(state),
+            AgentContextHandle::TextThread(context) => context.hash_for_key(state),
             AgentContextHandle::Rules(context) => context.hash_for_key(state),
             AgentContextHandle::Image(context) => context.hash_for_key(state),
         }

crates/agent/src/context_picker.rs 🔗

@@ -6,7 +6,7 @@ mod symbol_context_picker;
 mod thread_context_picker;
 
 use std::ops::Range;
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
 use std::sync::Arc;
 
 use anyhow::{Result, anyhow};
@@ -22,11 +22,14 @@ use gpui::{
 };
 use language::Buffer;
 use multi_buffer::MultiBufferRow;
+use paths::contexts_dir;
 use project::{Entry, ProjectPath};
 use prompt_store::{PromptStore, UserPromptId};
 use rules_context_picker::{RulesContextEntry, RulesContextPicker};
 use symbol_context_picker::SymbolContextPicker;
-use thread_context_picker::{ThreadContextEntry, ThreadContextPicker, render_thread_context_entry};
+use thread_context_picker::{
+    ThreadContextEntry, ThreadContextPicker, render_thread_context_entry, unordered_thread_entries,
+};
 use ui::{
     ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
 };
@@ -37,7 +40,7 @@ use crate::AssistantPanel;
 use crate::context::RULES_ICON;
 use crate::context_store::ContextStore;
 use crate::thread::ThreadId;
-use crate::thread_store::ThreadStore;
+use crate::thread_store::{TextThreadStore, ThreadStore};
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 enum ContextPickerEntry {
@@ -164,6 +167,7 @@ pub(super) struct ContextPicker {
     workspace: WeakEntity<Workspace>,
     context_store: WeakEntity<ContextStore>,
     thread_store: Option<WeakEntity<ThreadStore>>,
+    text_thread_store: Option<WeakEntity<TextThreadStore>>,
     prompt_store: Option<Entity<PromptStore>>,
     _subscriptions: Vec<Subscription>,
 }
@@ -172,6 +176,7 @@ impl ContextPicker {
     pub fn new(
         workspace: WeakEntity<Workspace>,
         thread_store: Option<WeakEntity<ThreadStore>>,
+        text_thread_store: Option<WeakEntity<TextThreadStore>>,
         context_store: WeakEntity<ContextStore>,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -208,6 +213,7 @@ impl ContextPicker {
             workspace,
             context_store,
             thread_store,
+            text_thread_store,
             prompt_store,
             _subscriptions: subscriptions,
         }
@@ -340,10 +346,15 @@ impl ContextPicker {
                     }));
                 }
                 ContextPickerMode::Thread => {
-                    if let Some(thread_store) = self.thread_store.as_ref() {
+                    if let Some((thread_store, text_thread_store)) = self
+                        .thread_store
+                        .as_ref()
+                        .zip(self.text_thread_store.as_ref())
+                    {
                         self.mode = ContextPickerState::Thread(cx.new(|cx| {
                             ThreadContextPicker::new(
                                 thread_store.clone(),
+                                text_thread_store.clone(),
                                 context_picker.clone(),
                                 self.context_store.clone(),
                                 window,
@@ -447,30 +458,53 @@ impl ContextPicker {
 
     fn add_recent_thread(
         &self,
-        thread: ThreadContextEntry,
+        entry: ThreadContextEntry,
         cx: &mut Context<Self>,
     ) -> Task<Result<()>> {
         let Some(context_store) = self.context_store.upgrade() else {
             return Task::ready(Err(anyhow!("context store not available")));
         };
 
-        let Some(thread_store) = self
-            .thread_store
-            .as_ref()
-            .and_then(|thread_store| thread_store.upgrade())
-        else {
-            return Task::ready(Err(anyhow!("thread store not available")));
-        };
+        match entry {
+            ThreadContextEntry::Thread { id, .. } => {
+                let Some(thread_store) = self
+                    .thread_store
+                    .as_ref()
+                    .and_then(|thread_store| thread_store.upgrade())
+                else {
+                    return Task::ready(Err(anyhow!("thread store not available")));
+                };
 
-        let open_thread_task = thread_store.update(cx, |this, cx| this.open_thread(&thread.id, cx));
-        cx.spawn(async move |this, cx| {
-            let thread = open_thread_task.await?;
-            context_store.update(cx, |context_store, cx| {
-                context_store.add_thread(thread, true, cx);
-            })?;
+                let open_thread_task =
+                    thread_store.update(cx, |this, cx| this.open_thread(&id, cx));
+                cx.spawn(async move |this, cx| {
+                    let thread = open_thread_task.await?;
+                    context_store.update(cx, |context_store, cx| {
+                        context_store.add_thread(thread, true, cx);
+                    })?;
+                    this.update(cx, |_this, cx| cx.notify())
+                })
+            }
+            ThreadContextEntry::Context { path, .. } => {
+                let Some(text_thread_store) = self
+                    .text_thread_store
+                    .as_ref()
+                    .and_then(|thread_store| thread_store.upgrade())
+                else {
+                    return Task::ready(Err(anyhow!("text thread store not available")));
+                };
 
-            this.update(cx, |_this, cx| cx.notify())
-        })
+                let task = text_thread_store
+                    .update(cx, |this, cx| this.open_local_context(path.clone(), cx));
+                cx.spawn(async move |this, cx| {
+                    let thread = task.await?;
+                    context_store.update(cx, |context_store, cx| {
+                        context_store.add_text_thread(thread, true, cx);
+                    })?;
+                    this.update(cx, |_this, cx| cx.notify())
+                })
+            }
+        }
     }
 
     fn recent_entries(&self, cx: &mut App) -> Vec<RecentEntry> {
@@ -485,6 +519,7 @@ impl ContextPicker {
         recent_context_picker_entries(
             context_store,
             self.thread_store.clone(),
+            self.text_thread_store.clone(),
             workspace,
             None,
             cx,
@@ -583,6 +618,7 @@ fn available_context_picker_entries(
 fn recent_context_picker_entries(
     context_store: Entity<ContextStore>,
     thread_store: Option<WeakEntity<ThreadStore>>,
+    text_thread_store: Option<WeakEntity<TextThreadStore>>,
     workspace: Entity<Workspace>,
     exclude_path: Option<ProjectPath>,
     cx: &App,
@@ -612,24 +648,34 @@ fn recent_context_picker_entries(
 
     let active_thread_id = workspace
         .panel::<AssistantPanel>(cx)
-        .map(|panel| panel.read(cx).active_thread(cx).read(cx).id());
+        .and_then(|panel| Some(panel.read(cx).active_thread()?.read(cx).id()));
+
+    if let Some((thread_store, text_thread_store)) = thread_store
+        .and_then(|store| store.upgrade())
+        .zip(text_thread_store.and_then(|store| store.upgrade()))
+    {
+        let mut threads = unordered_thread_entries(thread_store, text_thread_store, cx)
+            .filter(|(_, thread)| match thread {
+                ThreadContextEntry::Thread { id, .. } => {
+                    Some(id) != active_thread_id && !current_threads.contains(id)
+                }
+                ThreadContextEntry::Context { .. } => true,
+            })
+            .collect::<Vec<_>>();
+
+        const RECENT_COUNT: usize = 2;
+        if threads.len() > RECENT_COUNT {
+            threads.select_nth_unstable_by_key(RECENT_COUNT - 1, |(updated_at, _)| {
+                std::cmp::Reverse(*updated_at)
+            });
+            threads.truncate(RECENT_COUNT);
+        }
+        threads.sort_unstable_by_key(|(updated_at, _)| std::cmp::Reverse(*updated_at));
 
-    if let Some(thread_store) = thread_store.and_then(|thread_store| thread_store.upgrade()) {
         recent.extend(
-            thread_store
-                .read(cx)
-                .reverse_chronological_threads()
+            threads
                 .into_iter()
-                .filter(|thread| {
-                    Some(&thread.id) != active_thread_id && !current_threads.contains(&thread.id)
-                })
-                .take(2)
-                .map(|thread| {
-                    RecentEntry::Thread(ThreadContextEntry {
-                        id: thread.id,
-                        summary: thread.summary,
-                    })
-                }),
+                .map(|(_, thread)| RecentEntry::Thread(thread)),
         );
     }
 
@@ -827,6 +873,7 @@ pub enum MentionLink {
     Selection(ProjectPath, Range<usize>),
     Fetch(String),
     Thread(ThreadId),
+    TextThread(Arc<Path>),
     Rule(UserPromptId),
 }
 
@@ -838,6 +885,8 @@ impl MentionLink {
     const FETCH: &str = "@fetch";
     const RULE: &str = "@rule";
 
+    const TEXT_THREAD_URL_PREFIX: &str = "text-thread://";
+
     const SEPARATOR: &str = ":";
 
     pub fn is_valid(url: &str) -> bool {
@@ -877,7 +926,22 @@ impl MentionLink {
     }
 
     pub fn for_thread(thread: &ThreadContextEntry) -> String {
-        format!("[@{}]({}:{})", thread.summary, Self::THREAD, thread.id)
+        match thread {
+            ThreadContextEntry::Thread { id, title } => {
+                format!("[@{}]({}:{})", title, Self::THREAD, id)
+            }
+            ThreadContextEntry::Context { path, title } => {
+                let filename = path.file_name().unwrap_or_default();
+                let escaped_filename = urlencoding::encode(&filename.to_string_lossy()).to_string();
+                format!(
+                    "[@{}]({}:{}{})",
+                    title,
+                    Self::THREAD,
+                    Self::TEXT_THREAD_URL_PREFIX,
+                    escaped_filename
+                )
+            }
+        }
     }
 
     pub fn for_fetch(url: &str) -> String {
@@ -939,8 +1003,15 @@ impl MentionLink {
                 Some(MentionLink::Selection(project_path, line_range))
             }
             Self::THREAD => {
-                let thread_id = ThreadId::from(argument);
-                Some(MentionLink::Thread(thread_id))
+                if let Some(encoded_filename) = argument.strip_prefix(Self::TEXT_THREAD_URL_PREFIX)
+                {
+                    let filename = urlencoding::decode(encoded_filename).ok()?;
+                    let path = contexts_dir().join(filename.as_ref()).into();
+                    Some(MentionLink::TextThread(path))
+                } else {
+                    let thread_id = ThreadId::from(argument);
+                    Some(MentionLink::Thread(thread_id))
+                }
             }
             Self::FETCH => Some(MentionLink::Fetch(argument.to_string())),
             Self::RULE => {

crates/agent/src/context_picker/completion_provider.rs 🔗

@@ -25,7 +25,7 @@ use workspace::Workspace;
 use crate::Thread;
 use crate::context::{AgentContextHandle, AgentContextKey, ContextCreasesAddon, RULES_ICON};
 use crate::context_store::ContextStore;
-use crate::thread_store::ThreadStore;
+use crate::thread_store::{TextThreadStore, ThreadStore};
 
 use super::fetch_context_picker::fetch_url_content;
 use super::file_context_picker::{FileMatch, search_files};
@@ -72,6 +72,7 @@ fn search(
     recent_entries: Vec<RecentEntry>,
     prompt_store: Option<Entity<PromptStore>>,
     thread_store: Option<WeakEntity<ThreadStore>>,
+    text_thread_context_store: Option<WeakEntity<assistant_context_editor::ContextStore>>,
     workspace: Entity<Workspace>,
     cx: &mut App,
 ) -> Task<Vec<Match>> {
@@ -101,9 +102,18 @@ fn search(
         }
 
         Some(ContextPickerMode::Thread) => {
-            if let Some(thread_store) = thread_store.as_ref().and_then(|t| t.upgrade()) {
-                let search_threads_task =
-                    search_threads(query.clone(), cancellation_flag.clone(), thread_store, cx);
+            if let Some((thread_store, context_store)) = thread_store
+                .as_ref()
+                .and_then(|t| t.upgrade())
+                .zip(text_thread_context_store.as_ref().and_then(|t| t.upgrade()))
+            {
+                let search_threads_task = search_threads(
+                    query.clone(),
+                    cancellation_flag.clone(),
+                    thread_store,
+                    context_store,
+                    cx,
+                );
                 cx.background_spawn(async move {
                     search_threads_task
                         .await
@@ -236,6 +246,7 @@ pub struct ContextPickerCompletionProvider {
     workspace: WeakEntity<Workspace>,
     context_store: WeakEntity<ContextStore>,
     thread_store: Option<WeakEntity<ThreadStore>>,
+    text_thread_store: Option<WeakEntity<TextThreadStore>>,
     editor: WeakEntity<Editor>,
     excluded_buffer: Option<WeakEntity<Buffer>>,
 }
@@ -245,6 +256,7 @@ impl ContextPickerCompletionProvider {
         workspace: WeakEntity<Workspace>,
         context_store: WeakEntity<ContextStore>,
         thread_store: Option<WeakEntity<ThreadStore>>,
+        text_thread_store: Option<WeakEntity<TextThreadStore>>,
         editor: WeakEntity<Editor>,
         exclude_buffer: Option<WeakEntity<Buffer>>,
     ) -> Self {
@@ -252,6 +264,7 @@ impl ContextPickerCompletionProvider {
             workspace,
             context_store,
             thread_store,
+            text_thread_store,
             editor,
             excluded_buffer: exclude_buffer,
         }
@@ -400,6 +413,7 @@ impl ContextPickerCompletionProvider {
         editor: Entity<Editor>,
         context_store: Entity<ContextStore>,
         thread_store: Entity<ThreadStore>,
+        text_thread_store: Entity<TextThreadStore>,
     ) -> Completion {
         let icon_for_completion = if recent {
             IconName::HistoryRerun
@@ -411,38 +425,58 @@ impl ContextPickerCompletionProvider {
         Completion {
             replace_range: source_range.clone(),
             new_text,
-            label: CodeLabel::plain(thread_entry.summary.to_string(), None),
+            label: CodeLabel::plain(thread_entry.title().to_string(), None),
             documentation: None,
             insert_text_mode: None,
             source: project::CompletionSource::Custom,
             icon_path: Some(icon_for_completion.path().into()),
             confirm: Some(confirm_completion_callback(
                 IconName::MessageBubbles.path().into(),
-                thread_entry.summary.clone(),
+                thread_entry.title().clone(),
                 excerpt_id,
                 source_range.start,
                 new_text_len,
                 editor.clone(),
                 context_store.clone(),
-                move |cx| {
-                    let thread_id = thread_entry.id.clone();
-                    let context_store = context_store.clone();
-                    let thread_store = thread_store.clone();
-                    cx.spawn::<_, Option<_>>(async move |cx| {
-                        let thread: Entity<Thread> = thread_store
-                            .update(cx, |thread_store, cx| {
-                                thread_store.open_thread(&thread_id, cx)
-                            })
-                            .ok()?
-                            .await
-                            .log_err()?;
-                        let context = context_store
-                            .update(cx, |context_store, cx| {
-                                context_store.add_thread(thread, false, cx)
-                            })
-                            .ok()??;
-                        Some(context)
-                    })
+                move |cx| match &thread_entry {
+                    ThreadContextEntry::Thread { id, .. } => {
+                        let thread_id = id.clone();
+                        let context_store = context_store.clone();
+                        let thread_store = thread_store.clone();
+                        cx.spawn::<_, Option<_>>(async move |cx| {
+                            let thread: Entity<Thread> = thread_store
+                                .update(cx, |thread_store, cx| {
+                                    thread_store.open_thread(&thread_id, cx)
+                                })
+                                .ok()?
+                                .await
+                                .log_err()?;
+                            let context = context_store
+                                .update(cx, |context_store, cx| {
+                                    context_store.add_thread(thread, false, cx)
+                                })
+                                .ok()??;
+                            Some(context)
+                        })
+                    }
+                    ThreadContextEntry::Context { path, .. } => {
+                        let path = path.clone();
+                        let context_store = context_store.clone();
+                        let text_thread_store = text_thread_store.clone();
+                        cx.spawn::<_, Option<_>>(async move |cx| {
+                            let thread = text_thread_store
+                                .update(cx, |store, cx| store.open_local_context(path, cx))
+                                .ok()?
+                                .await
+                                .log_err()?;
+                            let context = context_store
+                                .update(cx, |context_store, cx| {
+                                    context_store.add_text_thread(thread, false, cx)
+                                })
+                                .ok()??;
+                            Some(context)
+                        })
+                    }
                 },
             )),
         }
@@ -733,6 +767,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
             ..snapshot.anchor_before(state.source_range.end);
 
         let thread_store = self.thread_store.clone();
+        let text_thread_store = self.text_thread_store.clone();
         let editor = self.editor.clone();
         let http_client = workspace.read(cx).client().http_client();
 
@@ -749,6 +784,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
         let recent_entries = recent_context_picker_entries(
             context_store.clone(),
             thread_store.clone(),
+            text_thread_store.clone(),
             workspace.clone(),
             excluded_path.clone(),
             cx,
@@ -768,6 +804,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
             recent_entries,
             prompt_store,
             thread_store.clone(),
+            text_thread_store.clone(),
             workspace.clone(),
             cx,
         );
@@ -819,6 +856,8 @@ impl CompletionProvider for ContextPickerCompletionProvider {
                             thread, is_recent, ..
                         }) => {
                             let thread_store = thread_store.as_ref().and_then(|t| t.upgrade())?;
+                            let text_thread_store =
+                                text_thread_store.as_ref().and_then(|t| t.upgrade())?;
                             Some(Self::completion_for_thread(
                                 thread,
                                 excerpt_id,
@@ -827,6 +866,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
                                 editor.clone(),
                                 context_store.clone(),
                                 thread_store,
+                                text_thread_store,
                             ))
                         }
 
@@ -1247,6 +1287,7 @@ mod tests {
                 workspace.downgrade(),
                 context_store.downgrade(),
                 None,
+                None,
                 editor_entity,
                 last_opened_buffer,
             ))));

crates/agent/src/context_picker/thread_context_picker.rs 🔗

@@ -1,6 +1,8 @@
+use std::path::Path;
 use std::sync::Arc;
 use std::sync::atomic::AtomicBool;
 
+use chrono::{DateTime, Utc};
 use fuzzy::StringMatchCandidate;
 use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
 use picker::{Picker, PickerDelegate};
@@ -9,7 +11,7 @@ use ui::{ListItem, prelude::*};
 use crate::context_picker::ContextPicker;
 use crate::context_store::{self, ContextStore};
 use crate::thread::ThreadId;
-use crate::thread_store::ThreadStore;
+use crate::thread_store::{TextThreadStore, ThreadStore};
 
 pub struct ThreadContextPicker {
     picker: Entity<Picker<ThreadContextPickerDelegate>>,
@@ -18,13 +20,18 @@ pub struct ThreadContextPicker {
 impl ThreadContextPicker {
     pub fn new(
         thread_store: WeakEntity<ThreadStore>,
+        text_thread_context_store: WeakEntity<TextThreadStore>,
         context_picker: WeakEntity<ContextPicker>,
         context_store: WeakEntity<context_store::ContextStore>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
-        let delegate =
-            ThreadContextPickerDelegate::new(thread_store, context_picker, context_store);
+        let delegate = ThreadContextPickerDelegate::new(
+            thread_store,
+            text_thread_context_store,
+            context_picker,
+            context_store,
+        );
         let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
 
         ThreadContextPicker { picker }
@@ -44,13 +51,29 @@ impl Render for ThreadContextPicker {
 }
 
 #[derive(Debug, Clone)]
-pub struct ThreadContextEntry {
-    pub id: ThreadId,
-    pub summary: SharedString,
+pub enum ThreadContextEntry {
+    Thread {
+        id: ThreadId,
+        title: SharedString,
+    },
+    Context {
+        path: Arc<Path>,
+        title: SharedString,
+    },
+}
+
+impl ThreadContextEntry {
+    pub fn title(&self) -> &SharedString {
+        match self {
+            Self::Thread { title, .. } => title,
+            Self::Context { title, .. } => title,
+        }
+    }
 }
 
 pub struct ThreadContextPickerDelegate {
     thread_store: WeakEntity<ThreadStore>,
+    text_thread_store: WeakEntity<TextThreadStore>,
     context_picker: WeakEntity<ContextPicker>,
     context_store: WeakEntity<context_store::ContextStore>,
     matches: Vec<ThreadContextEntry>,
@@ -60,6 +83,7 @@ pub struct ThreadContextPickerDelegate {
 impl ThreadContextPickerDelegate {
     pub fn new(
         thread_store: WeakEntity<ThreadStore>,
+        text_thread_store: WeakEntity<TextThreadStore>,
         context_picker: WeakEntity<ContextPicker>,
         context_store: WeakEntity<context_store::ContextStore>,
     ) -> Self {
@@ -67,6 +91,7 @@ impl ThreadContextPickerDelegate {
             thread_store,
             context_picker,
             context_store,
+            text_thread_store,
             matches: Vec::new(),
             selected_index: 0,
         }
@@ -103,11 +128,21 @@ impl PickerDelegate for ThreadContextPickerDelegate {
         window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Task<()> {
-        let Some(thread_store) = self.thread_store.upgrade() else {
+        let Some((thread_store, text_thread_context_store)) = self
+            .thread_store
+            .upgrade()
+            .zip(self.text_thread_store.upgrade())
+        else {
             return Task::ready(());
         };
 
-        let search_task = search_threads(query, Arc::new(AtomicBool::default()), thread_store, cx);
+        let search_task = search_threads(
+            query,
+            Arc::new(AtomicBool::default()),
+            thread_store,
+            text_thread_context_store,
+            cx,
+        );
         cx.spawn_in(window, async move |this, cx| {
             let matches = search_task.await;
             this.update(cx, |this, cx| {
@@ -124,24 +159,48 @@ impl PickerDelegate for ThreadContextPickerDelegate {
             return;
         };
 
-        let Some(thread_store) = self.thread_store.upgrade() else {
-            return;
-        };
+        match entry {
+            ThreadContextEntry::Thread { id, .. } => {
+                let Some(thread_store) = self.thread_store.upgrade() else {
+                    return;
+                };
+                let open_thread_task =
+                    thread_store.update(cx, |this, cx| this.open_thread(&id, cx));
 
-        let open_thread_task = thread_store.update(cx, |this, cx| this.open_thread(&entry.id, cx));
+                cx.spawn(async move |this, cx| {
+                    let thread = open_thread_task.await?;
+                    this.update(cx, |this, cx| {
+                        this.delegate
+                            .context_store
+                            .update(cx, |context_store, cx| {
+                                context_store.add_thread(thread, true, cx)
+                            })
+                            .ok();
+                    })
+                })
+                .detach_and_log_err(cx);
+            }
+            ThreadContextEntry::Context { path, .. } => {
+                let Some(text_thread_store) = self.text_thread_store.upgrade() else {
+                    return;
+                };
+                let task = text_thread_store
+                    .update(cx, |this, cx| this.open_local_context(path.clone(), cx));
 
-        cx.spawn(async move |this, cx| {
-            let thread = open_thread_task.await?;
-            this.update(cx, |this, cx| {
-                this.delegate
-                    .context_store
-                    .update(cx, |context_store, cx| {
-                        context_store.add_thread(thread, true, cx)
+                cx.spawn(async move |this, cx| {
+                    let thread = task.await?;
+                    this.update(cx, |this, cx| {
+                        this.delegate
+                            .context_store
+                            .update(cx, |context_store, cx| {
+                                context_store.add_text_thread(thread, true, cx)
+                            })
+                            .ok();
                     })
-                    .ok();
-            })
-        })
-        .detach_and_log_err(cx);
+                })
+                .detach_and_log_err(cx);
+            }
+        }
     }
 
     fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
@@ -168,13 +227,20 @@ impl PickerDelegate for ThreadContextPickerDelegate {
 }
 
 pub fn render_thread_context_entry(
-    thread: &ThreadContextEntry,
+    entry: &ThreadContextEntry,
     context_store: WeakEntity<ContextStore>,
     cx: &mut App,
 ) -> Div {
-    let added = context_store.upgrade().map_or(false, |ctx_store| {
-        ctx_store.read(cx).includes_thread(&thread.id)
-    });
+    let is_added = match entry {
+        ThreadContextEntry::Thread { id, .. } => context_store
+            .upgrade()
+            .map_or(false, |ctx_store| ctx_store.read(cx).includes_thread(&id)),
+        ThreadContextEntry::Context { path, .. } => {
+            context_store.upgrade().map_or(false, |ctx_store| {
+                ctx_store.read(cx).includes_text_thread(path)
+            })
+        }
+    };
 
     h_flex()
         .gap_1p5()
@@ -189,9 +255,9 @@ pub fn render_thread_context_entry(
                         .size(IconSize::XSmall)
                         .color(Color::Muted),
                 )
-                .child(Label::new(thread.summary.clone()).truncate()),
+                .child(Label::new(entry.title().clone()).truncate()),
         )
-        .when(added, |el| {
+        .when(is_added, |el| {
             el.child(
                 h_flex()
                     .gap_1()
@@ -211,28 +277,54 @@ pub struct ThreadMatch {
     pub is_recent: bool,
 }
 
+pub fn unordered_thread_entries(
+    thread_store: Entity<ThreadStore>,
+    text_thread_store: Entity<TextThreadStore>,
+    cx: &App,
+) -> impl Iterator<Item = (DateTime<Utc>, ThreadContextEntry)> {
+    let threads = thread_store.read(cx).unordered_threads().map(|thread| {
+        (
+            thread.updated_at,
+            ThreadContextEntry::Thread {
+                id: thread.id.clone(),
+                title: thread.summary.clone(),
+            },
+        )
+    });
+
+    let text_threads = text_thread_store
+        .read(cx)
+        .unordered_contexts()
+        .map(|context| {
+            (
+                context.mtime.to_utc(),
+                ThreadContextEntry::Context {
+                    path: context.path.clone(),
+                    title: context.title.clone().into(),
+                },
+            )
+        });
+
+    threads.chain(text_threads)
+}
+
 pub(crate) fn search_threads(
     query: String,
     cancellation_flag: Arc<AtomicBool>,
     thread_store: Entity<ThreadStore>,
+    text_thread_store: Entity<TextThreadStore>,
     cx: &mut App,
 ) -> Task<Vec<ThreadMatch>> {
-    let threads = thread_store
-        .read(cx)
-        .reverse_chronological_threads()
-        .into_iter()
-        .map(|thread| ThreadContextEntry {
-            id: thread.id,
-            summary: thread.summary,
-        })
-        .collect::<Vec<_>>();
+    let mut threads =
+        unordered_thread_entries(thread_store, text_thread_store, cx).collect::<Vec<_>>();
+    threads.sort_unstable_by_key(|(updated_at, _)| std::cmp::Reverse(*updated_at));
 
     let executor = cx.background_executor().clone();
     cx.background_spawn(async move {
         if query.is_empty() {
             threads
                 .into_iter()
-                .map(|thread| ThreadMatch {
+                .map(|(_, thread)| ThreadMatch {
                     thread,
                     is_recent: false,
                 })
@@ -241,7 +333,7 @@ pub(crate) fn search_threads(
             let candidates = threads
                 .iter()
                 .enumerate()
-                .map(|(id, thread)| StringMatchCandidate::new(id, &thread.summary))
+                .map(|(id, (_, thread))| StringMatchCandidate::new(id, &thread.title()))
                 .collect::<Vec<_>>();
             let matches = fuzzy::match_strings(
                 &candidates,
@@ -256,7 +348,7 @@ pub(crate) fn search_threads(
             matches
                 .into_iter()
                 .map(|mat| ThreadMatch {
-                    thread: threads[mat.candidate_id].clone(),
+                    thread: threads[mat.candidate_id].1.clone(),
                     is_recent: false,
                 })
                 .collect()

crates/agent/src/context_store.rs 🔗

@@ -1,8 +1,9 @@
 use std::ops::Range;
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
 use std::sync::Arc;
 
 use anyhow::{Result, anyhow};
+use assistant_context_editor::AssistantContext;
 use collections::{HashSet, IndexSet};
 use futures::{self, FutureExt};
 use gpui::{App, Context, Entity, EventEmitter, Image, SharedString, Task, WeakEntity};
@@ -18,7 +19,7 @@ use crate::ThreadStore;
 use crate::context::{
     AgentContextHandle, AgentContextKey, ContextId, DirectoryContextHandle, FetchedUrlContext,
     FileContextHandle, ImageContext, RulesContextHandle, SelectionContextHandle,
-    SymbolContextHandle, ThreadContextHandle,
+    SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
 };
 use crate::context_strip::SuggestedContext;
 use crate::thread::{MessageId, Thread, ThreadId};
@@ -29,6 +30,7 @@ pub struct ContextStore {
     next_context_id: ContextId,
     context_set: IndexSet<AgentContextKey>,
     context_thread_ids: HashSet<ThreadId>,
+    context_text_thread_paths: HashSet<Arc<Path>>,
 }
 
 pub enum ContextStoreEvent {
@@ -48,6 +50,7 @@ impl ContextStore {
             next_context_id: ContextId::zero(),
             context_set: IndexSet::default(),
             context_thread_ids: HashSet::default(),
+            context_text_thread_paths: HashSet::default(),
         }
     }
 
@@ -227,6 +230,31 @@ impl ContextStore {
         }
     }
 
+    pub fn add_text_thread(
+        &mut self,
+        context: Entity<AssistantContext>,
+        remove_if_exists: bool,
+        cx: &mut Context<Self>,
+    ) -> Option<AgentContextHandle> {
+        let context_id = self.next_context_id.post_inc();
+        let context = AgentContextHandle::TextThread(TextThreadContextHandle {
+            context,
+            context_id,
+        });
+
+        if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
+            if remove_if_exists {
+                self.remove_context(&context, cx);
+                None
+            } else {
+                Some(existing.as_ref().clone())
+            }
+        } else {
+            self.insert_context(context.clone(), cx);
+            Some(context)
+        }
+    }
+
     pub fn add_rules(
         &mut self,
         prompt_id: UserPromptId,
@@ -364,6 +392,18 @@ impl ContextStore {
                     );
                 }
             }
+            SuggestedContext::TextThread { context, name: _ } => {
+                if let Some(context) = context.upgrade() {
+                    let context_id = self.next_context_id.post_inc();
+                    self.insert_context(
+                        AgentContextHandle::TextThread(TextThreadContextHandle {
+                            context,
+                            context_id,
+                        }),
+                        cx,
+                    );
+                }
+            }
         }
     }
 
@@ -380,6 +420,10 @@ impl ContextStore {
                     return false;
                 }
             }
+            AgentContextHandle::TextThread(text_thread_context) => {
+                self.context_text_thread_paths
+                    .extend(text_thread_context.context.read(cx).path().cloned());
+            }
             _ => {}
         }
         let inserted = self.context_set.insert(AgentContextKey(context));
@@ -399,6 +443,11 @@ impl ContextStore {
                     self.context_thread_ids
                         .remove(thread_context.thread.read(cx).id());
                 }
+                AgentContextHandle::TextThread(text_thread_context) => {
+                    if let Some(path) = text_thread_context.context.read(cx).path() {
+                        self.context_text_thread_paths.remove(path);
+                    }
+                }
                 _ => {}
             }
             cx.emit(ContextStoreEvent::ContextRemoved(key));
@@ -468,6 +517,10 @@ impl ContextStore {
         self.context_thread_ids.contains(thread_id)
     }
 
+    pub fn includes_text_thread(&self, path: &Arc<Path>) -> bool {
+        self.context_text_thread_paths.contains(path)
+    }
+
     pub fn includes_user_rules(&self, prompt_id: UserPromptId) -> bool {
         self.context_set
             .contains(&RulesContextHandle::lookup_key(prompt_id))
@@ -496,6 +549,7 @@ impl ContextStore {
                 | AgentContextHandle::Selection(_)
                 | AgentContextHandle::FetchedUrl(_)
                 | AgentContextHandle::Thread(_)
+                | AgentContextHandle::TextThread(_)
                 | AgentContextHandle::Rules(_)
                 | AgentContextHandle::Image(_) => None,
             })

crates/agent/src/context_strip.rs 🔗

@@ -1,6 +1,7 @@
 use std::path::Path;
 use std::rc::Rc;
 
+use assistant_context_editor::AssistantContext;
 use collections::HashSet;
 use editor::Editor;
 use file_icons::FileIcons;
@@ -18,7 +19,7 @@ use crate::context::{AgentContextHandle, ContextKind};
 use crate::context_picker::ContextPicker;
 use crate::context_store::ContextStore;
 use crate::thread::Thread;
-use crate::thread_store::ThreadStore;
+use crate::thread_store::{TextThreadStore, ThreadStore};
 use crate::ui::{AddedContext, ContextPill};
 use crate::{
     AcceptSuggestedContext, AssistantPanel, FocusDown, FocusLeft, FocusRight, FocusUp,
@@ -43,6 +44,7 @@ impl ContextStrip {
         context_store: Entity<ContextStore>,
         workspace: WeakEntity<Workspace>,
         thread_store: Option<WeakEntity<ThreadStore>>,
+        text_thread_store: Option<WeakEntity<TextThreadStore>>,
         context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
         suggest_context_kind: SuggestContextKind,
         window: &mut Window,
@@ -52,6 +54,7 @@ impl ContextStrip {
             ContextPicker::new(
                 workspace.clone(),
                 thread_store.clone(),
+                text_thread_store,
                 context_store.downgrade(),
                 window,
                 cx,
@@ -141,27 +144,42 @@ impl ContextStrip {
         }
 
         let workspace = self.workspace.upgrade()?;
-        let active_thread = workspace
-            .read(cx)
-            .panel::<AssistantPanel>(cx)?
-            .read(cx)
-            .active_thread(cx);
-        let weak_active_thread = active_thread.downgrade();
+        let panel = workspace.read(cx).panel::<AssistantPanel>(cx)?.read(cx);
 
-        let active_thread = active_thread.read(cx);
+        if let Some(active_thread) = panel.active_thread() {
+            let weak_active_thread = active_thread.downgrade();
 
-        if self
-            .context_store
-            .read(cx)
-            .includes_thread(active_thread.id())
-        {
-            return None;
-        }
+            let active_thread = active_thread.read(cx);
 
-        Some(SuggestedContext::Thread {
-            name: active_thread.summary_or_default(),
-            thread: weak_active_thread,
-        })
+            if self
+                .context_store
+                .read(cx)
+                .includes_thread(active_thread.id())
+            {
+                return None;
+            }
+
+            Some(SuggestedContext::Thread {
+                name: active_thread.summary_or_default(),
+                thread: weak_active_thread,
+            })
+        } else if let Some(active_context_editor) = panel.active_context_editor() {
+            let context = active_context_editor.read(cx).context();
+            let weak_context = context.downgrade();
+            let context = context.read(cx);
+            let path = context.path()?;
+
+            if self.context_store.read(cx).includes_text_thread(path) {
+                return None;
+            }
+
+            Some(SuggestedContext::TextThread {
+                name: context.summary_or_default(),
+                context: weak_context,
+            })
+        } else {
+            None
+        }
     }
 
     fn handle_context_picker_event(
@@ -538,6 +556,10 @@ pub enum SuggestedContext {
         name: SharedString,
         thread: WeakEntity<Thread>,
     },
+    TextThread {
+        name: SharedString,
+        context: WeakEntity<AssistantContext>,
+    },
 }
 
 impl SuggestedContext {
@@ -545,6 +567,7 @@ impl SuggestedContext {
         match self {
             Self::File { name, .. } => name,
             Self::Thread { name, .. } => name,
+            Self::TextThread { name, .. } => name,
         }
     }
 
@@ -552,6 +575,7 @@ impl SuggestedContext {
         match self {
             Self::File { icon_path, .. } => icon_path.clone(),
             Self::Thread { .. } => None,
+            Self::TextThread { .. } => None,
         }
     }
 
@@ -559,6 +583,7 @@ impl SuggestedContext {
         match self {
             Self::File { .. } => ContextKind::File,
             Self::Thread { .. } => ContextKind::Thread,
+            Self::TextThread { .. } => ContextKind::TextThread,
         }
     }
 }

crates/agent/src/history_store.rs 🔗

@@ -163,7 +163,10 @@ impl HistoryStore {
             history_entries.push(HistoryEntry::Thread(thread));
         }
 
-        for context in self.context_store.update(cx, |this, _cx| this.contexts()) {
+        for context in self
+            .context_store
+            .update(cx, |this, _cx| this.reverse_chronological_contexts())
+        {
             history_entries.push(HistoryEntry::Context(context));
         }
 

crates/agent/src/inline_assistant.rs 🔗

@@ -48,6 +48,7 @@ use crate::buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent};
 use crate::context_store::ContextStore;
 use crate::inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent};
 use crate::terminal_inline_assistant::TerminalInlineAssistant;
+use crate::thread_store::TextThreadStore;
 use crate::thread_store::ThreadStore;
 
 pub fn init(
@@ -192,16 +193,20 @@ impl InlineAssistant {
         if let Some(editor) = item.act_as::<Editor>(cx) {
             editor.update(cx, |editor, cx| {
                 if is_assistant2_enabled {
-                    let thread_store = workspace
-                        .read(cx)
-                        .panel::<AssistantPanel>(cx)
+                    let panel = workspace.read(cx).panel::<AssistantPanel>(cx);
+                    let thread_store = panel
+                        .as_ref()
                         .map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade());
+                    let text_thread_store = panel.map(|assistant_panel| {
+                        assistant_panel.read(cx).text_thread_store().downgrade()
+                    });
 
                     editor.add_code_action_provider(
                         Rc::new(AssistantCodeActionProvider {
                             editor: cx.entity().downgrade(),
                             workspace: workspace.downgrade(),
                             thread_store,
+                            text_thread_store,
                         }),
                         window,
                         cx,
@@ -253,6 +258,8 @@ impl InlineAssistant {
             .and_then(|assistant_panel| assistant_panel.prompt_store().as_ref().cloned());
         let thread_store =
             assistant_panel.map(|assistant_panel| assistant_panel.thread_store().downgrade());
+        let text_thread_store =
+            assistant_panel.map(|assistant_panel| assistant_panel.text_thread_store().downgrade());
 
         let handle_assist =
             |window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target {
@@ -264,6 +271,7 @@ impl InlineAssistant {
                             workspace.project().downgrade(),
                             prompt_store,
                             thread_store,
+                            text_thread_store,
                             window,
                             cx,
                         )
@@ -277,6 +285,7 @@ impl InlineAssistant {
                             workspace.project().downgrade(),
                             prompt_store,
                             thread_store,
+                            text_thread_store,
                             window,
                             cx,
                         )
@@ -332,6 +341,7 @@ impl InlineAssistant {
         project: WeakEntity<Project>,
         prompt_store: Option<Entity<PromptStore>>,
         thread_store: Option<WeakEntity<ThreadStore>>,
+        text_thread_store: Option<WeakEntity<TextThreadStore>>,
         window: &mut Window,
         cx: &mut App,
     ) {
@@ -465,6 +475,7 @@ impl InlineAssistant {
                     context_store,
                     workspace.clone(),
                     thread_store.clone(),
+                    text_thread_store.clone(),
                     window,
                     cx,
                 )
@@ -537,6 +548,7 @@ impl InlineAssistant {
         workspace: Entity<Workspace>,
         prompt_store: Option<Entity<PromptStore>>,
         thread_store: Option<WeakEntity<ThreadStore>>,
+        text_thread_store: Option<WeakEntity<TextThreadStore>>,
         window: &mut Window,
         cx: &mut App,
     ) -> InlineAssistId {
@@ -582,6 +594,7 @@ impl InlineAssistant {
                 context_store,
                 workspace.downgrade(),
                 thread_store,
+                text_thread_store,
                 window,
                 cx,
             )
@@ -1729,6 +1742,7 @@ struct AssistantCodeActionProvider {
     editor: WeakEntity<Editor>,
     workspace: WeakEntity<Workspace>,
     thread_store: Option<WeakEntity<ThreadStore>>,
+    text_thread_store: Option<WeakEntity<TextThreadStore>>,
 }
 
 const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant2";
@@ -1803,6 +1817,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
         let editor = self.editor.clone();
         let workspace = self.workspace.clone();
         let thread_store = self.thread_store.clone();
+        let text_thread_store = self.text_thread_store.clone();
         let prompt_store = PromptStore::global(cx);
         window.spawn(cx, async move |cx| {
             let workspace = workspace.upgrade().context("workspace was released")?;
@@ -1855,6 +1870,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
                     workspace,
                     prompt_store,
                     thread_store,
+                    text_thread_store,
                     window,
                     cx,
                 );

crates/agent/src/inline_prompt_editor.rs 🔗

@@ -6,7 +6,7 @@ use crate::context_store::ContextStore;
 use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
 use crate::message_editor::{extract_message_creases, insert_message_creases};
 use crate::terminal_codegen::TerminalCodegen;
-use crate::thread_store::ThreadStore;
+use crate::thread_store::{TextThreadStore, ThreadStore};
 use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
 use crate::{RemoveAllContext, ToggleContextPicker};
 use client::ErrorExt;
@@ -846,6 +846,7 @@ impl PromptEditor<BufferCodegen> {
         context_store: Entity<ContextStore>,
         workspace: WeakEntity<Workspace>,
         thread_store: Option<WeakEntity<ThreadStore>>,
+        text_thread_store: Option<WeakEntity<TextThreadStore>>,
         window: &mut Window,
         cx: &mut Context<PromptEditor<BufferCodegen>>,
     ) -> PromptEditor<BufferCodegen> {
@@ -889,6 +890,7 @@ impl PromptEditor<BufferCodegen> {
                 workspace.clone(),
                 context_store.downgrade(),
                 thread_store.clone(),
+                text_thread_store.clone(),
                 prompt_editor_entity,
                 codegen_buffer.as_ref().map(Entity::downgrade),
             ))));
@@ -902,6 +904,7 @@ impl PromptEditor<BufferCodegen> {
                 context_store.clone(),
                 workspace.clone(),
                 thread_store.clone(),
+                text_thread_store.clone(),
                 context_picker_menu_handle.clone(),
                 SuggestContextKind::Thread,
                 window,
@@ -1023,6 +1026,7 @@ impl PromptEditor<TerminalCodegen> {
         context_store: Entity<ContextStore>,
         workspace: WeakEntity<Workspace>,
         thread_store: Option<WeakEntity<ThreadStore>>,
+        text_thread_store: Option<WeakEntity<TextThreadStore>>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -1059,6 +1063,7 @@ impl PromptEditor<TerminalCodegen> {
                 workspace.clone(),
                 context_store.downgrade(),
                 thread_store.clone(),
+                text_thread_store.clone(),
                 prompt_editor_entity,
                 None,
             ))));
@@ -1072,6 +1077,7 @@ impl PromptEditor<TerminalCodegen> {
                 context_store.clone(),
                 workspace.clone(),
                 thread_store.clone(),
+                text_thread_store.clone(),
                 context_picker_menu_handle.clone(),
                 SuggestContextKind::Thread,
                 window,

crates/agent/src/message_editor.rs 🔗

@@ -45,7 +45,7 @@ use crate::context_store::ContextStore;
 use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
 use crate::profile_selector::ProfileSelector;
 use crate::thread::{MessageCrease, Thread, TokenUsageRatio};
-use crate::thread_store::ThreadStore;
+use crate::thread_store::{TextThreadStore, ThreadStore};
 use crate::{
     ActiveThread, AgentDiffPane, Chat, ExpandMessageEditor, Follow, NewThread, OpenAgentDiff,
     RemoveAllContext, ToggleContextPicker, ToggleProfileSelector, register_agent_preview,
@@ -80,6 +80,7 @@ pub(crate) fn create_editor(
     workspace: WeakEntity<Workspace>,
     context_store: WeakEntity<ContextStore>,
     thread_store: WeakEntity<ThreadStore>,
+    text_thread_store: WeakEntity<TextThreadStore>,
     window: &mut Window,
     cx: &mut App,
 ) -> Entity<Editor> {
@@ -121,6 +122,7 @@ pub(crate) fn create_editor(
             workspace,
             context_store,
             Some(thread_store),
+            Some(text_thread_store),
             editor_entity,
             None,
         ))));
@@ -136,6 +138,7 @@ impl MessageEditor {
         context_store: Entity<ContextStore>,
         prompt_store: Option<Entity<PromptStore>>,
         thread_store: WeakEntity<ThreadStore>,
+        text_thread_store: WeakEntity<TextThreadStore>,
         thread: Entity<Thread>,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -147,6 +150,7 @@ impl MessageEditor {
             workspace.clone(),
             context_store.downgrade(),
             thread_store.clone(),
+            text_thread_store.clone(),
             window,
             cx,
         );
@@ -156,6 +160,7 @@ impl MessageEditor {
                 context_store.clone(),
                 workspace.clone(),
                 Some(thread_store.clone()),
+                Some(text_thread_store.clone()),
                 context_picker_menu_handle.clone(),
                 SuggestContextKind::File,
                 window,
@@ -1400,16 +1405,19 @@ impl AgentPreview for MessageEditor {
     fn agent_preview(
         workspace: WeakEntity<Workspace>,
         active_thread: Entity<ActiveThread>,
-        thread_store: WeakEntity<ThreadStore>,
         window: &mut Window,
         cx: &mut App,
     ) -> Option<AnyElement> {
         if let Some(workspace) = workspace.upgrade() {
             let fs = workspace.read(cx).app_state().fs.clone();
             let user_store = workspace.read(cx).app_state().user_store.clone();
-            let weak_project = workspace.read(cx).project().clone().downgrade();
+            let project = workspace.read(cx).project().clone();
+            let weak_project = project.downgrade();
             let context_store = cx.new(|_cx| ContextStore::new(weak_project, None));
-            let thread = active_thread.read(cx).thread().clone();
+            let active_thread = active_thread.read(cx);
+            let thread = active_thread.thread().clone();
+            let thread_store = active_thread.thread_store().clone();
+            let text_thread_store = active_thread.text_thread_store().clone();
 
             let default_message_editor = cx.new(|cx| {
                 MessageEditor::new(
@@ -1418,7 +1426,8 @@ impl AgentPreview for MessageEditor {
                     user_store,
                     context_store,
                     None,
-                    thread_store,
+                    thread_store.downgrade(),
+                    text_thread_store.downgrade(),
                     thread,
                     window,
                     cx,

crates/agent/src/terminal_inline_assistant.rs 🔗

@@ -4,7 +4,7 @@ use crate::inline_prompt_editor::{
     CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
 };
 use crate::terminal_codegen::{CLEAR_INPUT, CodegenEvent, TerminalCodegen};
-use crate::thread_store::ThreadStore;
+use crate::thread_store::{TextThreadStore, ThreadStore};
 use anyhow::{Context as _, Result};
 use client::telemetry::Telemetry;
 use collections::{HashMap, VecDeque};
@@ -71,6 +71,7 @@ impl TerminalInlineAssistant {
         project: WeakEntity<Project>,
         prompt_store: Option<Entity<PromptStore>>,
         thread_store: Option<WeakEntity<ThreadStore>>,
+        text_thread_store: Option<WeakEntity<TextThreadStore>>,
         window: &mut Window,
         cx: &mut App,
     ) {
@@ -91,6 +92,7 @@ impl TerminalInlineAssistant {
                 context_store.clone(),
                 workspace.clone(),
                 thread_store.clone(),
+                text_thread_store.clone(),
                 window,
                 cx,
             )

crates/agent/src/thread_store.rs 🔗

@@ -58,6 +58,8 @@ impl SharedProjectContext {
     }
 }
 
+pub type TextThreadStore = assistant_context_editor::ContextStore;
+
 pub struct ThreadStore {
     project: Entity<Project>,
     tools: Entity<ToolWorkingSet>,
@@ -361,6 +363,10 @@ impl ThreadStore {
         self.threads.len()
     }
 
+    pub fn unordered_threads(&self) -> impl Iterator<Item = &SerializedThreadMetadata> {
+        self.threads.iter()
+    }
+
     pub fn reverse_chronological_threads(&self) -> Vec<SerializedThreadMetadata> {
         let mut threads = self.threads.iter().cloned().collect::<Vec<_>>();
         threads.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.updated_at));

crates/agent/src/ui/context_pill.rs 🔗

@@ -16,7 +16,8 @@ use crate::context::{
     AgentContext, AgentContextHandle, ContextId, ContextKind, DirectoryContext,
     DirectoryContextHandle, FetchedUrlContext, FileContext, FileContextHandle, ImageContext,
     ImageStatus, RulesContext, RulesContextHandle, SelectionContext, SelectionContextHandle,
-    SymbolContext, SymbolContextHandle, ThreadContext, ThreadContextHandle,
+    SymbolContext, SymbolContextHandle, TextThreadContext, TextThreadContextHandle, ThreadContext,
+    ThreadContextHandle,
 };
 
 #[derive(IntoElement)]
@@ -301,6 +302,7 @@ impl AddedContext {
             AgentContextHandle::Selection(handle) => Self::pending_selection(handle, cx),
             AgentContextHandle::FetchedUrl(handle) => Some(Self::fetched_url(handle)),
             AgentContextHandle::Thread(handle) => Some(Self::pending_thread(handle, cx)),
+            AgentContextHandle::TextThread(handle) => Some(Self::pending_text_thread(handle, cx)),
             AgentContextHandle::Rules(handle) => Self::pending_rules(handle, prompt_store, cx),
             AgentContextHandle::Image(handle) => Some(Self::image(handle)),
         }
@@ -314,6 +316,7 @@ impl AddedContext {
             AgentContext::Selection(context) => Self::attached_selection(context, cx),
             AgentContext::FetchedUrl(context) => Self::fetched_url(context.clone()),
             AgentContext::Thread(context) => Self::attached_thread(context),
+            AgentContext::TextThread(context) => Self::attached_text_thread(context),
             AgentContext::Rules(context) => Self::attached_rules(context),
             AgentContext::Image(context) => Self::image(context.clone()),
         }
@@ -520,6 +523,43 @@ impl AddedContext {
         }
     }
 
+    fn pending_text_thread(handle: TextThreadContextHandle, cx: &App) -> AddedContext {
+        AddedContext {
+            kind: ContextKind::TextThread,
+            name: handle.title(cx),
+            parent: None,
+            tooltip: None,
+            icon_path: None,
+            status: ContextStatus::Ready,
+            render_hover: {
+                let context = handle.context.clone();
+                Some(Rc::new(move |_, cx| {
+                    let text = context.read(cx).to_xml(cx);
+                    ContextPillHover::new_text(text.into(), cx).into()
+                }))
+            },
+            handle: AgentContextHandle::TextThread(handle),
+        }
+    }
+
+    fn attached_text_thread(context: &TextThreadContext) -> AddedContext {
+        AddedContext {
+            kind: ContextKind::TextThread,
+            name: context.title.clone(),
+            parent: None,
+            tooltip: None,
+            icon_path: None,
+            status: ContextStatus::Ready,
+            render_hover: {
+                let text = context.text.clone();
+                Some(Rc::new(move |_, cx| {
+                    ContextPillHover::new_text(text.clone(), cx).into()
+                }))
+            },
+            handle: AgentContextHandle::TextThread(context.handle.clone()),
+        }
+    }
+
     fn pending_rules(
         handle: RulesContextHandle,
         prompt_store: Option<&Entity<PromptStore>>,

crates/agent/src/ui/preview/agent_preview.rs 🔗

@@ -6,16 +6,11 @@ use std::sync::OnceLock;
 use ui::{AnyElement, Component, ComponentScope, Window};
 use workspace::Workspace;
 
-use crate::{ActiveThread, ThreadStore};
+use crate::ActiveThread;
 
 /// Function type for creating agent component previews
-pub type PreviewFn = fn(
-    WeakEntity<Workspace>,
-    Entity<ActiveThread>,
-    WeakEntity<ThreadStore>,
-    &mut Window,
-    &mut App,
-) -> Option<AnyElement>;
+pub type PreviewFn =
+    fn(WeakEntity<Workspace>, Entity<ActiveThread>, &mut Window, &mut App) -> Option<AnyElement>;
 
 /// Distributed slice for preview registration functions
 #[distributed_slice]
@@ -32,7 +27,6 @@ pub trait AgentPreview: Component + Sized {
     fn agent_preview(
         workspace: WeakEntity<Workspace>,
         active_thread: Entity<ActiveThread>,
-        thread_store: WeakEntity<ThreadStore>,
         window: &mut Window,
         cx: &mut App,
     ) -> Option<AnyElement>;
@@ -75,14 +69,13 @@ pub fn get_agent_preview(
     id: &ComponentId,
     workspace: WeakEntity<Workspace>,
     active_thread: Entity<ActiveThread>,
-    thread_store: WeakEntity<ThreadStore>,
     window: &mut Window,
     cx: &mut App,
 ) -> Option<AnyElement> {
     let registry = get_or_init_registry();
     registry
         .get(id)
-        .and_then(|preview_fn| preview_fn(workspace, active_thread, thread_store, window, cx))
+        .and_then(|preview_fn| preview_fn(workspace, active_thread, window, cx))
 }
 
 /// Get all registered agent previews.

crates/assistant_context_editor/src/context.rs 🔗

@@ -32,7 +32,7 @@ use serde::{Deserialize, Serialize};
 use smallvec::SmallVec;
 use std::{
     cmp::{Ordering, max},
-    fmt::Debug,
+    fmt::{Debug, Write as _},
     iter, mem,
     ops::Range,
     path::Path,
@@ -2539,6 +2539,26 @@ impl AssistantContext {
         Some(user_message)
     }
 
+    pub fn to_xml(&self, cx: &App) -> String {
+        let mut output = String::new();
+        let buffer = self.buffer.read(cx);
+        for message in self.messages(cx) {
+            if message.status != MessageStatus::Done {
+                continue;
+            }
+
+            writeln!(&mut output, "<{}>", message.role).unwrap();
+            for chunk in buffer.text_for_range(message.offset_range) {
+                output.push_str(chunk);
+            }
+            if !output.ends_with('\n') {
+                output.push('\n');
+            }
+            writeln!(&mut output, "</{}>", message.role).unwrap();
+        }
+        output
+    }
+
     pub fn to_completion_request(
         &self,
         request_type: RequestType,

crates/assistant_context_editor/src/context_store.rs 🔗

@@ -339,7 +339,11 @@ impl ContextStore {
         }
     }
 
-    pub fn contexts(&self) -> Vec<SavedContextMetadata> {
+    pub fn unordered_contexts(&self) -> impl Iterator<Item = &SavedContextMetadata> {
+        self.contexts_metadata.iter()
+    }
+
+    pub fn reverse_chronological_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

crates/component_preview/Cargo.toml 🔗

@@ -21,6 +21,7 @@ client.workspace = true
 collections.workspace = true
 component.workspace = true
 db.workspace = true
+futures.workspace = true
 gpui.workspace = true
 languages.workspace = true
 log.workspace = true
@@ -30,6 +31,7 @@ prompt_store.workspace = true
 serde.workspace = true
 ui.workspace = true
 ui_input.workspace = true
+util.workspace = true
 workspace-hack.workspace = true
 workspace.workspace = true
 assistant_tool.workspace = true

crates/component_preview/src/component_preview.rs 🔗

@@ -8,7 +8,7 @@ mod preview_support;
 use std::iter::Iterator;
 use std::sync::Arc;
 
-use agent::{ActiveThread, ThreadStore};
+use agent::{ActiveThread, TextThreadStore, ThreadStore};
 use client::UserStore;
 use component::{ComponentId, ComponentMetadata, components};
 use gpui::{
@@ -21,11 +21,13 @@ use gpui::{ListState, ScrollHandle, ScrollStrategy, UniformListScrollHandle};
 use languages::LanguageRegistry;
 use notifications::status_toast::{StatusToast, ToastIcon};
 use persistence::COMPONENT_PREVIEW_DB;
-use preview_support::active_thread::{load_preview_thread_store, static_active_thread};
+use preview_support::active_thread::{
+    load_preview_text_thread_store, load_preview_thread_store, static_active_thread,
+};
 use project::Project;
 use ui::{Divider, HighlightedLabel, ListItem, ListSubHeader, prelude::*};
-
 use ui_input::SingleLineInput;
+use util::ResultExt as _;
 use workspace::{AppState, ItemId, SerializableItem, delete_unloaded_items};
 use workspace::{Item, Workspace, WorkspaceId, item::ItemEvent};
 
@@ -120,6 +122,7 @@ struct ComponentPreview {
 
     // preview support
     thread_store: Option<Entity<ThreadStore>>,
+    text_thread_store: Option<Entity<TextThreadStore>>,
     active_thread: Option<Entity<ActiveThread>>,
 }
 
@@ -137,23 +140,29 @@ impl ComponentPreview {
         let workspace_clone = workspace.clone();
         let project_clone = project.clone();
 
-        let entity = cx.weak_entity();
-        window
-            .spawn(cx, async move |cx| {
-                let thread_store_task =
-                    load_preview_thread_store(workspace_clone.clone(), project_clone.clone(), cx)
-                        .await;
-
-                if let Ok(thread_store) = thread_store_task.await {
-                    entity
-                        .update_in(cx, |this, window, cx| {
-                            this.thread_store = Some(thread_store.clone());
-                            this.create_active_thread(window, cx);
-                        })
-                        .ok();
-                }
-            })
-            .detach();
+        cx.spawn_in(window, async move |entity, cx| {
+            let thread_store_future =
+                load_preview_thread_store(workspace_clone.clone(), project_clone.clone(), cx);
+            let text_thread_store_future =
+                load_preview_text_thread_store(workspace_clone.clone(), project_clone.clone(), cx);
+
+            let (thread_store_result, text_thread_store_result) =
+                futures::join!(thread_store_future, text_thread_store_future);
+
+            if let (Some(thread_store), Some(text_thread_store)) = (
+                thread_store_result.log_err(),
+                text_thread_store_result.log_err(),
+            ) {
+                entity
+                    .update_in(cx, |this, window, cx| {
+                        this.thread_store = Some(thread_store.clone());
+                        this.text_thread_store = Some(text_thread_store.clone());
+                        this.create_active_thread(window, cx);
+                    })
+                    .ok();
+            }
+        })
+        .detach();
 
         let sorted_components = components().all_sorted();
         let selected_index = selected_index.into().unwrap_or(0);
@@ -195,6 +204,7 @@ impl ComponentPreview {
             filter_editor,
             filter_text: String::new(),
             thread_store: None,
+            text_thread_store: None,
             active_thread: None,
         };
 
@@ -220,12 +230,17 @@ impl ComponentPreview {
         let weak_handle = self.workspace.clone();
         if let Some(workspace) = workspace.upgrade() {
             let project = workspace.read(cx).project().clone();
-            if let Some(thread_store) = self.thread_store.clone() {
+            if let Some((thread_store, text_thread_store)) = self
+                .thread_store
+                .clone()
+                .zip(self.text_thread_store.clone())
+            {
                 let active_thread = static_active_thread(
                     weak_handle,
                     project,
                     language_registry,
                     thread_store,
+                    text_thread_store,
                     window,
                     cx,
                 );
@@ -625,15 +640,11 @@ impl ComponentPreview {
 
         // Check if the component's scope is Agent
         if scope == ComponentScope::Agent {
-            if let (Some(thread_store), Some(active_thread)) = (
-                self.thread_store.as_ref().map(|ts| ts.downgrade()),
-                self.active_thread.clone(),
-            ) {
+            if let Some(active_thread) = self.active_thread.clone() {
                 if let Some(element) = agent::get_agent_preview(
                     &component.id(),
                     self.workspace.clone(),
                     active_thread,
-                    thread_store,
                     window,
                     cx,
                 ) {
@@ -688,7 +699,6 @@ impl ComponentPreview {
                 .child(ComponentPreviewPage::new(
                     component.clone(),
                     self.workspace.clone(),
-                    self.thread_store.as_ref().map(|ts| ts.downgrade()),
                     self.active_thread.clone(),
                 ))
                 .into_any_element()
@@ -1037,7 +1047,6 @@ pub struct ComponentPreviewPage {
     // languages: Arc<LanguageRegistry>,
     component: ComponentMetadata,
     workspace: WeakEntity<Workspace>,
-    thread_store: Option<WeakEntity<ThreadStore>>,
     active_thread: Option<Entity<ActiveThread>>,
 }
 
@@ -1045,7 +1054,6 @@ impl ComponentPreviewPage {
     pub fn new(
         component: ComponentMetadata,
         workspace: WeakEntity<Workspace>,
-        thread_store: Option<WeakEntity<ThreadStore>>,
         active_thread: Option<Entity<ActiveThread>>,
         // languages: Arc<LanguageRegistry>
     ) -> Self {
@@ -1053,7 +1061,6 @@ impl ComponentPreviewPage {
             // languages,
             component,
             workspace,
-            thread_store,
             active_thread,
         }
     }
@@ -1086,14 +1093,11 @@ impl ComponentPreviewPage {
 
     fn render_preview(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
         // Try to get agent preview first if we have an active thread
-        let maybe_agent_preview = if let (Some(thread_store), Some(active_thread)) =
-            (self.thread_store.as_ref(), self.active_thread.as_ref())
-        {
+        let maybe_agent_preview = if let Some(active_thread) = self.active_thread.as_ref() {
             agent::get_agent_preview(
                 &self.component.id(),
                 self.workspace.clone(),
                 active_thread.clone(),
-                thread_store.clone(),
                 window,
                 cx,
             )

crates/component_preview/src/preview_support/active_thread.rs 🔗

@@ -2,31 +2,47 @@ use languages::LanguageRegistry;
 use project::Project;
 use std::sync::Arc;
 
-use agent::{ActiveThread, ContextStore, MessageSegment, ThreadStore};
+use agent::{ActiveThread, ContextStore, MessageSegment, TextThreadStore, ThreadStore};
+use anyhow::{Result, anyhow};
 use assistant_tool::ToolWorkingSet;
 use gpui::{AppContext, AsyncApp, Entity, Task, WeakEntity};
 use prompt_store::PromptBuilder;
 use ui::{App, Window};
 use workspace::Workspace;
 
-pub async fn load_preview_thread_store(
+pub fn load_preview_thread_store(
     workspace: WeakEntity<Workspace>,
     project: Entity<Project>,
     cx: &mut AsyncApp,
-) -> Task<anyhow::Result<Entity<ThreadStore>>> {
-    cx.spawn(async move |cx| {
-        workspace
-            .update(cx, |_, cx| {
-                ThreadStore::load(
-                    project.clone(),
-                    cx.new(|_| ToolWorkingSet::default()),
-                    None,
-                    Arc::new(PromptBuilder::new(None).unwrap()),
-                    cx,
-                )
-            })?
-            .await
-    })
+) -> Task<Result<Entity<ThreadStore>>> {
+    workspace
+        .update(cx, |_, cx| {
+            ThreadStore::load(
+                project.clone(),
+                cx.new(|_| ToolWorkingSet::default()),
+                None,
+                Arc::new(PromptBuilder::new(None).unwrap()),
+                cx,
+            )
+        })
+        .unwrap_or(Task::ready(Err(anyhow!("workspace dropped"))))
+}
+
+pub fn load_preview_text_thread_store(
+    workspace: WeakEntity<Workspace>,
+    project: Entity<Project>,
+    cx: &mut AsyncApp,
+) -> Task<Result<Entity<TextThreadStore>>> {
+    workspace
+        .update(cx, |_, cx| {
+            TextThreadStore::new(
+                project.clone(),
+                Arc::new(PromptBuilder::new(None).unwrap()),
+                Default::default(),
+                cx,
+            )
+        })
+        .unwrap_or(Task::ready(Err(anyhow!("workspace dropped"))))
 }
 
 pub fn static_active_thread(
@@ -34,6 +50,7 @@ pub fn static_active_thread(
     project: Entity<Project>,
     language_registry: Arc<LanguageRegistry>,
     thread_store: Entity<ThreadStore>,
+    text_thread_store: Entity<TextThreadStore>,
     window: &mut Window,
     cx: &mut App,
 ) -> Entity<ActiveThread> {
@@ -59,6 +76,7 @@ pub fn static_active_thread(
         ActiveThread::new(
             thread,
             thread_store,
+            text_thread_store,
             context_store,
             language_registry,
             workspace.clone(),