diff options
Diffstat (limited to 'makima/src/db/models.rs')
| -rw-r--r-- | makima/src/db/models.rs | 589 |
1 files changed, 589 insertions, 0 deletions
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index 617e590..5064b97 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -36,6 +36,16 @@ pub enum BodyElement { Heading { level: u8, text: String }, /// Paragraph text Paragraph { text: String }, + /// Code block with optional language + Code { + language: Option<String>, + content: String, + }, + /// List (ordered or unordered) + List { + ordered: bool, + items: Vec<String>, + }, /// Chart visualization Chart { #[serde(rename = "chartType")] @@ -245,3 +255,582 @@ pub struct RestoreVersionRequest { /// The current version (for optimistic locking) pub current_version: i32, } + +// ============================================================================= +// Mesh/Task Types +// ============================================================================= + +/// Task status for orchestrating Claude Code instances +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "lowercase")] +pub enum TaskStatus { + Pending, + Running, + Paused, + Blocked, + Done, + Failed, + Merged, +} + +impl std::fmt::Display for TaskStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TaskStatus::Pending => write!(f, "pending"), + TaskStatus::Running => write!(f, "running"), + TaskStatus::Paused => write!(f, "paused"), + TaskStatus::Blocked => write!(f, "blocked"), + TaskStatus::Done => write!(f, "done"), + TaskStatus::Failed => write!(f, "failed"), + TaskStatus::Merged => write!(f, "merged"), + } + } +} + +impl std::str::FromStr for TaskStatus { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s.to_lowercase().as_str() { + "pending" => Ok(TaskStatus::Pending), + "running" => Ok(TaskStatus::Running), + "paused" => Ok(TaskStatus::Paused), + "blocked" => Ok(TaskStatus::Blocked), + "done" => Ok(TaskStatus::Done), + "failed" => Ok(TaskStatus::Failed), + "merged" => Ok(TaskStatus::Merged), + _ => Err(format!("Unknown task status: {}", s)), + } + } +} + +/// Merge mode for task completion +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "lowercase")] +pub enum MergeMode { + /// Create a PR for review + Pr, + /// Auto-merge to target branch + Auto, + /// Manual merge by user + Manual, +} + +impl std::fmt::Display for MergeMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MergeMode::Pr => write!(f, "pr"), + MergeMode::Auto => write!(f, "auto"), + MergeMode::Manual => write!(f, "manual"), + } + } +} + +impl std::str::FromStr for MergeMode { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s.to_lowercase().as_str() { + "pr" => Ok(MergeMode::Pr), + "auto" => Ok(MergeMode::Auto), + "manual" => Ok(MergeMode::Manual), + _ => Err(format!("Unknown merge mode: {}", s)), + } + } +} + +/// Task record from the database +#[derive(Debug, Clone, FromRow, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct Task { + pub id: Uuid, + pub owner_id: Uuid, + pub parent_task_id: Option<Uuid>, + /// Depth in task hierarchy: 0=orchestrator (top-level), 1=subtask (max) + pub depth: i32, + pub name: String, + pub description: Option<String>, + pub status: String, + pub priority: i32, + pub plan: String, + + // Daemon/container info + pub daemon_id: Option<Uuid>, + pub container_id: Option<String>, + pub overlay_path: Option<String>, + + // Repository info + pub repository_url: Option<String>, + pub base_branch: Option<String>, + pub target_branch: Option<String>, + + // Merge settings + pub merge_mode: Option<String>, + pub pr_url: Option<String>, + + // Completion action settings + /// Path to user's local repository (outside ~/.makima) + pub target_repo_path: Option<String>, + /// Action on completion: "none", "branch", "merge", "pr" + pub completion_action: Option<String>, + + // Progress tracking + pub progress_summary: Option<String>, + pub last_output: Option<String>, + pub error_message: Option<String>, + + // Timestamps + pub started_at: Option<DateTime<Utc>>, + pub completed_at: Option<DateTime<Utc>>, + pub version: i32, + pub created_at: DateTime<Utc>, + pub updated_at: DateTime<Utc>, + + // Task continuation + /// Task ID to continue from (copy worktree from this task when starting). + /// Used for sequential subtask dependencies. + #[serde(skip_serializing_if = "Option::is_none")] + pub continue_from_task_id: Option<Uuid>, + /// Files to copy from parent task's worktree when starting. + #[serde(skip_serializing_if = "Option::is_none")] + pub copy_files: Option<serde_json::Value>, +} + +impl Task { + /// Parse status string to TaskStatus enum + pub fn status_enum(&self) -> Result<TaskStatus, String> { + self.status.parse() + } + + /// Parse merge_mode string to MergeMode enum + pub fn merge_mode_enum(&self) -> Option<Result<MergeMode, String>> { + self.merge_mode.as_ref().map(|s| s.parse()) + } +} + +/// Summary of a task for list views +#[derive(Debug, Clone, FromRow, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct TaskSummary { + pub id: Uuid, + pub parent_task_id: Option<Uuid>, + /// Depth in task hierarchy: 0=orchestrator (top-level), 1=subtask (max) + pub depth: i32, + pub name: String, + pub status: String, + pub priority: i32, + pub progress_summary: Option<String>, + pub subtask_count: i64, + pub version: i32, + pub created_at: DateTime<Utc>, + pub updated_at: DateTime<Utc>, +} + +/// Response for task list endpoint +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct TaskListResponse { + pub tasks: Vec<TaskSummary>, + pub total: i64, +} + +/// Request payload for creating a new task +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct CreateTaskRequest { + /// Name of the task + pub name: String, + /// Optional description + pub description: Option<String>, + /// The plan/instructions for Claude Code + pub plan: String, + /// Parent task ID (for subtasks) + pub parent_task_id: Option<Uuid>, + /// Priority (higher = more urgent) + #[serde(default)] + pub priority: i32, + /// Repository URL + pub repository_url: Option<String>, + /// Base branch for overlay + pub base_branch: Option<String>, + /// Target branch to merge into + pub target_branch: Option<String>, + /// Merge mode (pr, auto, manual) + pub merge_mode: Option<String>, + /// Path to user's local repository (outside ~/.makima) + pub target_repo_path: Option<String>, + /// Action on completion: "none", "branch", "merge", "pr" + pub completion_action: Option<String>, + /// Task ID to continue from (copy worktree from this task when starting) + pub continue_from_task_id: Option<Uuid>, + /// Files to copy from parent task's worktree when starting + #[serde(skip_serializing_if = "Option::is_none")] + pub copy_files: Option<Vec<String>>, +} + +/// Request payload for updating a task +#[derive(Debug, Default, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct UpdateTaskRequest { + pub name: Option<String>, + pub description: Option<String>, + pub plan: Option<String>, + pub status: Option<String>, + pub priority: Option<i32>, + pub progress_summary: Option<String>, + pub last_output: Option<String>, + pub error_message: Option<String>, + pub merge_mode: Option<String>, + pub pr_url: Option<String>, + /// Path to user's local repository (outside ~/.makima) + pub target_repo_path: Option<String>, + /// Action on completion: "none", "branch", "merge", "pr" + pub completion_action: Option<String>, + /// The daemon currently running this task + pub daemon_id: Option<Uuid>, + /// Explicitly clear daemon_id (set to NULL) + #[serde(default)] + pub clear_daemon_id: bool, + /// Version for optimistic locking + pub version: Option<i32>, +} + +/// Task with its subtasks for detail view +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct TaskWithSubtasks { + #[serde(flatten)] + pub task: Task, + pub subtasks: Vec<TaskSummary>, +} + +/// Request to send a message to a running task's stdin. +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct SendMessageRequest { + /// The message to send to the task's stdin. + pub message: String, +} + +// ============================================================================= +// Daemon Types +// ============================================================================= + +/// Daemon status +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "lowercase")] +pub enum DaemonStatus { + Connected, + Disconnected, + Unhealthy, +} + +impl std::fmt::Display for DaemonStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DaemonStatus::Connected => write!(f, "connected"), + DaemonStatus::Disconnected => write!(f, "disconnected"), + DaemonStatus::Unhealthy => write!(f, "unhealthy"), + } + } +} + +impl std::str::FromStr for DaemonStatus { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s.to_lowercase().as_str() { + "connected" => Ok(DaemonStatus::Connected), + "disconnected" => Ok(DaemonStatus::Disconnected), + "unhealthy" => Ok(DaemonStatus::Unhealthy), + _ => Err(format!("Unknown daemon status: {}", s)), + } + } +} + +/// Connected daemon record from the database +#[derive(Debug, Clone, FromRow, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct Daemon { + pub id: Uuid, + pub owner_id: Uuid, + pub connection_id: String, + pub hostname: Option<String>, + pub machine_id: Option<String>, + pub max_concurrent_tasks: i32, + pub current_task_count: i32, + pub status: String, + pub last_heartbeat_at: DateTime<Utc>, + pub connected_at: DateTime<Utc>, + pub disconnected_at: Option<DateTime<Utc>>, +} + +impl Daemon { + /// Parse status string to DaemonStatus enum + pub fn status_enum(&self) -> Result<DaemonStatus, String> { + self.status.parse() + } +} + +/// Response for daemon list endpoint +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DaemonListResponse { + pub daemons: Vec<Daemon>, + pub total: i64, +} + +/// Response for daemon directories endpoint +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DaemonDirectoriesResponse { + /// List of suggested directories from connected daemons + pub directories: Vec<DaemonDirectory>, +} + +/// A suggested directory from a daemon +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DaemonDirectory { + /// Path to the directory + pub path: String, + /// Display label for the directory + pub label: String, + /// Type of directory: "working", "makima", "worktrees" + pub directory_type: String, + /// Daemon hostname this directory is from + pub hostname: Option<String>, + /// Whether the directory already exists (for validation) + #[serde(skip_serializing_if = "Option::is_none")] + pub exists: Option<bool>, +} + +// ============================================================================= +// Task Event Types +// ============================================================================= + +/// Task event record from the database +#[derive(Debug, Clone, FromRow, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct TaskEvent { + pub id: Uuid, + pub task_id: Uuid, + pub event_type: String, + pub previous_status: Option<String>, + pub new_status: Option<String>, + #[sqlx(json)] + pub event_data: Option<serde_json::Value>, + pub created_at: DateTime<Utc>, +} + +/// Response for task events list endpoint +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct TaskEventListResponse { + pub events: Vec<TaskEvent>, + pub total: i64, +} + +/// A single output entry from a Claude Code task +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct TaskOutputEntry { + pub id: Uuid, + pub task_id: Uuid, + /// Message type: "assistant", "tool_use", "tool_result", "result", "system", "error", "raw" + pub message_type: String, + /// Main text content + pub content: String, + /// Tool name if tool_use message + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_name: Option<String>, + /// Tool input JSON if tool_use message + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_input: Option<serde_json::Value>, + /// Whether tool result was an error + #[serde(skip_serializing_if = "Option::is_none")] + pub is_error: Option<bool>, + /// Cost in USD if result message + #[serde(skip_serializing_if = "Option::is_none")] + pub cost_usd: Option<f64>, + /// Duration in ms if result message + #[serde(skip_serializing_if = "Option::is_none")] + pub duration_ms: Option<u64>, + /// Timestamp when this output was recorded + pub created_at: DateTime<Utc>, +} + +impl TaskOutputEntry { + /// Convert a TaskEvent with event_type='output' to a TaskOutputEntry + pub fn from_task_event(event: TaskEvent) -> Option<Self> { + if event.event_type != "output" { + return None; + } + let data = event.event_data?; + Some(Self { + id: event.id, + task_id: event.task_id, + message_type: data.get("messageType")?.as_str()?.to_string(), + content: data.get("content")?.as_str().unwrap_or("").to_string(), + tool_name: data.get("toolName").and_then(|v| v.as_str()).map(|s| s.to_string()), + tool_input: data.get("toolInput").cloned(), + is_error: data.get("isError").and_then(|v| v.as_bool()), + cost_usd: data.get("costUsd").and_then(|v| v.as_f64()), + duration_ms: data.get("durationMs").and_then(|v| v.as_u64()), + created_at: event.created_at, + }) + } +} + +/// Response for task output history endpoint +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct TaskOutputResponse { + pub entries: Vec<TaskOutputEntry>, + pub total: usize, + pub task_id: Uuid, +} + +// ============================================================================= +// Mesh Chat History Types +// ============================================================================= + +/// Mesh chat conversation for persisting history +#[derive(Debug, Clone, FromRow, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct MeshChatConversation { + pub id: Uuid, + pub owner_id: Uuid, + pub name: Option<String>, + pub is_active: bool, + pub created_at: DateTime<Utc>, + pub updated_at: DateTime<Utc>, +} + +/// Individual message in a mesh chat conversation +#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct MeshChatMessageRecord { + pub id: Uuid, + pub conversation_id: Uuid, + pub role: String, + pub content: String, + pub context_type: String, + pub context_task_id: Option<Uuid>, + /// Tool calls made during this message (JSON, nullable) + pub tool_calls: Option<serde_json::Value>, + /// Pending questions requiring user response (JSON, nullable) + pub pending_questions: Option<serde_json::Value>, + pub created_at: DateTime<Utc>, +} + +/// Response for chat history endpoint +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct MeshChatHistoryResponse { + pub conversation_id: Uuid, + pub messages: Vec<MeshChatMessageRecord>, +} + +// ============================================================================= +// Merge API Types +// ============================================================================= + +/// Information about a task branch +#[derive(Debug, Clone, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct BranchInfo { + /// Full branch name + pub name: String, + /// Task ID extracted from branch name (if parseable) + pub task_id: Option<Uuid>, + /// Whether this branch has been merged + pub is_merged: bool, + /// Short SHA of the last commit + pub last_commit: String, + /// Subject line of the last commit + pub last_commit_message: String, +} + +/// Response for branch list endpoint +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct BranchListResponse { + pub branches: Vec<BranchInfo>, +} + +/// Request to start a merge +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct MergeStartRequest { + /// Branch name to merge + pub source_branch: String, +} + +/// Current merge state +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct MergeStatusResponse { + /// Whether a merge is in progress + pub in_progress: bool, + /// Branch being merged (if in progress) + pub source_branch: Option<String>, + /// Files with unresolved conflicts + pub conflicted_files: Vec<String>, +} + +/// Request to resolve a conflict +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct MergeResolveRequest { + /// File path to resolve + pub file: String, + /// Resolution strategy: "ours" or "theirs" + pub strategy: String, +} + +/// Request to commit a merge +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct MergeCommitRequest { + /// Commit message + pub message: String, +} + +/// Request to skip a subtask branch +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct MergeSkipRequest { + /// Subtask ID to skip + pub subtask_id: Uuid, + /// Reason for skipping + pub reason: String, +} + +/// Result of a merge operation +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct MergeResultResponse { + /// Whether the operation succeeded + pub success: bool, + /// Human-readable message + pub message: String, + /// Commit SHA (if a commit was created) + pub commit_sha: Option<String>, + /// Conflicted files (if conflicts occurred) + pub conflicts: Option<Vec<String>>, +} + +/// Response to check if all branches are merged +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct MergeCompleteCheckResponse { + /// Whether the orchestrator can mark itself as complete + pub can_complete: bool, + /// Branches not yet merged or skipped + pub unmerged_branches: Vec<String>, + /// Count of merged branches + pub merged_count: u32, + /// Count of skipped branches + pub skipped_count: u32, +} |
