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 */}