feat: implement hooks system for project configuration

- Add HooksEditor component for managing project hooks
- Add ProjectSettings component for project-specific configurations
- Create hooksManager utility for hook operations
- Add hooks type definitions
- Update backend commands to support hooks functionality
- Integrate hooks into main app, agent execution, and Claude sessions
- Update API and utilities to handle hooks data
This commit is contained in:
Vivek R
2025-07-06 14:10:44 +05:30
parent 1922ffc145
commit 6b9393f4d3
15 changed files with 2294 additions and 311 deletions

View File

@@ -2165,7 +2165,7 @@ pub async fn get_recently_modified_files(
.collect())
}
/// Tracks multiple session messages at once (batch operation)
/// Track session messages from the frontend for checkpointing
#[tauri::command]
pub async fn track_session_messages(
state: tauri::State<'_, crate::checkpoint::state::CheckpointState>,
@@ -2174,17 +2174,148 @@ pub async fn track_session_messages(
project_path: String,
messages: Vec<String>,
) -> Result<(), String> {
let mgr = state
log::info!(
"Tracking {} messages for session {}",
messages.len(),
session_id
);
let manager = state
.get_or_create_manager(
session_id,
project_id,
std::path::PathBuf::from(project_path),
session_id.clone(),
project_id.clone(),
PathBuf::from(&project_path),
)
.await
.map_err(|e| e.to_string())?;
.map_err(|e| format!("Failed to get checkpoint manager: {}", e))?;
for m in messages {
mgr.track_message(m).await.map_err(|e| e.to_string())?;
for message in messages {
manager
.track_message(message)
.await
.map_err(|e| format!("Failed to track message: {}", e))?;
}
Ok(())
}
/// Gets hooks configuration from settings at specified scope
#[tauri::command]
pub async fn get_hooks_config(scope: String, project_path: Option<String>) -> Result<serde_json::Value, String> {
log::info!("Getting hooks config for scope: {}, project: {:?}", scope, project_path);
let settings_path = match scope.as_str() {
"user" => {
get_claude_dir()
.map_err(|e| e.to_string())?
.join("settings.json")
},
"project" => {
let path = project_path.ok_or("Project path required for project scope")?;
PathBuf::from(path).join(".claude").join("settings.json")
},
"local" => {
let path = project_path.ok_or("Project path required for local scope")?;
PathBuf::from(path).join(".claude").join("settings.local.json")
},
_ => return Err("Invalid scope".to_string())
};
if !settings_path.exists() {
log::info!("Settings file does not exist at {:?}, returning empty hooks", settings_path);
return Ok(serde_json::json!({}));
}
let content = fs::read_to_string(&settings_path)
.map_err(|e| format!("Failed to read settings: {}", e))?;
let settings: serde_json::Value = serde_json::from_str(&content)
.map_err(|e| format!("Failed to parse settings: {}", e))?;
Ok(settings.get("hooks").cloned().unwrap_or(serde_json::json!({})))
}
/// Updates hooks configuration in settings at specified scope
#[tauri::command]
pub async fn update_hooks_config(
scope: String,
hooks: serde_json::Value,
project_path: Option<String>
) -> Result<String, String> {
log::info!("Updating hooks config for scope: {}, project: {:?}", scope, project_path);
let settings_path = match scope.as_str() {
"user" => {
get_claude_dir()
.map_err(|e| e.to_string())?
.join("settings.json")
},
"project" => {
let path = project_path.ok_or("Project path required for project scope")?;
let claude_dir = PathBuf::from(path).join(".claude");
fs::create_dir_all(&claude_dir)
.map_err(|e| format!("Failed to create .claude directory: {}", e))?;
claude_dir.join("settings.json")
},
"local" => {
let path = project_path.ok_or("Project path required for local scope")?;
let claude_dir = PathBuf::from(path).join(".claude");
fs::create_dir_all(&claude_dir)
.map_err(|e| format!("Failed to create .claude directory: {}", e))?;
claude_dir.join("settings.local.json")
},
_ => return Err("Invalid scope".to_string())
};
// Read existing settings or create new
let mut settings = if settings_path.exists() {
let content = fs::read_to_string(&settings_path)
.map_err(|e| format!("Failed to read settings: {}", e))?;
serde_json::from_str(&content)
.map_err(|e| format!("Failed to parse settings: {}", e))?
} else {
serde_json::json!({})
};
// Update hooks section
settings["hooks"] = hooks;
// Write back with pretty formatting
let json_string = serde_json::to_string_pretty(&settings)
.map_err(|e| format!("Failed to serialize settings: {}", e))?;
fs::write(&settings_path, json_string)
.map_err(|e| format!("Failed to write settings: {}", e))?;
Ok("Hooks configuration updated successfully".to_string())
}
/// Validates a hook command by dry-running it
#[tauri::command]
pub async fn validate_hook_command(command: String) -> Result<serde_json::Value, String> {
log::info!("Validating hook command syntax");
// Validate syntax without executing
let mut cmd = std::process::Command::new("bash");
cmd.arg("-n") // Syntax check only
.arg("-c")
.arg(&command);
match cmd.output() {
Ok(output) => {
if output.status.success() {
Ok(serde_json::json!({
"valid": true,
"message": "Command syntax is valid"
}))
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Ok(serde_json::json!({
"valid": false,
"message": format!("Syntax error: {}", stderr)
}))
}
}
Err(e) => Err(format!("Failed to validate command: {}", e))
}
}