import { useTranslation } from "react-i18next"; import type { Task, TaskTokenUsage, WorkflowStepResult } from "@fusion/core"; import { extractTimingEvents, getActiveRuntimeMs, getEndToEndDurationMs, getTimedDurationMs, getWallClockSinceFirstExecutionMs, getWorkflowRuntimeMs, type TimingEvent } from "../utils/taskTiming"; import "./TaskTokenStatsPanel.css"; interface TaskTokenStatsPanelProps { tokenUsage?: TaskTokenUsage; loading: boolean; task?: Pick< Task, | "log" | "timedExecutionMs" | "workflowStepResults" | "executionMode" | "status" | "paused" | "currentStep" | "steps" | "mergeRetries" | "workflowStepRetries" | "stuckKillCount" | "postReviewFixCount" | "recoveryRetryCount" | "taskDoneRetryCount" | "nextRecoveryAt" | "checkedOutBy" | "assignedAgentId" | "blockedBy" | "sessionFile" | "executionStartedAt" | "executionCompletedAt" | "firstExecutionAt" | "cumulativeActiveMs" | "column" | "columnMovedAt" >; } interface WorkflowTimingSummary { timedStepCount: number; totalDurationMs: number; longestStep?: { name: string; durationMs: number }; } function formatTokenCount(value: number): string { return value.toLocaleString(); } function formatHitRatio(ratio: number): string { return `${(ratio * 100).toFixed(1)}%`; } function formatTimestamp(value: string): string { const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) { return value; } return parsed.toLocaleString(); } function formatDuration(valueMs: number): string { if (valueMs < 1000) { return `${Math.round(valueMs)} ms`; } const valueSeconds = valueMs / 1000; if (valueSeconds < 60) { return `${valueSeconds.toFixed(1)} s`; } const minutes = Math.floor(valueSeconds / 60); const seconds = Math.round(valueSeconds % 60); return `${minutes}m ${seconds}s`; } function summarizeWorkflowTiming(results: WorkflowStepResult[]): WorkflowTimingSummary { const nowMs = Date.now(); const timedResults = results .map((step) => { if (!step.startedAt) { return null; } const startedMs = new Date(step.startedAt).getTime(); if (Number.isNaN(startedMs)) { return null; } let endMs: number; if (step.completedAt) { const completedMs = new Date(step.completedAt).getTime(); if (Number.isNaN(completedMs) || completedMs < startedMs) { return null; } endMs = completedMs; } else { endMs = Math.max(startedMs, nowMs); } return { name: step.workflowStepName || step.workflowStepId, durationMs: endMs - startedMs, }; }) .filter((value): value is { name: string; durationMs: number } => value !== null); const totalDurationMs = getWorkflowRuntimeMs(results, nowMs) ?? 0; const longestStep = timedResults.reduce<{ name: string; durationMs: number } | undefined>((longest, step) => { if (!longest || step.durationMs > longest.durationMs) { return step; } return longest; }, undefined); return { timedStepCount: timedResults.length, totalDurationMs, longestStep, }; } export function TaskTokenStatsPanel({ tokenUsage, loading, task }: TaskTokenStatsPanelProps) { const { t } = useTranslation("app"); const nowMs = Date.now(); const timingEvents = extractTimingEvents(task?.log ?? []); const timedTimingEvents = timingEvents.filter((event) => typeof event.durationMs === "number"); const logTimingDurationMs = timedTimingEvents.reduce((sum, event) => sum + (event.durationMs ?? 0), 0); const parsedTimingDurationMs = getTimedDurationMs(task?.log) ?? 0; const totalTimingDurationMs = typeof task?.timedExecutionMs === "number" ? task.timedExecutionMs : Math.max(logTimingDurationMs, parsedTimingDurationMs); const longestTimingEvent = timedTimingEvents.reduce((longest, event) => { if (!longest || (event.durationMs ?? 0) > (longest.durationMs ?? 0)) { return event; } return longest; }, undefined); const workflowTiming = summarizeWorkflowTiming(task?.workflowStepResults ?? []); const activeRuntimeMs = task ? getActiveRuntimeMs(task, nowMs) : null; const endToEndDurationMs = getEndToEndDurationMs(task?.executionStartedAt, task?.executionCompletedAt, nowMs); const wallClockSinceFirstExecutionMs = getWallClockSinceFirstExecutionMs( task?.firstExecutionAt, task?.executionCompletedAt, nowMs, ); // Canonical fallback order for Task Detail Stats total runtime: // 1) durable wall-clock execution window (`executionStartedAt` → `executionCompletedAt`), // 2) server aggregate `timedExecutionMs` when present, // 3) legacy local aggregate (`[timing]` sum + workflow runtime). // This avoids double counting when workflow timings appear in both `[timing]` // logs and `workflowStepResults`. const totalExecutionMs = activeRuntimeMs ?? (endToEndDurationMs ?? (typeof task?.timedExecutionMs === "number" ? task.timedExecutionMs : totalTimingDurationMs + workflowTiming.totalDurationMs)); const showWallClockSinceFirstExecution = wallClockSinceFirstExecutionMs != null && wallClockSinceFirstExecutionMs !== totalExecutionMs; const taskStepCount = task?.steps?.length ?? 0; return (

{t("taskDetail.executionAndTokenStats", "Execution & Token Stats")}

{t("taskDetail.executionTiming", "Execution Timing")}
{t("taskDetail.timingEvents", "Timing events")} {timingEvents.length.toLocaleString()}
{t("taskDetail.timedDuration", "Timed duration")} {formatDuration(totalTimingDurationMs)}
{t("taskDetail.workflowTimedSteps", "Workflow timed steps")} {workflowTiming.timedStepCount.toLocaleString()}
{t("taskDetail.workflowRuntime", "Workflow runtime")} {formatDuration(workflowTiming.totalDurationMs)}
{t("taskDetail.totalExecutionTime", "Total execution time")} {formatDuration(totalExecutionMs)}
{showWallClockSinceFirstExecution ? (
{t("taskDetail.wallClockSinceFirst", "Wall-clock since first execution")} {formatDuration(wallClockSinceFirstExecutionMs)}
) : null}
{t("taskDetail.longestTimingEvent", "Longest timing event")}
{longestTimingEvent?.durationMs ? `${longestTimingEvent.summary} (${formatDuration(longestTimingEvent.durationMs)})` : t("taskDetail.noTimedEvents", "No timed events recorded yet.")}
{t("taskDetail.longestWorkflowStep", "Longest workflow step")}
{workflowTiming.longestStep ? `${workflowTiming.longestStep.name} (${formatDuration(workflowTiming.longestStep.durationMs)})` : t("taskDetail.noWorkflowStepTimings", "No completed workflow step timings yet.")}
{t("taskDetail.executionDetails", "Execution Details")}
{t("taskDetail.executionMode", "Execution mode")}
{task?.executionMode === "fast" ? t("taskDetail.executionModeFast", "Fast") : t("taskDetail.executionModeStandard", "Standard")}
{t("taskDetail.runtimeStatus", "Runtime status")}
{task?.status ?? t("taskDetail.notSet", "Not set")}
{t("taskDetail.paused", "Paused")}
{task?.paused ? t("taskDetail.yes", "Yes") : t("taskDetail.no", "No")}
{t("taskDetail.stepProgress", "Step progress")}
{taskStepCount > 0 ? `${Math.min((task?.currentStep ?? 0) + 1, taskStepCount)} / ${taskStepCount}` : t("taskDetail.noSteps", "No steps")}
{t("taskDetail.retriesLabel", "Retries (recovery / workflow / merge / task_done)")}
{`${task?.recoveryRetryCount ?? 0} / ${task?.workflowStepRetries ?? 0} / ${task?.mergeRetries ?? 0} / ${task?.taskDoneRetryCount ?? 0}`}
{t("taskDetail.recoveryState", "Recovery state")}
{task?.nextRecoveryAt ? t("taskDetail.nextRecoveryAt", "Next recovery at {{time}}", { time: formatTimestamp(task.nextRecoveryAt) }) : t("taskDetail.noScheduledRecovery", "No scheduled recovery")}
{t("taskDetail.selfHealCounters", "Self-heal counters")}
{t("taskDetail.selfHealValues", "stuck kills: {{stuckKills}}, post-review fixes: {{postReviewFixes}}", { stuckKills: task?.stuckKillCount ?? 0, postReviewFixes: task?.postReviewFixCount ?? 0 })}
{t("taskDetail.runtimeLinks", "Runtime links")}
{[ task?.assignedAgentId ? t("taskDetail.agentLink", "agent {{id}}", { id: task.assignedAgentId }) : null, task?.checkedOutBy ? t("taskDetail.checkoutLink", "checkout {{id}}", { id: task.checkedOutBy }) : null, task?.blockedBy ? t("taskDetail.blockedByLink", "blocked by {{id}}", { id: task.blockedBy }) : null, task?.sessionFile ? t("taskDetail.hasSession", "has session") : null, ].filter(Boolean).join(", ") || t("taskDetail.noRuntimeLinks", "No runtime links")}
{t("taskDetail.tokenUsage", "Token Usage")}
{!tokenUsage && loading ? (
{t("taskDetail.loadingTokenStats", "Loading token statistics…")}
) : !tokenUsage ? (
{t("taskDetail.noTokenUsage", "No token usage recorded for this task yet.")}
) : ( <>
{t("taskDetail.inputTokens", "Input")} {formatTokenCount(tokenUsage.inputTokens)}
{t("taskDetail.outputTokens", "Output")} {formatTokenCount(tokenUsage.outputTokens)}
{t("taskDetail.cacheRead", "Cache read")} {formatTokenCount(tokenUsage.cachedTokens)}
{t("taskDetail.cacheWrite", "Cache write")} {formatTokenCount(tokenUsage.cacheWriteTokens ?? 0)}
{t("taskDetail.totalTokens", "Total")} {formatTokenCount(tokenUsage.totalTokens)}
{t("taskDetail.cacheHitRatio", "Cache hit ratio:")}{" "} {(tokenUsage.inputTokens + tokenUsage.cachedTokens) > 0 ? formatHitRatio(tokenUsage.cachedTokens / (tokenUsage.inputTokens + tokenUsage.cachedTokens)) : "—"}
{t("taskDetail.cacheBreakdown", "(read {{read}} / write {{write}} / input {{input}})", { read: formatTokenCount(tokenUsage.cachedTokens), write: formatTokenCount(tokenUsage.cacheWriteTokens ?? 0), input: formatTokenCount(tokenUsage.inputTokens) })}
{t("taskDetail.firstUsed", "First used")}
{t("taskDetail.lastUsed", "Last used")}
)}
); }