Allow attaching text threads as context (#29947)

Release Notes:

- N/A

---------

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
This commit is contained in:
Max Brunsfeld 2025-05-05 13:59:21 -07:00 committed by GitHub
parent 7f868a2eff
commit dd79c29af9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 784 additions and 245 deletions

3
Cargo.lock generated
View file

@ -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",
]

View file

@ -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

View file

@ -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(),

View file

@ -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};

View file

@ -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,42 +862,39 @@ 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| {
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,
fs,
workspace,
project,
self.fs.clone(),
self.workspace.clone(),
self.project.clone(),
lsp_adapter_delegate,
window,
cx,
)
});
this.set_active_view(
ActiveView::prompt_editor(
editor.clone(),
this.language_registry.clone(),
window,
cx,
),
self.set_active_view(
ActiveView::prompt_editor(editor.clone(), self.language_registry.clone(), window, cx),
window,
cx,
);
anyhow::Ok(())
})??;
Ok(())
})
}
pub(crate) fn open_thread_by_id(
@ -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,
)

View file

@ -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),
}

View file

@ -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,13 +458,15 @@ 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")));
};
match entry {
ThreadContextEntry::Thread { id, .. } => {
let Some(thread_store) = self
.thread_store
.as_ref()
@ -462,16 +475,37 @@ impl ContextPicker {
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));
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")));
};
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> {
let Some(workspace) = self.workspace.upgrade() else {
@ -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,9 +1003,16 @@ impl MentionLink {
Some(MentionLink::Selection(project_path, line_range))
}
Self::THREAD => {
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 => {
let prompt_id = UserPromptId(Uuid::try_parse(argument).ok()?);

View file

@ -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,21 +425,22 @@ 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();
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| {
@ -443,6 +458,25 @@ impl ContextPickerCompletionProvider {
.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,
))));

View file

@ -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,11 +159,13 @@ impl PickerDelegate for ThreadContextPickerDelegate {
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(&entry.id, 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?;
@ -143,6 +180,28 @@ impl PickerDelegate for ThreadContextPickerDelegate {
})
.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 = 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();
})
})
.detach_and_log_err(cx);
}
}
}
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.context_picker
@ -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()

View file

@ -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,
})

View file

@ -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,11 +144,9 @@ impl ContextStrip {
}
let workspace = self.workspace.upgrade()?;
let active_thread = workspace
.read(cx)
.panel::<AssistantPanel>(cx)?
.read(cx)
.active_thread(cx);
let panel = workspace.read(cx).panel::<AssistantPanel>(cx)?.read(cx);
if let Some(active_thread) = panel.active_thread() {
let weak_active_thread = active_thread.downgrade();
let active_thread = active_thread.read(cx);
@ -162,6 +163,23 @@ impl ContextStrip {
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,
}
}
}

View file

@ -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));
}

View file

@ -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,
);

View file

@ -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,

View file

@ -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,

View file

@ -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,
)

View file

@ -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));

View file

@ -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>>,

View file

@ -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.

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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,17 +140,23 @@ 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;
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);
if let Ok(thread_store) = thread_store_task.await {
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();
@ -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,
)

View file

@ -2,19 +2,19 @@ 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| {
) -> Task<Result<Entity<ThreadStore>>> {
workspace
.update(cx, |_, cx| {
ThreadStore::load(
@ -24,9 +24,25 @@ pub async fn load_preview_thread_store(
Arc::new(PromptBuilder::new(None).unwrap()),
cx,
)
})?
.await
})
.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(),