From 8b17a175c3e7e27b789812eba4e3cd760beadb10 Mon Sep 17 00:00:00 2001 From: soryu Date: Tue, 6 Jan 2026 04:08:11 +0000 Subject: Initial Control system --- makima/frontend/src/components/mesh/PRPreview.tsx | 314 ++++++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 makima/frontend/src/components/mesh/PRPreview.tsx (limited to 'makima/frontend/src/components/mesh/PRPreview.tsx') diff --git a/makima/frontend/src/components/mesh/PRPreview.tsx b/makima/frontend/src/components/mesh/PRPreview.tsx new file mode 100644 index 0000000..fc202b0 --- /dev/null +++ b/makima/frontend/src/components/mesh/PRPreview.tsx @@ -0,0 +1,314 @@ +import { useState, useMemo } from "react"; +import type { TaskWithSubtasks, TaskSummary } from "../../lib/api"; +import { OverlayDiffViewer } from "./OverlayDiffViewer"; + +interface PRPreviewProps { + task: TaskWithSubtasks; + diff?: string; + changedFiles?: string[]; + loading?: boolean; + onCreatePR?: (title: string, body: string, draft: boolean) => Promise; + onAutoMerge?: () => Promise; + onClose: () => void; +} + +interface PRFormData { + title: string; + body: string; + isDraft: boolean; +} + +function generatePRTitle(task: TaskWithSubtasks): string { + // Generate a PR title based on the task name + const prefix = task.parentTaskId ? "feat" : "feat"; + return `${prefix}: ${task.name}`; +} + +function generatePRBody(task: TaskWithSubtasks, changedFiles?: string[]): string { + const sections: string[] = []; + + // Summary + sections.push("## Summary\n"); + if (task.description) { + sections.push(task.description + "\n"); + } else { + sections.push("_Add a brief description of the changes..._\n"); + } + + // Plan/Implementation details + sections.push("\n## Implementation\n"); + if (task.plan) { + // Truncate if too long + const planPreview = task.plan.length > 500 + ? task.plan.substring(0, 500) + "..." + : task.plan; + sections.push("```\n" + planPreview + "\n```\n"); + } + + // Subtasks summary + if (task.subtasks.length > 0) { + sections.push("\n## Subtasks\n"); + task.subtasks.forEach((subtask: TaskSummary) => { + const emoji = subtask.status === "done" || subtask.status === "merged" ? "+" : + subtask.status === "running" ? "~" : "-"; + sections.push(`- [${emoji === "+" ? "x" : " "}] ${subtask.name} (${subtask.status})\n`); + }); + } + + // Changed files + if (changedFiles && changedFiles.length > 0) { + sections.push("\n## Changed Files\n"); + changedFiles.slice(0, 20).forEach((file) => { + sections.push(`- \`${file}\`\n`); + }); + if (changedFiles.length > 20) { + sections.push(`\n_...and ${changedFiles.length - 20} more files_\n`); + } + } + + // Test plan + sections.push("\n## Test Plan\n"); + sections.push("- [ ] Manual testing completed\n"); + sections.push("- [ ] Unit tests added/updated\n"); + sections.push("- [ ] Integration tests passing\n"); + + // Footer + sections.push("\n---\n"); + sections.push("_Generated by makima mesh orchestrator_\n"); + + return sections.join(""); +} + +export function PRPreview({ + task, + diff = "", + changedFiles = [], + loading = false, + onCreatePR, + onAutoMerge, + onClose, +}: PRPreviewProps) { + const [showDiff, setShowDiff] = useState(false); + const [creating, setCreating] = useState(false); + const [error, setError] = useState(null); + + const [formData, setFormData] = useState(() => ({ + title: generatePRTitle(task), + body: generatePRBody(task, changedFiles), + isDraft: false, + })); + + const handleCreatePR = async () => { + if (!onCreatePR || creating) return; + + setCreating(true); + setError(null); + + try { + await onCreatePR(formData.title, formData.body, formData.isDraft); + onClose(); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to create PR"); + } finally { + setCreating(false); + } + }; + + const handleAutoMerge = async () => { + if (!onAutoMerge || creating) return; + + if (!confirm("Are you sure you want to auto-merge this task directly to the target branch?")) { + return; + } + + setCreating(true); + setError(null); + + try { + await onAutoMerge(); + onClose(); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to auto-merge"); + } finally { + setCreating(false); + } + }; + + // Calculate stats + const stats = useMemo(() => { + const completedSubtasks = task.subtasks.filter( + (s) => s.status === "done" || s.status === "merged" + ).length; + return { + filesChanged: changedFiles.length, + subtasksCompleted: completedSubtasks, + subtasksTotal: task.subtasks.length, + isReady: completedSubtasks === task.subtasks.length || task.subtasks.length === 0, + }; + }, [task.subtasks, changedFiles]); + + return ( +
+ {/* Header */} +
+
+ Create Pull Request +
+ +
+ + {/* Content */} +
+ {/* Status badges */} +
+ + {task.baseBranch || "main"} → {task.targetBranch || task.baseBranch || "main"} + + + {stats.filesChanged} files changed + + {task.subtasks.length > 0 && ( + + {stats.subtasksCompleted}/{stats.subtasksTotal} subtasks complete + + )} +
+ + {/* Warning if subtasks not complete */} + {!stats.isReady && ( +
+ Some subtasks are not yet complete. Consider waiting before creating the PR. +
+ )} + + {/* Error message */} + {error && ( +
+ {error} +
+ )} + + {/* PR Title */} +
+ + setFormData({ ...formData, title: e.target.value })} + className="w-full bg-transparent border border-[rgba(117,170,252,0.25)] text-[#dbe7ff] font-mono text-sm px-3 py-2 outline-none focus:border-[#3f6fb3]" + placeholder="PR title" + disabled={creating} + /> +
+ + {/* PR Body */} +
+
+ + +
+