mirror of
https://github.com/openai/codex.git
synced 2026-09-14 11:57:03 +00:00
tui: derive slash command codecs from schema
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -4,15 +4,17 @@ use strum_macros::IntoStaticStr;
|
||||
|
||||
use crate::app_event::FeedbackCategory;
|
||||
use crate::bottom_pane::StatusLineItem;
|
||||
use crate::slash_command_protocol::SlashArgsParser;
|
||||
use crate::slash_command_protocol::SlashArgsSerializer;
|
||||
use crate::slash_command_protocol::SlashCommandArgs;
|
||||
use crate::slash_command_protocol::SlashArgsCodec;
|
||||
use crate::slash_command_protocol::SlashCommandParseInput;
|
||||
use crate::slash_command_protocol::SlashCommandUsageErrorKind;
|
||||
use crate::slash_command_protocol::SlashSerializedText;
|
||||
pub(crate) use crate::slash_command_protocol::SlashTextArg;
|
||||
use crate::slash_command_protocol::enum_choice;
|
||||
use crate::slash_command_protocol::from_str_value;
|
||||
use crate::slash_command_protocol::list;
|
||||
use crate::slash_command_protocol::named_or_positional;
|
||||
use crate::slash_command_protocol::positional;
|
||||
use crate::slash_command_protocol::remainder;
|
||||
use crate::slash_command_protocol::string;
|
||||
use crate::slash_command_protocol::text;
|
||||
|
||||
@@ -120,10 +122,26 @@ const FEEDBACK_CATEGORY_CHOICES: &[(&str, FeedbackCategory)] = &[
|
||||
("other", FeedbackCategory::Other),
|
||||
];
|
||||
|
||||
pub(crate) trait SlashCommandInlineArgs: SlashCommandArgs + Sized {
|
||||
pub(crate) trait SlashCommandInlineArgs: Sized {
|
||||
const USAGE_LINES: &'static [&'static str];
|
||||
fn codec() -> Box<dyn SlashArgsCodec<Self>>;
|
||||
|
||||
fn into_invocation(self) -> SlashCommandInvocation;
|
||||
|
||||
fn parse_inline(input: SlashCommandParseInput<'_>) -> Result<Self, SlashCommandUsageErrorKind> {
|
||||
let codec = Self::codec();
|
||||
let mut parser = crate::slash_command_protocol::SlashArgsParser::new(input)?;
|
||||
let value = codec.parse(&mut parser)?;
|
||||
codec.finish(parser)?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn serialize_inline(&self) -> SlashSerializedText {
|
||||
let codec = Self::codec();
|
||||
let mut serializer = crate::slash_command_protocol::SlashArgsSerializer::default();
|
||||
codec.serialize(self, &mut serializer);
|
||||
serializer.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -131,24 +149,16 @@ pub(crate) struct FastArgs {
|
||||
pub(crate) mode: FastSlashCommandArgs,
|
||||
}
|
||||
|
||||
impl SlashCommandArgs for FastArgs {
|
||||
fn parse(input: SlashCommandParseInput<'_>) -> Result<Self, SlashCommandUsageErrorKind> {
|
||||
let mut parser = SlashArgsParser::new(input)?;
|
||||
let mode = parser.positional(&enum_choice(FAST_MODE_CHOICES).ascii_case_insensitive())?;
|
||||
parser.finish()?;
|
||||
Ok(Self { mode })
|
||||
}
|
||||
|
||||
fn serialize(&self) -> SlashSerializedText {
|
||||
let mut serializer = SlashArgsSerializer::default();
|
||||
serializer.positional(&self.mode, &enum_choice(FAST_MODE_CHOICES));
|
||||
serializer.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommandInlineArgs for FastArgs {
|
||||
const USAGE_LINES: &'static [&'static str] = &["/fast", "/fast [on|off|status]"];
|
||||
|
||||
fn codec() -> Box<dyn SlashArgsCodec<Self>> {
|
||||
Box::new(
|
||||
positional(enum_choice(FAST_MODE_CHOICES).ascii_case_insensitive())
|
||||
.map_result(|mode| Ok(Self { mode }), |args| args.mode),
|
||||
)
|
||||
}
|
||||
|
||||
fn into_invocation(self) -> SlashCommandInvocation {
|
||||
SlashCommandInvocation::Fast(self)
|
||||
}
|
||||
@@ -159,23 +169,15 @@ pub(crate) struct RenameArgs {
|
||||
pub(crate) title: SlashTextArg,
|
||||
}
|
||||
|
||||
impl SlashCommandArgs for RenameArgs {
|
||||
fn parse(input: SlashCommandParseInput<'_>) -> Result<Self, SlashCommandUsageErrorKind> {
|
||||
let parser = SlashArgsParser::new(input)?;
|
||||
let title = parser.required_remainder(&text())?;
|
||||
Ok(Self { title })
|
||||
}
|
||||
|
||||
fn serialize(&self) -> SlashSerializedText {
|
||||
let mut serializer = SlashArgsSerializer::default();
|
||||
serializer.remainder(&self.title, &text());
|
||||
serializer.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommandInlineArgs for RenameArgs {
|
||||
const USAGE_LINES: &'static [&'static str] = &["/rename", "/rename <title>"];
|
||||
|
||||
fn codec() -> Box<dyn SlashArgsCodec<Self>> {
|
||||
Box::new(
|
||||
remainder(text()).map_result(|title| Ok(Self { title }), |args| args.title.clone()),
|
||||
)
|
||||
}
|
||||
|
||||
fn into_invocation(self) -> SlashCommandInvocation {
|
||||
SlashCommandInvocation::Rename(self)
|
||||
}
|
||||
@@ -186,23 +188,15 @@ pub(crate) struct PlanArgs {
|
||||
pub(crate) prompt: SlashTextArg,
|
||||
}
|
||||
|
||||
impl SlashCommandArgs for PlanArgs {
|
||||
fn parse(input: SlashCommandParseInput<'_>) -> Result<Self, SlashCommandUsageErrorKind> {
|
||||
let parser = SlashArgsParser::new(input)?;
|
||||
let prompt = parser.required_remainder(&text())?;
|
||||
Ok(Self { prompt })
|
||||
}
|
||||
|
||||
fn serialize(&self) -> SlashSerializedText {
|
||||
let mut serializer = SlashArgsSerializer::default();
|
||||
serializer.remainder(&self.prompt, &text());
|
||||
serializer.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommandInlineArgs for PlanArgs {
|
||||
const USAGE_LINES: &'static [&'static str] = &["/plan", "/plan <prompt>"];
|
||||
|
||||
fn codec() -> Box<dyn SlashArgsCodec<Self>> {
|
||||
Box::new(
|
||||
remainder(text()).map_result(|prompt| Ok(Self { prompt }), |args| args.prompt.clone()),
|
||||
)
|
||||
}
|
||||
|
||||
fn into_invocation(self) -> SlashCommandInvocation {
|
||||
SlashCommandInvocation::Plan(self)
|
||||
}
|
||||
@@ -213,23 +207,16 @@ pub(crate) struct ReviewArgs {
|
||||
pub(crate) instructions: SlashTextArg,
|
||||
}
|
||||
|
||||
impl SlashCommandArgs for ReviewArgs {
|
||||
fn parse(input: SlashCommandParseInput<'_>) -> Result<Self, SlashCommandUsageErrorKind> {
|
||||
let parser = SlashArgsParser::new(input)?;
|
||||
let instructions = parser.required_remainder(&text())?;
|
||||
Ok(Self { instructions })
|
||||
}
|
||||
|
||||
fn serialize(&self) -> SlashSerializedText {
|
||||
let mut serializer = SlashArgsSerializer::default();
|
||||
serializer.remainder(&self.instructions, &text());
|
||||
serializer.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommandInlineArgs for ReviewArgs {
|
||||
const USAGE_LINES: &'static [&'static str] = &["/review", "/review <instructions>"];
|
||||
|
||||
fn codec() -> Box<dyn SlashArgsCodec<Self>> {
|
||||
Box::new(remainder(text()).map_result(
|
||||
|instructions| Ok(Self { instructions }),
|
||||
|args| args.instructions.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
fn into_invocation(self) -> SlashCommandInvocation {
|
||||
SlashCommandInvocation::Review(self)
|
||||
}
|
||||
@@ -240,30 +227,19 @@ pub(crate) struct SandboxReadRootArgs {
|
||||
pub(crate) path: String,
|
||||
}
|
||||
|
||||
impl SlashCommandArgs for SandboxReadRootArgs {
|
||||
fn parse(input: SlashCommandParseInput<'_>) -> Result<Self, SlashCommandUsageErrorKind> {
|
||||
let mut parser = SlashArgsParser::new(input)?;
|
||||
let path = match parser.named("path", &string())? {
|
||||
Some(path) => path,
|
||||
None => parser.positional(&string())?,
|
||||
};
|
||||
parser.finish()?;
|
||||
Ok(Self { path })
|
||||
}
|
||||
|
||||
fn serialize(&self) -> SlashSerializedText {
|
||||
let mut serializer = SlashArgsSerializer::default();
|
||||
serializer.positional(&self.path, &string());
|
||||
serializer.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommandInlineArgs for SandboxReadRootArgs {
|
||||
const USAGE_LINES: &'static [&'static str] = &[
|
||||
"/sandbox-add-read-dir <absolute-path>",
|
||||
"/sandbox-add-read-dir --path=<absolute-path>",
|
||||
];
|
||||
|
||||
fn codec() -> Box<dyn SlashArgsCodec<Self>> {
|
||||
Box::new(
|
||||
named_or_positional("path", string())
|
||||
.map_result(|path| Ok(Self { path }), |args| args.path.clone()),
|
||||
)
|
||||
}
|
||||
|
||||
fn into_invocation(self) -> SlashCommandInvocation {
|
||||
SlashCommandInvocation::SandboxReadRoot(self)
|
||||
}
|
||||
@@ -274,27 +250,19 @@ pub(crate) struct FeedbackArgs {
|
||||
pub(crate) category: FeedbackCategory,
|
||||
}
|
||||
|
||||
impl SlashCommandArgs for FeedbackArgs {
|
||||
fn parse(input: SlashCommandParseInput<'_>) -> Result<Self, SlashCommandUsageErrorKind> {
|
||||
let mut parser = SlashArgsParser::new(input)?;
|
||||
let category = parser.positional(&enum_choice(FEEDBACK_CATEGORY_CHOICES))?;
|
||||
parser.finish()?;
|
||||
Ok(Self { category })
|
||||
}
|
||||
|
||||
fn serialize(&self) -> SlashSerializedText {
|
||||
let mut serializer = SlashArgsSerializer::default();
|
||||
serializer.positional(&self.category, &enum_choice(FEEDBACK_CATEGORY_CHOICES));
|
||||
serializer.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommandInlineArgs for FeedbackArgs {
|
||||
const USAGE_LINES: &'static [&'static str] = &[
|
||||
"/feedback",
|
||||
"/feedback <bad-result|good-result|bug|safety-check|other>",
|
||||
];
|
||||
|
||||
fn codec() -> Box<dyn SlashArgsCodec<Self>> {
|
||||
Box::new(
|
||||
positional(enum_choice(FEEDBACK_CATEGORY_CHOICES))
|
||||
.map_result(|category| Ok(Self { category }), |args| args.category),
|
||||
)
|
||||
}
|
||||
|
||||
fn into_invocation(self) -> SlashCommandInvocation {
|
||||
SlashCommandInvocation::Feedback(self)
|
||||
}
|
||||
@@ -305,30 +273,22 @@ pub(crate) struct StatuslineArgs {
|
||||
pub(crate) items: Vec<StatusLineItem>,
|
||||
}
|
||||
|
||||
impl SlashCommandArgs for StatuslineArgs {
|
||||
fn parse(input: SlashCommandParseInput<'_>) -> Result<Self, SlashCommandUsageErrorKind> {
|
||||
let mut parser = SlashArgsParser::new(input)?;
|
||||
let items = parser.positional_list(&from_str_value::<StatusLineItem>())?;
|
||||
parser.finish()?;
|
||||
if items.is_empty() {
|
||||
return Err(SlashCommandUsageErrorKind::InvalidInlineArgs);
|
||||
}
|
||||
Ok(Self { items })
|
||||
}
|
||||
|
||||
fn serialize(&self) -> SlashSerializedText {
|
||||
let mut serializer = SlashArgsSerializer::default();
|
||||
serializer.list(
|
||||
self.items.iter().cloned(),
|
||||
&from_str_value::<StatusLineItem>(),
|
||||
);
|
||||
serializer.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommandInlineArgs for StatuslineArgs {
|
||||
const USAGE_LINES: &'static [&'static str] = &["/statusline", "/statusline <item>..."];
|
||||
|
||||
fn codec() -> Box<dyn SlashArgsCodec<Self>> {
|
||||
Box::new(list(from_str_value::<StatusLineItem>()).map_result(
|
||||
|items| {
|
||||
if items.is_empty() {
|
||||
Err(SlashCommandUsageErrorKind::InvalidInlineArgs)
|
||||
} else {
|
||||
Ok(Self { items })
|
||||
}
|
||||
},
|
||||
|args| args.items.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
fn into_invocation(self) -> SlashCommandInvocation {
|
||||
SlashCommandInvocation::Statusline(self)
|
||||
}
|
||||
@@ -368,13 +328,13 @@ impl SlashCommandInvocation {
|
||||
let prefix = format!("/{}", self.command().command());
|
||||
match self {
|
||||
Self::Bare(_) => SlashSerializedText::empty().with_prefix(&prefix),
|
||||
Self::Fast(args) => args.serialize().with_prefix(&prefix),
|
||||
Self::Rename(args) => args.serialize().with_prefix(&prefix),
|
||||
Self::Plan(args) => args.serialize().with_prefix(&prefix),
|
||||
Self::Review(args) => args.serialize().with_prefix(&prefix),
|
||||
Self::SandboxReadRoot(args) => args.serialize().with_prefix(&prefix),
|
||||
Self::Feedback(args) => args.serialize().with_prefix(&prefix),
|
||||
Self::Statusline(args) => args.serialize().with_prefix(&prefix),
|
||||
Self::Fast(args) => args.serialize_inline().with_prefix(&prefix),
|
||||
Self::Rename(args) => args.serialize_inline().with_prefix(&prefix),
|
||||
Self::Plan(args) => args.serialize_inline().with_prefix(&prefix),
|
||||
Self::Review(args) => args.serialize_inline().with_prefix(&prefix),
|
||||
Self::SandboxReadRoot(args) => args.serialize_inline().with_prefix(&prefix),
|
||||
Self::Feedback(args) => args.serialize_inline().with_prefix(&prefix),
|
||||
Self::Statusline(args) => args.serialize_inline().with_prefix(&prefix),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,7 +372,7 @@ fn parse_typed_inline<T>(
|
||||
where
|
||||
T: SlashCommandInlineArgs,
|
||||
{
|
||||
T::parse(input).map(T::into_invocation)
|
||||
T::parse_inline(input).map(T::into_invocation)
|
||||
}
|
||||
|
||||
// ===== /model =====
|
||||
|
||||
@@ -241,11 +241,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait SlashCommandArgs: Sized {
|
||||
fn parse(input: SlashCommandParseInput<'_>) -> Result<Self, SlashCommandUsageErrorKind>;
|
||||
fn serialize(&self) -> SlashSerializedText;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SlashArgsParser<'a> {
|
||||
input: SlashCommandParseInput<'a>,
|
||||
@@ -429,6 +424,187 @@ impl SlashArgsSerializer {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait SlashArgsCodec<T> {
|
||||
fn parse<'a>(&self, parser: &mut SlashArgsParser<'a>) -> Result<T, SlashCommandUsageErrorKind>;
|
||||
|
||||
fn serialize(&self, value: &T, serializer: &mut SlashArgsSerializer);
|
||||
|
||||
fn finish<'a>(&self, parser: SlashArgsParser<'a>) -> Result<(), SlashCommandUsageErrorKind> {
|
||||
parser.finish()
|
||||
}
|
||||
|
||||
fn map_result<U, P, S>(
|
||||
self,
|
||||
parse_map: P,
|
||||
serialize_map: S,
|
||||
) -> SlashMapResultCodec<Self, P, S, T, U>
|
||||
where
|
||||
Self: Sized,
|
||||
P: Fn(T) -> Result<U, SlashCommandUsageErrorKind>,
|
||||
S: Fn(&U) -> T,
|
||||
{
|
||||
SlashMapResultCodec {
|
||||
inner: self,
|
||||
parse_map,
|
||||
serialize_map,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SlashMapResultCodec<C, P, S, T, U> {
|
||||
inner: C,
|
||||
parse_map: P,
|
||||
serialize_map: S,
|
||||
_phantom: PhantomData<fn(T) -> U>,
|
||||
}
|
||||
|
||||
impl<C, P, S, T, U> SlashArgsCodec<U> for SlashMapResultCodec<C, P, S, T, U>
|
||||
where
|
||||
C: SlashArgsCodec<T>,
|
||||
P: Fn(T) -> Result<U, SlashCommandUsageErrorKind>,
|
||||
S: Fn(&U) -> T,
|
||||
{
|
||||
fn parse<'a>(&self, parser: &mut SlashArgsParser<'a>) -> Result<U, SlashCommandUsageErrorKind> {
|
||||
let parsed = self.inner.parse(parser)?;
|
||||
(self.parse_map)(parsed)
|
||||
}
|
||||
|
||||
fn serialize(&self, value: &U, serializer: &mut SlashArgsSerializer) {
|
||||
let mapped = (self.serialize_map)(value);
|
||||
self.inner.serialize(&mapped, serializer);
|
||||
}
|
||||
|
||||
fn finish<'a>(&self, parser: SlashArgsParser<'a>) -> Result<(), SlashCommandUsageErrorKind> {
|
||||
self.inner.finish(parser)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SlashPositionalCodec<S> {
|
||||
spec: S,
|
||||
}
|
||||
|
||||
pub(crate) fn positional<S>(spec: S) -> SlashPositionalCodec<S> {
|
||||
SlashPositionalCodec { spec }
|
||||
}
|
||||
|
||||
impl<T, S> SlashArgsCodec<T> for SlashPositionalCodec<S>
|
||||
where
|
||||
S: SlashTokenValueSpec<T>,
|
||||
{
|
||||
fn parse<'a>(&self, parser: &mut SlashArgsParser<'a>) -> Result<T, SlashCommandUsageErrorKind> {
|
||||
parser.positional(&self.spec)
|
||||
}
|
||||
|
||||
fn serialize(&self, value: &T, serializer: &mut SlashArgsSerializer) {
|
||||
serializer.positional(value, &self.spec);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SlashListCodec<S> {
|
||||
spec: S,
|
||||
}
|
||||
|
||||
pub(crate) fn list<S>(spec: S) -> SlashListCodec<S> {
|
||||
SlashListCodec { spec }
|
||||
}
|
||||
|
||||
impl<T, S> SlashArgsCodec<Vec<T>> for SlashListCodec<S>
|
||||
where
|
||||
T: Clone,
|
||||
S: SlashTokenValueSpec<T>,
|
||||
{
|
||||
fn parse<'a>(
|
||||
&self,
|
||||
parser: &mut SlashArgsParser<'a>,
|
||||
) -> Result<Vec<T>, SlashCommandUsageErrorKind> {
|
||||
parser.positional_list(&self.spec)
|
||||
}
|
||||
|
||||
fn serialize(&self, value: &Vec<T>, serializer: &mut SlashArgsSerializer) {
|
||||
serializer.list(value.iter().cloned(), &self.spec);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct SlashNamedCodec<S> {
|
||||
key: &'static str,
|
||||
spec: S,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn named<S>(key: &'static str, spec: S) -> SlashNamedCodec<S> {
|
||||
SlashNamedCodec { key, spec }
|
||||
}
|
||||
|
||||
impl<T, S> SlashArgsCodec<Option<T>> for SlashNamedCodec<S>
|
||||
where
|
||||
S: SlashTokenValueSpec<T>,
|
||||
{
|
||||
fn parse<'a>(
|
||||
&self,
|
||||
parser: &mut SlashArgsParser<'a>,
|
||||
) -> Result<Option<T>, SlashCommandUsageErrorKind> {
|
||||
parser.named(self.key, &self.spec)
|
||||
}
|
||||
|
||||
fn serialize(&self, value: &Option<T>, serializer: &mut SlashArgsSerializer) {
|
||||
if let Some(value) = value {
|
||||
serializer.named(self.key, value, &self.spec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SlashNamedOrPositionalCodec<S> {
|
||||
key: &'static str,
|
||||
spec: S,
|
||||
}
|
||||
|
||||
pub(crate) fn named_or_positional<S>(key: &'static str, spec: S) -> SlashNamedOrPositionalCodec<S> {
|
||||
SlashNamedOrPositionalCodec { key, spec }
|
||||
}
|
||||
|
||||
impl<T, S> SlashArgsCodec<T> for SlashNamedOrPositionalCodec<S>
|
||||
where
|
||||
S: SlashTokenValueSpec<T>,
|
||||
{
|
||||
fn parse<'a>(&self, parser: &mut SlashArgsParser<'a>) -> Result<T, SlashCommandUsageErrorKind> {
|
||||
match parser.named(self.key, &self.spec)? {
|
||||
Some(value) => Ok(value),
|
||||
None => parser.positional(&self.spec),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize(&self, value: &T, serializer: &mut SlashArgsSerializer) {
|
||||
serializer.positional(value, &self.spec);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SlashRemainderCodec<S> {
|
||||
spec: S,
|
||||
}
|
||||
|
||||
pub(crate) fn remainder<S>(spec: S) -> SlashRemainderCodec<S> {
|
||||
SlashRemainderCodec { spec }
|
||||
}
|
||||
|
||||
impl<T, S> SlashArgsCodec<T> for SlashRemainderCodec<S>
|
||||
where
|
||||
S: SlashTextValueSpec<T>,
|
||||
{
|
||||
fn parse<'a>(&self, parser: &mut SlashArgsParser<'a>) -> Result<T, SlashCommandUsageErrorKind> {
|
||||
parser.required_remainder(&self.spec)
|
||||
}
|
||||
|
||||
fn serialize(&self, value: &T, serializer: &mut SlashArgsSerializer) {
|
||||
serializer.remainder(value, &self.spec);
|
||||
}
|
||||
|
||||
fn finish<'a>(&self, _parser: SlashArgsParser<'a>) -> Result<(), SlashCommandUsageErrorKind> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn trim_text_arg(text: &str, text_elements: &[TextElement]) -> Option<SlashTextArg> {
|
||||
let trimmed_start = text.len() - text.trim_start().len();
|
||||
let trimmed = text.trim();
|
||||
|
||||
@@ -4,15 +4,17 @@ use strum_macros::IntoStaticStr;
|
||||
|
||||
use crate::app_event::FeedbackCategory;
|
||||
use crate::bottom_pane::StatusLineItem;
|
||||
use crate::slash_command_protocol::SlashArgsParser;
|
||||
use crate::slash_command_protocol::SlashArgsSerializer;
|
||||
use crate::slash_command_protocol::SlashCommandArgs;
|
||||
use crate::slash_command_protocol::SlashArgsCodec;
|
||||
use crate::slash_command_protocol::SlashCommandParseInput;
|
||||
use crate::slash_command_protocol::SlashCommandUsageErrorKind;
|
||||
use crate::slash_command_protocol::SlashSerializedText;
|
||||
pub(crate) use crate::slash_command_protocol::SlashTextArg;
|
||||
use crate::slash_command_protocol::enum_choice;
|
||||
use crate::slash_command_protocol::from_str_value;
|
||||
use crate::slash_command_protocol::list;
|
||||
use crate::slash_command_protocol::named_or_positional;
|
||||
use crate::slash_command_protocol::positional;
|
||||
use crate::slash_command_protocol::remainder;
|
||||
use crate::slash_command_protocol::string;
|
||||
use crate::slash_command_protocol::text;
|
||||
|
||||
@@ -119,10 +121,26 @@ const FEEDBACK_CATEGORY_CHOICES: &[(&str, FeedbackCategory)] = &[
|
||||
("other", FeedbackCategory::Other),
|
||||
];
|
||||
|
||||
pub(crate) trait SlashCommandInlineArgs: SlashCommandArgs + Sized {
|
||||
pub(crate) trait SlashCommandInlineArgs: Sized {
|
||||
const USAGE_LINES: &'static [&'static str];
|
||||
fn codec() -> Box<dyn SlashArgsCodec<Self>>;
|
||||
|
||||
fn into_invocation(self) -> SlashCommandInvocation;
|
||||
|
||||
fn parse_inline(input: SlashCommandParseInput<'_>) -> Result<Self, SlashCommandUsageErrorKind> {
|
||||
let codec = Self::codec();
|
||||
let mut parser = crate::slash_command_protocol::SlashArgsParser::new(input)?;
|
||||
let value = codec.parse(&mut parser)?;
|
||||
codec.finish(parser)?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn serialize_inline(&self) -> SlashSerializedText {
|
||||
let codec = Self::codec();
|
||||
let mut serializer = crate::slash_command_protocol::SlashArgsSerializer::default();
|
||||
codec.serialize(self, &mut serializer);
|
||||
serializer.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -130,24 +148,16 @@ pub(crate) struct FastArgs {
|
||||
pub(crate) mode: FastSlashCommandArgs,
|
||||
}
|
||||
|
||||
impl SlashCommandArgs for FastArgs {
|
||||
fn parse(input: SlashCommandParseInput<'_>) -> Result<Self, SlashCommandUsageErrorKind> {
|
||||
let mut parser = SlashArgsParser::new(input)?;
|
||||
let mode = parser.positional(&enum_choice(FAST_MODE_CHOICES).ascii_case_insensitive())?;
|
||||
parser.finish()?;
|
||||
Ok(Self { mode })
|
||||
}
|
||||
|
||||
fn serialize(&self) -> SlashSerializedText {
|
||||
let mut serializer = SlashArgsSerializer::default();
|
||||
serializer.positional(&self.mode, &enum_choice(FAST_MODE_CHOICES));
|
||||
serializer.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommandInlineArgs for FastArgs {
|
||||
const USAGE_LINES: &'static [&'static str] = &["/fast", "/fast [on|off|status]"];
|
||||
|
||||
fn codec() -> Box<dyn SlashArgsCodec<Self>> {
|
||||
Box::new(
|
||||
positional(enum_choice(FAST_MODE_CHOICES).ascii_case_insensitive())
|
||||
.map_result(|mode| Ok(Self { mode }), |args| args.mode),
|
||||
)
|
||||
}
|
||||
|
||||
fn into_invocation(self) -> SlashCommandInvocation {
|
||||
SlashCommandInvocation::Fast(self)
|
||||
}
|
||||
@@ -158,23 +168,15 @@ pub(crate) struct RenameArgs {
|
||||
pub(crate) title: SlashTextArg,
|
||||
}
|
||||
|
||||
impl SlashCommandArgs for RenameArgs {
|
||||
fn parse(input: SlashCommandParseInput<'_>) -> Result<Self, SlashCommandUsageErrorKind> {
|
||||
let parser = SlashArgsParser::new(input)?;
|
||||
let title = parser.required_remainder(&text())?;
|
||||
Ok(Self { title })
|
||||
}
|
||||
|
||||
fn serialize(&self) -> SlashSerializedText {
|
||||
let mut serializer = SlashArgsSerializer::default();
|
||||
serializer.remainder(&self.title, &text());
|
||||
serializer.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommandInlineArgs for RenameArgs {
|
||||
const USAGE_LINES: &'static [&'static str] = &["/rename", "/rename <title>"];
|
||||
|
||||
fn codec() -> Box<dyn SlashArgsCodec<Self>> {
|
||||
Box::new(
|
||||
remainder(text()).map_result(|title| Ok(Self { title }), |args| args.title.clone()),
|
||||
)
|
||||
}
|
||||
|
||||
fn into_invocation(self) -> SlashCommandInvocation {
|
||||
SlashCommandInvocation::Rename(self)
|
||||
}
|
||||
@@ -185,23 +187,15 @@ pub(crate) struct PlanArgs {
|
||||
pub(crate) prompt: SlashTextArg,
|
||||
}
|
||||
|
||||
impl SlashCommandArgs for PlanArgs {
|
||||
fn parse(input: SlashCommandParseInput<'_>) -> Result<Self, SlashCommandUsageErrorKind> {
|
||||
let parser = SlashArgsParser::new(input)?;
|
||||
let prompt = parser.required_remainder(&text())?;
|
||||
Ok(Self { prompt })
|
||||
}
|
||||
|
||||
fn serialize(&self) -> SlashSerializedText {
|
||||
let mut serializer = SlashArgsSerializer::default();
|
||||
serializer.remainder(&self.prompt, &text());
|
||||
serializer.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommandInlineArgs for PlanArgs {
|
||||
const USAGE_LINES: &'static [&'static str] = &["/plan", "/plan <prompt>"];
|
||||
|
||||
fn codec() -> Box<dyn SlashArgsCodec<Self>> {
|
||||
Box::new(
|
||||
remainder(text()).map_result(|prompt| Ok(Self { prompt }), |args| args.prompt.clone()),
|
||||
)
|
||||
}
|
||||
|
||||
fn into_invocation(self) -> SlashCommandInvocation {
|
||||
SlashCommandInvocation::Plan(self)
|
||||
}
|
||||
@@ -212,23 +206,16 @@ pub(crate) struct ReviewArgs {
|
||||
pub(crate) instructions: SlashTextArg,
|
||||
}
|
||||
|
||||
impl SlashCommandArgs for ReviewArgs {
|
||||
fn parse(input: SlashCommandParseInput<'_>) -> Result<Self, SlashCommandUsageErrorKind> {
|
||||
let parser = SlashArgsParser::new(input)?;
|
||||
let instructions = parser.required_remainder(&text())?;
|
||||
Ok(Self { instructions })
|
||||
}
|
||||
|
||||
fn serialize(&self) -> SlashSerializedText {
|
||||
let mut serializer = SlashArgsSerializer::default();
|
||||
serializer.remainder(&self.instructions, &text());
|
||||
serializer.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommandInlineArgs for ReviewArgs {
|
||||
const USAGE_LINES: &'static [&'static str] = &["/review", "/review <instructions>"];
|
||||
|
||||
fn codec() -> Box<dyn SlashArgsCodec<Self>> {
|
||||
Box::new(remainder(text()).map_result(
|
||||
|instructions| Ok(Self { instructions }),
|
||||
|args| args.instructions.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
fn into_invocation(self) -> SlashCommandInvocation {
|
||||
SlashCommandInvocation::Review(self)
|
||||
}
|
||||
@@ -239,30 +226,19 @@ pub(crate) struct SandboxReadRootArgs {
|
||||
pub(crate) path: String,
|
||||
}
|
||||
|
||||
impl SlashCommandArgs for SandboxReadRootArgs {
|
||||
fn parse(input: SlashCommandParseInput<'_>) -> Result<Self, SlashCommandUsageErrorKind> {
|
||||
let mut parser = SlashArgsParser::new(input)?;
|
||||
let path = match parser.named("path", &string())? {
|
||||
Some(path) => path,
|
||||
None => parser.positional(&string())?,
|
||||
};
|
||||
parser.finish()?;
|
||||
Ok(Self { path })
|
||||
}
|
||||
|
||||
fn serialize(&self) -> SlashSerializedText {
|
||||
let mut serializer = SlashArgsSerializer::default();
|
||||
serializer.positional(&self.path, &string());
|
||||
serializer.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommandInlineArgs for SandboxReadRootArgs {
|
||||
const USAGE_LINES: &'static [&'static str] = &[
|
||||
"/sandbox-add-read-dir <absolute-path>",
|
||||
"/sandbox-add-read-dir --path=<absolute-path>",
|
||||
];
|
||||
|
||||
fn codec() -> Box<dyn SlashArgsCodec<Self>> {
|
||||
Box::new(
|
||||
named_or_positional("path", string())
|
||||
.map_result(|path| Ok(Self { path }), |args| args.path.clone()),
|
||||
)
|
||||
}
|
||||
|
||||
fn into_invocation(self) -> SlashCommandInvocation {
|
||||
SlashCommandInvocation::SandboxReadRoot(self)
|
||||
}
|
||||
@@ -273,27 +249,19 @@ pub(crate) struct FeedbackArgs {
|
||||
pub(crate) category: FeedbackCategory,
|
||||
}
|
||||
|
||||
impl SlashCommandArgs for FeedbackArgs {
|
||||
fn parse(input: SlashCommandParseInput<'_>) -> Result<Self, SlashCommandUsageErrorKind> {
|
||||
let mut parser = SlashArgsParser::new(input)?;
|
||||
let category = parser.positional(&enum_choice(FEEDBACK_CATEGORY_CHOICES))?;
|
||||
parser.finish()?;
|
||||
Ok(Self { category })
|
||||
}
|
||||
|
||||
fn serialize(&self) -> SlashSerializedText {
|
||||
let mut serializer = SlashArgsSerializer::default();
|
||||
serializer.positional(&self.category, &enum_choice(FEEDBACK_CATEGORY_CHOICES));
|
||||
serializer.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommandInlineArgs for FeedbackArgs {
|
||||
const USAGE_LINES: &'static [&'static str] = &[
|
||||
"/feedback",
|
||||
"/feedback <bad-result|good-result|bug|safety-check|other>",
|
||||
];
|
||||
|
||||
fn codec() -> Box<dyn SlashArgsCodec<Self>> {
|
||||
Box::new(
|
||||
positional(enum_choice(FEEDBACK_CATEGORY_CHOICES))
|
||||
.map_result(|category| Ok(Self { category }), |args| args.category),
|
||||
)
|
||||
}
|
||||
|
||||
fn into_invocation(self) -> SlashCommandInvocation {
|
||||
SlashCommandInvocation::Feedback(self)
|
||||
}
|
||||
@@ -304,30 +272,22 @@ pub(crate) struct StatuslineArgs {
|
||||
pub(crate) items: Vec<StatusLineItem>,
|
||||
}
|
||||
|
||||
impl SlashCommandArgs for StatuslineArgs {
|
||||
fn parse(input: SlashCommandParseInput<'_>) -> Result<Self, SlashCommandUsageErrorKind> {
|
||||
let mut parser = SlashArgsParser::new(input)?;
|
||||
let items = parser.positional_list(&from_str_value::<StatusLineItem>())?;
|
||||
parser.finish()?;
|
||||
if items.is_empty() {
|
||||
return Err(SlashCommandUsageErrorKind::InvalidInlineArgs);
|
||||
}
|
||||
Ok(Self { items })
|
||||
}
|
||||
|
||||
fn serialize(&self) -> SlashSerializedText {
|
||||
let mut serializer = SlashArgsSerializer::default();
|
||||
serializer.list(
|
||||
self.items.iter().cloned(),
|
||||
&from_str_value::<StatusLineItem>(),
|
||||
);
|
||||
serializer.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommandInlineArgs for StatuslineArgs {
|
||||
const USAGE_LINES: &'static [&'static str] = &["/statusline", "/statusline <item>..."];
|
||||
|
||||
fn codec() -> Box<dyn SlashArgsCodec<Self>> {
|
||||
Box::new(list(from_str_value::<StatusLineItem>()).map_result(
|
||||
|items| {
|
||||
if items.is_empty() {
|
||||
Err(SlashCommandUsageErrorKind::InvalidInlineArgs)
|
||||
} else {
|
||||
Ok(Self { items })
|
||||
}
|
||||
},
|
||||
|args| args.items.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
fn into_invocation(self) -> SlashCommandInvocation {
|
||||
SlashCommandInvocation::Statusline(self)
|
||||
}
|
||||
@@ -367,13 +327,13 @@ impl SlashCommandInvocation {
|
||||
let prefix = format!("/{}", self.command().command());
|
||||
match self {
|
||||
Self::Bare(_) => SlashSerializedText::empty().with_prefix(&prefix),
|
||||
Self::Fast(args) => args.serialize().with_prefix(&prefix),
|
||||
Self::Rename(args) => args.serialize().with_prefix(&prefix),
|
||||
Self::Plan(args) => args.serialize().with_prefix(&prefix),
|
||||
Self::Review(args) => args.serialize().with_prefix(&prefix),
|
||||
Self::SandboxReadRoot(args) => args.serialize().with_prefix(&prefix),
|
||||
Self::Feedback(args) => args.serialize().with_prefix(&prefix),
|
||||
Self::Statusline(args) => args.serialize().with_prefix(&prefix),
|
||||
Self::Fast(args) => args.serialize_inline().with_prefix(&prefix),
|
||||
Self::Rename(args) => args.serialize_inline().with_prefix(&prefix),
|
||||
Self::Plan(args) => args.serialize_inline().with_prefix(&prefix),
|
||||
Self::Review(args) => args.serialize_inline().with_prefix(&prefix),
|
||||
Self::SandboxReadRoot(args) => args.serialize_inline().with_prefix(&prefix),
|
||||
Self::Feedback(args) => args.serialize_inline().with_prefix(&prefix),
|
||||
Self::Statusline(args) => args.serialize_inline().with_prefix(&prefix),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,7 +371,7 @@ fn parse_typed_inline<T>(
|
||||
where
|
||||
T: SlashCommandInlineArgs,
|
||||
{
|
||||
T::parse(input).map(T::into_invocation)
|
||||
T::parse_inline(input).map(T::into_invocation)
|
||||
}
|
||||
|
||||
// ===== /model =====
|
||||
|
||||
@@ -241,11 +241,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait SlashCommandArgs: Sized {
|
||||
fn parse(input: SlashCommandParseInput<'_>) -> Result<Self, SlashCommandUsageErrorKind>;
|
||||
fn serialize(&self) -> SlashSerializedText;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SlashArgsParser<'a> {
|
||||
input: SlashCommandParseInput<'a>,
|
||||
@@ -429,6 +424,187 @@ impl SlashArgsSerializer {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait SlashArgsCodec<T> {
|
||||
fn parse<'a>(&self, parser: &mut SlashArgsParser<'a>) -> Result<T, SlashCommandUsageErrorKind>;
|
||||
|
||||
fn serialize(&self, value: &T, serializer: &mut SlashArgsSerializer);
|
||||
|
||||
fn finish<'a>(&self, parser: SlashArgsParser<'a>) -> Result<(), SlashCommandUsageErrorKind> {
|
||||
parser.finish()
|
||||
}
|
||||
|
||||
fn map_result<U, P, S>(
|
||||
self,
|
||||
parse_map: P,
|
||||
serialize_map: S,
|
||||
) -> SlashMapResultCodec<Self, P, S, T, U>
|
||||
where
|
||||
Self: Sized,
|
||||
P: Fn(T) -> Result<U, SlashCommandUsageErrorKind>,
|
||||
S: Fn(&U) -> T,
|
||||
{
|
||||
SlashMapResultCodec {
|
||||
inner: self,
|
||||
parse_map,
|
||||
serialize_map,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SlashMapResultCodec<C, P, S, T, U> {
|
||||
inner: C,
|
||||
parse_map: P,
|
||||
serialize_map: S,
|
||||
_phantom: PhantomData<fn(T) -> U>,
|
||||
}
|
||||
|
||||
impl<C, P, S, T, U> SlashArgsCodec<U> for SlashMapResultCodec<C, P, S, T, U>
|
||||
where
|
||||
C: SlashArgsCodec<T>,
|
||||
P: Fn(T) -> Result<U, SlashCommandUsageErrorKind>,
|
||||
S: Fn(&U) -> T,
|
||||
{
|
||||
fn parse<'a>(&self, parser: &mut SlashArgsParser<'a>) -> Result<U, SlashCommandUsageErrorKind> {
|
||||
let parsed = self.inner.parse(parser)?;
|
||||
(self.parse_map)(parsed)
|
||||
}
|
||||
|
||||
fn serialize(&self, value: &U, serializer: &mut SlashArgsSerializer) {
|
||||
let mapped = (self.serialize_map)(value);
|
||||
self.inner.serialize(&mapped, serializer);
|
||||
}
|
||||
|
||||
fn finish<'a>(&self, parser: SlashArgsParser<'a>) -> Result<(), SlashCommandUsageErrorKind> {
|
||||
self.inner.finish(parser)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SlashPositionalCodec<S> {
|
||||
spec: S,
|
||||
}
|
||||
|
||||
pub(crate) fn positional<S>(spec: S) -> SlashPositionalCodec<S> {
|
||||
SlashPositionalCodec { spec }
|
||||
}
|
||||
|
||||
impl<T, S> SlashArgsCodec<T> for SlashPositionalCodec<S>
|
||||
where
|
||||
S: SlashTokenValueSpec<T>,
|
||||
{
|
||||
fn parse<'a>(&self, parser: &mut SlashArgsParser<'a>) -> Result<T, SlashCommandUsageErrorKind> {
|
||||
parser.positional(&self.spec)
|
||||
}
|
||||
|
||||
fn serialize(&self, value: &T, serializer: &mut SlashArgsSerializer) {
|
||||
serializer.positional(value, &self.spec);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SlashListCodec<S> {
|
||||
spec: S,
|
||||
}
|
||||
|
||||
pub(crate) fn list<S>(spec: S) -> SlashListCodec<S> {
|
||||
SlashListCodec { spec }
|
||||
}
|
||||
|
||||
impl<T, S> SlashArgsCodec<Vec<T>> for SlashListCodec<S>
|
||||
where
|
||||
T: Clone,
|
||||
S: SlashTokenValueSpec<T>,
|
||||
{
|
||||
fn parse<'a>(
|
||||
&self,
|
||||
parser: &mut SlashArgsParser<'a>,
|
||||
) -> Result<Vec<T>, SlashCommandUsageErrorKind> {
|
||||
parser.positional_list(&self.spec)
|
||||
}
|
||||
|
||||
fn serialize(&self, value: &Vec<T>, serializer: &mut SlashArgsSerializer) {
|
||||
serializer.list(value.iter().cloned(), &self.spec);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct SlashNamedCodec<S> {
|
||||
key: &'static str,
|
||||
spec: S,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn named<S>(key: &'static str, spec: S) -> SlashNamedCodec<S> {
|
||||
SlashNamedCodec { key, spec }
|
||||
}
|
||||
|
||||
impl<T, S> SlashArgsCodec<Option<T>> for SlashNamedCodec<S>
|
||||
where
|
||||
S: SlashTokenValueSpec<T>,
|
||||
{
|
||||
fn parse<'a>(
|
||||
&self,
|
||||
parser: &mut SlashArgsParser<'a>,
|
||||
) -> Result<Option<T>, SlashCommandUsageErrorKind> {
|
||||
parser.named(self.key, &self.spec)
|
||||
}
|
||||
|
||||
fn serialize(&self, value: &Option<T>, serializer: &mut SlashArgsSerializer) {
|
||||
if let Some(value) = value {
|
||||
serializer.named(self.key, value, &self.spec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SlashNamedOrPositionalCodec<S> {
|
||||
key: &'static str,
|
||||
spec: S,
|
||||
}
|
||||
|
||||
pub(crate) fn named_or_positional<S>(key: &'static str, spec: S) -> SlashNamedOrPositionalCodec<S> {
|
||||
SlashNamedOrPositionalCodec { key, spec }
|
||||
}
|
||||
|
||||
impl<T, S> SlashArgsCodec<T> for SlashNamedOrPositionalCodec<S>
|
||||
where
|
||||
S: SlashTokenValueSpec<T>,
|
||||
{
|
||||
fn parse<'a>(&self, parser: &mut SlashArgsParser<'a>) -> Result<T, SlashCommandUsageErrorKind> {
|
||||
match parser.named(self.key, &self.spec)? {
|
||||
Some(value) => Ok(value),
|
||||
None => parser.positional(&self.spec),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize(&self, value: &T, serializer: &mut SlashArgsSerializer) {
|
||||
serializer.positional(value, &self.spec);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SlashRemainderCodec<S> {
|
||||
spec: S,
|
||||
}
|
||||
|
||||
pub(crate) fn remainder<S>(spec: S) -> SlashRemainderCodec<S> {
|
||||
SlashRemainderCodec { spec }
|
||||
}
|
||||
|
||||
impl<T, S> SlashArgsCodec<T> for SlashRemainderCodec<S>
|
||||
where
|
||||
S: SlashTextValueSpec<T>,
|
||||
{
|
||||
fn parse<'a>(&self, parser: &mut SlashArgsParser<'a>) -> Result<T, SlashCommandUsageErrorKind> {
|
||||
parser.required_remainder(&self.spec)
|
||||
}
|
||||
|
||||
fn serialize(&self, value: &T, serializer: &mut SlashArgsSerializer) {
|
||||
serializer.remainder(value, &self.spec);
|
||||
}
|
||||
|
||||
fn finish<'a>(&self, _parser: SlashArgsParser<'a>) -> Result<(), SlashCommandUsageErrorKind> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn trim_text_arg(text: &str, text_elements: &[TextElement]) -> Option<SlashTextArg> {
|
||||
let trimmed_start = text.len() - text.trim_start().len();
|
||||
let trimmed = text.trim();
|
||||
|
||||
Reference in New Issue
Block a user