import { useId, useState } from "react"; import { ChevronRight } from "lucide-react"; import { cn } from "@/lib/utils"; import type { TaskChatProviderActivityItem } from "./task-chat-model"; import { statusNoticePresentation, type StatusNoticeTone } from "./status-notice-presentation"; import { DetailList } from "./TaskChatProtocolActivityRow"; /** Payload keys already reflected by the chip itself. */ const HIDDEN_DETAILS = new Set(["Severity", "Category", "Scope", "Recoverable", "User Actionable", "Summary", "Phase"]); const TONE_CLASS: Record = { neutral: "border-border text-muted-foreground", progress: "border-border text-muted-foreground", warning: "border-(--tc-status-notice-warning-border) bg-(--tc-status-notice-warning-bg) text-(--tc-status-notice-warning-fg)", danger: "border-destructive/35 bg-destructive/5 text-destructive", }; /** * An inline harness status chip between the turn's activity rows: context * compaction, retries, model fallback and provider banners. A fatal provider * error renders as a red row with the provider's real message instead. */ export function TaskChatStatusNotice({ item, resolved = false, }: { item: TaskChatProviderActivityItem; /** A retry the turn moved past: the chip fades to a quiet receipt. */ resolved?: boolean; }) { const [open, setOpen] = useState(false); const detailId = `task-chat-status-notice-${useId().replaceAll(":", "")}`; const presentation = statusNoticePresentation(item, { resolved }); if (!presentation) return null; const Icon = presentation.icon; const extra = item.details.filter((entry) => !HIDDEN_DETAILS.has(entry.label) && entry.value !== presentation.detail); // The chip may show a short reason; keep the provider's full message on expand. const fullSummary = item.summary?.trim() || item.details.find((entry) => entry.label === "Summary")?.value; const expandedText = fullSummary && fullSummary !== presentation.detail ? fullSummary : presentation.detail; if (presentation.fatal) { return (
{presentation.label}

{presentation.detail}

); } // Any message can be truncated on a narrow screen, so it stays reachable. const expandable = extra.length > 0 || Boolean(presentation.detail) || expandedText !== presentation.detail; const chip = ( <> {presentation.label}{presentation.active && presentation.tone === "progress" ? "…" : ""} {presentation.detail ? ( {presentation.detail} ) : null} {expandable ? ( ) : null} ); const chipClass = cn( "inline-flex min-h-6 max-w-full min-w-0 items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs transition-opacity duration-(--motion-status-notice-fade)", TONE_CLASS[presentation.tone], resolved && "opacity-60", ); return (
{expandable ? ( ) : ( {chip} )} {expandable && open ? (
{expandedText ?

{expandedText}

: null}
) : null}
); }