pub use crate::state::{FileState, StateSnapshot, ToolState};
use crate::core::{Tool, ToolArgs, ToolError, ToolResult};
use anyhow::Result;
use std::sync::{Arc, Mutex};
pub struct StateTool {
name: String,
}
impl StateTool {
pub fn new() -> Self {
Self {
name: "_state".to_string(),
}
}
}
impl Tool for StateTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
"Display current state of open files and tool context"
}
fn signature(&self) -> &str {
"_state"
}
fn validate_args(&self, _args: &ToolArgs) -> Result<(), ToolError> {
Ok(()) }
fn execute(&mut self, _args: &ToolArgs, state: &Arc<Mutex<ToolState>>) -> Result<ToolResult> {
let state = state
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock state: {}", e))?;
let summary = state.get_summary();
let mut full_response = format!("Current tool state:\n\n{}", summary);
if let Some(file_state) = state.get_current_file_state() {
let window_content = file_state.get_window_with_line_numbers();
if !window_content.is_empty() {
full_response.push_str("\n\nCurrent window:\n");
full_response.push_str(&window_content.join("\n"));
}
}
Ok(ToolResult::success_with_data(
full_response,
serde_json::json!({
"summary": summary,
"working_directory": state.working_directory,
"current_file": state.current_file,
"open_files": state.open_files.keys().collect::<Vec<_>>(),
"history_count": state.history.len()
}),
))
}
fn get_parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {},
"required": []
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_state_creation() {
let content = vec![
"line1".to_string(),
"line2".to_string(),
"line3".to_string(),
];
let file_state = FileState::new(content, 2);
assert_eq!(file_state.window_size, 2);
assert_eq!(file_state.window_start, 0);
assert_eq!(file_state.total_lines(), 3);
assert!(!file_state.modified);
}
#[test]
fn test_file_state_windowing() {
let content: Vec<String> = (1..=10).map(|i| format!("line {}", i)).collect();
let mut file_state = FileState::new(content, 3);
let window = file_state.get_window();
assert_eq!(window.len(), 3);
assert_eq!(window[0], "line 1");
assert_eq!(window[2], "line 3");
file_state.scroll_down();
let window = file_state.get_window();
assert_eq!(window[0], "line 4");
file_state.goto_line(8);
assert!(file_state.window_start >= 5); }
#[test]
fn test_tool_state_file_management() {
let mut state = ToolState::new();
let path = PathBuf::from("test.txt");
let content = vec!["line1".to_string(), "line2".to_string()];
state.open_file(path.clone(), content, 10).unwrap();
assert_eq!(state.current_file, Some(path.clone()));
assert!(state.open_files.contains_key(&path));
let file_state = state.get_current_file_state().unwrap();
assert_eq!(file_state.total_lines(), 2);
state.close_file(&path);
assert!(!state.open_files.contains_key(&path));
assert_eq!(state.current_file, None);
}
#[test]
fn test_state_tool_execution() {
let mut tool = StateTool::new();
let state = Arc::new(Mutex::new(ToolState::new()));
let args = ToolArgs::from_args(&[]);
let result = tool.execute(&args, &state).unwrap();
assert!(result.success);
assert!(result.data.is_some());
}
use std::path::PathBuf;
}