Files
inkreach-agent/scripts/beautifului/tool-chips.json
T
Rick Ye a5951849c8 v0.6.0: beautifului 整合 + 会话/工作区/队列/权限/命令/附件全链路
- beautifului.dev 组件直连:Sidebar Nav、Tool Chips、Approval Card(抽屉化)、Streaming/Thinking、Code Block、Selection Actions、Prompt Bar、Search
2026-09-06 09:57:14 +08:00

19 lines
15 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"$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<string, React.ReactNode> = {\n think: <path d=\"M12 2l2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8z\" />,\n write: <g fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5z\" /></g>,\n run: <g fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M4 17l6-5-6-5M12 19h8\" /></g>,\n read: <g fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\" /><path d=\"M14 2v6h6\" /></g>,\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<string, ToolDiffLine[]> = {\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<string, ToolDiffLine[]>;\n labels?: Partial<ToolChipsLabels>;\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<Set<string>>(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 <div className={`min-h-[220px] w-full max-w-80 pb-1${className ? ` ${className}` : \"\"}`}>\n {/* collapsed run header */}\n <button\n type=\"button\"\n aria-expanded={open}\n onClick={() =>\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 <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"transition-transform duration-200\" style={{ transform: open ? \"rotate(0deg)\" : \"rotate(-90deg)\" }}>\n <path d=\"M6 9l6 6 6-6\" />\n </svg>\n <span className=\"tabular-nums\">{copy.header}</span>\n </button>\n\n {/* tool call rows */}\n <div className=\"grid transition-[grid-template-rows,opacity] duration-300\" style={{ gridTemplateRows: open ? \"1fr\" : \"0fr\", opacity: open ? 1 : 0 }}>\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 <div className=\"-mx-1 overflow-hidden px-1.5 pb-1\">\n <div className=\"mt-1.5 flex flex-col gap-1\">\n {steps.slice(0, step).map((row) => {\n const rowOpen = openRows.has(row.label);\n return (\n <div key={row.label} style={{ animation: \"fade-up 300ms cubic-bezier(0.23,1,0.32,1) both\" }}>\n <button\n type=\"button\"\n aria-expanded={rowOpen}\n onClick={() => 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 <span className=\"relative flex size-4 shrink-0 items-center justify-center text-ink-3\">\n <svg\n width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill={row.icon === \"think\" ? \"currentColor\" : \"none\"} stroke=\"currentColor\"\n className={`transition-opacity duration-100 group-hover/row:opacity-0 ${rowOpen ? \"opacity-0\" : \"\"}`}\n >\n {Icons[row.icon]}\n </svg>\n <svg\n width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"\n className={`absolute transition-[opacity,transform] duration-150 group-hover/row:opacity-100 ${rowOpen ? \"opacity-100\" : \"opacity-0\"}`}\n style={{ transform: rowOpen ? \"rotate(0deg)\" : \"rotate(-90deg)\" }}\n >\n <path d=\"M6 9l6 6 6-6\" />\n </svg>\n </span>\n <span className=\"shrink-0 text-[12.5px] font-medium text-ink\">{row.label}</span>\n <span\n className={`inline-flex h-5.5 min-w-0 flex-1 cursor-pointer items-center truncate rounded-chip bg-field px-1.5\n text-[11.5px] text-ink-2 shadow-hairline transition-colors duration-100 hover:bg-hover-2\n ${row.mono ? \"font-mono\" : \"\"}`}\n >\n {row.chip}\n </span>\n </button>\n\n {/* expanded detail */}\n <div\n className=\"grid transition-[grid-template-rows,opacity] duration-300\"\n style={{ gridTemplateRows: rowOpen ? \"1fr\" : \"0fr\", opacity: rowOpen ? 1 : 0, transitionTimingFunction: \"cubic-bezier(0.23, 1, 0.32, 1)\" }}\n >\n <div className=\"min-h-0 overflow-hidden\">\n <div className=\"mt-0.5 mb-1 ml-2 flex flex-col gap-0.5 border-l border-line py-0.5 pl-3.5\">\n {row.detail.map((line) => (\n <span\n key={line.text}\n className={`truncate text-[11.5px] leading-[1.6] ${row.detailMono ? \"font-mono\" : \"\"} ${line.tone === \"add\" ? \"text-green\" : \"text-ink-2\"}`}\n >\n {line.text}\n </span>\n ))}\n </div>\n </div>\n </div>\n </div>\n );\n })}\n </div>\n\n {/* file-diff chips */}\n {step >= total && (\n <div className=\"mt-2.5 flex max-w-full flex-wrap gap-1.5 border-t border-line pt-2.5\">\n {diffs.map((d, i) => (\n <span\n key={d.file}\n data-diffchip\n className=\"relative\"\n onMouseEnter={openPreview(d.file)}\n onMouseLeave={closePreview(d.file)}\n >\n <button\n type=\"button\"\n aria-expanded={preview?.file === d.file}\n aria-label={`Show diff for ${d.file}`}\n onFocus={openPreview(d.file)}\n onBlur={closePreview(d.file)}\n className=\"inline-flex h-7 max-w-full items-center gap-2 rounded-chip\n bg-surface px-2 font-mono text-[11.5px] text-ink shadow-btn\n transition-colors duration-100 hover:bg-hover\"\n style={{ animation: `pop-in 250ms cubic-bezier(0.23,1,0.32,1) ${i * 80}ms both` }}\n >\n <span className=\"min-w-0 truncate\">{d.file}</span>\n <span className=\"shrink-0 text-green tabular-nums\">+{d.add}</span>\n {d.del > 0 && <span className=\"shrink-0 text-red tabular-nums\">−{d.del}</span>}\n </button>\n\n </span>\n ))}\n <button\n type=\"button\"\n className=\"inline-flex h-7 items-center rounded-chip px-1.5 font-mono text-[11.5px] text-ink-3\n underline decoration-transparent underline-offset-2 transition-colors duration-100\n hover:text-ink-2 hover:decoration-current\"\n style={{ animation: `fade-in 300ms ease-out ${diffs.length * 80}ms both` }}\n >\n {copy.more}\n </button>\n </div>\n )}\n </div>\n </div>\n {preview && typeof document !== \"undefined\" && createPortal(\n <div\n className=\"fixed z-50 w-72 overflow-hidden rounded-[10px] bg-surface shadow-overlay\"\n style={{\n left: preview.x,\n top: preview.top,\n bottom: preview.bottom,\n animation: \"pop-in 160ms cubic-bezier(0.23,1,0.32,1) both\",\n transformOrigin: preview.top === undefined ? \"bottom left\" : \"top left\",\n }}\n >\n <div className=\"flex items-center justify-between border-b border-line px-2.5 py-1.5 font-mono text-[11px]\">\n <span className=\"min-w-0 truncate text-ink-2\">{preview.file}</span>\n <span className=\"shrink-0 tabular-nums\">\n <span className=\"text-green\">+{diffs.find((diff) => diff.file === preview.file)?.add}</span>\n {(diffs.find((diff) => diff.file === preview.file)?.del ?? 0) > 0 && (\n <span className=\"text-red\"> −{diffs.find((diff) => diff.file === preview.file)?.del}</span>\n )}\n </span>\n </div>\n <div className=\"py-1 font-mono text-[11px] leading-[1.8]\">\n {(diffLines[preview.file] ?? []).map((line, index) => (\n <div\n key={index}\n className={`flex gap-2 px-2.5 whitespace-pre ${\n line.tone === \"add\"\n ? \"bg-green-tint text-green\"\n : line.tone === \"del\"\n ? \"bg-red-tint text-red\"\n : \"text-ink-2\"\n }`}\n >\n <span className=\"w-3 shrink-0 select-none\">{line.tone === \"add\" ? \"+\" : line.tone === \"del\" ? \"−\" : \" \"}</span>\n <span className=\"min-w-0 truncate\">{line.text}</span>\n </div>\n ))}\n </div>\n </div>,\n document.body,\n )}\n </div>\n );\n}\n",
"type": "registry:component",
"target": "components/primitives/ToolChips.tsx"
}
]
}