import { useId, useState } from "react"; import { Brain, ChevronDown, ChevronRight, CirclePause, Gauge, } from "lucide-react"; import { useReducedMotion } from "motion/react"; import { MarkdownBody } from "@/components/MarkdownBody"; import { cn } from "@/lib/utils"; import { useTaskChatExpansion } from "./expansion-state"; import type { TaskChatActivityPhaseItem } from "./task-chat-model"; import { protocolActivityIsRunning, protocolActivityLabel, protocolActivityPresentation, } from "./task-chat-activity-presentation"; import { TaskChatProtocolActivityDetails, hasTaskChatProtocolActivityDetails, } from "./TaskChatProtocolActivityRow"; import { TaskChatUsageReadout } from "./TaskChatUsageReadout"; import { toolActivityPresentation } from "./tool-taxonomy"; type Activity = TaskChatActivityPhaseItem["items"][number]; function presentation(item: Activity, active: boolean) { if (item.kind === "tool") { const tool = toolActivityPresentation({ name: item.rawName ?? item.name, target: item.target, }); const running = active && (item.status === "pending" || item.status === "in_progress"); return { icon: tool.icon, label: item.status === "failed" ? tool.failedLabel : item.status === "interrupted" ? tool.interruptedLabel : running ? tool.runningLabel : tool.completedLabel, target: item.target, mono: true, running, }; } if (item.kind === "thinking") { const running = active && Boolean(item.streaming); return { icon: Brain, label: running ? "Thinking" : "Thought", target: item.lines .filter((line) => line.trim()) .at(-1) ?.trim(), mono: false, running, }; } if (item.kind === "protocol") { const protocol = protocolActivityPresentation(item); if (!protocol) return null; return { icon: protocol.icon, label: protocolActivityLabel(item, protocol), target: protocol.detail, mono: true, running: active && protocolActivityIsRunning(item), }; } if (item.kind === "marker") return { icon: CirclePause, label: item.label, target: item.detail, mono: false, running: false, }; const { used, size, inputTokens, outputTokens, costUsd } = item.usage; const usage = [ size > 0 ? `${used.toLocaleString()}/${size.toLocaleString()} ctx` : undefined, inputTokens != null || outputTokens != null ? `↑${(inputTokens ?? 0).toLocaleString()} ↓${(outputTokens ?? 0).toLocaleString()}` : undefined, costUsd != null ? `$${costUsd.toFixed(4)}` : undefined, ] .filter(Boolean) .join(" · "); return { icon: Gauge, label: item.label ?? "Token usage", target: usage || item.detail, mono: false, running: false, }; } function isFailure(item: Activity) { return ( (item.kind === "tool" && item.status === "failed") || (item.kind === "protocol" && item.surface === "provider_activity" && item.status === "failed") || (item.kind === "marker" && item.tone === "error") ); } function ActivityContent({ item, active, }: { item: Activity; active: boolean; }) { const row = presentation(item, active); if (!row) return null; const Icon = row.icon; return ( {row.label} {row.target ? ( {row.target} ) : null} ); } /** Only a new logical activity moves; token and status updates stay mounted. */ function RollingActivity({ item, active, }: { item: Activity; active: boolean; }) { const reducedMotion = useReducedMotion(); const [frame, setFrame] = useState({ current: item, previous: null as Activity | null, }); if (frame.current.id !== item.id) setFrame({ current: item, previous: reducedMotion ? null : frame.current }); else if (frame.current !== item) setFrame({ ...frame, current: item }); return ( {frame.previous && !reducedMotion ? ( ) : null} ); } function ActivityDetails({ item }: { item: Activity }) { if (item.kind === "thinking") return {item.lines.join("\n")}; if (item.kind === "usage") return ; if (item.kind === "protocol") return ; if (item.kind === "marker") return (

{item.detail ?? item.label}

); return ( <> {item.target ? (

{item.target}

) : null} {item.detail ? (
          {item.detail}
        
) : null} {item.decision ?

Permission {item.decision}

: null} {item.diff ? (

{item.diff.path} · +{item.diff.added} −{item.diff.removed}

) : null} ); } function hasActivityDetails(item: Activity): boolean { if (item.kind === "protocol") return hasTaskChatProtocolActivityDetails(item); if (item.kind === "thinking") return item.lines.some((line) => line.trim()); if (item.kind === "tool") return Boolean( item.target?.trim() || item.detail?.trim() || item.diff || item.decision, ); if (item.kind === "marker") return Boolean(item.detail?.trim()); return Boolean( item.detail || item.usage.size > 0 || item.usage.inputTokens != null || item.usage.outputTokens != null || item.usage.costUsd != null, ); } function ExpandedActivity({ item, active, }: { item: Activity; active: boolean; }) { const [open, setOpen] = useTaskChatExpansion( `runner-detail:${item.id}`, false, ); const detailId = useId(); const expandable = hasActivityDetails(item); const content = ; return (
  • {expandable ? ( ) : (
    {content}
    )} {expandable && open ? (
    ) : null}
  • ); } /** One rolling activity between commentary messages, with persistent optional history. */ export function TaskChatRunnerActivityGroup({ item, defaultExpanded = false, }: { item: TaskChatActivityPhaseItem; defaultExpanded?: boolean; }) { const [expanded, setExpanded] = useTaskChatExpansion( item.id, defaultExpanded, ); const historyId = useId(); const activities = item.items.filter( (activity) => presentation(activity, false) !== null, ); const latest = activities.at(-1); const failures = activities.filter(isFailure).length; const countLabel = `${activities.length} ${activities.length === 1 ? "activity" : "activities"}`; return (
    {item.interstitial ? (
    {item.interstitial.text}
    ) : null} {latest ? (
    {expanded ? (
      {activities.map((activity, index) => ( ))}
    ) : null}
    ) : null}
    ); }