Support @-mentions in inline assists and when editing old agent panel messages (#29734)

Closes #ISSUE

Co-authored-by: Bennet <bennet@zed.dev>

Release Notes:

- Added support for context `@mentions` in the inline prompt editor and
when editing past messages in the agent panel.

---------

Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
This commit is contained in:
Cole Miller 2025-05-02 16:08:53 -04:00 committed by GitHub
parent c918f6cde1
commit 9547d42b15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 499 additions and 156 deletions

View file

@ -2,9 +2,10 @@ use crate::context::{AgentContextHandle, RULES_ICON};
use crate::context_picker::{ContextPicker, MentionLink}; use crate::context_picker::{ContextPicker, MentionLink};
use crate::context_store::ContextStore; use crate::context_store::ContextStore;
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind}; use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::message_editor::insert_message_creases;
use crate::thread::{ use crate::thread::{
LastRestoreCheckpoint, MessageId, MessageSegment, Thread, ThreadError, ThreadEvent, LastRestoreCheckpoint, MessageCrease, MessageId, MessageSegment, Thread, ThreadError,
ThreadFeedback, ThreadEvent, ThreadFeedback,
}; };
use crate::thread_store::{RulesLoadingError, ThreadStore}; use crate::thread_store::{RulesLoadingError, ThreadStore};
use crate::tool_use::{PendingToolUseStatus, ToolUse}; use crate::tool_use::{PendingToolUseStatus, ToolUse};
@ -1240,6 +1241,7 @@ impl ActiveThread {
&mut self, &mut self,
message_id: MessageId, message_id: MessageId,
message_segments: &[MessageSegment], message_segments: &[MessageSegment],
message_creases: &[MessageCrease],
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
@ -1258,6 +1260,7 @@ impl ActiveThread {
); );
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
editor.set_text(message_text.clone(), window, cx); editor.set_text(message_text.clone(), window, cx);
insert_message_creases(editor, message_creases, &self.context_store, window, cx);
editor.focus_handle(cx).focus(window); editor.focus_handle(cx).focus(window);
editor.move_to_end(&editor::actions::MoveToEnd, window, cx); editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
}); });
@ -1705,6 +1708,7 @@ impl ActiveThread {
let Some(message) = self.thread.read(cx).message(message_id) else { let Some(message) = self.thread.read(cx).message(message_id) else {
return Empty.into_any(); return Empty.into_any();
}; };
let message_creases = message.creases.clone();
let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else { let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else {
return Empty.into_any(); return Empty.into_any();
@ -1900,6 +1904,7 @@ impl ActiveThread {
open_context(&context, workspace, window, cx); open_context(&context, workspace, window, cx);
cx.notify(); cx.notify();
} }
cx.stop_propagation();
} }
})), })),
) )
@ -1985,15 +1990,13 @@ impl ActiveThread {
) )
}), }),
) )
.when(editing_message_state.is_none(), |this| {
this.tooltip(Tooltip::text("Click To Edit"))
})
.on_click(cx.listener({ .on_click(cx.listener({
let message_segments = message.segments.clone(); let message_segments = message.segments.clone();
move |this, _, window, cx| { move |this, _, window, cx| {
this.start_editing_message( this.start_editing_message(
message_id, message_id,
&message_segments, &message_segments,
&message_creases,
window, window,
cx, cx,
); );
@ -2361,6 +2364,7 @@ impl ActiveThread {
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();
move |text, window, cx| { move |text, window, cx| {
open_markdown_link(text, workspace.clone(), window, cx); open_markdown_link(text, workspace.clone(), window, cx);
cx.stop_propagation();
} }
})) }))
.into_any_element() .into_any_element()

View file

@ -4,10 +4,12 @@ use std::path::PathBuf;
use std::{ops::Range, path::Path, sync::Arc}; use std::{ops::Range, path::Path, sync::Arc};
use assistant_tool::outline; use assistant_tool::outline;
use collections::HashSet; use collections::{HashMap, HashSet};
use editor::display_map::CreaseId;
use editor::{Addon, Editor};
use futures::future; use futures::future;
use futures::{FutureExt, future::Shared}; use futures::{FutureExt, future::Shared};
use gpui::{App, AppContext as _, Entity, SharedString, Task}; use gpui::{App, AppContext as _, Entity, SharedString, Subscription, Task};
use language::{Buffer, ParseStatus}; use language::{Buffer, ParseStatus};
use language_model::{LanguageModelImage, LanguageModelRequestMessage, MessageContent}; use language_model::{LanguageModelImage, LanguageModelRequestMessage, MessageContent};
use project::{Project, ProjectEntryId, ProjectPath, Worktree}; use project::{Project, ProjectEntryId, ProjectPath, Worktree};
@ -15,10 +17,11 @@ use prompt_store::{PromptStore, UserPromptId};
use ref_cast::RefCast; use ref_cast::RefCast;
use rope::Point; use rope::Point;
use text::{Anchor, OffsetRangeExt as _}; use text::{Anchor, OffsetRangeExt as _};
use ui::{ElementId, IconName}; use ui::{Context, ElementId, IconName};
use util::markdown::MarkdownCodeBlock; use util::markdown::MarkdownCodeBlock;
use util::{ResultExt as _, post_inc}; use util::{ResultExt as _, post_inc};
use crate::context_store::{ContextStore, ContextStoreEvent};
use crate::thread::Thread; use crate::thread::Thread;
pub const RULES_ICON: IconName = IconName::Context; pub const RULES_ICON: IconName = IconName::Context;
@ -67,7 +70,7 @@ pub enum AgentContextHandle {
} }
impl AgentContextHandle { impl AgentContextHandle {
fn id(&self) -> ContextId { pub fn id(&self) -> ContextId {
match self { match self {
Self::File(context) => context.context_id, Self::File(context) => context.context_id,
Self::Directory(context) => context.context_id, Self::Directory(context) => context.context_id,
@ -1036,6 +1039,69 @@ impl Hash for AgentContextKey {
} }
} }
#[derive(Default)]
pub struct ContextCreasesAddon {
creases: HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>>,
_subscription: Option<Subscription>,
}
impl Addon for ContextCreasesAddon {
fn to_any(&self) -> &dyn std::any::Any {
self
}
fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
Some(self)
}
}
impl ContextCreasesAddon {
pub fn new() -> Self {
Self {
creases: HashMap::default(),
_subscription: None,
}
}
pub fn add_creases(
&mut self,
context_store: &Entity<ContextStore>,
key: AgentContextKey,
creases: impl IntoIterator<Item = (CreaseId, SharedString)>,
cx: &mut Context<Editor>,
) {
self.creases.entry(key).or_default().extend(creases);
self._subscription = Some(cx.subscribe(
&context_store,
|editor, _, event, cx| match event {
ContextStoreEvent::ContextRemoved(key) => {
let Some(this) = editor.addon_mut::<Self>() else {
return;
};
let (crease_ids, replacement_texts): (Vec<_>, Vec<_>) = this
.creases
.remove(key)
.unwrap_or_default()
.into_iter()
.unzip();
let ranges = editor
.remove_creases(crease_ids, cx)
.into_iter()
.map(|(_, range)| range)
.collect::<Vec<_>>();
editor.unfold_ranges(&ranges, false, false, cx);
editor.edit(ranges.into_iter().zip(replacement_texts), cx);
cx.notify();
}
},
))
}
pub fn into_inner(self) -> HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>> {
self.creases
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -11,7 +11,7 @@ use std::sync::Arc;
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
pub use completion_provider::ContextPickerCompletionProvider; pub use completion_provider::ContextPickerCompletionProvider;
use editor::display_map::{Crease, FoldId}; use editor::display_map::{Crease, CreaseId, CreaseMetadata, FoldId};
use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset}; use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};
use fetch_context_picker::FetchContextPicker; use fetch_context_picker::FetchContextPicker;
use file_context_picker::FileContextPicker; use file_context_picker::FileContextPicker;
@ -675,21 +675,20 @@ fn selection_ranges(
}) })
} }
pub(crate) fn insert_fold_for_mention( pub(crate) fn insert_crease_for_mention(
excerpt_id: ExcerptId, excerpt_id: ExcerptId,
crease_start: text::Anchor, crease_start: text::Anchor,
content_len: usize, content_len: usize,
crease_label: SharedString, crease_label: SharedString,
crease_icon_path: SharedString, crease_icon_path: SharedString,
editor_entity: Entity<Editor>, editor_entity: Entity<Editor>,
window: &mut Window,
cx: &mut App, cx: &mut App,
) { ) -> Option<CreaseId> {
editor_entity.update(cx, |editor, cx| { editor_entity.update(cx, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx); let snapshot = editor.buffer().read(cx).snapshot(cx);
let Some(start) = snapshot.anchor_in_excerpt(excerpt_id, crease_start) else { let start = snapshot.anchor_in_excerpt(excerpt_id, crease_start)?;
return;
};
let start = start.bias_right(&snapshot); let start = start.bias_right(&snapshot);
let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len); let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
@ -701,10 +700,10 @@ pub(crate) fn insert_fold_for_mention(
editor_entity.downgrade(), editor_entity.downgrade(),
); );
editor.display_map.update(cx, |display_map, cx| { let ids = editor.insert_creases(vec![crease.clone()], cx);
display_map.fold(vec![crease], cx); editor.fold_creases(vec![crease], false, window, cx);
}); Some(ids[0])
}); })
} }
pub fn crease_for_mention( pub fn crease_for_mention(
@ -714,20 +713,20 @@ pub fn crease_for_mention(
editor_entity: WeakEntity<Editor>, editor_entity: WeakEntity<Editor>,
) -> Crease<Anchor> { ) -> Crease<Anchor> {
let placeholder = FoldPlaceholder { let placeholder = FoldPlaceholder {
render: render_fold_icon_button(icon_path, label, editor_entity), render: render_fold_icon_button(icon_path.clone(), label.clone(), editor_entity),
merge_adjacent: false, merge_adjacent: false,
..Default::default() ..Default::default()
}; };
let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any(); let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
let crease = Crease::inline( Crease::inline(
range, range,
placeholder.clone(), placeholder.clone(),
fold_toggle("mention"), fold_toggle("mention"),
render_trailer, render_trailer,
); )
crease .with_metadata(CreaseMetadata { icon_path, label })
} }
fn render_fold_icon_button( fn render_fold_icon_button(

View file

@ -19,9 +19,11 @@ use prompt_store::PromptStore;
use rope::Point; use rope::Point;
use text::{Anchor, OffsetRangeExt, ToPoint}; use text::{Anchor, OffsetRangeExt, ToPoint};
use ui::prelude::*; use ui::prelude::*;
use util::ResultExt as _;
use workspace::Workspace; use workspace::Workspace;
use crate::context::RULES_ICON; use crate::Thread;
use crate::context::{AgentContextHandle, AgentContextKey, ContextCreasesAddon, RULES_ICON};
use crate::context_store::ContextStore; use crate::context_store::ContextStore;
use crate::thread_store::ThreadStore; use crate::thread_store::ThreadStore;
@ -310,7 +312,7 @@ impl ContextPickerCompletionProvider {
let context_store = context_store.clone(); let context_store = context_store.clone();
let selections = selections.clone(); let selections = selections.clone();
let selection_infos = selection_infos.clone(); let selection_infos = selection_infos.clone();
move |_, _: &mut Window, cx: &mut App| { move |_, window: &mut Window, cx: &mut App| {
context_store.update(cx, |context_store, cx| { context_store.update(cx, |context_store, cx| {
for (buffer, range) in &selections { for (buffer, range) in &selections {
context_store.add_selection( context_store.add_selection(
@ -323,7 +325,7 @@ impl ContextPickerCompletionProvider {
let editor = editor.clone(); let editor = editor.clone();
let selection_infos = selection_infos.clone(); let selection_infos = selection_infos.clone();
cx.defer(move |cx| { window.defer(cx, move |window, cx| {
let mut current_offset = 0; let mut current_offset = 0;
for (file_name, link, line_range) in selection_infos.iter() { for (file_name, link, line_range) in selection_infos.iter() {
let snapshot = let snapshot =
@ -354,9 +356,8 @@ impl ContextPickerCompletionProvider {
); );
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
editor.display_map.update(cx, |display_map, cx| { editor.insert_creases(vec![crease.clone()], cx);
display_map.fold(vec![crease], cx); editor.fold_creases(vec![crease], false, window, cx);
});
}); });
current_offset += text_len + 1; current_offset += text_len + 1;
@ -419,21 +420,26 @@ impl ContextPickerCompletionProvider {
source_range.start, source_range.start,
new_text_len, new_text_len,
editor.clone(), editor.clone(),
context_store.clone(),
move |cx| { move |cx| {
let thread_id = thread_entry.id.clone(); let thread_id = thread_entry.id.clone();
let context_store = context_store.clone(); let context_store = context_store.clone();
let thread_store = thread_store.clone(); let thread_store = thread_store.clone();
cx.spawn(async move |cx| { cx.spawn::<_, Option<_>>(async move |cx| {
let thread = thread_store let thread: Entity<Thread> = thread_store
.update(cx, |thread_store, cx| { .update(cx, |thread_store, cx| {
thread_store.open_thread(&thread_id, cx) thread_store.open_thread(&thread_id, cx)
})? })
.await?; .ok()?
context_store.update(cx, |context_store, cx| { .await
context_store.add_thread(thread, false, cx) .log_err()?;
}) let context = context_store
.update(cx, |context_store, cx| {
context_store.add_thread(thread, false, cx)
})
.ok()??;
Some(context)
}) })
.detach_and_log_err(cx);
}, },
)), )),
} }
@ -463,11 +469,13 @@ impl ContextPickerCompletionProvider {
source_range.start, source_range.start,
new_text_len, new_text_len,
editor.clone(), editor.clone(),
context_store.clone(),
move |cx| { move |cx| {
let user_prompt_id = rules.prompt_id; let user_prompt_id = rules.prompt_id;
context_store.update(cx, |context_store, cx| { let context = context_store.update(cx, |context_store, cx| {
context_store.add_rules(user_prompt_id, false, cx); context_store.add_rules(user_prompt_id, false, cx)
}); });
Task::ready(context)
}, },
)), )),
} }
@ -498,27 +506,33 @@ impl ContextPickerCompletionProvider {
source_range.start, source_range.start,
new_text_len, new_text_len,
editor.clone(), editor.clone(),
context_store.clone(),
move |cx| { move |cx| {
let context_store = context_store.clone(); let context_store = context_store.clone();
let http_client = http_client.clone(); let http_client = http_client.clone();
let url_to_fetch = url_to_fetch.clone(); let url_to_fetch = url_to_fetch.clone();
cx.spawn(async move |cx| { cx.spawn(async move |cx| {
if context_store.update(cx, |context_store, _| { if let Some(context) = context_store
context_store.includes_url(&url_to_fetch) .update(cx, |context_store, _| {
})? { context_store.get_url_context(url_to_fetch.clone())
return Ok(()); })
.ok()?
{
return Some(context);
} }
let content = cx let content = cx
.background_spawn(fetch_url_content( .background_spawn(fetch_url_content(
http_client, http_client,
url_to_fetch.to_string(), url_to_fetch.to_string(),
)) ))
.await?; .await
context_store.update(cx, |context_store, cx| { .log_err()?;
context_store.add_fetched_url(url_to_fetch.to_string(), content, cx) context_store
}) .update(cx, |context_store, cx| {
context_store.add_fetched_url(url_to_fetch.to_string(), content, cx)
})
.ok()
}) })
.detach_and_log_err(cx);
}, },
)), )),
} }
@ -577,15 +591,23 @@ impl ContextPickerCompletionProvider {
source_range.start, source_range.start,
new_text_len, new_text_len,
editor, editor,
context_store.clone(),
move |cx| { move |cx| {
context_store.update(cx, |context_store, cx| { if is_directory {
let task = if is_directory { Task::ready(
Task::ready(context_store.add_directory(&project_path, false, cx)) context_store
} else { .update(cx, |context_store, cx| {
context_store.add_directory(&project_path, false, cx)
})
.log_err()
.flatten(),
)
} else {
let result = context_store.update(cx, |context_store, cx| {
context_store.add_file_from_path(project_path.clone(), false, cx) context_store.add_file_from_path(project_path.clone(), false, cx)
}; });
task.detach_and_log_err(cx); cx.spawn(async move |_| result.await.log_err().flatten())
}) }
}, },
)), )),
} }
@ -640,18 +662,19 @@ impl ContextPickerCompletionProvider {
source_range.start, source_range.start,
new_text_len, new_text_len,
editor.clone(), editor.clone(),
context_store.clone(),
move |cx| { move |cx| {
let symbol = symbol.clone(); let symbol = symbol.clone();
let context_store = context_store.clone(); let context_store = context_store.clone();
let workspace = workspace.clone(); let workspace = workspace.clone();
super::symbol_context_picker::add_symbol( let result = super::symbol_context_picker::add_symbol(
symbol.clone(), symbol.clone(),
false, false,
workspace.clone(), workspace.clone(),
context_store.downgrade(), context_store.downgrade(),
cx, cx,
) );
.detach_and_log_err(cx); cx.spawn(async move |_| result.await.log_err()?.0)
}, },
)), )),
}) })
@ -873,24 +896,44 @@ fn confirm_completion_callback(
start: Anchor, start: Anchor,
content_len: usize, content_len: usize,
editor: Entity<Editor>, editor: Entity<Editor>,
add_context_fn: impl Fn(&mut App) -> () + Send + Sync + 'static, context_store: Entity<ContextStore>,
add_context_fn: impl Fn(&mut App) -> Task<Option<AgentContextHandle>> + Send + Sync + 'static,
) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> { ) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
Arc::new(move |_, _, cx| { Arc::new(move |_, window, cx| {
add_context_fn(cx); let context = add_context_fn(cx);
let crease_text = crease_text.clone(); let crease_text = crease_text.clone();
let crease_icon_path = crease_icon_path.clone(); let crease_icon_path = crease_icon_path.clone();
let editor = editor.clone(); let editor = editor.clone();
cx.defer(move |cx| { let context_store = context_store.clone();
crate::context_picker::insert_fold_for_mention( window.defer(cx, move |window, cx| {
let crease_id = crate::context_picker::insert_crease_for_mention(
excerpt_id, excerpt_id,
start, start,
content_len, content_len,
crease_text, crease_text.clone(),
crease_icon_path, crease_icon_path,
editor, editor.clone(),
window,
cx, cx,
); );
cx.spawn(async move |cx| {
let crease_id = crease_id?;
let context = context.await?;
editor
.update(cx, |editor, cx| {
if let Some(addon) = editor.addon_mut::<ContextCreasesAddon>() {
addon.add_creases(
&context_store,
AgentContextKey(context),
[(crease_id, crease_text)],
cx,
);
}
})
.ok()
})
.detach();
}); });
false false
}) })

