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<void>;
onAutoMerge?: () => Promise<void>;
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<string | null>(null);
const [formData, setFormData] = useState<PRFormData>(() => ({
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 (
<div className="panel flex flex-col max-h-[80vh] overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-[rgba(117,170,252,0.2)] shrink-0">
<div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase">
Create Pull Request
</div>
<button
onClick={onClose}
className="font-mono text-xs text-[#555] hover:text-[#9bc3ff]"
>
Cancel
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{/* Status badges */}
<div className="flex flex-wrap gap-2">
<span className="px-2 py-0.5 font-mono text-[10px] text-[#75aafc] bg-[rgba(117,170,252,0.1)] border border-[rgba(117,170,252,0.2)]">
{task.baseBranch || "main"} → {task.targetBranch || task.baseBranch || "main"}
</span>
<span className="px-2 py-0.5 font-mono text-[10px] text-[#9bc3ff] bg-[rgba(117,170,252,0.1)] border border-[rgba(117,170,252,0.2)]">
{stats.filesChanged} files changed
</span>
{task.subtasks.length > 0 && (
<span
className={`px-2 py-0.5 font-mono text-[10px] border ${
stats.isReady
? "text-green-400 bg-green-400/10 border-green-400/20"
: "text-yellow-400 bg-yellow-400/10 border-yellow-400/20"
}`}
>
{stats.subtasksCompleted}/{stats.subtasksTotal} subtasks complete
</span>
)}
</div>
{/* Warning if subtasks not complete */}
{!stats.isReady && (
<div className="bg-yellow-400/10 border border-yellow-400/30 p-3 font-mono text-xs text-yellow-400">
Some subtasks are not yet complete. Consider waiting before creating the PR.
</div>
)}
{/* Error message */}
{error && (
<div className="bg-red-400/10 border border-red-400/30 p-3 font-mono text-xs text-red-400">
{error}
</div>
)}
{/* PR Title */}
<div className="space-y-2">
<label className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase">
Title
</label>
<input
type="text"
value={formData.title}
onChange={(e) => 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}
/>
</div>
{/* PR Body */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase">
Description
</label>
<button
onClick={() => setFormData({
...formData,
body: generatePRBody(task, changedFiles),
})}
className="font-mono text-[10px] text-[#555] hover:text-[#9bc3ff]"
>
Regenerate
</button>
</div>
<textarea
value={formData.body}
onChange={(e) => setFormData({ ...formData, body: e.target.value })}
className="w-full bg-transparent border border-[rgba(117,170,252,0.25)] text-[#dbe7ff] font-mono text-xs px-3 py-2 outline-none focus:border-[#3f6fb3] min-h-[200px] resize-y"
placeholder="PR description (markdown)"
disabled={creating}
/>
</div>
{/* Options */}
<div className="flex items-center gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={formData.isDraft}
onChange={(e) => setFormData({ ...formData, isDraft: e.target.checked })}
className="w-4 h-4 accent-[#75aafc]"
disabled={creating}
/>
<span className="font-mono text-xs text-[#9bc3ff]">Create as draft</span>
</label>
</div>
{/* Diff preview toggle */}
<div className="border-t border-[rgba(117,170,252,0.2)] pt-4">
<button
onClick={() => setShowDiff(!showDiff)}
className="flex items-center gap-2 font-mono text-xs text-[#75aafc] hover:text-[#9bc3ff]"
>
<span>{showDiff ? "▼" : "▶"}</span>
<span>
{showDiff ? "Hide" : "Show"} diff preview ({stats.filesChanged} files)
</span>
</button>
</div>
{/* Inline diff viewer */}
{showDiff && (
<div className="border border-[rgba(117,170,252,0.2)]">
<OverlayDiffViewer
diff={diff}
changedFiles={changedFiles}
loading={loading}
title="Changes to be merged"
/>
</div>
)}
</div>
{/* Footer actions */}
<div className="flex items-center justify-between p-4 border-t border-[rgba(117,170,252,0.2)] shrink-0">
<div className="font-mono text-[10px] text-[#555]">
{task.repositoryUrl && (
<span className="truncate max-w-[200px] inline-block align-middle">
{task.repositoryUrl}
</span>
)}
</div>
<div className="flex items-center gap-2">
{task.mergeMode === "auto" && onAutoMerge && (
<button
onClick={handleAutoMerge}
disabled={creating || !stats.isReady}
className="px-4 py-2 font-mono text-xs text-yellow-400 border border-yellow-400/30 hover:border-yellow-400/50 hover:bg-yellow-400/10 disabled:opacity-50 disabled:cursor-not-allowed transition-colors uppercase"
>
{creating ? "..." : "Auto-Merge"}
</button>
)}
{onCreatePR && (
<button
onClick={handleCreatePR}
disabled={creating || !formData.title.trim()}
className="px-4 py-2 font-mono text-xs text-green-400 border border-green-400/30 hover:border-green-400/50 hover:bg-green-400/10 disabled:opacity-50 disabled:cursor-not-allowed transition-colors uppercase"
>
{creating ? "Creating..." : formData.isDraft ? "Create Draft PR" : "Create PR"}
</button>
)}
</div>
</div>
</div>
);
}