Skip to main content

winx_code_agent/
types.rs

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/// Type of shell environment initialization
9///
10/// This enum represents the different ways the Initialize tool can be called,
11/// depending on the current state of the conversation and what the user is requesting.
12#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, PartialEq)]
13#[serde(rename_all = "snake_case")]
14pub enum InitializeType {
15    /// Initial call at the start of a conversation
16    ///
17    /// This should be used for the first Initialize call in a conversation.
18    /// It sets up a new shell environment with the specified parameters.
19    FirstCall,
20
21    /// User requested to change the mode
22    ///
23    /// This should be used when the user asks to switch between modes
24    /// (e.g., from "wcgw" to "architect" or "`code_writer`").
25    UserAskedModeChange,
26
27    /// Reset the shell environment due to issues
28    ///
29    /// This should be used when the shell environment appears to be in a bad state
30    /// and needs to be reset to continue properly.
31    ResetShell,
32
33    /// User requested to change the workspace
34    ///
35    /// This should be used when the user asks to switch to a different
36    /// workspace or project directory during the conversation.
37    UserAskedChangeWorkspace,
38}
39
40#[derive(Debug, Clone, PartialEq)]
41pub enum ModeName {
42    Wcgw,
43    Architect,
44    CodeWriter,
45}
46
47// Custom serializer implementation to ensure values are properly quoted in JSON
48impl 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
61// Custom deserializer to support multiple aliases
62impl<'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
77// Implement schema generation for JSON schema since we removed the derive
78impl 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        // Only process if we have a list of globs
99        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/// Parameters for initializing the shell environment
171///
172/// This struct represents the parameters needed to initialize or update the shell environment.
173/// It is used by the Initialize tool, which must be called before any other shell tools.
174#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
175pub struct Initialize {
176    /// Initialization type, indicating the purpose of the call
177    ///
178    /// - `FirstCall`: Initial setup for a new conversation
179    /// - `UserAskedModeChange`: User requested to change the mode during a conversation
180    /// - `ResetShell`: Reset the shell if it's not working properly
181    /// - `UserAskedChangeWorkspace`: User requested to change the workspace during a conversation
182    #[serde(rename = "type")]
183    #[serde(default = "default_init_type")]
184    pub init_type: InitializeType,
185
186    /// Path to the workspace directory or file
187    ///
188    /// This can be an absolute path or a path relative to the current directory.
189    /// If it's a file, the parent directory will be used as the workspace.
190    /// If it doesn't exist and is an absolute path, it will be created.
191    /// If it's a relative path and doesn't exist, an error will be returned.
192    pub any_workspace_path: String,
193
194    /// List of files to read initially
195    ///
196    /// These files can be absolute paths or paths relative to the workspace.
197    /// They will be read and their contents provided in the response.
198    #[serde(default)]
199    pub initial_files_to_read: Vec<String>,
200
201    /// ID of a task to resume
202    ///
203    /// If provided during a `first_call`, the task with this ID will be resumed.
204    /// This allows continuing a conversation from a previous session.
205    #[serde(default = "String::new")]
206    #[serde(deserialize_with = "deserialize_string_or_null")]
207    pub task_id_to_resume: String,
208
209    /// Mode name for the shell environment
210    ///
211    /// - `wcgw`: Full permissions (default)
212    /// - `architect`: Restricted permissions, read-only
213    /// - `code_writer`: Custom permissions for code writing
214    #[serde(default = "default_mode_name")]
215    pub mode_name: ModeName,
216
217    /// ID of the thread session
218    ///
219    /// If not provided for a `first_call`, a new ID will be generated.
220    /// This ID must be included in all subsequent tool calls.
221    #[serde(default)]
222    #[serde(deserialize_with = "deserialize_string_or_null")]
223    pub thread_id: String,
224
225    /// Configuration for `code_writer` mode
226    ///
227    /// Only used when `mode_name` is "`code_writer`".
228    /// Specifies allowed commands and file globs for writing/editing.
229    #[serde(default)]
230    #[serde(deserialize_with = "deserialize_code_writer_config")]
231    pub code_writer_config: Option<CodeWriterConfig>,
232}
233
234// Custom deserializer for strings that might be null
235fn deserialize_string_or_null<'de, D>(deserializer: D) -> Result<String, D::Error>
236where
237    D: serde::Deserializer<'de>,
238{
239    // First try to deserialize as a string
240    let result = serde_json::Value::deserialize(deserializer)?;
241
242    match result {
243        // Return empty string for null values
244        serde_json::Value::Null => Ok(String::new()),
245        // If it's a string, use that
246        serde_json::Value::String(s) => {
247            // Handle "null" string specially
248            if s == "null" {
249                Ok(String::new())
250            } else {
251                Ok(s)
252            }
253        }
254        // Otherwise try to convert to a string
255        _ => match serde_json::to_string(&result) {
256            Ok(s) => Ok(s),
257            Err(_) => Ok(String::new()),
258        },
259    }
260}
261
262// Custom deserializer for code_writer_config that handles the "null" string case
263fn deserialize_code_writer_config<'de, D>(
264    deserializer: D,
265) -> Result<Option<CodeWriterConfig>, D::Error>
266where
267    D: serde::Deserializer<'de>,
268{
269    // This handles multiple possible input types
270    let value = serde_json::Value::deserialize(deserializer)?;
271
272    match value {
273        // If it's explicitly null or the string "null", return None
274        serde_json::Value::Null => Ok(None),
275        serde_json::Value::String(s) if s == "null" => Ok(None),
276        // Otherwise try to parse it as CodeWriterConfig
277        _ => {
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                    // Log the error and the value for debugging
285                    tracing::error!("Failed to parse CodeWriterConfig: {}. Value: {}", e, value);
286                    Ok(None) // Fall back to None on parse error
287                }
288            }
289        }
290    }
291}
292
293/// Default `mode_name` for Initialize
294fn default_mode_name() -> ModeName {
295    ModeName::Wcgw
296}
297
298/// Default `init_type` for Initialize
299fn default_init_type() -> InitializeType {
300    InitializeType::FirstCall
301}
302
303// Mode types
304#[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
321// Implement schema generation for Modes
322impl 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/// Special key types for shell interaction
333/// Matches wcgw Python's Specials enum exactly
334#[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/// Parameters for the `ReadFiles` tool
354///
355/// This struct represents the parameters needed to read one or more files.
356/// Line ranges can be specified in the path itself (e.g., "file.rs:10-20").
357#[derive(Debug, Clone, Serialize, JsonSchema)]
358pub struct ReadFiles {
359    /// List of file paths to read.
360    /// Supports line range syntax: "file.rs:10-20" for lines 10-20,
361    /// "file.rs:10-" for line 10 onwards, "file.rs:-20" for first 20 lines.
362    pub file_paths: Vec<String>,
363
364    // Internal fields - not part of MCP schema (parsed from file_paths)
365    #[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
374// Custom deserializer for ReadFiles - parses line ranges from file paths like wcgw Python
375impl<'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        // Parse line ranges from file paths (like wcgw Python's model_post_init)
404        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    /// Line numbers are always shown (like wcgw Python)
456    pub fn show_line_numbers(&self) -> bool {
457        true
458    }
459
460    /// Get the clean file path without line range suffix
461    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
501/// Default true value for `status_check`
502fn default_true() -> bool {
503    true
504}
505
506/// Types of actions that can be performed with the `BashCommand` tool
507#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
508#[serde(tag = "type", rename_all = "snake_case")]
509pub enum BashCommandAction {
510    /// Execute a shell command
511    Command {
512        command: String,
513        #[serde(default)]
514        is_background: bool,
515    },
516
517    /// Check the status of a running command
518    StatusCheck {
519        #[serde(default = "default_true")]
520        status_check: bool,
521        bg_command_id: Option<String>,
522    },
523
524    /// Send text to a running command
525    SendText { send_text: String, bg_command_id: Option<String> },
526
527    /// Send special keys to a running command
528    SendSpecials { send_specials: Vec<SpecialKey>, bg_command_id: Option<String> },
529
530    /// Send ASCII characters to a running command
531    SendAscii { send_ascii: Vec<u8>, bg_command_id: Option<String> },
532}
533
534/// Parameters for the `BashCommand` tool
535#[derive(Debug, Clone, Serialize, JsonSchema)]
536pub struct BashCommand {
537    /// The action to perform (command, status check, etc.)
538    pub action_json: BashCommandAction,
539
540    /// Optional timeout in seconds to wait for command completion
541    #[serde(default)]
542    #[serde(skip_serializing_if = "Option::is_none")]
543    pub wait_for_seconds: Option<f32>,
544
545    /// The thread ID for this session
546    #[serde(default)]
547    pub thread_id: String,
548}
549
550// Custom deserialization for BashCommand to handle string-encoded action_json
551impl<'de> Deserialize<'de> for BashCommand {
552    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
553    where
554        D: serde::Deserializer<'de>,
555    {
556        // Define an intermediate struct with the same fields but different types
557        #[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        // Deserialize to the helper struct first
568        let helper = BashCommandHelper::deserialize(deserializer)?;
569
570        // Process action_json which could be a string or an object
571        let action_json = match helper.action_json {
572            serde_json::Value::String(s) => {
573                // If it's a string, normalize newlines and try to parse it as JSON
574                // Replace literal newlines with space to avoid JSON parsing errors
575                let sanitized = s.replace('\n', " ");
576                match serde_json::from_str(&sanitized) {
577                    Ok(json) => json,
578                    Err(e) => {
579                        // If strict JSON parsing fails, try to be more lenient
580                        // For commands containing literal newlines, just wrap the string in a command object
581                        tracing::warn!(
582                            "Failed to parse action_json as JSON, trying fallback: {}",
583                            e
584                        );
585
586                        // Check for common JSON syntax issues
587                        if s.contains("command") && s.contains('{') && s.contains('}') {
588                            // It looks like JSON but has issues, let's try to sanitize it
589
590                            // Detailed error for troubleshooting
591                            tracing::debug!("JSON parse error on: {}", s);
592
593                            // Common issues: unescaped quotes, newlines, tabs
594                            let re_sanitized = s
595                                .replace('\n', "\\n") // Replace newlines with escaped newlines
596                                .replace('\r', "\\r") // Replace carriage returns with escaped versions
597                                .replace('\t', "\\t"); // Replace tabs with escaped versions
598
599                            // Attempt to fix unquoted field values (e.g., convert {field: value} to {"field": "value"})
600                            let re_sanitized = if !s.contains('"') && s.contains(':') {
601                                // Very likely unquoted keys/values
602                                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                                    // Log the specific parsing error for debugging
612                                    tracing::error!("Secondary JSON parse error: {}", err);
613                                    // Last resort fallback - assume it's a command string
614                                    // MUST include "type": "command" for serde tagged enum
615                                    serde_json::json!({"type": "command", "command": s})
616                                }
617                            }
618                        } else {
619                            // Assume it's a simple command string
620                            // MUST include "type": "command" for serde tagged enum
621                            tracing::info!("Treating as simple command: {}", s);
622                            serde_json::json!({"type": "command", "command": s})
623                        }
624                    }
625                }
626            }
627            // If it's already an object or other JSON value, use it directly
628            value => value,
629        };
630
631        // Now deserialize the action_json to our BashCommandAction enum
632        let mut action: BashCommandAction =
633            serde_json::from_value(action_json.clone()).map_err(|e| {
634// Log both the error and the problematic JSON for debugging
635tracing::error!(
636    "Failed to deserialize action_json to BashCommandAction: {}\nProblematic JSON: {}",
637    e,
638    action_json
639);
640
641// For the SyntaxError: Unexpected token case
642let 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        // Return the properly constructed BashCommand
657        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// Bash command mode
666#[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// File edit mode
679#[derive(Debug, Clone, JsonSchema, PartialEq)]
680pub struct FileEditMode {
681    pub allowed_globs: AllowedGlobs,
682}
683
684// Write if empty mode
685#[derive(Debug, Clone, JsonSchema, PartialEq)]
686pub struct WriteIfEmptyMode {
687    pub allowed_globs: AllowedGlobs,
688}
689
690/// Parameters for the `FileWriteOrEdit` tool
691///
692/// This struct represents the parameters needed to write or edit a file
693/// with optional search/replace blocks for partial edits.
694#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
695pub struct FileWriteOrEdit {
696    /// Path to the file to write or edit
697    ///
698    /// This must be an absolute path (~ allowed).
699    pub file_path: String,
700
701    /// Percentage of the file that will be changed
702    ///
703    /// If > 50%, the content is treated as the entire file content.
704    /// If <= 50%, the content is treated as search/replace blocks.
705    pub percentage_to_change: u32,
706
707    /// Content for the file or search/replace blocks
708    ///
709    /// If `percentage_to_change` > 50%, this is the entire file content.
710    /// If `percentage_to_change` <= 50%, this contains search/replace blocks
711    /// in the format:
712    /// ```text
713    /// <<<<<<< SEARCH
714    /// old content to find
715    /// =======
716    /// new content to replace with
717    /// >>>>>>> REPLACE
718    /// ```
719    pub text_or_search_replace_blocks: String,
720
721    /// The thread ID for this session
722    pub thread_id: String,
723}
724
725/// Parameters for the `ContextSave` tool
726///
727/// This struct represents the parameters needed to save context information
728/// about a task, including file contents from specified globs.
729#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
730pub struct ContextSave {
731    /// Unique identifier for the task
732    ///
733    /// This should be a unique string that identifies the task. It can be
734    /// a random 3-word identifier or a user-provided value.
735    pub id: String,
736
737    /// Root path of the project
738    ///
739    /// This should be an absolute path to the project root. If empty, no
740    /// project root will be used.
741    pub project_root_path: String,
742
743    /// Description of the task
744    ///
745    /// This should contain a detailed description of the task, including
746    /// relevant context, problems, and objectives.
747    pub description: String,
748
749    /// List of file glob patterns
750    ///
751    /// These glob patterns identify the files that should be included in
752    /// the saved context. Patterns can be absolute or relative to the project root.
753    pub relevant_file_globs: Vec<String>,
754}
755
756/// Parameters for the `ReadImage` tool
757///
758/// This struct represents the parameters needed to read an image file.
759#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
760pub struct ReadImage {
761    /// Path to the image file to read
762    ///
763    /// This can be an absolute path or a path relative to the current working directory.
764    pub file_path: String,
765}