Skip to main content

claude_codes/
tool_inputs.rs

1//! Typed tool input definitions for Claude Code tools.
2//!
3//! This module provides strongly-typed structs for the input parameters of each
4//! Claude Code tool. Using these types instead of raw `serde_json::Value` provides:
5//!
6//! - Compile-time type checking
7//! - IDE autocompletion and documentation
8//! - Self-documenting API
9//!
10//! # Example
11//!
12//! ```
13//! use claude_codes::{ToolInput, BashInput};
14//!
15//! // Parse a tool input from JSON
16//! let json = serde_json::json!({
17//!     "command": "ls -la",
18//!     "description": "List files in current directory"
19//! });
20//!
21//! let input: ToolInput = serde_json::from_value(json).unwrap();
22//! if let ToolInput::Bash(bash) = input {
23//!     assert_eq!(bash.command, "ls -la");
24//! }
25//! ```
26
27use serde::{Deserialize, Deserializer, Serialize, Serializer};
28use serde_json::Value;
29use std::collections::HashMap;
30use std::fmt;
31
32// ============================================================================
33// Individual Tool Input Structs
34// ============================================================================
35
36/// Input for the Bash tool - executes shell commands.
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
38pub struct BashInput {
39    /// The bash command to execute (required)
40    pub command: String,
41
42    /// Human-readable description of what the command does
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub description: Option<String>,
45
46    /// Timeout in milliseconds (max 600000)
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub timeout: Option<u64>,
49
50    /// Whether to run the command in the background
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub run_in_background: Option<bool>,
53}
54
55/// Input for the Read tool - reads file contents.
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
57pub struct ReadInput {
58    /// The absolute path to the file to read
59    pub file_path: String,
60
61    /// The line number to start reading from (1-indexed)
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub offset: Option<i64>,
64
65    /// The number of lines to read
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub limit: Option<i64>,
68}
69
70/// Input for the Write tool - writes content to a file.
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
72pub struct WriteInput {
73    /// The absolute path to the file to write
74    pub file_path: String,
75
76    /// The content to write to the file
77    pub content: String,
78}
79
80/// Input for the Edit tool - performs string replacements in files.
81#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
82pub struct EditInput {
83    /// The absolute path to the file to modify
84    pub file_path: String,
85
86    /// The text to replace
87    pub old_string: String,
88
89    /// The text to replace it with
90    pub new_string: String,
91
92    /// Replace all occurrences of old_string (default false)
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub replace_all: Option<bool>,
95}
96
97/// Input for the Glob tool - finds files matching a pattern.
98///
99/// The `deny_unknown_fields` attribute ensures Glob only matches exact
100/// Glob inputs and doesn't accidentally match Grep inputs (which share
101/// the `pattern` field but have additional Grep-specific fields).
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
103#[serde(deny_unknown_fields)]
104pub struct GlobInput {
105    /// The glob pattern to match files against (e.g., "**/*.rs")
106    pub pattern: String,
107
108    /// The directory to search in (defaults to current working directory)
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub path: Option<String>,
111}
112
113/// Input for the Grep tool - searches file contents.
114#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
115pub struct GrepInput {
116    /// The regular expression pattern to search for
117    pub pattern: String,
118
119    /// File or directory to search in
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub path: Option<String>,
122
123    /// Glob pattern to filter files (e.g., "*.js")
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub glob: Option<String>,
126
127    /// File type to search (e.g., "js", "py", "rust")
128    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
129    pub file_type: Option<String>,
130
131    /// Case insensitive search
132    #[serde(rename = "-i", skip_serializing_if = "Option::is_none")]
133    pub case_insensitive: Option<bool>,
134
135    /// Show line numbers in output
136    #[serde(rename = "-n", skip_serializing_if = "Option::is_none")]
137    pub line_numbers: Option<bool>,
138
139    /// Number of lines to show after each match
140    #[serde(rename = "-A", skip_serializing_if = "Option::is_none")]
141    pub after_context: Option<u32>,
142
143    /// Number of lines to show before each match
144    #[serde(rename = "-B", skip_serializing_if = "Option::is_none")]
145    pub before_context: Option<u32>,
146
147    /// Number of lines to show before and after each match
148    #[serde(rename = "-C", skip_serializing_if = "Option::is_none")]
149    pub context: Option<u32>,
150
151    /// Output mode: content, files_with_matches, or count
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub output_mode: Option<GrepOutputMode>,
154
155    /// Enable multiline mode
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub multiline: Option<bool>,
158
159    /// Limit output to first N lines/entries
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub head_limit: Option<u32>,
162
163    /// Skip first N lines/entries
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub offset: Option<u32>,
166}
167
168/// Input for the Task tool - launches subagents.
169#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
170pub struct TaskInput {
171    /// A short (3-5 word) description of the task
172    pub description: String,
173
174    /// The task for the agent to perform
175    pub prompt: String,
176
177    /// The type of specialized agent to use
178    pub subagent_type: SubagentType,
179
180    /// Whether to run the agent in the background
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub run_in_background: Option<bool>,
183
184    /// Optional model to use for this agent
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub model: Option<String>,
187
188    /// Maximum number of agentic turns
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub max_turns: Option<u32>,
191
192    /// Optional agent ID to resume from
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub resume: Option<String>,
195}
196
197/// Input for the WebFetch tool - fetches and processes web content.
198#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
199pub struct WebFetchInput {
200    /// The URL to fetch content from
201    pub url: String,
202
203    /// The prompt to run on the fetched content
204    pub prompt: String,
205}
206
207/// Input for the WebSearch tool - searches the web.
208#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
209pub struct WebSearchInput {
210    /// The search query to use
211    pub query: String,
212
213    /// Only include search results from these domains
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub allowed_domains: Option<Vec<String>>,
216
217    /// Never include search results from these domains
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub blocked_domains: Option<Vec<String>>,
220}
221
222/// Input for the TodoWrite tool - manages task lists.
223#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
224pub struct TodoWriteInput {
225    /// The updated todo list
226    pub todos: Vec<TodoItem>,
227}
228
229/// Status of a todo item.
230#[derive(Debug, Clone, PartialEq, Eq, Hash)]
231pub enum TodoStatus {
232    Pending,
233    InProgress,
234    Completed,
235    /// A status not yet known to this version of the crate.
236    Unknown(String),
237}
238
239impl TodoStatus {
240    pub fn as_str(&self) -> &str {
241        match self {
242            Self::Pending => "pending",
243            Self::InProgress => "in_progress",
244            Self::Completed => "completed",
245            Self::Unknown(s) => s.as_str(),
246        }
247    }
248}
249
250impl fmt::Display for TodoStatus {
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        f.write_str(self.as_str())
253    }
254}
255
256impl From<&str> for TodoStatus {
257    fn from(s: &str) -> Self {
258        match s {
259            "pending" => Self::Pending,
260            "in_progress" => Self::InProgress,
261            "completed" => Self::Completed,
262            other => Self::Unknown(other.to_string()),
263        }
264    }
265}
266
267impl Serialize for TodoStatus {
268    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
269        serializer.serialize_str(self.as_str())
270    }
271}
272
273impl<'de> Deserialize<'de> for TodoStatus {
274    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
275        let s = String::deserialize(deserializer)?;
276        Ok(Self::from(s.as_str()))
277    }
278}
279
280/// Output mode for the Grep tool.
281#[derive(Debug, Clone, PartialEq, Eq, Hash)]
282pub enum GrepOutputMode {
283    /// Show matching lines with context.
284    Content,
285    /// Show only file paths containing matches.
286    FilesWithMatches,
287    /// Show match counts per file.
288    Count,
289    /// A mode not yet known to this version of the crate.
290    Unknown(String),
291}
292
293impl GrepOutputMode {
294    pub fn as_str(&self) -> &str {
295        match self {
296            Self::Content => "content",
297            Self::FilesWithMatches => "files_with_matches",
298            Self::Count => "count",
299            Self::Unknown(s) => s.as_str(),
300        }
301    }
302}
303
304impl fmt::Display for GrepOutputMode {
305    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306        f.write_str(self.as_str())
307    }
308}
309
310impl From<&str> for GrepOutputMode {
311    fn from(s: &str) -> Self {
312        match s {
313            "content" => Self::Content,
314            "files_with_matches" => Self::FilesWithMatches,
315            "count" => Self::Count,
316            other => Self::Unknown(other.to_string()),
317        }
318    }
319}
320
321impl Serialize for GrepOutputMode {
322    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
323        serializer.serialize_str(self.as_str())
324    }
325}
326
327impl<'de> Deserialize<'de> for GrepOutputMode {
328    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
329        let s = String::deserialize(deserializer)?;
330        Ok(Self::from(s.as_str()))
331    }
332}
333
334/// Type of specialized subagent for the Task tool.
335#[derive(Debug, Clone, PartialEq, Eq, Hash)]
336pub enum SubagentType {
337    /// Command execution specialist.
338    Bash,
339    /// Fast codebase exploration agent.
340    Explore,
341    /// Software architect agent for planning.
342    Plan,
343    /// General-purpose agent.
344    GeneralPurpose,
345    /// A subagent type not yet known to this version of the crate.
346    Unknown(String),
347}
348
349impl SubagentType {
350    pub fn as_str(&self) -> &str {
351        match self {
352            Self::Bash => "Bash",
353            Self::Explore => "Explore",
354            Self::Plan => "Plan",
355            Self::GeneralPurpose => "general-purpose",
356            Self::Unknown(s) => s.as_str(),
357        }
358    }
359}
360
361impl fmt::Display for SubagentType {
362    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
363        f.write_str(self.as_str())
364    }
365}
366
367impl From<&str> for SubagentType {
368    fn from(s: &str) -> Self {
369        match s {
370            "Bash" => Self::Bash,
371            "Explore" => Self::Explore,
372            "Plan" => Self::Plan,
373            "general-purpose" => Self::GeneralPurpose,
374            other => Self::Unknown(other.to_string()),
375        }
376    }
377}
378
379impl Serialize for SubagentType {
380    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
381        serializer.serialize_str(self.as_str())
382    }
383}
384
385impl<'de> Deserialize<'de> for SubagentType {
386    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
387        let s = String::deserialize(deserializer)?;
388        Ok(Self::from(s.as_str()))
389    }
390}
391
392/// Type of Jupyter notebook cell.
393#[derive(Debug, Clone, PartialEq, Eq, Hash)]
394pub enum NotebookCellType {
395    /// Code cell.
396    Code,
397    /// Markdown cell.
398    Markdown,
399    /// A cell type not yet known to this version of the crate.
400    Unknown(String),
401}
402
403impl NotebookCellType {
404    pub fn as_str(&self) -> &str {
405        match self {
406            Self::Code => "code",
407            Self::Markdown => "markdown",
408            Self::Unknown(s) => s.as_str(),
409        }
410    }
411}
412
413impl fmt::Display for NotebookCellType {
414    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
415        f.write_str(self.as_str())
416    }
417}
418
419impl From<&str> for NotebookCellType {
420    fn from(s: &str) -> Self {
421        match s {
422            "code" => Self::Code,
423            "markdown" => Self::Markdown,
424            other => Self::Unknown(other.to_string()),
425        }
426    }
427}
428
429impl Serialize for NotebookCellType {
430    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
431        serializer.serialize_str(self.as_str())
432    }
433}
434
435impl<'de> Deserialize<'de> for NotebookCellType {
436    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
437        let s = String::deserialize(deserializer)?;
438        Ok(Self::from(s.as_str()))
439    }
440}
441
442/// Type of edit to perform on a notebook cell.
443#[derive(Debug, Clone, PartialEq, Eq, Hash)]
444pub enum NotebookEditMode {
445    /// Replace the cell's content.
446    Replace,
447    /// Insert a new cell.
448    Insert,
449    /// Delete the cell.
450    Delete,
451    /// An edit mode not yet known to this version of the crate.
452    Unknown(String),
453}
454
455impl NotebookEditMode {
456    pub fn as_str(&self) -> &str {
457        match self {
458            Self::Replace => "replace",
459            Self::Insert => "insert",
460            Self::Delete => "delete",
461            Self::Unknown(s) => s.as_str(),
462        }
463    }
464}
465
466impl fmt::Display for NotebookEditMode {
467    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
468        f.write_str(self.as_str())
469    }
470}
471
472impl From<&str> for NotebookEditMode {
473    fn from(s: &str) -> Self {
474        match s {
475            "replace" => Self::Replace,
476            "insert" => Self::Insert,
477            "delete" => Self::Delete,
478            other => Self::Unknown(other.to_string()),
479        }
480    }
481}
482
483impl Serialize for NotebookEditMode {
484    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
485        serializer.serialize_str(self.as_str())
486    }
487}
488
489impl<'de> Deserialize<'de> for NotebookEditMode {
490    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
491        let s = String::deserialize(deserializer)?;
492        Ok(Self::from(s.as_str()))
493    }
494}
495
496/// A single todo item in a task list.
497#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
498pub struct TodoItem {
499    /// The task description (imperative form)
500    pub content: String,
501
502    /// Current status
503    pub status: TodoStatus,
504
505    /// The present continuous form shown during execution
506    #[serde(rename = "activeForm")]
507    pub active_form: String,
508}
509
510/// Input for the AskUserQuestion tool - asks the user questions.
511#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
512pub struct AskUserQuestionInput {
513    /// Questions to ask the user (1-4 questions)
514    pub questions: Vec<Question>,
515
516    /// User answers collected by the permission component
517    #[serde(skip_serializing_if = "Option::is_none")]
518    pub answers: Option<HashMap<String, String>>,
519
520    /// Optional metadata for tracking and analytics
521    #[serde(skip_serializing_if = "Option::is_none")]
522    pub metadata: Option<QuestionMetadata>,
523}
524
525/// A question to ask the user.
526#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
527pub struct Question {
528    /// The complete question to ask the user
529    pub question: String,
530
531    /// Very short label displayed as a chip/tag (max 12 chars)
532    pub header: String,
533
534    /// The available choices for this question (2-4 options)
535    pub options: Vec<QuestionOption>,
536
537    /// Whether multiple options can be selected
538    #[serde(rename = "multiSelect", default)]
539    pub multi_select: bool,
540}
541
542/// An option for a question.
543#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
544pub struct QuestionOption {
545    /// The display text for this option
546    pub label: String,
547
548    /// Explanation of what this option means
549    #[serde(skip_serializing_if = "Option::is_none")]
550    pub description: Option<String>,
551}
552
553/// Metadata for questions.
554#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
555pub struct QuestionMetadata {
556    /// Optional identifier for the source of this question
557    #[serde(skip_serializing_if = "Option::is_none")]
558    pub source: Option<String>,
559}
560
561/// Input for the NotebookEdit tool - edits Jupyter notebooks.
562#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
563pub struct NotebookEditInput {
564    /// The absolute path to the Jupyter notebook file
565    pub notebook_path: String,
566
567    /// The new source for the cell
568    pub new_source: String,
569
570    /// The ID of the cell to edit
571    #[serde(skip_serializing_if = "Option::is_none")]
572    pub cell_id: Option<String>,
573
574    /// The type of the cell (code or markdown)
575    #[serde(skip_serializing_if = "Option::is_none")]
576    pub cell_type: Option<NotebookCellType>,
577
578    /// The type of edit to make (replace, insert, delete)
579    #[serde(skip_serializing_if = "Option::is_none")]
580    pub edit_mode: Option<NotebookEditMode>,
581}
582
583/// Input for the TaskOutput tool - retrieves output from background tasks.
584#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
585pub struct TaskOutputInput {
586    /// The task ID to get output from
587    pub task_id: String,
588
589    /// Whether to wait for completion (default true)
590    #[serde(default = "default_true")]
591    pub block: bool,
592
593    /// Max wait time in ms (default 30000, max 600000)
594    #[serde(default = "default_timeout")]
595    pub timeout: u64,
596}
597
598fn default_true() -> bool {
599    true
600}
601
602fn default_timeout() -> u64 {
603    30000
604}
605
606/// Input for the KillShell tool - kills a running background shell.
607#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
608pub struct KillShellInput {
609    /// The ID of the background shell to kill
610    pub shell_id: String,
611}
612
613/// Input for the Skill tool - executes a skill.
614#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
615pub struct SkillInput {
616    /// The skill name (e.g., "commit", "review-pr")
617    pub skill: String,
618
619    /// Optional arguments for the skill
620    #[serde(skip_serializing_if = "Option::is_none")]
621    pub args: Option<String>,
622}
623
624/// Input for the EnterPlanMode tool - enters planning mode.
625///
626/// This is an empty struct as EnterPlanMode takes no parameters.
627/// The `deny_unknown_fields` attribute ensures this only matches
628/// empty JSON objects `{}`, not arbitrary JSON.
629#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
630#[serde(deny_unknown_fields)]
631pub struct EnterPlanModeInput {}
632
633/// Input for the ExitPlanMode tool - exits planning mode.
634///
635/// The `deny_unknown_fields` attribute ensures this only matches JSON objects
636/// that contain known fields (or are empty), not arbitrary JSON.
637#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
638#[serde(deny_unknown_fields)]
639pub struct ExitPlanModeInput {
640    /// Prompt-based permissions needed to implement the plan
641    #[serde(rename = "allowedPrompts", skip_serializing_if = "Option::is_none")]
642    pub allowed_prompts: Option<Vec<AllowedPrompt>>,
643
644    /// Whether to push the plan to a remote Claude.ai session
645    #[serde(rename = "pushToRemote", skip_serializing_if = "Option::is_none")]
646    pub push_to_remote: Option<bool>,
647
648    /// The remote session ID if pushed to remote
649    #[serde(rename = "remoteSessionId", skip_serializing_if = "Option::is_none")]
650    pub remote_session_id: Option<String>,
651
652    /// The remote session URL if pushed to remote
653    #[serde(rename = "remoteSessionUrl", skip_serializing_if = "Option::is_none")]
654    pub remote_session_url: Option<String>,
655
656    /// The remote session title if pushed to remote
657    #[serde(rename = "remoteSessionTitle", skip_serializing_if = "Option::is_none")]
658    pub remote_session_title: Option<String>,
659
660    /// The plan content from plan mode
661    #[serde(skip_serializing_if = "Option::is_none")]
662    pub plan: Option<String>,
663}
664
665/// An allowed prompt permission for plan mode.
666#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
667pub struct AllowedPrompt {
668    /// The tool this prompt applies to
669    pub tool: String,
670
671    /// Semantic description of the action
672    pub prompt: String,
673}
674
675/// Input for the MultiEdit tool - batch file edits.
676#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
677pub struct MultiEditInput {
678    /// The absolute path to the file to modify
679    pub file_path: String,
680
681    /// Array of edit operations to apply
682    pub edits: Vec<MultiEditOperation>,
683}
684
685/// A single edit operation within a MultiEdit.
686#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
687pub struct MultiEditOperation {
688    /// The text to replace
689    pub old_string: String,
690
691    /// The text to replace it with
692    pub new_string: String,
693}
694
695/// Input for the LS tool - lists files and directories.
696#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
697#[serde(deny_unknown_fields)]
698pub struct LsInput {
699    /// The absolute path to the directory to list
700    pub path: String,
701}
702
703/// Input for the NotebookRead tool - reads notebook cells.
704#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
705pub struct NotebookReadInput {
706    /// The absolute path to the notebook file
707    pub notebook_path: String,
708}
709
710/// Input for the ScheduleWakeup tool - schedules delayed loop actions.
711#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
712pub struct ScheduleWakeupInput {
713    /// Seconds from now to wake up (clamped to [60, 3600])
714    #[serde(rename = "delaySeconds")]
715    pub delay_seconds: f64,
716
717    /// Short explanation of the chosen delay
718    pub reason: String,
719
720    /// The /loop prompt to fire on wake-up
721    pub prompt: String,
722}
723
724/// Input for the ToolSearch tool - fetches deferred tool schemas.
725#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
726#[serde(deny_unknown_fields)]
727pub struct ToolSearchInput {
728    /// Query to find deferred tools
729    pub query: String,
730
731    /// Maximum number of results to return
732    #[serde(skip_serializing_if = "Option::is_none")]
733    pub max_results: Option<u32>,
734}
735
736// ============================================================================
737// ToolInput Enum - Unified type for all tool inputs
738// ============================================================================
739
740/// Unified enum representing input for any Claude Code tool.
741///
742/// This enum uses `#[serde(untagged)]` to automatically deserialize based on
743/// the structure of the JSON. The `Unknown` variant serves as a fallback for:
744/// - New tools added in future Claude CLI versions
745/// - Custom MCP tools provided by users
746/// - Any tool input that doesn't match known schemas
747///
748/// # Example
749///
750/// ```
751/// use claude_codes::ToolInput;
752///
753/// // Known tool - deserializes to specific variant
754/// let bash_json = serde_json::json!({"command": "ls"});
755/// let input: ToolInput = serde_json::from_value(bash_json).unwrap();
756/// assert!(matches!(input, ToolInput::Bash(_)));
757///
758/// // Unknown tool - falls back to Unknown variant
759/// let custom_json = serde_json::json!({"custom_field": "value"});
760/// let input: ToolInput = serde_json::from_value(custom_json).unwrap();
761/// assert!(matches!(input, ToolInput::Unknown(_)));
762/// ```
763///
764/// # Note on Ordering
765///
766/// The variants are ordered from most specific (most required fields) to least
767/// specific to ensure correct deserialization with `#[serde(untagged)]`.
768#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
769#[serde(untagged)]
770pub enum ToolInput {
771    /// Edit tool - has unique field combination (file_path, old_string, new_string)
772    Edit(EditInput),
773
774    /// Write tool - file_path + content
775    Write(WriteInput),
776
777    /// MultiEdit tool - batch file edits (file_path + edits, before Read)
778    MultiEdit(MultiEditInput),
779
780    /// AskUserQuestion tool - has questions array
781    AskUserQuestion(AskUserQuestionInput),
782
783    /// TodoWrite tool - has todos array
784    TodoWrite(TodoWriteInput),
785
786    /// Task tool - description + prompt + subagent_type
787    Task(TaskInput),
788
789    /// NotebookEdit tool - notebook_path + new_source
790    NotebookEdit(NotebookEditInput),
791
792    /// WebFetch tool - url + prompt
793    WebFetch(WebFetchInput),
794
795    /// TaskOutput tool - task_id + block + timeout
796    TaskOutput(TaskOutputInput),
797
798    /// Bash tool - has command field
799    Bash(BashInput),
800
801    /// Read tool - has file_path
802    Read(ReadInput),
803
804    /// Glob tool - has pattern field (with deny_unknown_fields, must come before Grep)
805    Glob(GlobInput),
806
807    /// Grep tool - has pattern field plus many optional fields
808    Grep(GrepInput),
809
810    /// ToolSearch tool - fetch deferred tool schemas (query + max_results)
811    ToolSearch(ToolSearchInput),
812
813    /// WebSearch tool - has query field
814    WebSearch(WebSearchInput),
815
816    /// KillShell tool - has shell_id
817    KillShell(KillShellInput),
818
819    /// Skill tool - has skill field
820    Skill(SkillInput),
821
822    /// ExitPlanMode tool
823    ExitPlanMode(ExitPlanModeInput),
824
825    /// ScheduleWakeup tool - schedule delayed wakeup (3 required fields)
826    ScheduleWakeup(ScheduleWakeupInput),
827
828    /// NotebookRead tool - read notebook cells (notebook_path required)
829    NotebookRead(NotebookReadInput),
830
831    /// LS tool - list files and directories
832    LS(LsInput),
833
834    /// EnterPlanMode tool (empty input)
835    EnterPlanMode(EnterPlanModeInput),
836
837    /// Unknown tool input - fallback for custom/new tools
838    ///
839    /// This variant captures any tool input that doesn't match the known schemas.
840    /// Use this for:
841    /// - MCP tools provided by users
842    /// - New tools in future Claude CLI versions
843    /// - Any custom tool integration
844    Unknown(Value),
845}
846
847impl ToolInput {
848    /// Returns the tool name if it can be determined from the input type.
849    ///
850    /// For `Unknown` variants, returns `None` since the tool name cannot be
851    /// determined from the input structure alone.
852    pub fn tool_name(&self) -> Option<&'static str> {
853        match self {
854            ToolInput::Bash(_) => Some("Bash"),
855            ToolInput::Read(_) => Some("Read"),
856            ToolInput::Write(_) => Some("Write"),
857            ToolInput::Edit(_) => Some("Edit"),
858            ToolInput::Glob(_) => Some("Glob"),
859            ToolInput::Grep(_) => Some("Grep"),
860            ToolInput::Task(_) => Some("Task"),
861            ToolInput::WebFetch(_) => Some("WebFetch"),
862            ToolInput::WebSearch(_) => Some("WebSearch"),
863            ToolInput::TodoWrite(_) => Some("TodoWrite"),
864            ToolInput::AskUserQuestion(_) => Some("AskUserQuestion"),
865            ToolInput::NotebookEdit(_) => Some("NotebookEdit"),
866            ToolInput::TaskOutput(_) => Some("TaskOutput"),
867            ToolInput::KillShell(_) => Some("KillShell"),
868            ToolInput::Skill(_) => Some("Skill"),
869            ToolInput::EnterPlanMode(_) => Some("EnterPlanMode"),
870            ToolInput::ExitPlanMode(_) => Some("ExitPlanMode"),
871            ToolInput::MultiEdit(_) => Some("MultiEdit"),
872            ToolInput::ScheduleWakeup(_) => Some("ScheduleWakeup"),
873            ToolInput::NotebookRead(_) => Some("NotebookRead"),
874            ToolInput::ToolSearch(_) => Some("ToolSearch"),
875            ToolInput::LS(_) => Some("LS"),
876            ToolInput::Unknown(_) => None,
877        }
878    }
879
880    /// Parse a tool-use `input` payload using the authoritative tool *name*
881    /// from the surrounding `ToolUse` block, instead of guessing the variant
882    /// from field shape.
883    ///
884    /// The [`Deserialize`] impl for `ToolInput` is `#[serde(untagged)]`, so it
885    /// resolves variants by structural shape in declaration order. Tools whose
886    /// inputs are structurally identical are therefore genuinely ambiguous to
887    /// the untagged impl — most notably [`WebSearch`](ToolInput::WebSearch) and
888    /// [`ToolSearch`](ToolInput::ToolSearch), which both deserialize cleanly
889    /// from a bare `{ "query": String }`, so the first-declared variant wins
890    /// and the other is never produced. The tool name disambiguates them.
891    ///
892    /// Falls back to [`ToolInput::Unknown`] when the named struct doesn't
893    /// deserialize, and defers to the untagged [`Deserialize`] impl for tool
894    /// names this crate doesn't model (e.g. MCP tools).
895    ///
896    /// # Examples
897    ///
898    /// ```
899    /// use claude_codes::ToolInput;
900    ///
901    /// // A bare-query WebSearch input — ambiguous to the untagged impl, which
902    /// // would pick `ToolSearch`. The name resolves it correctly.
903    /// let input = serde_json::json!({ "query": "rust async" });
904    /// let parsed = ToolInput::from_named_input("WebSearch", input);
905    /// assert!(matches!(parsed, ToolInput::WebSearch(_)));
906    /// ```
907    pub fn from_named_input(name: &str, input: Value) -> Self {
908        match name {
909            "Bash" => Self::parse_named(input, ToolInput::Bash),
910            "Read" => Self::parse_named(input, ToolInput::Read),
911            "Write" => Self::parse_named(input, ToolInput::Write),
912            "Edit" => Self::parse_named(input, ToolInput::Edit),
913            "Glob" => Self::parse_named(input, ToolInput::Glob),
914            "Grep" => Self::parse_named(input, ToolInput::Grep),
915            "Task" => Self::parse_named(input, ToolInput::Task),
916            "WebFetch" => Self::parse_named(input, ToolInput::WebFetch),
917            "WebSearch" => Self::parse_named(input, ToolInput::WebSearch),
918            "TodoWrite" => Self::parse_named(input, ToolInput::TodoWrite),
919            "AskUserQuestion" => Self::parse_named(input, ToolInput::AskUserQuestion),
920            "NotebookEdit" => Self::parse_named(input, ToolInput::NotebookEdit),
921            "TaskOutput" => Self::parse_named(input, ToolInput::TaskOutput),
922            "KillShell" => Self::parse_named(input, ToolInput::KillShell),
923            "Skill" => Self::parse_named(input, ToolInput::Skill),
924            "EnterPlanMode" => Self::parse_named(input, ToolInput::EnterPlanMode),
925            "ExitPlanMode" => Self::parse_named(input, ToolInput::ExitPlanMode),
926            "MultiEdit" => Self::parse_named(input, ToolInput::MultiEdit),
927            "ScheduleWakeup" => Self::parse_named(input, ToolInput::ScheduleWakeup),
928            "NotebookRead" => Self::parse_named(input, ToolInput::NotebookRead),
929            "ToolSearch" => Self::parse_named(input, ToolInput::ToolSearch),
930            "LS" => Self::parse_named(input, ToolInput::LS),
931            // Unknown tool name (e.g. an MCP tool): fall back to structural
932            // parsing, preserving the raw payload if even that fails.
933            _ => serde_json::from_value(input.clone()).unwrap_or(ToolInput::Unknown(input)),
934        }
935    }
936
937    /// Deserialize `input` into a specific tool-input struct `T` and wrap it in
938    /// the matching [`ToolInput`] variant, preserving the raw payload as
939    /// [`ToolInput::Unknown`] if it doesn't fit the expected shape.
940    fn parse_named<T>(input: Value, wrap: fn(T) -> ToolInput) -> ToolInput
941    where
942        T: serde::de::DeserializeOwned,
943    {
944        match serde_json::from_value::<T>(input.clone()) {
945            Ok(parsed) => wrap(parsed),
946            Err(_) => ToolInput::Unknown(input),
947        }
948    }
949
950    /// Try to get the input as a Bash input.
951    pub fn as_bash(&self) -> Option<&BashInput> {
952        match self {
953            ToolInput::Bash(input) => Some(input),
954            _ => None,
955        }
956    }
957
958    /// Try to get the input as a Read input.
959    pub fn as_read(&self) -> Option<&ReadInput> {
960        match self {
961            ToolInput::Read(input) => Some(input),
962            _ => None,
963        }
964    }
965
966    /// Try to get the input as a Write input.
967    pub fn as_write(&self) -> Option<&WriteInput> {
968        match self {
969            ToolInput::Write(input) => Some(input),
970            _ => None,
971        }
972    }
973
974    /// Try to get the input as an Edit input.
975    pub fn as_edit(&self) -> Option<&EditInput> {
976        match self {
977            ToolInput::Edit(input) => Some(input),
978            _ => None,
979        }
980    }
981
982    /// Try to get the input as a Glob input.
983    pub fn as_glob(&self) -> Option<&GlobInput> {
984        match self {
985            ToolInput::Glob(input) => Some(input),
986            _ => None,
987        }
988    }
989
990    /// Try to get the input as a Grep input.
991    pub fn as_grep(&self) -> Option<&GrepInput> {
992        match self {
993            ToolInput::Grep(input) => Some(input),
994            _ => None,
995        }
996    }
997
998    /// Try to get the input as a Task input.
999    pub fn as_task(&self) -> Option<&TaskInput> {
1000        match self {
1001            ToolInput::Task(input) => Some(input),
1002            _ => None,
1003        }
1004    }
1005
1006    /// Try to get the input as a WebFetch input.
1007    pub fn as_web_fetch(&self) -> Option<&WebFetchInput> {
1008        match self {
1009            ToolInput::WebFetch(input) => Some(input),
1010            _ => None,
1011        }
1012    }
1013
1014    /// Try to get the input as a WebSearch input.
1015    pub fn as_web_search(&self) -> Option<&WebSearchInput> {
1016        match self {
1017            ToolInput::WebSearch(input) => Some(input),
1018            _ => None,
1019        }
1020    }
1021
1022    /// Try to get the input as a TodoWrite input.
1023    pub fn as_todo_write(&self) -> Option<&TodoWriteInput> {
1024        match self {
1025            ToolInput::TodoWrite(input) => Some(input),
1026            _ => None,
1027        }
1028    }
1029
1030    /// Try to get the input as an AskUserQuestion input.
1031    pub fn as_ask_user_question(&self) -> Option<&AskUserQuestionInput> {
1032        match self {
1033            ToolInput::AskUserQuestion(input) => Some(input),
1034            _ => None,
1035        }
1036    }
1037
1038    /// Try to get the input as a NotebookEdit input.
1039    pub fn as_notebook_edit(&self) -> Option<&NotebookEditInput> {
1040        match self {
1041            ToolInput::NotebookEdit(input) => Some(input),
1042            _ => None,
1043        }
1044    }
1045
1046    /// Try to get the input as a TaskOutput input.
1047    pub fn as_task_output(&self) -> Option<&TaskOutputInput> {
1048        match self {
1049            ToolInput::TaskOutput(input) => Some(input),
1050            _ => None,
1051        }
1052    }
1053
1054    /// Try to get the input as a KillShell input.
1055    pub fn as_kill_shell(&self) -> Option<&KillShellInput> {
1056        match self {
1057            ToolInput::KillShell(input) => Some(input),
1058            _ => None,
1059        }
1060    }
1061
1062    /// Try to get the input as a Skill input.
1063    pub fn as_skill(&self) -> Option<&SkillInput> {
1064        match self {
1065            ToolInput::Skill(input) => Some(input),
1066            _ => None,
1067        }
1068    }
1069
1070    /// Try to get the input as an unknown Value.
1071    pub fn as_unknown(&self) -> Option<&Value> {
1072        match self {
1073            ToolInput::Unknown(value) => Some(value),
1074            _ => None,
1075        }
1076    }
1077
1078    /// Check if this is an unknown tool input.
1079    pub fn is_unknown(&self) -> bool {
1080        matches!(self, ToolInput::Unknown(_))
1081    }
1082}
1083
1084// ============================================================================
1085// Conversion implementations
1086// ============================================================================
1087
1088impl From<BashInput> for ToolInput {
1089    fn from(input: BashInput) -> Self {
1090        ToolInput::Bash(input)
1091    }
1092}
1093
1094impl From<ReadInput> for ToolInput {
1095    fn from(input: ReadInput) -> Self {
1096        ToolInput::Read(input)
1097    }
1098}
1099
1100impl From<WriteInput> for ToolInput {
1101    fn from(input: WriteInput) -> Self {
1102        ToolInput::Write(input)
1103    }
1104}
1105
1106impl From<EditInput> for ToolInput {
1107    fn from(input: EditInput) -> Self {
1108        ToolInput::Edit(input)
1109    }
1110}
1111
1112impl From<GlobInput> for ToolInput {
1113    fn from(input: GlobInput) -> Self {
1114        ToolInput::Glob(input)
1115    }
1116}
1117
1118impl From<GrepInput> for ToolInput {
1119    fn from(input: GrepInput) -> Self {
1120        ToolInput::Grep(input)
1121    }
1122}
1123
1124impl From<TaskInput> for ToolInput {
1125    fn from(input: TaskInput) -> Self {
1126        ToolInput::Task(input)
1127    }
1128}
1129
1130impl From<WebFetchInput> for ToolInput {
1131    fn from(input: WebFetchInput) -> Self {
1132        ToolInput::WebFetch(input)
1133    }
1134}
1135
1136impl From<WebSearchInput> for ToolInput {
1137    fn from(input: WebSearchInput) -> Self {
1138        ToolInput::WebSearch(input)
1139    }
1140}
1141
1142impl From<TodoWriteInput> for ToolInput {
1143    fn from(input: TodoWriteInput) -> Self {
1144        ToolInput::TodoWrite(input)
1145    }
1146}
1147
1148impl From<AskUserQuestionInput> for ToolInput {
1149    fn from(input: AskUserQuestionInput) -> Self {
1150        ToolInput::AskUserQuestion(input)
1151    }
1152}
1153
1154#[cfg(test)]
1155mod tests {
1156    use super::*;
1157
1158    #[test]
1159    fn test_bash_input_parsing() {
1160        let json = serde_json::json!({
1161            "command": "ls -la",
1162            "description": "List files",
1163            "timeout": 5000,
1164            "run_in_background": false
1165        });
1166
1167        let input: BashInput = serde_json::from_value(json).unwrap();
1168        assert_eq!(input.command, "ls -la");
1169        assert_eq!(input.description, Some("List files".to_string()));
1170        assert_eq!(input.timeout, Some(5000));
1171        assert_eq!(input.run_in_background, Some(false));
1172    }
1173
1174    #[test]
1175    fn test_bash_input_minimal() {
1176        let json = serde_json::json!({
1177            "command": "echo hello"
1178        });
1179
1180        let input: BashInput = serde_json::from_value(json).unwrap();
1181        assert_eq!(input.command, "echo hello");
1182        assert_eq!(input.description, None);
1183        assert_eq!(input.timeout, None);
1184    }
1185
1186    #[test]
1187    fn test_read_input_parsing() {
1188        let json = serde_json::json!({
1189            "file_path": "/home/user/test.rs",
1190            "offset": 10,
1191            "limit": 100
1192        });
1193
1194        let input: ReadInput = serde_json::from_value(json).unwrap();
1195        assert_eq!(input.file_path, "/home/user/test.rs");
1196        assert_eq!(input.offset, Some(10));
1197        assert_eq!(input.limit, Some(100));
1198    }
1199
1200    #[test]
1201    fn test_write_input_parsing() {
1202        let json = serde_json::json!({
1203            "file_path": "/tmp/test.txt",
1204            "content": "Hello, world!"
1205        });
1206
1207        let input: WriteInput = serde_json::from_value(json).unwrap();
1208        assert_eq!(input.file_path, "/tmp/test.txt");
1209        assert_eq!(input.content, "Hello, world!");
1210    }
1211
1212    #[test]
1213    fn test_edit_input_parsing() {
1214        let json = serde_json::json!({
1215            "file_path": "/home/user/code.rs",
1216            "old_string": "fn old()",
1217            "new_string": "fn new()",
1218            "replace_all": true
1219        });
1220
1221        let input: EditInput = serde_json::from_value(json).unwrap();
1222        assert_eq!(input.file_path, "/home/user/code.rs");
1223        assert_eq!(input.old_string, "fn old()");
1224        assert_eq!(input.new_string, "fn new()");
1225        assert_eq!(input.replace_all, Some(true));
1226    }
1227
1228    #[test]
1229    fn test_glob_input_parsing() {
1230        let json = serde_json::json!({
1231            "pattern": "**/*.rs",
1232            "path": "/home/user/project"
1233        });
1234
1235        let input: GlobInput = serde_json::from_value(json).unwrap();
1236        assert_eq!(input.pattern, "**/*.rs");
1237        assert_eq!(input.path, Some("/home/user/project".to_string()));
1238    }
1239
1240    #[test]
1241    fn test_grep_input_parsing() {
1242        let json = serde_json::json!({
1243            "pattern": "fn\\s+\\w+",
1244            "path": "/home/user/project",
1245            "type": "rust",
1246            "-i": true,
1247            "-C": 3
1248        });
1249
1250        let input: GrepInput = serde_json::from_value(json).unwrap();
1251        assert_eq!(input.pattern, "fn\\s+\\w+");
1252        assert_eq!(input.file_type, Some("rust".to_string()));
1253        assert_eq!(input.case_insensitive, Some(true));
1254        assert_eq!(input.context, Some(3));
1255    }
1256
1257    #[test]
1258    fn test_task_input_parsing() {
1259        let json = serde_json::json!({
1260            "description": "Search codebase",
1261            "prompt": "Find all usages of foo()",
1262            "subagent_type": "Explore",
1263            "run_in_background": true
1264        });
1265
1266        let input: TaskInput = serde_json::from_value(json).unwrap();
1267        assert_eq!(input.description, "Search codebase");
1268        assert_eq!(input.prompt, "Find all usages of foo()");
1269        assert_eq!(input.subagent_type, SubagentType::Explore);
1270        assert_eq!(input.run_in_background, Some(true));
1271    }
1272
1273    #[test]
1274    fn test_web_fetch_input_parsing() {
1275        let json = serde_json::json!({
1276            "url": "https://example.com",
1277            "prompt": "Extract the main content"
1278        });
1279
1280        let input: WebFetchInput = serde_json::from_value(json).unwrap();
1281        assert_eq!(input.url, "https://example.com");
1282        assert_eq!(input.prompt, "Extract the main content");
1283    }
1284
1285    #[test]
1286    fn test_web_search_input_parsing() {
1287        let json = serde_json::json!({
1288            "query": "rust serde tutorial",
1289            "allowed_domains": ["docs.rs", "crates.io"]
1290        });
1291
1292        let input: WebSearchInput = serde_json::from_value(json).unwrap();
1293        assert_eq!(input.query, "rust serde tutorial");
1294        assert_eq!(
1295            input.allowed_domains,
1296            Some(vec!["docs.rs".to_string(), "crates.io".to_string()])
1297        );
1298    }
1299
1300    #[test]
1301    fn test_todo_write_input_parsing() {
1302        let json = serde_json::json!({
1303            "todos": [
1304                {
1305                    "content": "Fix the bug",
1306                    "status": "in_progress",
1307                    "activeForm": "Fixing the bug"
1308                },
1309                {
1310                    "content": "Write tests",
1311                    "status": "pending",
1312                    "activeForm": "Writing tests"
1313                }
1314            ]
1315        });
1316
1317        let input: TodoWriteInput = serde_json::from_value(json).unwrap();
1318        assert_eq!(input.todos.len(), 2);
1319        assert_eq!(input.todos[0].content, "Fix the bug");
1320        assert_eq!(input.todos[0].status, TodoStatus::InProgress);
1321        assert_eq!(input.todos[1].status, TodoStatus::Pending);
1322    }
1323
1324    #[test]
1325    fn test_ask_user_question_input_parsing() {
1326        let json = serde_json::json!({
1327            "questions": [
1328                {
1329                    "question": "Which framework?",
1330                    "header": "Framework",
1331                    "options": [
1332                        {"label": "React", "description": "Popular UI library"},
1333                        {"label": "Vue", "description": "Progressive framework"}
1334                    ],
1335                    "multiSelect": false
1336                }
1337            ]
1338        });
1339
1340        let input: AskUserQuestionInput = serde_json::from_value(json).unwrap();
1341        assert_eq!(input.questions.len(), 1);
1342        assert_eq!(input.questions[0].question, "Which framework?");
1343        assert_eq!(input.questions[0].options.len(), 2);
1344        assert_eq!(input.questions[0].options[0].label, "React");
1345    }
1346
1347    #[test]
1348    fn test_tool_input_enum_bash() {
1349        let json = serde_json::json!({
1350            "command": "ls -la"
1351        });
1352
1353        let input: ToolInput = serde_json::from_value(json).unwrap();
1354        assert!(matches!(input, ToolInput::Bash(_)));
1355        assert_eq!(input.tool_name(), Some("Bash"));
1356        assert!(input.as_bash().is_some());
1357    }
1358
1359    #[test]
1360    fn test_tool_input_enum_edit() {
1361        let json = serde_json::json!({
1362            "file_path": "/test.rs",
1363            "old_string": "old",
1364            "new_string": "new"
1365        });
1366
1367        let input: ToolInput = serde_json::from_value(json).unwrap();
1368        assert!(matches!(input, ToolInput::Edit(_)));
1369        assert_eq!(input.tool_name(), Some("Edit"));
1370    }
1371
1372    #[test]
1373    fn test_tool_input_enum_unknown() {
1374        // Custom MCP tool with unknown structure
1375        let json = serde_json::json!({
1376            "custom_field": "custom_value",
1377            "another_field": 42
1378        });
1379
1380        let input: ToolInput = serde_json::from_value(json).unwrap();
1381        assert!(matches!(input, ToolInput::Unknown(_)));
1382        assert_eq!(input.tool_name(), None);
1383        assert!(input.is_unknown());
1384
1385        let unknown = input.as_unknown().unwrap();
1386        assert_eq!(unknown.get("custom_field").unwrap(), "custom_value");
1387    }
1388
1389    #[test]
1390    fn test_tool_input_roundtrip() {
1391        let original = BashInput {
1392            command: "echo test".to_string(),
1393            description: Some("Test command".to_string()),
1394            timeout: Some(5000),
1395            run_in_background: None,
1396        };
1397
1398        let tool_input: ToolInput = original.clone().into();
1399        let json = serde_json::to_value(&tool_input).unwrap();
1400        let parsed: ToolInput = serde_json::from_value(json).unwrap();
1401
1402        if let ToolInput::Bash(bash) = parsed {
1403            assert_eq!(bash.command, original.command);
1404            assert_eq!(bash.description, original.description);
1405        } else {
1406            panic!("Expected Bash variant");
1407        }
1408    }
1409
1410    #[test]
1411    fn test_notebook_edit_input_parsing() {
1412        let json = serde_json::json!({
1413            "notebook_path": "/home/user/notebook.ipynb",
1414            "new_source": "print('hello')",
1415            "cell_id": "abc123",
1416            "cell_type": "code",
1417            "edit_mode": "replace"
1418        });
1419
1420        let input: NotebookEditInput = serde_json::from_value(json).unwrap();
1421        assert_eq!(input.notebook_path, "/home/user/notebook.ipynb");
1422        assert_eq!(input.new_source, "print('hello')");
1423        assert_eq!(input.cell_id, Some("abc123".to_string()));
1424    }
1425
1426    #[test]
1427    fn test_task_output_input_parsing() {
1428        let json = serde_json::json!({
1429            "task_id": "task-123",
1430            "block": false,
1431            "timeout": 60000
1432        });
1433
1434        let input: TaskOutputInput = serde_json::from_value(json).unwrap();
1435        assert_eq!(input.task_id, "task-123");
1436        assert!(!input.block);
1437        assert_eq!(input.timeout, 60000);
1438    }
1439
1440    #[test]
1441    fn test_skill_input_parsing() {
1442        let json = serde_json::json!({
1443            "skill": "commit",
1444            "args": "-m 'Fix bug'"
1445        });
1446
1447        let input: SkillInput = serde_json::from_value(json).unwrap();
1448        assert_eq!(input.skill, "commit");
1449        assert_eq!(input.args, Some("-m 'Fix bug'".to_string()));
1450    }
1451
1452    #[test]
1453    fn test_multi_edit_input_parsing() {
1454        let json = serde_json::json!({
1455            "file_path": "/tmp/test.rs",
1456            "edits": [
1457                {"old_string": "foo", "new_string": "bar"},
1458                {"old_string": "baz", "new_string": "qux"}
1459            ]
1460        });
1461
1462        let input: MultiEditInput = serde_json::from_value(json.clone()).unwrap();
1463        assert_eq!(input.file_path, "/tmp/test.rs");
1464        assert_eq!(input.edits.len(), 2);
1465        assert_eq!(input.edits[0].old_string, "foo");
1466        assert_eq!(input.edits[1].new_string, "qux");
1467
1468        // Also test via ToolInput enum
1469        let tool_input: ToolInput = serde_json::from_value(json).unwrap();
1470        assert_eq!(tool_input.tool_name(), Some("MultiEdit"));
1471    }
1472
1473    #[test]
1474    fn test_ls_input_parsing() {
1475        let json = serde_json::json!({"path": "/home/user/project"});
1476
1477        let input: LsInput = serde_json::from_value(json.clone()).unwrap();
1478        assert_eq!(input.path, "/home/user/project");
1479
1480        let tool_input: ToolInput = serde_json::from_value(json).unwrap();
1481        assert_eq!(tool_input.tool_name(), Some("LS"));
1482    }
1483
1484    #[test]
1485    fn test_notebook_read_input_parsing() {
1486        let json = serde_json::json!({"notebook_path": "/tmp/analysis.ipynb"});
1487
1488        let input: NotebookReadInput = serde_json::from_value(json.clone()).unwrap();
1489        assert_eq!(input.notebook_path, "/tmp/analysis.ipynb");
1490
1491        let tool_input: ToolInput = serde_json::from_value(json).unwrap();
1492        assert_eq!(tool_input.tool_name(), Some("NotebookRead"));
1493    }
1494
1495    #[test]
1496    fn test_schedule_wakeup_input_parsing() {
1497        let json = serde_json::json!({
1498            "delaySeconds": 270.0,
1499            "reason": "checking build status",
1500            "prompt": "check the build"
1501        });
1502
1503        let input: ScheduleWakeupInput = serde_json::from_value(json.clone()).unwrap();
1504        assert_eq!(input.delay_seconds, 270.0);
1505        assert_eq!(input.reason, "checking build status");
1506        assert_eq!(input.prompt, "check the build");
1507
1508        let tool_input: ToolInput = serde_json::from_value(json).unwrap();
1509        assert_eq!(tool_input.tool_name(), Some("ScheduleWakeup"));
1510    }
1511
1512    #[test]
1513    fn test_tool_search_input_parsing() {
1514        let json = serde_json::json!({
1515            "query": "select:Read,Edit,Grep",
1516            "max_results": 5
1517        });
1518
1519        let input: ToolSearchInput = serde_json::from_value(json.clone()).unwrap();
1520        assert_eq!(input.query, "select:Read,Edit,Grep");
1521        assert_eq!(input.max_results, Some(5));
1522
1523        let tool_input: ToolInput = serde_json::from_value(json).unwrap();
1524        assert_eq!(tool_input.tool_name(), Some("ToolSearch"));
1525    }
1526
1527    #[test]
1528    fn test_from_named_input_disambiguates_websearch_from_toolsearch() {
1529        let bare_query = serde_json::json!({ "query": "rust async" });
1530
1531        // The untagged impl misclassifies a bare-query WebSearch as ToolSearch,
1532        // since ToolSearch is declared first and the shapes are identical.
1533        let guessed: ToolInput = serde_json::from_value(bare_query.clone()).unwrap();
1534        assert!(matches!(guessed, ToolInput::ToolSearch(_)));
1535
1536        // The name-aware path resolves it correctly.
1537        let named = ToolInput::from_named_input("WebSearch", bare_query.clone());
1538        assert!(matches!(named, ToolInput::WebSearch(_)));
1539        assert_eq!(named.tool_name(), Some("WebSearch"));
1540
1541        // And the reverse: a bare-query ToolSearch stays ToolSearch.
1542        let named = ToolInput::from_named_input("ToolSearch", bare_query);
1543        assert!(matches!(named, ToolInput::ToolSearch(_)));
1544        assert_eq!(named.tool_name(), Some("ToolSearch"));
1545    }
1546
1547    #[test]
1548    fn test_from_named_input_known_tool() {
1549        let json = serde_json::json!({ "command": "ls -la" });
1550        let parsed = ToolInput::from_named_input("Bash", json);
1551        match parsed {
1552            ToolInput::Bash(b) => assert_eq!(b.command, "ls -la"),
1553            other => panic!("expected Bash, got {other:?}"),
1554        }
1555    }
1556
1557    #[test]
1558    fn test_from_named_input_unknown_name_defers_to_structural() {
1559        // An unmodeled (e.g. MCP) tool name with a shape we don't recognize
1560        // falls through to Unknown without error.
1561        let json = serde_json::json!({ "custom_field": 42 });
1562        let parsed = ToolInput::from_named_input("mcp__server__custom", json.clone());
1563        assert!(matches!(parsed, ToolInput::Unknown(_)));
1564
1565        // An unmodeled name whose payload happens to match a known shape still
1566        // resolves structurally via the untagged impl.
1567        let bash_shaped = serde_json::json!({ "command": "echo hi" });
1568        let parsed = ToolInput::from_named_input("mcp__server__shell", bash_shaped);
1569        assert!(matches!(parsed, ToolInput::Bash(_)));
1570    }
1571
1572    #[test]
1573    fn test_from_named_input_malformed_payload_falls_back_to_unknown() {
1574        // A known name with a payload that doesn't fit its struct is preserved
1575        // as Unknown rather than panicking or dropping data.
1576        let bad = serde_json::json!({ "not_a_command": true });
1577        let parsed = ToolInput::from_named_input("Bash", bad.clone());
1578        match parsed {
1579            ToolInput::Unknown(v) => assert_eq!(v, bad),
1580            other => panic!("expected Unknown, got {other:?}"),
1581        }
1582    }
1583}