summaryrefslogtreecommitdiff
path: root/makima/src/db/models.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-06 04:08:11 +0000
committersoryu <soryu@soryu.co>2026-01-11 03:01:13 +0000
commit8b17a175c3e7e27b789812eba4e3cd760beadb10 (patch)
tree7864dcaa2fa9db47fdfd4e8bfdb0b1dde832aa33 /makima/src/db/models.rs
parentf79c416c58557d2f946aa5332989afdfa8c021cd (diff)
downloadsoryu-8b17a175c3e7e27b789812eba4e3cd760beadb10.tar.gz
soryu-8b17a175c3e7e27b789812eba4e3cd760beadb10.zip
Initial Control system
Diffstat (limited to 'makima/src/db/models.rs')
-rw-r--r--makima/src/db/models.rs589
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,
+}