1#[derive(Debug, Clone, serde::Deserialize)]
7pub struct KiroExtCommand {
8 pub name: String,
9 #[serde(default)]
10 pub description: String,
11 #[serde(default)]
12 pub input_hint: Option<String>,
13 #[serde(default)]
14 pub meta: Option<KiroCommandMeta>,
15}
16
17#[derive(Debug, Clone, serde::Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct KiroCommandMeta {
21 pub input_type: Option<String>,
23 pub options_method: Option<String>,
25 #[serde(default)]
27 pub local: bool,
28}
29
30impl KiroExtCommand {
31 pub fn is_executable(&self) -> bool {
36 match &self.meta {
37 None => true,
38 Some(meta) => !meta.local && meta.input_type.as_deref() != Some("selection"),
39 }
40 }
41}
42
43#[derive(serde::Deserialize)]
46#[serde(untagged)]
47pub(crate) enum KiroCommandsPayload {
48 Wrapped { commands: Vec<KiroExtCommand> },
50 AcpStyle {
52 #[serde(rename = "availableCommands")]
53 commands: Vec<KiroExtCommand>,
54 },
55 Bare(Vec<KiroExtCommand>),
57}
58
59impl KiroCommandsPayload {
60 pub(crate) fn commands(self) -> Vec<KiroExtCommand> {
61 match self {
62 Self::Wrapped { commands } => commands,
63 Self::AcpStyle { commands } => commands,
64 Self::Bare(commands) => commands,
65 }
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
76 fn is_executable_no_meta_returns_true() {
77 let cmd = KiroExtCommand {
78 name: "/compact".into(),
79 description: String::new(),
80 input_hint: None,
81 meta: None,
82 };
83 assert!(cmd.is_executable());
84 }
85
86 #[test]
87 fn is_executable_local_command_returns_false() {
88 let cmd = KiroExtCommand {
89 name: "/quit".into(),
90 description: String::new(),
91 input_hint: None,
92 meta: Some(KiroCommandMeta {
93 input_type: None,
94 options_method: None,
95 local: true,
96 }),
97 };
98 assert!(!cmd.is_executable());
99 }
100
101 #[test]
102 fn is_executable_selection_input_type_returns_false() {
103 let cmd = KiroExtCommand {
104 name: "/model".into(),
105 description: String::new(),
106 input_hint: None,
107 meta: Some(KiroCommandMeta {
108 input_type: Some("selection".into()),
109 options_method: Some("_kiro.dev/commands/model/options".into()),
110 local: false,
111 }),
112 };
113 assert!(!cmd.is_executable());
114 }
115
116 #[test]
117 fn is_executable_panel_input_type_returns_true() {
118 let cmd = KiroExtCommand {
119 name: "/context".into(),
120 description: String::new(),
121 input_hint: None,
122 meta: Some(KiroCommandMeta {
123 input_type: Some("panel".into()),
124 options_method: None,
125 local: false,
126 }),
127 };
128 assert!(cmd.is_executable());
129 }
130
131 #[test]
134 fn payload_wrapped_shape() {
135 let json = r#"{ "commands": [{ "name": "/compact" }] }"#;
136 let payload: KiroCommandsPayload = serde_json::from_str(json).unwrap();
137 let cmds = payload.commands();
138 assert_eq!(cmds.len(), 1);
139 assert_eq!(cmds[0].name, "/compact");
140 }
141
142 #[test]
143 fn payload_acp_style_shape() {
144 let json = r#"{ "availableCommands": [{ "name": "/help" }] }"#;
145 let payload: KiroCommandsPayload = serde_json::from_str(json).unwrap();
146 let cmds = payload.commands();
147 assert_eq!(cmds.len(), 1);
148 assert_eq!(cmds[0].name, "/help");
149 }
150
151 #[test]
152 fn payload_bare_array_shape() {
153 let json = r#"[{ "name": "/context" }]"#;
154 let payload: KiroCommandsPayload = serde_json::from_str(json).unwrap();
155 let cmds = payload.commands();
156 assert_eq!(cmds.len(), 1);
157 assert_eq!(cmds[0].name, "/context");
158 }
159
160 #[test]
163 fn command_minimal_deserialization() {
164 let json = r#"{ "name": "/compact" }"#;
165 let cmd: KiroExtCommand = serde_json::from_str(json).unwrap();
166 assert_eq!(cmd.name, "/compact");
167 assert_eq!(cmd.description, "");
168 assert!(cmd.input_hint.is_none());
169 assert!(cmd.meta.is_none());
170 }
171
172 #[test]
173 fn command_full_deserialization() {
174 let json = r#"{
175 "name": "/model",
176 "description": "Switch AI model",
177 "input_hint": "Choose a model",
178 "meta": {
179 "inputType": "selection",
180 "optionsMethod": "_kiro.dev/commands/model/options",
181 "local": true
182 }
183 }"#;
184 let cmd: KiroExtCommand = serde_json::from_str(json).unwrap();
185 assert_eq!(cmd.name, "/model");
186 assert_eq!(cmd.description, "Switch AI model");
187 assert_eq!(cmd.input_hint.as_deref(), Some("Choose a model"));
188 let meta = cmd.meta.unwrap();
189 assert_eq!(meta.input_type.as_deref(), Some("selection"));
190 assert_eq!(
191 meta.options_method.as_deref(),
192 Some("_kiro.dev/commands/model/options")
193 );
194 assert!(meta.local);
195 }
196}