View file

@ -130,21 +130,19 @@ impl PickerDelegate for FileContextPickerDelegate {
let is_directory = mat.is_dir; let is_directory = mat.is_dir;
let Some(task) = self self.context_store
.context_store
.update(cx, |context_store, cx| { .update(cx, |context_store, cx| {
if is_directory { if is_directory {
Task::ready(context_store.add_directory(&project_path, true, cx)) context_store
.add_directory(&project_path, true, cx)
.log_err();
} else { } else {
context_store.add_file_from_path(project_path.clone(), true, cx) context_store
.add_file_from_path(project_path.clone(), true, cx)
.detach_and_log_err(cx);
} }
}) })
.ok() .ok();
else {
return;
};
task.detach_and_log_err(cx);
} }
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) { fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {

View file

@ -14,6 +14,7 @@ use ui::{ListItem, prelude::*};
use util::ResultExt as _; use util::ResultExt as _;
use workspace::Workspace; use workspace::Workspace;
use crate::context::AgentContextHandle;
use crate::context_picker::ContextPicker; use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore; use crate::context_store::ContextStore;
@ -143,7 +144,7 @@ impl PickerDelegate for SymbolContextPickerDelegate {
let selected_index = self.selected_index; let selected_index = self.selected_index;
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
let included = add_symbol_task.await?; let (_, included) = add_symbol_task.await?;
this.update(cx, |this, _| { this.update(cx, |this, _| {
if let Some(mat) = this.delegate.matches.get_mut(selected_index) { if let Some(mat) = this.delegate.matches.get_mut(selected_index) {
mat.is_included = included; mat.is_included = included;
@ -187,7 +188,7 @@ pub(crate) fn add_symbol(
workspace: Entity<Workspace>, workspace: Entity<Workspace>,
context_store: WeakEntity<ContextStore>, context_store: WeakEntity<ContextStore>,
cx: &mut App, cx: &mut App,
) -> Task<Result<bool>> { ) -> Task<Result<(Option<AgentContextHandle>, bool)>> {
let project = workspace.read(cx).project().clone(); let project = workspace.read(cx).project().clone();
let open_buffer_task = project.update(cx, |project, cx| { let open_buffer_task = project.update(cx, |project, cx| {
project.open_buffer(symbol.path.clone(), cx) project.open_buffer(symbol.path.clone(), cx)

View file

@ -5,7 +5,7 @@ use std::sync::Arc;
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use collections::{HashSet, IndexSet}; use collections::{HashSet, IndexSet};
use futures::{self, FutureExt}; use futures::{self, FutureExt};
use gpui::{App, Context, Entity, Image, SharedString, Task, WeakEntity}; use gpui::{App, Context, Entity, EventEmitter, Image, SharedString, Task, WeakEntity};
use language::Buffer; use language::Buffer;
use language_model::LanguageModelImage; use language_model::LanguageModelImage;
use project::image_store::is_image_file; use project::image_store::is_image_file;
@ -31,6 +31,12 @@ pub struct ContextStore {
context_thread_ids: HashSet<ThreadId>, context_thread_ids: HashSet<ThreadId>,
} }
pub enum ContextStoreEvent {
ContextRemoved(AgentContextKey),
}
impl EventEmitter<ContextStoreEvent> for ContextStore {}
impl ContextStore { impl ContextStore {
pub fn new( pub fn new(
project: WeakEntity<Project>, project: WeakEntity<Project>,
@ -82,7 +88,7 @@ impl ContextStore {
project_path: ProjectPath, project_path: ProjectPath,
remove_if_exists: bool, remove_if_exists: bool,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<()>> { ) -> Task<Result<Option<AgentContextHandle>>> {
let Some(project) = self.project.upgrade() else { let Some(project) = self.project.upgrade() else {
return Task::ready(Err(anyhow!("failed to read project"))); return Task::ready(Err(anyhow!("failed to read project")));
}; };
@ -108,21 +114,22 @@ impl ContextStore {
buffer: Entity<Buffer>, buffer: Entity<Buffer>,
remove_if_exists: bool, remove_if_exists: bool,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) -> Option<AgentContextHandle> {
let context_id = self.next_context_id.post_inc(); let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::File(FileContextHandle { buffer, context_id }); let context = AgentContextHandle::File(FileContextHandle { buffer, context_id });
let already_included = if self.has_context(&context) { if let Some(key) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
if remove_if_exists { if remove_if_exists {
self.remove_context(&context, cx); self.remove_context(&context, cx);
None
} else {
Some(key.as_ref().clone())
} }
true } else if self.path_included_in_directory(project_path, cx).is_some() {
None
} else { } else {
self.path_included_in_directory(project_path, cx).is_some() self.insert_context(context.clone(), cx);
}; Some(context)
if !already_included {
self.insert_context(context, cx);
} }
} }
@ -131,7 +138,7 @@ impl ContextStore {
project_path: &ProjectPath, project_path: &ProjectPath,
remove_if_exists: bool, remove_if_exists: bool,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Result<()> { ) -> Result<Option<AgentContextHandle>> {
let Some(project) = self.project.upgrade() else { let Some(project) = self.project.upgrade() else {
return Err(anyhow!("failed to read project")); return Err(anyhow!("failed to read project"));
}; };
@ -150,15 +157,20 @@ impl ContextStore {
context_id, context_id,
}); });
if self.has_context(&context) { let context =
if remove_if_exists { if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
self.remove_context(&context, cx); if remove_if_exists {
} self.remove_context(&context, cx);
} else if self.path_included_in_directory(project_path, cx).is_none() { None
self.insert_context(context, cx); } else {
} Some(existing.as_ref().clone())
}
} else {
self.insert_context(context.clone(), cx);
Some(context)
};
anyhow::Ok(()) anyhow::Ok(context)
} }
pub fn add_symbol( pub fn add_symbol(
@ -169,7 +181,7 @@ impl ContextStore {
enclosing_range: Range<Anchor>, enclosing_range: Range<Anchor>,
remove_if_exists: bool, remove_if_exists: bool,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> bool { ) -> (Option<AgentContextHandle>, bool) {
let context_id = self.next_context_id.post_inc(); let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::Symbol(SymbolContextHandle { let context = AgentContextHandle::Symbol(SymbolContextHandle {
buffer, buffer,
@ -179,14 +191,18 @@ impl ContextStore {
context_id, context_id,
}); });
if self.has_context(&context) { if let Some(key) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
if remove_if_exists { let handle = if remove_if_exists {
self.remove_context(&context, cx); self.remove_context(&context, cx);
} None
return false; } else {
Some(key.as_ref().clone())
};
return (handle, false);
} }
self.insert_context(context, cx) let included = self.insert_context(context.clone(), cx);
(Some(context), included)
} }
pub fn add_thread( pub fn add_thread(
@ -194,16 +210,20 @@ impl ContextStore {
thread: Entity<Thread>, thread: Entity<Thread>,
remove_if_exists: bool, remove_if_exists: bool,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) -> Option<AgentContextHandle> {
let context_id = self.next_context_id.post_inc(); let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::Thread(ThreadContextHandle { thread, context_id }); let context = AgentContextHandle::Thread(ThreadContextHandle { thread, context_id });
if self.has_context(&context) { if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
if remove_if_exists { if remove_if_exists {
self.remove_context(&context, cx); self.remove_context(&context, cx);
None
} else {
Some(existing.as_ref().clone())
} }
} else { } else {
self.insert_context(context, cx); self.insert_context(context.clone(), cx);
Some(context)
} }
} }
@ -212,19 +232,23 @@ impl ContextStore {
prompt_id: UserPromptId, prompt_id: UserPromptId,
remove_if_exists: bool, remove_if_exists: bool,
cx: &mut Context<ContextStore>, cx: &mut Context<ContextStore>,
) { ) -> Option<AgentContextHandle> {
let context_id = self.next_context_id.post_inc(); let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::Rules(RulesContextHandle { let context = AgentContextHandle::Rules(RulesContextHandle {
prompt_id, prompt_id,
context_id, context_id,
}); });
if self.has_context(&context) { if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
if remove_if_exists { if remove_if_exists {
self.remove_context(&context, cx); self.remove_context(&context, cx);
None
} else {
Some(existing.as_ref().clone())
} }
} else { } else {
self.insert_context(context, cx); self.insert_context(context.clone(), cx);
Some(context)
} }
} }
@ -233,14 +257,15 @@ impl ContextStore {
url: String, url: String,
text: impl Into<SharedString>, text: impl Into<SharedString>,
cx: &mut Context<ContextStore>, cx: &mut Context<ContextStore>,
) { ) -> AgentContextHandle {
let context = AgentContextHandle::FetchedUrl(FetchedUrlContext { let context = AgentContextHandle::FetchedUrl(FetchedUrlContext {
url: url.into(), url: url.into(),
text: text.into(), text: text.into(),
context_id: self.next_context_id.post_inc(), context_id: self.next_context_id.post_inc(),
}); });
self.insert_context(context, cx); self.insert_context(context.clone(), cx);
context
} }
pub fn add_image_from_path( pub fn add_image_from_path(
@ -248,7 +273,7 @@ impl ContextStore {
project_path: ProjectPath, project_path: ProjectPath,
remove_if_exists: bool, remove_if_exists: bool,
cx: &mut Context<ContextStore>, cx: &mut Context<ContextStore>,
) -> Task<Result<()>> { ) -> Task<Result<Option<AgentContextHandle>>> {
let project = self.project.clone(); let project = self.project.clone();
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
let open_image_task = project.update(cx, |project, cx| { let open_image_task = project.update(cx, |project, cx| {
@ -262,7 +287,7 @@ impl ContextStore {
image, image,
remove_if_exists, remove_if_exists,
cx, cx,
); )
}) })
}) })
} }
@ -277,7 +302,7 @@ impl ContextStore {
image: Arc<Image>, image: Arc<Image>,
remove_if_exists: bool, remove_if_exists: bool,
cx: &mut Context<ContextStore>, cx: &mut Context<ContextStore>,
) { ) -> Option<AgentContextHandle> {
let image_task = LanguageModelImage::from_image(image.clone(), cx).shared(); let image_task = LanguageModelImage::from_image(image.clone(), cx).shared();
let context = AgentContextHandle::Image(ImageContext { let context = AgentContextHandle::Image(ImageContext {
project_path, project_path,
@ -288,11 +313,12 @@ impl ContextStore {
if self.has_context(&context) { if self.has_context(&context) {
if remove_if_exists { if remove_if_exists {
self.remove_context(&context, cx); self.remove_context(&context, cx);
return; return None;
} }
} }
self.insert_context(context, cx); self.insert_context(context.clone(), cx);
Some(context)
} }
pub fn add_selection( pub fn add_selection(
@ -364,9 +390,9 @@ impl ContextStore {
} }
pub fn remove_context(&mut self, context: &AgentContextHandle, cx: &mut Context<Self>) { pub fn remove_context(&mut self, context: &AgentContextHandle, cx: &mut Context<Self>) {
if self if let Some((_, key)) = self
.context_set .context_set
.shift_remove(AgentContextKey::ref_cast(context)) .shift_remove_full(AgentContextKey::ref_cast(context))
{ {
match context { match context {
AgentContextHandle::Thread(thread_context) => { AgentContextHandle::Thread(thread_context) => {
@ -375,6 +401,7 @@ impl ContextStore {
} }
_ => {} _ => {}
} }
cx.emit(ContextStoreEvent::ContextRemoved(key));
cx.notify(); cx.notify();
} }
} }
@ -451,6 +478,12 @@ impl ContextStore {
.contains(&FetchedUrlContext::lookup_key(url.into())) .contains(&FetchedUrlContext::lookup_key(url.into()))
} }
pub fn get_url_context(&self, url: SharedString) -> Option<AgentContextHandle> {
self.context_set
.get(&FetchedUrlContext::lookup_key(url))
.map(|key| key.as_ref().clone())
}
pub fn file_paths(&self, cx: &App) -> HashSet<ProjectPath> { pub fn file_paths(&self, cx: &App) -> HashSet<ProjectPath> {
self.context() self.context()
.filter_map(|context| match context { .filter_map(|context| match context {

View file

@ -1199,6 +1199,7 @@ impl InlineAssistant {
) -> Vec<InlineAssistId> { ) -> Vec<InlineAssistId> {
let assist_group = self.assist_groups.get_mut(&assist_group_id).unwrap(); let assist_group = self.assist_groups.get_mut(&assist_group_id).unwrap();
assist_group.linked = false; assist_group.linked = false;
for assist_id in &assist_group.assist_ids { for assist_id in &assist_group.assist_ids {
let assist = self.assists.get_mut(assist_id).unwrap(); let assist = self.assists.get_mut(assist_id).unwrap();
if let Some(editor_decorations) = assist.decorations.as_ref() { if let Some(editor_decorations) = assist.decorations.as_ref() {

View file

@ -1,8 +1,10 @@
use crate::assistant_model_selector::{AssistantModelSelector, ModelType}; use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
use crate::buffer_codegen::BufferCodegen; use crate::buffer_codegen::BufferCodegen;
use crate::context_picker::ContextPicker; use crate::context::ContextCreasesAddon;
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
use crate::context_store::ContextStore; use crate::context_store::ContextStore;
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind}; use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::message_editor::{extract_message_creases, insert_message_creases};
use crate::terminal_codegen::TerminalCodegen; use crate::terminal_codegen::TerminalCodegen;
use crate::thread_store::ThreadStore; use crate::thread_store::ThreadStore;
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist}; use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
@ -245,13 +247,22 @@ impl<T: 'static> PromptEditor<T> {
pub fn unlink(&mut self, window: &mut Window, cx: &mut Context<Self>) { pub fn unlink(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let prompt = self.prompt(cx); let prompt = self.prompt(cx);
let existing_creases = self.editor.update(cx, extract_message_creases);
let focus = self.editor.focus_handle(cx).contains_focused(window, cx); let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
self.editor = cx.new(|cx| { self.editor = cx.new(|cx| {
let mut editor = Editor::auto_height(Self::MAX_LINES as usize, window, cx); let mut editor = Editor::auto_height(Self::MAX_LINES as usize, window, cx);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
editor.set_placeholder_text(Self::placeholder_text(&self.mode, window, cx), cx);
editor.set_placeholder_text("Add a prompt…", cx); editor.set_placeholder_text("Add a prompt…", cx);
editor.set_text(prompt, window, cx); editor.set_text(prompt, window, cx);
insert_message_creases(
&mut editor,
&existing_creases,
&self.context_store,
window,
cx,
);
if focus { if focus {
window.focus(&editor.focus_handle(cx)); window.focus(&editor.focus_handle(cx));
} }
@ -860,8 +871,19 @@ impl PromptEditor<BufferCodegen> {
// typing in one will make what you typed appear in all of them. // typing in one will make what you typed appear in all of them.
editor.set_show_cursor_when_unfocused(true, cx); editor.set_show_cursor_when_unfocused(true, cx);
editor.set_placeholder_text(Self::placeholder_text(&mode, window, cx), cx); editor.set_placeholder_text(Self::placeholder_text(&mode, window, cx), cx);
editor.register_addon(ContextCreasesAddon::new());
editor editor
}); });
let prompt_editor_entity = prompt_editor.downgrade();
prompt_editor.update(cx, |editor, _| {
editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
workspace.clone(),
context_store.downgrade(),
thread_store.clone(),
prompt_editor_entity,
))));
});
let context_picker_menu_handle = PopoverMenuHandle::default(); let context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default(); let model_selector_menu_handle = PopoverMenuHandle::default();
@ -1015,6 +1037,17 @@ impl PromptEditor<TerminalCodegen> {
editor.set_placeholder_text(Self::placeholder_text(&mode, window, cx), cx); editor.set_placeholder_text(Self::placeholder_text(&mode, window, cx), cx);
editor editor
}); });
let prompt_editor_entity = prompt_editor.downgrade();
prompt_editor.update(cx, |editor, _| {
editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
workspace.clone(),
context_store.downgrade(),
thread_store.clone(),
prompt_editor_entity,
))));
});
let context_picker_menu_handle = PopoverMenuHandle::default(); let context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default(); let model_selector_menu_handle = PopoverMenuHandle::default();

View file

@ -2,15 +2,15 @@ use std::collections::BTreeMap;
use std::sync::Arc; use std::sync::Arc;
use crate::assistant_model_selector::{AssistantModelSelector, ModelType}; use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
use crate::context::{ContextLoadResult, load_context}; use crate::context::{AgentContextKey, ContextCreasesAddon, ContextLoadResult, load_context};
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip}; use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
use crate::ui::{AgentPreview, AnimatedLabel}; use crate::ui::{AgentPreview, AnimatedLabel};
use buffer_diff::BufferDiff; use buffer_diff::BufferDiff;
use collections::HashSet; use collections::{HashMap, HashSet};
use editor::actions::{MoveUp, Paste}; use editor::actions::{MoveUp, Paste};
use editor::{ use editor::{
ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorEvent, EditorMode, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorEvent,
EditorStyle, MultiBuffer, EditorMode, EditorStyle, MultiBuffer,
}; };
use feature_flags::{FeatureFlagAppExt, NewBillingFeatureFlag}; use feature_flags::{FeatureFlagAppExt, NewBillingFeatureFlag};
use file_icons::FileIcons; use file_icons::FileIcons;
@ -35,11 +35,11 @@ use util::ResultExt as _;
use workspace::Workspace; use workspace::Workspace;
use zed_llm_client::CompletionMode; use zed_llm_client::CompletionMode;
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider}; use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
use crate::context_store::ContextStore; use crate::context_store::ContextStore;
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind}; use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::profile_selector::ProfileSelector; use crate::profile_selector::ProfileSelector;
use crate::thread::{Thread, TokenUsageRatio}; use crate::thread::{MessageCrease, Thread, TokenUsageRatio};
use crate::thread_store::ThreadStore; use crate::thread_store::ThreadStore;
use crate::{ use crate::{
ActiveThread, AgentDiff, Chat, ExpandMessageEditor, NewThread, OpenAgentDiff, RemoveAllContext, ActiveThread, AgentDiff, Chat, ExpandMessageEditor, NewThread, OpenAgentDiff, RemoveAllContext,
@ -105,6 +105,7 @@ pub(crate) fn create_editor(
max_entries_visible: 12, max_entries_visible: 12,
placement: Some(ContextMenuPlacement::Above), placement: Some(ContextMenuPlacement::Above),
}); });
editor.register_addon(ContextCreasesAddon::new());
editor editor
}); });
@ -290,10 +291,11 @@ impl MessageEditor {
return; return;
} }
let user_message = self.editor.update(cx, |editor, cx| { let (user_message, user_message_creases) = self.editor.update(cx, |editor, cx| {
let creases = extract_message_creases(editor, cx);
let text = editor.text(cx); let text = editor.text(cx);
editor.clear(window, cx); editor.clear(window, cx);
text (text, creases)
}); });
self.last_estimated_token_count.take(); self.last_estimated_token_count.take();
@ -311,7 +313,13 @@ impl MessageEditor {
thread thread
.update(cx, |thread, cx| { .update(cx, |thread, cx| {
thread.insert_user_message(user_message, loaded_context, checkpoint.ok(), cx); thread.insert_user_message(
user_message,
loaded_context,
checkpoint.ok(),
user_message_creases,
cx,
);
}) })
.log_err(); .log_err();
@ -1164,6 +1172,53 @@ impl MessageEditor {
} }
} }
pub fn extract_message_creases(
editor: &mut Editor,
cx: &mut Context<'_, Editor>,
) -> Vec<MessageCrease> {
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let mut contexts_by_crease_id = editor
.addon_mut::<ContextCreasesAddon>()
.map(std::mem::take)
.unwrap_or_default()
.into_inner()
.into_iter()
.flat_map(|(key, creases)| {
let context = key.0;
creases
.into_iter()
.map(move |(id, _)| (id, context.clone()))
})
.collect::<HashMap<_, _>>();
// Filter the addon's list of creases based on what the editor reports,
// since the addon might have removed creases in it.
let creases = editor.display_map.update(cx, |display_map, cx| {
display_map
.snapshot(cx)
.crease_snapshot
.creases()
.filter_map(|(id, crease)| {
Some((
id,
(
crease.range().to_offset(&buffer_snapshot),
crease.metadata()?.clone(),
),
))
})
.map(|(id, (range, metadata))| {
let context = contexts_by_crease_id.remove(&id);
MessageCrease {
range,
metadata,
context,
}
})
.collect()
});
creases
}
impl EventEmitter<MessageEditorEvent> for MessageEditor {} impl EventEmitter<MessageEditorEvent> for MessageEditor {}
pub enum MessageEditorEvent { pub enum MessageEditorEvent {
@ -1204,6 +1259,43 @@ impl Render for MessageEditor {
} }
} }
pub fn insert_message_creases(
editor: &mut Editor,
message_creases: &[MessageCrease],
context_store: &Entity<ContextStore>,
window: &mut Window,
cx: &mut Context<'_, Editor>,
) {
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let creases = message_creases
.iter()
.map(|crease| {
let start = buffer_snapshot.anchor_after(crease.range.start);
let end = buffer_snapshot.anchor_before(crease.range.end);
crease_for_mention(
crease.metadata.label.clone(),
crease.metadata.icon_path.clone(),
start..end,
cx.weak_entity(),
)
})
.collect::<Vec<_>>();
let ids = editor.insert_creases(creases.clone(), cx);
editor.fold_creases(creases, false, window, cx);
if let Some(addon) = editor.addon_mut::<ContextCreasesAddon>() {
for (crease, id) in message_creases.iter().zip(ids) {
if let Some(context) = crease.context.as_ref() {
let key = AgentContextKey(context.clone());
addon.add_creases(
context_store,
key,
vec![(id, crease.metadata.label.clone())],
cx,
);
}
}
}
}
impl Component for MessageEditor { impl Component for MessageEditor {
fn scope() -> ComponentScope { fn scope() -> ComponentScope {
ComponentScope::Agent ComponentScope::Agent

View file

@ -9,6 +9,7 @@ use assistant_settings::AssistantSettings;
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet}; use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use collections::HashMap; use collections::HashMap;
use editor::display_map::CreaseMetadata;
use feature_flags::{self, FeatureFlagAppExt}; use feature_flags::{self, FeatureFlagAppExt};
use futures::future::Shared; use futures::future::Shared;
use futures::{FutureExt, StreamExt as _}; use futures::{FutureExt, StreamExt as _};
@ -39,10 +40,10 @@ use uuid::Uuid;
use zed_llm_client::CompletionMode; use zed_llm_client::CompletionMode;
use crate::ThreadStore; use crate::ThreadStore;
use crate::context::{AgentContext, ContextLoadResult, LoadedContext}; use crate::context::{AgentContext, AgentContextHandle, ContextLoadResult, LoadedContext};
use crate::thread_store::{ use crate::thread_store::{
SerializedLanguageModel, SerializedMessage, SerializedMessageSegment, SerializedThread, SerializedCrease, SerializedLanguageModel, SerializedMessage, SerializedMessageSegment,
SerializedToolResult, SerializedToolUse, SharedProjectContext, SerializedThread, SerializedToolResult, SerializedToolUse, SharedProjectContext,
}; };
use crate::tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState}; use crate::tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState};
@ -96,6 +97,15 @@ impl MessageId {
} }
} }
/// Stored information that can be used to resurrect a context crease when creating an editor for a past message.
#[derive(Clone, Debug)]
pub struct MessageCrease {
pub range: Range<usize>,
pub metadata: CreaseMetadata,
/// None for a deserialized message, Some otherwise.
pub context: Option<AgentContextHandle>,
}
/// A message in a [`Thread`]. /// A message in a [`Thread`].
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Message { pub struct Message {
@ -103,6 +113,7 @@ pub struct Message {
pub role: Role, pub role: Role,
pub segments: Vec<MessageSegment>, pub segments: Vec<MessageSegment>,
pub loaded_context: LoadedContext, pub loaded_context: LoadedContext,
pub creases: Vec<MessageCrease>,
} }
impl Message { impl Message {
@ -473,6 +484,18 @@ impl Thread {
text: message.context, text: message.context,
images: Vec::new(), images: Vec::new(),
}, },
creases: message
.creases
.into_iter()
.map(|crease| MessageCrease {
range: crease.start..crease.end,
metadata: CreaseMetadata {
icon_path: crease.icon_path,
label: crease.label,
},
context: None,
})
.collect(),
}) })
.collect(), .collect(),
next_message_id, next_message_id,
@ -826,6 +849,7 @@ impl Thread {
text: impl Into<String>, text: impl Into<String>,
loaded_context: ContextLoadResult, loaded_context: ContextLoadResult,
git_checkpoint: Option<GitStoreCheckpoint>, git_checkpoint: Option<GitStoreCheckpoint>,
creases: Vec<MessageCrease>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> MessageId { ) -> MessageId {
if !loaded_context.referenced_buffers.is_empty() { if !loaded_context.referenced_buffers.is_empty() {
@ -840,6 +864,7 @@ impl Thread {
Role::User, Role::User,
vec![MessageSegment::Text(text.into())], vec![MessageSegment::Text(text.into())],
loaded_context.loaded_context, loaded_context.loaded_context,
creases,
cx, cx,
); );
@ -860,7 +885,13 @@ impl Thread {
segments: Vec<MessageSegment>, segments: Vec<MessageSegment>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> MessageId { ) -> MessageId {
self.insert_message(Role::Assistant, segments, LoadedContext::default(), cx) self.insert_message(
Role::Assistant,
segments,
LoadedContext::default(),
Vec::new(),
cx,
)
} }
pub fn insert_message( pub fn insert_message(
@ -868,6 +899,7 @@ impl Thread {
role: Role, role: Role,
segments: Vec<MessageSegment>, segments: Vec<MessageSegment>,
loaded_context: LoadedContext, loaded_context: LoadedContext,
creases: Vec<MessageCrease>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> MessageId { ) -> MessageId {
let id = self.next_message_id.post_inc(); let id = self.next_message_id.post_inc();
@ -876,6 +908,7 @@ impl Thread {
role, role,
segments, segments,
loaded_context, loaded_context,
creases,
}); });
self.touch_updated_at(); self.touch_updated_at();
cx.emit(ThreadEvent::MessageAdded(id)); cx.emit(ThreadEvent::MessageAdded(id));
@ -995,6 +1028,16 @@ impl Thread {
}) })
.collect(), .collect(),
context: message.loaded_context.text.clone(), context: message.loaded_context.text.clone(),
creases: message
.creases
.iter()
.map(|crease| SerializedCrease {
start: crease.range.start,
end: crease.range.end,
icon_path: crease.metadata.icon_path.clone(),
label: crease.metadata.label.clone(),
})
.collect(),
}) })
.collect(), .collect(),
initial_project_snapshot, initial_project_snapshot,
@ -2502,7 +2545,13 @@ mod tests {
// Insert user message with context // Insert user message with context
let message_id = thread.update(cx, |thread, cx| { let message_id = thread.update(cx, |thread, cx| {
thread.insert_user_message("Please explain this code", loaded_context, None, cx) thread.insert_user_message(
"Please explain this code",
loaded_context,
None,
Vec::new(),
cx,
)
}); });
// Check content and context in message object // Check content and context in message object
@ -2578,7 +2627,7 @@ fn main() {{
.update(|cx| load_context(new_contexts, &project, &None, cx)) .update(|cx| load_context(new_contexts, &project, &None, cx))
.await; .await;
let message1_id = thread.update(cx, |thread, cx| { let message1_id = thread.update(cx, |thread, cx| {
thread.insert_user_message("Message 1", loaded_context, None, cx) thread.insert_user_message("Message 1", loaded_context, None, Vec::new(), cx)
}); });
// Second message with contexts 1 and 2 (context 1 should be skipped as it's already included) // Second message with contexts 1 and 2 (context 1 should be skipped as it's already included)
@ -2593,7 +2642,7 @@ fn main() {{
.update(|cx| load_context(new_contexts, &project, &None, cx)) .update(|cx| load_context(new_contexts, &project, &None, cx))
.await; .await;
let message2_id = thread.update(cx, |thread, cx| { let message2_id = thread.update(cx, |thread, cx| {
thread.insert_user_message("Message 2", loaded_context, None, cx) thread.insert_user_message("Message 2", loaded_context, None, Vec::new(), cx)
}); });
// Third message with all three contexts (contexts 1 and 2 should be skipped) // Third message with all three contexts (contexts 1 and 2 should be skipped)
@ -2609,7 +2658,7 @@ fn main() {{
.update(|cx| load_context(new_contexts, &project, &None, cx)) .update(|cx| load_context(new_contexts, &project, &None, cx))
.await; .await;
let message3_id = thread.update(cx, |thread, cx| { let message3_id = thread.update(cx, |thread, cx| {
thread.insert_user_message("Message 3", loaded_context, None, cx) thread.insert_user_message("Message 3", loaded_context, None, Vec::new(), cx)
}); });
// Check what contexts are included in each message // Check what contexts are included in each message
@ -2723,6 +2772,7 @@ fn main() {{
"What is the best way to learn Rust?", "What is the best way to learn Rust?",
ContextLoadResult::default(), ContextLoadResult::default(),
None, None,
Vec::new(),
cx, cx,
) )
}); });
@ -2756,6 +2806,7 @@ fn main() {{
"Are there any good books?", "Are there any good books?",
ContextLoadResult::default(), ContextLoadResult::default(),
None, None,
Vec::new(),
cx, cx,
) )
}); });
@ -2805,7 +2856,7 @@ fn main() {{
// Insert user message with the buffer as context // Insert user message with the buffer as context
thread.update(cx, |thread, cx| { thread.update(cx, |thread, cx| {
thread.insert_user_message("Explain this code", loaded_context, None, cx) thread.insert_user_message("Explain this code", loaded_context, None, Vec::new(), cx)
}); });
// Create a request and check that it doesn't have a stale buffer warning yet // Create a request and check that it doesn't have a stale buffer warning yet
@ -2839,6 +2890,7 @@ fn main() {{
"What does the code do now?", "What does the code do now?",
ContextLoadResult::default(), ContextLoadResult::default(),
None, None,
Vec::new(),
cx, cx,
) )
}); });

