diff options
| author | soryu <soryu@soryu.co> | 2026-01-26 19:00:20 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-26 19:00:20 +0000 |
| commit | 39c743467391e00c7c970753e6165b025784af76 (patch) | |
| tree | cecb31cbc57c9ee046b1f8b9aed5934c3c2570bb | |
| parent | cb4f2fc40dbabb40de948512eee74c7e46264665 (diff) | |
| download | soryu-39c743467391e00c7c970753e6165b025784af76.tar.gz soryu-39c743467391e00c7c970753e6165b025784af76.zip | |
[WIP] Heartbeat checkpoint - 2026-01-26 19:00:20 UTC
| -rw-r--r-- | makima/src/daemon/ws/protocol.rs | 87 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh.rs | 611 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh_daemon.rs | 62 | ||||
| -rw-r--r-- | makima/src/server/mod.rs | 5 | ||||
| -rw-r--r-- | makima/src/server/state.rs | 28 |
5 files changed, 793 insertions, 0 deletions
diff --git a/makima/src/daemon/ws/protocol.rs b/makima/src/daemon/ws/protocol.rs index 2e7caef..7e26bac 100644 --- a/makima/src/daemon/ws/protocol.rs +++ b/makima/src/daemon/ws/protocol.rs @@ -328,6 +328,74 @@ pub enum DaemonMessage { /// Error message if failed error: Option<String>, }, + + // ========================================================================= + // Export Patch Response Messages + // ========================================================================= + + /// Response to CreateExportPatch command. + ExportPatchCreated { + #[serde(rename = "taskId")] + task_id: Uuid, + #[serde(rename = "contractId")] + contract_id: Uuid, + success: bool, + /// The patch content (uncompressed git diff) + #[serde(rename = "patchContent")] + patch_content: Option<String>, + /// Number of files in the patch + #[serde(rename = "filesCount")] + files_count: Option<usize>, + /// Lines added + #[serde(rename = "linesAdded")] + lines_added: Option<usize>, + /// Lines removed + #[serde(rename = "linesRemoved")] + lines_removed: Option<usize>, + /// Base commit SHA for applying the patch + #[serde(rename = "baseCommitSha")] + base_commit_sha: Option<String>, + /// Error message if failed + error: Option<String>, + }, + + /// Response to GetWorktreeInfo command. + WorktreeInfo { + #[serde(rename = "taskId")] + task_id: Uuid, + success: bool, + /// Worktree path + path: Option<String>, + /// Current branch name + branch: Option<String>, + /// Base commit SHA + #[serde(rename = "baseCommit")] + base_commit: Option<String>, + /// List of changed files + #[serde(rename = "filesChanged")] + files_changed: Option<Vec<ChangedFileInfo>>, + /// Whether there are uncommitted changes + #[serde(rename = "hasUncommittedChanges")] + has_uncommitted_changes: Option<bool>, + /// Error message if failed + error: Option<String>, + }, +} + +/// Information about a changed file in a worktree. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChangedFileInfo { + /// File path relative to worktree root + pub path: String, + /// Status: "added", "modified", "deleted" + pub status: String, + /// Lines added + #[serde(rename = "linesAdded")] + pub lines_added: i32, + /// Lines removed + #[serde(rename = "linesRemoved")] + pub lines_removed: i32, } /// Information about a branch (used in BranchList message). @@ -654,6 +722,25 @@ pub enum DaemonCommand { source_dir: Option<String>, }, + // ========================================================================= + // Export Patch Commands + // ========================================================================= + + /// Create an export patch for a task's changes. + CreateExportPatch { + #[serde(rename = "taskId")] + task_id: Uuid, + /// Contract ID for creating the patch file. + #[serde(rename = "contractId")] + contract_id: Uuid, + }, + + /// Get worktree information for a task. + GetWorktreeInfo { + #[serde(rename = "taskId")] + task_id: Uuid, + }, + /// Error response. Error { code: String, diff --git a/makima/src/server/handlers/mesh.rs b/makima/src/server/handlers/mesh.rs index 3d64eb4..aa63e1c 100644 --- a/makima/src/server/handlers/mesh.rs +++ b/makima/src/server/handlers/mesh.rs @@ -3716,3 +3716,614 @@ pub async fn restart_daemon( }) .into_response() } + +// ============================================================================= +// Manual Git Action Endpoints +// ============================================================================= + +/// Request for exporting a patch. +#[derive(Debug, serde::Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ExportPatchRequest { + /// Optional custom name for the patch file. + pub file_name: Option<String>, +} + +/// Response for export patch request. +#[derive(Debug, serde::Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ExportPatchResponse { + /// Whether the export command was sent successfully. + pub success: bool, + /// The task ID. + pub task_id: Uuid, + /// Message describing the result. + pub message: String, +} + +/// Request for pushing a branch. +#[derive(Debug, serde::Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct PushBranchRequest { + /// Target repository path (defaults to task's target_repo_path). + pub target_repo_path: Option<String>, + /// Branch name to push to (defaults to task branch name). + pub branch_name: Option<String>, +} + +/// Response for push branch request. +#[derive(Debug, serde::Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct PushBranchResponse { + /// Whether the push command was sent successfully. + pub success: bool, + /// The task ID. + pub task_id: Uuid, + /// Message describing the result. + pub message: String, +} + +/// Request for creating a pull request. +#[derive(Debug, serde::Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct CreatePRRequest { + /// Optional custom PR title (defaults to task name). + pub title: Option<String>, + /// Optional custom PR body. + pub body: Option<String>, + /// Base branch for the PR (defaults to main). + pub base_branch: Option<String>, +} + +/// Response for create PR request. +#[derive(Debug, serde::Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct CreatePRResponse { + /// Whether the PR command was sent successfully. + pub success: bool, + /// The task ID. + pub task_id: Uuid, + /// Message describing the result. + pub message: String, +} + +/// Information about a changed file. +#[derive(Debug, serde::Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ChangedFile { + /// File path relative to worktree root. + pub path: String, + /// Status: "added", "modified", "deleted". + pub status: String, + /// Lines added. + pub lines_added: i32, + /// Lines removed. + pub lines_removed: i32, +} + +/// Response for worktree info request. +#[derive(Debug, serde::Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct WorktreeInfoResponse { + /// Whether the request was successful. + pub success: bool, + /// The task ID. + pub task_id: Uuid, + /// Worktree path (if known locally, otherwise message about async response). + pub path: Option<String>, + /// Current branch name. + pub branch: Option<String>, + /// Base commit SHA. + pub base_commit: Option<String>, + /// List of changed files. + pub files_changed: Option<Vec<ChangedFile>>, + /// Whether there are uncommitted changes. + pub has_uncommitted_changes: Option<bool>, + /// Message describing the result. + pub message: String, +} + +/// Export a patch for a task's changes. +/// +/// POST /api/v1/mesh/tasks/{id}/export-patch +/// +/// Generates a git diff patch for the task's worktree changes and creates +/// a contract file containing the patch with apply instructions. +#[utoipa::path( + post, + path = "/api/v1/mesh/tasks/{id}/export-patch", + params( + ("id" = Uuid, Path, description = "Task ID") + ), + request_body = ExportPatchRequest, + responses( + (status = 202, description = "Export patch command sent", body = ExportPatchResponse), + (status = 400, description = "Invalid request (task has no contract)", body = ApiError), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Task not found", body = ApiError), + (status = 503, description = "Database not configured or no daemon connected", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Mesh" +)] +pub async fn export_patch( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path(id): Path<Uuid>, + Json(_req): Json<ExportPatchRequest>, +) -> 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(); + } + }; + + // Task must have a contract to create a patch file + let contract_id = match task.contract_id { + Some(cid) => cid, + None => { + return ( + StatusCode::BAD_REQUEST, + Json(ApiError::new( + "NO_CONTRACT", + "Task must be associated with a contract to export a patch file", + )), + ) + .into_response(); + } + }; + + // Find daemon to execute the command + let daemon_id = match task.daemon_id { + Some(did) if state.is_daemon_connected(did) => did, + _ => { + // Try to find any connected daemon for this owner + match state.get_any_daemon_for_owner(auth.owner_id) { + Some(daemon_id) => daemon_id, + None => { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new( + "NO_DAEMON", + "No daemons connected for your account", + )), + ) + .into_response(); + } + } + } + }; + + // Send CreateExportPatch command to daemon + let command = DaemonCommand::CreateExportPatch { + task_id: id, + contract_id, + }; + + if let Err(e) = state.send_daemon_command(daemon_id, command).await { + tracing::error!("Failed to send CreateExportPatch command: {}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DAEMON_ERROR", e)), + ) + .into_response(); + } + + tracing::info!( + task_id = %id, + contract_id = %contract_id, + "Export patch command sent" + ); + + ( + StatusCode::ACCEPTED, + Json(ExportPatchResponse { + success: true, + task_id: id, + message: "Export patch command sent. The patch file will be created when ready.".to_string(), + }), + ) + .into_response() +} + +/// Push a task's branch to the target repository. +/// +/// POST /api/v1/mesh/tasks/{id}/push-branch +/// +/// Pushes the task's worktree changes to the target repository as a branch. +/// This reuses the existing RetryCompletionAction mechanism with action="branch". +#[utoipa::path( + post, + path = "/api/v1/mesh/tasks/{id}/push-branch", + params( + ("id" = Uuid, Path, description = "Task ID") + ), + request_body = PushBranchRequest, + responses( + (status = 200, description = "Push branch command sent", body = PushBranchResponse), + (status = 400, description = "Invalid request", body = ApiError), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Task not found", body = ApiError), + (status = 503, description = "Database not configured or no daemon connected", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Mesh" +)] +pub async fn push_branch( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path(id): Path<Uuid>, + Json(req): Json<PushBranchRequest>, +) -> 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(); + } + }; + + // Determine target repo path + let target_repo_path = match req.target_repo_path.or_else(|| task.target_repo_path.clone()) { + Some(path) if !path.is_empty() => path, + _ => { + return ( + StatusCode::BAD_REQUEST, + Json(ApiError::new( + "NO_TARGET_REPO", + "Target repository path must be specified or set on the task", + )), + ) + .into_response(); + } + }; + + // Find daemon to execute the command + let daemon_id = match task.daemon_id { + Some(did) if state.is_daemon_connected(did) => did, + _ => { + match state.get_any_daemon_for_owner(auth.owner_id) { + Some(daemon_id) => daemon_id, + None => { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new( + "NO_DAEMON", + "No daemons connected for your account", + )), + ) + .into_response(); + } + } + } + }; + + // Send RetryCompletionAction command with action="branch" + let command = DaemonCommand::RetryCompletionAction { + task_id: id, + task_name: task.name.clone(), + action: "branch".to_string(), + target_repo_path: target_repo_path.clone(), + target_branch: req.branch_name.or_else(|| task.target_branch.clone()), + }; + + if let Err(e) = state.send_daemon_command(daemon_id, command).await { + tracing::error!("Failed to send push branch command: {}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DAEMON_ERROR", e)), + ) + .into_response(); + } + + tracing::info!( + task_id = %id, + target_repo = %target_repo_path, + "Push branch command sent" + ); + + ( + StatusCode::OK, + Json(PushBranchResponse { + success: true, + task_id: id, + message: "Push branch command sent. Check task output for results.".to_string(), + }), + ) + .into_response() +} + +/// Create a pull request for a task's changes. +/// +/// POST /api/v1/mesh/tasks/{id}/create-pr +/// +/// Creates a pull request for the task's worktree changes. +/// This reuses the existing RetryCompletionAction mechanism with action="pr". +#[utoipa::path( + post, + path = "/api/v1/mesh/tasks/{id}/create-pr", + params( + ("id" = Uuid, Path, description = "Task ID") + ), + request_body = CreatePRRequest, + responses( + (status = 200, description = "Create PR command sent", body = CreatePRResponse), + (status = 400, description = "Invalid request", body = ApiError), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Task not found", body = ApiError), + (status = 503, description = "Database not configured or no daemon connected", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Mesh" +)] +pub async fn create_pr( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path(id): Path<Uuid>, + Json(req): Json<CreatePRRequest>, +) -> 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(); + } + }; + + // Determine target repo path + let target_repo_path = match task.target_repo_path.clone() { + Some(path) if !path.is_empty() => path, + _ => { + return ( + StatusCode::BAD_REQUEST, + Json(ApiError::new( + "NO_TARGET_REPO", + "Target repository path must be set on the task", + )), + ) + .into_response(); + } + }; + + // Find daemon to execute the command + let daemon_id = match task.daemon_id { + Some(did) if state.is_daemon_connected(did) => did, + _ => { + match state.get_any_daemon_for_owner(auth.owner_id) { + Some(daemon_id) => daemon_id, + None => { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new( + "NO_DAEMON", + "No daemons connected for your account", + )), + ) + .into_response(); + } + } + } + }; + + // Determine target branch for PR + let target_branch = req.base_branch.or_else(|| task.target_branch.clone()); + + // Send RetryCompletionAction command with action="pr" + let command = DaemonCommand::RetryCompletionAction { + task_id: id, + task_name: req.title.unwrap_or_else(|| task.name.clone()), + action: "pr".to_string(), + target_repo_path: target_repo_path.clone(), + target_branch, + }; + + if let Err(e) = state.send_daemon_command(daemon_id, command).await { + tracing::error!("Failed to send create PR command: {}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DAEMON_ERROR", e)), + ) + .into_response(); + } + + tracing::info!( + task_id = %id, + target_repo = %target_repo_path, + "Create PR command sent" + ); + + ( + StatusCode::OK, + Json(CreatePRResponse { + success: true, + task_id: id, + message: "Create PR command sent. Check task output for results.".to_string(), + }), + ) + .into_response() +} + +/// Get worktree information for a task. +/// +/// GET /api/v1/mesh/tasks/{id}/worktree-info +/// +/// Returns information about the task's worktree including changed files, +/// current branch, and whether there are uncommitted changes. +#[utoipa::path( + get, + path = "/api/v1/mesh/tasks/{id}/worktree-info", + params( + ("id" = Uuid, Path, description = "Task ID") + ), + responses( + (status = 202, description = "Worktree info command sent", body = WorktreeInfoResponse), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Task not found", body = ApiError), + (status = 503, description = "Database not configured or no daemon 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(); + } + }; + + // Find daemon to execute the command + let daemon_id = match task.daemon_id { + Some(did) if state.is_daemon_connected(did) => did, + _ => { + match state.get_any_daemon_for_owner(auth.owner_id) { + Some(daemon_id) => daemon_id, + None => { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new( + "NO_DAEMON", + "No daemons connected for your account", + )), + ) + .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(); + } + + tracing::info!( + task_id = %id, + "Get worktree info command sent" + ); + + ( + StatusCode::ACCEPTED, + Json(WorktreeInfoResponse { + success: true, + task_id: id, + path: None, + branch: None, + base_commit: None, + files_changed: None, + has_uncommitted_changes: None, + message: "Worktree info command sent. Response will be streamed via task events.".to_string(), + }), + ) + .into_response() +} diff --git a/makima/src/server/handlers/mesh_daemon.rs b/makima/src/server/handlers/mesh_daemon.rs index f5a3c10..84c7a93 100644 --- a/makima/src/server/handlers/mesh_daemon.rs +++ b/makima/src/server/handlers/mesh_daemon.rs @@ -470,6 +470,68 @@ pub enum DaemonMessage { #[serde(rename = "prNumber")] pr_number: Option<i32>, }, + /// Response to CreateExportPatch command + ExportPatchCreated { + #[serde(rename = "taskId")] + task_id: Uuid, + #[serde(rename = "contractId")] + contract_id: Uuid, + success: bool, + /// The patch content (uncompressed git diff) + #[serde(rename = "patchContent")] + patch_content: Option<String>, + /// Number of files in the patch + #[serde(rename = "filesCount")] + files_count: Option<usize>, + /// Lines added + #[serde(rename = "linesAdded")] + lines_added: Option<usize>, + /// Lines removed + #[serde(rename = "linesRemoved")] + lines_removed: Option<usize>, + /// Base commit SHA for applying the patch + #[serde(rename = "baseCommitSha")] + base_commit_sha: Option<String>, + /// Error message if failed + error: Option<String>, + }, + /// Response to GetWorktreeInfo command + WorktreeInfo { + #[serde(rename = "taskId")] + task_id: Uuid, + success: bool, + /// Worktree path + path: Option<String>, + /// Current branch name + branch: Option<String>, + /// Base commit SHA + #[serde(rename = "baseCommit")] + base_commit: Option<String>, + /// List of changed files + #[serde(rename = "filesChanged")] + files_changed: Option<Vec<ChangedFileInfo>>, + /// Whether there are uncommitted changes + #[serde(rename = "hasUncommittedChanges")] + has_uncommitted_changes: Option<bool>, + /// Error message if failed + error: Option<String>, + }, +} + +/// Information about a changed file in a worktree. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChangedFileInfo { + /// File path relative to worktree root + pub path: String, + /// Status: "added", "modified", "deleted" + pub status: String, + /// Lines added + #[serde(rename = "linesAdded")] + pub lines_added: i32, + /// Lines removed + #[serde(rename = "linesRemoved")] + pub lines_removed: i32, } /// Validated daemon authentication result. diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs index 75f64c6..f6745c0 100644 --- a/makima/src/server/mod.rs +++ b/makima/src/server/mod.rs @@ -84,6 +84,11 @@ pub fn make_router(state: SharedState) -> Router { .route("/mesh/tasks/{id}/check-target", post(mesh::check_target_exists)) .route("/mesh/tasks/{id}/reassign", post(mesh::reassign_task)) .route("/mesh/tasks/{id}/continue", post(mesh::continue_task)) + // Manual git action endpoints + .route("/mesh/tasks/{id}/export-patch", post(mesh::export_patch)) + .route("/mesh/tasks/{id}/push-branch", post(mesh::push_branch)) + .route("/mesh/tasks/{id}/create-pr", post(mesh::create_pr)) + .route("/mesh/tasks/{id}/worktree-info", get(mesh::get_worktree_info)) .route("/mesh/chat", post(mesh_chat::mesh_toplevel_chat_handler)) .route( "/mesh/chat/history", diff --git a/makima/src/server/state.rs b/makima/src/server/state.rs index b954efe..81adc83 100644 --- a/makima/src/server/state.rs +++ b/makima/src/server/state.rs @@ -442,6 +442,25 @@ pub enum DaemonCommand { source_dir: Option<String>, }, + // ========================================================================= + // Export Patch Commands + // ========================================================================= + + /// Create an export patch for a task's changes + CreateExportPatch { + #[serde(rename = "taskId")] + task_id: Uuid, + /// Contract ID for creating the patch file + #[serde(rename = "contractId")] + contract_id: Uuid, + }, + + /// Get worktree information for a task + GetWorktreeInfo { + #[serde(rename = "taskId")] + task_id: Uuid, + }, + /// Error response Error { code: String, message: String }, @@ -1003,6 +1022,15 @@ impl AppState { .any(|entry| entry.value().id == daemon_id) } + /// Get any connected daemon for a specific owner. + /// Returns the daemon ID if found. + pub fn get_any_daemon_for_owner(&self, owner_id: Uuid) -> Option<Uuid> { + self.daemon_connections + .iter() + .find(|entry| entry.value().owner_id == owner_id) + .map(|entry| entry.value().id) + } + // ========================================================================= // Tool Key Management // ========================================================================= |
