use crate::error::AppError; use crate::models::agent::Agent; use crate::models::agent_task::AgentTask; use crate::models::module::{ProjectModule, MODULE_AGENT_TASK_RUNNER}; use crate::models::project::Project; use crate::AppState; use tauri::State; const TASK_LIST_RESULT_PREVIEW_MAX_BYTES: usize = 20_000; const TASK_LIST_ERROR_PREVIEW_MAX_BYTES: usize = 8_000; const TASK_LIST_TRUNCATION_NOTICE: &str = "\n\n[... contenu tronque pour preserver la fluidite de l'interface ...]"; fn truncate_for_task_list(value: Option, max_bytes: usize) -> Option { let Some(content) = value else { return None; }; if content.len() <= max_bytes { return Some(content); } let mut boundary = max_bytes; while boundary > 0 && !content.is_char_boundary(boundary) { boundary -= 1; } let mut truncated = content[..boundary].to_string(); truncated.push_str(TASK_LIST_TRUNCATION_NOTICE); Some(truncated) } #[tauri::command] pub fn create_agent_task( state: State<'_, AppState>, project_id: String, agent_id: String, title: String, description: String, ) -> Result { if title.trim().is_empty() { return Err(AppError::from( "Le titre de la tâche est obligatoire".to_string(), )); } let db = state .db .lock() .map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?; let enabled = ProjectModule::is_enabled(&db, &project_id, MODULE_AGENT_TASK_RUNNER)?; if !enabled { return Err(AppError::from( "Le module Agent task runner est désactivé pour ce projet".to_string(), )); } Project::get_by_id(&db, &project_id)?; Agent::get_by_id(&db, &agent_id)?; let task = AgentTask::insert( &db, &project_id, &agent_id, title.trim(), description.trim(), )?; Ok(task) } #[tauri::command] pub fn list_agent_tasks( state: State<'_, AppState>, project_id: String, ) -> Result, AppError> { let db = state .db .lock() .map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?; let tasks = AgentTask::list_by_project(&db, &project_id)? .into_iter() .map(|mut task| { task.result = truncate_for_task_list(task.result, TASK_LIST_RESULT_PREVIEW_MAX_BYTES); task.error = truncate_for_task_list(task.error, TASK_LIST_ERROR_PREVIEW_MAX_BYTES); task }) .collect(); Ok(tasks) } #[tauri::command] pub fn retry_agent_task(state: State<'_, AppState>, task_id: String) -> Result<(), AppError> { let db = state .db .lock() .map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?; let task = AgentTask::get_by_id(&db, &task_id)?; let enabled = ProjectModule::is_enabled(&db, &task.project_id, MODULE_AGENT_TASK_RUNNER)?; if !enabled { return Err(AppError::from( "Le module Agent task runner est désactivé pour ce projet".to_string(), )); } if task.status != "error" && task.status != "cancelled" && task.status != "done" { return Err(AppError::from(format!( "Impossible de relancer une tâche avec le statut '{}'", task.status ))); } AgentTask::retry(&db, &task_id)?; Ok(()) } #[tauri::command] pub async fn cancel_agent_task( state: State<'_, AppState>, task_id: String, ) -> Result<(), AppError> { { let db = state .db .lock() .map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?; let task = AgentTask::get_by_id(&db, &task_id)?; if task.status == "done" || task.status == "cancelled" { return Err(AppError::from(format!( "Impossible d'annuler une tâche avec le statut '{}'", task.status ))); } } state.process_registry.cancel_task(&task_id).await; let db = state .db .lock() .map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?; let task = AgentTask::get_by_id(&db, &task_id)?; if task.status == "done" || task.status == "cancelled" { return Err(AppError::from(format!( "Impossible d'annuler une tâche avec le statut '{}'", task.status ))); } AgentTask::cancel(&db, &task_id)?; Ok(()) } #[cfg(test)] mod tests { use super::truncate_for_task_list; #[test] fn test_truncate_for_task_list_keeps_short_content() { let input = Some("short".to_string()); let output = truncate_for_task_list(input, 10); assert_eq!(output.as_deref(), Some("short")); } #[test] fn test_truncate_for_task_list_truncates_long_content() { let input = Some("x".repeat(20)); let output = truncate_for_task_list(input, 10).expect("content should exist"); assert!(output.starts_with("xxxxxxxxxx")); assert!(output.contains("contenu tronque")); } #[test] fn test_truncate_for_task_list_respects_utf8_boundaries() { let input = Some("éééé".to_string()); let output = truncate_for_task_list(input, 3).expect("content should exist"); assert!(output.starts_with("é")); assert!(!output.starts_with("éé")); } }