import { useQuery, useQueryClient } from "@tanstack/react-query"; import dayjs from "dayjs"; import duration from "dayjs/plugin/duration"; import { useEffect, useState } from "react"; import ReactMarkdown from "react-markdown"; import rehypeHighlight from "rehype-highlight"; import remarkGfm from "remark-gfm"; import "highlight.js/styles/github-dark-dimmed.min.css"; dayjs.extend(duration); import { useSSE } from "../hooks/useSSE"; import { api } from "../lib/api"; import { ActivityLog } from "./ActivityLog"; import { LabelChip } from "./LabelChip"; import { LiveCodeChanges } from "./LiveCodeChanges"; import { SubtaskList } from "./SubtaskList"; import { TaskChatDrawer } from "./TaskChatDrawer"; import { Field, FieldLabel, formatRelative } from "./TaskDetailFields"; import { Badge } from "./ui/badge"; import { Button } from "./ui/button"; import { Separator } from "./ui/separator"; import { Sheet, SheetContent, SheetDescription, SheetTitle } from "./ui/sheet"; import { Skeleton } from "./ui/skeleton"; const _TASK_STATUS_LABELS: Record = { backlog: "Backlog", todo: "Todo", in_progress: "In Progress", in_review: "In Review", done: "Done", cancelled: "Cancelled", }; const REVIEW_ACTIONS = { reject: { label: "Reject", variant: "outline" as const }, complete: { label: "Complete", variant: "default" as const }, }; const TASK_DETAIL_SHEET_CLASS = "overflow-hidden p-0 gap-0 !w-[60%] max-md:!w-full"; interface TaskDetailProps { taskId: string; labels?: { name: string; color: string; description: string }[]; onClose: () => void; onRefresh: () => void; onAgentClick?: (agentId: string) => void; } function formatElapsed(ms: number): string { const d = dayjs.duration(ms); const h = Math.floor(d.asHours()); const m = d.minutes(); const s = d.seconds(); if (h > 0) return `${h}h ${m}m ${s}s`; if (m > 0) return `${m}m ${s}s`; return `${s}s`; } function formatPrLabel(prUrl: string, task?: any): string { if (task?.metadata?.branch) return String(task.metadata.branch); if (task?.id) return `vtit-agent-coding/task-${task.id}`; return "PR"; } function LiveDuration({ startedAt, finishedMinutes }: { startedAt: string | null; finishedMinutes: number | null }) { const [now, setNow] = useState(Date.now()); const active = startedAt != null && finishedMinutes == null; useEffect(() => { if (!active) return; const id = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(id); }, [active]); if (!startedAt) return ; if (active) { const elapsed = now - dayjs(startedAt).valueOf(); return {formatElapsed(elapsed)}; } return {formatElapsed(finishedMinutes! * 60_000)}; } function _annotationValue(task: any, key: string): string | null { const annotations = task?.metadata?.annotations; if (!annotations || typeof annotations !== "object" || Array.isArray(annotations)) return null; const value = annotations[key]; return typeof value === "string" && value ? value : null; } export function TaskDetail({ taskId, labels = [], onClose, onRefresh, onAgentClick: _onAgentClick }: TaskDetailProps) { const queryClient = useQueryClient(); const [chatOpen, setChatOpen] = useState(false); const { notes: sseNotes, reconnecting } = useSSE({ taskId, enabled: true }); const labelByName = new Map(labels.map((label) => [label.name, label])); const { data: task, isLoading: loading } = useQuery({ queryKey: ["task", taskId], queryFn: () => api.tasks.get(taskId), }); const { data: repositories = [] } = useQuery({ queryKey: ["repositories"], queryFn: () => api.repositories.list(), staleTime: 60_000, }); const { data: agents = [] } = useQuery({ queryKey: ["agents"], queryFn: () => api.agents.list(), staleTime: 60_000, }); const dependsOn: string[] = task?.depends_on || []; const { data: depTitles = {} } = useQuery({ queryKey: ["dep-titles", dependsOn], queryFn: async () => { const entries = await Promise.all(dependsOn.map((id) => api.tasks.get(id).then((t: any) => [id, t.title] as const))); return Object.fromEntries(entries); }, enabled: dependsOn.length > 0, }); async function reload() { await queryClient.invalidateQueries({ queryKey: ["task", taskId] }); } async function handleReviewAction(action: "reject" | "complete") { if (action === "reject") await api.tasks.reject(taskId); else await api.tasks.complete(taskId); await reload(); onRefresh(); } const content = loading ? (
) : !task ? (

Task not found.

) : null; if (content) { return ( { if (!open) onClose(); }} > Task Task details
{content}
); } const repo = repositories.find((r: any) => r.id === task.repository_id); const detailsContent = (
Status (Trạng thái)
Assigned to (Phụ trách)
{ if (task.status === "backlog" || task.status === "todo") { return ; } const branchName = task.metadata?.branch ? String(task.metadata.branch) : null; let linkUrl = task.pr_url; if (!linkUrl && branchName && repo) { linkUrl = repo.provider === "gitlab" ? `${repo.url}/-/tree/${branchName}` : `${repo.url}/tree/${branchName}`; } return linkUrl ? ( {branchName || formatPrLabel(linkUrl, task)} ) : ( ); })()} /> {task.cost_micro_usd !== undefined && task.cost_micro_usd !== null && task.cost_micro_usd > 0 && ( ${(task.cost_micro_usd / 1000000).toFixed(4)}} /> )} {task.scheduled_at && ( {new Date(task.scheduled_at).getTime() > Date.now() ? dayjs(task.scheduled_at).format("MM-DD HH:mm") : formatRelative(task.scheduled_at)} } /> )} n.action === "claimed")?.created_at ?? null} finishedMinutes={task.duration_minutes} /> } />
{task.status === "in_review" && (
{(Object.entries(REVIEW_ACTIONS) as [keyof typeof REVIEW_ACTIONS, (typeof REVIEW_ACTIONS)[keyof typeof REVIEW_ACTIONS]][]).map( ([action, config]) => ( ), )}
)} {dependsOn.length > 0 && (
Depends on
{dependsOn.map((depId) => ( {depTitles[depId] || depId} ))}
)}
Description {task.description ? (
{task.description}
) : (

No description.

)}
{/* Render Rich Metadata from _TASK.schema.json */} {(() => { const meta = typeof task.metadata === "string" ? JSON.parse(task.metadata) : task.metadata || {}; return ( <> {meta.businessContext && (
Business Context (Bối cảnh nghiệp vụ)
{meta.businessContext}
)} {meta.technicalNotes && (
Technical Notes
{meta.technicalNotes}
)} {meta.steps?.length > 0 && (
Steps ({meta.steps.length})
{meta.steps.map((step: any, idx: number) => (
{step.order ?? idx + 1}. {step.action} {step.estimatedMinutes && {step.estimatedMinutes}m}
{step.detail &&

{step.detail}

} {step.codeHint && ( {step.codeHint} )} {step.verifyCommand && (
Verify: {step.verifyCommand}
)}
))}
)} {meta.qcChecks?.length > 0 && (
QC Checks ({meta.qcChecks.length})
{meta.qcChecks.map((qc: any, idx: number) => (
{qc.severity === "MUST" ? "🔴 MUST" : "🟡 SHOULD"}
[{qc.category}] {qc.item}
))}
)} {meta.affectedFiles?.length > 0 && (
Affected Files ({meta.affectedFiles.length})
{meta.affectedFiles.map((file: any, idx: number) => (
{file.path} {file.changeType || "MODIFY"}
))}
)} {meta.acceptanceTests?.length > 0 && (
Acceptance Tests ({meta.acceptanceTests.length})
{meta.acceptanceTests.map((at: any, idx: number) => (
{at.id || `AT-${idx + 1}`} {at.testType}
{at.scenario}
{at.given && (
GIVEN: {at.given}
)} {at.when && (
WHEN: {at.when}
)} {at.then && (
THEN: {at.then}
)}
))}
)} ); })()} {task.input && (
Raw Input Payload
            {JSON.stringify(task.input, null, 2)}
          
)} {task.subtask_count > 0 && ( <>
Subtasks ({task.subtask_count}) { /* navigate to subtask */ }} />
)}
Activity
); return ( <> { if (!open) onClose(); }} > {task.title} Task detail panel
{/* Header */}
#{task.seq} {task.title} {task.blocked && ( Blocked )}
{repo && ( {repo.name} )} {task.labels?.map((name: string) => { const label = labelByName.get(name); return ; })}
{detailsContent}
{/* Dim mask when chat drawer is open */} {chatOpen && (
setChatOpen(false)} /> )} {task.assigned_to && ( )} ); }