{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"tool-timeline","type":"registry:ui","title":"ToolTimeline","description":"Compact session summary of an agent's tool calls: one row per action on a vertical rail, with the target it touched, its diff stats, and when it ran","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["ai-agents"],"registryDependencies":["https://fancy-ui.rama.app/r/internals.json","https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/sound.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/tool-timeline/ToolTimeline.tsx","content":"\"use client\";\n\nimport { forwardRef } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../../utils\";\nimport { formatRelativeTime } from \"../../internals/relative-time\";\nimport { useNow } from \"../../internals/use-elapsed\";\nimport { useSoundCue } from \"../../sound/use-sound\";\nimport type { ToolTimelineItemData } from \"../../internals/ai-types\";\nimport \"./tool-timeline.css\";\n\n/**\n * Props for ToolTimeline\n */\nexport interface ToolTimelineProps {\n\t/** The agent's activity log, oldest first */\n\titems: ToolTimelineItemData[];\n\t/** Called when a row is activated; supplying it turns every row into a button */\n\tonSelect?: (item: ToolTimelineItemData, index: number) => void;\n\t/** Replaces the built-in row body, keeping the rail and its dot */\n\titem?: (item: ToolTimelineItemData, index: number) => ReactNode;\n\t/** Tighter rows with the detail line dropped */\n\tcompact?: boolean;\n\t/** Accessible name for the list */\n\tlabel?: string;\n\t/** Additional CSS classes */\n\tclassName?: string;\n\t/**\n\t * Plays the matching interface cue through the sound controller. Off\n\t * by default; only audible once the user has enabled sound.\n\t */\n\tsound?: boolean;\n}\n\nfunction iso(timestamp: Date | number): string {\n\tconst date = timestamp instanceof Date ? timestamp : new Date(timestamp);\n\treturn Number.isFinite(date.getTime()) ? date.toISOString() : \"\";\n}\n\n/**\n * ToolTimeline renders an agent's activity log as a vertical rail of dated,\n * diffed rows. A shared clock (`useNow`) keeps relative timestamps fresh\n * while the list stays mounted, at the cost of one interval for the whole\n * list rather than one per row.\n */\nexport const ToolTimeline = forwardRef<HTMLDivElement, ToolTimelineProps>(function ToolTimeline(\n\t{ items, onSelect, item, compact = false, label = \"Activity\", className, sound = false },\n\tref\n) {\n\tconst now = useNow();\n\tconst playCue = useSoundCue(sound);\n\n\t// A row activation is a fresh gesture every time — an appended entry or\n\t// the shared clock ticking never routes through here, only an actual\n\t// pick does.\n\tfunction selectEntry(entry: ToolTimelineItemData, index: number) {\n\t\tplayCue(\"select\");\n\t\tonSelect?.(entry, index);\n\t}\n\n\treturn (\n\t\t<div\n\t\t\tref={ref}\n\t\t\tclassName={cn(\n\t\t\t\t\"ft-tooltimeline w-full text-sm\",\n\t\t\t\tcompact && \"ft-tooltimeline-compact\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t>\n\t\t\t<ol className=\"ft-tooltimeline-list\" aria-label={label}>\n\t\t\t\t{items.map((entry, index) => {\n\t\t\t\t\tconst body = item ? (\n\t\t\t\t\t\titem(entry, index)\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t<span className=\"ft-tooltimeline-text min-w-0 flex-1\">\n\t\t\t\t\t\t\t\t<span className=\"flex min-w-0 items-baseline gap-1.5\">\n\t\t\t\t\t\t\t\t\t<span className=\"ft-tooltimeline-verb font-medium\">{entry.verb}</span>\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\tclassName=\"ft-tooltimeline-target truncate font-mono text-[0.8125rem]\"\n\t\t\t\t\t\t\t\t\t\ttitle={entry.target}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{entry.target}\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t{!compact && entry.detail ? (\n\t\t\t\t\t\t\t\t\t<span className=\"ft-tooltimeline-detail text-muted-foreground mt-0.5 block truncate\">\n\t\t\t\t\t\t\t\t\t\t{entry.detail}\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t</span>\n\n\t\t\t\t\t\t\t<span className=\"ft-tooltimeline-meta flex shrink-0 items-baseline gap-2\">\n\t\t\t\t\t\t\t\t{entry.additions != null ? (\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\tclassName=\"ft-tooltimeline-additions tabular-nums\"\n\t\t\t\t\t\t\t\t\t\trole=\"img\"\n\t\t\t\t\t\t\t\t\t\taria-label={`${entry.additions} additions`}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t+{entry.additions}\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t{entry.deletions != null ? (\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\tclassName=\"ft-tooltimeline-deletions tabular-nums\"\n\t\t\t\t\t\t\t\t\t\trole=\"img\"\n\t\t\t\t\t\t\t\t\t\taria-label={`${entry.deletions} deletions`}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t−{entry.deletions}\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t{entry.timestamp != null ? (\n\t\t\t\t\t\t\t\t\t<time\n\t\t\t\t\t\t\t\t\t\tclassName=\"ft-tooltimeline-time text-muted-foreground tabular-nums\"\n\t\t\t\t\t\t\t\t\t\tdateTime={iso(entry.timestamp)}\n\t\t\t\t\t\t\t\t\t\ttitle={iso(entry.timestamp)}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{formatRelativeTime(entry.timestamp, { now })}\n\t\t\t\t\t\t\t\t\t</time>\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</>\n\t\t\t\t\t);\n\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<li key={entry.id} className=\"ft-tooltimeline-row\">\n\t\t\t\t\t\t\t<span className=\"ft-tooltimeline-dot\" aria-hidden=\"true\" />\n\n\t\t\t\t\t\t\t{onSelect ? (\n\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\tclassName=\"ft-tooltimeline-body hover:bg-muted/60 focus-visible:ring-ring flex w-full cursor-pointer items-baseline gap-3 rounded-md text-left transition-colors focus-visible:ring-1 focus-visible:outline-none\"\n\t\t\t\t\t\t\t\t\tonClick={() => selectEntry(entry, index)}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{body}\n\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<span className=\"ft-tooltimeline-body flex items-baseline gap-3 rounded-md\">\n\t\t\t\t\t\t\t\t\t{body}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</li>\n\t\t\t\t\t);\n\t\t\t\t})}\n\t\t\t</ol>\n\t\t</div>\n\t);\n});\n","type":"registry:ui","target":"@ui/fancy/components/tool-timeline/ToolTimeline.tsx"},{"path":"react/src/components/tool-timeline/index.ts","content":"\"use client\";\n\nexport { ToolTimeline } from \"./ToolTimeline\";\nexport type { ToolTimelineProps } from \"./ToolTimeline\";\n","type":"registry:ui","target":"@ui/fancy/components/tool-timeline/index.ts"},{"path":"react/src/components/tool-timeline/tool-timeline.css","content":".ft-tooltimeline {\n\t/* Rail and dot are mixes of currentColor, so one set of values reads\n\t   correctly in both light and dark themes. The stat colours cannot do that\n\t   — they are text, and no single token clears 4.5:1 on both white and near\n\t   black — so they are read at their point of use off the `--ft-status-*`\n\t   vocabulary this family shares, whose defaults are `light-dark()` pairs. */\n\t--ft-tooltimeline-rail: color-mix(in oklab, currentColor 16%, transparent);\n\t--ft-tooltimeline-dot: color-mix(in oklab, currentColor 38%, transparent);\n\t--ft-tooltimeline-row-pad: 0.375rem;\n\t--ft-tooltimeline-enter-duration: 320ms;\n\n\t/* Geometry shared by the rail, the dot, and the row indent, so moving the\n\t   rail moves all three together. */\n\t--ft-tooltimeline-dot-size: 0.5rem;\n\t--ft-tooltimeline-rail-x: 0.25rem;\n}\n\n.ft-tooltimeline-compact {\n\t--ft-tooltimeline-row-pad: 0.125rem;\n}\n\n.ft-tooltimeline-list {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.ft-tooltimeline-row {\n\tposition: relative;\n\t/* Leaves room for the rail; the body adds the rest of the text indent. */\n\tpadding-left: 0.875rem;\n}\n\n/* The connecting line is drawn per row rather than once behind the list, so it\n   stretches with the row it belongs to and needs no height measurement. */\n.ft-tooltimeline-row::before {\n\tcontent: \"\";\n\tposition: absolute;\n\ttop: 0;\n\tbottom: 0;\n\tleft: calc(var(--ft-tooltimeline-rail-x) - 1px);\n\twidth: 2px;\n\tbackground: var(--ft-tooltimeline-rail);\n}\n\n/* Trim the line at the first and last dot instead of letting it overshoot the\n   list into empty space. */\n.ft-tooltimeline-row:first-child::before {\n\ttop: calc(var(--ft-tooltimeline-row-pad) + 0.625rem);\n}\n\n.ft-tooltimeline-row:last-child::before {\n\tbottom: auto;\n\theight: calc(var(--ft-tooltimeline-row-pad) + 0.625rem);\n}\n\n.ft-tooltimeline-row:only-child::before {\n\tdisplay: none;\n}\n\n.ft-tooltimeline-dot {\n\tposition: absolute;\n\t/* Aligned to the middle of the first text line: half a 1.25rem line box\n\t   minus half the dot. */\n\ttop: calc(var(--ft-tooltimeline-row-pad) + 0.375rem);\n\tleft: 0;\n\twidth: var(--ft-tooltimeline-dot-size);\n\theight: var(--ft-tooltimeline-dot-size);\n\tborder-radius: 9999px;\n\tbackground: var(--ft-tooltimeline-dot);\n}\n\n.ft-tooltimeline-body {\n\tpadding: var(--ft-tooltimeline-row-pad) 0.5rem;\n}\n\n.ft-tooltimeline-text {\n\tmin-width: 0;\n}\n\n.ft-tooltimeline-meta {\n\tmargin-left: auto;\n}\n\n.ft-tooltimeline-additions {\n\tcolor: var(\n\t\t--ft-tooltimeline-additions,\n\t\tvar(--ft-status-done, light-dark(oklch(0.5 0.14 145), oklch(0.72 0.15 145)))\n\t);\n}\n\n.ft-tooltimeline-deletions {\n\tcolor: var(\n\t\t--ft-tooltimeline-deletions,\n\t\tvar(--ft-status-error, light-dark(oklch(0.5 0.19 25), oklch(0.7 0.18 25)))\n\t);\n}\n\n/*\n * The entrance lives entirely inside `no-preference`, so reduced motion is not\n * a degraded variant to keep in sync: with the rule gone a new row simply\n * appears at its final position and opacity.\n */\n@media (prefers-reduced-motion: no-preference) {\n\t.ft-tooltimeline-row {\n\t\tanimation: ft-tooltimeline-enter var(--ft-tooltimeline-enter-duration)\n\t\t\tcubic-bezier(0.16, 1, 0.3, 1) both;\n\t}\n}\n\n@keyframes ft-tooltimeline-enter {\n\tfrom {\n\t\topacity: 0;\n\t\ttransform: translateY(-0.25rem);\n\t}\n\tto {\n\t\topacity: 1;\n\t\ttransform: none;\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/tool-timeline/tool-timeline.css"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/tool-timeline?framework=react"}}