Files
g3/crates/g3-core/src/tools/shell.rs
Dhanji R. Prasanna ed1c31dd70 Improve tool output formatting
1. str_replace: Show insertion/deletion counts with colors
   " +N insertions | -M deletions" (green/red)

2. write_file: Compact format with human-readable sizes
   " wrote N lines | Xk chars"

3. read_file: Cleaner format
   "🔍 N lines read" instead of "📄 File content (N lines)"

4. webdriver_quit: Show correct driver name (safaridriver vs chromedriver)

5. read_file: When start position exceeds file length, read last 100 chars
   with explanation instead of failing

6. shell: Remove redundant "Command failed:" prefix from error messages
2026-01-11 19:52:00 +05:30

120 lines
4.0 KiB
Rust

//! Shell command execution tools.
use anyhow::Result;
use tracing::debug;
use crate::ui_writer::UiWriter;
use crate::utils::resolve_paths_in_shell_command;
use crate::utils::shell_escape_command;
use crate::ToolCall;
use super::executor::ToolContext;
/// Execute the `shell` tool.
pub async fn execute_shell<W: UiWriter>(tool_call: &ToolCall, ctx: &ToolContext<'_, W>) -> Result<String> {
debug!("Processing shell tool call");
let command = match tool_call.args.get("command").and_then(|v| v.as_str()) {
Some(cmd) => cmd,
None => {
debug!("No command parameter found in args: {:?}", tool_call.args);
return Ok("❌ Missing command argument".to_string());
}
};
debug!("Command string: {}", command);
// First resolve any file paths with Unicode space fallback (macOS screenshot names)
let resolved_command = resolve_paths_in_shell_command(command);
debug!("Resolved command: {}", resolved_command);
let escaped_command = shell_escape_command(&resolved_command);
let executor = g3_execution::CodeExecutor::new();
struct ToolOutputReceiver<'a, W: UiWriter> {
ui_writer: &'a W,
}
impl<'a, W: UiWriter> g3_execution::OutputReceiver for ToolOutputReceiver<'a, W> {
fn on_output_line(&self, line: &str) {
self.ui_writer.update_tool_output_line(line);
}
}
let receiver = ToolOutputReceiver {
ui_writer: ctx.ui_writer,
};
debug!(
"ABOUT TO CALL execute_bash_streaming_in_dir: escaped_command='{}', working_dir={:?}",
escaped_command, ctx.working_dir
);
match executor
.execute_bash_streaming_in_dir(&escaped_command, &receiver, ctx.working_dir)
.await
{
Ok(result) => {
if result.success {
Ok(if result.stdout.is_empty() {
"✅ Command executed successfully".to_string()
} else {
result.stdout.trim().to_string()
})
} else {
Ok(format!("{}", result.stderr.trim()))
}
}
Err(e) => Ok(format!("❌ Execution error: {}", e)),
}
}
/// Execute the `background_process` tool.
pub async fn execute_background_process<W: UiWriter>(
tool_call: &ToolCall,
ctx: &ToolContext<'_, W>,
) -> Result<String> {
debug!("Processing background_process tool call");
let name = match tool_call.args.get("name").and_then(|v| v.as_str()) {
Some(n) => n,
None => return Ok("❌ Missing 'name' argument".to_string()),
};
let command = match tool_call.args.get("command").and_then(|v| v.as_str()) {
Some(c) => c,
None => return Ok("❌ Missing 'command' argument".to_string()),
};
// Use provided working_dir, or fall back to context working_dir, or current dir
let work_dir = tool_call
.args
.get("working_dir")
.and_then(|v| v.as_str())
.map(|s| std::path::PathBuf::from(shellexpand::tilde(s).as_ref()))
.or_else(|| ctx.working_dir.map(std::path::PathBuf::from))
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
match ctx.background_process_manager.start(name, command, &work_dir) {
Ok(info) => Ok(format!(
"✅ Background process '{}' started\n\n\
**PID:** {}\n\
**Log file:** {}\n\
**Working dir:** {}\n\n\
To interact with this process, use the shell tool:\n\
- View logs: `tail -100 {}`\n\
- Follow logs: `tail -f {}` (blocks until Ctrl+C)\n\
- Check status: `ps -p {}`\n\
- Stop process: `kill {}`",
info.name,
info.pid,
info.log_file.display(),
info.working_dir.display(),
info.log_file.display(),
info.log_file.display(),
info.pid,
info.pid
)),
Err(e) => Ok(format!("❌ Failed to start background process: {}", e)),
}
}