tools.rs

  1use std::path::PathBuf;
  2
  3use agentic_coding_protocol::{self as acp, PushToolCallParams, ToolCallLocation};
  4use itertools::Itertools;
  5use schemars::JsonSchema;
  6use serde::{Deserialize, Serialize};
  7use util::ResultExt;
  8
  9pub enum ClaudeTool {
 10    Task(Option<TaskToolParams>),
 11    NotebookRead(Option<NotebookReadToolParams>),
 12    NotebookEdit(Option<NotebookEditToolParams>),
 13    Edit(Option<EditToolParams>),
 14    MultiEdit(Option<MultiEditToolParams>),
 15    ReadFile(Option<ReadToolParams>),
 16    Write(Option<WriteToolParams>),
 17    Ls(Option<LsToolParams>),
 18    Glob(Option<GlobToolParams>),
 19    Grep(Option<GrepToolParams>),
 20    Terminal(Option<BashToolParams>),
 21    WebFetch(Option<WebFetchToolParams>),
 22    WebSearch(Option<WebSearchToolParams>),
 23    TodoWrite(Option<TodoWriteToolParams>),
 24    ExitPlanMode(Option<ExitPlanModeToolParams>),
 25    Other {
 26        name: String,
 27        input: serde_json::Value,
 28    },
 29}
 30
 31impl ClaudeTool {
 32    pub fn infer(tool_name: &str, input: serde_json::Value) -> Self {
 33        match tool_name {
 34            // Known tools
 35            "mcp__zed__Read" => Self::ReadFile(serde_json::from_value(input).log_err()),
 36            "mcp__zed__Edit" => Self::Edit(serde_json::from_value(input).log_err()),
 37            "MultiEdit" => Self::MultiEdit(serde_json::from_value(input).log_err()),
 38            "Write" => Self::Write(serde_json::from_value(input).log_err()),
 39            "LS" => Self::Ls(serde_json::from_value(input).log_err()),
 40            "Glob" => Self::Glob(serde_json::from_value(input).log_err()),
 41            "Grep" => Self::Grep(serde_json::from_value(input).log_err()),
 42            "Bash" => Self::Terminal(serde_json::from_value(input).log_err()),
 43            "WebFetch" => Self::WebFetch(serde_json::from_value(input).log_err()),
 44            "WebSearch" => Self::WebSearch(serde_json::from_value(input).log_err()),
 45            "TodoWrite" => Self::TodoWrite(serde_json::from_value(input).log_err()),
 46            "exit_plan_mode" => Self::ExitPlanMode(serde_json::from_value(input).log_err()),
 47            "Task" => Self::Task(serde_json::from_value(input).log_err()),
 48            "NotebookRead" => Self::NotebookRead(serde_json::from_value(input).log_err()),
 49            "NotebookEdit" => Self::NotebookEdit(serde_json::from_value(input).log_err()),
 50            // Inferred from name
 51            _ => {
 52                let tool_name = tool_name.to_lowercase();
 53
 54                if tool_name.contains("edit") || tool_name.contains("write") {
 55                    Self::Edit(None)
 56                } else if tool_name.contains("terminal") {
 57                    Self::Terminal(None)
 58                } else {
 59                    Self::Other {
 60                        name: tool_name.to_string(),
 61                        input,
 62                    }
 63                }
 64            }
 65        }
 66    }
 67
 68    pub fn label(&self) -> String {
 69        match &self {
 70            Self::Task(Some(params)) => params.description.clone(),
 71            Self::Task(None) => "Task".into(),
 72            Self::NotebookRead(Some(params)) => {
 73                format!("Read Notebook {}", params.notebook_path.display())
 74            }
 75            Self::NotebookRead(None) => "Read Notebook".into(),
 76            Self::NotebookEdit(Some(params)) => {
 77                format!("Edit Notebook {}", params.notebook_path.display())
 78            }
 79            Self::NotebookEdit(None) => "Edit Notebook".into(),
 80            Self::Terminal(Some(params)) => format!("`{}`", params.command),
 81            Self::Terminal(None) => "Terminal".into(),
 82            Self::ReadFile(_) => "Read File".into(),
 83            Self::Ls(Some(params)) => {
 84                format!("List Directory {}", params.path.display())
 85            }
 86            Self::Ls(None) => "List Directory".into(),
 87            Self::Edit(Some(params)) => {
 88                format!("Edit {}", params.abs_path.display())
 89            }
 90            Self::Edit(None) => "Edit".into(),
 91            Self::MultiEdit(Some(params)) => {
 92                format!("Multi Edit {}", params.file_path.display())
 93            }
 94            Self::MultiEdit(None) => "Multi Edit".into(),
 95            Self::Write(Some(params)) => {
 96                format!("Write {}", params.file_path.display())
 97            }
 98            Self::Write(None) => "Write".into(),
 99            Self::Glob(Some(params)) => {
100                format!("Glob `{params}`")
101            }
102            Self::Glob(None) => "Glob".into(),
103            Self::Grep(Some(params)) => format!("`{params}`"),
104            Self::Grep(None) => "Grep".into(),
105            Self::WebFetch(Some(params)) => format!("Fetch {}", params.url),
106            Self::WebFetch(None) => "Fetch".into(),
107            Self::WebSearch(Some(params)) => format!("Web Search: {}", params),
108            Self::WebSearch(None) => "Web Search".into(),
109            Self::TodoWrite(Some(params)) => format!(
110                "Update TODOs: {}",
111                params.todos.iter().map(|todo| &todo.content).join(", ")
112            ),
113            Self::TodoWrite(None) => "Update TODOs".into(),
114            Self::ExitPlanMode(_) => "Exit Plan Mode".into(),
115            Self::Other { name, .. } => name.clone(),
116        }
117    }
118
119    pub fn content(&self) -> Option<acp::ToolCallContent> {
120        match &self {
121            Self::Other { input, .. } => Some(acp::ToolCallContent::Markdown {
122                markdown: format!(
123                    "```json\n{}```",
124                    serde_json::to_string_pretty(&input).unwrap_or("{}".to_string())
125                ),
126            }),
127            Self::Task(Some(params)) => Some(acp::ToolCallContent::Markdown {
128                markdown: params.prompt.clone(),
129            }),
130            Self::NotebookRead(Some(params)) => Some(acp::ToolCallContent::Markdown {
131                markdown: params.notebook_path.display().to_string(),
132            }),
133            Self::NotebookEdit(Some(params)) => Some(acp::ToolCallContent::Markdown {
134                markdown: params.new_source.clone(),
135            }),
136            Self::Terminal(Some(params)) => Some(acp::ToolCallContent::Markdown {
137                markdown: format!(
138                    "`{}`\n\n{}",
139                    params.command,
140                    params.description.as_deref().unwrap_or_default()
141                ),
142            }),
143            Self::ReadFile(Some(params)) => Some(acp::ToolCallContent::Markdown {
144                markdown: params.abs_path.display().to_string(),
145            }),
146            Self::Ls(Some(params)) => Some(acp::ToolCallContent::Markdown {
147                markdown: params.path.display().to_string(),
148            }),
149            Self::Glob(Some(params)) => Some(acp::ToolCallContent::Markdown {
150                markdown: params.to_string(),
151            }),
152            Self::Grep(Some(params)) => Some(acp::ToolCallContent::Markdown {
153                markdown: format!("`{params}`"),
154            }),
155            Self::WebFetch(Some(params)) => Some(acp::ToolCallContent::Markdown {
156                markdown: params.prompt.clone(),
157            }),
158            Self::WebSearch(Some(params)) => Some(acp::ToolCallContent::Markdown {
159                markdown: params.to_string(),
160            }),
161            Self::TodoWrite(Some(params)) => Some(acp::ToolCallContent::Markdown {
162                markdown: params
163                    .todos
164                    .iter()
165                    .map(|todo| {
166                        format!(
167                            "- {} {}: {}",
168                            match todo.status {
169                                TodoStatus::Completed => "",
170                                TodoStatus::InProgress => "🚧",
171                                TodoStatus::Pending => "",
172                            },
173                            todo.priority,
174                            todo.content
175                        )
176                    })
177                    .join("\n"),
178            }),
179            Self::ExitPlanMode(Some(params)) => Some(acp::ToolCallContent::Markdown {
180                markdown: params.plan.clone(),
181            }),
182            Self::Edit(Some(params)) => Some(acp::ToolCallContent::Diff {
183                diff: acp::Diff {
184                    path: params.abs_path.clone(),
185                    old_text: Some(params.old_text.clone()),
186                    new_text: params.new_text.clone(),
187                },
188            }),
189            Self::Write(Some(params)) => Some(acp::ToolCallContent::Diff {
190                diff: acp::Diff {
191                    path: params.file_path.clone(),
192                    old_text: None,
193                    new_text: params.content.clone(),
194                },
195            }),
196            Self::MultiEdit(Some(params)) => {
197                // todo: show multiple edits in a multibuffer?
198                params.edits.first().map(|edit| acp::ToolCallContent::Diff {
199                    diff: acp::Diff {
200                        path: params.file_path.clone(),
201                        old_text: Some(edit.old_string.clone()),
202                        new_text: edit.new_string.clone(),
203                    },
204                })
205            }
206            Self::Task(None)
207            | Self::NotebookRead(None)
208            | Self::NotebookEdit(None)
209            | Self::Terminal(None)
210            | Self::ReadFile(None)
211            | Self::Ls(None)
212            | Self::Glob(None)
213            | Self::Grep(None)
214            | Self::WebFetch(None)
215            | Self::WebSearch(None)
216            | Self::TodoWrite(None)
217            | Self::ExitPlanMode(None)
218            | Self::Edit(None)
219            | Self::Write(None)
220            | Self::MultiEdit(None) => None,
221        }
222    }
223
224    pub fn icon(&self) -> acp::Icon {
225        match self {
226            Self::Task(_) => acp::Icon::Hammer,
227            Self::NotebookRead(_) => acp::Icon::FileSearch,
228            Self::NotebookEdit(_) => acp::Icon::Pencil,
229            Self::Edit(_) => acp::Icon::Pencil,
230            Self::MultiEdit(_) => acp::Icon::Pencil,
231            Self::Write(_) => acp::Icon::Pencil,
232            Self::ReadFile(_) => acp::Icon::FileSearch,
233            Self::Ls(_) => acp::Icon::Folder,
234            Self::Glob(_) => acp::Icon::FileSearch,
235            Self::Grep(_) => acp::Icon::Regex,
236            Self::Terminal(_) => acp::Icon::Terminal,
237            Self::WebSearch(_) => acp::Icon::Globe,
238            Self::WebFetch(_) => acp::Icon::Globe,
239            Self::TodoWrite(_) => acp::Icon::LightBulb,
240            Self::ExitPlanMode(_) => acp::Icon::Hammer,
241            Self::Other { .. } => acp::Icon::Hammer,
242        }
243    }
244
245    pub fn confirmation(&self, description: Option<String>) -> acp::ToolCallConfirmation {
246        match &self {
247            Self::Edit(_) | Self::Write(_) | Self::NotebookEdit(_) | Self::MultiEdit(_) => {
248                acp::ToolCallConfirmation::Edit { description }
249            }
250            Self::WebFetch(params) => acp::ToolCallConfirmation::Fetch {
251                urls: params
252                    .as_ref()
253                    .map(|p| vec![p.url.clone()])
254                    .unwrap_or_default(),
255                description,
256            },
257            Self::Terminal(Some(BashToolParams {
258                description,
259                command,
260                ..
261            })) => acp::ToolCallConfirmation::Execute {
262                command: command.clone(),
263                root_command: command.clone(),
264                description: description.clone(),
265            },
266            Self::ExitPlanMode(Some(params)) => acp::ToolCallConfirmation::Other {
267                description: if let Some(description) = description {
268                    format!("{description} {}", params.plan)
269                } else {
270                    params.plan.clone()
271                },
272            },
273            Self::Task(Some(params)) => acp::ToolCallConfirmation::Other {
274                description: if let Some(description) = description {
275                    format!("{description} {}", params.description)
276                } else {
277                    params.description.clone()
278                },
279            },
280            Self::Ls(Some(LsToolParams { path, .. }))
281            | Self::ReadFile(Some(ReadToolParams { abs_path: path, .. })) => {
282                let path = path.display();
283                acp::ToolCallConfirmation::Other {
284                    description: if let Some(description) = description {
285                        format!("{description} {path}")
286                    } else {
287                        path.to_string()
288                    },
289                }
290            }
291            Self::NotebookRead(Some(NotebookReadToolParams { notebook_path, .. })) => {
292                let path = notebook_path.display();
293                acp::ToolCallConfirmation::Other {
294                    description: if let Some(description) = description {
295                        format!("{description} {path}")
296                    } else {
297                        path.to_string()
298                    },
299                }
300            }
301            Self::Glob(Some(params)) => acp::ToolCallConfirmation::Other {
302                description: if let Some(description) = description {
303                    format!("{description} {params}")
304                } else {
305                    params.to_string()
306                },
307            },
308            Self::Grep(Some(params)) => acp::ToolCallConfirmation::Other {
309                description: if let Some(description) = description {
310                    format!("{description} {params}")
311                } else {
312                    params.to_string()
313                },
314            },
315            Self::WebSearch(Some(params)) => acp::ToolCallConfirmation::Other {
316                description: if let Some(description) = description {
317                    format!("{description} {params}")
318                } else {
319                    params.to_string()
320                },
321            },
322            Self::TodoWrite(Some(params)) => {
323                let params = params.todos.iter().map(|todo| &todo.content).join(", ");
324                acp::ToolCallConfirmation::Other {
325                    description: if let Some(description) = description {
326                        format!("{description} {params}")
327                    } else {
328                        params
329                    },
330                }
331            }
332            Self::Terminal(None)
333            | Self::Task(None)
334            | Self::NotebookRead(None)
335            | Self::ExitPlanMode(None)
336            | Self::Ls(None)
337            | Self::Glob(None)
338            | Self::Grep(None)
339            | Self::ReadFile(None)
340            | Self::WebSearch(None)
341            | Self::TodoWrite(None)
342            | Self::Other { .. } => acp::ToolCallConfirmation::Other {
343                description: description.unwrap_or("".to_string()),
344            },
345        }
346    }
347
348    pub fn locations(&self) -> Vec<acp::ToolCallLocation> {
349        match &self {
350            Self::Edit(Some(EditToolParams { abs_path, .. })) => vec![ToolCallLocation {
351                path: abs_path.clone(),
352                line: None,
353            }],
354            Self::MultiEdit(Some(MultiEditToolParams { file_path, .. })) => {
355                vec![ToolCallLocation {
356                    path: file_path.clone(),
357                    line: None,
358                }]
359            }
360            Self::Write(Some(WriteToolParams { file_path, .. })) => vec![ToolCallLocation {
361                path: file_path.clone(),
362                line: None,
363            }],
364            Self::ReadFile(Some(ReadToolParams {
365                abs_path, offset, ..
366            })) => vec![ToolCallLocation {
367                path: abs_path.clone(),
368                line: *offset,
369            }],
370            Self::NotebookRead(Some(NotebookReadToolParams { notebook_path, .. })) => {
371                vec![ToolCallLocation {
372                    path: notebook_path.clone(),
373                    line: None,
374                }]
375            }
376            Self::NotebookEdit(Some(NotebookEditToolParams { notebook_path, .. })) => {
377                vec![ToolCallLocation {
378                    path: notebook_path.clone(),
379                    line: None,
380                }]
381            }
382            Self::Glob(Some(GlobToolParams {
383                path: Some(path), ..
384            })) => vec![ToolCallLocation {
385                path: path.clone(),
386                line: None,
387            }],
388            Self::Ls(Some(LsToolParams { path, .. })) => vec![ToolCallLocation {
389                path: path.clone(),
390                line: None,
391            }],
392            Self::Grep(Some(GrepToolParams {
393                path: Some(path), ..
394            })) => vec![ToolCallLocation {
395                path: PathBuf::from(path),
396                line: None,
397            }],
398            Self::Task(_)
399            | Self::NotebookRead(None)
400            | Self::NotebookEdit(None)
401            | Self::Edit(None)
402            | Self::MultiEdit(None)
403            | Self::Write(None)
404            | Self::ReadFile(None)
405            | Self::Ls(None)
406            | Self::Glob(_)
407            | Self::Grep(_)
408            | Self::Terminal(_)
409            | Self::WebFetch(_)
410            | Self::WebSearch(_)
411            | Self::TodoWrite(_)
412            | Self::ExitPlanMode(_)
413            | Self::Other { .. } => vec![],
414        }
415    }
416
417    pub fn as_acp(&self) -> PushToolCallParams {
418        PushToolCallParams {
419            label: self.label(),
420            content: self.content(),
421            icon: self.icon(),
422            locations: self.locations(),
423        }
424    }
425}
426
427#[derive(Deserialize, JsonSchema, Debug)]
428pub struct EditToolParams {
429    /// The absolute path to the file to read.
430    pub abs_path: PathBuf,
431    /// The old text to replace (must be unique in the file)
432    pub old_text: String,
433    /// The new text.
434    pub new_text: String,
435}
436
437#[derive(Serialize)]
438#[serde(rename_all = "camelCase")]
439pub struct EditToolResponse;
440
441#[derive(Deserialize, JsonSchema, Debug)]
442pub struct ReadToolParams {
443    /// The absolute path to the file to read.
444    pub abs_path: PathBuf,
445    /// Which line to start reading from. Omit to start from the beginning.
446    #[serde(skip_serializing_if = "Option::is_none")]
447    pub offset: Option<u32>,
448    /// How many lines to read. Omit for the whole file.
449    #[serde(skip_serializing_if = "Option::is_none")]
450    pub limit: Option<u32>,
451}
452
453#[derive(Serialize)]
454#[serde(rename_all = "camelCase")]
455pub struct ReadToolResponse {
456    pub content: String,
457}
458
459#[derive(Deserialize, JsonSchema, Debug)]
460pub struct WriteToolParams {
461    /// Absolute path for new file
462    pub file_path: PathBuf,
463    /// File content
464    pub content: String,
465}
466
467#[derive(Deserialize, JsonSchema, Debug)]
468pub struct BashToolParams {
469    /// Shell command to execute
470    pub command: String,
471    /// 5-10 word description of what command does
472    #[serde(skip_serializing_if = "Option::is_none")]
473    pub description: Option<String>,
474    /// Timeout in ms (max 600000ms/10min, default 120000ms)
475    #[serde(skip_serializing_if = "Option::is_none")]
476    pub timeout: Option<u32>,
477}
478
479#[derive(Deserialize, JsonSchema, Debug)]
480pub struct GlobToolParams {
481    /// Glob pattern like **/*.js or src/**/*.ts
482    pub pattern: String,
483    /// Directory to search in (omit for current directory)
484    #[serde(skip_serializing_if = "Option::is_none")]
485    pub path: Option<PathBuf>,
486}
487
488impl std::fmt::Display for GlobToolParams {
489    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
490        if let Some(path) = &self.path {
491            write!(f, "{}", path.display())?;
492        }
493        write!(f, "{}", self.pattern)
494    }
495}
496
497#[derive(Deserialize, JsonSchema, Debug)]
498pub struct LsToolParams {
499    /// Absolute path to directory
500    pub path: PathBuf,
501    /// Array of glob patterns to ignore
502    #[serde(default, skip_serializing_if = "Vec::is_empty")]
503    pub ignore: Vec<String>,
504}
505
506#[derive(Deserialize, JsonSchema, Debug)]
507pub struct GrepToolParams {
508    /// Regex pattern to search for
509    pub pattern: String,
510    /// File/directory to search (defaults to current directory)
511    #[serde(skip_serializing_if = "Option::is_none")]
512    pub path: Option<String>,
513    /// "content" (shows lines), "files_with_matches" (default), "count"
514    #[serde(skip_serializing_if = "Option::is_none")]
515    pub output_mode: Option<GrepOutputMode>,
516    /// Filter files with glob pattern like "*.js"
517    #[serde(skip_serializing_if = "Option::is_none")]
518    pub glob: Option<String>,
519    /// File type filter like "js", "py", "rust"
520    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
521    pub file_type: Option<String>,
522    /// Case insensitive search
523    #[serde(rename = "-i", default, skip_serializing_if = "is_false")]
524    pub case_insensitive: bool,
525    /// Show line numbers (content mode only)
526    #[serde(rename = "-n", default, skip_serializing_if = "is_false")]
527    pub line_numbers: bool,
528    /// Lines after match (content mode only)
529    #[serde(rename = "-A", skip_serializing_if = "Option::is_none")]
530    pub after_context: Option<u32>,
531    /// Lines before match (content mode only)
532    #[serde(rename = "-B", skip_serializing_if = "Option::is_none")]
533    pub before_context: Option<u32>,
534    /// Lines before and after match (content mode only)
535    #[serde(rename = "-C", skip_serializing_if = "Option::is_none")]
536    pub context: Option<u32>,
537    /// Enable multiline/cross-line matching
538    #[serde(default, skip_serializing_if = "is_false")]
539    pub multiline: bool,
540    /// Limit output to first N results
541    #[serde(skip_serializing_if = "Option::is_none")]
542    pub head_limit: Option<u32>,
543}
544
545impl std::fmt::Display for GrepToolParams {
546    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
547        write!(f, "grep")?;
548
549        // Boolean flags
550        if self.case_insensitive {
551            write!(f, " -i")?;
552        }
553        if self.line_numbers {
554            write!(f, " -n")?;
555        }
556
557        // Context options
558        if let Some(after) = self.after_context {
559            write!(f, " -A {}", after)?;
560        }
561        if let Some(before) = self.before_context {
562            write!(f, " -B {}", before)?;
563        }
564        if let Some(context) = self.context {
565            write!(f, " -C {}", context)?;
566        }
567
568        // Output mode
569        if let Some(mode) = &self.output_mode {
570            match mode {
571                GrepOutputMode::FilesWithMatches => write!(f, " -l")?,
572                GrepOutputMode::Count => write!(f, " -c")?,
573                GrepOutputMode::Content => {} // Default mode
574            }
575        }
576
577        // Head limit
578        if let Some(limit) = self.head_limit {
579            write!(f, " | head -{}", limit)?;
580        }
581
582        // Glob pattern
583        if let Some(glob) = &self.glob {
584            write!(f, " --include=\"{}\"", glob)?;
585        }
586
587        // File type
588        if let Some(file_type) = &self.file_type {
589            write!(f, " --type={}", file_type)?;
590        }
591
592        // Multiline
593        if self.multiline {
594            write!(f, " -P")?; // Perl-compatible regex for multiline
595        }
596
597        // Pattern (escaped if contains special characters)
598        write!(f, " \"{}\"", self.pattern)?;
599
600        // Path
601        if let Some(path) = &self.path {
602            write!(f, " {}", path)?;
603        }
604
605        Ok(())
606    }
607}
608
609#[derive(Deserialize, Serialize, JsonSchema, strum::Display, Debug)]
610#[serde(rename_all = "snake_case")]
611pub enum TodoPriority {
612    High,
613    Medium,
614    Low,
615}
616
617impl Into<acp::PlanEntryPriority> for TodoPriority {
618    fn into(self) -> acp::PlanEntryPriority {
619        match self {
620            TodoPriority::High => acp::PlanEntryPriority::High,
621            TodoPriority::Medium => acp::PlanEntryPriority::Medium,
622            TodoPriority::Low => acp::PlanEntryPriority::Low,
623        }
624    }
625}
626
627#[derive(Deserialize, Serialize, JsonSchema, Debug)]
628#[serde(rename_all = "snake_case")]
629pub enum TodoStatus {
630    Pending,
631    InProgress,
632    Completed,
633}
634
635impl Into<acp::PlanEntryStatus> for TodoStatus {
636    fn into(self) -> acp::PlanEntryStatus {
637        match self {
638            TodoStatus::Pending => acp::PlanEntryStatus::Pending,
639            TodoStatus::InProgress => acp::PlanEntryStatus::InProgress,
640            TodoStatus::Completed => acp::PlanEntryStatus::Completed,
641        }
642    }
643}
644
645#[derive(Deserialize, Serialize, JsonSchema, Debug)]
646pub struct Todo {
647    /// Unique identifier
648    pub id: String,
649    /// Task description
650    pub content: String,
651    /// Priority level of the todo
652    pub priority: TodoPriority,
653    /// Current status of the todo
654    pub status: TodoStatus,
655}
656
657impl Into<acp::PlanEntry> for Todo {
658    fn into(self) -> acp::PlanEntry {
659        acp::PlanEntry {
660            content: self.content,
661            priority: self.priority.into(),
662            status: self.status.into(),
663        }
664    }
665}
666
667#[derive(Deserialize, JsonSchema, Debug)]
668pub struct TodoWriteToolParams {
669    pub todos: Vec<Todo>,
670}
671
672#[derive(Deserialize, JsonSchema, Debug)]
673pub struct ExitPlanModeToolParams {
674    /// Implementation plan in markdown format
675    pub plan: String,
676}
677
678#[derive(Deserialize, JsonSchema, Debug)]
679pub struct TaskToolParams {
680    /// Short 3-5 word description of task
681    pub description: String,
682    /// Detailed task for agent to perform
683    pub prompt: String,
684}
685
686#[derive(Deserialize, JsonSchema, Debug)]
687pub struct NotebookReadToolParams {
688    /// Absolute path to .ipynb file
689    pub notebook_path: PathBuf,
690    /// Specific cell ID to read
691    #[serde(skip_serializing_if = "Option::is_none")]
692    pub cell_id: Option<String>,
693}
694
695#[derive(Deserialize, Serialize, JsonSchema, Debug)]
696#[serde(rename_all = "snake_case")]
697pub enum CellType {
698    Code,
699    Markdown,
700}
701
702#[derive(Deserialize, Serialize, JsonSchema, Debug)]
703#[serde(rename_all = "snake_case")]
704pub enum EditMode {
705    Replace,
706    Insert,
707    Delete,
708}
709
710#[derive(Deserialize, JsonSchema, Debug)]
711pub struct NotebookEditToolParams {
712    /// Absolute path to .ipynb file
713    pub notebook_path: PathBuf,
714    /// New cell content
715    pub new_source: String,
716    /// Cell ID to edit
717    #[serde(skip_serializing_if = "Option::is_none")]
718    pub cell_id: Option<String>,
719    /// Type of cell (code or markdown)
720    #[serde(skip_serializing_if = "Option::is_none")]
721    pub cell_type: Option<CellType>,
722    /// Edit operation mode
723    #[serde(skip_serializing_if = "Option::is_none")]
724    pub edit_mode: Option<EditMode>,
725}
726
727#[derive(Deserialize, Serialize, JsonSchema, Debug)]
728pub struct MultiEditItem {
729    /// The text to search for and replace
730    pub old_string: String,
731    /// The replacement text
732    pub new_string: String,
733    /// Whether to replace all occurrences or just the first
734    #[serde(default, skip_serializing_if = "is_false")]
735    pub replace_all: bool,
736}
737
738#[derive(Deserialize, JsonSchema, Debug)]
739pub struct MultiEditToolParams {
740    /// Absolute path to file
741    pub file_path: PathBuf,
742    /// List of edits to apply
743    pub edits: Vec<MultiEditItem>,
744}
745
746fn is_false(v: &bool) -> bool {
747    !*v
748}
749
750#[derive(Deserialize, JsonSchema, Debug)]
751#[serde(rename_all = "snake_case")]
752pub enum GrepOutputMode {
753    Content,
754    FilesWithMatches,
755    Count,
756}
757
758#[derive(Deserialize, JsonSchema, Debug)]
759pub struct WebFetchToolParams {
760    /// Valid URL to fetch
761    #[serde(rename = "url")]
762    pub url: String,
763    /// What to extract from content
764    pub prompt: String,
765}
766
767#[derive(Deserialize, JsonSchema, Debug)]
768pub struct WebSearchToolParams {
769    /// Search query (min 2 chars)
770    pub query: String,
771    /// Only include these domains
772    #[serde(default, skip_serializing_if = "Vec::is_empty")]
773    pub allowed_domains: Vec<String>,
774    /// Exclude these domains
775    #[serde(default, skip_serializing_if = "Vec::is_empty")]
776    pub blocked_domains: Vec<String>,
777}
778
779impl std::fmt::Display for WebSearchToolParams {
780    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
781        write!(f, "\"{}\"", self.query)?;
782
783        if !self.allowed_domains.is_empty() {
784            write!(f, " (allowed: {})", self.allowed_domains.join(", "))?;
785        }
786
787        if !self.blocked_domains.is_empty() {
788            write!(f, " (blocked: {})", self.blocked_domains.join(", "))?;
789        }
790
791        Ok(())
792    }
793}