//! Database models for the files table. use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::FromRow; use utoipa::ToSchema; use uuid::Uuid; /// TranscriptEntry stored in JSONB - matches frontend TranscriptEntry #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct TranscriptEntry { pub id: String, pub speaker: String, pub start: f32, pub end: f32, pub text: String, pub is_final: bool, } /// Chart type for visualization elements #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "lowercase")] pub enum ChartType { Line, Bar, Pie, Area, } /// Body element types for structured file content #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(tag = "type", rename_all = "camelCase")] pub enum BodyElement { /// Heading element (h1-h6) Heading { level: u8, text: String }, /// Paragraph text Paragraph { text: String }, /// Code block with optional language Code { language: Option, content: String, }, /// List (ordered or unordered) List { ordered: bool, items: Vec, }, /// Chart visualization Chart { #[serde(rename = "chartType")] chart_type: ChartType, title: Option, data: serde_json::Value, config: Option, }, /// Image element (deferred for MVP) Image { src: String, alt: Option, caption: Option, }, } /// File record from the database. #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct File { pub id: Uuid, pub owner_id: Uuid, pub name: String, pub description: Option, #[sqlx(json)] pub transcript: Vec, pub location: Option, /// AI-generated summary of the transcript pub summary: Option, /// Structured body content (headings, paragraphs, charts) #[sqlx(json)] pub body: Vec, /// Version number for optimistic locking pub version: i32, pub created_at: DateTime, pub updated_at: DateTime, } /// Request payload for creating a new file. #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct CreateFileRequest { /// Name of the file (auto-generated if not provided) pub name: Option, /// Optional description pub description: Option, /// Transcript entries pub transcript: Vec, /// Storage location (e.g., s3://bucket/path) - not used yet pub location: Option, } /// Request payload for updating an existing file. #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct UpdateFileRequest { /// New name (optional) pub name: Option, /// New description (optional) pub description: Option, /// New transcript (optional) pub transcript: Option>, /// AI-generated summary (optional) pub summary: Option, /// Structured body content (optional) pub body: Option>, /// Version for optimistic locking (required for updates from frontend) pub version: Option, } /// Response for file list endpoint. #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct FileListResponse { pub files: Vec, pub total: i64, } /// Summary of a file for list views (excludes full transcript). #[derive(Debug, Clone, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct FileSummary { pub id: Uuid, pub name: String, pub description: Option, pub transcript_count: usize, /// Duration derived from last transcript end time pub duration: Option, /// Version number for optimistic locking pub version: i32, pub created_at: DateTime, pub updated_at: DateTime, } impl From for FileSummary { fn from(file: File) -> Self { let duration = file .transcript .iter() .map(|t| t.end) .fold(0.0_f32, f32::max); Self { id: file.id, name: file.name, description: file.description, transcript_count: file.transcript.len(), duration: if duration > 0.0 { Some(duration) } else { None }, version: file.version, created_at: file.created_at, updated_at: file.updated_at, } } } // ============================================================================= // Version History Types // ============================================================================= /// Source of a version change #[derive(Debug, Clone, Serialize, Deserialize, ToSchema, sqlx::Type)] #[sqlx(type_name = "varchar")] #[serde(rename_all = "lowercase")] pub enum VersionSource { #[sqlx(rename = "user")] User, #[sqlx(rename = "llm")] Llm, #[sqlx(rename = "system")] System, } impl std::fmt::Display for VersionSource { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { VersionSource::User => write!(f, "user"), VersionSource::Llm => write!(f, "llm"), VersionSource::System => write!(f, "system"), } } } impl std::str::FromStr for VersionSource { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "user" => Ok(VersionSource::User), "llm" => Ok(VersionSource::Llm), "system" => Ok(VersionSource::System), _ => Err(format!("Unknown version source: {}", s)), } } } /// Full version record from the database #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct FileVersion { pub id: Uuid, pub file_id: Uuid, pub version: i32, pub name: String, pub description: Option, pub summary: Option, #[sqlx(json)] pub body: Vec, pub source: String, pub change_description: Option, pub created_at: DateTime, } /// Summary of a version for list views #[derive(Debug, Clone, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct FileVersionSummary { pub version: i32, pub source: String, pub created_at: DateTime, pub change_description: Option, } impl From for FileVersionSummary { fn from(v: FileVersion) -> Self { Self { version: v.version, source: v.source, created_at: v.created_at, change_description: v.change_description, } } } /// Response for version list endpoint #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct FileVersionListResponse { pub versions: Vec, pub total: i64, } /// Request to restore a file to a previous version #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct RestoreVersionRequest { /// The version to restore to pub target_version: i32, /// 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 { 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 { 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, /// Depth in task hierarchy: 0=orchestrator (top-level), 1=subtask (max) pub depth: i32, pub name: String, pub description: Option, pub status: String, pub priority: i32, pub plan: String, // Daemon/container info pub daemon_id: Option, pub container_id: Option, pub overlay_path: Option, // Repository info pub repository_url: Option, pub base_branch: Option, pub target_branch: Option, // Merge settings pub merge_mode: Option, pub pr_url: Option, // Completion action settings /// Path to user's local repository (outside ~/.makima) pub target_repo_path: Option, /// Action on completion: "none", "branch", "merge", "pr" pub completion_action: Option, // Progress tracking pub progress_summary: Option, pub last_output: Option, pub error_message: Option, // Timestamps pub started_at: Option>, pub completed_at: Option>, pub version: i32, pub created_at: DateTime, pub updated_at: DateTime, // 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, /// Files to copy from parent task's worktree when starting. #[serde(skip_serializing_if = "Option::is_none")] pub copy_files: Option, } impl Task { /// Parse status string to TaskStatus enum pub fn status_enum(&self) -> Result { self.status.parse() } /// Parse merge_mode string to MergeMode enum pub fn merge_mode_enum(&self) -> Option> { 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, /// 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, pub subtask_count: i64, pub version: i32, pub created_at: DateTime, pub updated_at: DateTime, } /// Response for task list endpoint #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct TaskListResponse { pub tasks: Vec, 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, /// The plan/instructions for Claude Code pub plan: String, /// Parent task ID (for subtasks) pub parent_task_id: Option, /// Priority (higher = more urgent) #[serde(default)] pub priority: i32, /// Repository URL pub repository_url: Option, /// Base branch for overlay pub base_branch: Option, /// Target branch to merge into pub target_branch: Option, /// Merge mode (pr, auto, manual) pub merge_mode: Option, /// Path to user's local repository (outside ~/.makima) pub target_repo_path: Option, /// Action on completion: "none", "branch", "merge", "pr" pub completion_action: Option, /// Task ID to continue from (copy worktree from this task when starting) pub continue_from_task_id: Option, /// Files to copy from parent task's worktree when starting #[serde(skip_serializing_if = "Option::is_none")] pub copy_files: Option>, } /// Request payload for updating a task #[derive(Debug, Default, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct UpdateTaskRequest { pub name: Option, pub description: Option, pub plan: Option, pub status: Option, pub priority: Option, pub progress_summary: Option, pub last_output: Option, pub error_message: Option, pub merge_mode: Option, pub pr_url: Option, /// Path to user's local repository (outside ~/.makima) pub target_repo_path: Option, /// Action on completion: "none", "branch", "merge", "pr" pub completion_action: Option, /// The daemon currently running this task pub daemon_id: Option, /// Explicitly clear daemon_id (set to NULL) #[serde(default)] pub clear_daemon_id: bool, /// Version for optimistic locking pub version: Option, } /// 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, } /// 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 { 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, pub machine_id: Option, pub max_concurrent_tasks: i32, pub current_task_count: i32, pub status: String, pub last_heartbeat_at: DateTime, pub connected_at: DateTime, pub disconnected_at: Option>, } impl Daemon { /// Parse status string to DaemonStatus enum pub fn status_enum(&self) -> Result { self.status.parse() } } /// Response for daemon list endpoint #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct DaemonListResponse { pub daemons: Vec, 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, } /// 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, /// Whether the directory already exists (for validation) #[serde(skip_serializing_if = "Option::is_none")] pub exists: Option, } // ============================================================================= // 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, pub new_status: Option, #[sqlx(json)] pub event_data: Option, pub created_at: DateTime, } /// Response for task events list endpoint #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct TaskEventListResponse { pub events: Vec, 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, /// Tool input JSON if tool_use message #[serde(skip_serializing_if = "Option::is_none")] pub tool_input: Option, /// Whether tool result was an error #[serde(skip_serializing_if = "Option::is_none")] pub is_error: Option, /// Cost in USD if result message #[serde(skip_serializing_if = "Option::is_none")] pub cost_usd: Option, /// Duration in ms if result message #[serde(skip_serializing_if = "Option::is_none")] pub duration_ms: Option, /// Timestamp when this output was recorded pub created_at: DateTime, } impl TaskOutputEntry { /// Convert a TaskEvent with event_type='output' to a TaskOutputEntry pub fn from_task_event(event: TaskEvent) -> Option { 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, 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, pub is_active: bool, pub created_at: DateTime, pub updated_at: DateTime, } /// 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, /// Tool calls made during this message (JSON, nullable) pub tool_calls: Option, /// Pending questions requiring user response (JSON, nullable) pub pending_questions: Option, pub created_at: DateTime, } /// Response for chat history endpoint #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct MeshChatHistoryResponse { pub conversation_id: Uuid, pub messages: Vec, } // ============================================================================= // 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, /// 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, } /// 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, /// Files with unresolved conflicts pub conflicted_files: Vec, } /// 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, /// Conflicted files (if conflicts occurred) pub conflicts: Option>, } /// 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, /// Count of merged branches pub merged_count: u32, /// Count of skipped branches pub skipped_count: u32, }