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 strip_tail_pipe(command: &str) -> String {
467 let trimmed = command.trim_end();
468 let Some((prefix, tail_part)) = trimmed.rsplit_once('|') else {
469 return command.to_string();
470 };
471
472 if is_tail_invocation(tail_part.trim()) {
473 prefix.trim_end().to_string()
474 } else {
475 command.to_string()
476 }
477}
478
479fn is_tail_invocation(tail_part: &str) -> bool {
480 let mut parts = tail_part.split_whitespace();
481 if parts.next() != Some("tail") {
482 return false;
483 }
484
485 match (parts.next(), parts.next(), parts.next()) {
486 (None, None, None) => true,
487 (Some(count), None, None) => tail_count_arg(count),
488 (Some("-n"), Some(count), None) => count.chars().all(|c| c.is_ascii_digit()),
489 _ => false,
490 }
491}
492
493fn tail_count_arg(arg: &str) -> bool {
494 if arg.chars().all(|c| c.is_ascii_digit()) {
495 return true;
496 }
497
498 arg.strip_prefix('-').is_some_and(|count| count.chars().all(|c| c.is_ascii_digit()))
499}
500
501fn default_true() -> bool {
503 true
504}
505
506#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
508#[serde(tag = "type", rename_all = "snake_case")]
509pub enum BashCommandAction {
510 Command {
512 command: String,
513 #[serde(default)]
514 is_background: bool,
515 },
516
517 StatusCheck {
519 #[serde(default = "default_true")]
520 status_check: bool,
521 bg_command_id: Option<String>,
522 },
523
524 SendText { send_text: String, bg_command_id: Option<String> },
526
527 SendSpecials { send_specials: Vec<SpecialKey>, bg_command_id: Option<String> },
529
530 SendAscii { send_ascii: Vec<u8>, bg_command_id: Option<String> },
532}
533
534#[derive(Debug, Clone, Serialize, JsonSchema)]
536pub struct BashCommand {
537 pub action_json: BashCommandAction,
539
540 #[serde(default)]
542 #[serde(skip_serializing_if = "Option::is_none")]
543 pub wait_for_seconds: Option<f32>,
544
545 #[serde(default)]
547 pub thread_id: String,
548}
549
550impl<'de> Deserialize<'de> for BashCommand {
552 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
553 where
554 D: serde::Deserializer<'de>,
555 {
556 #[derive(Deserialize)]
558 struct BashCommandHelper {
559 action_json: serde_json::Value,
560 #[serde(default)]
561 wait_for_seconds: Option<f32>,
562 #[serde(default)]
563 #[serde(deserialize_with = "deserialize_string_or_null")]
564 thread_id: String,
565 }
566
567 let helper = BashCommandHelper::deserialize(deserializer)?;
569
570 let action_json = match helper.action_json {
572 serde_json::Value::String(s) => {
573 let sanitized = s.replace('\n', " ");
576 match serde_json::from_str(&sanitized) {
577 Ok(json) => json,
578 Err(e) => {
579 tracing::warn!(
582 "Failed to parse action_json as JSON, trying fallback: {}",
583 e
584 );
585
586 if s.contains("command") && s.contains('{') && s.contains('}') {
588 tracing::debug!("JSON parse error on: {}", s);
592
593 let re_sanitized = s
595 .replace('\n', "\\n") .replace('\r', "\\r") .replace('\t', "\\t"); let re_sanitized = if !s.contains('"') && s.contains(':') {
601 tracing::debug!("Attempting to fix unquoted JSON keys/values");
603 re_sanitized
604 } else {
605 re_sanitized
606 };
607
608 match serde_json::from_str(&re_sanitized) {
609 Ok(json) => json,
610 Err(err) => {
611 tracing::error!("Secondary JSON parse error: {}", err);
613 serde_json::json!({"type": "command", "command": s})
616 }
617 }
618 } else {
619 tracing::info!("Treating as simple command: {}", s);
622 serde_json::json!({"type": "command", "command": s})
623 }
624 }
625 }
626 }
627 value => value,
629 };
630
631 let mut action: BashCommandAction =
633 serde_json::from_value(action_json.clone()).map_err(|e| {
634tracing::error!(
636 "Failed to deserialize action_json to BashCommandAction: {}\nProblematic JSON: {}",
637 e,
638 action_json
639);
640
641let err_str = e.to_string();
643if err_str.contains("unexpected token") || err_str.contains("Unexpected token") {
644 return serde::de::Error::custom(format!(
645 "JSON syntax error: {e}. Please check your JSON structure. Each field name should be in quotes, and string values should be in quotes."
646 ));
647}
648
649serde::de::Error::custom(format!("Invalid action_json: {e}. Please ensure your JSON is properly formatted."))
650 })?;
651
652 if let BashCommandAction::Command { command, .. } = &mut action {
653 *command = strip_tail_pipe(command);
654 }
655
656 Ok(BashCommand {
658 action_json: action,
659 wait_for_seconds: helper.wait_for_seconds,
660 thread_id: normalize_thread_id(&helper.thread_id),
661 })
662 }
663}
664
665#[derive(Debug, Clone, JsonSchema, PartialEq)]
667pub struct BashCommandMode {
668 pub bash_mode: BashMode,
669 pub allowed_commands: AllowedCommands,
670}
671
672#[derive(Debug, Clone, Copy, JsonSchema, PartialEq)]
673pub enum BashMode {
674 NormalMode,
675 RestrictedMode,
676}
677
678#[derive(Debug, Clone, JsonSchema, PartialEq)]
680pub struct FileEditMode {
681 pub allowed_globs: AllowedGlobs,
682}
683
684#[derive(Debug, Clone, JsonSchema, PartialEq)]
686pub struct WriteIfEmptyMode {
687 pub allowed_globs: AllowedGlobs,
688}
689
690#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
695pub struct FileWriteOrEdit {
696 pub file_path: String,
700
701 pub percentage_to_change: u32,
706
707 pub text_or_search_replace_blocks: String,
720
721 pub thread_id: String,
723}
724
725#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
730pub struct ContextSave {
731 pub id: String,
736
737 pub project_root_path: String,
742
743 pub description: String,
748
749 pub relevant_file_globs: Vec<String>,
754}
755
756#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
760pub struct ReadImage {
761 pub file_path: String,
765}