import React from "react"; import { useTranslation } from "react-i18next"; import ReactMarkdown from "react-markdown"; import type { Components } from "react-markdown"; import remarkGfm from "remark-gfm"; import type { TaskDetail, TaskStep, TaskTokenUsagePerModel, WorkflowStepResult } from "@fusion/core"; import { costFor, type CostResult, type ModelPricingOverrides } from "../../../core/src/model-pricing"; import { createMermaidCodeComponent, sharedRehypePlugins } from "./markdownPipeline"; import { ProviderIcon } from "./ProviderIcon"; import { linkifyFilePaths, linkifyReactChildren } from "../utils/filePathLinkify"; import { inferProviderIconKey } from "../utils/providerIconKey"; const EMPTY_MARKDOWN_CHILD_SEPARATOR = ""; const STRING_OBJECT_TAG = "[object String]"; const markdownLinkifyCodeComponent: NonNullable = ({ children, ...props }) => { const text = React.Children.toArray(children).join(EMPTY_MARKDOWN_CHILD_SEPARATOR); const linkedChildren = linkifyFilePaths(text); if (linkedChildren.length === 1 && Object.prototype.toString.call(linkedChildren[0]) === STRING_OBJECT_TAG) { return {children}; } return {linkedChildren}; }; const markdownLinkifyComponents: Components = { p: ({ children, ...props }) =>

{linkifyReactChildren(children)}

, li: ({ children, ...props }) =>
  • {linkifyReactChildren(children)}
  • , code: createMermaidCodeComponent("task-summary-mermaid-diagram", markdownLinkifyCodeComponent), }; interface TaskSummaryTabProps { task: TaskDetail; pricingOverrides?: ModelPricingOverrides; } function getCompletedSteps(steps: TaskStep[] | undefined): TaskStep[] { return (steps ?? []).filter((step) => step.status === "done" || step.status === "skipped"); } function getRenderableWorkflowResults(results: WorkflowStepResult[] | undefined): WorkflowStepResult[] { return (results ?? []).filter((result) => result.status !== "pending"); } interface TokenCostRow { key: string; label: string; modelProvider?: string; modelId?: string; inputTokens: number; outputTokens: number; cachedTokens: number; cacheWriteTokens: number; totalTokens: number; cost: CostResult; } function formatCount(n: number): string { return Number.isFinite(n) ? Math.round(n).toLocaleString() : "0"; } function formatCost(usd: number | null, unavailable: boolean): string { if (unavailable || usd === null || !Number.isFinite(usd)) { return "—"; } return `$${usd.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; } function toTokenBucketKey(bucket: Pick): string { return `${bucket.modelProvider ?? ""}:${bucket.modelId ?? ""}`; } function toTokenCostRow( bucket: Pick, unknownLabel: string, now: number, pricingOverrides?: ModelPricingOverrides, ): TokenCostRow { const modelId = bucket.modelId?.trim() || undefined; const modelProvider = bucket.modelProvider?.trim() || undefined; const label = modelId ?? unknownLabel; return { key: toTokenBucketKey({ modelProvider, modelId }), label, modelProvider, modelId, inputTokens: bucket.inputTokens, outputTokens: bucket.outputTokens, cachedTokens: bucket.cachedTokens, cacheWriteTokens: bucket.cacheWriteTokens, totalTokens: bucket.totalTokens, cost: costFor( { inputTokens: bucket.inputTokens, outputTokens: bucket.outputTokens, cachedTokens: bucket.cachedTokens, cacheWriteTokens: bucket.cacheWriteTokens, }, { provider: modelProvider, model: modelId }, now, pricingOverrides, ), }; } /** * FNXC:TaskDetailSummaryTokenCost 2026-06-27-00:00: * Done-task Summary shows durable token usage broken down by model with derived USD cost. Use already-loaded task.tokenUsage.perModel buckets plus costFor and global pricing overrides threaded from TaskDetailModal; do not fetch or persist cost here. Unpriced models render “—” instead of $0 and make the task total unavailable so estimates are never understated. */ function buildTokenCostRows(task: TaskDetail, unknownLabel: string, pricingOverrides?: ModelPricingOverrides): TokenCostRow[] { const tokenUsage = task.tokenUsage; if (!tokenUsage) return []; const buckets = tokenUsage.perModel?.length ? tokenUsage.perModel : [ { modelProvider: tokenUsage.modelProvider, modelId: tokenUsage.modelId, inputTokens: tokenUsage.inputTokens, outputTokens: tokenUsage.outputTokens, cachedTokens: tokenUsage.cachedTokens, cacheWriteTokens: tokenUsage.cacheWriteTokens, totalTokens: tokenUsage.totalTokens, }, ]; const merged = new Map>(); buckets.forEach((bucket) => { const modelProvider = bucket.modelProvider?.trim() || undefined; const modelId = bucket.modelId?.trim() || undefined; const key = toTokenBucketKey({ modelProvider, modelId }); const current = merged.get(key); if (!current) { merged.set(key, { ...bucket, modelProvider, modelId }); return; } current.inputTokens += bucket.inputTokens; current.outputTokens += bucket.outputTokens; current.cachedTokens += bucket.cachedTokens; current.cacheWriteTokens += bucket.cacheWriteTokens; current.totalTokens += bucket.totalTokens; }); const now = Date.now(); return Array.from(merged.values()).map((bucket) => toTokenCostRow(bucket, unknownLabel, now, pricingOverrides)); } function totalCostForRows(rows: TokenCostRow[]): { usd: number | null; unavailable: boolean } { let usd = 0; let unavailable = false; rows.forEach((row) => { if (row.totalTokens <= 0) return; if (row.cost.unavailable || row.cost.usd === null || !Number.isFinite(row.cost.usd)) { unavailable = true; return; } usd += row.cost.usd; }); return { usd: unavailable ? null : usd, unavailable }; } /** * FNXC:TaskDetailSummaryTab 2026-06-27-00:00: * TaskSummaryTab aggregates read-only completion data already loaded on TaskDetail: agent-written summary, changed-file metadata, implementation steps, workflow-step outcomes, and retry counts. It does not fetch, persist, or generate AI content so done-task details remain a front-end composition only. */ export function TaskSummaryTab({ task, pricingOverrides }: TaskSummaryTabProps) { const { t } = useTranslation("app"); const summary = task.summary?.trim(); const changedFiles = task.mergeDetails?.landedFiles?.length ? task.mergeDetails.landedFiles : task.modifiedFiles ?? []; const completedSteps = getCompletedSteps(task.steps); const workflowResults = getRenderableWorkflowResults(task.workflowStepResults); const retryTotal = task.retrySummary?.total ?? 0; const hasChangedStats = task.mergeDetails?.filesChanged != null || task.mergeDetails?.insertions != null || task.mergeDetails?.deletions != null; const hasChangedContent = changedFiles.length > 0 || hasChangedStats || Boolean(task.mergeDetails?.commitSha); const hasAgentWork = completedSteps.length > 0 || workflowResults.length > 0 || retryTotal > 0; const tokenCostRows = buildTokenCostRows(task, t("taskDetail.summaryTab.unknownModel", "(unknown)"), pricingOverrides); const totalCost = totalCostForRows(tokenCostRows); return (

    {t("taskDetail.summaryTab.completionHeading", "Completion summary")}

    {summary ? (
    {summary}
    ) : (

    {t("taskDetail.summaryTab.noCompletionSummary", "No completion summary was recorded for this task.")}

    )}
    {hasChangedContent ? (

    {t("taskDetail.summaryTab.changedHeading", "What changed")}

    {(hasChangedStats || task.mergeDetails?.commitSha) && (
    {task.mergeDetails?.commitSha && (
    {t("taskDetail.summaryTab.commit", "Commit")}
    {task.mergeDetails.commitSha.slice(0, 7)}
    )} {task.mergeDetails?.filesChanged != null && (
    {t("taskDetail.summaryTab.filesChanged", "Files")}
    {task.mergeDetails.filesChanged}
    )} {task.mergeDetails?.insertions != null && (
    {t("taskDetail.summaryTab.insertions", "Added")}
    +{task.mergeDetails.insertions}
    )} {task.mergeDetails?.deletions != null && (
    {t("taskDetail.summaryTab.deletions", "Removed")}
    -{task.mergeDetails.deletions}
    )}
    )} {changedFiles.length > 0 ? (
      {changedFiles.map((path) => (
    • {path}
    • ))}
    ) : (

    {t("taskDetail.summaryTab.noChangedFiles", "No changed-file list is available for this task.")}

    )}
    ) : null} {task.tokenUsage ? (

    {t("taskDetail.summaryTab.tokenCostHeading", "Token usage & cost")}

    {tokenCostRows.map((row) => ( ))}
    {t("taskDetail.summaryTab.model", "Model")} {t("taskDetail.summaryTab.inputTokens", "Input")} {t("taskDetail.summaryTab.outputTokens", "Output")} {t("taskDetail.summaryTab.cachedTokens", "Cached")} {t("taskDetail.summaryTab.totalTokens", "Total")} {t("taskDetail.summaryTab.cost", "Cost")}
    {row.label} {formatCount(row.inputTokens)} {formatCount(row.outputTokens)} {formatCount(row.cachedTokens)} {formatCount(row.totalTokens)} {row.cost.unavailable || row.cost.usd === null ? ( — ) : ( formatCost(row.cost.usd, row.cost.unavailable) )}
    {t("taskDetail.summaryTab.totalCost", "Total cost")} {formatCost(totalCost.usd, totalCost.unavailable)}
    ) : null} {hasAgentWork ? (

    {t("taskDetail.summaryTab.agentWorkHeading", "Work done by agents")}

    {completedSteps.length > 0 && (
    {t("taskDetail.summaryTab.completedSteps", "Completed steps")}
      {completedSteps.map((step, index) => (
    • {step.status} {step.name}
    • ))}
    )} {workflowResults.length > 0 && (
    {t("taskDetail.summaryTab.workflowResults", "Workflow results")}
      {workflowResults.map((result) => (
    • {result.status.replace("_", " ")} {result.workflowStepName}
    • ))}
    )} {retryTotal > 0 && (

    {t("taskDetail.summaryTab.retries", "Agents retried this task {{count}} time{{plural}}.", { count: retryTotal, plural: retryTotal === 1 ? "" : "s" })}

    )}
    ) : (

    {t("taskDetail.summaryTab.agentWorkHeading", "Work done by agents")}

    {t("taskDetail.summaryTab.noAgentWork", "No completed steps or workflow results are available for this task.")}

    )}
    ); }