{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "tool-chips", "type": "registry:component", "title": "Tool Chips", "description": "Code edits and tool calls as compact chips.", "registryDependencies": [ "https://www.beautifului.dev/r/foundation.json" ], "files": [ { "path": "components/primitives/ToolChips.tsx", "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\n\n/* ─────────────────────────────────────────────────────────\n * TOOL CHIPS\n * An agent run as compact rows: tool calls with inline\n * chips, then file-diff chips summarizing the edits.\n * Hover a row to reveal its chevron; every row expands\n * to show what the tool actually did.\n * ───────────────────────────────────────────────────────── */\n\nconst STEP_MS = 700;\n\nconst Icons: Record = {\n think: ,\n write: ,\n run: ,\n read: ,\n};\n\nexport type ToolDetailLine = { text: string; tone?: \"add\" };\n\nexport type ToolStep = {\n icon: string;\n label: string;\n chip: string;\n mono: boolean;\n detailMono: boolean;\n detail: ToolDetailLine[];\n};\n\nexport type ToolDiff = { file: string; add: number; del: number };\n\nexport type ToolDiffLine = { text: string; tone: \"add\" | \"del\" | \"ctx\" };\n\nexport type ToolChipsLabels = {\n header: string;\n more: string;\n};\n\nconst DEFAULT_LABELS: ToolChipsLabels = {\n header: \"4 tool calls, 2 messages\",\n more: \"+2 more\",\n};\n\nconst ROWS: ToolStep[] = [\n {\n icon: \"think\", label: \"Thinking\", chip: \"Planning the churn schedule…\", mono: false, detailMono: false,\n detail: [\n { text: \"Weekend demand carries pistachio, so it churns first.\" },\n { text: \"Batch capacity leaves two evening freezer windows.\" },\n ],\n },\n {\n icon: \"write\", label: \"Write 204 lines\", chip: \"ChurnSchedule.tsx\", mono: true, detailMono: true,\n detail: [\n { text: \"+ const windows = slots.filter((s) => s.temp <= -12)\", tone: \"add\" },\n { text: \"+ return schedule(windows, { hero: \\\"pistachio\\\" })\", tone: \"add\" },\n ],\n },\n {\n icon: \"run\", label: \"Rebuild and verify\", chip: \"npm run freeze\", mono: true, detailMono: true,\n detail: [\n { text: \"✓ built in 1.2s\" },\n { text: \"✓ 34 checks passed\" },\n ],\n },\n {\n icon: \"read\", label: \"Read image\", chip: \"flavor-chart.png\", mono: true, detailMono: false,\n detail: [\n { text: \"1280 × 720 · line chart, three summers.\" },\n { text: \"Mint chip trends up 12% through July.\" },\n ],\n },\n];\n\nconst DIFFS: ToolDiff[] = [\n { file: \"flavors.css\", add: 13, del: 0 },\n { file: \"ChurnSchedule.tsx\", add: 74, del: 41 },\n { file: \"menu.ts\", add: 8, del: 2 },\n];\n\n/* hovering a file chip opens its diff — green added, red removed */\nconst DIFF_LINES: Record = {\n \"flavors.css\": [\n { text: \".scoop-card {\", tone: \"ctx\" },\n { text: \" gap: 14px;\", tone: \"del\" },\n { text: \" gap: 12px;\", tone: \"add\" },\n { text: \" container-type: inline-size;\", tone: \"add\" },\n { text: \"}\", tone: \"ctx\" },\n ],\n \"ChurnSchedule.tsx\": [\n { text: \"const slots = coldSlots(week);\", tone: \"ctx\" },\n { text: \"const windows = slots;\", tone: \"del\" },\n { text: \"const windows = slots.filter(\", tone: \"add\" },\n { text: \" (s) => s.temp <= -12,\", tone: \"add\" },\n { text: \");\", tone: \"add\" },\n ],\n \"menu.ts\": [\n { text: \"export const hero = \\\"mint-chip\\\";\", tone: \"del\" },\n { text: \"export const hero = \\\"pistachio\\\";\", tone: \"add\" },\n ],\n};\n\nexport default function ToolChips({\n steps = ROWS,\n diffs = DIFFS,\n diffLines = DIFF_LINES,\n labels,\n className,\n onOpenChange,\n onToggleRow,\n}: {\n /** Accepted for gallery/registry parity; ToolChips has no visual variants. */\n variant?: string;\n steps?: ToolStep[];\n diffs?: ToolDiff[];\n diffLines?: Record;\n labels?: Partial;\n className?: string;\n onOpenChange?: (open: boolean) => void;\n onToggleRow?: (label: string, open: boolean) => void;\n} = {}) {\n const copy = { ...DEFAULT_LABELS, ...labels };\n const [step, setStep] = useState(0);\n const [open, setOpen] = useState(true);\n const [openRows, setOpenRows] = useState>(new Set());\n /* Rendered in a body portal so animated/translated reply wrappers cannot\n * redefine the fixed-position coordinate system. */\n const [preview, setPreview] = useState<{\n file: string;\n x: number;\n top?: number;\n bottom?: number;\n } | null>(null);\n const openPreview = (file: string) => (event: React.SyntheticEvent) => {\n const rect = (event.currentTarget as Element).closest(\"[data-diffchip]\")!.getBoundingClientRect();\n const previewHeight = 38 + (diffLines[file]?.length ?? 0) * 19;\n const fitsBelow = rect.bottom + 6 + previewHeight <= window.innerHeight - 12;\n setPreview({\n file,\n x: Math.max(12, Math.min(rect.left, window.innerWidth - 300)),\n ...(fitsBelow\n ? { top: rect.bottom + 6 }\n : { bottom: window.innerHeight - rect.top + 6 }),\n });\n };\n const closePreview = (file: string) => () =>\n setPreview((current) => (current?.file === file ? null : current));\n const total = steps.length + 1; // rows, then diff chips\n\n useEffect(() => {\n if (step >= total) return;\n const t = setTimeout(() => setStep((s) => s + 1), STEP_MS);\n return () => clearTimeout(t);\n }, [step, total]);\n\n const toggleRow = (label: string) =>\n setOpenRows((current) => {\n const next = new Set(current);\n next.has(label) ? next.delete(label) : next.add(label);\n onToggleRow?.(label, next.has(label));\n return next;\n });\n\n return (\n
\n {/* collapsed run header */}\n \n setOpen((current) => {\n onOpenChange?.(!current);\n return !current;\n })\n }\n className=\"-mx-1.5 flex w-fit items-center gap-1.5 rounded-control px-1.5 py-1 text-[12.5px] text-ink-2 transition-colors duration-100 hover:bg-hover-2\"\n >\n \n \n \n {copy.header}\n \n\n {/* tool call rows */}\n
\n {/* -mx-1 + px-1.5 keeps content at the same x while giving the\n row hover pills room inside this overflow-hidden clip box */}\n
\n
\n {steps.slice(0, step).map((row) => {\n const rowOpen = openRows.has(row.label);\n return (\n
\n toggleRow(row.label)}\n className=\"group/row -mx-[3px] flex h-7 w-[calc(100%+6px)] min-w-0 items-center gap-2 rounded-control px-[3px] text-left transition-colors duration-100 hover:bg-hover-2\"\n >\n \n \n {Icons[row.icon]}\n \n \n \n \n \n {row.label}\n \n {row.chip}\n \n \n\n {/* expanded detail */}\n \n
\n
\n {row.detail.map((line) => (\n \n {line.text}\n \n ))}\n
\n
\n
\n
\n );\n })}\n
\n\n {/* file-diff chips */}\n {step >= total && (\n
\n {diffs.map((d, i) => (\n \n \n {d.file}\n +{d.add}\n {d.del > 0 && −{d.del}}\n \n\n \n ))}\n \n {copy.more}\n \n
\n )}\n
\n
\n {preview && typeof document !== \"undefined\" && createPortal(\n \n
\n {preview.file}\n \n +{diffs.find((diff) => diff.file === preview.file)?.add}\n {(diffs.find((diff) => diff.file === preview.file)?.del ?? 0) > 0 && (\n −{diffs.find((diff) => diff.file === preview.file)?.del}\n )}\n \n
\n
\n {(diffLines[preview.file] ?? []).map((line, index) => (\n \n {line.tone === \"add\" ? \"+\" : line.tone === \"del\" ? \"−\" : \" \"}\n {line.text}\n
\n ))}\n \n ,\n document.body,\n )}\n \n );\n}\n", "type": "registry:component", "target": "components/primitives/ToolChips.tsx" } ] }