pub fn normalize_command_name(name: &str) -> Option<String> {
let mut out = String::with_capacity(name.len());
let mut prev_underscore = false;
for c in name.chars() {
let mapped = if c.is_ascii_alphanumeric() {
c.to_ascii_lowercase()
} else {
'_'
};
if mapped == '_' {
if prev_underscore {
continue;
}
prev_underscore = true;
} else {
prev_underscore = false;
}
out.push(mapped);
}
let trimmed = out.trim_matches('_');
if trimmed.is_empty() {
return None;
}
let mut result: String = trimmed.chars().take(32).collect();
while result.ends_with('_') {
result.pop();
}
if result.is_empty() {
None
} else {
Some(result)
}
}
#[derive(Debug, Clone)]
pub enum RemoteCommand {
Projects,
Sessions,
Resume { session_id: Option<String> },
New { project_dir: Option<String> },
Status,
Stream { level: Option<String> },
Workspace { scope: String },
Cancel,
Approve,
Reject,
Help,
Models,
Agents,
Switch { name: String },
History,
DeleteSession { session_id: Option<String> },
Message { text: String },
}
pub fn parse_remote_command(input: &str) -> RemoteCommand {
let trimmed = input.trim();
if !trimmed.starts_with('/') {
return RemoteCommand::Message {
text: trimmed.to_string(),
};
}
let mut parts = trimmed.splitn(2, char::is_whitespace);
let cmd = parts.next().unwrap_or("");
let arg = parts.next().map(|s| s.trim().to_string());
match cmd.to_lowercase().as_str() {
"/projects" | "/p" => RemoteCommand::Projects,
"/sessions" | "/s" => RemoteCommand::Sessions,
"/resume" => RemoteCommand::Resume { session_id: arg },
"/new" => RemoteCommand::New { project_dir: arg },
"/status" => RemoteCommand::Status,
"/stream" => {
if let Some(level) = arg {
RemoteCommand::Stream { level: Some(level) }
} else {
RemoteCommand::Stream { level: None }
}
}
"/workspace" => {
if let Some(scope) = arg {
RemoteCommand::Workspace { scope }
} else {
RemoteCommand::Status }
}
"/cancel" | "/stop" => RemoteCommand::Cancel,
"/approve" | "/y" | "/yes" => RemoteCommand::Approve,
"/reject" | "/no" => RemoteCommand::Reject,
"/help" | "/h" => RemoteCommand::Help,
"/history" | "/hist" => RemoteCommand::History,
"/delete" => RemoteCommand::DeleteSession { session_id: arg },
"/models" => RemoteCommand::Models,
"/agents" => RemoteCommand::Agents,
"/switch" => {
if let Some(name) = arg {
RemoteCommand::Switch { name }
} else {
RemoteCommand::Projects }
}
_ => {
RemoteCommand::Message {
text: trimmed.to_string(),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_message() {
let cmd = parse_remote_command("fix the login bug");
assert!(matches!(cmd, RemoteCommand::Message { text } if text == "fix the login bug"));
}
#[test]
fn parse_slash_commands() {
assert!(matches!(
parse_remote_command("/p"),
RemoteCommand::Projects
));
assert!(matches!(
parse_remote_command("/s"),
RemoteCommand::Sessions
));
assert!(matches!(
parse_remote_command("/cancel"),
RemoteCommand::Cancel
));
assert!(matches!(
parse_remote_command("/stop"),
RemoteCommand::Cancel
));
assert!(matches!(parse_remote_command("/y"), RemoteCommand::Approve));
assert!(matches!(parse_remote_command("/no"), RemoteCommand::Reject));
assert!(matches!(parse_remote_command("/h"), RemoteCommand::Help));
}
#[test]
fn parse_stream_level() {
let cmd = parse_remote_command("/stream full");
assert!(matches!(cmd, RemoteCommand::Stream { level } if level.as_deref() == Some("full")));
}
#[test]
fn unknown_slash_falls_back_to_message() {
let cmd = parse_remote_command("/unknown stuff");
assert!(matches!(cmd, RemoteCommand::Message { text } if text == "/unknown stuff"));
}
}