import { useCallback, useEffect, useRef, useState, type ReactNode } from "react"; import { Check } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Card } from "@/components/ui/card"; import { IssueReferencePill } from "@/components/IssueReferencePill"; import { Link, useCaseHref } from "@/lib/router"; import { copyTextToClipboard } from "@/lib/clipboard"; import { cn } from "@/lib/utils"; // ----------------------------------------------------------------------------- // CaseFieldsPanel (PAP-12968 §3) — the generic key-value renderer for a case's // `fields` JSON blob. The server stores arbitrary agent-authored JSON, so the UI // renders by *value type* (Postel's law: never crash on unexpected shapes) and // preserves the skill's key insertion order (does NOT alphabetize). // ----------------------------------------------------------------------------- const URL_RE = /^https?:\/\/\S+$/i; const CASE_ID_RE = /^[A-Z][A-Z0-9]*-C\d+$/; const ISSUE_ID_RE = /^[A-Z][A-Z0-9]*-\d+$/; const ISSUE_ID_IN_TEXT_RE = /\b[A-Z][A-Z0-9]*-\d+\b/g; function isPlainObject(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } /** A muted em-dash for null / empty / missing values. */ function EmptyValue() { return —; } function isIssueIdentifierField(fieldKey: string | undefined): boolean { if (!fieldKey) return false; const normalized = fieldKey.toLowerCase().replace(/[^a-z0-9]/g, ""); return normalized.includes("issueidentifier") || normalized.includes("taskidentifier"); } function stringifyCopyValue(value: unknown): string { if (typeof value === "string") return value; if (typeof value === "number" || typeof value === "boolean") return String(value); return JSON.stringify(value, null, 2); } function extractIssueIdentifiers(value: unknown, fieldKey?: string): string[] { if (!isIssueIdentifierField(fieldKey)) { return typeof value === "string" && ISSUE_ID_RE.test(value.trim()) ? [value.trim()] : []; } const identifiers: string[] = []; const add = (candidate: unknown) => { if (typeof candidate !== "string") return; for (const match of candidate.matchAll(ISSUE_ID_IN_TEXT_RE)) identifiers.push(match[0]); }; if (Array.isArray(value)) value.forEach(add); else add(value); return [...new Set(identifiers)]; } function IssueIdentifierValue({ identifiers }: { identifiers: string[] }) { return ( {identifiers.map((identifier) => ( ))} ); } function CopyableCompactValue({ value, children, className, }: { value: unknown; children: ReactNode; className?: string; }) { const text = stringifyCopyValue(value); const [copied, setCopied] = useState(false); const timerRef = useRef>(undefined); useEffect(() => () => clearTimeout(timerRef.current), []); const handleCopy = useCallback(() => { void copyTextToClipboard(text).then(() => { setCopied(true); clearTimeout(timerRef.current); timerRef.current = setTimeout(() => setCopied(false), 1500); }); }, [text]); return ( {copied ? ( Copied ) : null} ); } function StringValue({ value, variant }: { value: string; variant: "compact" | "full" }) { const caseHref = useCaseHref(); const trimmed = value.trim(); if (trimmed === "") return ; if (URL_RE.test(trimmed)) { return ( {trimmed} ↗ ); } if (CASE_ID_RE.test(trimmed)) { return ( {trimmed} ); } if (variant === "compact") { return ( {value} ); } return {value}; } export function CaseFieldValue({ value, fieldKey, variant = "full", }: { value: unknown; fieldKey?: string; variant?: "compact" | "full"; }) { if (value === null || value === undefined) return ; const issueIdentifiers = extractIssueIdentifiers(value, fieldKey); if (issueIdentifiers.length > 0) return ; if (typeof value === "string") return ; if (typeof value === "number") { if (!Number.isFinite(value)) return {String(value)}; if (variant === "compact") { return ( {value.toLocaleString()} ); } return {value.toLocaleString()}; } if (typeof value === "boolean") { return value ? ( ) : ( ); } if (Array.isArray(value)) { if (value.length === 0) return ; if (variant === "compact") { return ( {stringifyCopyValue(value)} ); } return (
{value.map((item, index) => ( {typeof item === "string" || typeof item === "number" || typeof item === "boolean" ? String(item) : JSON.stringify(item)} ))}
); } if (isPlainObject(value)) { const snippet = JSON.stringify(value); if (variant === "compact") { return ( {snippet} ); } return (
        {JSON.stringify(value, null, 2)}
      
); } return {String(value)}; } export function CaseFieldsPanel({ fields }: { fields: Record }) { const entries = Object.entries(fields ?? {}); return (

Fields

from the skill's schema — rendered generically
{entries.length === 0 ? (
No fields set
) : (
{entries.map(([key, value]) => (
{key}
))}
)}
); }