import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import {
applyFixToBranch,
cancelTicket,
deleteWorktreeCmd,
getTicketResult,
getWorktreeDiff,
retryTicket,
} from "../../lib/api";
import { getErrorMessage } from "../../lib/errors";
import type { ProcessedTicket, Worktree } from "../../lib/types";
import ConfirmModal from "../ui/ConfirmModal";
function statusBadgeClass(status: string): string {
switch (status) {
case "Pending":
return "bg-yellow-100 text-yellow-700";
case "Analyzing":
return "bg-blue-100 text-blue-700";
case "Developing":
return "bg-purple-100 text-purple-700";
case "Done":
return "bg-green-100 text-green-700";
case "Error":
return "bg-red-100 text-red-700";
case "Cancelled":
return "bg-gray-100 text-gray-500";
default:
return "bg-gray-100 text-gray-700";
}
}
function DiffViewer({ diff }: { diff: string }) {
if (!diff) {
return
No changes detected.
;
}
const lines = diff.split("\n");
return (
{lines.map((line, i) => {
let cls = "";
if (line.startsWith("+++") || line.startsWith("---")) cls = "text-gray-400";
else if (line.startsWith("+")) cls = "bg-green-900/20 text-green-400";
else if (line.startsWith("-")) cls = "bg-red-900/20 text-red-400";
else if (line.startsWith("@@")) cls = "text-blue-400";
else if (line.startsWith("diff ")) cls = "font-bold text-yellow-400";
return (
{line}
);
})}
);
}
export default function TicketDetail() {
const { ticketId } = useParams();
const navigate = useNavigate();
const [ticket, setTicket] = useState(null);
const [worktree, setWorktree] = useState(null);
const [diff, setDiff] = useState(null);
const [targetBranch, setTargetBranch] = useState("");
const [tab, setTab] = useState<"info" | "analyst" | "developer" | "diff">("info");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [isDeleteWorktreeModalOpen, setIsDeleteWorktreeModalOpen] = useState(false);
async function loadData() {
if (!ticketId) return;
try {
const result = await getTicketResult(ticketId);
setTicket(result.ticket);
setWorktree(result.worktree);
if (result.ticket.developer_report) setTab("developer");
else if (result.ticket.analyst_report) setTab("analyst");
if (result.worktree && result.worktree.status === "Active") {
try {
const d = await getWorktreeDiff(result.worktree.id);
setDiff(d);
} catch {
setDiff(null);
}
} else {
setDiff(null);
}
} catch (err) {
setError(getErrorMessage(err));
}
}
useEffect(() => {
loadData();
}, [ticketId]);
async function handleRetry() {
if (!ticketId) return;
setLoading(true);
try {
await retryTicket(ticketId);
await loadData();
} catch (err) {
setError(getErrorMessage(err));
}
setLoading(false);
}
async function handleCancel() {
if (!ticketId) return;
setLoading(true);
try {
await cancelTicket(ticketId);
await loadData();
} catch (err) {
setError(getErrorMessage(err));
}
setLoading(false);
}
async function handleApplyFix() {
if (!worktree || !targetBranch) return;
setLoading(true);
setError("");
try {
await applyFixToBranch(worktree.id, targetBranch);
await loadData();
} catch (err) {
setError(getErrorMessage(err));
}
setLoading(false);
}
async function handleDeleteWorktree() {
if (!worktree) return;
setIsDeleteWorktreeModalOpen(false);
setLoading(true);
try {
await deleteWorktreeCmd(worktree.id);
setWorktree(null);
setDiff(null);
} catch (err) {
setError(getErrorMessage(err));
}
setLoading(false);
}
if (!ticket) {
return Loading...
;
}
const tabs = [
{ key: "info" as const, label: "Info" },
{
key: "analyst" as const,
label: "Analyst Report",
disabled: !ticket.analyst_report,
},
{
key: "developer" as const,
label: "Developer Report",
disabled: !ticket.developer_report,
},
{ key: "diff" as const, label: "Diff", disabled: !diff && !worktree },
];
return (
#{ticket.artifact_id}
{ticket.artifact_title}
{ticket.status}
{(ticket.status === "Error" || ticket.status === "Done" || ticket.status === "Cancelled") && (
)}
{(ticket.status === "Pending" ||
ticket.status === "Analyzing" ||
ticket.status === "Developing") && (
)}
{error && (
{error}
)}
{tabs.map((t) => (
))}
{tab === "info" && (
Status:
{ticket.status}
Detected:
{new Date(ticket.detected_at).toLocaleString()}
{ticket.processed_at && (
Processed:
{new Date(ticket.processed_at).toLocaleString()}
)}
{worktree && (
Worktree:
{worktree.branch_name}
{worktree.status}
)}
{worktree && worktree.status === "Active" && (
)}
{worktree && worktree.status === "Merged" && (
Fix applied to branch: {worktree.merged_into}
)}
)}
{tab === "analyst" && ticket.analyst_report && (
{ticket.analyst_report}
)}
{tab === "developer" && ticket.developer_report && (
{ticket.developer_report}
)}
{tab === "diff" &&
}
setIsDeleteWorktreeModalOpen(false)}
onConfirm={() => void handleDeleteWorktree()}
/>
);
}