import { useRef, useState, type ComponentType, type SVGProps } from "react"; import { Brain, OctagonX } from "lucide-react"; import { MarkdownBody } from "@/components/MarkdownBody"; import { useSecondTick } from "@/hooks/useSecondTick"; import { cn } from "@/lib/utils"; import type { TaskChatItem, TaskChatMessageItem, TaskChatMarkerItem, TaskChatProtocolItem, TaskChatProviderActivityItem, TaskChatRuntimeRequestDecision, TaskChatRuntimeRequestItem, TaskChatThinkingItem, TaskChatToolItem, } from "./task-chat-model"; import { TaskChatAgentIdentity } from "./TaskChatBubble"; import { TaskChatActivityPhase } from "./TaskChatActivityPhase"; import { TaskChatProtocolActivityRow } from "./TaskChatProtocolActivityRow"; import { TaskChatProtocolCard } from "./TaskChatProtocolCard"; import { TaskChatPlanPreviewCard } from "./TaskChatPlanPreviewCard"; import { TaskChatThinking } from "./TaskChatThinking"; import { TaskChatToolCard } from "./TaskChatToolCard"; import { TaskChatUsageReadout } from "./TaskChatUsageReadout"; import { protocolActivityIsRunning, protocolActivityLabel, protocolActivityPresentation, } from "./task-chat-activity-presentation"; import { buildTurnTimelineRows, isTerminalRunStatus, paperclipRunnerFinalResponse, paperclipRunnerTimelineItems, } from "./transcript-adapter"; import { toolTaxonomy } from "./tool-taxonomy"; function lastOf( items: readonly TaskChatItem[], predicate: (item: TaskChatItem) => item is T, ): T | undefined { for (let index = items.length - 1; index >= 0; index -= 1) { const item = items[index]; if (predicate(item)) return item; } return undefined; } function isHeadlineProtocolActivity( item: TaskChatItem, ): item is TaskChatProtocolItem { return ( item.kind === "protocol" && protocolActivityPresentation(item) !== null ); } function currentActivityStatusItems( items: readonly TaskChatItem[], ): readonly TaskChatItem[] { let boundaryIndex = -1; for (let index = items.length - 1; index >= 0; index -= 1) { const item = items[index]; if ( item.kind === "message" || item.kind === "plan_document" || (item.kind === "protocol" && (item.surface === "runtime_request" || item.surface === "run_result" || item.surface === "run_terminal")) ) { boundaryIndex = index; break; } } return items.slice(boundaryIndex + 1); } type FoldedNarration = | { kind: "commentary"; item: TaskChatMessageItem; order: number } | { kind: "reasoning"; item: TaskChatThinkingItem; line: string | null; lineIndex: number; order: number; }; function latestFoldedNarration( items: readonly TaskChatItem[], ): FoldedNarration | null { let latest: FoldedNarration | null = null; for (const [index, item] of items.entries()) { const order = item.kind === "message" || item.kind === "thinking" ? (item.transcriptIndex ?? index) : -1; if (item.kind === "message" && item.interstitial && item.text.trim()) { if (!latest || order >= latest.order) latest = { kind: "commentary", item, order }; continue; } if (item.kind !== "thinking") continue; let lineIndex = -1; for ( let candidate = item.lines.length - 1; candidate >= 0; candidate -= 1 ) { if (item.lines[candidate]?.trim()) { lineIndex = candidate; break; } } if (!latest || order >= latest.order) { latest = { kind: "reasoning", item, line: lineIndex < 0 ? null : item.lines[lineIndex]!.trim(), lineIndex, order, }; } } return latest; } function FoldedReasoningTicker({ logicalKey, text, }: { logicalKey: string; text: string; }) { const [ticker, setTicker] = useState({ logicalKey, motionKey: 0, current: text, exiting: null as string | null, }); if (ticker.logicalKey !== logicalKey) { setTicker({ logicalKey, motionKey: ticker.motionKey + 1, current: text, exiting: ticker.current, }); } else if (ticker.current !== text) { // Token fragments update the mounted line. Only a new logical line moves // the ticker, so streaming text does not restart the animation per token. setTicker({ ...ticker, current: text }); } return (
{ticker.exiting !== null ? ( setTicker((current) => ({ ...current, exiting: null })) } > {ticker.exiting} ) : null} 0 && "cot-line-enter", )} aria-live="polite" aria-atomic="true" > {ticker.current}
); } function FoldedLiveNarration({ narration }: { narration: FoldedNarration }) { if (narration.kind === "reasoning") { if (!narration.line) return null; return ( ); } return (
{narration.item.text}
); } function formatCompactDuration(ms: number | null): string | null { if (ms == null || !Number.isFinite(ms)) return null; const totalSeconds = Math.max(0, Math.floor(ms / 1000)); if (totalSeconds < 60) return `${totalSeconds}s`; const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return seconds === 0 ? `${minutes}m` : `${minutes}m ${seconds}s`; } function terminalStatusFailed(status: string): boolean { return ( status === "failed" || status === "cancelled" || status === "timed_out" || status === "interrupted" ); } function RunnerActivityTimeline({ items }: { items: readonly TaskChatItem[] }) { if (items.length === 0) return null; return (
    {items.map((item, index) => (
  1. {item.kind === "message" ? (
    {item.text}
    ) : item.kind === "thinking" ? ( ) : item.kind === "tool" ? ( ) : item.kind === "usage" ? ( ) : item.kind === "marker" ? ( ) : item.kind === "protocol" ? ( ) : null}
  2. ))}
); } function RunnerActivityMarker({ item }: { item: TaskChatMarkerItem }) { return (
{item.label} {item.detail ? ( {item.detail} ) : null}
); } function RunnerTurnStatus({ status, startedAtMs, finishedAtMs, }: { status: string; startedAtMs: number | null; finishedAtMs?: number | null; }) { const terminal = isTerminalRunStatus(status); useSecondTick(!terminal && startedAtMs != null); const elapsedMs = startedAtMs == null ? null : Math.max( 0, (terminal ? (finishedAtMs ?? Date.now()) : Date.now()) - startedAtMs, ); const elapsed = formatCompactDuration(elapsedMs); const failed = terminalStatusFailed(status); const label = terminal ? (failed ? "Stopped" : "Worked") : "Working"; const semanticLabel = terminal ? elapsed ? `${label} ${failed ? "after" : "for"} ${elapsed}` : label : `${label} for ${elapsed ?? "0s"}`; return ( {semanticLabel} ); } function RunnerCurrentActivityTail({ items, status, }: { items: readonly TaskChatItem[]; status: string; }) { if (isTerminalRunStatus(status)) return null; const activity = lastOf< TaskChatThinkingItem | TaskChatToolItem | TaskChatProtocolItem >( items, ( item, ): item is TaskChatThinkingItem | TaskChatToolItem | TaskChatProtocolItem => item.kind === "thinking" || item.kind === "tool" || isHeadlineProtocolActivity(item), ); let Icon: ComponentType> | null = null; let label = "Thinking"; let detail: string | undefined; let family: string | undefined; let active = true; if (activity?.kind === "tool") { const taxonomy = toolTaxonomy(activity.rawName ?? activity.name); Icon = taxonomy.icon; label = taxonomy.verbLabel; detail = activity.target; active = activity.status === "pending" || activity.status === "in_progress"; } else if (activity?.kind === "protocol") { const presentation = protocolActivityPresentation(activity); if (presentation) { Icon = presentation.icon; label = protocolActivityLabel(activity, presentation); detail = presentation.detail; active = protocolActivityIsRunning(activity); family = activity.surface === "provider_activity" ? activity.family : activity.surface; } } return (
{Icon ? ( ) : null} {label} {detail ? ( {detail} ) : null}
); } export function TaskChatRunnerTurn({ runId, agentName, agentIcon, items, status, startedAtMs, finishedAtMs, activityUnavailable = false, suppressFinal = false, onRuntimeRequestDecision, }: { /** Stable identity used to clear replay-latched final text for the next turn. */ runId?: string | null; agentName?: string | null; agentIcon?: string | null; items: readonly TaskChatItem[]; status: string; startedAtMs: number | null; finishedAtMs?: number | null; activityUnavailable?: boolean; /** Accepted wait/interaction authority overrides an early provider final. */ suppressFinal?: boolean; onRuntimeRequestDecision?: ( item: TaskChatRuntimeRequestItem, decision: TaskChatRuntimeRequestDecision, ) => void | Promise; }) { const terminal = isTerminalRunStatus(status); const narration = latestFoldedNarration(items); const timelineRows = buildTurnTimelineRows( paperclipRunnerTimelineItems(items), !terminal, ); const currentActivityItems = currentActivityStatusItems(items); const yielded = items.some( (item) => item.kind === "protocol" && item.surface === "run_result" && item.disposition === "yielded", ); const observedFinal = suppressFinal ? undefined : paperclipRunnerFinalResponse(items, { allowFallback: terminal, }); const observedProviderText = Boolean( observedFinal && items.some( (item) => item.kind === "message" && item.id === observedFinal.id && item.channel !== "progress", ), ); // A reconnect/replay can briefly rebuild the transcript without the final // item (or with an earlier, shorter prefix). Provider-authored final text // always replaces a structured summary fallback, even when it is shorter; // within either class, displayed answer text remains monotonic. const finalRef = useRef<{ runId?: string | null; item?: TaskChatMessageItem; providerText?: boolean; }>({ runId }); if (finalRef.current.runId !== runId) finalRef.current = { runId }; // A provider final can arrive before the accepted yielded result. Clear any // replay latch once the control plane establishes that this turn is waiting // for continuation rather than presenting a durable assistant reply. if (yielded || suppressFinal) finalRef.current = { runId }; if ( observedFinal && (!finalRef.current.item || (observedProviderText && !finalRef.current.providerText) || (observedProviderText === Boolean(finalRef.current.providerText) && observedFinal.text.length >= finalRef.current.item.text.length)) ) { finalRef.current.item = observedFinal; finalRef.current.providerText = observedProviderText; } const final = finalRef.current.item; return (
{agentName ? ( ) : null}
{!terminal && narration && !final ? (
) : null} {activityUnavailable ? (
Live runner activity is temporarily unavailable. Retrying…
) : null} {timelineRows.length > 0 ? (
{timelineRows.map((row) => (
{row.kind === "activity_phase" ? ( null} renderChildren={(children) => ( )} /> ) : row.kind === "plan_document" ? ( ) : row.kind === "protocol" ? ( ) : null}
))}
) : null} {final ? (
{final.text}
) : null}
); }