netget/events/types.rs
1//! Event type definitions
2
3use bytes::Bytes;
4use std::collections::HashMap;
5
6use crate::state::app_state::{EventHandlerMode, WebSearchMode};
7
8/// HTTP response to be sent back to the client
9#[derive(Debug, Clone)]
10pub struct HttpResponse {
11 pub status: u16,
12 pub headers: HashMap<String, String>,
13 pub body: Bytes,
14}
15
16/// Main application event enum
17#[derive(Debug)]
18pub enum AppEvent {
19 /// User command
20 UserCommand(UserCommand),
21 /// Tick/timeout event
22 Tick,
23 /// Shutdown signal
24 Shutdown,
25}
26
27/// UDP response to be sent back
28#[derive(Debug, Clone)]
29pub struct UdpResponse {
30 pub data: Vec<u8>,
31}
32
33/// User commands parsed from input
34/// These are ONLY slash commands - all other input goes to LLM for interpretation
35#[derive(Debug, Clone)]
36pub enum UserCommand {
37 /// Query current status (slash command: /status)
38 Status,
39 /// Show current model (slash command: /model)
40 ShowModel,
41 /// Change the Ollama model (slash command: /model <name>)
42 ChangeModel { model: String },
43 /// Show current log level (slash command: /log)
44 ShowLogLevel,
45 /// Change log level (slash command: /log <level>)
46 ChangeLogLevel { level: String },
47 /// Show current web search status (slash command: /web)
48 ShowWebSearch,
49 /// Set web search mode (slash command: /web on|off|ask)
50 SetWebSearch { mode: WebSearchMode },
51 /// Show current event handler mode (slash command: /handler)
52 ShowEventHandler,
53 /// Set event handler mode (slash command: /handler any|script|static|llm)
54 SetEventHandler { mode: EventHandlerMode },
55 /// Generate test output lines (slash command: /test <count>)
56 TestOutput { count: usize },
57 /// Test web search approval prompt (slash command: /test_ask)
58 TestAsk,
59 /// Set custom footer status message (slash command: /footer_status <message>)
60 SetFooterStatus { message: Option<String> },
61 /// Show protocol documentation (slash command: /docs [protocol])
62 ShowDocs { protocol: Option<String> },
63 /// Show environment information (slash command: /env or /environment)
64 ShowEnvironment,
65 /// Show usage statistics (slash command: /usage)
66 ShowUsage,
67 /// List registered protocols grouped by development state
68 /// (slash command: /stability or /protocols)
69 ShowStability,
70 /// Stop everything - all servers, connections, and clients (slash command: /stop)
71 StopAll,
72 /// Stop a specific server, connection, or client by unified ID (slash command: /stop <id>)
73 StopById { id: u32 },
74 /// Save all servers/clients or a specific one by ID (slash command: /save <name> [id])
75 Save { name: String, id: Option<u32> },
76 /// Load servers/clients from a file (slash command: /load <name>)
77 Load { name: String },
78 /// SQLite database management (slash command: /sqlite [db_id] [query])
79 #[cfg(feature = "sqlite")]
80 Sqlite {
81 db_id: Option<u32>,
82 query: Option<String>,
83 },
84 /// Show current LLM backend info (slash command: /backend)
85 ShowBackend,
86 /// Switch LLM backend (slash command: /backend ollama [url] or /backend openai <url> [api-key])
87 SetBackend { args: String },
88 /// List available simple protocols (slash command: /simple)
89 ListSimple,
90 /// Start a simple protocol server (slash command: /simple <protocol>)
91 StartSimple { protocol: String },
92 /// List running servers and clients and show the create/update command shape
93 /// (slash command: /manage)
94 Manage,
95 /// Update a running server or client's instruction by unified id
96 /// (slash command: /update <id> <instruction text>)
97 Update { id: u32, instruction: String },
98 /// Quit the application (slash command: /quit)
99 Quit,
100 /// Unknown slash command (error case)
101 UnknownSlashCommand { command: String },
102 /// Regular user input (not a slash command) - send to LLM for interpretation
103 Interpret { input: String },
104}
105
106impl UserCommand {
107 /// Parse a user input string into a command
108 /// Only handles slash commands - everything else goes to LLM for interpretation
109 pub fn parse(input: &str) -> Self {
110 let trimmed = input.trim();
111
112 // Check if it's a slash command
113 if !trimmed.starts_with('/') {
114 // Not a slash command - send to LLM for interpretation
115 return UserCommand::Interpret {
116 input: trimmed.to_string(),
117 };
118 }
119
120 // Parse slash commands
121 let input_lower = trimmed.to_lowercase();
122
123 if input_lower == "/status" || input_lower == "/?" {
124 return UserCommand::Status;
125 }
126
127 if input_lower == "/quit" || input_lower == "/exit" || input_lower == "/q" {
128 return UserCommand::Quit;
129 }
130
131 // /model command
132 if input_lower.starts_with("/model") {
133 let rest = trimmed[6..].trim();
134 if rest.is_empty() {
135 // Show current model
136 return UserCommand::ShowModel;
137 }
138 return UserCommand::ChangeModel {
139 model: rest.to_string(),
140 };
141 }
142
143 // /log command
144 if input_lower.starts_with("/log") {
145 let rest = trimmed[4..].trim();
146 if rest.is_empty() {
147 // Show current log level
148 return UserCommand::ShowLogLevel;
149 }
150 return UserCommand::ChangeLogLevel {
151 level: rest.to_string(),
152 };
153 }
154
155 // /web command
156 if input_lower.starts_with("/web") {
157 let rest = trimmed[4..].trim();
158 if rest.is_empty() {
159 // Show current web search status
160 return UserCommand::ShowWebSearch;
161 }
162 // Parse on/off/ask argument using WebSearchMode's FromStr
163 match rest.parse::<WebSearchMode>() {
164 Ok(mode) => return UserCommand::SetWebSearch { mode },
165 Err(_) => {
166 // Unknown argument - treat as unknown command
167 return UserCommand::UnknownSlashCommand {
168 command: trimmed.to_string(),
169 };
170 }
171 }
172 }
173
174 // /handler command
175 if input_lower.starts_with("/handler") {
176 let rest = trimmed[8..].trim();
177 if rest.is_empty() {
178 // Show current event handler mode
179 return UserCommand::ShowEventHandler;
180 }
181 // Parse any/script/static/llm argument using EventHandlerMode's FromStr
182 match rest.parse::<EventHandlerMode>() {
183 Ok(mode) => return UserCommand::SetEventHandler { mode },
184 Err(_) => {
185 // Unknown argument - treat as unknown command
186 return UserCommand::UnknownSlashCommand {
187 command: trimmed.to_string(),
188 };
189 }
190 }
191 }
192
193 // /test_ask command - test web search approval prompt
194 if input_lower == "/test_ask" {
195 return UserCommand::TestAsk;
196 }
197
198 // /test command - generate test output lines
199 if input_lower.starts_with("/test") {
200 let rest = trimmed[5..].trim();
201 if let Ok(count) = rest.parse::<usize>() {
202 return UserCommand::TestOutput { count };
203 }
204 // Invalid count - treat as unknown command
205 }
206
207 // /footer_status command - set custom footer status message
208 if input_lower.starts_with("/footer_status") {
209 let rest = trimmed[14..].trim();
210 let message = if rest.is_empty() {
211 None
212 } else {
213 // Replace literal \n with actual newlines
214 Some(rest.replace("\\n", "\n"))
215 };
216 return UserCommand::SetFooterStatus { message };
217 }
218
219 // /docs command - show protocol documentation
220 if input_lower.starts_with("/docs") {
221 let rest = trimmed[5..].trim();
222 let protocol = if rest.is_empty() {
223 None
224 } else {
225 Some(rest.to_string())
226 };
227 return UserCommand::ShowDocs { protocol };
228 }
229
230 // /env, /environment command - show environment information
231 if input_lower == "/env" || input_lower == "/environment" {
232 return UserCommand::ShowEnvironment;
233 }
234
235 // /usage command - show usage statistics
236 if input_lower == "/usage" {
237 return UserCommand::ShowUsage;
238 }
239
240 // /stability, /protocols command - list protocols grouped by maturity
241 if input_lower == "/stability" || input_lower == "/protocols" {
242 return UserCommand::ShowStability;
243 }
244
245 // /save command - save configuration to file
246 if input_lower.starts_with("/save") {
247 let rest = trimmed[5..].trim();
248 if rest.is_empty() {
249 // No filename provided - treat as unknown command
250 return UserCommand::UnknownSlashCommand {
251 command: trimmed.to_string(),
252 };
253 }
254 // Parse name and optional ID
255 let parts: Vec<&str> = rest.split_whitespace().collect();
256 let name = parts[0].to_string();
257 let id = if parts.len() > 1 {
258 parts[1].parse::<u32>().ok()
259 } else {
260 None
261 };
262 return UserCommand::Save { name, id };
263 }
264
265 // /load command - load configuration from file
266 if input_lower.starts_with("/load") {
267 let rest = trimmed[5..].trim();
268 if rest.is_empty() {
269 // No filename provided - treat as unknown command
270 return UserCommand::UnknownSlashCommand {
271 command: trimmed.to_string(),
272 };
273 }
274 return UserCommand::Load {
275 name: rest.to_string(),
276 };
277 }
278
279 // /stop command - stop everything or specific ID
280 if input_lower.starts_with("/stop") {
281 let rest = trimmed[5..].trim();
282 if rest.is_empty() {
283 // Stop everything
284 return UserCommand::StopAll;
285 }
286 // Try to parse ID
287 if let Ok(id) = rest.parse::<u32>() {
288 return UserCommand::StopById { id };
289 }
290 // Invalid ID - treat as unknown command
291 return UserCommand::UnknownSlashCommand {
292 command: trimmed.to_string(),
293 };
294 }
295
296 // /sqlite command - database management
297 #[cfg(feature = "sqlite")]
298 if input_lower.starts_with("/sqlite") {
299 let rest = trimmed[7..].trim();
300 if rest.is_empty() {
301 // List all databases
302 return UserCommand::Sqlite {
303 db_id: None,
304 query: None,
305 };
306 }
307
308 // Try to parse first part as database ID
309 let parts: Vec<&str> = rest.splitn(2, ' ').collect();
310 if let Ok(id) = parts[0].parse::<u32>() {
311 // Database ID provided
312 if parts.len() > 1 {
313 // Query provided
314 return UserCommand::Sqlite {
315 db_id: Some(id),
316 query: Some(parts[1].trim().to_string()),
317 };
318 } else {
319 // No query - show schema
320 return UserCommand::Sqlite {
321 db_id: Some(id),
322 query: None,
323 };
324 }
325 } else {
326 // Not a database ID - treat entire rest as query for first DB
327 return UserCommand::Sqlite {
328 db_id: None,
329 query: Some(rest.to_string()),
330 };
331 }
332 }
333
334 // /backend command - show or switch LLM backend
335 if input_lower.starts_with("/backend") {
336 let rest = trimmed[8..].trim();
337 if rest.is_empty() {
338 return UserCommand::ShowBackend;
339 }
340 return UserCommand::SetBackend {
341 args: rest.to_string(),
342 };
343 }
344
345 // /simple command - start simple protocol or list available protocols
346 if input_lower.starts_with("/simple") {
347 let rest = trimmed[7..].trim();
348 if rest.is_empty() {
349 // List available simple protocols
350 return UserCommand::ListSimple;
351 }
352 // Start specific simple protocol
353 return UserCommand::StartSimple {
354 protocol: rest.to_string(),
355 };
356 }
357
358 // /manage command - list servers/clients and show create/update shapes
359 if input_lower == "/manage" {
360 return UserCommand::Manage;
361 }
362
363 // /update command - update a running server/client's instruction by id
364 if input_lower.starts_with("/update") {
365 let rest = trimmed[7..].trim();
366 // Expect: <id> <instruction text...>
367 let mut parts = rest.splitn(2, char::is_whitespace);
368 let id_part = parts.next().unwrap_or("").trim();
369 let instruction = parts.next().unwrap_or("").trim().to_string();
370 if let Ok(id) = id_part.parse::<u32>() {
371 if !instruction.is_empty() {
372 return UserCommand::Update { id, instruction };
373 }
374 }
375 // Malformed - show usage as an unknown command
376 return UserCommand::UnknownSlashCommand {
377 command: trimmed.to_string(),
378 };
379 }
380
381 // Unknown slash command - return error, don't send to LLM
382 // This prevents accidental LLM calls from typos like "/modle"
383 UserCommand::UnknownSlashCommand {
384 command: trimmed.to_string(),
385 }
386 }
387}