use std::path::Path;
use serde_json::{json, Value};
use super::{files_dir, validate_read_path};
pub(super) fn schemas() -> Vec<Value> {
vec![
json!({
"type": "function",
"function": {
"name": "open_in_editor",
"description": "Open a file in the default editor (invokes `code` on PATH), optionally at a line number.",
"parameters": {
"type": "object",
"properties": {
"path": { "type": "string", "description": "File path" },
"line": { "type": "number", "description": "Line number (optional)" }
},
"required": ["path"]
}
}
}),
json!({
"type": "function",
"function": {
"name": "reveal_in_explorer",
"description": "Show a file or folder in the system file manager.",
"parameters": {
"type": "object",
"properties": {
"path": { "type": "string", "description": "File or folder path" }
},
"required": ["path"]
}
}
}),
json!({
"type": "function",
"function": {
"name": "open_url",
"description": "Open a URL or local file in the default browser. Accepts http/https URLs, file:// URIs, absolute local file paths, OR a bare filename that resolves under ~/.claudette/files/. For files just produced by generate_code/write_file, pass the absolute `path` field from that tool's response — do not reconstruct a file:// URL by hand.",
"parameters": {
"type": "object",
"properties": {
"url": { "type": "string", "description": "URL (http/https/file://), absolute local file path, or bare filename in the scratch dir" }
},
"required": ["url"]
}
}
}),
]
}
pub(super) fn dispatch(name: &str, input: &str) -> Option<Result<String, String>> {
let result = match name {
"open_in_editor" => run_open_in_editor(input),
"reveal_in_explorer" => run_reveal_in_explorer(input),
"open_url" => run_open_url(input),
_ => return None,
};
Some(result)
}
fn run_open_in_editor(input: &str) -> Result<String, String> {
let v: Value = serde_json::from_str(input)
.map_err(|e| format!("open_in_editor: invalid JSON ({e}): {input}"))?;
let path_str = v
.get("path")
.and_then(Value::as_str)
.ok_or("open_in_editor: missing 'path'")?;
let line = v.get("line").and_then(Value::as_u64);
let resolved = validate_read_path(path_str)?;
let target = match line {
Some(n) => format!("{}:{n}", resolved.display()),
None => resolved.display().to_string(),
};
#[cfg(target_os = "windows")]
{
std::process::Command::new("cmd")
.args(["/C", "code", "--goto", &target])
.spawn()
.map_err(|e| format!("open_in_editor: failed to launch editor: {e}"))?;
}
#[cfg(not(target_os = "windows"))]
{
std::process::Command::new("code")
.arg("--goto")
.arg(&target)
.spawn()
.map_err(|e| format!("open_in_editor: failed to launch editor: {e}"))?;
}
Ok(json!({
"ok": true,
"opened": target,
})
.to_string())
}
fn run_reveal_in_explorer(input: &str) -> Result<String, String> {
let v: Value = serde_json::from_str(input)
.map_err(|e| format!("reveal_in_explorer: invalid JSON ({e}): {input}"))?;
let path_str = v
.get("path")
.and_then(Value::as_str)
.ok_or("reveal_in_explorer: missing 'path'")?;
let resolved = validate_read_path(path_str)?;
#[cfg(target_os = "windows")]
{
std::process::Command::new("explorer")
.arg(format!("/select,{}", resolved.display()))
.spawn()
.map_err(|e| format!("reveal_in_explorer: failed to launch explorer: {e}"))?;
}
#[cfg(not(target_os = "windows"))]
{
let parent = resolved.parent().unwrap_or(&resolved);
std::process::Command::new("xdg-open")
.arg(parent.as_os_str())
.spawn()
.map_err(|e| format!("reveal_in_explorer: failed to open file manager: {e}"))?;
}
Ok(json!({
"ok": true,
"revealed": resolved.display().to_string(),
})
.to_string())
}
fn run_open_url(input: &str) -> Result<String, String> {
let v: Value = serde_json::from_str(input)
.map_err(|e| format!("open_url: invalid JSON ({e}): {input}"))?;
let url = v
.get("url")
.and_then(Value::as_str)
.ok_or("open_url: missing 'url'")?;
let is_url =
url.starts_with("http://") || url.starts_with("https://") || url.starts_with("file://");
let scratch_resolved = if !is_url && !Path::new(url).exists() {
let candidate = files_dir().join(url);
candidate.is_file().then(|| candidate.display().to_string())
} else {
None
};
let target_owned = scratch_resolved.unwrap_or_else(|| url.to_string());
let target = target_owned.as_str();
if !is_url && !Path::new(target).exists() {
return Err(format!("open_url: not a URL or existing local file: {url}"));
}
#[cfg(target_os = "windows")]
{
std::process::Command::new("rundll32")
.args(["url.dll,FileProtocolHandler", target])
.spawn()
.map_err(|e| format!("open_url: failed to open: {e}"))?;
}
#[cfg(not(target_os = "windows"))]
{
std::process::Command::new("xdg-open")
.arg(target)
.spawn()
.map_err(|e| format!("open_url: failed to open: {e}"))?;
}
Ok(json!({
"ok": true,
"opened": target,
})
.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn open_in_editor_rejects_missing_path() {
let err = run_open_in_editor("{}").unwrap_err();
assert!(err.contains("missing"), "got: {err}");
}
#[test]
fn reveal_in_explorer_rejects_missing_path() {
let err = run_reveal_in_explorer("{}").unwrap_err();
assert!(err.contains("missing"), "got: {err}");
}
#[test]
fn open_url_rejects_missing_url() {
let err = run_open_url("{}").unwrap_err();
assert!(err.contains("missing"), "got: {err}");
}
#[test]
fn open_url_rejects_bare_string_that_is_not_a_path() {
let err = run_open_url(r#"{"url":"not-a-url-nor-a-file"}"#).unwrap_err();
assert!(err.contains("not a URL"), "got: {err}");
}
#[test]
fn schemas_lists_three_tools() {
let schemas = schemas();
assert_eq!(schemas.len(), 3);
let names: Vec<&str> = schemas
.iter()
.filter_map(|v| v.pointer("/function/name").and_then(Value::as_str))
.collect();
assert_eq!(names, ["open_in_editor", "reveal_in_explorer", "open_url"]);
}
}