use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::Result;
use async_trait::async_trait;
use serde::Deserialize;
use serde_json::json;
use super::{ApprovalRequirement, Tool, ToolContext, ToolDef, ToolResult};
pub struct OpenFileTool;
#[derive(Deserialize)]
struct OpenFileArgs {
path: String,
}
#[allow(dead_code)]
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum OpenStrategy {
MacOpen,
XdgOpen,
WindowsStart,
Wslview,
Headless(String),
}
pub(crate) fn pick_open_strategy() -> OpenStrategy {
if let Some(reason) = ssh_signal() {
return OpenStrategy::Headless(reason);
}
if let Some(reason) = ci_signal() {
return OpenStrategy::Headless(reason);
}
#[cfg(target_os = "macos")]
{
return OpenStrategy::MacOpen;
}
#[cfg(target_os = "windows")]
{
return OpenStrategy::WindowsStart;
}
#[cfg(all(unix, not(target_os = "macos")))]
{
if is_wsl() {
if which::which("wslview").is_ok() {
return OpenStrategy::Wslview;
}
return OpenStrategy::Headless(
"WSL detected but `wslview` is not installed (install the `wslu` \
package, or open the file manually from Windows Explorer)"
.into(),
);
}
let has_display = std::env::var("DISPLAY")
.map(|v| !v.is_empty())
.unwrap_or(false)
|| std::env::var("WAYLAND_DISPLAY")
.map(|v| !v.is_empty())
.unwrap_or(false);
if !has_display {
return OpenStrategy::Headless(
"no graphical session ($DISPLAY and $WAYLAND_DISPLAY both empty — \
likely a server / container / headless console)"
.into(),
);
}
return OpenStrategy::XdgOpen;
}
#[allow(unreachable_code)]
OpenStrategy::Headless("unsupported platform".into())
}
fn ssh_signal() -> Option<String> {
for v in ["SSH_CLIENT", "SSH_CONNECTION", "SSH_TTY"] {
if let Ok(s) = std::env::var(v) {
if !s.is_empty() {
return Some(format!("running over SSH (${} is set)", v));
}
}
}
None
}
fn ci_signal() -> Option<String> {
for v in ["CI", "GITHUB_ACTIONS", "GITLAB_CI", "BUILDKITE"] {
if let Ok(s) = std::env::var(v) {
if !s.is_empty() {
return Some(format!("running in CI (${} is set)", v));
}
}
}
None
}
#[cfg(all(unix, not(target_os = "macos")))]
fn is_wsl() -> bool {
if std::env::var("WSL_DISTRO_NAME")
.map(|s| !s.is_empty())
.unwrap_or(false)
{
return true;
}
std::fs::read_to_string("/proc/version")
.map(|s| s.to_lowercase().contains("microsoft"))
.unwrap_or(false)
}
fn strategy_command_name(s: &OpenStrategy) -> &'static str {
match s {
OpenStrategy::MacOpen => "open",
OpenStrategy::XdgOpen => "xdg-open",
OpenStrategy::WindowsStart => "cmd /c start",
OpenStrategy::Wslview => "wslview",
OpenStrategy::Headless(_) => "(headless)",
}
}
#[async_trait]
impl Tool for OpenFileTool {
fn definition(&self) -> ToolDef {
ToolDef {
name: "open_file",
description: "Open a local file (HTML / PDF / image / SVG / etc.) in the user's default GUI application — \
typically a browser for HTML, image viewer for PNG / JPG, PDF reader for PDF.\n\
\n\
USE ONLY WHEN:\n\
1. The user explicitly asks to preview / open / view a file, OR\n\
2. Previewing is the obvious next step (e.g. you just generated an HTML mockup the user requested) AND you have ASKED the user first.\n\
\n\
DO NOT auto-open after every write_file / edit_file. Files existing on disk don't need to pop windows; \
the user will preview them when they want to. When in doubt, ask before calling this tool.\n\
\n\
Cross-platform: macOS uses `open`, Linux desktop `xdg-open`, Windows `cmd /c start`, WSL `wslview`. \
Headless / SSH / CI sessions refuse with a clear reason so you can tell the user to fetch the file \
another way instead of pretending a window opened.".to_string(),
parameters: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "File path to open. Absolute, or relative to the current working directory. Must exist."
}
},
"required": ["path"]
}),
}
}
fn approval(&self, _args: &str) -> ApprovalRequirement {
ApprovalRequirement::RequireApproval(
"Launches a GUI application (browser / viewer) — user-visible side effect.".into(),
)
}
fn approval_with_context(&self, args: &str, ctx: &ToolContext) -> ApprovalRequirement {
let parsed = match serde_json::from_str::<OpenFileArgs>(args) {
Ok(p) => p,
Err(_) => return self.approval(args),
};
let wd = match ctx.working_dir.try_read() {
Ok(g) => g.clone(),
Err(_) => return self.approval(args),
};
match super::approval_for_path(
&parsed.path,
&wd,
super::ExternalPathAction::Enumerate,
) {
Ok(approval) => approval,
Err(_) => self.approval(args),
}
}
async fn execute(&self, args: &str, ctx: &ToolContext) -> Result<ToolResult> {
let parsed: OpenFileArgs = serde_json::from_str(args)?;
let path = parsed.path.as_str();
let wd = ctx.working_dir.read().await.clone();
let target = if Path::new(path).is_absolute() {
PathBuf::from(path)
} else {
wd.join(path)
};
let target = std::fs::canonicalize(&target).unwrap_or(target);
if !target.exists() {
return Ok(ToolResult {
call_id: String::new(),
output: format!("File not found: {}", target.display()),
success: false,
});
}
let strategy = pick_open_strategy();
let target_str = target.to_string_lossy().to_string();
let mut cmd = match &strategy {
OpenStrategy::MacOpen => {
let mut c = Command::new("open");
c.arg(&target_str);
c
}
OpenStrategy::XdgOpen => {
let mut c = Command::new("xdg-open");
c.arg(&target_str);
c
}
OpenStrategy::WindowsStart => {
let mut c = Command::new("cmd");
c.args(["/c", "start", "", &target_str]);
c
}
OpenStrategy::Wslview => {
let mut c = Command::new("wslview");
c.arg(&target_str);
c
}
OpenStrategy::Headless(reason) => {
return Ok(ToolResult {
call_id: String::new(),
output: format!(
"Cannot open in GUI: {}.\n\nFile path for manual viewing:\n {}",
reason,
target.display()
),
success: false,
});
}
};
cmd.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
match cmd.spawn() {
Ok(_child) => Ok(ToolResult {
call_id: String::new(),
output: format!(
"Opened {} via `{}`.",
target.display(),
strategy_command_name(&strategy)
),
success: true,
}),
Err(e) => Ok(ToolResult {
call_id: String::new(),
output: format!(
"Failed to launch `{}`: {}.\n\nFile path for manual viewing:\n {}",
strategy_command_name(&strategy),
e,
target.display()
),
success: false,
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ssh_signal_returns_none_in_clean_env() {
if std::env::var("SSH_CLIENT").is_ok()
|| std::env::var("SSH_CONNECTION").is_ok()
|| std::env::var("SSH_TTY").is_ok()
{
return;
}
assert!(ssh_signal().is_none());
}
#[test]
fn strategy_command_name_covers_every_variant() {
for s in [
OpenStrategy::MacOpen,
OpenStrategy::XdgOpen,
OpenStrategy::WindowsStart,
OpenStrategy::Wslview,
OpenStrategy::Headless("test".into()),
] {
assert!(!strategy_command_name(&s).is_empty());
}
}
}