Compare commits
4 commits
main
...
message-ed
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d36304963e | ||
![]() |
0d71351b02 | ||
![]() |
b06fe288f3 | ||
![]() |
fd0ffb737f |
8 changed files with 554 additions and 670 deletions
|
@ -331,8 +331,6 @@
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"enter": "agent::Chat",
|
"enter": "agent::Chat",
|
||||||
"up": "agent::PreviousHistoryMessage",
|
|
||||||
"down": "agent::NextHistoryMessage",
|
|
||||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||||
"ctrl-shift-y": "agent::KeepAll",
|
"ctrl-shift-y": "agent::KeepAll",
|
||||||
"ctrl-shift-n": "agent::RejectAll"
|
"ctrl-shift-n": "agent::RejectAll"
|
||||||
|
|
|
@ -383,8 +383,6 @@
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"enter": "agent::Chat",
|
"enter": "agent::Chat",
|
||||||
"up": "agent::PreviousHistoryMessage",
|
|
||||||
"down": "agent::NextHistoryMessage",
|
|
||||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||||
"cmd-shift-y": "agent::KeepAll",
|
"cmd-shift-y": "agent::KeepAll",
|
||||||
"cmd-shift-n": "agent::RejectAll"
|
"cmd-shift-n": "agent::RejectAll"
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
mod completion_provider;
|
mod completion_provider;
|
||||||
mod message_history;
|
mod message_editor;
|
||||||
mod model_selector;
|
mod model_selector;
|
||||||
mod model_selector_popover;
|
mod model_selector_popover;
|
||||||
mod thread_view;
|
mod thread_view;
|
||||||
|
|
||||||
pub use message_history::MessageHistory;
|
|
||||||
pub use model_selector::AcpModelSelector;
|
pub use model_selector::AcpModelSelector;
|
||||||
pub use model_selector_popover::AcpModelSelectorPopover;
|
pub use model_selector_popover::AcpModelSelectorPopover;
|
||||||
pub use thread_view::AcpThreadView;
|
pub use thread_view::AcpThreadView;
|
||||||
|
|
479
crates/agent_ui/src/acp/message_editor.rs
Normal file
479
crates/agent_ui/src/acp/message_editor.rs
Normal file
|
@ -0,0 +1,479 @@
|
||||||
|
use crate::acp::completion_provider::ContextPickerCompletionProvider;
|
||||||
|
use crate::acp::completion_provider::MentionSet;
|
||||||
|
use acp_thread::MentionUri;
|
||||||
|
use agent_client_protocol as acp;
|
||||||
|
use anyhow::Result;
|
||||||
|
use collections::HashSet;
|
||||||
|
use editor::{
|
||||||
|
AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode,
|
||||||
|
EditorStyle, MultiBuffer,
|
||||||
|
};
|
||||||
|
use file_icons::FileIcons;
|
||||||
|
use gpui::{
|
||||||
|
AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, Task, TextStyle, WeakEntity,
|
||||||
|
};
|
||||||
|
use language::Language;
|
||||||
|
use language::{Buffer, BufferSnapshot};
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use project::{CompletionIntent, Project};
|
||||||
|
use settings::Settings;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use theme::ThemeSettings;
|
||||||
|
use ui::{
|
||||||
|
ActiveTheme, App, IconName, InteractiveElement, IntoElement, ParentElement, Render,
|
||||||
|
SharedString, Styled, TextSize, Window, div,
|
||||||
|
};
|
||||||
|
use util::ResultExt;
|
||||||
|
use workspace::Workspace;
|
||||||
|
use zed_actions::agent::Chat;
|
||||||
|
|
||||||
|
pub const MIN_EDITOR_LINES: usize = 4;
|
||||||
|
pub const MAX_EDITOR_LINES: usize = 8;
|
||||||
|
|
||||||
|
pub struct MessageEditor {
|
||||||
|
editor: Entity<Editor>,
|
||||||
|
project: Entity<Project>,
|
||||||
|
mention_set: Arc<Mutex<MentionSet>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum MessageEditorEvent {
|
||||||
|
Chat,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<MessageEditorEvent> for MessageEditor {}
|
||||||
|
|
||||||
|
impl MessageEditor {
|
||||||
|
pub fn new(
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
project: Entity<Project>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let language = Language::new(
|
||||||
|
language::LanguageConfig {
|
||||||
|
completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mention_set = Arc::new(Mutex::new(MentionSet::default()));
|
||||||
|
let editor = cx.new(|cx| {
|
||||||
|
let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
|
||||||
|
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||||
|
|
||||||
|
let mut editor = Editor::new(
|
||||||
|
editor::EditorMode::AutoHeight {
|
||||||
|
min_lines: MIN_EDITOR_LINES,
|
||||||
|
max_lines: Some(MAX_EDITOR_LINES),
|
||||||
|
},
|
||||||
|
buffer,
|
||||||
|
None,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
editor.set_placeholder_text("Message the agent - @ to include files", cx);
|
||||||
|
editor.set_show_indent_guides(false, cx);
|
||||||
|
editor.set_soft_wrap();
|
||||||
|
editor.set_use_modal_editing(true);
|
||||||
|
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
|
||||||
|
mention_set.clone(),
|
||||||
|
workspace,
|
||||||
|
cx.weak_entity(),
|
||||||
|
))));
|
||||||
|
editor.set_context_menu_options(ContextMenuOptions {
|
||||||
|
min_entries_visible: 12,
|
||||||
|
max_entries_visible: 12,
|
||||||
|
placement: Some(ContextMenuPlacement::Above),
|
||||||
|
});
|
||||||
|
editor
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
editor,
|
||||||
|
project,
|
||||||
|
mention_set,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self, cx: &App) -> bool {
|
||||||
|
self.editor.read(cx).is_empty(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contents(&self, cx: &mut Context<Self>) -> Task<Result<Vec<acp::ContentBlock>>> {
|
||||||
|
let contents = self.mention_set.lock().contents(self.project.clone(), cx);
|
||||||
|
let editor = self.editor.clone();
|
||||||
|
|
||||||
|
cx.spawn(async move |_, cx| {
|
||||||
|
let contents = contents.await?;
|
||||||
|
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
let mut ix = 0;
|
||||||
|
let mut chunks: Vec<acp::ContentBlock> = Vec::new();
|
||||||
|
let text = editor.text(cx);
|
||||||
|
editor.display_map.update(cx, |map, cx| {
|
||||||
|
let snapshot = map.snapshot(cx);
|
||||||
|
for (crease_id, crease) in snapshot.crease_snapshot.creases() {
|
||||||
|
// Skip creases that have been edited out of the message buffer.
|
||||||
|
if !crease.range().start.is_valid(&snapshot.buffer_snapshot) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(mention) = contents.get(&crease_id) {
|
||||||
|
let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
|
||||||
|
if crease_range.start > ix {
|
||||||
|
chunks.push(text[ix..crease_range.start].into());
|
||||||
|
}
|
||||||
|
chunks.push(acp::ContentBlock::Resource(acp::EmbeddedResource {
|
||||||
|
annotations: None,
|
||||||
|
resource: acp::EmbeddedResourceResource::TextResourceContents(
|
||||||
|
acp::TextResourceContents {
|
||||||
|
mime_type: None,
|
||||||
|
text: mention.content.clone(),
|
||||||
|
uri: mention.uri.to_uri(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
ix = crease_range.end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ix < text.len() {
|
||||||
|
let last_chunk = text[ix..].trim_end();
|
||||||
|
if !last_chunk.is_empty() {
|
||||||
|
chunks.push(last_chunk.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
chunks
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
editor.clear(window, cx);
|
||||||
|
editor.remove_creases(self.mention_set.lock().drain(), cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
cx.emit(MessageEditorEvent::Chat)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_dragged_files(
|
||||||
|
&self,
|
||||||
|
paths: Vec<project::ProjectPath>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let buffer = self.editor.read(cx).buffer().clone();
|
||||||
|
let Some((&excerpt_id, _, _)) = buffer.read(cx).snapshot(cx).as_singleton() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(buffer) = buffer.read(cx).as_singleton() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for path in paths {
|
||||||
|
let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
|
||||||
|
let path_prefix = abs_path
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or(path.path.as_os_str())
|
||||||
|
.display()
|
||||||
|
.to_string();
|
||||||
|
let completion = ContextPickerCompletionProvider::completion_for_path(
|
||||||
|
path,
|
||||||
|
&path_prefix,
|
||||||
|
false,
|
||||||
|
entry.is_dir(),
|
||||||
|
excerpt_id,
|
||||||
|
anchor..anchor,
|
||||||
|
self.editor.clone(),
|
||||||
|
self.mention_set.clone(),
|
||||||
|
self.project.clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.editor.update(cx, |message_editor, cx| {
|
||||||
|
message_editor.edit(
|
||||||
|
[(
|
||||||
|
multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
|
||||||
|
completion.new_text,
|
||||||
|
)],
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if let Some(confirm) = completion.confirm.clone() {
|
||||||
|
confirm(CompletionIntent::Complete, window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_expanded(&mut self, expanded: bool, cx: &mut Context<Self>) {
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
if expanded {
|
||||||
|
editor.set_mode(EditorMode::Full {
|
||||||
|
scale_ui_elements_with_buffer_font_size: false,
|
||||||
|
show_active_line_background: false,
|
||||||
|
sized_by_content: false,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
editor.set_mode(EditorMode::AutoHeight {
|
||||||
|
min_lines: MIN_EDITOR_LINES,
|
||||||
|
max_lines: Some(MAX_EDITOR_LINES),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
cx.notify()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
fn set_draft_message(
|
||||||
|
message_editor: Entity<Editor>,
|
||||||
|
mention_set: Arc<Mutex<MentionSet>>,
|
||||||
|
project: Entity<Project>,
|
||||||
|
message: Option<&[acp::ContentBlock]>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Option<BufferSnapshot> {
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
let message = message?;
|
||||||
|
|
||||||
|
let mut text = String::new();
|
||||||
|
let mut mentions = Vec::new();
|
||||||
|
|
||||||
|
for chunk in message {
|
||||||
|
match chunk {
|
||||||
|
acp::ContentBlock::Text(text_content) => {
|
||||||
|
text.push_str(&text_content.text);
|
||||||
|
}
|
||||||
|
acp::ContentBlock::Resource(acp::EmbeddedResource {
|
||||||
|
resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
if let Some(ref mention @ MentionUri::File(ref abs_path)) =
|
||||||
|
MentionUri::parse(&resource.uri).log_err()
|
||||||
|
{
|
||||||
|
let project_path = project
|
||||||
|
.read(cx)
|
||||||
|
.project_path_for_absolute_path(&abs_path, cx);
|
||||||
|
let start = text.len();
|
||||||
|
let content = mention.to_uri();
|
||||||
|
text.push_str(&content);
|
||||||
|
let end = text.len();
|
||||||
|
if let Some(project_path) = project_path {
|
||||||
|
let filename: SharedString = project_path
|
||||||
|
.path
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
.into();
|
||||||
|
mentions.push((start..end, project_path, filename));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
acp::ContentBlock::Image(_)
|
||||||
|
| acp::ContentBlock::Audio(_)
|
||||||
|
| acp::ContentBlock::Resource(_)
|
||||||
|
| acp::ContentBlock::ResourceLink(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let snapshot = message_editor.update(cx, |editor, cx| {
|
||||||
|
editor.set_text(text, window, cx);
|
||||||
|
editor.buffer().read(cx).snapshot(cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
for (range, project_path, filename) in mentions {
|
||||||
|
let crease_icon_path = if project_path.path.is_dir() {
|
||||||
|
FileIcons::get_folder_icon(false, cx)
|
||||||
|
.unwrap_or_else(|| IconName::Folder.path().into())
|
||||||
|
} else {
|
||||||
|
FileIcons::get_icon(Path::new(project_path.path.as_ref()), cx)
|
||||||
|
.unwrap_or_else(|| IconName::File.path().into())
|
||||||
|
};
|
||||||
|
|
||||||
|
let anchor = snapshot.anchor_before(range.start);
|
||||||
|
if let Some(project_path) = project.read(cx).absolute_path(&project_path, cx) {
|
||||||
|
let crease_id = crate::context_picker::insert_crease_for_mention(
|
||||||
|
anchor.excerpt_id,
|
||||||
|
anchor.text_anchor,
|
||||||
|
range.end - range.start,
|
||||||
|
filename,
|
||||||
|
crease_icon_path,
|
||||||
|
message_editor.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(crease_id) = crease_id {
|
||||||
|
mention_set.lock().insert(crease_id, project_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let snapshot = snapshot.as_singleton().unwrap().2.clone();
|
||||||
|
Some(snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
editor.set_text(text, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Focusable for MessageEditor {
|
||||||
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||||
|
self.editor.focus_handle(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for MessageEditor {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.key_context("MessageEditor")
|
||||||
|
.on_action(cx.listener(Self::chat))
|
||||||
|
.flex_1()
|
||||||
|
.child({
|
||||||
|
let settings = ThemeSettings::get_global(cx);
|
||||||
|
let font_size = TextSize::Small
|
||||||
|
.rems(cx)
|
||||||
|
.to_pixels(settings.agent_font_size(cx));
|
||||||
|
let line_height = settings.buffer_line_height.value() * font_size;
|
||||||
|
|
||||||
|
let text_style = TextStyle {
|
||||||
|
color: cx.theme().colors().text,
|
||||||
|
font_family: settings.buffer_font.family.clone(),
|
||||||
|
font_fallbacks: settings.buffer_font.fallbacks.clone(),
|
||||||
|
font_features: settings.buffer_font.features.clone(),
|
||||||
|
font_size: font_size.into(),
|
||||||
|
line_height: line_height.into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
EditorElement::new(
|
||||||
|
&self.editor,
|
||||||
|
EditorStyle {
|
||||||
|
background: cx.theme().colors().editor_background,
|
||||||
|
local_player: cx.theme().players().local(),
|
||||||
|
text: text_style,
|
||||||
|
syntax: cx.theme().syntax().clone(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use agent_client_protocol as acp;
|
||||||
|
use fs::FakeFs;
|
||||||
|
use gpui::{AppContext, TestAppContext};
|
||||||
|
use lsp::{CompletionContext, CompletionTriggerKind};
|
||||||
|
use project::{CompletionIntent, Project};
|
||||||
|
use serde_json::json;
|
||||||
|
use util::path;
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
use crate::acp::{message_editor::MessageEditor, thread_view::tests::init_test};
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_at_mention_removal(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree("/project", json!({"file": ""})).await;
|
||||||
|
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
|
||||||
|
|
||||||
|
let (workspace, cx) =
|
||||||
|
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||||
|
|
||||||
|
let message_editor = cx.update(|window, cx| {
|
||||||
|
cx.new(|cx| MessageEditor::new(workspace.downgrade(), project.clone(), window, cx))
|
||||||
|
});
|
||||||
|
let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
let excerpt_id = editor.update(cx, |editor, cx| {
|
||||||
|
editor
|
||||||
|
.buffer()
|
||||||
|
.read(cx)
|
||||||
|
.excerpt_ids()
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
let completions = editor.update_in(cx, |editor, window, cx| {
|
||||||
|
editor.set_text("Hello @", window, cx);
|
||||||
|
let buffer = editor.buffer().read(cx).as_singleton().unwrap();
|
||||||
|
let completion_provider = editor.completion_provider().unwrap();
|
||||||
|
completion_provider.completions(
|
||||||
|
excerpt_id,
|
||||||
|
&buffer,
|
||||||
|
text::Anchor::MAX,
|
||||||
|
CompletionContext {
|
||||||
|
trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
|
||||||
|
trigger_character: Some("@".into()),
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let [_, completion]: [_; 2] = completions
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|response| response.completions)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.try_into()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
editor.update_in(cx, |editor, window, cx| {
|
||||||
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||||
|
let start = snapshot
|
||||||
|
.anchor_in_excerpt(excerpt_id, completion.replace_range.start)
|
||||||
|
.unwrap();
|
||||||
|
let end = snapshot
|
||||||
|
.anchor_in_excerpt(excerpt_id, completion.replace_range.end)
|
||||||
|
.unwrap();
|
||||||
|
editor.edit([(start..end, completion.new_text)], cx);
|
||||||
|
(completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
// Backspace over the inserted crease (and the following space).
|
||||||
|
editor.update_in(cx, |editor, window, cx| {
|
||||||
|
editor.backspace(&Default::default(), window, cx);
|
||||||
|
editor.backspace(&Default::default(), window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
let content = message_editor
|
||||||
|
.update_in(cx, |message_editor, _window, cx| {
|
||||||
|
message_editor.contents(cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// We don't send a resource link for the deleted crease.
|
||||||
|
pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,92 +0,0 @@
|
||||||
pub struct MessageHistory<T> {
|
|
||||||
items: Vec<T>,
|
|
||||||
current: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Default for MessageHistory<T> {
|
|
||||||
fn default() -> Self {
|
|
||||||
MessageHistory {
|
|
||||||
items: Vec::new(),
|
|
||||||
current: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> MessageHistory<T> {
|
|
||||||
pub fn push(&mut self, message: T) {
|
|
||||||
self.current.take();
|
|
||||||
self.items.push(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset_position(&mut self) {
|
|
||||||
self.current.take();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prev(&mut self) -> Option<&T> {
|
|
||||||
if self.items.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_ix = self
|
|
||||||
.current
|
|
||||||
.get_or_insert(self.items.len())
|
|
||||||
.saturating_sub(1);
|
|
||||||
|
|
||||||
self.current = Some(new_ix);
|
|
||||||
self.items.get(new_ix)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next(&mut self) -> Option<&T> {
|
|
||||||
let current = self.current.as_mut()?;
|
|
||||||
*current += 1;
|
|
||||||
|
|
||||||
self.items.get(*current).or_else(|| {
|
|
||||||
self.current.take();
|
|
||||||
None
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn items(&self) -> &[T] {
|
|
||||||
&self.items
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_prev_next() {
|
|
||||||
let mut history = MessageHistory::default();
|
|
||||||
|
|
||||||
// Test empty history
|
|
||||||
assert_eq!(history.prev(), None);
|
|
||||||
assert_eq!(history.next(), None);
|
|
||||||
|
|
||||||
// Add some messages
|
|
||||||
history.push("first");
|
|
||||||
history.push("second");
|
|
||||||
history.push("third");
|
|
||||||
|
|
||||||
// Test prev navigation
|
|
||||||
assert_eq!(history.prev(), Some(&"third"));
|
|
||||||
assert_eq!(history.prev(), Some(&"second"));
|
|
||||||
assert_eq!(history.prev(), Some(&"first"));
|
|
||||||
assert_eq!(history.prev(), Some(&"first"));
|
|
||||||
|
|
||||||
assert_eq!(history.next(), Some(&"second"));
|
|
||||||
|
|
||||||
// Test mixed navigation
|
|
||||||
history.push("fourth");
|
|
||||||
assert_eq!(history.prev(), Some(&"fourth"));
|
|
||||||
assert_eq!(history.prev(), Some(&"third"));
|
|
||||||
assert_eq!(history.next(), Some(&"fourth"));
|
|
||||||
assert_eq!(history.next(), None);
|
|
||||||
|
|
||||||
// Test that push resets navigation
|
|
||||||
history.prev();
|
|
||||||
history.prev();
|
|
||||||
history.push("fifth");
|
|
||||||
assert_eq!(history.prev(), Some(&"fifth"));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -10,10 +10,7 @@ use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
|
||||||
use audio::{Audio, Sound};
|
use audio::{Audio, Sound};
|
||||||
use buffer_diff::BufferDiff;
|
use buffer_diff::BufferDiff;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use editor::{
|
use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
|
||||||
AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode,
|
|
||||||
EditorStyle, MinimapVisibility, MultiBuffer, PathKey,
|
|
||||||
};
|
|
||||||
use file_icons::FileIcons;
|
use file_icons::FileIcons;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
|
Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
|
||||||
|
@ -22,20 +19,15 @@ use gpui::{
|
||||||
Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop,
|
Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop,
|
||||||
linear_gradient, list, percentage, point, prelude::*, pulsating_between,
|
linear_gradient, list, percentage, point, prelude::*, pulsating_between,
|
||||||
};
|
};
|
||||||
|
use language::Buffer;
|
||||||
use language::language_settings::SoftWrap;
|
use language::language_settings::SoftWrap;
|
||||||
use language::{Buffer, Language};
|
|
||||||
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
||||||
use parking_lot::Mutex;
|
use project::Project;
|
||||||
use project::{CompletionIntent, Project};
|
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
use settings::{Settings as _, SettingsStore};
|
use settings::{Settings as _, SettingsStore};
|
||||||
use std::path::PathBuf;
|
use std::{collections::BTreeMap, process::ExitStatus, rc::Rc, time::Duration};
|
||||||
use std::{
|
|
||||||
cell::RefCell, collections::BTreeMap, path::Path, process::ExitStatus, rc::Rc, sync::Arc,
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
use terminal_view::TerminalView;
|
use terminal_view::TerminalView;
|
||||||
use text::{Anchor, BufferSnapshot};
|
use text::Anchor;
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::{
|
use ui::{
|
||||||
Disclosure, Divider, DividerColor, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState,
|
Disclosure, Divider, DividerColor, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState,
|
||||||
|
@ -43,13 +35,11 @@ use ui::{
|
||||||
};
|
};
|
||||||
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
|
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
|
||||||
use workspace::{CollaboratorId, Workspace};
|
use workspace::{CollaboratorId, Workspace};
|
||||||
use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage, ToggleModelSelector};
|
use zed_actions::agent::{Chat, ToggleModelSelector};
|
||||||
|
|
||||||
use crate::acp::AcpModelSelectorPopover;
|
use crate::acp::AcpModelSelectorPopover;
|
||||||
use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
|
use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
|
||||||
use crate::acp::message_history::MessageHistory;
|
|
||||||
use crate::agent_diff::AgentDiff;
|
use crate::agent_diff::AgentDiff;
|
||||||
use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
|
|
||||||
use crate::ui::{AgentNotification, AgentNotificationEvent};
|
use crate::ui::{AgentNotification, AgentNotificationEvent};
|
||||||
use crate::{
|
use crate::{
|
||||||
AgentDiffPane, AgentPanel, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll,
|
AgentDiffPane, AgentPanel, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll,
|
||||||
|
@ -64,11 +54,8 @@ pub struct AcpThreadView {
|
||||||
thread_state: ThreadState,
|
thread_state: ThreadState,
|
||||||
diff_editors: HashMap<EntityId, Entity<Editor>>,
|
diff_editors: HashMap<EntityId, Entity<Editor>>,
|
||||||
terminal_views: HashMap<EntityId, Entity<TerminalView>>,
|
terminal_views: HashMap<EntityId, Entity<TerminalView>>,
|
||||||
message_editor: Entity<Editor>,
|
message_editor: Entity<MessageEditor>,
|
||||||
model_selector: Option<Entity<AcpModelSelectorPopover>>,
|
model_selector: Option<Entity<AcpModelSelectorPopover>>,
|
||||||
message_set_from_history: Option<BufferSnapshot>,
|
|
||||||
_message_editor_subscription: Subscription,
|
|
||||||
mention_set: Arc<Mutex<MentionSet>>,
|
|
||||||
notifications: Vec<WindowHandle<AgentNotification>>,
|
notifications: Vec<WindowHandle<AgentNotification>>,
|
||||||
notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
|
notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
|
||||||
last_error: Option<Entity<Markdown>>,
|
last_error: Option<Entity<Markdown>>,
|
||||||
|
@ -81,9 +68,8 @@ pub struct AcpThreadView {
|
||||||
plan_expanded: bool,
|
plan_expanded: bool,
|
||||||
editor_expanded: bool,
|
editor_expanded: bool,
|
||||||
terminal_expanded: bool,
|
terminal_expanded: bool,
|
||||||
message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
|
|
||||||
_cancel_task: Option<Task<()>>,
|
_cancel_task: Option<Task<()>>,
|
||||||
_subscriptions: [Subscription; 1],
|
_subscriptions: [Subscription; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ThreadState {
|
enum ThreadState {
|
||||||
|
@ -108,81 +94,18 @@ impl AcpThreadView {
|
||||||
agent: Rc<dyn AgentServer>,
|
agent: Rc<dyn AgentServer>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
|
|
||||||
min_lines: usize,
|
|
||||||
max_lines: Option<usize>,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let language = Language::new(
|
let message_editor =
|
||||||
language::LanguageConfig {
|
cx.new(|cx| MessageEditor::new(workspace.clone(), project.clone(), window, cx));
|
||||||
completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mention_set = Arc::new(Mutex::new(MentionSet::default()));
|
|
||||||
|
|
||||||
let message_editor = cx.new(|cx| {
|
|
||||||
let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
|
|
||||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
|
||||||
|
|
||||||
let mut editor = Editor::new(
|
|
||||||
editor::EditorMode::AutoHeight {
|
|
||||||
min_lines,
|
|
||||||
max_lines: max_lines,
|
|
||||||
},
|
|
||||||
buffer,
|
|
||||||
None,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
editor.set_placeholder_text("Message the agent - @ to include files", cx);
|
|
||||||
editor.set_show_indent_guides(false, cx);
|
|
||||||
editor.set_soft_wrap();
|
|
||||||
editor.set_use_modal_editing(true);
|
|
||||||
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
|
|
||||||
mention_set.clone(),
|
|
||||||
workspace.clone(),
|
|
||||||
cx.weak_entity(),
|
|
||||||
))));
|
|
||||||
editor.set_context_menu_options(ContextMenuOptions {
|
|
||||||
min_entries_visible: 12,
|
|
||||||
max_entries_visible: 12,
|
|
||||||
placement: Some(ContextMenuPlacement::Above),
|
|
||||||
});
|
|
||||||
editor
|
|
||||||
});
|
|
||||||
|
|
||||||
let message_editor_subscription =
|
|
||||||
cx.subscribe(&message_editor, |this, editor, event, cx| {
|
|
||||||
if let editor::EditorEvent::BufferEdited = &event {
|
|
||||||
let buffer = editor
|
|
||||||
.read(cx)
|
|
||||||
.buffer()
|
|
||||||
.read(cx)
|
|
||||||
.as_singleton()
|
|
||||||
.unwrap()
|
|
||||||
.read(cx)
|
|
||||||
.snapshot();
|
|
||||||
if let Some(message) = this.message_set_from_history.clone()
|
|
||||||
&& message.version() != buffer.version()
|
|
||||||
{
|
|
||||||
this.message_set_from_history = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
if this.message_set_from_history.is_none() {
|
|
||||||
this.message_history.borrow_mut().reset_position();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let mention_set = mention_set.clone();
|
|
||||||
|
|
||||||
let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0));
|
let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0));
|
||||||
|
|
||||||
let subscription = cx.observe_global_in::<SettingsStore>(window, Self::settings_changed);
|
let subscriptions = [
|
||||||
|
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
|
||||||
|
cx.subscribe_in(&message_editor, window, Self::on_message_editor_event),
|
||||||
|
];
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
agent: agent.clone(),
|
agent: agent.clone(),
|
||||||
|
@ -191,9 +114,6 @@ impl AcpThreadView {
|
||||||
thread_state: Self::initial_state(agent, workspace, project, window, cx),
|
thread_state: Self::initial_state(agent, workspace, project, window, cx),
|
||||||
message_editor,
|
message_editor,
|
||||||
model_selector: None,
|
model_selector: None,
|
||||||
message_set_from_history: None,
|
|
||||||
_message_editor_subscription: message_editor_subscription,
|
|
||||||
mention_set,
|
|
||||||
notifications: Vec::new(),
|
notifications: Vec::new(),
|
||||||
notification_subscriptions: HashMap::default(),
|
notification_subscriptions: HashMap::default(),
|
||||||
diff_editors: Default::default(),
|
diff_editors: Default::default(),
|
||||||
|
@ -208,8 +128,7 @@ impl AcpThreadView {
|
||||||
plan_expanded: false,
|
plan_expanded: false,
|
||||||
editor_expanded: false,
|
editor_expanded: false,
|
||||||
terminal_expanded: true,
|
terminal_expanded: true,
|
||||||
message_history,
|
_subscriptions: subscriptions,
|
||||||
_subscriptions: [subscription],
|
|
||||||
_cancel_task: None,
|
_cancel_task: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -377,189 +296,65 @@ impl AcpThreadView {
|
||||||
|
|
||||||
fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
|
fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
|
||||||
self.editor_expanded = is_expanded;
|
self.editor_expanded = is_expanded;
|
||||||
self.message_editor.update(cx, |editor, _| {
|
self.message_editor.update(cx, |editor, cx| {
|
||||||
if self.editor_expanded {
|
editor.set_expanded(is_expanded, cx);
|
||||||
editor.set_mode(EditorMode::Full {
|
|
||||||
scale_ui_elements_with_buffer_font_size: false,
|
|
||||||
show_active_line_background: false,
|
|
||||||
sized_by_content: false,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
editor.set_mode(EditorMode::AutoHeight {
|
|
||||||
min_lines: MIN_EDITOR_LINES,
|
|
||||||
max_lines: Some(MAX_EDITOR_LINES),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn on_message_editor_event(
|
||||||
|
&mut self,
|
||||||
|
_: &Entity<MessageEditor>,
|
||||||
|
event: &MessageEditorEvent,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
MessageEditorEvent::Chat => self.chat(window, cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chat(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.last_error.take();
|
self.last_error.take();
|
||||||
|
|
||||||
let mut ix = 0;
|
let Some(thread) = self.thread().cloned() else {
|
||||||
let mut chunks: Vec<acp::ContentBlock> = Vec::new();
|
return;
|
||||||
let project = self.project.clone();
|
};
|
||||||
|
|
||||||
let contents = self.mention_set.lock().contents(project, cx);
|
let contents = self
|
||||||
|
.message_editor
|
||||||
|
.update(cx, |message_editor, cx| message_editor.contents(cx));
|
||||||
|
let task = cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let contents = contents.await?;
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
if contents.is_empty() {
|
||||||
let contents = match contents.await {
|
return Ok(());
|
||||||
Ok(contents) => contents,
|
}
|
||||||
Err(e) => {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.last_error =
|
|
||||||
Some(cx.new(|cx| Markdown::new(e.to_string().into(), None, None, cx)));
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
this.message_editor.update(cx, |editor, cx| {
|
|
||||||
let text = editor.text(cx);
|
|
||||||
editor.display_map.update(cx, |map, cx| {
|
|
||||||
let snapshot = map.snapshot(cx);
|
|
||||||
for (crease_id, crease) in snapshot.crease_snapshot.creases() {
|
|
||||||
// Skip creases that have been edited out of the message buffer.
|
|
||||||
if !crease.range().start.is_valid(&snapshot.buffer_snapshot) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(mention) = contents.get(&crease_id) {
|
|
||||||
let crease_range =
|
|
||||||
crease.range().to_offset(&snapshot.buffer_snapshot);
|
|
||||||
if crease_range.start > ix {
|
|
||||||
chunks.push(text[ix..crease_range.start].into());
|
|
||||||
}
|
|
||||||
chunks.push(acp::ContentBlock::Resource(acp::EmbeddedResource {
|
|
||||||
annotations: None,
|
|
||||||
resource: acp::EmbeddedResourceResource::TextResourceContents(
|
|
||||||
acp::TextResourceContents {
|
|
||||||
mime_type: None,
|
|
||||||
text: mention.content.clone(),
|
|
||||||
uri: mention.uri.to_uri(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
ix = crease_range.end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ix < text.len() {
|
|
||||||
let last_chunk = text[ix..].trim_end();
|
|
||||||
if !last_chunk.is_empty() {
|
|
||||||
chunks.push(last_chunk.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if chunks.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(thread) = this.thread() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx));
|
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
let result = task.await;
|
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
if let Err(err) = result {
|
|
||||||
this.last_error =
|
|
||||||
Some(cx.new(|cx| {
|
|
||||||
Markdown::new(err.to_string().into(), None, None, cx)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
let mention_set = this.mention_set.clone();
|
|
||||||
|
|
||||||
this.set_editor_is_expanded(false, cx);
|
this.set_editor_is_expanded(false, cx);
|
||||||
|
|
||||||
this.message_editor.update(cx, |editor, cx| {
|
|
||||||
editor.clear(window, cx);
|
|
||||||
editor.remove_creases(mention_set.lock().drain(), cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
this.scroll_to_bottom(cx);
|
this.scroll_to_bottom(cx);
|
||||||
|
this.message_editor.update(cx, |message_editor, cx| {
|
||||||
|
message_editor.clear(window, cx);
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
let send = thread.update(cx, |thread, cx| thread.send(contents, cx))?;
|
||||||
|
send.await
|
||||||
|
});
|
||||||
|
|
||||||
this.message_history.borrow_mut().push(chunks);
|
cx.spawn(async move |this, cx| {
|
||||||
})
|
if let Err(e) = task.await {
|
||||||
.ok();
|
this.update(cx, |this, cx| {
|
||||||
|
this.last_error =
|
||||||
|
Some(cx.new(|cx| Markdown::new(e.to_string().into(), None, None, cx)));
|
||||||
|
cx.notify()
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn previous_history_message(
|
|
||||||
&mut self,
|
|
||||||
_: &PreviousHistoryMessage,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if self.message_set_from_history.is_none() && !self.message_editor.read(cx).is_empty(cx) {
|
|
||||||
self.message_editor.update(cx, |editor, cx| {
|
|
||||||
editor.move_up(&Default::default(), window, cx);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.message_set_from_history = Self::set_draft_message(
|
|
||||||
self.message_editor.clone(),
|
|
||||||
self.mention_set.clone(),
|
|
||||||
self.project.clone(),
|
|
||||||
self.message_history
|
|
||||||
.borrow_mut()
|
|
||||||
.prev()
|
|
||||||
.map(|blocks| blocks.as_slice()),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_history_message(
|
|
||||||
&mut self,
|
|
||||||
_: &NextHistoryMessage,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if self.message_set_from_history.is_none() {
|
|
||||||
self.message_editor.update(cx, |editor, cx| {
|
|
||||||
editor.move_down(&Default::default(), window, cx);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut message_history = self.message_history.borrow_mut();
|
|
||||||
let next_history = message_history.next();
|
|
||||||
|
|
||||||
let set_draft_message = Self::set_draft_message(
|
|
||||||
self.message_editor.clone(),
|
|
||||||
self.mention_set.clone(),
|
|
||||||
self.project.clone(),
|
|
||||||
Some(
|
|
||||||
next_history
|
|
||||||
.map(|blocks| blocks.as_slice())
|
|
||||||
.unwrap_or_else(|| &[]),
|
|
||||||
),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
// If we reset the text to an empty string because we ran out of history,
|
|
||||||
// we don't want to mark it as coming from the history
|
|
||||||
self.message_set_from_history = if next_history.is_some() {
|
|
||||||
set_draft_message
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) {
|
fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if let Some(thread) = self.thread() {
|
if let Some(thread) = self.thread() {
|
||||||
AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err();
|
AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err();
|
||||||
|
@ -587,91 +382,6 @@ impl AcpThreadView {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_draft_message(
|
|
||||||
message_editor: Entity<Editor>,
|
|
||||||
mention_set: Arc<Mutex<MentionSet>>,
|
|
||||||
project: Entity<Project>,
|
|
||||||
message: Option<&[acp::ContentBlock]>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Option<BufferSnapshot> {
|
|
||||||
cx.notify();
|
|
||||||
|
|
||||||
let message = message?;
|
|
||||||
|
|
||||||
let mut text = String::new();
|
|
||||||
let mut mentions = Vec::new();
|
|
||||||
|
|
||||||
for chunk in message {
|
|
||||||
match chunk {
|
|
||||||
acp::ContentBlock::Text(text_content) => {
|
|
||||||
text.push_str(&text_content.text);
|
|
||||||
}
|
|
||||||
acp::ContentBlock::Resource(acp::EmbeddedResource {
|
|
||||||
resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
|
|
||||||
..
|
|
||||||
}) => {
|
|
||||||
let path = PathBuf::from(&resource.uri);
|
|
||||||
let project_path = project.read(cx).project_path_for_absolute_path(&path, cx);
|
|
||||||
let start = text.len();
|
|
||||||
let content = MentionUri::File(path).to_uri();
|
|
||||||
text.push_str(&content);
|
|
||||||
let end = text.len();
|
|
||||||
if let Some(project_path) = project_path {
|
|
||||||
let filename: SharedString = project_path
|
|
||||||
.path
|
|
||||||
.file_name()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string()
|
|
||||||
.into();
|
|
||||||
mentions.push((start..end, project_path, filename));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
acp::ContentBlock::Image(_)
|
|
||||||
| acp::ContentBlock::Audio(_)
|
|
||||||
| acp::ContentBlock::Resource(_)
|
|
||||||
| acp::ContentBlock::ResourceLink(_) => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let snapshot = message_editor.update(cx, |editor, cx| {
|
|
||||||
editor.set_text(text, window, cx);
|
|
||||||
editor.buffer().read(cx).snapshot(cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
for (range, project_path, filename) in mentions {
|
|
||||||
let crease_icon_path = if project_path.path.is_dir() {
|
|
||||||
FileIcons::get_folder_icon(false, cx)
|
|
||||||
.unwrap_or_else(|| IconName::Folder.path().into())
|
|
||||||
} else {
|
|
||||||
FileIcons::get_icon(Path::new(project_path.path.as_ref()), cx)
|
|
||||||
.unwrap_or_else(|| IconName::File.path().into())
|
|
||||||
};
|
|
||||||
|
|
||||||
let anchor = snapshot.anchor_before(range.start);
|
|
||||||
if let Some(project_path) = project.read(cx).absolute_path(&project_path, cx) {
|
|
||||||
let crease_id = crate::context_picker::insert_crease_for_mention(
|
|
||||||
anchor.excerpt_id,
|
|
||||||
anchor.text_anchor,
|
|
||||||
range.end - range.start,
|
|
||||||
filename,
|
|
||||||
crease_icon_path,
|
|
||||||
message_editor.clone(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(crease_id) = crease_id {
|
|
||||||
mention_set.lock().insert(crease_id, project_path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let snapshot = snapshot.as_singleton().unwrap().2.clone();
|
|
||||||
Some(snapshot.text)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_thread_event(
|
fn handle_thread_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
thread: &Entity<AcpThread>,
|
thread: &Entity<AcpThread>,
|
||||||
|
@ -2516,34 +2226,7 @@ impl AcpThreadView {
|
||||||
.size_full()
|
.size_full()
|
||||||
.pt_1()
|
.pt_1()
|
||||||
.pr_2p5()
|
.pr_2p5()
|
||||||
.child(div().flex_1().child({
|
.child(self.message_editor.clone())
|
||||||
let settings = ThemeSettings::get_global(cx);
|
|
||||||
let font_size = TextSize::Small
|
|
||||||
.rems(cx)
|
|
||||||
.to_pixels(settings.agent_font_size(cx));
|
|
||||||
let line_height = settings.buffer_line_height.value() * font_size;
|
|
||||||
|
|
||||||
let text_style = TextStyle {
|
|
||||||
color: cx.theme().colors().text,
|
|
||||||
font_family: settings.buffer_font.family.clone(),
|
|
||||||
font_fallbacks: settings.buffer_font.fallbacks.clone(),
|
|
||||||
font_features: settings.buffer_font.features.clone(),
|
|
||||||
font_size: font_size.into(),
|
|
||||||
line_height: line_height.into(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
EditorElement::new(
|
|
||||||
&self.message_editor,
|
|
||||||
EditorStyle {
|
|
||||||
background: editor_bg_color,
|
|
||||||
local_player: cx.theme().players().local(),
|
|
||||||
text: text_style,
|
|
||||||
syntax: cx.theme().syntax().clone(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.absolute()
|
.absolute()
|
||||||
|
@ -2604,7 +2287,7 @@ impl AcpThreadView {
|
||||||
button.tooltip(Tooltip::text("Type a message to submit"))
|
button.tooltip(Tooltip::text("Type a message to submit"))
|
||||||
})
|
})
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
this.chat(&Chat, window, cx);
|
this.chat(window, cx);
|
||||||
}))
|
}))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
} else {
|
} else {
|
||||||
|
@ -3067,55 +2750,11 @@ impl AcpThreadView {
|
||||||
paths: Vec<project::ProjectPath>,
|
paths: Vec<project::ProjectPath>,
|
||||||
_added_worktrees: Vec<Entity<project::Worktree>>,
|
_added_worktrees: Vec<Entity<project::Worktree>>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<'_, Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let buffer = self.message_editor.read(cx).buffer().clone();
|
self.message_editor.update(cx, |message_editor, cx| {
|
||||||
let Some((&excerpt_id, _, _)) = buffer.read(cx).snapshot(cx).as_singleton() else {
|
message_editor.insert_dragged_files(paths, window, cx);
|
||||||
return;
|
})
|
||||||
};
|
|
||||||
let Some(buffer) = buffer.read(cx).as_singleton() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
for path in paths {
|
|
||||||
let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
|
|
||||||
let path_prefix = abs_path
|
|
||||||
.file_name()
|
|
||||||
.unwrap_or(path.path.as_os_str())
|
|
||||||
.display()
|
|
||||||
.to_string();
|
|
||||||
let completion = ContextPickerCompletionProvider::completion_for_path(
|
|
||||||
path,
|
|
||||||
&path_prefix,
|
|
||||||
false,
|
|
||||||
entry.is_dir(),
|
|
||||||
excerpt_id,
|
|
||||||
anchor..anchor,
|
|
||||||
self.message_editor.clone(),
|
|
||||||
self.mention_set.clone(),
|
|
||||||
self.project.clone(),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
self.message_editor.update(cx, |message_editor, cx| {
|
|
||||||
message_editor.edit(
|
|
||||||
[(
|
|
||||||
multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
|
|
||||||
completion.new_text,
|
|
||||||
)],
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
if let Some(confirm) = completion.confirm.clone() {
|
|
||||||
confirm(CompletionIntent::Complete, window, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3132,9 +2771,6 @@ impl Render for AcpThreadView {
|
||||||
v_flex()
|
v_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
.key_context("AcpThread")
|
.key_context("AcpThread")
|
||||||
.on_action(cx.listener(Self::chat))
|
|
||||||
.on_action(cx.listener(Self::previous_history_message))
|
|
||||||
.on_action(cx.listener(Self::next_history_message))
|
|
||||||
.on_action(cx.listener(Self::open_agent_diff))
|
.on_action(cx.listener(Self::open_agent_diff))
|
||||||
.bg(cx.theme().colors().panel_background)
|
.bg(cx.theme().colors().panel_background)
|
||||||
.child(match &self.thread_state {
|
.child(match &self.thread_state {
|
||||||
|
@ -3430,18 +3066,17 @@ fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
pub(crate) mod tests {
|
||||||
|
use std::{path::Path, sync::Arc};
|
||||||
|
|
||||||
use agent_client_protocol::SessionId;
|
use agent_client_protocol::SessionId;
|
||||||
use editor::EditorSettings;
|
use editor::EditorSettings;
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
use futures::future::try_join_all;
|
use futures::future::try_join_all;
|
||||||
use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
|
use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
|
||||||
use lsp::{CompletionContext, CompletionTriggerKind};
|
use parking_lot::Mutex;
|
||||||
use project::CompletionIntent;
|
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use serde_json::json;
|
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use util::path;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -3469,7 +3104,7 @@ mod tests {
|
||||||
cx.deactivate_window();
|
cx.deactivate_window();
|
||||||
|
|
||||||
thread_view.update_in(cx, |thread_view, window, cx| {
|
thread_view.update_in(cx, |thread_view, window, cx| {
|
||||||
thread_view.chat(&Chat, window, cx);
|
thread_view.chat(window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
|
@ -3496,7 +3131,7 @@ mod tests {
|
||||||
cx.deactivate_window();
|
cx.deactivate_window();
|
||||||
|
|
||||||
thread_view.update_in(cx, |thread_view, window, cx| {
|
thread_view.update_in(cx, |thread_view, window, cx| {
|
||||||
thread_view.chat(&Chat, window, cx);
|
thread_view.chat(window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
|
@ -3542,7 +3177,7 @@ mod tests {
|
||||||
cx.deactivate_window();
|
cx.deactivate_window();
|
||||||
|
|
||||||
thread_view.update_in(cx, |thread_view, window, cx| {
|
thread_view.update_in(cx, |thread_view, window, cx| {
|
||||||
thread_view.chat(&Chat, window, cx);
|
thread_view.chat(window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
|
@ -3554,109 +3189,6 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_crease_removal(cx: &mut TestAppContext) {
|
|
||||||
init_test(cx);
|
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
|
||||||
fs.insert_tree("/project", json!({"file": ""})).await;
|
|
||||||
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
|
|
||||||
let agent = StubAgentServer::default();
|
|
||||||
let (workspace, cx) =
|
|
||||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
|
||||||
let thread_view = cx.update(|window, cx| {
|
|
||||||
cx.new(|cx| {
|
|
||||||
AcpThreadView::new(
|
|
||||||
Rc::new(agent),
|
|
||||||
workspace.downgrade(),
|
|
||||||
project,
|
|
||||||
Rc::new(RefCell::new(MessageHistory::default())),
|
|
||||||
1,
|
|
||||||
None,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
|
|
||||||
let excerpt_id = message_editor.update(cx, |editor, cx| {
|
|
||||||
editor
|
|
||||||
.buffer()
|
|
||||||
.read(cx)
|
|
||||||
.excerpt_ids()
|
|
||||||
.into_iter()
|
|
||||||
.next()
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
let completions = message_editor.update_in(cx, |editor, window, cx| {
|
|
||||||
editor.set_text("Hello @", window, cx);
|
|
||||||
let buffer = editor.buffer().read(cx).as_singleton().unwrap();
|
|
||||||
let completion_provider = editor.completion_provider().unwrap();
|
|
||||||
completion_provider.completions(
|
|
||||||
excerpt_id,
|
|
||||||
&buffer,
|
|
||||||
Anchor::MAX,
|
|
||||||
CompletionContext {
|
|
||||||
trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
|
|
||||||
trigger_character: Some("@".into()),
|
|
||||||
},
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let [_, completion]: [_; 2] = completions
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|response| response.completions)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.try_into()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
message_editor.update_in(cx, |editor, window, cx| {
|
|
||||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
|
||||||
let start = snapshot
|
|
||||||
.anchor_in_excerpt(excerpt_id, completion.replace_range.start)
|
|
||||||
.unwrap();
|
|
||||||
let end = snapshot
|
|
||||||
.anchor_in_excerpt(excerpt_id, completion.replace_range.end)
|
|
||||||
.unwrap();
|
|
||||||
editor.edit([(start..end, completion.new_text)], cx);
|
|
||||||
(completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
// Backspace over the inserted crease (and the following space).
|
|
||||||
message_editor.update_in(cx, |editor, window, cx| {
|
|
||||||
editor.backspace(&Default::default(), window, cx);
|
|
||||||
editor.backspace(&Default::default(), window, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
thread_view.update_in(cx, |thread_view, window, cx| {
|
|
||||||
thread_view.chat(&Chat, window, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
let content = thread_view.update_in(cx, |thread_view, _window, _cx| {
|
|
||||||
thread_view
|
|
||||||
.message_history
|
|
||||||
.borrow()
|
|
||||||
.items()
|
|
||||||
.iter()
|
|
||||||
.flatten()
|
|
||||||
.cloned()
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
});
|
|
||||||
|
|
||||||
// We don't send a resource link for the deleted crease.
|
|
||||||
pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn setup_thread_view(
|
async fn setup_thread_view(
|
||||||
agent: impl AgentServer + 'static,
|
agent: impl AgentServer + 'static,
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
|
@ -3668,16 +3200,7 @@ mod tests {
|
||||||
|
|
||||||
let thread_view = cx.update(|window, cx| {
|
let thread_view = cx.update(|window, cx| {
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
AcpThreadView::new(
|
AcpThreadView::new(Rc::new(agent), workspace.downgrade(), project, window, cx)
|
||||||
Rc::new(agent),
|
|
||||||
workspace.downgrade(),
|
|
||||||
project,
|
|
||||||
Rc::new(RefCell::new(MessageHistory::default())),
|
|
||||||
1,
|
|
||||||
None,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
|
@ -3888,7 +3411,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_test(cx: &mut TestAppContext) {
|
pub(crate) fn init_test(cx: &mut TestAppContext) {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::ops::{Not, Range};
|
use std::ops::{Not, Range};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
@ -11,7 +10,6 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::NewExternalAgentThread;
|
use crate::NewExternalAgentThread;
|
||||||
use crate::agent_diff::AgentDiffThread;
|
use crate::agent_diff::AgentDiffThread;
|
||||||
use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
|
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
|
||||||
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
|
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
|
||||||
|
@ -477,8 +475,6 @@ pub struct AgentPanel {
|
||||||
configuration_subscription: Option<Subscription>,
|
configuration_subscription: Option<Subscription>,
|
||||||
local_timezone: UtcOffset,
|
local_timezone: UtcOffset,
|
||||||
active_view: ActiveView,
|
active_view: ActiveView,
|
||||||
acp_message_history:
|
|
||||||
Rc<RefCell<crate::acp::MessageHistory<Vec<agent_client_protocol::ContentBlock>>>>,
|
|
||||||
previous_view: Option<ActiveView>,
|
previous_view: Option<ActiveView>,
|
||||||
history_store: Entity<HistoryStore>,
|
history_store: Entity<HistoryStore>,
|
||||||
history: Entity<ThreadHistory>,
|
history: Entity<ThreadHistory>,
|
||||||
|
@ -766,7 +762,6 @@ impl AgentPanel {
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
inline_assist_context_store,
|
inline_assist_context_store,
|
||||||
previous_view: None,
|
previous_view: None,
|
||||||
acp_message_history: Default::default(),
|
|
||||||
history_store: history_store.clone(),
|
history_store: history_store.clone(),
|
||||||
history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
|
history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
|
||||||
hovered_recent_history_item: None,
|
hovered_recent_history_item: None,
|
||||||
|
@ -963,7 +958,6 @@ impl AgentPanel {
|
||||||
) {
|
) {
|
||||||
let workspace = self.workspace.clone();
|
let workspace = self.workspace.clone();
|
||||||
let project = self.project.clone();
|
let project = self.project.clone();
|
||||||
let message_history = self.acp_message_history.clone();
|
|
||||||
let fs = self.fs.clone();
|
let fs = self.fs.clone();
|
||||||
|
|
||||||
const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
|
const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
|
||||||
|
@ -1007,16 +1001,7 @@ impl AgentPanel {
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
let thread_view = cx.new(|cx| {
|
let thread_view = cx.new(|cx| {
|
||||||
crate::acp::AcpThreadView::new(
|
crate::acp::AcpThreadView::new(server, workspace.clone(), project, window, cx)
|
||||||
server,
|
|
||||||
workspace.clone(),
|
|
||||||
project,
|
|
||||||
message_history,
|
|
||||||
MIN_EDITOR_LINES,
|
|
||||||
Some(MAX_EDITOR_LINES),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.set_active_view(ActiveView::ExternalAgentThread { thread_view }, window, cx);
|
this.set_active_view(ActiveView::ExternalAgentThread { thread_view }, window, cx);
|
||||||
|
@ -1570,8 +1555,6 @@ impl AgentPanel {
|
||||||
self.active_view = new_view;
|
self.active_view = new_view;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.acp_message_history.borrow_mut().reset_position();
|
|
||||||
|
|
||||||
self.focus_handle(cx).focus(window);
|
self.focus_handle(cx).focus(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -285,10 +285,6 @@ pub mod agent {
|
||||||
ResetOnboarding,
|
ResetOnboarding,
|
||||||
/// Starts a chat conversation with the agent.
|
/// Starts a chat conversation with the agent.
|
||||||
Chat,
|
Chat,
|
||||||
/// Displays the previous message in the history.
|
|
||||||
PreviousHistoryMessage,
|
|
||||||
/// Displays the next message in the history.
|
|
||||||
NextHistoryMessage,
|
|
||||||
/// Toggles the language model selector dropdown.
|
/// Toggles the language model selector dropdown.
|
||||||
#[action(deprecated_aliases = ["assistant::ToggleModelSelector", "assistant2::ToggleModelSelector"])]
|
#[action(deprecated_aliases = ["assistant::ToggleModelSelector", "assistant2::ToggleModelSelector"])]
|
||||||
ToggleModelSelector
|
ToggleModelSelector
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue