acp: Rename assistant::QuoteSelection and support it in agent2 threads (#36628)

Release Notes:

- N/A
This commit is contained in:
Cole Miller 2025-08-20 17:31:25 -04:00 committed by Joseph T. Lyons
parent b070dc66b3
commit 1ee07a4baf
11 changed files with 148 additions and 79 deletions

View file

@ -138,7 +138,7 @@
"find": "buffer_search::Deploy", "find": "buffer_search::Deploy",
"ctrl-f": "buffer_search::Deploy", "ctrl-f": "buffer_search::Deploy",
"ctrl-h": "buffer_search::DeployReplace", "ctrl-h": "buffer_search::DeployReplace",
"ctrl->": "assistant::QuoteSelection", "ctrl->": "agent::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor", "ctrl-<": "assistant::InsertIntoEditor",
"ctrl-alt-e": "editor::SelectEnclosingSymbol", "ctrl-alt-e": "editor::SelectEnclosingSymbol",
"ctrl-shift-backspace": "editor::GoToPreviousChange", "ctrl-shift-backspace": "editor::GoToPreviousChange",
@ -241,7 +241,7 @@
"ctrl-shift-i": "agent::ToggleOptionsMenu", "ctrl-shift-i": "agent::ToggleOptionsMenu",
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu", "ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor", "shift-alt-escape": "agent::ExpandMessageEditor",
"ctrl->": "assistant::QuoteSelection", "ctrl->": "agent::QuoteSelection",
"ctrl-alt-e": "agent::RemoveAllContext", "ctrl-alt-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus", "ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-enter": "agent::ContinueThread", "ctrl-shift-enter": "agent::ContinueThread",

View file

@ -162,7 +162,7 @@
"cmd-alt-f": "buffer_search::DeployReplace", "cmd-alt-f": "buffer_search::DeployReplace",
"cmd-alt-l": ["buffer_search::Deploy", { "selection_search_enabled": true }], "cmd-alt-l": ["buffer_search::Deploy", { "selection_search_enabled": true }],
"cmd-e": ["buffer_search::Deploy", { "focus": false }], "cmd-e": ["buffer_search::Deploy", { "focus": false }],
"cmd->": "assistant::QuoteSelection", "cmd->": "agent::QuoteSelection",
"cmd-<": "assistant::InsertIntoEditor", "cmd-<": "assistant::InsertIntoEditor",
"cmd-alt-e": "editor::SelectEnclosingSymbol", "cmd-alt-e": "editor::SelectEnclosingSymbol",
"alt-enter": "editor::OpenSelectionsInMultibuffer" "alt-enter": "editor::OpenSelectionsInMultibuffer"
@ -281,7 +281,7 @@
"cmd-shift-i": "agent::ToggleOptionsMenu", "cmd-shift-i": "agent::ToggleOptionsMenu",
"cmd-alt-shift-n": "agent::ToggleNewThreadMenu", "cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor", "shift-alt-escape": "agent::ExpandMessageEditor",
"cmd->": "assistant::QuoteSelection", "cmd->": "agent::QuoteSelection",
"cmd-alt-e": "agent::RemoveAllContext", "cmd-alt-e": "agent::RemoveAllContext",
"cmd-shift-e": "project_panel::ToggleFocus", "cmd-shift-e": "project_panel::ToggleFocus",
"cmd-ctrl-b": "agent::ToggleBurnMode", "cmd-ctrl-b": "agent::ToggleBurnMode",

View file

@ -17,8 +17,8 @@
"bindings": { "bindings": {
"ctrl-i": "agent::ToggleFocus", "ctrl-i": "agent::ToggleFocus",
"ctrl-shift-i": "agent::ToggleFocus", "ctrl-shift-i": "agent::ToggleFocus",
"ctrl-shift-l": "assistant::QuoteSelection", // In cursor uses "Ask" mode "ctrl-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode
"ctrl-l": "assistant::QuoteSelection", // In cursor uses "Agent" mode "ctrl-l": "agent::QuoteSelection", // In cursor uses "Agent" mode
"ctrl-k": "assistant::InlineAssist", "ctrl-k": "assistant::InlineAssist",
"ctrl-shift-k": "assistant::InsertIntoEditor" "ctrl-shift-k": "assistant::InsertIntoEditor"
} }

View file

@ -17,8 +17,8 @@
"bindings": { "bindings": {
"cmd-i": "agent::ToggleFocus", "cmd-i": "agent::ToggleFocus",
"cmd-shift-i": "agent::ToggleFocus", "cmd-shift-i": "agent::ToggleFocus",
"cmd-shift-l": "assistant::QuoteSelection", // In cursor uses "Ask" mode "cmd-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode
"cmd-l": "assistant::QuoteSelection", // In cursor uses "Agent" mode "cmd-l": "agent::QuoteSelection", // In cursor uses "Agent" mode
"cmd-k": "assistant::InlineAssist", "cmd-k": "assistant::InlineAssist",
"cmd-shift-k": "assistant::InsertIntoEditor" "cmd-shift-k": "assistant::InsertIntoEditor"
} }

View file

@ -108,62 +108,7 @@ impl ContextPickerCompletionProvider {
confirm: Some(Arc::new(|_, _, _| true)), confirm: Some(Arc::new(|_, _, _| true)),
}), }),
ContextPickerEntry::Action(action) => { ContextPickerEntry::Action(action) => {
let (new_text, on_action) = match action { Self::completion_for_action(action, source_range, message_editor, workspace, cx)
ContextPickerAction::AddSelections => {
const PLACEHOLDER: &str = "selection ";
let selections = selection_ranges(workspace, cx)
.into_iter()
.enumerate()
.map(|(ix, (buffer, range))| {
(
buffer,
range,
(PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1),
)
})
.collect::<Vec<_>>();
let new_text: String = PLACEHOLDER.repeat(selections.len());
let callback = Arc::new({
let source_range = source_range.clone();
move |_, window: &mut Window, cx: &mut App| {
let selections = selections.clone();
let message_editor = message_editor.clone();
let source_range = source_range.clone();
window.defer(cx, move |window, cx| {
message_editor
.update(cx, |message_editor, cx| {
message_editor.confirm_mention_for_selection(
source_range,
selections,
window,
cx,
)
})
.ok();
});
false
}
});
(new_text, callback)
}
};
Some(Completion {
replace_range: source_range,
new_text,
label: CodeLabel::plain(action.label().to_string(), None),
icon_path: Some(action.icon().path().into()),
documentation: None,
source: project::CompletionSource::Custom,
insert_text_mode: None,
// This ensures that when a user accepts this completion, the
// completion menu will still be shown after "@category " is
// inserted
confirm: Some(on_action),
})
} }
} }
} }
@ -359,6 +304,71 @@ impl ContextPickerCompletionProvider {
}) })
} }
pub(crate) fn completion_for_action(
action: ContextPickerAction,
source_range: Range<Anchor>,
message_editor: WeakEntity<MessageEditor>,
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Option<Completion> {
let (new_text, on_action) = match action {
ContextPickerAction::AddSelections => {
const PLACEHOLDER: &str = "selection ";
let selections = selection_ranges(workspace, cx)
.into_iter()
.enumerate()
.map(|(ix, (buffer, range))| {
(
buffer,
range,
(PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1),
)
})
.collect::<Vec<_>>();
let new_text: String = PLACEHOLDER.repeat(selections.len());
let callback = Arc::new({
let source_range = source_range.clone();
move |_, window: &mut Window, cx: &mut App| {
let selections = selections.clone();
let message_editor = message_editor.clone();
let source_range = source_range.clone();
window.defer(cx, move |window, cx| {
message_editor
.update(cx, |message_editor, cx| {
message_editor.confirm_mention_for_selection(
source_range,
selections,
window,
cx,
)
})
.ok();
});
false
}
});
(new_text, callback)
}
};
Some(Completion {
replace_range: source_range,
new_text,
label: CodeLabel::plain(action.label().to_string(), None),
icon_path: Some(action.icon().path().into()),
documentation: None,
source: project::CompletionSource::Custom,
insert_text_mode: None,
// This ensures that when a user accepts this completion, the
// completion menu will still be shown after "@category " is
// inserted
confirm: Some(on_action),
})
}
fn search( fn search(
&self, &self,
mode: Option<ContextPickerMode>, mode: Option<ContextPickerMode>,

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
acp::completion_provider::ContextPickerCompletionProvider, acp::completion_provider::ContextPickerCompletionProvider,
context_picker::fetch_context_picker::fetch_url_content, context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content},
}; };
use acp_thread::{MentionUri, selection_name}; use acp_thread::{MentionUri, selection_name};
use agent_client_protocol as acp; use agent_client_protocol as acp;
@ -27,7 +27,7 @@ use gpui::{
}; };
use language::{Buffer, Language}; use language::{Buffer, Language};
use language_model::LanguageModelImage; use language_model::LanguageModelImage;
use project::{Project, ProjectPath, Worktree}; use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree};
use prompt_store::PromptStore; use prompt_store::PromptStore;
use rope::Point; use rope::Point;
use settings::Settings; use settings::Settings;
@ -561,21 +561,24 @@ impl MessageEditor {
let range = snapshot.anchor_after(offset + range_to_fold.start) let range = snapshot.anchor_after(offset + range_to_fold.start)
..snapshot.anchor_after(offset + range_to_fold.end); ..snapshot.anchor_after(offset + range_to_fold.end);
let path = buffer // TODO support selections from buffers with no path
.read(cx) let Some(project_path) = buffer.read(cx).project_path(cx) else {
.file() continue;
.map_or(PathBuf::from("untitled"), |file| file.path().to_path_buf()); };
let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else {
continue;
};
let snapshot = buffer.read(cx).snapshot(); let snapshot = buffer.read(cx).snapshot();
let point_range = selection_range.to_point(&snapshot); let point_range = selection_range.to_point(&snapshot);
let line_range = point_range.start.row..point_range.end.row; let line_range = point_range.start.row..point_range.end.row;
let uri = MentionUri::Selection { let uri = MentionUri::Selection {
path: path.clone(), path: abs_path.clone(),
line_range: line_range.clone(), line_range: line_range.clone(),
}; };
let crease = crate::context_picker::crease_for_mention( let crease = crate::context_picker::crease_for_mention(
selection_name(&path, &line_range).into(), selection_name(&abs_path, &line_range).into(),
uri.icon_path(cx), uri.icon_path(cx),
range, range,
self.editor.downgrade(), self.editor.downgrade(),
@ -587,8 +590,7 @@ impl MessageEditor {
crease_ids.first().copied().unwrap() crease_ids.first().copied().unwrap()
}); });
self.mention_set self.mention_set.insert_uri(crease_id, uri);
.insert_uri(crease_id, MentionUri::Selection { path, line_range });
} }
} }
@ -948,6 +950,38 @@ impl MessageEditor {
.detach(); .detach();
} }
pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let buffer = self.editor.read(cx).buffer().clone();
let Some(buffer) = buffer.read(cx).as_singleton() else {
return;
};
let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let Some(completion) = ContextPickerCompletionProvider::completion_for_action(
ContextPickerAction::AddSelections,
anchor..anchor,
cx.weak_entity(),
&workspace,
cx,
) else {
return;
};
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 {
confirm(CompletionIntent::Complete, window, cx);
}
}
pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) { pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
self.editor.update(cx, |message_editor, cx| { self.editor.update(cx, |message_editor, cx| {
message_editor.set_read_only(read_only); message_editor.set_read_only(read_only);

View file

@ -4097,6 +4097,12 @@ impl AcpThreadView {
}) })
} }
pub(crate) fn insert_selections(&self, window: &mut Window, cx: &mut Context<Self>) {
self.message_editor.update(cx, |message_editor, cx| {
message_editor.insert_selections(window, cx);
})
}
fn render_thread_retry_status_callout( fn render_thread_retry_status_callout(
&self, &self,
_window: &mut Window, _window: &mut Window,

View file

@ -903,6 +903,16 @@ impl AgentPanel {
} }
} }
fn active_thread_view(&self) -> Option<&Entity<AcpThreadView>> {
match &self.active_view {
ActiveView::ExternalAgentThread { thread_view } => Some(thread_view),
ActiveView::Thread { .. }
| ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => None,
}
}
fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) { fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
if cx.has_flag::<GeminiAndNativeFeatureFlag>() { if cx.has_flag::<GeminiAndNativeFeatureFlag>() {
return self.new_agent_thread(AgentType::NativeAgent, window, cx); return self.new_agent_thread(AgentType::NativeAgent, window, cx);
@ -3882,7 +3892,11 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
// Wait to create a new context until the workspace is no longer // Wait to create a new context until the workspace is no longer
// being updated. // being updated.
cx.defer_in(window, move |panel, window, cx| { cx.defer_in(window, move |panel, window, cx| {
if let Some(message_editor) = panel.active_message_editor() { if let Some(thread_view) = panel.active_thread_view() {
thread_view.update(cx, |thread_view, cx| {
thread_view.insert_selections(window, cx);
});
} else if let Some(message_editor) = panel.active_message_editor() {
message_editor.update(cx, |message_editor, cx| { message_editor.update(cx, |message_editor, cx| {
message_editor.context_store().update(cx, |store, cx| { message_editor.context_store().update(cx, |store, cx| {
let buffer = buffer.read(cx); let buffer = buffer.read(cx);

View file

@ -128,6 +128,12 @@ actions!(
] ]
); );
#[derive(Clone, Copy, Debug, PartialEq, Eq, Action)]
#[action(namespace = agent)]
#[action(deprecated_aliases = ["assistant::QuoteSelection"])]
/// Quotes the current selection in the agent panel's message editor.
pub struct QuoteSelection;
/// Creates a new conversation thread, optionally based on an existing thread. /// Creates a new conversation thread, optionally based on an existing thread.
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)] #[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
#[action(namespace = agent)] #[action(namespace = agent)]

View file

@ -1,4 +1,5 @@
use crate::{ use crate::{
QuoteSelection,
language_model_selector::{LanguageModelSelector, language_model_selector}, language_model_selector::{LanguageModelSelector, language_model_selector},
ui::BurnModeTooltip, ui::BurnModeTooltip,
}; };
@ -89,8 +90,6 @@ actions!(
CycleMessageRole, CycleMessageRole,
/// Inserts the selected text into the active editor. /// Inserts the selected text into the active editor.
InsertIntoEditor, InsertIntoEditor,
/// Quotes the current selection in the assistant conversation.
QuoteSelection,
/// Splits the conversation at the current cursor position. /// Splits the conversation at the current cursor position.
Split, Split,
] ]

View file

@ -16,7 +16,7 @@ To begin, type a message in a `You` block.
As you type, the remaining tokens count for the selected model is updated. As you type, the remaining tokens count for the selected model is updated.
Inserting text from an editor is as simple as highlighting the text and running `assistant: quote selection` ({#kb assistant::QuoteSelection}); Zed will wrap it in a fenced code block if it is code. Inserting text from an editor is as simple as highlighting the text and running `agent: quote selection` ({#kb agent::QuoteSelection}); Zed will wrap it in a fenced code block if it is code.
![Quoting a selection](https://zed.dev/img/assistant/quoting-a-selection.png) ![Quoting a selection](https://zed.dev/img/assistant/quoting-a-selection.png)
@ -148,7 +148,7 @@ Usage: `/terminal [<number>]`
The `/selection` command inserts the selected text in the editor into the context. This is useful for referencing specific parts of your code. The `/selection` command inserts the selected text in the editor into the context. This is useful for referencing specific parts of your code.
This is equivalent to the `assistant: quote selection` command ({#kb assistant::QuoteSelection}). This is equivalent to the `agent: quote selection` command ({#kb agent::QuoteSelection}).
Usage: `/selection` Usage: `/selection`