// Tauri commands wrapping kon_storage task CRUD. // Pattern mirrors transcripts.rs — TaskDto is the camelCase frontend shape, // storage functions are aliased with db_ prefix to avoid name collisions. use serde::{Deserialize, Serialize}; use uuid::Uuid; use kon_storage::{ complete_subtask_and_check_parent as db_complete_subtask, complete_task as db_complete_task, delete_task as db_delete_task, get_task_by_id as db_get_task, insert_subtask as db_insert_subtask, insert_task as db_insert_task, list_subtasks as db_list_subtasks, list_tasks as db_list_tasks, uncomplete_task as db_uncomplete_task, update_task as db_update_task, TaskRow, }; use crate::AppState; /// Frontend-facing task shape. Matches the in-memory object in page.svelte.js. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct TaskDto { pub id: String, pub text: String, pub bucket: String, pub list_id: Option, pub effort: Option, pub notes: String, pub done: bool, pub done_at: Option, pub created_at: String, pub source_transcript_id: Option, pub parent_task_id: Option, } impl From for TaskDto { fn from(r: TaskRow) -> Self { Self { id: r.id, text: r.text, bucket: r.bucket, list_id: r.list_id, effort: r.effort, notes: r.notes, done: r.done, done_at: r.done_at, created_at: r.created_at, source_transcript_id: r.source_transcript_id, parent_task_id: r.parent_task_id, } } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateTaskRequest { pub id: String, pub text: String, pub bucket: String, #[serde(default)] pub source_transcript_id: Option, #[serde(default)] pub list_id: Option, #[serde(default)] pub effort: Option, } #[tauri::command] pub async fn create_task_cmd( state: tauri::State<'_, AppState>, request: CreateTaskRequest, ) -> Result { db_insert_task( &state.db, &request.id, &request.text, &request.bucket, request.source_transcript_id.as_deref(), request.list_id.as_deref(), request.effort.as_deref(), ) .await .map_err(|e| e.to_string())?; // Fetch the freshly-inserted row so the frontend can stop doing // client-side object construction. Mirrors list_tasks_cmd's shape. db_get_task(&state.db, &request.id) .await .map_err(|e| e.to_string())? .map(TaskDto::from) .ok_or_else(|| format!("Task {} not found after insert", request.id)) } /// Patch-shaped update. Any field omitted (or explicit `null`) leaves the /// column untouched via `COALESCE` in the storage layer. Matches /// `update_transcript`'s partial-update philosophy. `done` / `doneAt` are /// intentionally absent — those flow through `complete_task_cmd` / /// `uncomplete_task_cmd` to stamp the server-side timestamp. #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UpdateTaskRequest { #[serde(default)] pub text: Option, #[serde(default)] pub bucket: Option, #[serde(default)] pub list_id: Option, #[serde(default)] pub effort: Option, #[serde(default)] pub notes: Option, } #[tauri::command] pub async fn update_task_cmd( state: tauri::State<'_, AppState>, id: String, patch: UpdateTaskRequest, ) -> Result { let row = db_update_task( &state.db, &id, patch.text.as_deref(), patch.bucket.as_deref(), patch.list_id.as_deref(), patch.effort.as_deref(), patch.notes.as_deref(), ) .await .map_err(|e| e.to_string())?; Ok(TaskDto::from(row)) } #[tauri::command] pub async fn list_tasks_cmd(state: tauri::State<'_, AppState>) -> Result, String> { db_list_tasks(&state.db) .await .map(|rows| rows.into_iter().map(TaskDto::from).collect()) .map_err(|e| e.to_string()) } #[tauri::command] pub async fn complete_task_cmd( state: tauri::State<'_, AppState>, id: String, ) -> Result<(), String> { db_complete_task(&state.db, &id) .await .map_err(|e| e.to_string()) } #[tauri::command] pub async fn delete_task_cmd(state: tauri::State<'_, AppState>, id: String) -> Result<(), String> { db_delete_task(&state.db, &id) .await .map_err(|e| e.to_string()) } #[tauri::command] pub async fn uncomplete_task_cmd( state: tauri::State<'_, AppState>, id: String, ) -> Result<(), String> { db_uncomplete_task(&state.db, &id) .await .map_err(|e| e.to_string()) } #[tauri::command] pub async fn decompose_and_store( state: tauri::State<'_, AppState>, parent_task_id: String, ) -> Result, String> { let parent = db_get_task(&state.db, &parent_task_id) .await .map_err(|e| e.to_string())? .ok_or_else(|| format!("Task {parent_task_id} not found"))?; let engine = state.llm_engine.clone(); let parent_text = parent.text.clone(); let steps = tokio::task::spawn_blocking(move || engine.decompose_task(&parent_text)) .await .map_err(|e| e.to_string())? .map_err(|e| e.to_string())?; let mut created = Vec::new(); for text in steps { let id = Uuid::new_v4().to_string(); db_insert_subtask(&state.db, &id, &text, &parent_task_id) .await .map_err(|e| e.to_string())?; if let Some(row) = db_get_task(&state.db, &id) .await .map_err(|e| e.to_string())? { created.push(TaskDto::from(row)); } } Ok(created) } #[tauri::command] pub async fn extract_tasks_from_transcript_cmd( state: tauri::State<'_, AppState>, transcript: String, ) -> Result, String> { let engine = state.llm_engine.clone(); tokio::task::spawn_blocking(move || engine.extract_tasks(&transcript)) .await .map_err(|e| e.to_string())? .map_err(|e| e.to_string()) } #[tauri::command] pub async fn list_subtasks_cmd( state: tauri::State<'_, AppState>, parent_task_id: String, ) -> Result, String> { db_list_subtasks(&state.db, &parent_task_id) .await .map(|rows| rows.into_iter().map(TaskDto::from).collect()) .map_err(|e| e.to_string()) } #[tauri::command] pub async fn complete_subtask_cmd( state: tauri::State<'_, AppState>, subtask_id: String, ) -> Result<(), String> { db_complete_subtask(&state.db, &subtask_id) .await .map_err(|e| e.to_string()) }