import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import i18n from '../../../i18n'; import { useQuery } from '@tanstack/react-query'; import { POLLING } from '../../../lib/constants.js'; import { SubtaskInfo, SubtaskActivity, SubtaskFiles, fetchSubtaskFiles, subtaskFileRawUrl, fetchSubtaskActivity } from '../../../api'; import { statusTone, formatStatusLabel, parseActivityLog, isPreviewable } from '../../../lib/utils'; import { ActivityTimeline } from '../../activity/ActivityTimeline'; import { LinkifiedText } from '../../../lib/linkified-text'; import { OutputPreviewProvider } from '../../../lib/output-preview-context'; import { stripOutputPrefix } from '../../../lib/output-path-detect'; export type SubtaskFilePreviewHandler = (taskId: number, jobId: string, category: string, filePath: string) => void; interface SubtasksPanelProps { taskId: number; subtasks: SubtaskInfo[]; subtaskCount: number; subtaskCompleted: number; subtaskActivities?: SubtaskActivity[]; onFilePreview?: SubtaskFilePreviewHandler; } interface SubtaskCardProps { taskId: number; subtask: SubtaskInfo; activity?: SubtaskActivity; onFilePreview?: SubtaskFilePreviewHandler; } const ACTIVE_STATUSES = new Set(['running', 'waiting_human', 'waiting_subtasks']); const CATEGORY_LABELS: Record = { output: 'Output files', logs: 'Logs', input: 'Input files', }; const CATEGORY_ORDER = ['output', 'logs', 'input']; function FileList({ taskId, jobId, category, files, onFilePreview }: { taskId: number; jobId: string; category: string; files: string[]; onFilePreview?: SubtaskFilePreviewHandler }) { const label = i18n.t('detail:subtasks.category.' + category, { defaultValue: CATEGORY_LABELS[category] ?? category }); return (
{label}
); } function SubtaskCard({ taskId, subtask, activity, onFilePreview }: SubtaskCardProps) { const [expanded, setExpanded] = useState(false); const tone = statusTone(subtask.status); const title = subtask.instruction.split('\n')[0]?.slice(0, 100) ?? ''; const isActive = ACTIVE_STATUSES.has(subtask.status); const { data: activityLog } = useQuery({ queryKey: ['subtaskActivity', taskId, subtask.id], queryFn: () => fetchSubtaskActivity(taskId, subtask.id), refetchInterval: POLLING.FAST, enabled: expanded && isActive, }); const displayLog = expanded && !isActive ? (activity?.activityLog ?? '') : (activityLog ?? ''); const activityEvents = expanded ? parseActivityLog(displayLog) : []; const { data: subtaskFiles, isLoading: filesLoading } = useQuery({ queryKey: ['subtask-files', taskId, subtask.id], queryFn: () => fetchSubtaskFiles(taskId, subtask.id), enabled: expanded, refetchInterval: isActive ? POLLING.MEDIUM : false, }); const currentMovement = activity?.currentMovement; const categories = subtaskFiles?.categories ?? {}; const hasFiles = Object.values(categories).some(f => f.length > 0); return (
{expanded && ( // Subtask-scoped preview context: any `output/...` link inside // this card opens the SUBTASK's workspace file (not the main // task's). Wrapping just the expanded body keeps the outer // (main task) provider in charge of everything else. { if (!onFilePreview) return; const relative = stripOutputPrefix(matchedPath); onFilePreview(taskId, subtask.id, 'output', relative); }} >
{activityEvents.length > 0 && (
Activity
)} {filesLoading &&
{t('subtasks.filesLoading')}
} {hasFiles && (
{t('subtasks.files')}
{CATEGORY_ORDER.map(cat => categories[cat] && categories[cat].length > 0 ? ( ) : null )}
)} {subtask.children && subtask.children.length > 0 && (
{t('subtasks.childTasks', { done: subtask.childCompleted ?? 0, total: subtask.childCount ?? subtask.children.length })}
{subtask.children.map(child => ( ))}
)}
)}
); } export function SubtasksPanel({ taskId, subtasks, subtaskCount, subtaskCompleted, subtaskActivities, onFilePreview }: SubtasksPanelProps) { const { t } = useTranslation('detail'); const progressPct = subtaskCount > 0 ? Math.round((subtaskCompleted / subtaskCount) * 100) : 0; return (
{t('subtasks.title')}
{subtaskCompleted}/{subtaskCount} {t('subtasks.done')}
{subtasks.map(subtask => ( a.jobId === subtask.id)} onFilePreview={onFilePreview} /> ))}
); }