feat(cli): add mcp prompt support via slash commands (#1323)

This commit is contained in:
Kalvin C
2025-02-27 15:47:29 -08:00
committed by GitHub
parent 5bf05d545e
commit d0ca46983e
24 changed files with 958 additions and 82 deletions
+164
View File
@@ -1,5 +1,7 @@
use anyhow::Result;
use rustyline::Editor;
use shlex;
use std::collections::HashMap;
#[derive(Debug)]
pub enum InputResult {
@@ -9,6 +11,15 @@ pub enum InputResult {
AddBuiltin(String),
ToggleTheme,
Retry,
ListPrompts,
PromptCommand(PromptCommandOptions),
}
#[derive(Debug)]
pub struct PromptCommandOptions {
pub name: String,
pub info: bool,
pub arguments: HashMap<String, String>,
}
pub fn get_input(
@@ -59,12 +70,67 @@ fn handle_slash_command(input: &str) -> Option<InputResult> {
Some(InputResult::Retry)
}
"/t" => Some(InputResult::ToggleTheme),
"/prompts" => Some(InputResult::ListPrompts),
s if s.starts_with("/prompt") => {
if s == "/prompt" {
// No arguments case
Some(InputResult::PromptCommand(PromptCommandOptions {
name: String::new(), // Empty name will trigger the error message in the rendering
info: false,
arguments: HashMap::new(),
}))
} else if let Some(stripped) = s.strip_prefix("/prompt ") {
// Has arguments case
parse_prompt_command(stripped)
} else {
// Handle invalid cases like "/promptxyz"
None
}
}
s if s.starts_with("/extension ") => Some(InputResult::AddExtension(s[11..].to_string())),
s if s.starts_with("/builtin ") => Some(InputResult::AddBuiltin(s[9..].to_string())),
_ => None,
}
}
fn parse_prompt_command(args: &str) -> Option<InputResult> {
let parts: Vec<String> = shlex::split(args).unwrap_or_default();
// set name to empty and error out in the rendering
let mut options = PromptCommandOptions {
name: parts.first().cloned().unwrap_or_default(),
info: false,
arguments: HashMap::new(),
};
// handle info at any point in the command
if parts.iter().any(|part| part == "--info") {
options.info = true;
}
// Parse remaining arguments
let mut i = 1;
while i < parts.len() {
let part = &parts[i];
// Skip flag arguments
if part == "--info" {
i += 1;
continue;
}
// Process key=value pairs - removed redundant contains check
if let Some((key, value)) = part.split_once('=') {
options.arguments.insert(key.to_string(), value.to_string());
}
i += 1;
}
Some(InputResult::PromptCommand(options))
}
fn print_help() {
println!(
"Available commands:
@@ -72,6 +138,8 @@ fn print_help() {
/t - Toggle Light/Dark/Ansi theme
/extension <command> - Add a stdio extension (format: ENV1=val1 command args...)
/builtin <names> - Add builtin extensions by name (comma-separated)
/prompts - List all available prompts by name
/prompt <name> [--info] [key=value...] - Get prompt info or execute a prompt
/? or /help - Display this help message
Navigation:
@@ -131,6 +199,33 @@ mod tests {
assert!(handle_slash_command("/unknown").is_none());
}
#[test]
fn test_prompt_command() {
// Test basic prompt info command
if let Some(InputResult::PromptCommand(opts)) =
handle_slash_command("/prompt test-prompt --info")
{
assert_eq!(opts.name, "test-prompt");
assert!(opts.info);
assert!(opts.arguments.is_empty());
} else {
panic!("Expected PromptCommand");
}
// Test prompt with arguments
if let Some(InputResult::PromptCommand(opts)) =
handle_slash_command("/prompt test-prompt arg1=val1 arg2=val2")
{
assert_eq!(opts.name, "test-prompt");
assert!(!opts.info);
assert_eq!(opts.arguments.len(), 2);
assert_eq!(opts.arguments.get("arg1"), Some(&"val1".to_string()));
assert_eq!(opts.arguments.get("arg2"), Some(&"val2".to_string()));
} else {
panic!("Expected PromptCommand");
}
}
// Test whitespace handling
#[test]
fn test_whitespace_handling() {
@@ -149,4 +244,73 @@ mod tests {
panic!("Expected AddBuiltin");
}
}
// Test prompt with no arguments
#[test]
fn test_prompt_no_args() {
// Test just "/prompt" with no arguments
if let Some(InputResult::PromptCommand(opts)) = handle_slash_command("/prompt") {
assert_eq!(opts.name, "");
assert!(!opts.info);
assert!(opts.arguments.is_empty());
} else {
panic!("Expected PromptCommand");
}
// Test invalid prompt command
assert!(handle_slash_command("/promptxyz").is_none());
}
// Test quoted arguments
#[test]
fn test_quoted_arguments() {
// Test prompt with quoted arguments
if let Some(InputResult::PromptCommand(opts)) = handle_slash_command(
r#"/prompt test-prompt arg1="value with spaces" arg2="another value""#,
) {
assert_eq!(opts.name, "test-prompt");
assert_eq!(opts.arguments.len(), 2);
assert_eq!(
opts.arguments.get("arg1"),
Some(&"value with spaces".to_string())
);
assert_eq!(
opts.arguments.get("arg2"),
Some(&"another value".to_string())
);
} else {
panic!("Expected PromptCommand");
}
// Test prompt with mixed quoted and unquoted arguments
if let Some(InputResult::PromptCommand(opts)) = handle_slash_command(
r#"/prompt test-prompt simple=value quoted="value with \"nested\" quotes""#,
) {
assert_eq!(opts.name, "test-prompt");
assert_eq!(opts.arguments.len(), 2);
assert_eq!(opts.arguments.get("simple"), Some(&"value".to_string()));
assert_eq!(
opts.arguments.get("quoted"),
Some(&r#"value with "nested" quotes"#.to_string())
);
} else {
panic!("Expected PromptCommand");
}
}
// Test invalid arguments
#[test]
fn test_invalid_arguments() {
// Test prompt with invalid arguments
if let Some(InputResult::PromptCommand(opts)) =
handle_slash_command(r#"/prompt test-prompt valid=value invalid_arg another_invalid"#)
{
assert_eq!(opts.name, "test-prompt");
assert_eq!(opts.arguments.len(), 1);
assert_eq!(opts.arguments.get("valid"), Some(&"value".to_string()));
// Invalid arguments are ignored but logged
} else {
panic!("Expected PromptCommand");
}
}
}