1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4pub fn normalize_thread_id(thread_id: &str) -> String {
5 thread_id.chars().filter(|c| c.is_alphanumeric() || *c == '_').collect()
6}
7
8#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, PartialEq)]
13#[serde(rename_all = "snake_case")]
14pub enum InitializeType {
15 FirstCall,
20
21 UserAskedModeChange,
26
27 ResetShell,
32
33 UserAskedChangeWorkspace,
38}
39
40#[derive(Debug, Clone, PartialEq)]
41pub enum ModeName {
42 Wcgw,
43 Architect,
44 CodeWriter,
45}
46
47impl Serialize for ModeName {
49 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
50 where
51 S: serde::Serializer,
52 {
53 match self {
54 ModeName::Wcgw => serializer.serialize_str("wcgw"),
55 ModeName::Architect => serializer.serialize_str("architect"),
56 ModeName::CodeWriter => serializer.serialize_str("code_writer"),
57 }
58 }
59}
60
61impl<'de> Deserialize<'de> for ModeName {
63 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
64 where
65 D: serde::Deserializer<'de>,
66 {
67 let s = String::deserialize(deserializer)?;
68 match s.as_str() {
69 "wcgw" => Ok(ModeName::Wcgw),
70 "architect" => Ok(ModeName::Architect),
71 "code_writer" | "code_write" | "code-writer" => Ok(ModeName::CodeWriter),
72 _ => Err(serde::de::Error::custom(format!("Unknown mode name: {s}"))),
73 }
74 }
75}
76
77impl JsonSchema for ModeName {
79 fn schema_name() -> std::borrow::Cow<'static, str> {
80 "ModeName".into()
81 }
82
83 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
84 schemars::Schema::new_ref("#/definitions/ModeName".to_string())
85 }
86}
87
88#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, PartialEq, Default)]
89pub struct CodeWriterConfig {
90 #[serde(default)]
91 pub allowed_globs: AllowedGlobs,
92 #[serde(default)]
93 pub allowed_commands: AllowedCommands,
94}
95
96impl CodeWriterConfig {
97 pub fn update_relative_globs(&mut self, workspace_root: &str) {
98 if let AllowedGlobs::List(globs) = &self.allowed_globs {
100 let updated_globs = globs
101 .iter()
102 .map(|glob| {
103 if std::path::Path::new(glob).is_absolute() {
104 glob.clone()
105 } else {
106 format!("{workspace_root}/{glob}")
107 }
108 })
109 .collect();
110
111 self.allowed_globs = AllowedGlobs::List(updated_globs);
112 }
113 }
114}
115
116#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, PartialEq)]
117#[serde(untagged)]
118pub enum AllowedGlobs {
119 All(String),
120 List(Vec<String>),
121}
122
123impl Default for AllowedGlobs {
124 fn default() -> Self {
125 AllowedGlobs::All("all".to_string())
126 }
127}
128
129impl AllowedGlobs {
130 #[allow(dead_code)]
131 pub fn is_allowed(&self, path: &str) -> bool {
132 match self {
133 AllowedGlobs::All(s) if s == "all" => true,
134 AllowedGlobs::List(globs) => globs.iter().any(|g| match glob::Pattern::new(g) {
135 Ok(pattern) => pattern.matches(path),
136 Err(_) => false,
137 }),
138 AllowedGlobs::All(_) => false,
139 }
140 }
141}
142
143#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, PartialEq)]
144#[serde(untagged)]
145pub enum AllowedCommands {
146 All(String),
147 List(Vec<String>),
148}
149
150impl Default for AllowedCommands {
151 fn default() -> Self {
152 AllowedCommands::All("all".to_string())
153 }
154}
155
156impl AllowedCommands {
157 #[allow(dead_code)]
158 pub fn is_allowed(&self, command_line: &str) -> bool {
159 match self {
160 AllowedCommands::All(s) if s == "all" => true,
161 AllowedCommands::List(commands) => {
162 let cmd_prog = command_line.split_whitespace().next().unwrap_or("");
163 commands.iter().any(|c| cmd_prog == c)
164 }
165 AllowedCommands::All(_) => false,
166 }
167 }
168}
169
170#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
175pub struct Initialize {
176 #[serde(rename = "type")]
183 #[serde(default = "default_init_type")]
184 pub init_type: InitializeType,
185
186 pub any_workspace_path: String,
193
194 #[serde(default)]
199 pub initial_files_to_read: Vec<String>,
200
201 #[serde(default = "String::new")]
206 #[serde(deserialize_with = "deserialize_string_or_null")]
207 pub task_id_to_resume: String,
208
209 #[serde(default = "default_mode_name")]
215 pub mode_name: ModeName,
216
217 #[serde(default)]
222 #[serde(deserialize_with = "deserialize_string_or_null")]
223 pub thread_id: String,
224
225 #[serde(default)]
230 #[serde(deserialize_with = "deserialize_code_writer_config")]
231 pub code_writer_config: Option<CodeWriterConfig>,
232}
233
234fn deserialize_string_or_null<'de, D>(deserializer: D) -> Result<String, D::Error>
236where
237 D: serde::Deserializer<'de>,
238{
239 let result = serde_json::Value::deserialize(deserializer)?;
241
242 match result {
243 serde_json::Value::Null => Ok(String::new()),
245 serde_json::Value::String(s) => {
247 if s == "null" {
249 Ok(String::new())
250 } else {
251 Ok(s)
252 }
253 }
254 _ => match serde_json::to_string(&result) {
256 Ok(s) => Ok(s),
257 Err(_) => Ok(String::new()),
258 },
259 }
260}
261
262fn deserialize_code_writer_config<'de, D>(
264 deserializer: D,
265) -> Result<Option<CodeWriterConfig>, D::Error>
266where
267 D: serde::Deserializer<'de>,
268{
269 let value = serde_json::Value::deserialize(deserializer)?;
271
272 match value {
273 serde_json::Value::Null => Ok(None),
275 serde_json::Value::String(s) if s == "null" => Ok(None),
276 _ => {
278 match serde_json::from_value::<CodeWriterConfig>(value.clone()) {
279 Ok(config) => {
280 tracing::debug!("Successfully parsed CodeWriterConfig: {:?}", config);
281 Ok(Some(config))
282 }
283 Err(e) => {
284 tracing::error!("Failed to parse CodeWriterConfig: {}. Value: {}", e, value);
286 Ok(None) }
288 }
289 }
290 }
291}
292
293fn default_mode_name() -> ModeName {
295 ModeName::Wcgw
296}
297
298fn default_init_type() -> InitializeType {
300 InitializeType::FirstCall
301}
302
303#[derive(Debug, Clone, Copy, PartialEq)]
305pub enum Modes {
306 Wcgw,
307 Architect,
308 CodeWriter,
309}
310
311impl std::fmt::Display for Modes {
312 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313 match self {
314 Modes::Wcgw => write!(f, "wcgw"),
315 Modes::Architect => write!(f, "architect"),
316 Modes::CodeWriter => write!(f, "code_writer"),
317 }
318 }
319}
320
321impl JsonSchema for Modes {
323 fn schema_name() -> std::borrow::Cow<'static, str> {
324 "Modes".into()
325 }
326
327 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
328 schemars::Schema::new_ref("#/definitions/Modes".to_string())
329 }
330}
331
332#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
335pub enum SpecialKey {
336 Enter,
337 #[serde(rename = "Key-up")]
338 KeyUp,
339 #[serde(rename = "Key-down")]
340 KeyDown,
341 #[serde(rename = "Key-left")]
342 KeyLeft,
343 #[serde(rename = "Key-right")]
344 KeyRight,
345 #[serde(rename = "Ctrl-c")]
346 CtrlC,
347 #[serde(rename = "Ctrl-d")]
348 CtrlD,
349 #[serde(rename = "Ctrl-z")]
350 CtrlZ,
351}
352
353#[derive(Debug, Clone, Serialize, JsonSchema)]
358pub struct ReadFiles {
359 pub file_paths: Vec<String>,
363
364 #[serde(skip)]
366 #[schemars(skip)]
367 pub start_line_nums: Vec<Option<usize>>,
368
369 #[serde(skip)]
370 #[schemars(skip)]
371 pub end_line_nums: Vec<Option<usize>>,
372}
373
374impl<'de> Deserialize<'de> for ReadFiles {
376 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
377 where
378 D: serde::Deserializer<'de>,
379 {
380 #[derive(Deserialize)]
381 struct ReadFilesHelper {
382 file_paths: Option<Vec<String>>,
383 }
384
385 let input = serde_json::Value::deserialize(deserializer)?;
386
387 if !input.is_object() {
388 if input.is_null() {
389 return Err(serde::de::Error::custom("Cannot convert null to ReadFiles object."));
390 }
391 return Err(serde::de::Error::custom(format!("Expected object, got {input}")));
392 }
393
394 let helper: ReadFilesHelper = serde_json::from_value(input.clone())
395 .map_err(|e| serde::de::Error::custom(format!("Failed to parse ReadFiles: {e}")))?;
396
397 let file_paths = match helper.file_paths {
398 Some(paths) if !paths.is_empty() => paths,
399 Some(_) => return Err(serde::de::Error::custom("file_paths must not be empty.")),
400 None => return Err(serde::de::Error::custom("file_paths is required.")),
401 };
402
403 let mut clean_file_paths = Vec::with_capacity(file_paths.len());
405 let mut start_line_nums = Vec::with_capacity(file_paths.len());
406 let mut end_line_nums = Vec::with_capacity(file_paths.len());
407
408 for path in file_paths {
409 let (clean_path, start, end) = parse_file_path_with_line_range(&path);
410 clean_file_paths.push(clean_path);
411 start_line_nums.push(start);
412 end_line_nums.push(end);
413 }
414
415 Ok(ReadFiles { file_paths: clean_file_paths, start_line_nums, end_line_nums })
416 }
417}
418
419fn parse_file_path_with_line_range(path: &str) -> (String, Option<usize>, Option<usize>) {
420 let Some((potential_path, line_spec)) = path.rsplit_once(':') else {
421 return (path.to_string(), None, None);
422 };
423
424 let Some((start, end)) = parse_line_spec(line_spec) else {
425 return (path.to_string(), None, None);
426 };
427
428 (potential_path.to_string(), start, end)
429}
430
431fn parse_line_spec(line_spec: &str) -> Option<(Option<usize>, Option<usize>)> {
432 if line_spec.chars().all(|c| c.is_ascii_digit()) {
433 return line_spec.parse().ok().map(|line| (Some(line), None));
434 }
435
436 let (start, end) = line_spec.split_once('-')?;
437
438 if start.is_empty() && !end.is_empty() && end.chars().all(|c| c.is_ascii_digit()) {
439 return end.parse().ok().map(|line| (None, Some(line)));
440 }
441
442 if !start.is_empty()
443 && start.chars().all(|c| c.is_ascii_digit())
444 && (end.is_empty() || end.chars().all(|c| c.is_ascii_digit()))
445 {
446 let start = start.parse().ok()?;
447 let end = if end.is_empty() { None } else { Some(end.parse().ok()?) };
448 return Some((Some(start), end));
449 }
450
451 None
452}
453
454impl ReadFiles {
455 pub fn show_line_numbers(&self) -> bool {
457 true
458 }
459
460 pub fn get_clean_path(&self, index: usize) -> String {
462 parse_file_path_with_line_range(&self.file_paths[index]).0
463 }
464}
465
466fn default_true() -> bool {
468 true
469}
470
471#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
473#[serde(tag = "type", rename_all = "snake_case")]
474pub enum BashCommandAction {
475 Command {
477 command: String,
478 #[serde(default)]
479 is_background: bool,
480 #[serde(default)]
486 allow_multi: bool,
487 },
488
489 StatusCheck {
497 #[serde(default = "default_true")]
498 status_check: bool,
499 bg_command_id: Option<String>,
500 #[serde(default)]
501 scrollback_lines: Option<usize>,
502 #[serde(default)]
503 verbose: bool,
504 },
505
506 SendText {
510 send_text: String,
511 bg_command_id: Option<String>,
512 #[serde(default)]
513 submit: bool,
514 },
515
516 SendSpecials {
519 send_specials: Vec<SpecialKey>,
520 bg_command_id: Option<String>,
521 #[serde(default)]
522 submit: bool,
523 },
524
525 SendAscii {
528 send_ascii: Vec<u8>,
529 bg_command_id: Option<String>,
530 #[serde(default)]
531 submit: bool,
532 },
533}
534
535#[derive(Debug, Clone, Serialize, JsonSchema)]
537pub struct BashCommand {
538 pub action_json: BashCommandAction,
540
541 #[serde(default)]
543 #[serde(skip_serializing_if = "Option::is_none")]
544 pub wait_for_seconds: Option<f32>,
545
546 #[serde(default)]
548 pub thread_id: String,
549}
550
551impl<'de> Deserialize<'de> for BashCommand {
553 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
554 where
555 D: serde::Deserializer<'de>,
556 {
557 #[derive(Deserialize)]
559 struct BashCommandHelper {
560 action_json: serde_json::Value,
561 #[serde(default)]
562 wait_for_seconds: Option<f32>,
563 #[serde(default)]
564 #[serde(deserialize_with = "deserialize_string_or_null")]
565 thread_id: String,
566 }
567
568 let helper = BashCommandHelper::deserialize(deserializer)?;
570
571 let action_json = match helper.action_json {
573 serde_json::Value::String(s) => {
574 let sanitized = s.replace('\n', " ");
577 match serde_json::from_str(&sanitized) {
578 Ok(json) => json,
579 Err(e) => {
580 tracing::warn!(
583 "Failed to parse action_json as JSON, trying fallback: {}",
584 e
585 );
586
587 if s.contains("command") && s.contains('{') && s.contains('}') {
589 tracing::debug!("JSON parse error on: {}", s);
593
594 let re_sanitized = s
596 .replace('\n', "\\n") .replace('\r', "\\r") .replace('\t', "\\t"); let re_sanitized = if !s.contains('"') && s.contains(':') {
602 tracing::debug!("Attempting to fix unquoted JSON keys/values");
604 re_sanitized
605 } else {
606 re_sanitized
607 };
608
609 match serde_json::from_str(&re_sanitized) {
610 Ok(json) => json,
611 Err(err) => {
612 tracing::error!("Secondary JSON parse error: {}", err);
614 serde_json::json!({"type": "command", "command": s})
617 }
618 }
619 } else {
620 tracing::info!("Treating as simple command: {}", s);
623 serde_json::json!({"type": "command", "command": s})
624 }
625 }
626 }
627 }
628 value => value,
630 };
631
632 let mut action: BashCommandAction =
634 serde_json::from_value(action_json.clone()).map_err(|e| {
635tracing::error!(
637 "Failed to deserialize action_json to BashCommandAction: {}\nProblematic JSON: {}",
638 e,
639 action_json
640);
641
642let err_str = e.to_string();
644if err_str.contains("unexpected token") || err_str.contains("Unexpected token") {
645 return serde::de::Error::custom(format!(
646 "JSON syntax error: {e}. Please check your JSON structure. Each field name should be in quotes, and string values should be in quotes."
647 ));
648}
649
650serde::de::Error::custom(format!("Invalid action_json: {e}. Please ensure your JSON is properly formatted."))
651 })?;
652
653 Ok(BashCommand {
655 action_json: action,
656 wait_for_seconds: helper.wait_for_seconds,
657 thread_id: normalize_thread_id(&helper.thread_id),
658 })
659 }
660}
661
662#[derive(Debug, Clone, JsonSchema, PartialEq)]
664pub struct BashCommandMode {
665 pub bash_mode: BashMode,
666 pub allowed_commands: AllowedCommands,
667}
668
669#[derive(Debug, Clone, Copy, JsonSchema, PartialEq)]
670pub enum BashMode {
671 NormalMode,
672 RestrictedMode,
673}
674
675#[derive(Debug, Clone, JsonSchema, PartialEq)]
677pub struct FileEditMode {
678 pub allowed_globs: AllowedGlobs,
679}
680
681#[derive(Debug, Clone, JsonSchema, PartialEq)]
683pub struct WriteIfEmptyMode {
684 pub allowed_globs: AllowedGlobs,
685}
686
687#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
692pub struct FileWriteOrEdit {
693 pub file_path: String,
697
698 pub percentage_to_change: u32,
703
704 pub text_or_search_replace_blocks: String,
717
718 pub thread_id: String,
720}
721
722#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
727pub struct ContextSave {
728 pub id: String,
733
734 pub project_root_path: String,
739
740 pub description: String,
745
746 pub relevant_file_globs: Vec<String>,
751}
752
753#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
757pub struct ReadImage {
758 pub file_path: String,
762}