tools.rs

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