View file

@ -733,6 +733,8 @@ pub struct SerializedMessage {
pub tool_results: Vec<SerializedToolResult>, pub tool_results: Vec<SerializedToolResult>,
#[serde(default)] #[serde(default)]
pub context: String, pub context: String,
#[serde(default)]
pub creases: Vec<SerializedCrease>,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@ -813,10 +815,19 @@ impl LegacySerializedMessage {
tool_uses: self.tool_uses, tool_uses: self.tool_uses,
tool_results: self.tool_results, tool_results: self.tool_results,
context: String::new(), context: String::new(),
creases: Vec::new(),
} }
} }
} }
#[derive(Debug, Serialize, Deserialize)]
pub struct SerializedCrease {
pub start: usize,
pub end: usize,
pub icon_path: SharedString,
pub label: SharedString,
}
struct GlobalThreadsDatabase( struct GlobalThreadsDatabase(
Shared<BoxFuture<'static, Result<Arc<ThreadsDatabase>, Arc<anyhow::Error>>>>, Shared<BoxFuture<'static, Result<Arc<ThreadsDatabase>, Arc<anyhow::Error>>>>,
); );

View file

@ -1815,10 +1815,6 @@ impl PromptEditor {
self.editor = cx.new(|cx| { self.editor = cx.new(|cx| {
let mut editor = Editor::auto_height(Self::MAX_LINES as usize, window, cx); let mut editor = Editor::auto_height(Self::MAX_LINES as usize, window, cx);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
editor.set_placeholder_text(
Self::placeholder_text(self.codegen.read(cx), window, cx),
cx,
);
editor.set_placeholder_text("Add a prompt…", cx); editor.set_placeholder_text("Add a prompt…", cx);
editor.set_text(prompt, window, cx); editor.set_text(prompt, window, cx);
if focus { if focus {

View file

@ -1054,7 +1054,7 @@ impl ContextEditor {
|_, _, _, _| Empty.into_any_element(), |_, _, _, _| Empty.into_any_element(),
) )
.with_metadata(CreaseMetadata { .with_metadata(CreaseMetadata {
icon: IconName::Ai, icon_path: SharedString::from(IconName::Ai.path()),
label: "Thinking Process".into(), label: "Thinking Process".into(),
}), }),
); );
@ -1097,7 +1097,7 @@ impl ContextEditor {
FoldPlaceholder { FoldPlaceholder {
render: render_fold_icon_button( render: render_fold_icon_button(
cx.entity().downgrade(), cx.entity().downgrade(),
section.icon, section.icon.path().into(),
section.label.clone(), section.label.clone(),
), ),
merge_adjacent: false, merge_adjacent: false,
@ -1107,7 +1107,7 @@ impl ContextEditor {
|_, _, _, _| Empty.into_any_element(), |_, _, _, _| Empty.into_any_element(),
) )
.with_metadata(CreaseMetadata { .with_metadata(CreaseMetadata {
icon: section.icon, icon_path: section.icon.path().into(),
label: section.label, label: section.label,
}), }),
); );
@ -2055,7 +2055,7 @@ impl ContextEditor {
FoldPlaceholder { FoldPlaceholder {
render: render_fold_icon_button( render: render_fold_icon_button(
weak_editor.clone(), weak_editor.clone(),
metadata.crease.icon, metadata.crease.icon_path.clone(),
metadata.crease.label.clone(), metadata.crease.label.clone(),
), ),
..Default::default() ..Default::default()
@ -2851,7 +2851,7 @@ fn render_thought_process_fold_icon_button(
fn render_fold_icon_button( fn render_fold_icon_button(
editor: WeakEntity<Editor>, editor: WeakEntity<Editor>,
icon: IconName, icon_path: SharedString,
label: SharedString, label: SharedString,
) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> { ) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
Arc::new(move |fold_id, fold_range, _cx| { Arc::new(move |fold_id, fold_range, _cx| {
@ -2859,7 +2859,7 @@ fn render_fold_icon_button(
ButtonLike::new(fold_id) ButtonLike::new(fold_id)
.style(ButtonStyle::Filled) .style(ButtonStyle::Filled)
.layer(ElevationIndex::ElevatedSurface) .layer(ElevationIndex::ElevatedSurface)
.child(Icon::new(icon)) .child(Icon::from_path(icon_path.clone()))
.child(Label::new(label.clone()).single_line()) .child(Label::new(label.clone()).single_line())
.on_click(move |_, window, cx| { .on_click(move |_, window, cx| {
editor editor

View file

@ -391,7 +391,7 @@ impl DisplayMap {
&mut self, &mut self,
crease_ids: impl IntoIterator<Item = CreaseId>, crease_ids: impl IntoIterator<Item = CreaseId>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) -> Vec<(CreaseId, Range<Anchor>)> {
let snapshot = self.buffer.read(cx).snapshot(cx); let snapshot = self.buffer.read(cx).snapshot(cx);
self.crease_map.remove(crease_ids, &snapshot) self.crease_map.remove(crease_ids, &snapshot)
} }

View file

@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
use std::{cmp::Ordering, fmt::Debug, ops::Range, sync::Arc}; use std::{cmp::Ordering, fmt::Debug, ops::Range, sync::Arc};
use sum_tree::{Bias, SeekTarget, SumTree}; use sum_tree::{Bias, SeekTarget, SumTree};
use text::Point; use text::Point;
use ui::{App, IconName, SharedString, Window}; use ui::{App, SharedString, Window};
use crate::{BlockStyle, FoldPlaceholder, RenderBlock}; use crate::{BlockStyle, FoldPlaceholder, RenderBlock};
@ -40,6 +40,10 @@ impl CreaseSnapshot {
} }
} }
pub fn creases(&self) -> impl Iterator<Item = (CreaseId, &Crease<Anchor>)> {
self.creases.iter().map(|item| (item.id, &item.crease))
}
/// Returns the first Crease starting on the specified buffer row. /// Returns the first Crease starting on the specified buffer row.
pub fn query_row<'a>( pub fn query_row<'a>(
&'a self, &'a self,
@ -147,7 +151,7 @@ pub enum Crease<T> {
/// Metadata about a [`Crease`], that is used for serialization. /// Metadata about a [`Crease`], that is used for serialization.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreaseMetadata { pub struct CreaseMetadata {
pub icon: IconName, pub icon_path: SharedString,
pub label: SharedString, pub label: SharedString,
} }
@ -237,6 +241,13 @@ impl<T> Crease<T> {
Crease::Block { range, .. } => range, Crease::Block { range, .. } => range,
} }
} }
pub fn metadata(&self) -> Option<&CreaseMetadata> {
match self {
Self::Inline { metadata, .. } => metadata.as_ref(),
Self::Block { .. } => None,
}
}
} }
impl<T> std::fmt::Debug for Crease<T> impl<T> std::fmt::Debug for Crease<T>
@ -305,7 +316,7 @@ impl CreaseMap {
&mut self, &mut self,
ids: impl IntoIterator<Item = CreaseId>, ids: impl IntoIterator<Item = CreaseId>,
snapshot: &MultiBufferSnapshot, snapshot: &MultiBufferSnapshot,
) { ) -> Vec<(CreaseId, Range<Anchor>)> {
let mut removals = Vec::new(); let mut removals = Vec::new();
for id in ids { for id in ids {
if let Some(range) = self.id_to_range.remove(&id) { if let Some(range) = self.id_to_range.remove(&id) {
@ -320,11 +331,11 @@ impl CreaseMap {
let mut new_creases = SumTree::new(snapshot); let mut new_creases = SumTree::new(snapshot);
let mut cursor = self.snapshot.creases.cursor::<ItemSummary>(snapshot); let mut cursor = self.snapshot.creases.cursor::<ItemSummary>(snapshot);
for (id, range) in removals { for (id, range) in &removals {
new_creases.append(cursor.slice(&range, Bias::Left, snapshot), snapshot); new_creases.append(cursor.slice(range, Bias::Left, snapshot), snapshot);
while let Some(item) = cursor.item() { while let Some(item) = cursor.item() {
cursor.next(snapshot); cursor.next(snapshot);
if item.id == id { if item.id == *id {
break; break;
} else { } else {
new_creases.push(item.clone(), snapshot); new_creases.push(item.clone(), snapshot);
@ -335,6 +346,8 @@ impl CreaseMap {
new_creases.append(cursor.suffix(snapshot), snapshot); new_creases.append(cursor.suffix(snapshot), snapshot);
new_creases new_creases
}; };
removals
} }
} }

View file

@ -16239,9 +16239,9 @@ impl Editor {
&mut self, &mut self,
ids: impl IntoIterator<Item = CreaseId>, ids: impl IntoIterator<Item = CreaseId>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) -> Vec<(CreaseId, Range<Anchor>)> {
self.display_map self.display_map
.update(cx, |map, cx| map.remove_creases(ids, cx)); .update(cx, |map, cx| map.remove_creases(ids, cx))
} }
pub fn longest_row(&self, cx: &mut App) -> DisplayRow { pub fn longest_row(&self, cx: &mut App) -> DisplayRow {

View file

@ -119,6 +119,7 @@ impl ExampleContext {
text.to_string(), text.to_string(),
ContextLoadResult::default(), ContextLoadResult::default(),
None, None,
Vec::new(),
cx, cx,
); );
}) })