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

View file

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

View file

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

View file

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

View file

@ -108,62 +108,7 @@ impl ContextPickerCompletionProvider {
confirm: Some(Arc::new(|_, _, _| true)),
}),
ContextPickerEntry::Action(action) => {
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),
})
Self::completion_for_action(action, source_range, message_editor, workspace, cx)
}
}
}
@ -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(
&self,
mode: Option<ContextPickerMode>,

View file

@ -1,6 +1,6 @@
use crate::{
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 agent_client_protocol as acp;
@ -27,7 +27,7 @@ use gpui::{
};
use language::{Buffer, Language};
use language_model::LanguageModelImage;
use project::{Project, ProjectPath, Worktree};
use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree};
use prompt_store::PromptStore;
use rope::Point;
use settings::Settings;
@ -561,21 +561,24 @@ impl MessageEditor {
let range = snapshot.anchor_after(offset + range_to_fold.start)
..snapshot.anchor_after(offset + range_to_fold.end);
let path = buffer
.read(cx)
.file()
.map_or(PathBuf::from("untitled"), |file| file.path().to_path_buf());
// TODO support selections from buffers with no path
let Some(project_path) = buffer.read(cx).project_path(cx) else {
continue;
};
let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else {
continue;
};
let snapshot = buffer.read(cx).snapshot();
let point_range = selection_range.to_point(&snapshot);
let line_range = point_range.start.row..point_range.end.row;
let uri = MentionUri::Selection {
path: path.clone(),
path: abs_path.clone(),
line_range: line_range.clone(),
};
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),
range,
self.editor.downgrade(),
@ -587,8 +590,7 @@ impl MessageEditor {
crease_ids.first().copied().unwrap()
});
self.mention_set
.insert_uri(crease_id, MentionUri::Selection { path, line_range });
self.mention_set.insert_uri(crease_id, uri);
}
}
@ -948,6 +950,38 @@ impl MessageEditor {
.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>) {
self.editor.update(cx, |message_editor, cx| {
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(
&self,
_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>) {
if cx.has_flag::<GeminiAndNativeFeatureFlag>() {
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
// being updated.
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.context_store().update(cx, |store, 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.
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
#[action(namespace = agent)]

View file

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