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 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 (

Execution & Token Stats

Execution Timing
Timing events {timingEvents.length.toLocaleString()}
Timed duration {formatDuration(totalTimingDurationMs)}
Workflow timed steps {workflowTiming.timedStepCount.toLocaleString()}
Workflow runtime {formatDuration(workflowTiming.totalDurationMs)}
Total execution time {formatDuration(totalExecutionMs)}
{showWallClockSinceFirstExecution ? (
Wall-clock since first execution {formatDuration(wallClockSinceFirstExecutionMs)}
) : null}
Longest timing event
{longestTimingEvent?.durationMs ? `${longestTimingEvent.summary} (${formatDuration(longestTimingEvent.durationMs)})` : "No timed events recorded yet."}
Longest workflow step
{workflowTiming.longestStep ? `${workflowTiming.longestStep.name} (${formatDuration(workflowTiming.longestStep.durationMs)})` : "No completed workflow step timings yet."}
Execution Details
Execution mode
{task?.executionMode === "fast" ? "Fast" : "Standard"}
Runtime status
{task?.status ?? "Not set"}
Paused
{task?.paused ? "Yes" : "No"}
Step progress
{taskStepCount > 0 ? `${Math.min((task?.currentStep ?? 0) + 1, taskStepCount)} / ${taskStepCount}` : "No steps"}
Retries (recovery / workflow / merge / task_done)
{`${task?.recoveryRetryCount ?? 0} / ${task?.workflowStepRetries ?? 0} / ${task?.mergeRetries ?? 0} / ${task?.taskDoneRetryCount ?? 0}`}
Recovery state
{task?.nextRecoveryAt ? `Next recovery at ${formatTimestamp(task.nextRecoveryAt)}` : "No scheduled recovery"}
Self-heal counters
{`stuck kills: ${task?.stuckKillCount ?? 0}, post-review fixes: ${task?.postReviewFixCount ?? 0}`}
Runtime links
{[ task?.assignedAgentId ? `agent ${task.assignedAgentId}` : null, task?.checkedOutBy ? `checkout ${task.checkedOutBy}` : null, task?.blockedBy ? `blocked by ${task.blockedBy}` : null, task?.sessionFile ? "has session" : null, ].filter(Boolean).join(", ") || "No runtime links"}
Token Usage
{!tokenUsage && loading ? (
Loading token statistics…
) : !tokenUsage ? (
No token usage recorded for this task yet.
) : ( <>
Input {formatTokenCount(tokenUsage.inputTokens)}
Output {formatTokenCount(tokenUsage.outputTokens)}
Cache read {formatTokenCount(tokenUsage.cachedTokens)}
Cache write {formatTokenCount(tokenUsage.cacheWriteTokens ?? 0)}
Total {formatTokenCount(tokenUsage.totalTokens)}
Cache hit ratio:{" "} {(tokenUsage.inputTokens + tokenUsage.cachedTokens) > 0 ? formatHitRatio(tokenUsage.cachedTokens / (tokenUsage.inputTokens + tokenUsage.cachedTokens)) : "—"}
(read {formatTokenCount(tokenUsage.cachedTokens)} / write {formatTokenCount(tokenUsage.cacheWriteTokens ?? 0)} / input {formatTokenCount(tokenUsage.inputTokens)})
First used
Last used
)}
); }