intent_engine/dashboard/
handlers.rs

1use axum::{
2    extract::{Path, Query, State},
3    http::StatusCode,
4    response::{IntoResponse, Json},
5};
6use serde_json::json;
7
8use super::models::*;
9use super::server::AppState;
10use crate::{
11    db::models::TaskSortBy, events::EventManager, search::SearchManager, tasks::TaskManager,
12    workspace::WorkspaceManager,
13};
14
15/// Get all tasks with optional filters
16pub async fn list_tasks(
17    State(state): State<AppState>,
18    Query(query): Query<TaskListQuery>,
19) -> impl IntoResponse {
20    let db_pool = state.current_project.read().await.db_pool.clone();
21    let task_mgr = TaskManager::new(&db_pool);
22
23    // Convert parent filter to Option<Option<i64>>
24    let parent_filter = query.parent.as_deref().map(|p| {
25        if p == "null" {
26            None
27        } else {
28            p.parse::<i64>().ok()
29        }
30    });
31
32    // Parse sort_by parameter
33    let sort_by = match query.sort_by.as_deref() {
34        Some("id") => Some(TaskSortBy::Id),
35        Some("priority") => Some(TaskSortBy::Priority),
36        Some("time") => Some(TaskSortBy::Time),
37        Some("focus") => Some(TaskSortBy::FocusAware),
38        _ => Some(TaskSortBy::FocusAware), // Default to FocusAware
39    };
40
41    match task_mgr
42        .find_tasks(
43            query.status.as_deref(),
44            parent_filter,
45            sort_by,
46            query.limit,
47            query.offset,
48        )
49        .await
50    {
51        Ok(result) => (StatusCode::OK, Json(ApiResponse { data: result })).into_response(),
52        Err(e) => {
53            tracing::error!("Failed to fetch tasks: {}", e);
54            (
55                StatusCode::INTERNAL_SERVER_ERROR,
56                Json(ApiError {
57                    code: "DATABASE_ERROR".to_string(),
58                    message: format!("Failed to list tasks: {}", e),
59                    details: None,
60                }),
61            )
62                .into_response()
63        },
64    }
65}
66
67/// Get a single task by ID
68pub async fn get_task(State(state): State<AppState>, Path(id): Path<i64>) -> impl IntoResponse {
69    let db_pool = state.current_project.read().await.db_pool.clone();
70    let task_mgr = TaskManager::new(&db_pool);
71
72    match task_mgr.get_task(id).await {
73        Ok(task) => (StatusCode::OK, Json(ApiResponse { data: task })).into_response(),
74        Err(e) if e.to_string().contains("not found") => (
75            StatusCode::NOT_FOUND,
76            Json(ApiError {
77                code: "TASK_NOT_FOUND".to_string(),
78                message: format!("Task {} not found", id),
79                details: None,
80            }),
81        )
82            .into_response(),
83        Err(e) => (
84            StatusCode::INTERNAL_SERVER_ERROR,
85            Json(ApiError {
86                code: "DATABASE_ERROR".to_string(),
87                message: format!("Failed to get task: {}", e),
88                details: None,
89            }),
90        )
91            .into_response(),
92    }
93}
94
95/// Create a new task
96pub async fn create_task(
97    State(state): State<AppState>,
98    Json(req): Json<CreateTaskRequest>,
99) -> impl IntoResponse {
100    let project = state.current_project.read().await;
101    let db_pool = project.db_pool.clone();
102    let project_path = project.project_path.to_string_lossy().to_string();
103    drop(project);
104
105    let task_mgr = TaskManager::with_websocket(
106        &db_pool,
107        std::sync::Arc::new(state.ws_state.clone()),
108        project_path,
109    );
110
111    // Note: add_task doesn't support priority - it's set separately via update_task
112    // Dashboard creates human-owned tasks (None = human)
113    let result = task_mgr
114        .add_task(&req.name, req.spec.as_deref(), req.parent_id, None)
115        .await;
116
117    match result {
118        Ok(mut task) => {
119            // If priority was requested, update it
120            if let Some(priority) = req.priority {
121                if let Ok(updated_task) = task_mgr
122                    .update_task(task.id, None, None, None, None, None, Some(priority))
123                    .await
124                {
125                    task = updated_task;
126                }
127                // Ignore priority update errors
128            }
129            (StatusCode::CREATED, Json(ApiResponse { data: task })).into_response()
130        },
131        Err(e) => (
132            StatusCode::BAD_REQUEST,
133            Json(ApiError {
134                code: "INVALID_REQUEST".to_string(),
135                message: format!("Failed to create task: {}", e),
136                details: None,
137            }),
138        )
139            .into_response(),
140    }
141}
142
143/// Update a task
144pub async fn update_task(
145    State(state): State<AppState>,
146    Path(id): Path<i64>,
147    Json(req): Json<UpdateTaskRequest>,
148) -> impl IntoResponse {
149    let project = state.current_project.read().await;
150    let db_pool = project.db_pool.clone();
151    let project_path = project.project_path.to_string_lossy().to_string();
152    drop(project);
153
154    let task_mgr = TaskManager::with_websocket(
155        &db_pool,
156        std::sync::Arc::new(state.ws_state.clone()),
157        project_path,
158    );
159
160    // First check if task exists
161    match task_mgr.get_task(id).await {
162        Err(e) if e.to_string().contains("not found") => {
163            return (
164                StatusCode::NOT_FOUND,
165                Json(ApiError {
166                    code: "TASK_NOT_FOUND".to_string(),
167                    message: format!("Task {} not found", id),
168                    details: None,
169                }),
170            )
171                .into_response()
172        },
173        Err(e) => {
174            return (
175                StatusCode::INTERNAL_SERVER_ERROR,
176                Json(ApiError {
177                    code: "DATABASE_ERROR".to_string(),
178                    message: format!("Database error: {}", e),
179                    details: None,
180                }),
181            )
182                .into_response()
183        },
184        Ok(_) => {},
185    }
186
187    // Update task fields
188    // Signature: update_task(id, name, spec, parent_id, status, complexity, priority)
189    match task_mgr
190        .update_task(
191            id,
192            req.name.as_deref(),
193            req.spec.as_deref(),
194            None, // parent_id - not supported via update API
195            req.status.as_deref(),
196            None, // complexity - not exposed in API
197            req.priority,
198        )
199        .await
200    {
201        Ok(task) => (StatusCode::OK, Json(ApiResponse { data: task })).into_response(),
202        Err(e) => (
203            StatusCode::BAD_REQUEST,
204            Json(ApiError {
205                code: "INVALID_REQUEST".to_string(),
206                message: format!("Failed to update task: {}", e),
207                details: None,
208            }),
209        )
210            .into_response(),
211    }
212}
213
214/// Delete a task
215pub async fn delete_task(State(state): State<AppState>, Path(id): Path<i64>) -> impl IntoResponse {
216    let project = state.current_project.read().await;
217    let db_pool = project.db_pool.clone();
218    let project_path = project.project_path.to_string_lossy().to_string();
219    drop(project);
220
221    let task_mgr = TaskManager::with_websocket(
222        &db_pool,
223        std::sync::Arc::new(state.ws_state.clone()),
224        project_path,
225    );
226
227    match task_mgr.delete_task(id).await {
228        Ok(_) => (StatusCode::NO_CONTENT).into_response(),
229        Err(e) if e.to_string().contains("not found") => (
230            StatusCode::NOT_FOUND,
231            Json(ApiError {
232                code: "TASK_NOT_FOUND".to_string(),
233                message: format!("Task {} not found", id),
234                details: None,
235            }),
236        )
237            .into_response(),
238        Err(e) => (
239            StatusCode::BAD_REQUEST,
240            Json(ApiError {
241                code: "INVALID_REQUEST".to_string(),
242                message: format!("Failed to delete task: {}", e),
243                details: None,
244            }),
245        )
246            .into_response(),
247    }
248}
249
250/// Start a task (set as current)
251pub async fn start_task(State(state): State<AppState>, Path(id): Path<i64>) -> impl IntoResponse {
252    let project = state.current_project.read().await;
253    let db_pool = project.db_pool.clone();
254    let project_path = project.project_path.to_string_lossy().to_string();
255    drop(project);
256
257    let task_mgr = TaskManager::with_websocket(
258        &db_pool,
259        std::sync::Arc::new(state.ws_state.clone()),
260        project_path,
261    );
262
263    match task_mgr.start_task(id, false).await {
264        Ok(task) => (StatusCode::OK, Json(ApiResponse { data: task })).into_response(),
265        Err(e) if e.to_string().contains("not found") => (
266            StatusCode::NOT_FOUND,
267            Json(ApiError {
268                code: "TASK_NOT_FOUND".to_string(),
269                message: format!("Task {} not found", id),
270                details: None,
271            }),
272        )
273            .into_response(),
274        Err(e) => (
275            StatusCode::BAD_REQUEST,
276            Json(ApiError {
277                code: "INVALID_REQUEST".to_string(),
278                message: format!("Failed to start task: {}", e),
279                details: None,
280            }),
281        )
282            .into_response(),
283    }
284}
285
286/// Complete the current task
287pub async fn done_task(State(state): State<AppState>) -> impl IntoResponse {
288    let project = state.current_project.read().await;
289    let db_pool = project.db_pool.clone();
290    let project_path = project.project_path.to_string_lossy().to_string();
291    drop(project);
292
293    let task_mgr = TaskManager::with_websocket(
294        &db_pool,
295        std::sync::Arc::new(state.ws_state.clone()),
296        project_path,
297    );
298
299    // Dashboard = human caller, no passphrase needed
300    match task_mgr.done_task(false).await {
301        Ok(task) => (StatusCode::OK, Json(ApiResponse { data: task })).into_response(),
302        Err(e) if e.to_string().contains("No current task") => (
303            StatusCode::BAD_REQUEST,
304            Json(ApiError {
305                code: "NO_CURRENT_TASK".to_string(),
306                message: "No current task to complete".to_string(),
307                details: None,
308            }),
309        )
310            .into_response(),
311        Err(e) => (
312            StatusCode::BAD_REQUEST,
313            Json(ApiError {
314                code: "INVALID_REQUEST".to_string(),
315                message: format!("Failed to complete task: {}", e),
316                details: None,
317            }),
318        )
319            .into_response(),
320    }
321}
322
323/// Spawn a subtask and switch to it
324/// Note: This creates a subtask of the CURRENT task, not an arbitrary parent
325pub async fn spawn_subtask(
326    State(state): State<AppState>,
327    Path(_parent_id): Path<i64>, // Ignored - uses current task
328    Json(req): Json<SpawnSubtaskRequest>,
329) -> impl IntoResponse {
330    let project = state.current_project.read().await;
331    let db_pool = project.db_pool.clone();
332    let project_path = project.project_path.to_string_lossy().to_string();
333    drop(project);
334
335    let task_mgr = TaskManager::with_websocket(
336        &db_pool,
337        std::sync::Arc::new(state.ws_state.clone()),
338        project_path,
339    );
340
341    // spawn_subtask uses the current task as parent automatically
342    match task_mgr.spawn_subtask(&req.name, req.spec.as_deref()).await {
343        Ok(response) => (StatusCode::CREATED, Json(ApiResponse { data: response })).into_response(),
344        Err(e) if e.to_string().contains("No current task") => (
345            StatusCode::BAD_REQUEST,
346            Json(ApiError {
347                code: "NO_CURRENT_TASK".to_string(),
348                message: "No current task to spawn subtask from".to_string(),
349                details: None,
350            }),
351        )
352            .into_response(),
353        Err(e) => (
354            StatusCode::BAD_REQUEST,
355            Json(ApiError {
356                code: "INVALID_REQUEST".to_string(),
357                message: format!("Failed to spawn subtask: {}", e),
358                details: None,
359            }),
360        )
361            .into_response(),
362    }
363}
364
365/// Get current task
366pub async fn get_current_task(State(state): State<AppState>) -> impl IntoResponse {
367    let db_pool = state.current_project.read().await.db_pool.clone();
368    let workspace_mgr = WorkspaceManager::new(&db_pool);
369
370    match workspace_mgr.get_current_task().await {
371        Ok(response) => {
372            if response.task.is_some() {
373                (StatusCode::OK, Json(ApiResponse { data: response })).into_response()
374            } else {
375                (
376                    StatusCode::OK,
377                    Json(json!({
378                        "data": null,
379                        "message": "No current task"
380                    })),
381                )
382                    .into_response()
383            }
384        },
385        Err(e) => (
386            StatusCode::INTERNAL_SERVER_ERROR,
387            Json(ApiError {
388                code: "DATABASE_ERROR".to_string(),
389                message: format!("Failed to get current task: {}", e),
390                details: None,
391            }),
392        )
393            .into_response(),
394    }
395}
396
397/// Pick next task recommendation
398pub async fn pick_next_task(State(state): State<AppState>) -> impl IntoResponse {
399    let db_pool = state.current_project.read().await.db_pool.clone();
400    let task_mgr = TaskManager::new(&db_pool);
401
402    match task_mgr.pick_next().await {
403        Ok(response) => (StatusCode::OK, Json(ApiResponse { data: response })).into_response(),
404        Err(e) => (
405            StatusCode::INTERNAL_SERVER_ERROR,
406            Json(ApiError {
407                code: "DATABASE_ERROR".to_string(),
408                message: format!("Failed to pick next task: {}", e),
409                details: None,
410            }),
411        )
412            .into_response(),
413    }
414}
415
416/// List events for a task
417pub async fn list_events(
418    State(state): State<AppState>,
419    Path(task_id): Path<i64>,
420    Query(query): Query<EventListQuery>,
421) -> impl IntoResponse {
422    let db_pool = state.current_project.read().await.db_pool.clone();
423    let event_mgr = EventManager::new(&db_pool);
424
425    // Signature: list_events(task_id, limit, log_type, since)
426    match event_mgr
427        .list_events(
428            Some(task_id),
429            query.limit.map(|l| l as i64),
430            query.event_type,
431            query.since,
432        )
433        .await
434    {
435        Ok(events) => (StatusCode::OK, Json(ApiResponse { data: events })).into_response(),
436        Err(e) => (
437            StatusCode::INTERNAL_SERVER_ERROR,
438            Json(ApiError {
439                code: "DATABASE_ERROR".to_string(),
440                message: format!("Failed to list events: {}", e),
441                details: None,
442            }),
443        )
444            .into_response(),
445    }
446}
447
448/// Add an event to a task
449pub async fn create_event(
450    State(state): State<AppState>,
451    Path(task_id): Path<i64>,
452    Json(req): Json<CreateEventRequest>,
453) -> impl IntoResponse {
454    let project = state.current_project.read().await;
455    let db_pool = project.db_pool.clone();
456    let project_path = project.project_path.to_string_lossy().to_string();
457    drop(project);
458
459    let event_mgr = EventManager::with_websocket(
460        &db_pool,
461        std::sync::Arc::new(state.ws_state.clone()),
462        project_path,
463    );
464
465    // Validate event type
466    if !["decision", "blocker", "milestone", "note"].contains(&req.event_type.as_str()) {
467        return (
468            StatusCode::BAD_REQUEST,
469            Json(ApiError {
470                code: "INVALID_REQUEST".to_string(),
471                message: format!("Invalid event type: {}", req.event_type),
472                details: None,
473            }),
474        )
475            .into_response();
476    }
477
478    // add_event signature: (task_id, log_type, discussion_data)
479    match event_mgr
480        .add_event(task_id, &req.event_type, &req.data)
481        .await
482    {
483        Ok(event) => (StatusCode::CREATED, Json(ApiResponse { data: event })).into_response(),
484        Err(e) => (
485            StatusCode::BAD_REQUEST,
486            Json(ApiError {
487                code: "INVALID_REQUEST".to_string(),
488                message: format!("Failed to create event: {}", e),
489                details: None,
490            }),
491        )
492            .into_response(),
493    }
494}
495
496/// Update an event
497pub async fn update_event(
498    State(state): State<AppState>,
499    Path((task_id, event_id)): Path<(i64, i64)>,
500    Json(req): Json<UpdateEventRequest>,
501) -> impl IntoResponse {
502    let project = state.current_project.read().await;
503    let db_pool = project.db_pool.clone();
504    let project_path = project.project_path.to_string_lossy().to_string();
505    drop(project);
506
507    let event_mgr = EventManager::with_websocket(
508        &db_pool,
509        std::sync::Arc::new(state.ws_state.clone()),
510        project_path,
511    );
512
513    // Validate event type if provided
514    if let Some(ref event_type) = req.event_type {
515        if !["decision", "blocker", "milestone", "note"].contains(&event_type.as_str()) {
516            return (
517                StatusCode::BAD_REQUEST,
518                Json(ApiError {
519                    code: "INVALID_REQUEST".to_string(),
520                    message: format!("Invalid event type: {}", event_type),
521                    details: None,
522                }),
523            )
524                .into_response();
525        }
526    }
527
528    match event_mgr
529        .update_event(event_id, req.event_type.as_deref(), req.data.as_deref())
530        .await
531    {
532        Ok(event) => {
533            // Verify the event belongs to the specified task
534            if event.task_id != task_id {
535                return (
536                    StatusCode::BAD_REQUEST,
537                    Json(ApiError {
538                        code: "INVALID_REQUEST".to_string(),
539                        message: format!("Event {} does not belong to task {}", event_id, task_id),
540                        details: None,
541                    }),
542                )
543                    .into_response();
544            }
545            (StatusCode::OK, Json(ApiResponse { data: event })).into_response()
546        },
547        Err(e) => (
548            StatusCode::BAD_REQUEST,
549            Json(ApiError {
550                code: "INVALID_REQUEST".to_string(),
551                message: format!("Failed to update event: {}", e),
552                details: None,
553            }),
554        )
555            .into_response(),
556    }
557}
558
559/// Delete an event
560pub async fn delete_event(
561    State(state): State<AppState>,
562    Path((task_id, event_id)): Path<(i64, i64)>,
563) -> impl IntoResponse {
564    let project = state.current_project.read().await;
565    let db_pool = project.db_pool.clone();
566    let project_path = project.project_path.to_string_lossy().to_string();
567    drop(project);
568
569    let event_mgr = EventManager::with_websocket(
570        &db_pool,
571        std::sync::Arc::new(state.ws_state.clone()),
572        project_path,
573    );
574
575    // First verify the event exists and belongs to the task
576    match sqlx::query_as::<_, crate::db::models::Event>(crate::sql_constants::SELECT_EVENT_BY_ID)
577        .bind(event_id)
578        .fetch_optional(&db_pool)
579        .await
580    {
581        Ok(Some(event)) => {
582            if event.task_id != task_id {
583                return (
584                    StatusCode::BAD_REQUEST,
585                    Json(ApiError {
586                        code: "INVALID_REQUEST".to_string(),
587                        message: format!("Event {} does not belong to task {}", event_id, task_id),
588                        details: None,
589                    }),
590                )
591                    .into_response();
592            }
593        },
594        Ok(None) => {
595            return (
596                StatusCode::NOT_FOUND,
597                Json(ApiError {
598                    code: "EVENT_NOT_FOUND".to_string(),
599                    message: format!("Event {} not found", event_id),
600                    details: None,
601                }),
602            )
603                .into_response();
604        },
605        Err(e) => {
606            return (
607                StatusCode::INTERNAL_SERVER_ERROR,
608                Json(ApiError {
609                    code: "DATABASE_ERROR".to_string(),
610                    message: format!("Database error: {}", e),
611                    details: None,
612                }),
613            )
614                .into_response();
615        },
616    }
617
618    match event_mgr.delete_event(event_id).await {
619        Ok(_) => (StatusCode::NO_CONTENT).into_response(),
620        Err(e) => (
621            StatusCode::BAD_REQUEST,
622            Json(ApiError {
623                code: "INVALID_REQUEST".to_string(),
624                message: format!("Failed to delete event: {}", e),
625                details: None,
626            }),
627        )
628            .into_response(),
629    }
630}
631
632/// Unified search across tasks and events
633pub async fn search(
634    State(state): State<AppState>,
635    Query(query): Query<SearchQuery>,
636) -> impl IntoResponse {
637    let db_pool = state.current_project.read().await.db_pool.clone();
638    let search_mgr = SearchManager::new(&db_pool);
639
640    match search_mgr
641        .search(
642            &query.query,
643            query.include_tasks,
644            query.include_events,
645            query.limit,
646            query.offset,
647            false,
648        )
649        .await
650    {
651        Ok(results) => (StatusCode::OK, Json(ApiResponse { data: results })).into_response(),
652        Err(e) => (
653            StatusCode::INTERNAL_SERVER_ERROR,
654            Json(ApiError {
655                code: "DATABASE_ERROR".to_string(),
656                message: format!("Search failed: {}", e),
657                details: None,
658            }),
659        )
660            .into_response(),
661    }
662}
663
664/// List all registered projects (from in-memory state)
665pub async fn list_projects(State(state): State<AppState>) -> impl IntoResponse {
666    // Use the same method as WebSocket init for consistency
667    let projects_info = {
668        let current_project = state.current_project.read().await;
669        state
670            .ws_state
671            .get_online_projects_with_current(
672                &current_project.project_name,
673                &current_project.project_path,
674                &current_project.db_path,
675                &state.host_project,
676                state.port,
677            )
678            .await
679    };
680
681    // Convert ProjectInfo to API response format with additional metadata
682    let port = state.port;
683    let pid = std::process::id();
684
685    let projects: Vec<serde_json::Value> = projects_info
686        .iter()
687        .map(|proj| {
688            json!({
689                "name": proj.name,
690                "path": proj.path,
691                "port": port,
692                "pid": pid,
693                "url": format!("http://127.0.0.1:{}", port),
694                "started_at": chrono::Utc::now().to_rfc3339(),
695                "mcp_connected": proj.mcp_connected,
696                "is_online": proj.is_online,  // Now included!
697                "mcp_agent": proj.agent,
698                "mcp_last_seen": if proj.mcp_connected {
699                    Some(chrono::Utc::now().to_rfc3339())
700                } else {
701                    None::<String>
702                },
703            })
704        })
705        .collect();
706
707    (StatusCode::OK, Json(ApiResponse { data: projects })).into_response()
708}
709
710/// Switch to a different project database dynamically
711pub async fn switch_project(
712    State(state): State<AppState>,
713    Json(req): Json<SwitchProjectRequest>,
714) -> impl IntoResponse {
715    use super::server::ProjectContext;
716    use std::path::PathBuf;
717
718    // Parse and validate project path
719    let project_path = PathBuf::from(&req.project_path);
720
721    if !project_path.exists() {
722        return (
723            StatusCode::NOT_FOUND,
724            Json(ApiError {
725                code: "PROJECT_NOT_FOUND".to_string(),
726                message: format!("Project path does not exist: {}", project_path.display()),
727                details: None,
728            }),
729        )
730            .into_response();
731    }
732
733    // Construct database path
734    let db_path = project_path.join(".intent-engine").join("project.db");
735
736    if !db_path.exists() {
737        return (
738            StatusCode::NOT_FOUND,
739            Json(ApiError {
740                code: "DATABASE_NOT_FOUND".to_string(),
741                message: format!(
742                    "Database not found at {}. Is this an Intent-Engine project?",
743                    db_path.display()
744                ),
745                details: None,
746            }),
747        )
748            .into_response();
749    }
750
751    // Create new database connection using the shared helper
752    // This ensures consistent configuration (WAL mode, timeouts) and correct path handling
753    let new_db_pool = match crate::db::create_pool(&db_path).await {
754        Ok(pool) => pool,
755        Err(e) => {
756            return (
757                StatusCode::INTERNAL_SERVER_ERROR,
758                Json(ApiError {
759                    code: "DATABASE_CONNECTION_ERROR".to_string(),
760                    message: format!("Failed to connect to database: {}", e),
761                    details: None,
762                }),
763            )
764                .into_response();
765        },
766    };
767
768    // Extract project name
769    let project_name = project_path
770        .file_name()
771        .and_then(|n| n.to_str())
772        .unwrap_or("unknown")
773        .to_string();
774
775    // Create new project context
776    let new_context = ProjectContext {
777        db_pool: new_db_pool,
778        project_name: project_name.clone(),
779        project_path: project_path.clone(),
780        db_path: db_path.clone(),
781    };
782
783    // Update the current project (write lock)
784    {
785        let mut current = state.current_project.write().await;
786        *current = new_context;
787    }
788
789    tracing::info!(
790        "Switched to project: {} at {}",
791        project_name,
792        project_path.display()
793    );
794
795    (
796        StatusCode::OK,
797        Json(ApiResponse {
798            data: json!({
799                "success": true,
800                "project_name": project_name,
801                "project_path": project_path.display().to_string(),
802                "database": db_path.display().to_string(),
803            }),
804        }),
805    )
806        .into_response()
807}
808
809/// Get task context (ancestors, siblings, children)
810pub async fn get_task_context(
811    State(state): State<AppState>,
812    Path(id): Path<i64>,
813) -> impl IntoResponse {
814    let db_pool = state.current_project.read().await.db_pool.clone();
815    let task_mgr = TaskManager::new(&db_pool);
816
817    match task_mgr.get_task_context(id).await {
818        Ok(context) => (StatusCode::OK, Json(ApiResponse { data: context })).into_response(),
819        Err(e) if e.to_string().contains("not found") => (
820            StatusCode::NOT_FOUND,
821            Json(ApiError {
822                code: "TASK_NOT_FOUND".to_string(),
823                message: format!("Task {} not found", id),
824                details: None,
825            }),
826        )
827            .into_response(),
828        Err(e) => (
829            StatusCode::INTERNAL_SERVER_ERROR,
830            Json(ApiError {
831                code: "DATABASE_ERROR".to_string(),
832                message: format!("Failed to get task context: {}", e),
833                details: None,
834            }),
835        )
836            .into_response(),
837    }
838}