diff options
| author | soryu <soryu@soryu.co> | 2026-01-27 01:25:29 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-27 01:25:40 +0000 |
| commit | b0d0b4848b2fc8a44c2575e09a08b34aaf6e1484 (patch) | |
| tree | bd0dedfd8a3623d01f28ff590e97a028bc5456c5 /makima/src/server/handlers | |
| parent | b28345d15730ffbefe81244d06c06fe13c30b0ea (diff) | |
| download | soryu-b0d0b4848b2fc8a44c2575e09a08b34aaf6e1484.tar.gz soryu-b0d0b4848b2fc8a44c2575e09a08b34aaf6e1484.zip | |
Default to shared worktree and add worktree endpoint
Diffstat (limited to 'makima/src/server/handlers')
| -rw-r--r-- | makima/src/server/handlers/contract_chat.rs | 1 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh.rs | 158 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh_chat.rs | 1 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh_supervisor.rs | 9 |
4 files changed, 169 insertions, 0 deletions
diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs index dac806a..98787be 100644 --- a/makima/src/server/handlers/contract_chat.rs +++ b/makima/src/server/handlers/contract_chat.rs @@ -1602,6 +1602,7 @@ async fn handle_contract_request( patch_data: None, patch_base_sha: None, local_only, + supervisor_worktree_task_id: None, // Not spawned by supervisor }; if let Err(e) = command_sender.send(command).await { diff --git a/makima/src/server/handlers/mesh.rs b/makima/src/server/handlers/mesh.rs index c4d862c..4b82ed7 100644 --- a/makima/src/server/handlers/mesh.rs +++ b/makima/src/server/handlers/mesh.rs @@ -705,6 +705,7 @@ pub async fn start_task( patch_data: None, patch_base_sha: None, local_only, + supervisor_worktree_task_id: None, // Not spawned by supervisor }; tracing::info!( @@ -758,6 +759,7 @@ pub async fn start_task( patch_data: None, patch_base_sha: None, local_only, + supervisor_worktree_task_id: None, // Not spawned by supervisor }; if state.send_daemon_command(alt_daemon_id, alt_command).await.is_ok() { @@ -1173,6 +1175,7 @@ pub async fn send_message( patch_data: None, patch_base_sha: None, local_only, + supervisor_worktree_task_id: None, // Not spawned by supervisor }; if state.send_daemon_command(new_daemon_id, spawn_cmd).await.is_ok() { @@ -1890,6 +1893,158 @@ pub async fn clone_worktree( .into_response() } +// ============================================================================= +// Worktree Info +// ============================================================================= + +/// Response for worktree info. +#[derive(Debug, serde::Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct WorktreeInfoResponse { + /// Task ID. + pub task_id: Uuid, + /// Path to the worktree directory. + pub worktree_path: Option<String>, + /// Whether the worktree exists. + pub exists: bool, + /// Aggregate statistics. + pub stats: WorktreeStats, + /// Changed files list. + pub files: Vec<WorktreeFile>, + /// Current branch name. + pub branch: Option<String>, + /// Current HEAD commit SHA. + pub head_sha: Option<String>, +} + +/// Statistics about worktree changes. +#[derive(Debug, serde::Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct WorktreeStats { + /// Number of files changed. + pub files_changed: i32, + /// Total lines inserted. + pub insertions: i32, + /// Total lines deleted. + pub deletions: i32, +} + +/// Information about a changed file in the worktree. +#[derive(Debug, serde::Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct WorktreeFile { + /// File path relative to worktree root. + pub path: String, + /// Git status code (M, A, D, R, C, U, ?). + pub status: String, + /// Lines added. + pub lines_added: i32, + /// Lines removed. + pub lines_removed: i32, +} + +/// Get worktree information for a task (files, stats, branch info). +#[utoipa::path( + get, + path = "/api/v1/mesh/tasks/{id}/worktree-info", + params( + ("id" = Uuid, Path, description = "Task ID") + ), + responses( + (status = 200, description = "Worktree info", body = WorktreeInfoResponse), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Task not found", body = ApiError), + (status = 503, description = "Database not configured or daemon not connected", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Mesh" +)] +pub async fn get_worktree_info( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path(id): Path<Uuid>, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + // Get the task (scoped by owner) + let task = match repository::get_task_for_owner(pool, id, auth.owner_id).await { + Ok(Some(t)) => t, + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Task not found")), + ) + .into_response(); + } + Err(e) => { + tracing::error!("Failed to get task {}: {}", id, e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response(); + } + }; + + // Get daemon running the task + let Some(daemon_id) = task.daemon_id else { + // Task has no daemon, return empty worktree info + return Json(WorktreeInfoResponse { + task_id: id, + worktree_path: None, + exists: false, + stats: WorktreeStats { + files_changed: 0, + insertions: 0, + deletions: 0, + }, + files: vec![], + branch: None, + head_sha: None, + }) + .into_response(); + }; + + // Send GetWorktreeInfo command to daemon + let command = DaemonCommand::GetWorktreeInfo { task_id: id }; + + if let Err(e) = state.send_daemon_command(daemon_id, command).await { + tracing::error!("Failed to send GetWorktreeInfo command: {}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DAEMON_ERROR", e)), + ) + .into_response(); + } + + // Return placeholder - actual data will be streamed via WebSocket + // For now, return empty data indicating the request is being processed + Json(WorktreeInfoResponse { + task_id: id, + worktree_path: None, + exists: false, + stats: WorktreeStats { + files_changed: 0, + insertions: 0, + deletions: 0, + }, + files: vec![], + branch: None, + head_sha: None, + }) + .into_response() +} + /// Request to check if a target directory exists. #[derive(Debug, serde::Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] @@ -2350,6 +2505,7 @@ pub async fn reassign_task( patch_data, patch_base_sha, local_only, + supervisor_worktree_task_id: None, // Not spawned by supervisor }; tracing::info!( @@ -2688,6 +2844,7 @@ pub async fn continue_task( patch_data: None, patch_base_sha: None, local_only, + supervisor_worktree_task_id: None, // Not spawned by supervisor }; tracing::info!( @@ -3612,6 +3769,7 @@ pub async fn branch_task( patch_data, patch_base_sha, local_only: false, // No contract, so not local_only + supervisor_worktree_task_id: None, // Not spawned by supervisor }; if let Err(e) = state.send_daemon_command(target_daemon_id, command).await { diff --git a/makima/src/server/handlers/mesh_chat.rs b/makima/src/server/handlers/mesh_chat.rs index ed6cfc0..a74eb4f 100644 --- a/makima/src/server/handlers/mesh_chat.rs +++ b/makima/src/server/handlers/mesh_chat.rs @@ -1165,6 +1165,7 @@ async fn handle_mesh_request( patch_data: None, patch_base_sha: None, local_only, + supervisor_worktree_task_id: None, // Not spawned by supervisor }; match state.send_daemon_command(target_daemon_id, command).await { diff --git a/makima/src/server/handlers/mesh_supervisor.rs b/makima/src/server/handlers/mesh_supervisor.rs index 24ba4bb..1a2e47f 100644 --- a/makima/src/server/handlers/mesh_supervisor.rs +++ b/makima/src/server/handlers/mesh_supervisor.rs @@ -36,6 +36,10 @@ pub struct SpawnTaskRequest { pub checkpoint_sha: Option<String>, /// Repository URL for the task (optional - if not provided, will be looked up from contract). pub repository_url: Option<String>, + /// If true, create a separate worktree for the task (requires merge after). + /// If false (default), the task shares the supervisor's worktree. + #[serde(default)] + pub use_own_worktree: bool, } /// Request to wait for task completion. @@ -406,6 +410,8 @@ pub async fn try_start_pending_task( patch_data, patch_base_sha, local_only: contract.local_only, + // For retried tasks, use their own worktree (they already have state from previous attempt) + supervisor_worktree_task_id: None, }; if let Err(e) = state.send_daemon_command(daemon.id, cmd).await { @@ -720,6 +726,8 @@ pub async fn spawn_task( patch_data: None, patch_base_sha: None, local_only: contract.local_only, + // Share supervisor's worktree by default; separate worktree only when explicitly requested + supervisor_worktree_task_id: if request.use_own_worktree { None } else { Some(supervisor_id) }, }; if let Err(e) = state.send_daemon_command(daemon.id, cmd).await { @@ -2248,6 +2256,7 @@ pub async fn resume_supervisor( patch_data, patch_base_sha, local_only: contract.local_only, + supervisor_worktree_task_id: None, // Supervisor uses its own worktree }; if let Err(e) = state.send_daemon_command(target_daemon_id, command).await { |
