mirror of
https://github.com/openai/codex.git
synced 2026-10-29 19:08:43 +00:00
prototyping
This commit is contained in:
@@ -271,9 +271,9 @@ use crate::render::renderable::FlexRenderable;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::render::renderable::RenderableExt;
|
||||
use crate::render::renderable::RenderableItem;
|
||||
use crate::slash_command::FastSlashCommandArgs;
|
||||
use crate::slash_command::ParsedSlashCommand;
|
||||
use crate::slash_command::SlashCommand;
|
||||
use crate::slash_command_invocation::FastSlashCommandArgs;
|
||||
use crate::slash_command_invocation::SlashCommandInvocation;
|
||||
use crate::status::RateLimitSnapshotDisplay;
|
||||
use crate::status_indicator_widget::STATUS_DETAILS_DEFAULT_MAX_LINES;
|
||||
use crate::status_indicator_widget::StatusDetailsCapitalization;
|
||||
@@ -4734,11 +4734,28 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_inline_command_invocation(
|
||||
&mut self,
|
||||
cmd: SlashCommand,
|
||||
record_history: bool,
|
||||
) -> Option<SlashCommandInvocation> {
|
||||
let (prepared_args, prepared_elements) = self
|
||||
.bottom_pane
|
||||
.prepare_inline_args_submission(record_history)?;
|
||||
match cmd.parse_invocation(&prepared_args, &prepared_elements) {
|
||||
Ok(invocation) => Some(invocation),
|
||||
Err(err) => {
|
||||
self.add_error_message(err.message());
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_command_with_args(
|
||||
&mut self,
|
||||
cmd: SlashCommand,
|
||||
args: String,
|
||||
_text_elements: Vec<TextElement>,
|
||||
text_elements: Vec<TextElement>,
|
||||
) {
|
||||
if !cmd.available_during_task() && self.bottom_pane.is_task_running() {
|
||||
let message = format!(
|
||||
@@ -4750,17 +4767,17 @@ impl ChatWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
match cmd.parse_invocation(&args) {
|
||||
Ok(ParsedSlashCommand::Bare(_)) => {
|
||||
match cmd.parse_invocation(&args, &text_elements) {
|
||||
Ok(SlashCommandInvocation::Bare(_)) => {
|
||||
self.dispatch_command(cmd);
|
||||
}
|
||||
Ok(ParsedSlashCommand::Fast(FastSlashCommandArgs::On)) => {
|
||||
Ok(SlashCommandInvocation::Fast(FastSlashCommandArgs::On)) => {
|
||||
self.set_service_tier_selection(Some(ServiceTier::Fast));
|
||||
}
|
||||
Ok(ParsedSlashCommand::Fast(FastSlashCommandArgs::Off)) => {
|
||||
Ok(SlashCommandInvocation::Fast(FastSlashCommandArgs::Off)) => {
|
||||
self.set_service_tier_selection(/*service_tier*/ None);
|
||||
}
|
||||
Ok(ParsedSlashCommand::Fast(FastSlashCommandArgs::Status)) => {
|
||||
Ok(SlashCommandInvocation::Fast(FastSlashCommandArgs::Status)) => {
|
||||
let status = if matches!(self.config.service_tier, Some(ServiceTier::Fast)) {
|
||||
"on"
|
||||
} else {
|
||||
@@ -4768,16 +4785,16 @@ impl ChatWidget {
|
||||
};
|
||||
self.add_info_message(format!("Fast mode is {status}."), /*hint*/ None);
|
||||
}
|
||||
Ok(ParsedSlashCommand::Rename) => {
|
||||
Ok(SlashCommandInvocation::Rename(_)) => {
|
||||
self.session_telemetry
|
||||
.counter("codex.thread.rename", /*inc*/ 1, &[]);
|
||||
let Some((prepared_args, _prepared_elements)) = self
|
||||
.bottom_pane
|
||||
.prepare_inline_args_submission(/*record_history*/ false)
|
||||
let Some(SlashCommandInvocation::Rename(prepared_args)) =
|
||||
self.prepare_inline_command_invocation(cmd, /*record_history*/ false)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(name) = codex_core::util::normalize_thread_name(&prepared_args) else {
|
||||
let Some(name) = codex_core::util::normalize_thread_name(&prepared_args.text)
|
||||
else {
|
||||
self.add_error_message("Thread name cannot be empty.".to_string());
|
||||
return;
|
||||
};
|
||||
@@ -4788,14 +4805,13 @@ impl ChatWidget {
|
||||
.send(AppEvent::CodexOp(Op::SetThreadName { name }));
|
||||
self.bottom_pane.drain_pending_submission_state();
|
||||
}
|
||||
Ok(ParsedSlashCommand::Plan) => {
|
||||
Ok(SlashCommandInvocation::Plan(_)) => {
|
||||
self.dispatch_command(cmd);
|
||||
if self.active_mode_kind() != ModeKind::Plan {
|
||||
return;
|
||||
}
|
||||
let Some((prepared_args, prepared_elements)) = self
|
||||
.bottom_pane
|
||||
.prepare_inline_args_submission(/*record_history*/ true)
|
||||
let Some(SlashCommandInvocation::Plan(prepared_args)) =
|
||||
self.prepare_inline_command_invocation(cmd, /*record_history*/ true)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
@@ -4803,11 +4819,15 @@ impl ChatWidget {
|
||||
.bottom_pane
|
||||
.take_recent_submission_images_with_placeholders();
|
||||
let remote_image_urls = self.take_remote_image_urls();
|
||||
let crate::slash_command_invocation::SlashCommandTextArg {
|
||||
text,
|
||||
text_elements,
|
||||
} = prepared_args;
|
||||
let user_message = UserMessage {
|
||||
text: prepared_args,
|
||||
text,
|
||||
local_images,
|
||||
remote_image_urls,
|
||||
text_elements: prepared_elements,
|
||||
text_elements,
|
||||
mention_bindings: self.bottom_pane.take_recent_submission_mention_bindings(),
|
||||
};
|
||||
if self.is_session_configured() {
|
||||
@@ -4819,33 +4839,31 @@ impl ChatWidget {
|
||||
self.queue_user_message(user_message);
|
||||
}
|
||||
}
|
||||
Ok(ParsedSlashCommand::Review) => {
|
||||
let Some((prepared_args, _prepared_elements)) = self
|
||||
.bottom_pane
|
||||
.prepare_inline_args_submission(/*record_history*/ false)
|
||||
Ok(SlashCommandInvocation::Review(_)) => {
|
||||
let Some(SlashCommandInvocation::Review(prepared_args)) =
|
||||
self.prepare_inline_command_invocation(cmd, /*record_history*/ false)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
self.submit_op(Op::Review {
|
||||
review_request: ReviewRequest {
|
||||
target: ReviewTarget::Custom {
|
||||
instructions: prepared_args,
|
||||
instructions: prepared_args.text,
|
||||
},
|
||||
user_facing_hint: None,
|
||||
},
|
||||
});
|
||||
self.bottom_pane.drain_pending_submission_state();
|
||||
}
|
||||
Ok(ParsedSlashCommand::SandboxReadRoot) => {
|
||||
let Some((prepared_args, _prepared_elements)) = self
|
||||
.bottom_pane
|
||||
.prepare_inline_args_submission(/*record_history*/ false)
|
||||
Ok(SlashCommandInvocation::SandboxReadRoot(_)) => {
|
||||
let Some(SlashCommandInvocation::SandboxReadRoot(prepared_args)) =
|
||||
self.prepare_inline_command_invocation(cmd, /*record_history*/ false)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
self.app_event_tx
|
||||
.send(AppEvent::BeginWindowsSandboxGrantReadRoot {
|
||||
path: prepared_args,
|
||||
path: prepared_args.text,
|
||||
});
|
||||
self.bottom_pane.drain_pending_submission_state();
|
||||
}
|
||||
|
||||
+152
-101
@@ -4,6 +4,12 @@ use strum_macros::EnumIter;
|
||||
use strum_macros::EnumString;
|
||||
use strum_macros::IntoStaticStr;
|
||||
|
||||
use codex_protocol::user_input::TextElement;
|
||||
|
||||
use crate::slash_command_invocation::FastSlashCommandArgs;
|
||||
use crate::slash_command_invocation::SlashCommandInvocation;
|
||||
use crate::slash_command_invocation::SlashCommandTextArg;
|
||||
|
||||
/// Commands that can be invoked by starting a message with a leading slash.
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Hash, EnumString, EnumIter, AsRefStr, IntoStaticStr,
|
||||
@@ -72,74 +78,119 @@ pub(crate) enum SlashCommandBareBehavior {
|
||||
OpensUi,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum FastSlashCommandArgs {
|
||||
On,
|
||||
Off,
|
||||
Status,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum ParsedSlashCommand {
|
||||
Bare(SlashCommandBareBehavior),
|
||||
Fast(FastSlashCommandArgs),
|
||||
Rename,
|
||||
Plan,
|
||||
Review,
|
||||
SandboxReadRoot,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum SlashCommandUsageErrorKind {
|
||||
UnexpectedInlineArgs,
|
||||
InvalidInlineArgs,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum SlashCommandArgsParser {
|
||||
NoArgs,
|
||||
Remainder(ParsedSlashCommand),
|
||||
Tokens(&'static [SlashCommandTokenChoice]),
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct SlashCommandParseInput<'a> {
|
||||
pub(crate) args: &'a str,
|
||||
pub(crate) text_elements: &'a [TextElement],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct SlashCommandTokenChoice {
|
||||
pub(crate) token: &'static str,
|
||||
pub(crate) parsed: ParsedSlashCommand,
|
||||
}
|
||||
pub(crate) type SlashCommandParser =
|
||||
for<'a> fn(
|
||||
SlashCommand,
|
||||
SlashCommandParseInput<'a>,
|
||||
) -> Result<SlashCommandInvocation, SlashCommandUsageErrorKind>;
|
||||
|
||||
const FAST_TOKEN_CHOICES: &[SlashCommandTokenChoice] = &[
|
||||
SlashCommandTokenChoice {
|
||||
token: "on",
|
||||
parsed: ParsedSlashCommand::Fast(FastSlashCommandArgs::On),
|
||||
},
|
||||
SlashCommandTokenChoice {
|
||||
token: "off",
|
||||
parsed: ParsedSlashCommand::Fast(FastSlashCommandArgs::Off),
|
||||
},
|
||||
SlashCommandTokenChoice {
|
||||
token: "status",
|
||||
parsed: ParsedSlashCommand::Fast(FastSlashCommandArgs::Status),
|
||||
},
|
||||
];
|
||||
|
||||
impl SlashCommandArgsParser {
|
||||
pub(crate) fn parse(
|
||||
self,
|
||||
args: &str,
|
||||
) -> Result<ParsedSlashCommand, SlashCommandUsageErrorKind> {
|
||||
match self {
|
||||
SlashCommandArgsParser::NoArgs => Err(SlashCommandUsageErrorKind::UnexpectedInlineArgs),
|
||||
SlashCommandArgsParser::Remainder(parsed) => Ok(parsed),
|
||||
SlashCommandArgsParser::Tokens(choices) => choices
|
||||
.iter()
|
||||
.find(|choice| choice.token.eq_ignore_ascii_case(args))
|
||||
.map(|choice| choice.parsed)
|
||||
.ok_or(SlashCommandUsageErrorKind::InvalidInlineArgs),
|
||||
}
|
||||
fn parse_no_args(
|
||||
command: SlashCommand,
|
||||
input: SlashCommandParseInput<'_>,
|
||||
) -> Result<SlashCommandInvocation, SlashCommandUsageErrorKind> {
|
||||
if input.args.trim().is_empty() {
|
||||
Ok(SlashCommandInvocation::Bare(command))
|
||||
} else {
|
||||
Err(SlashCommandUsageErrorKind::UnexpectedInlineArgs)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_fast(
|
||||
command: SlashCommand,
|
||||
input: SlashCommandParseInput<'_>,
|
||||
) -> Result<SlashCommandInvocation, SlashCommandUsageErrorKind> {
|
||||
if !input.text_elements.is_empty() {
|
||||
return Err(SlashCommandUsageErrorKind::InvalidInlineArgs);
|
||||
}
|
||||
match input.args.trim() {
|
||||
"" => Ok(SlashCommandInvocation::Bare(command)),
|
||||
args if args.eq_ignore_ascii_case("on") => {
|
||||
Ok(SlashCommandInvocation::Fast(FastSlashCommandArgs::On))
|
||||
}
|
||||
args if args.eq_ignore_ascii_case("off") => {
|
||||
Ok(SlashCommandInvocation::Fast(FastSlashCommandArgs::Off))
|
||||
}
|
||||
args if args.eq_ignore_ascii_case("status") => {
|
||||
Ok(SlashCommandInvocation::Fast(FastSlashCommandArgs::Status))
|
||||
}
|
||||
_ => Err(SlashCommandUsageErrorKind::InvalidInlineArgs),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_text_arg(
|
||||
command: SlashCommand,
|
||||
input: SlashCommandParseInput<'_>,
|
||||
) -> Result<SlashCommandInvocation, SlashCommandUsageErrorKind> {
|
||||
let args = input.args.trim();
|
||||
if args.is_empty() {
|
||||
return Ok(SlashCommandInvocation::Bare(command));
|
||||
}
|
||||
|
||||
let arg = SlashCommandTextArg::new(args.to_string(), input.text_elements.to_vec());
|
||||
|
||||
match command {
|
||||
SlashCommand::Rename => Ok(SlashCommandInvocation::Rename(arg)),
|
||||
SlashCommand::Plan => Ok(SlashCommandInvocation::Plan(arg)),
|
||||
SlashCommand::Review => Ok(SlashCommandInvocation::Review(arg)),
|
||||
SlashCommand::SandboxReadRoot => Ok(SlashCommandInvocation::SandboxReadRoot(arg)),
|
||||
_ => Err(SlashCommandUsageErrorKind::InvalidInlineArgs),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_rich_text(
|
||||
command: SlashCommand,
|
||||
input: SlashCommandParseInput<'_>,
|
||||
) -> Result<SlashCommandInvocation, SlashCommandUsageErrorKind> {
|
||||
parse_text_arg(command, input)
|
||||
}
|
||||
|
||||
fn parse_model(
|
||||
command: SlashCommand,
|
||||
input: SlashCommandParseInput<'_>,
|
||||
) -> Result<SlashCommandInvocation, SlashCommandUsageErrorKind> {
|
||||
parse_no_args(command, input)
|
||||
}
|
||||
|
||||
fn parse_ui_only(
|
||||
command: SlashCommand,
|
||||
input: SlashCommandParseInput<'_>,
|
||||
) -> Result<SlashCommandInvocation, SlashCommandUsageErrorKind> {
|
||||
parse_no_args(command, input)
|
||||
}
|
||||
|
||||
fn parse_dispatch_only(
|
||||
command: SlashCommand,
|
||||
input: SlashCommandParseInput<'_>,
|
||||
) -> Result<SlashCommandInvocation, SlashCommandUsageErrorKind> {
|
||||
parse_no_args(command, input)
|
||||
}
|
||||
|
||||
fn parse_fast_command(
|
||||
command: SlashCommand,
|
||||
input: SlashCommandParseInput<'_>,
|
||||
) -> Result<SlashCommandInvocation, SlashCommandUsageErrorKind> {
|
||||
parse_fast(command, input)
|
||||
}
|
||||
|
||||
fn parse_rich_text_command(
|
||||
command: SlashCommand,
|
||||
input: SlashCommandParseInput<'_>,
|
||||
) -> Result<SlashCommandInvocation, SlashCommandUsageErrorKind> {
|
||||
parse_rich_text(command, input)
|
||||
}
|
||||
|
||||
impl SlashCommand {
|
||||
pub(crate) fn spec(self) -> SlashCommandSpec {
|
||||
match self {
|
||||
@@ -150,7 +201,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/model"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_model,
|
||||
},
|
||||
SlashCommand::Fast => SlashCommandSpec {
|
||||
description: "toggle Fast mode to enable fastest inference at 2X plan usage",
|
||||
@@ -159,7 +210,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/fast", "/fast [on|off|status]"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::Tokens(FAST_TOKEN_CHOICES),
|
||||
parser: parse_fast_command,
|
||||
},
|
||||
SlashCommand::Approvals => SlashCommandSpec {
|
||||
description: "choose what Codex is allowed to do",
|
||||
@@ -168,7 +219,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: true,
|
||||
usage_lines: &["/approvals"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::Permissions => SlashCommandSpec {
|
||||
description: "choose what Codex is allowed to do",
|
||||
@@ -177,7 +228,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/permissions"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::ElevateSandbox => SlashCommandSpec {
|
||||
description: "set up elevated agent sandbox",
|
||||
@@ -186,7 +237,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/setup-default-sandbox"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::SandboxReadRoot => SlashCommandSpec {
|
||||
description: "let sandbox read a directory: /sandbox-add-read-dir <absolute_path>",
|
||||
@@ -195,7 +246,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/sandbox-add-read-dir <absolute-path>"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::Remainder(ParsedSlashCommand::SandboxReadRoot),
|
||||
parser: parse_rich_text_command,
|
||||
},
|
||||
SlashCommand::Experimental => SlashCommandSpec {
|
||||
description: "toggle experimental features",
|
||||
@@ -204,7 +255,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/experimental"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::Skills => SlashCommandSpec {
|
||||
description: "use skills to improve how Codex performs specific tasks",
|
||||
@@ -213,7 +264,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/skills"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::Review => SlashCommandSpec {
|
||||
description: "review my current changes and find issues",
|
||||
@@ -222,7 +273,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/review", "/review <instructions>"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::Remainder(ParsedSlashCommand::Review),
|
||||
parser: parse_rich_text_command,
|
||||
},
|
||||
SlashCommand::Rename => SlashCommandSpec {
|
||||
description: "rename the current thread",
|
||||
@@ -231,7 +282,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/rename", "/rename <title>"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::Remainder(ParsedSlashCommand::Rename),
|
||||
parser: parse_rich_text_command,
|
||||
},
|
||||
SlashCommand::New => SlashCommandSpec {
|
||||
description: "start a new chat during a conversation",
|
||||
@@ -240,7 +291,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/new"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Resume => SlashCommandSpec {
|
||||
description: "resume a saved chat",
|
||||
@@ -249,7 +300,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/resume"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::Fork => SlashCommandSpec {
|
||||
description: "fork the current chat",
|
||||
@@ -258,7 +309,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/fork"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Init => SlashCommandSpec {
|
||||
description: "create an AGENTS.md file with instructions for Codex",
|
||||
@@ -267,7 +318,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/init"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Compact => SlashCommandSpec {
|
||||
description: "summarize conversation to prevent hitting the context limit",
|
||||
@@ -276,7 +327,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/compact"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Plan => SlashCommandSpec {
|
||||
description: "switch to Plan mode",
|
||||
@@ -285,7 +336,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/plan", "/plan <prompt>"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::Remainder(ParsedSlashCommand::Plan),
|
||||
parser: parse_rich_text_command,
|
||||
},
|
||||
SlashCommand::Collab => SlashCommandSpec {
|
||||
description: "change collaboration mode (experimental)",
|
||||
@@ -294,7 +345,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/collab"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::Agent => SlashCommandSpec {
|
||||
description: "switch the active agent thread",
|
||||
@@ -303,7 +354,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/agent"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::Diff => SlashCommandSpec {
|
||||
description: "show git diff (including untracked files)",
|
||||
@@ -312,7 +363,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/diff"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Copy => SlashCommandSpec {
|
||||
description: "copy the latest Codex output to your clipboard",
|
||||
@@ -321,7 +372,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/copy"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Mention => SlashCommandSpec {
|
||||
description: "mention a file",
|
||||
@@ -330,7 +381,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/mention"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Status => SlashCommandSpec {
|
||||
description: "show current session configuration and token usage",
|
||||
@@ -339,7 +390,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/status"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::DebugConfig => SlashCommandSpec {
|
||||
description: "show config layers and requirement sources for debugging",
|
||||
@@ -348,7 +399,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/debug-config"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Title => SlashCommandSpec {
|
||||
description: "configure which items appear in the terminal title",
|
||||
@@ -357,7 +408,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/title"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Statusline => SlashCommandSpec {
|
||||
description: "configure which items appear in the status line",
|
||||
@@ -366,7 +417,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/statusline"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::Theme => SlashCommandSpec {
|
||||
description: "choose a syntax highlighting theme",
|
||||
@@ -375,7 +426,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/theme"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::Mcp => SlashCommandSpec {
|
||||
description: "list configured MCP tools",
|
||||
@@ -384,7 +435,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/mcp"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Apps => SlashCommandSpec {
|
||||
description: "manage apps",
|
||||
@@ -393,7 +444,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/apps"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Plugins => SlashCommandSpec {
|
||||
description: "browse plugins",
|
||||
@@ -402,7 +453,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/plugins"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Logout => SlashCommandSpec {
|
||||
description: "log out of Codex",
|
||||
@@ -411,7 +462,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/logout"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Quit => SlashCommandSpec {
|
||||
description: "exit Codex",
|
||||
@@ -420,7 +471,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: true,
|
||||
usage_lines: &["/quit"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Exit => SlashCommandSpec {
|
||||
description: "exit Codex",
|
||||
@@ -429,7 +480,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/exit"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Feedback => SlashCommandSpec {
|
||||
description: "send logs to maintainers",
|
||||
@@ -438,7 +489,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/feedback"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::Rollout => SlashCommandSpec {
|
||||
description: "print the rollout file path",
|
||||
@@ -447,7 +498,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/rollout"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Ps => SlashCommandSpec {
|
||||
description: "list background terminals",
|
||||
@@ -456,7 +507,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/ps"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Stop => SlashCommandSpec {
|
||||
description: "stop all background terminals",
|
||||
@@ -465,7 +516,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/stop"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Clear => SlashCommandSpec {
|
||||
description: "clear the terminal and start a new chat",
|
||||
@@ -474,7 +525,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/clear"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Personality => SlashCommandSpec {
|
||||
description: "choose a communication style for Codex",
|
||||
@@ -483,7 +534,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/personality"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::Realtime => SlashCommandSpec {
|
||||
description: "toggle realtime voice mode (experimental)",
|
||||
@@ -492,7 +543,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/realtime"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Settings => SlashCommandSpec {
|
||||
description: "configure realtime microphone/speaker",
|
||||
@@ -501,7 +552,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/settings"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::TestApproval => SlashCommandSpec {
|
||||
description: "test approval request",
|
||||
@@ -510,7 +561,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/test-approval"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::MultiAgents => SlashCommandSpec {
|
||||
description: "switch the active agent thread",
|
||||
@@ -519,7 +570,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/subagents"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::MemoryDrop => SlashCommandSpec {
|
||||
description: "DO NOT USE",
|
||||
@@ -528,7 +579,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/debug-m-drop"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::MemoryUpdate => SlashCommandSpec {
|
||||
description: "DO NOT USE",
|
||||
@@ -537,7 +588,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/debug-m-update"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -591,7 +642,7 @@ pub(crate) struct SlashCommandSpec {
|
||||
pub(crate) hide_in_command_popup: bool,
|
||||
pub(crate) usage_lines: &'static [&'static str],
|
||||
pub(crate) bare_behavior: SlashCommandBareBehavior,
|
||||
pub(crate) args_parser: SlashCommandArgsParser,
|
||||
pub(crate) parser: SlashCommandParser,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use crate::slash_command::ParsedSlashCommand;
|
||||
use codex_protocol::user_input::TextElement;
|
||||
|
||||
use crate::slash_command::SlashCommand;
|
||||
use crate::slash_command::SlashCommandParseInput;
|
||||
use crate::slash_command::SlashCommandUsageErrorKind;
|
||||
use crate::slash_command_invocation::SlashCommandInvocation;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct SlashCommandUsageError {
|
||||
@@ -25,19 +28,29 @@ impl SlashCommand {
|
||||
pub(crate) fn parse_invocation(
|
||||
self,
|
||||
args: &str,
|
||||
) -> Result<ParsedSlashCommand, SlashCommandUsageError> {
|
||||
let trimmed = args.trim();
|
||||
text_elements: &[TextElement],
|
||||
) -> Result<SlashCommandInvocation, SlashCommandUsageError> {
|
||||
let spec = self.spec();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(ParsedSlashCommand::Bare(spec.bare_behavior));
|
||||
if args.trim().is_empty() {
|
||||
return match spec.bare_behavior {
|
||||
crate::slash_command::SlashCommandBareBehavior::DispatchesDirectly
|
||||
| crate::slash_command::SlashCommandBareBehavior::OpensUi => {
|
||||
Ok(SlashCommandInvocation::Bare(self))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
spec.args_parser
|
||||
.parse(trimmed)
|
||||
.map_err(|kind| SlashCommandUsageError {
|
||||
command: self,
|
||||
kind,
|
||||
})
|
||||
(spec.parser)(
|
||||
self,
|
||||
SlashCommandParseInput {
|
||||
args,
|
||||
text_elements,
|
||||
},
|
||||
)
|
||||
.map_err(|kind| SlashCommandUsageError {
|
||||
command: self,
|
||||
kind,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,22 +59,41 @@ mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
use crate::slash_command::FastSlashCommandArgs;
|
||||
use crate::slash_command::SlashCommandBareBehavior;
|
||||
use crate::slash_command_invocation::FastSlashCommandArgs;
|
||||
use crate::slash_command_invocation::SlashCommandTextArg;
|
||||
|
||||
#[test]
|
||||
fn review_bare_form_is_marked_as_ui_driven() {
|
||||
assert_eq!(
|
||||
SlashCommand::Review.parse_invocation(""),
|
||||
Ok(ParsedSlashCommand::Bare(SlashCommandBareBehavior::OpensUi))
|
||||
SlashCommand::Review.parse_invocation("", &[]),
|
||||
Ok(SlashCommandInvocation::Bare(SlashCommand::Review))
|
||||
);
|
||||
assert_eq!(
|
||||
SlashCommand::Review.spec().bare_behavior,
|
||||
SlashCommandBareBehavior::OpensUi
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_accepts_nonempty_inline_args() {
|
||||
assert_eq!(
|
||||
SlashCommand::Fast.parse_invocation("status"),
|
||||
Ok(ParsedSlashCommand::Fast(FastSlashCommandArgs::Status))
|
||||
SlashCommand::Fast.parse_invocation("status", &[]),
|
||||
Ok(SlashCommandInvocation::Fast(FastSlashCommandArgs::Status))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_preserves_placeholder_elements() {
|
||||
let placeholder = "[Image #1]".to_string();
|
||||
let text_elements = vec![TextElement::new((0..11).into(), Some(placeholder.clone()))];
|
||||
|
||||
assert_eq!(
|
||||
SlashCommand::Review.parse_invocation(&placeholder, &text_elements),
|
||||
Ok(SlashCommandInvocation::Review(SlashCommandTextArg::new(
|
||||
placeholder,
|
||||
text_elements,
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,7 +101,7 @@ mod tests {
|
||||
fn clear_rejects_unexpected_inline_args() {
|
||||
assert_eq!(
|
||||
SlashCommand::Clear
|
||||
.parse_invocation("now")
|
||||
.parse_invocation("now", &[])
|
||||
.unwrap_err()
|
||||
.message(),
|
||||
"'/clear' does not accept inline arguments. Usage: /clear"
|
||||
|
||||
@@ -1,40 +1,166 @@
|
||||
use codex_protocol::user_input::ByteRange;
|
||||
use codex_protocol::user_input::TextElement;
|
||||
|
||||
use crate::slash_command::SlashCommand;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct SlashCommandInvocation {
|
||||
command: SlashCommand,
|
||||
args: Vec<String>,
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum FastSlashCommandArgs {
|
||||
On,
|
||||
Off,
|
||||
Status,
|
||||
}
|
||||
|
||||
impl FastSlashCommandArgs {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
FastSlashCommandArgs::On => "on",
|
||||
FastSlashCommandArgs::Off => "off",
|
||||
FastSlashCommandArgs::Status => "status",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct SlashCommandTextArg {
|
||||
pub(crate) text: String,
|
||||
pub(crate) text_elements: Vec<TextElement>,
|
||||
}
|
||||
|
||||
impl SlashCommandTextArg {
|
||||
pub(crate) fn new(text: String, text_elements: Vec<TextElement>) -> Self {
|
||||
Self {
|
||||
text,
|
||||
text_elements,
|
||||
}
|
||||
}
|
||||
|
||||
fn with_prefix(&self, prefix: &str) -> SerializedSlashCommand {
|
||||
let prefix_len = prefix.len();
|
||||
let text = if self.text.is_empty() {
|
||||
prefix.to_string()
|
||||
} else {
|
||||
format!("{prefix} {}", self.text)
|
||||
};
|
||||
let offset = prefix_len + usize::from(!self.text.is_empty());
|
||||
let text_elements = self
|
||||
.text_elements
|
||||
.iter()
|
||||
.map(|element| {
|
||||
element.map_range(|byte_range| ByteRange {
|
||||
start: byte_range.start + offset,
|
||||
end: byte_range.end + offset,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
SerializedSlashCommand {
|
||||
text,
|
||||
text_elements,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum SlashCommandInvocation {
|
||||
Bare(SlashCommand),
|
||||
Fast(FastSlashCommandArgs),
|
||||
Rename(SlashCommandTextArg),
|
||||
Plan(SlashCommandTextArg),
|
||||
Review(SlashCommandTextArg),
|
||||
SandboxReadRoot(SlashCommandTextArg),
|
||||
}
|
||||
|
||||
impl SlashCommandInvocation {
|
||||
pub(crate) fn bare(command: SlashCommand) -> Self {
|
||||
Self {
|
||||
command,
|
||||
args: Vec::new(),
|
||||
Self::Bare(command)
|
||||
}
|
||||
|
||||
pub(crate) fn command(&self) -> SlashCommand {
|
||||
match self {
|
||||
SlashCommandInvocation::Bare(command) => *command,
|
||||
SlashCommandInvocation::Fast(_) => SlashCommand::Fast,
|
||||
SlashCommandInvocation::Rename(_) => SlashCommand::Rename,
|
||||
SlashCommandInvocation::Plan(_) => SlashCommand::Plan,
|
||||
SlashCommandInvocation::Review(_) => SlashCommand::Review,
|
||||
SlashCommandInvocation::SandboxReadRoot(_) => SlashCommand::SandboxReadRoot,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn serialize(&self) -> SerializedSlashCommand {
|
||||
let command = self.command().command();
|
||||
let prefix = format!("/{command}");
|
||||
match self {
|
||||
SlashCommandInvocation::Bare(_) => SerializedSlashCommand {
|
||||
text: prefix,
|
||||
text_elements: Vec::new(),
|
||||
},
|
||||
SlashCommandInvocation::Fast(args) => SerializedSlashCommand {
|
||||
text: format!("{prefix} {}", args.as_str()),
|
||||
text_elements: Vec::new(),
|
||||
},
|
||||
SlashCommandInvocation::Rename(arg)
|
||||
| SlashCommandInvocation::Plan(arg)
|
||||
| SlashCommandInvocation::Review(arg)
|
||||
| SlashCommandInvocation::SandboxReadRoot(arg) => arg.with_prefix(&prefix),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn into_prefixed_string(self) -> String {
|
||||
let command = self.command.command();
|
||||
let joined = match shlex::try_join(
|
||||
std::iter::once(command).chain(self.args.iter().map(String::as_str)),
|
||||
) {
|
||||
Ok(joined) => joined,
|
||||
Err(err) => panic!("slash command invocation should serialize: {err}"),
|
||||
};
|
||||
format!("/{joined}")
|
||||
self.serialize().text
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct SerializedSlashCommand {
|
||||
pub(crate) text: String,
|
||||
pub(crate) text_elements: Vec<TextElement>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bare_invocation_serializes_with_leading_slash() {
|
||||
let invocation = SlashCommandInvocation::bare(SlashCommand::Model);
|
||||
|
||||
assert_eq!(invocation.into_prefixed_string(), "/model");
|
||||
assert_eq!(
|
||||
invocation.serialize(),
|
||||
SerializedSlashCommand {
|
||||
text: "/model".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_invocation_serializes_canonical_token() {
|
||||
let invocation = SlashCommandInvocation::Fast(FastSlashCommandArgs::Status);
|
||||
|
||||
assert_eq!(
|
||||
invocation.serialize(),
|
||||
SerializedSlashCommand {
|
||||
text: "/fast status".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variadic_invocation_preserves_placeholder_ranges() {
|
||||
let placeholder = "[Image #1]".to_string();
|
||||
let invocation = SlashCommandInvocation::Plan(SlashCommandTextArg::new(
|
||||
format!("review {placeholder}"),
|
||||
vec![TextElement::new((7..18).into(), Some(placeholder.clone()))],
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
invocation.serialize(),
|
||||
SerializedSlashCommand {
|
||||
text: format!("/plan review {placeholder}"),
|
||||
text_elements: vec![TextElement::new((13..24).into(), Some(placeholder),)],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,9 +315,9 @@ use crate::render::renderable::FlexRenderable;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::render::renderable::RenderableExt;
|
||||
use crate::render::renderable::RenderableItem;
|
||||
use crate::slash_command::FastSlashCommandArgs;
|
||||
use crate::slash_command::ParsedSlashCommand;
|
||||
use crate::slash_command::SlashCommand;
|
||||
use crate::slash_command_invocation::FastSlashCommandArgs;
|
||||
use crate::slash_command_invocation::SlashCommandInvocation;
|
||||
use crate::status::RateLimitSnapshotDisplay;
|
||||
use crate::status_indicator_widget::STATUS_DETAILS_DEFAULT_MAX_LINES;
|
||||
use crate::status_indicator_widget::StatusDetailsCapitalization;
|
||||
@@ -4881,11 +4881,28 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_inline_command_invocation(
|
||||
&mut self,
|
||||
cmd: SlashCommand,
|
||||
record_history: bool,
|
||||
) -> Option<SlashCommandInvocation> {
|
||||
let (prepared_args, prepared_elements) = self
|
||||
.bottom_pane
|
||||
.prepare_inline_args_submission(record_history)?;
|
||||
match cmd.parse_invocation(&prepared_args, &prepared_elements) {
|
||||
Ok(invocation) => Some(invocation),
|
||||
Err(err) => {
|
||||
self.add_error_message(err.message());
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_command_with_args(
|
||||
&mut self,
|
||||
cmd: SlashCommand,
|
||||
args: String,
|
||||
_text_elements: Vec<TextElement>,
|
||||
text_elements: Vec<TextElement>,
|
||||
) {
|
||||
if !cmd.available_during_task() && self.bottom_pane.is_task_running() {
|
||||
let message = format!(
|
||||
@@ -4897,17 +4914,17 @@ impl ChatWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
match cmd.parse_invocation(&args) {
|
||||
Ok(ParsedSlashCommand::Bare(_)) => {
|
||||
match cmd.parse_invocation(&args, &text_elements) {
|
||||
Ok(SlashCommandInvocation::Bare(_)) => {
|
||||
self.dispatch_command(cmd);
|
||||
}
|
||||
Ok(ParsedSlashCommand::Fast(FastSlashCommandArgs::On)) => {
|
||||
Ok(SlashCommandInvocation::Fast(FastSlashCommandArgs::On)) => {
|
||||
self.set_service_tier_selection(Some(ServiceTier::Fast));
|
||||
}
|
||||
Ok(ParsedSlashCommand::Fast(FastSlashCommandArgs::Off)) => {
|
||||
Ok(SlashCommandInvocation::Fast(FastSlashCommandArgs::Off)) => {
|
||||
self.set_service_tier_selection(/*service_tier*/ None);
|
||||
}
|
||||
Ok(ParsedSlashCommand::Fast(FastSlashCommandArgs::Status)) => {
|
||||
Ok(SlashCommandInvocation::Fast(FastSlashCommandArgs::Status)) => {
|
||||
let status = if matches!(self.config.service_tier, Some(ServiceTier::Fast)) {
|
||||
"on"
|
||||
} else {
|
||||
@@ -4915,16 +4932,16 @@ impl ChatWidget {
|
||||
};
|
||||
self.add_info_message(format!("Fast mode is {status}."), /*hint*/ None);
|
||||
}
|
||||
Ok(ParsedSlashCommand::Rename) => {
|
||||
Ok(SlashCommandInvocation::Rename(_)) => {
|
||||
self.session_telemetry
|
||||
.counter("codex.thread.rename", /*inc*/ 1, &[]);
|
||||
let Some((prepared_args, _prepared_elements)) = self
|
||||
.bottom_pane
|
||||
.prepare_inline_args_submission(/*record_history*/ false)
|
||||
let Some(SlashCommandInvocation::Rename(prepared_args)) =
|
||||
self.prepare_inline_command_invocation(cmd, /*record_history*/ false)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(name) = codex_core::util::normalize_thread_name(&prepared_args) else {
|
||||
let Some(name) = codex_core::util::normalize_thread_name(&prepared_args.text)
|
||||
else {
|
||||
self.add_error_message("Thread name cannot be empty.".to_string());
|
||||
return;
|
||||
};
|
||||
@@ -4934,14 +4951,13 @@ impl ChatWidget {
|
||||
self.app_event_tx.set_thread_name(name);
|
||||
self.bottom_pane.drain_pending_submission_state();
|
||||
}
|
||||
Ok(ParsedSlashCommand::Plan) => {
|
||||
Ok(SlashCommandInvocation::Plan(_)) => {
|
||||
self.dispatch_command(cmd);
|
||||
if self.active_mode_kind() != ModeKind::Plan {
|
||||
return;
|
||||
}
|
||||
let Some((prepared_args, prepared_elements)) = self
|
||||
.bottom_pane
|
||||
.prepare_inline_args_submission(/*record_history*/ true)
|
||||
let Some(SlashCommandInvocation::Plan(prepared_args)) =
|
||||
self.prepare_inline_command_invocation(cmd, /*record_history*/ true)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
@@ -4949,11 +4965,15 @@ impl ChatWidget {
|
||||
.bottom_pane
|
||||
.take_recent_submission_images_with_placeholders();
|
||||
let remote_image_urls = self.take_remote_image_urls();
|
||||
let crate::slash_command_invocation::SlashCommandTextArg {
|
||||
text,
|
||||
text_elements,
|
||||
} = prepared_args;
|
||||
let user_message = UserMessage {
|
||||
text: prepared_args,
|
||||
text,
|
||||
local_images,
|
||||
remote_image_urls,
|
||||
text_elements: prepared_elements,
|
||||
text_elements,
|
||||
mention_bindings: self.bottom_pane.take_recent_submission_mention_bindings(),
|
||||
};
|
||||
if self.is_session_configured() {
|
||||
@@ -4965,31 +4985,29 @@ impl ChatWidget {
|
||||
self.queue_user_message(user_message);
|
||||
}
|
||||
}
|
||||
Ok(ParsedSlashCommand::Review) => {
|
||||
let Some((prepared_args, _prepared_elements)) = self
|
||||
.bottom_pane
|
||||
.prepare_inline_args_submission(/*record_history*/ false)
|
||||
Ok(SlashCommandInvocation::Review(_)) => {
|
||||
let Some(SlashCommandInvocation::Review(prepared_args)) =
|
||||
self.prepare_inline_command_invocation(cmd, /*record_history*/ false)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
self.submit_op(AppCommand::review(ReviewRequest {
|
||||
target: ReviewTarget::Custom {
|
||||
instructions: prepared_args,
|
||||
instructions: prepared_args.text,
|
||||
},
|
||||
user_facing_hint: None,
|
||||
}));
|
||||
self.bottom_pane.drain_pending_submission_state();
|
||||
}
|
||||
Ok(ParsedSlashCommand::SandboxReadRoot) => {
|
||||
let Some((prepared_args, _prepared_elements)) = self
|
||||
.bottom_pane
|
||||
.prepare_inline_args_submission(/*record_history*/ false)
|
||||
Ok(SlashCommandInvocation::SandboxReadRoot(_)) => {
|
||||
let Some(SlashCommandInvocation::SandboxReadRoot(prepared_args)) =
|
||||
self.prepare_inline_command_invocation(cmd, /*record_history*/ false)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
self.app_event_tx
|
||||
.send(AppEvent::BeginWindowsSandboxGrantReadRoot {
|
||||
path: prepared_args,
|
||||
path: prepared_args.text,
|
||||
});
|
||||
self.bottom_pane.drain_pending_submission_state();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,12 @@ use strum_macros::EnumIter;
|
||||
use strum_macros::EnumString;
|
||||
use strum_macros::IntoStaticStr;
|
||||
|
||||
use codex_protocol::user_input::TextElement;
|
||||
|
||||
use crate::slash_command_invocation::FastSlashCommandArgs;
|
||||
use crate::slash_command_invocation::SlashCommandInvocation;
|
||||
use crate::slash_command_invocation::SlashCommandTextArg;
|
||||
|
||||
/// Commands that can be invoked by starting a message with a leading slash.
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Hash, EnumString, EnumIter, AsRefStr, IntoStaticStr,
|
||||
@@ -71,74 +77,112 @@ pub(crate) enum SlashCommandBareBehavior {
|
||||
OpensUi,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum FastSlashCommandArgs {
|
||||
On,
|
||||
Off,
|
||||
Status,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum ParsedSlashCommand {
|
||||
Bare(SlashCommandBareBehavior),
|
||||
Fast(FastSlashCommandArgs),
|
||||
Rename,
|
||||
Plan,
|
||||
Review,
|
||||
SandboxReadRoot,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum SlashCommandUsageErrorKind {
|
||||
UnexpectedInlineArgs,
|
||||
InvalidInlineArgs,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum SlashCommandArgsParser {
|
||||
NoArgs,
|
||||
Remainder(ParsedSlashCommand),
|
||||
Tokens(&'static [SlashCommandTokenChoice]),
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct SlashCommandParseInput<'a> {
|
||||
pub(crate) args: &'a str,
|
||||
pub(crate) text_elements: &'a [TextElement],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct SlashCommandTokenChoice {
|
||||
pub(crate) token: &'static str,
|
||||
pub(crate) parsed: ParsedSlashCommand,
|
||||
}
|
||||
pub(crate) type SlashCommandParser =
|
||||
for<'a> fn(
|
||||
SlashCommand,
|
||||
SlashCommandParseInput<'a>,
|
||||
) -> Result<SlashCommandInvocation, SlashCommandUsageErrorKind>;
|
||||
|
||||
const FAST_TOKEN_CHOICES: &[SlashCommandTokenChoice] = &[
|
||||
SlashCommandTokenChoice {
|
||||
token: "on",
|
||||
parsed: ParsedSlashCommand::Fast(FastSlashCommandArgs::On),
|
||||
},
|
||||
SlashCommandTokenChoice {
|
||||
token: "off",
|
||||
parsed: ParsedSlashCommand::Fast(FastSlashCommandArgs::Off),
|
||||
},
|
||||
SlashCommandTokenChoice {
|
||||
token: "status",
|
||||
parsed: ParsedSlashCommand::Fast(FastSlashCommandArgs::Status),
|
||||
},
|
||||
];
|
||||
|
||||
impl SlashCommandArgsParser {
|
||||
pub(crate) fn parse(
|
||||
self,
|
||||
args: &str,
|
||||
) -> Result<ParsedSlashCommand, SlashCommandUsageErrorKind> {
|
||||
match self {
|
||||
SlashCommandArgsParser::NoArgs => Err(SlashCommandUsageErrorKind::UnexpectedInlineArgs),
|
||||
SlashCommandArgsParser::Remainder(parsed) => Ok(parsed),
|
||||
SlashCommandArgsParser::Tokens(choices) => choices
|
||||
.iter()
|
||||
.find(|choice| choice.token.eq_ignore_ascii_case(args))
|
||||
.map(|choice| choice.parsed)
|
||||
.ok_or(SlashCommandUsageErrorKind::InvalidInlineArgs),
|
||||
}
|
||||
fn parse_no_args(
|
||||
command: SlashCommand,
|
||||
input: SlashCommandParseInput<'_>,
|
||||
) -> Result<SlashCommandInvocation, SlashCommandUsageErrorKind> {
|
||||
if input.args.trim().is_empty() {
|
||||
Ok(SlashCommandInvocation::Bare(command))
|
||||
} else {
|
||||
Err(SlashCommandUsageErrorKind::UnexpectedInlineArgs)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_fast(
|
||||
command: SlashCommand,
|
||||
input: SlashCommandParseInput<'_>,
|
||||
) -> Result<SlashCommandInvocation, SlashCommandUsageErrorKind> {
|
||||
if !input.text_elements.is_empty() {
|
||||
return Err(SlashCommandUsageErrorKind::InvalidInlineArgs);
|
||||
}
|
||||
match input.args.trim() {
|
||||
"" => Ok(SlashCommandInvocation::Bare(command)),
|
||||
args if args.eq_ignore_ascii_case("on") => {
|
||||
Ok(SlashCommandInvocation::Fast(FastSlashCommandArgs::On))
|
||||
}
|
||||
args if args.eq_ignore_ascii_case("off") => {
|
||||
Ok(SlashCommandInvocation::Fast(FastSlashCommandArgs::Off))
|
||||
}
|
||||
args if args.eq_ignore_ascii_case("status") => {
|
||||
Ok(SlashCommandInvocation::Fast(FastSlashCommandArgs::Status))
|
||||
}
|
||||
_ => Err(SlashCommandUsageErrorKind::InvalidInlineArgs),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_text_arg(
|
||||
command: SlashCommand,
|
||||
input: SlashCommandParseInput<'_>,
|
||||
) -> Result<SlashCommandInvocation, SlashCommandUsageErrorKind> {
|
||||
let args = input.args.trim();
|
||||
if args.is_empty() {
|
||||
return Ok(SlashCommandInvocation::Bare(command));
|
||||
}
|
||||
|
||||
let arg = SlashCommandTextArg::new(args.to_string(), input.text_elements.to_vec());
|
||||
|
||||
match command {
|
||||
SlashCommand::Rename => Ok(SlashCommandInvocation::Rename(arg)),
|
||||
SlashCommand::Plan => Ok(SlashCommandInvocation::Plan(arg)),
|
||||
SlashCommand::Review => Ok(SlashCommandInvocation::Review(arg)),
|
||||
SlashCommand::SandboxReadRoot => Ok(SlashCommandInvocation::SandboxReadRoot(arg)),
|
||||
_ => Err(SlashCommandUsageErrorKind::InvalidInlineArgs),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_model(
|
||||
command: SlashCommand,
|
||||
input: SlashCommandParseInput<'_>,
|
||||
) -> Result<SlashCommandInvocation, SlashCommandUsageErrorKind> {
|
||||
parse_no_args(command, input)
|
||||
}
|
||||
|
||||
fn parse_ui_only(
|
||||
command: SlashCommand,
|
||||
input: SlashCommandParseInput<'_>,
|
||||
) -> Result<SlashCommandInvocation, SlashCommandUsageErrorKind> {
|
||||
parse_no_args(command, input)
|
||||
}
|
||||
|
||||
fn parse_dispatch_only(
|
||||
command: SlashCommand,
|
||||
input: SlashCommandParseInput<'_>,
|
||||
) -> Result<SlashCommandInvocation, SlashCommandUsageErrorKind> {
|
||||
parse_no_args(command, input)
|
||||
}
|
||||
|
||||
fn parse_fast_command(
|
||||
command: SlashCommand,
|
||||
input: SlashCommandParseInput<'_>,
|
||||
) -> Result<SlashCommandInvocation, SlashCommandUsageErrorKind> {
|
||||
parse_fast(command, input)
|
||||
}
|
||||
|
||||
fn parse_rich_text_command(
|
||||
command: SlashCommand,
|
||||
input: SlashCommandParseInput<'_>,
|
||||
) -> Result<SlashCommandInvocation, SlashCommandUsageErrorKind> {
|
||||
parse_text_arg(command, input)
|
||||
}
|
||||
|
||||
impl SlashCommand {
|
||||
pub(crate) fn spec(self) -> SlashCommandSpec {
|
||||
match self {
|
||||
@@ -149,7 +193,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/model"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_model,
|
||||
},
|
||||
SlashCommand::Fast => SlashCommandSpec {
|
||||
description: "toggle Fast mode to enable fastest inference at 2X plan usage",
|
||||
@@ -158,7 +202,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/fast", "/fast [on|off|status]"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::Tokens(FAST_TOKEN_CHOICES),
|
||||
parser: parse_fast_command,
|
||||
},
|
||||
SlashCommand::Approvals => SlashCommandSpec {
|
||||
description: "choose what Codex is allowed to do",
|
||||
@@ -167,7 +211,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: true,
|
||||
usage_lines: &["/approvals"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::Permissions => SlashCommandSpec {
|
||||
description: "choose what Codex is allowed to do",
|
||||
@@ -176,7 +220,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/permissions"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::ElevateSandbox => SlashCommandSpec {
|
||||
description: "set up elevated agent sandbox",
|
||||
@@ -185,7 +229,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/setup-default-sandbox"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::SandboxReadRoot => SlashCommandSpec {
|
||||
description: "let sandbox read a directory: /sandbox-add-read-dir <absolute_path>",
|
||||
@@ -194,7 +238,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/sandbox-add-read-dir <absolute-path>"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::Remainder(ParsedSlashCommand::SandboxReadRoot),
|
||||
parser: parse_rich_text_command,
|
||||
},
|
||||
SlashCommand::Experimental => SlashCommandSpec {
|
||||
description: "toggle experimental features",
|
||||
@@ -203,7 +247,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/experimental"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::Skills => SlashCommandSpec {
|
||||
description: "use skills to improve how Codex performs specific tasks",
|
||||
@@ -212,7 +256,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/skills"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::Review => SlashCommandSpec {
|
||||
description: "review my current changes and find issues",
|
||||
@@ -221,7 +265,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/review", "/review <instructions>"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::Remainder(ParsedSlashCommand::Review),
|
||||
parser: parse_rich_text_command,
|
||||
},
|
||||
SlashCommand::Rename => SlashCommandSpec {
|
||||
description: "rename the current thread",
|
||||
@@ -230,7 +274,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/rename", "/rename <title>"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::Remainder(ParsedSlashCommand::Rename),
|
||||
parser: parse_rich_text_command,
|
||||
},
|
||||
SlashCommand::New => SlashCommandSpec {
|
||||
description: "start a new chat during a conversation",
|
||||
@@ -239,7 +283,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/new"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Resume => SlashCommandSpec {
|
||||
description: "resume a saved chat",
|
||||
@@ -248,7 +292,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/resume"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::Fork => SlashCommandSpec {
|
||||
description: "fork the current chat",
|
||||
@@ -257,7 +301,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/fork"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Init => SlashCommandSpec {
|
||||
description: "create an AGENTS.md file with instructions for Codex",
|
||||
@@ -266,7 +310,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/init"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Compact => SlashCommandSpec {
|
||||
description: "summarize conversation to prevent hitting the context limit",
|
||||
@@ -275,7 +319,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/compact"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Plan => SlashCommandSpec {
|
||||
description: "switch to Plan mode",
|
||||
@@ -284,7 +328,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/plan", "/plan <prompt>"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::Remainder(ParsedSlashCommand::Plan),
|
||||
parser: parse_rich_text_command,
|
||||
},
|
||||
SlashCommand::Collab => SlashCommandSpec {
|
||||
description: "change collaboration mode (experimental)",
|
||||
@@ -293,7 +337,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/collab"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::Agent => SlashCommandSpec {
|
||||
description: "switch the active agent thread",
|
||||
@@ -302,7 +346,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/agent"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::Diff => SlashCommandSpec {
|
||||
description: "show git diff (including untracked files)",
|
||||
@@ -311,7 +355,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/diff"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Copy => SlashCommandSpec {
|
||||
description: "copy the latest Codex output to your clipboard",
|
||||
@@ -320,7 +364,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/copy"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Mention => SlashCommandSpec {
|
||||
description: "mention a file",
|
||||
@@ -329,7 +373,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/mention"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Status => SlashCommandSpec {
|
||||
description: "show current session configuration and token usage",
|
||||
@@ -338,7 +382,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/status"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::DebugConfig => SlashCommandSpec {
|
||||
description: "show config layers and requirement sources for debugging",
|
||||
@@ -347,7 +391,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/debug-config"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Statusline => SlashCommandSpec {
|
||||
description: "configure which items appear in the status line",
|
||||
@@ -356,7 +400,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/statusline"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::Theme => SlashCommandSpec {
|
||||
description: "choose a syntax highlighting theme",
|
||||
@@ -365,7 +409,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/theme"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::Mcp => SlashCommandSpec {
|
||||
description: "list configured MCP tools",
|
||||
@@ -374,7 +418,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/mcp"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Apps => SlashCommandSpec {
|
||||
description: "manage apps",
|
||||
@@ -383,7 +427,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/apps"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Plugins => SlashCommandSpec {
|
||||
description: "browse plugins",
|
||||
@@ -392,7 +436,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/plugins"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Logout => SlashCommandSpec {
|
||||
description: "log out of Codex",
|
||||
@@ -401,7 +445,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/logout"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Quit => SlashCommandSpec {
|
||||
description: "exit Codex",
|
||||
@@ -410,7 +454,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: true,
|
||||
usage_lines: &["/quit"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Exit => SlashCommandSpec {
|
||||
description: "exit Codex",
|
||||
@@ -419,7 +463,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/exit"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Feedback => SlashCommandSpec {
|
||||
description: "send logs to maintainers",
|
||||
@@ -428,7 +472,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/feedback"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::Rollout => SlashCommandSpec {
|
||||
description: "print the rollout file path",
|
||||
@@ -437,7 +481,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/rollout"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Ps => SlashCommandSpec {
|
||||
description: "list background terminals",
|
||||
@@ -446,7 +490,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/ps"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Stop => SlashCommandSpec {
|
||||
description: "stop all background terminals",
|
||||
@@ -455,7 +499,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/stop"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Clear => SlashCommandSpec {
|
||||
description: "clear the terminal and start a new chat",
|
||||
@@ -464,7 +508,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/clear"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Personality => SlashCommandSpec {
|
||||
description: "choose a communication style for Codex",
|
||||
@@ -473,7 +517,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/personality"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::Realtime => SlashCommandSpec {
|
||||
description: "toggle realtime voice mode (experimental)",
|
||||
@@ -482,7 +526,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/realtime"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::Settings => SlashCommandSpec {
|
||||
description: "configure realtime microphone/speaker",
|
||||
@@ -491,7 +535,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/settings"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::TestApproval => SlashCommandSpec {
|
||||
description: "test approval request",
|
||||
@@ -500,7 +544,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/test-approval"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::MultiAgents => SlashCommandSpec {
|
||||
description: "switch the active agent thread",
|
||||
@@ -509,7 +553,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/subagents"],
|
||||
bare_behavior: SlashCommandBareBehavior::OpensUi,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_ui_only,
|
||||
},
|
||||
SlashCommand::MemoryDrop => SlashCommandSpec {
|
||||
description: "DO NOT USE",
|
||||
@@ -518,7 +562,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/debug-m-drop"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
SlashCommand::MemoryUpdate => SlashCommandSpec {
|
||||
description: "DO NOT USE",
|
||||
@@ -527,7 +571,7 @@ impl SlashCommand {
|
||||
hide_in_command_popup: false,
|
||||
usage_lines: &["/debug-m-update"],
|
||||
bare_behavior: SlashCommandBareBehavior::DispatchesDirectly,
|
||||
args_parser: SlashCommandArgsParser::NoArgs,
|
||||
parser: parse_dispatch_only,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -581,7 +625,7 @@ pub(crate) struct SlashCommandSpec {
|
||||
pub(crate) hide_in_command_popup: bool,
|
||||
pub(crate) usage_lines: &'static [&'static str],
|
||||
pub(crate) bare_behavior: SlashCommandBareBehavior,
|
||||
pub(crate) args_parser: SlashCommandArgsParser,
|
||||
pub(crate) parser: SlashCommandParser,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use crate::slash_command::ParsedSlashCommand;
|
||||
use codex_protocol::user_input::TextElement;
|
||||
|
||||
use crate::slash_command::SlashCommand;
|
||||
use crate::slash_command::SlashCommandParseInput;
|
||||
use crate::slash_command::SlashCommandUsageErrorKind;
|
||||
use crate::slash_command_invocation::SlashCommandInvocation;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct SlashCommandUsageError {
|
||||
@@ -25,19 +28,29 @@ impl SlashCommand {
|
||||
pub(crate) fn parse_invocation(
|
||||
self,
|
||||
args: &str,
|
||||
) -> Result<ParsedSlashCommand, SlashCommandUsageError> {
|
||||
let trimmed = args.trim();
|
||||
text_elements: &[TextElement],
|
||||
) -> Result<SlashCommandInvocation, SlashCommandUsageError> {
|
||||
let spec = self.spec();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(ParsedSlashCommand::Bare(spec.bare_behavior));
|
||||
if args.trim().is_empty() {
|
||||
return match spec.bare_behavior {
|
||||
crate::slash_command::SlashCommandBareBehavior::DispatchesDirectly
|
||||
| crate::slash_command::SlashCommandBareBehavior::OpensUi => {
|
||||
Ok(SlashCommandInvocation::Bare(self))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
spec.args_parser
|
||||
.parse(trimmed)
|
||||
.map_err(|kind| SlashCommandUsageError {
|
||||
command: self,
|
||||
kind,
|
||||
})
|
||||
(spec.parser)(
|
||||
self,
|
||||
SlashCommandParseInput {
|
||||
args,
|
||||
text_elements,
|
||||
},
|
||||
)
|
||||
.map_err(|kind| SlashCommandUsageError {
|
||||
command: self,
|
||||
kind,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,22 +59,41 @@ mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
use crate::slash_command::FastSlashCommandArgs;
|
||||
use crate::slash_command::SlashCommandBareBehavior;
|
||||
use crate::slash_command_invocation::FastSlashCommandArgs;
|
||||
use crate::slash_command_invocation::SlashCommandTextArg;
|
||||
|
||||
#[test]
|
||||
fn review_bare_form_is_marked_as_ui_driven() {
|
||||
assert_eq!(
|
||||
SlashCommand::Review.parse_invocation(""),
|
||||
Ok(ParsedSlashCommand::Bare(SlashCommandBareBehavior::OpensUi))
|
||||
SlashCommand::Review.parse_invocation("", &[]),
|
||||
Ok(SlashCommandInvocation::Bare(SlashCommand::Review))
|
||||
);
|
||||
assert_eq!(
|
||||
SlashCommand::Review.spec().bare_behavior,
|
||||
SlashCommandBareBehavior::OpensUi
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_accepts_nonempty_inline_args() {
|
||||
assert_eq!(
|
||||
SlashCommand::Fast.parse_invocation("status"),
|
||||
Ok(ParsedSlashCommand::Fast(FastSlashCommandArgs::Status))
|
||||
SlashCommand::Fast.parse_invocation("status", &[]),
|
||||
Ok(SlashCommandInvocation::Fast(FastSlashCommandArgs::Status))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_preserves_placeholder_elements() {
|
||||
let placeholder = "[Image #1]".to_string();
|
||||
let text_elements = vec![TextElement::new((0..11).into(), Some(placeholder.clone()))];
|
||||
|
||||
assert_eq!(
|
||||
SlashCommand::Review.parse_invocation(&placeholder, &text_elements),
|
||||
Ok(SlashCommandInvocation::Review(SlashCommandTextArg::new(
|
||||
placeholder,
|
||||
text_elements,
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,7 +101,7 @@ mod tests {
|
||||
fn clear_rejects_unexpected_inline_args() {
|
||||
assert_eq!(
|
||||
SlashCommand::Clear
|
||||
.parse_invocation("now")
|
||||
.parse_invocation("now", &[])
|
||||
.unwrap_err()
|
||||
.message(),
|
||||
"'/clear' does not accept inline arguments. Usage: /clear"
|
||||
|
||||
@@ -1,40 +1,166 @@
|
||||
use codex_protocol::user_input::ByteRange;
|
||||
use codex_protocol::user_input::TextElement;
|
||||
|
||||
use crate::slash_command::SlashCommand;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct SlashCommandInvocation {
|
||||
command: SlashCommand,
|
||||
args: Vec<String>,
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum FastSlashCommandArgs {
|
||||
On,
|
||||
Off,
|
||||
Status,
|
||||
}
|
||||
|
||||
impl FastSlashCommandArgs {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
FastSlashCommandArgs::On => "on",
|
||||
FastSlashCommandArgs::Off => "off",
|
||||
FastSlashCommandArgs::Status => "status",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct SlashCommandTextArg {
|
||||
pub(crate) text: String,
|
||||
pub(crate) text_elements: Vec<TextElement>,
|
||||
}
|
||||
|
||||
impl SlashCommandTextArg {
|
||||
pub(crate) fn new(text: String, text_elements: Vec<TextElement>) -> Self {
|
||||
Self {
|
||||
text,
|
||||
text_elements,
|
||||
}
|
||||
}
|
||||
|
||||
fn with_prefix(&self, prefix: &str) -> SerializedSlashCommand {
|
||||
let prefix_len = prefix.len();
|
||||
let text = if self.text.is_empty() {
|
||||
prefix.to_string()
|
||||
} else {
|
||||
format!("{prefix} {}", self.text)
|
||||
};
|
||||
let offset = prefix_len + usize::from(!self.text.is_empty());
|
||||
let text_elements = self
|
||||
.text_elements
|
||||
.iter()
|
||||
.map(|element| {
|
||||
element.map_range(|byte_range| ByteRange {
|
||||
start: byte_range.start + offset,
|
||||
end: byte_range.end + offset,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
SerializedSlashCommand {
|
||||
text,
|
||||
text_elements,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum SlashCommandInvocation {
|
||||
Bare(SlashCommand),
|
||||
Fast(FastSlashCommandArgs),
|
||||
Rename(SlashCommandTextArg),
|
||||
Plan(SlashCommandTextArg),
|
||||
Review(SlashCommandTextArg),
|
||||
SandboxReadRoot(SlashCommandTextArg),
|
||||
}
|
||||
|
||||
impl SlashCommandInvocation {
|
||||
pub(crate) fn bare(command: SlashCommand) -> Self {
|
||||
Self {
|
||||
command,
|
||||
args: Vec::new(),
|
||||
Self::Bare(command)
|
||||
}
|
||||
|
||||
pub(crate) fn command(&self) -> SlashCommand {
|
||||
match self {
|
||||
SlashCommandInvocation::Bare(command) => *command,
|
||||
SlashCommandInvocation::Fast(_) => SlashCommand::Fast,
|
||||
SlashCommandInvocation::Rename(_) => SlashCommand::Rename,
|
||||
SlashCommandInvocation::Plan(_) => SlashCommand::Plan,
|
||||
SlashCommandInvocation::Review(_) => SlashCommand::Review,
|
||||
SlashCommandInvocation::SandboxReadRoot(_) => SlashCommand::SandboxReadRoot,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn serialize(&self) -> SerializedSlashCommand {
|
||||
let command = self.command().command();
|
||||
let prefix = format!("/{command}");
|
||||
match self {
|
||||
SlashCommandInvocation::Bare(_) => SerializedSlashCommand {
|
||||
text: prefix,
|
||||
text_elements: Vec::new(),
|
||||
},
|
||||
SlashCommandInvocation::Fast(args) => SerializedSlashCommand {
|
||||
text: format!("{prefix} {}", args.as_str()),
|
||||
text_elements: Vec::new(),
|
||||
},
|
||||
SlashCommandInvocation::Rename(arg)
|
||||
| SlashCommandInvocation::Plan(arg)
|
||||
| SlashCommandInvocation::Review(arg)
|
||||
| SlashCommandInvocation::SandboxReadRoot(arg) => arg.with_prefix(&prefix),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn into_prefixed_string(self) -> String {
|
||||
let command = self.command.command();
|
||||
let joined = match shlex::try_join(
|
||||
std::iter::once(command).chain(self.args.iter().map(String::as_str)),
|
||||
) {
|
||||
Ok(joined) => joined,
|
||||
Err(err) => panic!("slash command invocation should serialize: {err}"),
|
||||
};
|
||||
format!("/{joined}")
|
||||
self.serialize().text
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct SerializedSlashCommand {
|
||||
pub(crate) text: String,
|
||||
pub(crate) text_elements: Vec<TextElement>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bare_invocation_serializes_with_leading_slash() {
|
||||
let invocation = SlashCommandInvocation::bare(SlashCommand::Model);
|
||||
|
||||
assert_eq!(invocation.into_prefixed_string(), "/model");
|
||||
assert_eq!(
|
||||
invocation.serialize(),
|
||||
SerializedSlashCommand {
|
||||
text: "/model".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_invocation_serializes_canonical_token() {
|
||||
let invocation = SlashCommandInvocation::Fast(FastSlashCommandArgs::Status);
|
||||
|
||||
assert_eq!(
|
||||
invocation.serialize(),
|
||||
SerializedSlashCommand {
|
||||
text: "/fast status".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variadic_invocation_preserves_placeholder_ranges() {
|
||||
let placeholder = "[Image #1]".to_string();
|
||||
let invocation = SlashCommandInvocation::Plan(SlashCommandTextArg::new(
|
||||
format!("review {placeholder}"),
|
||||
vec![TextElement::new((7..18).into(), Some(placeholder.clone()))],
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
invocation.serialize(),
|
||||
SerializedSlashCommand {
|
||||
text: format!("/plan review {placeholder}"),
|
||||
text_elements: vec![TextElement::new((13..24).into(), Some(placeholder),)],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user