use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub struct SubmissionParser;
impl SubmissionParser {
pub fn parse(content: &str) -> Submission {
let trimmed = content.trim();
let lower = trimmed.to_lowercase();
tracing::debug!("[SubmissionParser::parse] Parsing input: {:?}", trimmed);
if lower == "/undo" {
return Submission::Undo;
}
if lower == "/redo" {
return Submission::Redo;
}
if lower == "/interrupt" || lower == "/stop" {
return Submission::Interrupt;
}
if lower == "/compact" {
return Submission::Compact;
}
if lower == "/clear" {
return Submission::Clear;
}
if lower == "/heartbeat" {
return Submission::Heartbeat;
}
if lower == "/summarize" || lower == "/summary" {
return Submission::Summarize;
}
if lower == "/suggest" {
return Submission::Suggest;
}
if lower == "/thread new" || lower == "/new" {
return Submission::NewThread;
}
if lower == "/help" || lower == "/?" {
return Submission::SystemCommand {
command: "help".to_string(),
args: vec![],
};
}
if lower == "/version" {
return Submission::SystemCommand {
command: "version".to_string(),
args: vec![],
};
}
if lower == "/tools" {
return Submission::SystemCommand {
command: "tools".to_string(),
args: vec![],
};
}
if lower == "/skills" {
return Submission::SystemCommand {
command: "skills".to_string(),
args: vec![],
};
}
if lower.starts_with("/skills ") {
let args: Vec<String> = trimmed
.split_whitespace()
.skip(1)
.map(|s| s.to_string())
.collect();
return Submission::SystemCommand {
command: "skills".to_string(),
args,
};
}
if lower == "/ping" {
return Submission::SystemCommand {
command: "ping".to_string(),
args: vec![],
};
}
if lower == "/debug" {
return Submission::SystemCommand {
command: "debug".to_string(),
args: vec![],
};
}
if lower == "/reasoning" || lower.starts_with("/reasoning ") {
let args: Vec<String> = trimmed
.split_whitespace()
.skip(1)
.map(|s| s.to_string())
.collect();
return Submission::SystemCommand {
command: "reasoning".to_string(),
args,
};
}
if lower == "/restart" {
tracing::debug!("[SubmissionParser::parse] Recognized /restart command");
return Submission::SystemCommand {
command: "restart".to_string(),
args: vec![],
};
}
if lower.starts_with("/model") {
let args: Vec<String> = trimmed
.split_whitespace()
.skip(1)
.map(|s| s.to_string())
.collect();
return Submission::SystemCommand {
command: "model".to_string(),
args,
};
}
if lower == "/quit" || lower == "/exit" || lower == "/shutdown" {
return Submission::Quit;
}
if lower == "/status" || lower == "/progress" {
return Submission::JobStatus { job_id: None };
}
if let Some(rest) = lower
.strip_prefix("/status ")
.or_else(|| lower.strip_prefix("/progress "))
{
let id = rest.trim().to_string();
if !id.is_empty() {
return Submission::JobStatus { job_id: Some(id) };
}
}
if lower == "/list" {
return Submission::JobStatus { job_id: None };
}
if let Some(rest) = lower.strip_prefix("/cancel ") {
let id = rest.trim().to_string();
if !id.is_empty() {
return Submission::JobCancel { job_id: id };
}
}
if let Some(rest) = lower.strip_prefix("/thread ") {
let rest = rest.trim();
if rest != "new"
&& let Ok(id) = Uuid::parse_str(rest)
{
return Submission::SwitchThread { thread_id: id };
}
}
if let Some(rest) = lower.strip_prefix("/resume ")
&& let Ok(id) = Uuid::parse_str(rest.trim())
{
return Submission::Resume { checkpoint_id: id };
}
if trimmed.starts_with('{')
&& let Ok(submission) = serde_json::from_str::<Submission>(trimmed)
&& matches!(submission, Submission::ExecApproval { .. })
{
return submission;
}
match lower.as_str() {
"yes" | "y" | "approve" | "ok" | "/approve" | "/yes" | "/y" => {
return Submission::ApprovalResponse {
approved: true,
always: false,
};
}
"always" | "a" | "yes always" | "approve always" | "/always" | "/a" => {
return Submission::ApprovalResponse {
approved: true,
always: true,
};
}
"no" | "n" | "deny" | "reject" | "cancel" | "/deny" | "/no" | "/n" => {
return Submission::ApprovalResponse {
approved: false,
always: false,
};
}
_ => {}
}
Submission::UserInput {
content: content.to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Submission {
UserInput {
content: String,
},
ExecApproval {
request_id: Uuid,
approved: bool,
always: bool,
},
ApprovalResponse {
approved: bool,
always: bool,
},
Interrupt,
Compact,
Undo,
Redo,
Resume {
checkpoint_id: Uuid,
},
Clear,
SwitchThread {
thread_id: Uuid,
},
NewThread,
Heartbeat,
Summarize,
Suggest,
JobStatus {
job_id: Option<String>,
},
JobCancel {
job_id: String,
},
Quit,
SystemCommand {
command: String,
args: Vec<String>,
},
}
impl Submission {
pub fn user_input(content: impl Into<String>) -> Self {
Self::UserInput {
content: content.into(),
}
}
#[cfg(test)]
pub fn approval(request_id: Uuid, approved: bool) -> Self {
Self::ExecApproval {
request_id,
approved,
always: false,
}
}
#[cfg(test)]
pub fn always_approve(request_id: Uuid) -> Self {
Self::ExecApproval {
request_id,
approved: true,
always: true,
}
}
#[cfg(test)]
pub fn interrupt() -> Self {
Self::Interrupt
}
#[cfg(test)]
pub fn compact() -> Self {
Self::Compact
}
#[cfg(test)]
pub fn undo() -> Self {
Self::Undo
}
#[cfg(test)]
pub fn redo() -> Self {
Self::Redo
}
#[cfg(test)]
pub fn starts_turn(&self) -> bool {
matches!(self, Self::UserInput { .. })
}
pub fn is_control(&self) -> bool {
matches!(
self,
Self::Interrupt
| Self::Compact
| Self::Undo
| Self::Redo
| Self::Clear
| Self::NewThread
| Self::Heartbeat
| Self::Summarize
| Self::Suggest
| Self::JobStatus { .. }
| Self::JobCancel { .. }
| Self::SystemCommand { .. }
)
}
}
#[derive(Debug, Clone)]
pub enum SubmissionResult {
Response {
content: String,
},
NeedApproval {
request_id: Uuid,
tool_name: String,
description: String,
parameters: serde_json::Value,
allow_always: bool,
},
Ok {
message: Option<String>,
},
Error {
message: String,
},
Interrupted,
}
impl SubmissionResult {
pub fn response(content: impl Into<String>) -> Self {
Self::Response {
content: content.into(),
}
}
#[cfg(test)]
pub fn ok() -> Self {
Self::Ok { message: None }
}
pub fn ok_with_message(message: impl Into<String>) -> Self {
Self::Ok {
message: Some(message.into()),
}
}
pub fn error(message: impl Into<String>) -> Self {
Self::Error {
message: message.into(),
}
}
pub fn pending(message: impl Into<String>) -> Self {
Self::Ok {
message: Some(message.into()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_submission_types() {
let input = Submission::user_input("Hello");
assert!(input.starts_turn());
assert!(!input.is_control());
let undo = Submission::undo();
assert!(!undo.starts_turn());
assert!(undo.is_control());
}
#[test]
fn test_parser_user_input() {
let submission = SubmissionParser::parse("Hello, how are you?");
assert!(
matches!(submission, Submission::UserInput { content } if content == "Hello, how are you?")
);
}
#[test]
fn test_parser_undo() {
let submission = SubmissionParser::parse("/undo");
assert!(matches!(submission, Submission::Undo));
let submission = SubmissionParser::parse("/UNDO");
assert!(matches!(submission, Submission::Undo));
}
#[test]
fn test_parser_redo() {
let submission = SubmissionParser::parse("/redo");
assert!(matches!(submission, Submission::Redo));
}
#[test]
fn test_parser_interrupt() {
let submission = SubmissionParser::parse("/interrupt");
assert!(matches!(submission, Submission::Interrupt));
let submission = SubmissionParser::parse("/stop");
assert!(matches!(submission, Submission::Interrupt));
}
#[test]
fn test_parser_compact() {
let submission = SubmissionParser::parse("/compact");
assert!(matches!(submission, Submission::Compact));
}
#[test]
fn test_parser_clear() {
let submission = SubmissionParser::parse("/clear");
assert!(matches!(submission, Submission::Clear));
}
#[test]
fn test_parser_new_thread() {
let submission = SubmissionParser::parse("/thread new");
assert!(matches!(submission, Submission::NewThread));
let submission = SubmissionParser::parse("/new");
assert!(matches!(submission, Submission::NewThread));
}
#[test]
fn test_parser_switch_thread() {
let uuid = Uuid::new_v4();
let submission = SubmissionParser::parse(&format!("/thread {}", uuid));
assert!(matches!(submission, Submission::SwitchThread { thread_id } if thread_id == uuid));
}
#[test]
fn test_parser_resume() {
let uuid = Uuid::new_v4();
let submission = SubmissionParser::parse(&format!("/resume {}", uuid));
assert!(
matches!(submission, Submission::Resume { checkpoint_id } if checkpoint_id == uuid)
);
}
#[test]
fn test_parser_heartbeat() {
let submission = SubmissionParser::parse("/heartbeat");
assert!(matches!(submission, Submission::Heartbeat));
}
#[test]
fn test_parser_summarize() {
let submission = SubmissionParser::parse("/summarize");
assert!(matches!(submission, Submission::Summarize));
let submission = SubmissionParser::parse("/summary");
assert!(matches!(submission, Submission::Summarize));
}
#[test]
fn test_parser_suggest() {
let submission = SubmissionParser::parse("/suggest");
assert!(matches!(submission, Submission::Suggest));
}
#[test]
fn test_parser_invalid_commands_become_user_input() {
let submission = SubmissionParser::parse("/thread not-a-uuid");
assert!(matches!(submission, Submission::UserInput { .. }));
let submission = SubmissionParser::parse("/unknown");
assert!(matches!(submission, Submission::UserInput { content } if content == "/unknown"));
}
#[test]
fn test_parser_approval_response_aliases() {
assert!(matches!(
SubmissionParser::parse("y"),
Submission::ApprovalResponse {
approved: true,
always: false
}
));
assert!(matches!(
SubmissionParser::parse("/approve"),
Submission::ApprovalResponse {
approved: true,
always: false
}
));
assert!(matches!(
SubmissionParser::parse("a"),
Submission::ApprovalResponse {
approved: true,
always: true
}
));
assert!(matches!(
SubmissionParser::parse("/always"),
Submission::ApprovalResponse {
approved: true,
always: true
}
));
assert!(matches!(
SubmissionParser::parse("n"),
Submission::ApprovalResponse {
approved: false,
always: false
}
));
assert!(matches!(
SubmissionParser::parse("/deny"),
Submission::ApprovalResponse {
approved: false,
always: false
}
));
}
#[test]
fn test_parser_json_exec_approval() {
let req_id = Uuid::new_v4();
let json = serde_json::to_string(&Submission::ExecApproval {
request_id: req_id,
approved: true,
always: false,
})
.expect("serialize");
let submission = SubmissionParser::parse(&json);
assert!(
matches!(submission, Submission::ExecApproval { request_id, approved, always }
if request_id == req_id && approved && !always)
);
}
#[test]
fn test_parser_json_exec_approval_always() {
let req_id = Uuid::new_v4();
let json = serde_json::to_string(&Submission::ExecApproval {
request_id: req_id,
approved: true,
always: true,
})
.expect("serialize");
let submission = SubmissionParser::parse(&json);
assert!(
matches!(submission, Submission::ExecApproval { request_id, approved, always }
if request_id == req_id && approved && always)
);
}
#[test]
fn test_parser_json_exec_approval_deny() {
let req_id = Uuid::new_v4();
let json = serde_json::to_string(&Submission::ExecApproval {
request_id: req_id,
approved: false,
always: false,
})
.expect("serialize");
let submission = SubmissionParser::parse(&json);
assert!(
matches!(submission, Submission::ExecApproval { request_id, approved, always }
if request_id == req_id && !approved && !always)
);
}
#[test]
fn test_parser_json_non_approval_stays_user_input() {
let json = r#"{"UserInput":{"content":"hello"}}"#;
let submission = SubmissionParser::parse(json);
assert!(matches!(submission, Submission::UserInput { .. }));
}
#[test]
fn test_parser_json_roundtrip_matches_approval_handler() {
let request_id = Uuid::new_v4();
let approval = Submission::ExecApproval {
request_id,
approved: true,
always: false,
};
let json = serde_json::to_string(&approval).expect("serialize");
eprintln!("Serialized approval JSON: {}", json);
let parsed = SubmissionParser::parse(&json);
assert!(
matches!(parsed, Submission::ExecApproval { request_id: rid, approved, always }
if rid == request_id && approved && !always),
"Expected ExecApproval, got {:?}",
parsed
);
}
#[test]
fn test_parser_system_command_help() {
let submission = SubmissionParser::parse("/help");
assert!(
matches!(submission, Submission::SystemCommand { command, args } if command == "help" && args.is_empty())
);
let submission = SubmissionParser::parse("/?");
assert!(
matches!(submission, Submission::SystemCommand { command, .. } if command == "help")
);
let submission = SubmissionParser::parse("/HELP");
assert!(
matches!(submission, Submission::SystemCommand { command, .. } if command == "help")
);
}
#[test]
fn test_parser_system_command_model() {
let submission = SubmissionParser::parse("/model");
assert!(
matches!(submission, Submission::SystemCommand { command, args } if command == "model" && args.is_empty())
);
let submission = SubmissionParser::parse("/model gpt-4o");
assert!(
matches!(submission, Submission::SystemCommand { command, args } if command == "model" && args == vec!["gpt-4o"])
);
let submission = SubmissionParser::parse("/MODEL Claude-3.5");
assert!(
matches!(submission, Submission::SystemCommand { command, args } if command == "model" && args == vec!["Claude-3.5"])
);
}
#[test]
fn test_parser_system_command_version() {
let submission = SubmissionParser::parse("/version");
assert!(
matches!(submission, Submission::SystemCommand { command, args } if command == "version" && args.is_empty())
);
}
#[test]
fn test_parser_system_command_tools() {
let submission = SubmissionParser::parse("/tools");
assert!(
matches!(submission, Submission::SystemCommand { command, args } if command == "tools" && args.is_empty())
);
}
#[test]
fn test_parser_system_command_ping() {
let submission = SubmissionParser::parse("/ping");
assert!(
matches!(submission, Submission::SystemCommand { command, args } if command == "ping" && args.is_empty())
);
}
#[test]
fn test_parser_system_command_debug() {
let submission = SubmissionParser::parse("/debug");
assert!(
matches!(submission, Submission::SystemCommand { command, args } if command == "debug" && args.is_empty())
);
}
#[test]
fn test_parser_system_command_is_control() {
let submission = SubmissionParser::parse("/help");
assert!(submission.is_control());
assert!(!submission.starts_turn());
}
#[test]
fn test_parser_system_command_skills() {
let submission = SubmissionParser::parse("/skills");
assert!(
matches!(submission, Submission::SystemCommand { command, args } if command == "skills" && args.is_empty())
);
let submission = SubmissionParser::parse("/SKILLS");
assert!(
matches!(submission, Submission::SystemCommand { command, .. } if command == "skills")
);
}
#[test]
fn test_parser_system_command_skills_search() {
let submission = SubmissionParser::parse("/skills search markdown");
assert!(
matches!(submission, Submission::SystemCommand { command, args }
if command == "skills" && args == vec!["search", "markdown"])
);
let submission = SubmissionParser::parse("/skills search code review tools");
assert!(
matches!(submission, Submission::SystemCommand { command, args }
if command == "skills" && args == vec!["search", "code", "review", "tools"])
);
}
#[test]
fn test_parser_job_status() {
let s = SubmissionParser::parse("/status");
assert!(matches!(s, Submission::JobStatus { job_id: None }));
let s = SubmissionParser::parse("/progress");
assert!(matches!(s, Submission::JobStatus { job_id: None }));
let s = SubmissionParser::parse("/status abc123");
assert!(matches!(s, Submission::JobStatus { job_id: Some(id) } if id == "abc123"));
let s = SubmissionParser::parse("/progress abc123");
assert!(matches!(s, Submission::JobStatus { job_id: Some(id) } if id == "abc123"));
let s = SubmissionParser::parse("/STATUS");
assert!(matches!(s, Submission::JobStatus { job_id: None }));
}
#[test]
fn test_parser_job_list() {
let s = SubmissionParser::parse("/list");
assert!(matches!(s, Submission::JobStatus { job_id: None }));
let s = SubmissionParser::parse("/LIST");
assert!(matches!(s, Submission::JobStatus { job_id: None }));
}
#[test]
fn test_parser_job_cancel() {
let s = SubmissionParser::parse("/cancel abc123");
assert!(matches!(s, Submission::JobCancel { job_id } if job_id == "abc123"));
let s = SubmissionParser::parse("/cancel");
assert!(matches!(s, Submission::UserInput { .. }));
}
#[test]
fn test_job_commands_are_control() {
assert!(SubmissionParser::parse("/status").is_control());
assert!(SubmissionParser::parse("/list").is_control());
assert!(SubmissionParser::parse("/cancel abc").is_control());
}
#[test]
fn test_parser_quit() {
assert!(matches!(SubmissionParser::parse("/quit"), Submission::Quit));
assert!(matches!(SubmissionParser::parse("/exit"), Submission::Quit));
assert!(matches!(
SubmissionParser::parse("/shutdown"),
Submission::Quit
));
assert!(matches!(SubmissionParser::parse("/QUIT"), Submission::Quit));
assert!(matches!(SubmissionParser::parse("/Exit"), Submission::Quit));
}
}