acp: Rename assistant::QuoteSelection
and support it in agent2 threads (#36628)
Release Notes: - N/A
This commit is contained in:
parent
b070dc66b3
commit
1ee07a4baf
11 changed files with 148 additions and 79 deletions
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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,
|
||||||
]
|
]
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
@ -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`
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue