Allow editing Agent2 messages (#36155)

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
This commit is contained in:
Conrad Irwin 2025-08-14 07:39:33 -06:00 committed by GitHub
parent ffac8c5128
commit e5402d5464
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 956 additions and 621 deletions

View file

@ -12,34 +12,25 @@ use audio::{Audio, Sound};
use buffer_diff::BufferDiff;
use collections::{HashMap, HashSet};
use editor::scroll::Autoscroll;
use editor::{
AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode,
EditorStyle, MinimapVisibility, MultiBuffer, PathKey, SelectionEffects,
};
use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, SelectionEffects};
use file_icons::FileIcons;
use gpui::{
Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, PlatformDisplay,
SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement,
Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop,
linear_gradient, list, percentage, point, prelude::*, pulsating_between,
Action, Animation, AnimationExt, App, BorderStyle, ClickEvent, EdgesRefinement, Empty, Entity,
EntityId, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton,
PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle,
TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div,
linear_color_stop, linear_gradient, list, percentage, point, prelude::*, pulsating_between,
};
use language::Buffer;
use language::language_settings::SoftWrap;
use language::{Buffer, Language};
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
use parking_lot::Mutex;
use project::{CompletionIntent, Project};
use project::Project;
use prompt_store::PromptId;
use rope::Point;
use settings::{Settings as _, SettingsStore};
use std::fmt::Write as _;
use std::path::PathBuf;
use std::{
cell::RefCell, collections::BTreeMap, path::Path, process::ExitStatus, rc::Rc, sync::Arc,
time::Duration,
};
use std::{collections::BTreeMap, process::ExitStatus, rc::Rc, time::Duration};
use terminal_view::TerminalView;
use text::{Anchor, BufferSnapshot};
use text::Anchor;
use theme::ThemeSettings;
use ui::{
Disclosure, Divider, DividerColor, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState,
@ -47,14 +38,12 @@ use ui::{
};
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{CollaboratorId, Workspace};
use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage, ToggleModelSelector};
use zed_actions::agent::{Chat, ToggleModelSelector};
use zed_actions::assistant::OpenRulesLibrary;
use crate::acp::AcpModelSelectorPopover;
use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
use crate::acp::message_history::MessageHistory;
use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
use crate::agent_diff::AgentDiff;
use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
use crate::ui::{AgentNotification, AgentNotificationEvent};
use crate::{
AgentDiffPane, AgentPanel, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll,
@ -62,6 +51,9 @@ use crate::{
const RESPONSE_PADDING_X: Pixels = px(19.);
pub const MIN_EDITOR_LINES: usize = 4;
pub const MAX_EDITOR_LINES: usize = 8;
pub struct AcpThreadView {
agent: Rc<dyn AgentServer>,
workspace: WeakEntity<Workspace>,
@ -71,11 +63,8 @@ pub struct AcpThreadView {
thread_state: ThreadState,
diff_editors: HashMap<EntityId, Entity<Editor>>,
terminal_views: HashMap<EntityId, Entity<TerminalView>>,
message_editor: Entity<Editor>,
message_editor: Entity<MessageEditor>,
model_selector: Option<Entity<AcpModelSelectorPopover>>,
message_set_from_history: Option<BufferSnapshot>,
_message_editor_subscription: Subscription,
mention_set: Arc<Mutex<MentionSet>>,
notifications: Vec<WindowHandle<AgentNotification>>,
notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
last_error: Option<Entity<Markdown>>,
@ -88,9 +77,16 @@ pub struct AcpThreadView {
plan_expanded: bool,
editor_expanded: bool,
terminal_expanded: bool,
message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
editing_message: Option<EditingMessage>,
_cancel_task: Option<Task<()>>,
_subscriptions: [Subscription; 1],
_subscriptions: [Subscription; 2],
}
struct EditingMessage {
index: usize,
message_id: UserMessageId,
editor: Entity<MessageEditor>,
_subscription: Subscription,
}
enum ThreadState {
@ -117,83 +113,30 @@ impl AcpThreadView {
project: Entity<Project>,
thread_store: Entity<ThreadStore>,
text_thread_store: Entity<TextThreadStore>,
message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
min_lines: usize,
max_lines: Option<usize>,
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 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(
MessageEditor::new(
workspace.clone(),
project.clone(),
thread_store.clone(),
text_thread_store.clone(),
editor::EditorMode::AutoHeight {
min_lines,
max_lines: max_lines,
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.clone(),
thread_store.downgrade(),
text_thread_store.downgrade(),
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 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 {
agent: agent.clone(),
@ -204,9 +147,6 @@ impl AcpThreadView {
thread_state: Self::initial_state(agent, workspace, project, window, cx),
message_editor,
model_selector: None,
message_set_from_history: None,
_message_editor_subscription: message_editor_subscription,
mention_set,
notifications: Vec::new(),
notification_subscriptions: HashMap::default(),
diff_editors: Default::default(),
@ -217,12 +157,12 @@ impl AcpThreadView {
auth_task: None,
expanded_tool_calls: HashSet::default(),
expanded_thinking_blocks: HashSet::default(),
editing_message: None,
edits_expanded: false,
plan_expanded: false,
editor_expanded: false,
terminal_expanded: true,
message_history,
_subscriptions: [subscription],
_subscriptions: subscriptions,
_cancel_task: None,
}
}
@ -370,7 +310,7 @@ impl AcpThreadView {
}
}
pub fn cancel(&mut self, cx: &mut Context<Self>) {
pub fn cancel_generation(&mut self, cx: &mut Context<Self>) {
self.last_error.take();
if let Some(thread) = self.thread() {
@ -390,193 +330,118 @@ impl AcpThreadView {
fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
self.editor_expanded = is_expanded;
self.message_editor.update(cx, |editor, _| {
if self.editor_expanded {
editor.set_mode(EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
sized_by_content: false,
})
self.message_editor.update(cx, |editor, cx| {
if is_expanded {
editor.set_mode(
EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
sized_by_content: false,
},
cx,
)
} else {
editor.set_mode(EditorMode::AutoHeight {
min_lines: MIN_EDITOR_LINES,
max_lines: Some(MAX_EDITOR_LINES),
})
editor.set_mode(
EditorMode::AutoHeight {
min_lines: MIN_EDITOR_LINES,
max_lines: Some(MAX_EDITOR_LINES),
},
cx,
)
}
});
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::Send => self.send(window, cx),
MessageEditorEvent::Cancel => self.cancel_generation(cx),
}
}
fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let contents = self
.message_editor
.update(cx, |message_editor, cx| message_editor.contents(window, cx));
self.send_impl(contents, window, cx)
}
fn send_impl(
&mut self,
contents: Task<anyhow::Result<Vec<acp::ContentBlock>>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.last_error.take();
self.editing_message.take();
let mut ix = 0;
let mut chunks: Vec<acp::ContentBlock> = Vec::new();
let project = self.project.clone();
let Some(thread) = self.thread().cloned() else {
return;
};
let task = cx.spawn_in(window, async move |this, cx| {
let contents = contents.await?;
let thread_store = self.thread_store.clone();
let text_thread_store = self.text_thread_store.clone();
let contents =
self.mention_set
.lock()
.contents(project, thread_store, text_thread_store, window, cx);
cx.spawn_in(window, async move |this, cx| {
let contents = match contents.await {
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;
}
};
if contents.is_empty() {
return Ok(());
}
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().to_string(),
},
),
}));
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.message_editor.update(cx, |editor, cx| {
editor.clear(window, cx);
editor.remove_creases(mention_set.lock().drain(), 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);
})
.ok();
cx.spawn(async move |this, cx| {
if let Err(e) = task.await {
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();
}
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 cancel_editing(&mut self, _: &ClickEvent, _window: &mut Window, cx: &mut Context<Self>) {
self.editing_message.take();
cx.notify();
}
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);
});
fn regenerate(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
let Some(editing_message) = self.editing_message.take() else {
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
};
let Some(thread) = self.thread().cloned() else {
return;
};
let rewind = thread.update(cx, |thread, cx| {
thread.rewind(editing_message.message_id, cx)
});
let contents = editing_message
.editor
.update(cx, |message_editor, cx| message_editor.contents(window, cx));
let task = cx.foreground_executor().spawn(async move {
rewind.await?;
contents.await
});
self.send_impl(task, window, cx);
}
fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) {
@ -606,92 +471,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 _ = write!(&mut text, "{}", MentionUri::File(path).to_uri());
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, MentionUri::File(project_path));
}
}
}
let snapshot = snapshot.as_singleton().unwrap().2.clone();
Some(snapshot.text)
}
fn handle_thread_event(
&mut self,
thread: &Entity<AcpThread>,
@ -968,12 +747,28 @@ impl AcpThreadView {
.border_1()
.border_color(cx.theme().colors().border)
.text_xs()
.children(message.content.markdown().map(|md| {
self.render_markdown(
md.clone(),
user_message_markdown_style(window, cx),
)
})),
.id("message")
.on_click(cx.listener({
move |this, _, window, cx| this.start_editing_message(index, window, cx)
}))
.children(
if let Some(editing) = self.editing_message.as_ref()
&& Some(&editing.message_id) == message.id.as_ref()
{
Some(
self.render_edit_message_editor(editing, cx)
.into_any_element(),
)
} else {
message.content.markdown().map(|md| {
self.render_markdown(
md.clone(),
user_message_markdown_style(window, cx),
)
.into_any_element()
})
},
),
)
.into_any(),
AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
@ -1035,7 +830,7 @@ impl AcpThreadView {
};
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
if index == total_entries - 1 && !is_generating {
let primary = if index == total_entries - 1 && !is_generating {
v_flex()
.w_full()
.child(primary)
@ -1043,6 +838,28 @@ impl AcpThreadView {
.into_any_element()
} else {
primary
};
if let Some(editing) = self.editing_message.as_ref()
&& editing.index < index
{
let backdrop = div()
.id(("backdrop", index))
.size_full()
.absolute()
.inset_0()
.bg(cx.theme().colors().panel_background)
.opacity(0.8)
.block_mouse_except_scroll()
.on_click(cx.listener(Self::cancel_editing));
div()
.relative()
.child(backdrop)
.child(primary)
.into_any_element()
} else {
primary
}
}
@ -2561,34 +2378,7 @@ impl AcpThreadView {
.size_full()
.pt_1()
.pr_2p5()
.child(div().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.message_editor,
EditorStyle {
background: editor_bg_color,
local_player: cx.theme().players().local(),
text: text_style,
syntax: cx.theme().syntax().clone(),
..Default::default()
},
)
}))
.child(self.message_editor.clone())
.child(
h_flex()
.absolute()
@ -2633,6 +2423,129 @@ impl AcpThreadView {
.into_any()
}
fn start_editing_message(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
let Some(thread) = self.thread() else {
return;
};
let Some(AgentThreadEntry::UserMessage(message)) = thread.read(cx).entries().get(index)
else {
return;
};
let Some(message_id) = message.id.clone() else {
return;
};
self.list_state.scroll_to_reveal_item(index);
let chunks = message.chunks.clone();
let editor = cx.new(|cx| {
let mut editor = MessageEditor::new(
self.workspace.clone(),
self.project.clone(),
self.thread_store.clone(),
self.text_thread_store.clone(),
editor::EditorMode::AutoHeight {
min_lines: 1,
max_lines: None,
},
window,
cx,
);
editor.set_message(&chunks, window, cx);
editor
});
let subscription =
cx.subscribe_in(&editor, window, |this, _, event, window, cx| match event {
MessageEditorEvent::Send => {
this.regenerate(&Default::default(), window, cx);
}
MessageEditorEvent::Cancel => {
this.cancel_editing(&Default::default(), window, cx);
}
});
editor.focus_handle(cx).focus(window);
self.editing_message.replace(EditingMessage {
index: index,
message_id: message_id.clone(),
editor,
_subscription: subscription,
});
cx.notify();
}
fn render_edit_message_editor(&self, editing: &EditingMessage, cx: &Context<Self>) -> Div {
v_flex()
.w_full()
.gap_2()
.child(editing.editor.clone())
.child(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Warning)
.color(Color::Warning)
.size(IconSize::XSmall),
)
.child(
Label::new("Editing will restart the thread from this point.")
.color(Color::Muted)
.size(LabelSize::XSmall),
)
.child(self.render_editing_message_editor_buttons(editing, cx)),
)
}
fn render_editing_message_editor_buttons(
&self,
editing: &EditingMessage,
cx: &Context<Self>,
) -> Div {
h_flex()
.gap_0p5()
.flex_1()
.justify_end()
.child(
IconButton::new("cancel-edit-message", IconName::Close)
.shape(ui::IconButtonShape::Square)
.icon_color(Color::Error)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = editing.editor.focus_handle(cx);
move |window, cx| {
Tooltip::for_action_in(
"Cancel Edit",
&menu::Cancel,
&focus_handle,
window,
cx,
)
}
})
.on_click(cx.listener(Self::cancel_editing)),
)
.child(
IconButton::new("confirm-edit-message", IconName::Return)
.disabled(editing.editor.read(cx).is_empty(cx))
.shape(ui::IconButtonShape::Square)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = editing.editor.focus_handle(cx);
move |window, cx| {
Tooltip::for_action_in(
"Regenerate",
&menu::Confirm,
&focus_handle,
window,
cx,
)
}
})
.on_click(cx.listener(Self::regenerate)),
)
}
fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
if self.thread().map_or(true, |thread| {
thread.read(cx).status() == ThreadStatus::Idle
@ -2649,7 +2562,7 @@ impl AcpThreadView {
button.tooltip(Tooltip::text("Type a message to submit"))
})
.on_click(cx.listener(|this, _, window, cx| {
this.chat(&Chat, window, cx);
this.send(window, cx);
}))
.into_any_element()
} else {
@ -2659,7 +2572,7 @@ impl AcpThreadView {
.tooltip(move |window, cx| {
Tooltip::for_action("Stop Generation", &editor::actions::Cancel, window, cx)
})
.on_click(cx.listener(|this, _event, _, cx| this.cancel(cx)))
.on_click(cx.listener(|this, _event, _, cx| this.cancel_generation(cx)))
.into_any_element()
}
}
@ -2723,10 +2636,10 @@ impl AcpThreadView {
if let Some(mention) = MentionUri::parse(&url).log_err() {
workspace.update(cx, |workspace, cx| match mention {
MentionUri::File(path) => {
MentionUri::File { abs_path, .. } => {
let project = workspace.project();
let Some((path, entry)) = project.update(cx, |project, cx| {
let path = project.find_project_path(path, cx)?;
let path = project.find_project_path(abs_path, cx)?;
let entry = project.entry_for_path(&path, cx)?;
Some((path, entry))
}) else {
@ -3175,57 +3088,11 @@ impl AcpThreadView {
paths: Vec<project::ProjectPath>,
_added_worktrees: Vec<Entity<project::Worktree>>,
window: &mut Window,
cx: &mut Context<'_, Self>,
cx: &mut Context<Self>,
) {
let buffer = self.message_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 Some(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,
) else {
continue;
};
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);
}
}
self.message_editor.update(cx, |message_editor, cx| {
message_editor.insert_dragged_files(paths, window, cx);
})
}
}
@ -3242,9 +3109,6 @@ impl Render for AcpThreadView {
v_flex()
.size_full()
.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))
.bg(cx.theme().colors().panel_background)
.child(match &self.thread_state {
@ -3540,13 +3404,16 @@ fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
}
#[cfg(test)]
mod tests {
pub(crate) mod tests {
use std::{path::Path, sync::Arc};
use agent::{TextThreadStore, ThreadStore};
use agent_client_protocol::SessionId;
use editor::EditorSettings;
use fs::FakeFs;
use futures::future::try_join_all;
use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
use parking_lot::Mutex;
use rand::Rng;
use settings::SettingsStore;
@ -3576,7 +3443,7 @@ mod tests {
cx.deactivate_window();
thread_view.update_in(cx, |thread_view, window, cx| {
thread_view.chat(&Chat, window, cx);
thread_view.send(window, cx);
});
cx.run_until_parked();
@ -3603,7 +3470,7 @@ mod tests {
cx.deactivate_window();
thread_view.update_in(cx, |thread_view, window, cx| {
thread_view.chat(&Chat, window, cx);
thread_view.send(window, cx);
});
cx.run_until_parked();
@ -3649,7 +3516,7 @@ mod tests {
cx.deactivate_window();
thread_view.update_in(cx, |thread_view, window, cx| {
thread_view.chat(&Chat, window, cx);
thread_view.send(window, cx);
});
cx.run_until_parked();
@ -3683,9 +3550,6 @@ mod tests {
project,
thread_store.clone(),
text_thread_store.clone(),
Rc::new(RefCell::new(MessageHistory::default())),
1,
None,
window,
cx,
)
@ -3899,7 +3763,7 @@ mod tests {
}
}
fn init_test(cx: &mut TestAppContext) {
pub(crate) fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);