Agent Activity
One feed for everything an agent does while it works: free text, reasoning steps, web searches with results, read, edit and run tool calls, or compact trace entries, shown alone or interleaved in order, then folded into a one-line summary.
Streaming Text
index.tsxFree-form thinking pours into a height-capped view; when it ends, the full log stays behind a disclosure labelled with how long it took.
Reasoning Steps
index.tsxA checklist of reasoning steps marked done, in progress or waiting, each with optional details on the right.
Web Search
index.tsxShows the query, result rows that appear one by one, and a count of the results left unlisted.
Tool Calls
index.tsxLists read, edit and run actions with file or command targets in monospace and optional added and removed line counts.
Mixed Activity
index.tsxReasoning, searches and tool calls interleave in one ordered feed, and the view glides to keep the latest event in sight.
Agent Trace
index.tsxMessages and actions stream into a tight ledger, and the finished run collapses to a count of its tool calls and messages.
API Reference
itemsAgentActivityItem[]Chronological activity entries. Append or update items as events stream.
—contentType?"text" | "search" | "mixed" | "step" | "tool" | "trace"Expected activity kind before the first streamed item arrives.
—status?"complete" | "working"Current run phase. Active runs always stay expanded.
workingduration?numberElapsed run time, in seconds. Used by the step-only summary.
0open?booleanControlled expanded state used after the run completes.
—defaultOpen?booleanInitial expanded state used after the run completes.
falseonOpenChange?((open: boolean) => void)Called when the completed activity disclosure changes state.
—collapseOnComplete?booleanCollapse the disclosure when status changes from working to complete.
trueactiveLabel?ReactNodeOptional label shown while the run is active.
—summary?ReactNodeOptional completed summary. Derived from the item types by default.
—renderWorkingStatus?((context: { label: ReactNode; duration: number; }) => ReactNode)Optional renderer for the contents of the active status row.
—renderCompletedStatus?((context: { summary: ReactNode; duration: number; }) => ReactNode)Optional renderer for the contents before the built-in disclosure chevron.
—maxHeight?numberMaximum visible activity height before the stream begins gliding.
208className?string—contentClassName?string—Install
Add it with the shadcn CLI, or copy the source manually.
shadcn init? You are set. Theme setupInstall dependencies
npm i clsx lucide-react motion tailwind-mergeAdd util files
// Shared motion tokens. Easing curves mirror the CSS custom properties in
// globals.css; springs are the canonical physics used across components.
// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.
export const EASE_OUT = [0.16, 1, 0.3, 1] as const;
export const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;
export const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;
/** CSS string form of EASE_OUT for inline style transitions. */
export const EASE_OUT_CSS = "cubic-bezier(0.16, 1, 0.3, 1)";
/** Press feedback on buttons and other tappable surfaces. */
export const SPRING_PRESS = {
type: "spring",
stiffness: 500,
damping: 30,
mass: 0.6,
} as const;
/** Content swaps — label/icon slots trading places inside a control. */
export const SPRING_SWAP = {
type: "spring",
stiffness: 460,
damping: 30,
mass: 0.55,
} as const;
/** Overlay panel entrances — modals and sheets summoned by pointer. */
export const SPRING_PANEL = {
type: "spring",
stiffness: 420,
damping: 40,
mass: 0.5,
} as const;
/** Shared-layout glides — pills, indicators and panels morphing between positions. */
export const SPRING_LAYOUT = {
type: "spring",
stiffness: 360,
damping: 32,
mass: 0.6,
} as const;
/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */
export const SPRING_MOUSE = {
stiffness: 200,
damping: 15,
mass: 0.3,
} as const;
/** Dragged handles and fills (sliders) — critically damped `useSpring` config,
* so the value follows the pointer butterily and never rebounds off an end. */
export const SPRING_GLIDE = {
stiffness: 700,
damping: 50,
mass: 0.5,
} as const;
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
import type { CSSProperties } from "react";
export const TEXT_SHIMMER_KEYFRAMES =
"@keyframes karaui-text-shimmer{from{background-position:200% 0}to{background-position:-200% 0}}";
export const TEXT_SHIMMER_CLASS_NAME =
"bg-[length:200%_100%] bg-clip-text text-transparent bg-[linear-gradient(110deg,var(--muted-foreground)_30%,var(--foreground)_50%,var(--muted-foreground)_70%)]";
export function textShimmerStyle(duration: number): CSSProperties {
return {
animation: `karaui-text-shimmer ${duration}s linear infinite`,
};
}
Copy the source code
"use client";
// karaui.kamisama-v0.my.id/components/agents/agent-activity
import { ChevronDown } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import {
type ReactNode,
useCallback,
useEffect,
useId,
useLayoutEffect,
useRef,
useState,
} from "react";
import { ThinkingShimmer } from "@/components/agents/loading-states/thinking-shimmer";
import { AgentDisclosure } from "@/components/agents/agent-disclosure";
import {
EASE_OUT,
SPRING_LAYOUT,
SPRING_SWAP,
} from "@/lib/ease";
import { cn } from "@/lib/utils";
import { ActivityRow } from "./activity-row";
import type {
AgentActivityContentType,
AgentActivityItem,
AgentActivityProps,
} from "./types";
export type {
AgentActivityContentType,
AgentActivityItem,
AgentActivityProps,
AgentActivitySearch,
AgentActivityStatus,
AgentActivityStep,
AgentActivityText,
AgentActivityTool,
AgentActivityTrace,
AgentSearchResult,
AgentStepStatus,
AgentTraceKind,
} from "./types";
function formatDuration(duration: number) {
const seconds = Math.max(0, Math.round(duration));
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
const remainder = seconds % 60;
return remainder === 0 ? `${minutes}m` : `${minutes}m ${remainder}s`;
}
function useControllableOpen({
open,
defaultOpen,
onOpenChange,
}: {
open?: boolean;
defaultOpen: boolean;
onOpenChange?: (open: boolean) => void;
}) {
const [internalOpen, setInternalOpen] = useState(defaultOpen);
const controlled = open !== undefined;
const currentOpen = open ?? internalOpen;
const setOpen = useCallback(
(next: boolean) => {
if (!controlled) setInternalOpen(next);
onOpenChange?.(next);
},
[controlled, onOpenChange],
);
return [currentOpen, setOpen] as const;
}
function getContentType(items: AgentActivityItem[]): AgentActivityContentType {
const first = items[0]?.type;
return first && items.every((item) => item.type === first) ? first : "mixed";
}
function getActiveLabel(type: AgentActivityContentType) {
if (type === "search") return "Searching the web…";
if (type === "tool") return "Running tools…";
if (type === "trace") return "Working through the run…";
if (type === "mixed") return "Working through it…";
return "Thinking…";
}
function getSummary(
type: AgentActivityContentType,
items: AgentActivityItem[],
duration: number,
): ReactNode {
if (type === "step" || type === "text") {
return (
<>
Thought for <span className="tabular-nums">{formatDuration(duration)}</span>
</>
);
}
if (type === "search") return "Searched the web";
if (type === "tool") {
return `Ran ${items.length} ${items.length === 1 ? "tool" : "tools"}`;
}
if (type === "trace") {
const messages = items.filter(
(item) =>
item.type === "trace" &&
(item.kind === "thinking" || item.kind === "message"),
).length;
const tools = items.length - messages;
return `${tools} ${tools === 1 ? "tool call" : "tool calls"}, ${messages} ${messages === 1 ? "message" : "messages"}`;
}
return `Completed ${items.length} ${items.length === 1 ? "step" : "steps"}`;
}
export function AgentActivity({
items,
contentType: initialContentType,
status = "working",
duration = 0,
open,
defaultOpen = false,
onOpenChange,
collapseOnComplete = true,
activeLabel,
summary,
renderWorkingStatus,
renderCompletedStatus,
maxHeight = 208,
className,
contentClassName,
}: AgentActivityProps) {
const reduce = useReducedMotion() ?? false;
const baseId = useId();
const triggerId = `${baseId}-trigger`;
const contentId = `${baseId}-content`;
const contentRef = useRef<HTMLDivElement>(null);
const viewportRef = useRef<HTMLDivElement>(null);
const previousStatus = useRef(status);
const [contentHeight, setContentHeight] = useState(0);
const [currentOpen, setOpen] = useControllableOpen({
open,
defaultOpen,
onOpenChange,
});
const working = status === "working";
const expanded = working || currentOpen;
const contentType = items.length
? getContentType(items)
: (initialContentType ?? "mixed");
const cappedHeight = Math.min(contentHeight, Math.max(0, maxHeight));
const viewportHeight = working ? Math.max(0, maxHeight) : cappedHeight;
const capped = contentHeight > maxHeight;
const streamOffset = working
? Math.min(0, viewportHeight - contentHeight)
: 0;
useLayoutEffect(() => {
const node = contentRef.current;
if (!node) return;
const measure = () => setContentHeight(node.offsetHeight);
measure();
if (typeof ResizeObserver === "undefined") return;
const observer = new ResizeObserver(measure);
observer.observe(node);
return () => observer.disconnect();
}, []);
useEffect(() => {
if (previousStatus.current === "working" && status === "complete") {
setOpen(!collapseOnComplete);
}
previousStatus.current = status;
}, [collapseOnComplete, setOpen, status]);
const toggle = () => {
const next = !currentOpen;
setOpen(next);
if (next) requestAnimationFrame(() => viewportRef.current?.scrollTo({ top: 0 }));
};
const liveLabel = activeLabel ?? getActiveLabel(contentType);
const completedSummary = summary ?? getSummary(contentType, items, duration);
const maskImage = capped
? working
? "linear-gradient(to bottom, transparent, black 12px)"
: "linear-gradient(to bottom, transparent, black 12px, black calc(100% - 12px), transparent)"
: undefined;
return (
<div
data-state={working ? "working" : expanded ? "open" : "closed"}
data-content={contentType}
aria-busy={working}
className={cn("w-full text-sm", className)}
>
{working ? (
<div
id={triggerId}
role="status"
className="flex h-7 min-w-0 items-center text-muted-foreground"
>
{renderWorkingStatus
? renderWorkingStatus({ label: liveLabel, duration })
: <ThinkingShimmer>{liveLabel}</ThinkingShimmer>}
</div>
) : (
<button
id={triggerId}
type="button"
aria-expanded={expanded}
aria-controls={contentId}
onClick={toggle}
className="group flex h-7 min-w-0 items-center gap-1.5 rounded-md text-left font-medium text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
<span className="truncate">
{renderCompletedStatus
? renderCompletedStatus({ summary: completedSummary, duration })
: completedSummary}
</span>
<motion.span
aria-hidden="true"
animate={{ rotate: expanded ? 180 : 0 }}
transition={reduce ? { duration: 0 } : SPRING_SWAP}
className="inline-flex shrink-0 text-muted-foreground/70 group-hover:text-foreground"
>
<ChevronDown className="size-3.5" />
</motion.span>
</button>
)}
<AgentDisclosure
id={contentId}
role="region"
aria-labelledby={triggerId}
open={expanded}
openHeight={viewportHeight}
>
<div
ref={viewportRef}
className={cn(
"scrollbar-hide pr-1",
capped && expanded && !working ? "overflow-y-auto" : "overflow-y-hidden",
)}
style={{ height: viewportHeight, maskImage, WebkitMaskImage: maskImage }}
>
<motion.div
ref={contentRef}
role="list"
initial={false}
animate={{ y: streamOffset }}
transition={reduce ? { duration: 0 } : SPRING_LAYOUT}
className={cn("space-y-0.5 py-2", contentClassName)}
>
<AnimatePresence mode="popLayout">
{items.map((item) => (
<motion.div
layout="position"
key={item.id}
role="listitem"
initial={reduce ? { opacity: 1 } : { opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
exit={reduce ? { opacity: 0 } : { opacity: 0, y: -3 }}
transition={
reduce
? { duration: 0 }
: {
opacity: { duration: 0.18, ease: EASE_OUT },
y: SPRING_LAYOUT,
layout: SPRING_LAYOUT,
}
}
>
<ActivityRow item={item} />
</motion.div>
))}
</AnimatePresence>
</motion.div>
</div>
</AgentDisclosure>
</div>
);
}
import {
Check,
Circle,
FileText,
Globe2,
ImageIcon,
MessageSquare,
PencilLine,
Search,
Sparkles,
SquareTerminal,
Wrench,
} from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { EASE_OUT, SPRING_LAYOUT } from "@/lib/ease";
import { cn } from "@/lib/utils";
import type {
AgentActivityItem,
AgentActivitySearch,
AgentActivityStep,
AgentActivityText,
AgentActivityTool,
AgentActivityTrace,
AgentSearchResult,
} from "./types";
function StepRow({ item }: { item: AgentActivityStep }) {
const state = item.status ?? "complete";
return (
<div className="flex min-h-7 items-start gap-2.5 rounded-md px-1.5 py-1">
<span
aria-hidden="true"
className="mt-0.5 grid size-4 shrink-0 place-items-center text-muted-foreground/70"
>
{state === "complete" ? (
<Check className="size-4" strokeWidth={1.8} />
) : state === "active" ? (
<span className="relative grid size-3 place-items-center">
<motion.span
className="absolute inset-0 rounded-full bg-foreground/10"
animate={{ opacity: [0.35, 0.8, 0.35] }}
transition={{ duration: 1.5, repeat: Number.POSITIVE_INFINITY }}
/>
<span className="size-1.5 rounded-full bg-foreground/60" />
</span>
) : (
<Circle className="size-3" strokeWidth={1.5} />
)}
</span>
<span
className={cn(
"min-w-0 flex-1 leading-5",
state === "pending" ? "text-muted-foreground/55" : "text-foreground/90",
)}
>
{item.label}
</span>
{item.meta ? (
<span className="shrink-0 leading-5 text-muted-foreground/55">
{item.meta}
</span>
) : null}
</div>
);
}
function TextRow({ item }: { item: AgentActivityText }) {
return (
<div className="rounded-md px-1.5 py-1 leading-5 text-muted-foreground">
{item.content}
</div>
);
}
function SearchResultRow({
result,
}: {
result: AgentSearchResult;
}) {
const content = (
<>
<span
aria-hidden="true"
className="grid size-5 shrink-0 place-items-center text-muted-foreground"
>
{result.icon ?? <Globe2 className="size-3" strokeWidth={2} />}
</span>
<span className="min-w-0 truncate font-medium text-foreground/90">
{result.title}
</span>
{result.domain ? (
<span className="min-w-0 truncate text-muted-foreground/55">
{result.domain}
</span>
) : null}
</>
);
const className = cn(
"flex min-h-7 items-center gap-2 rounded-md px-1.5 py-1 text-left outline-none transition-colors",
result.url && "focus-visible:ring-2 focus-visible:ring-ring",
);
return result.url ? (
<a href={result.url} className={className}>
{content}
</a>
) : (
<div className={className}>{content}</div>
);
}
function SearchRow({ item }: { item: AgentActivitySearch }) {
const reduce = useReducedMotion() ?? false;
const enter = reduce ? { opacity: 1 } : { opacity: 0, y: 6 };
const visible = { opacity: 1, y: 0 };
const exit = reduce ? { opacity: 0 } : { opacity: 0, y: -3 };
const transition = reduce
? { duration: 0 }
: {
opacity: { duration: 0.18, ease: EASE_OUT },
y: SPRING_LAYOUT,
layout: SPRING_LAYOUT,
};
return (
<div className="space-y-0.5">
<div className="flex min-h-7 items-center gap-2.5 rounded-md px-1.5 py-1 text-muted-foreground">
<Search aria-hidden="true" className="size-4 shrink-0" strokeWidth={1.7} />
<span className="min-w-0 truncate">{item.query}</span>
</div>
{item.results?.length ? (
<div className="space-y-0.5 pl-4">
<AnimatePresence initial mode="popLayout">
{item.results.map((result) => (
<motion.div
layout="position"
key={result.id}
initial={enter}
animate={visible}
exit={exit}
transition={transition}
>
<SearchResultRow result={result} />
</motion.div>
))}
</AnimatePresence>
</div>
) : null}
<AnimatePresence initial>
{item.moreCount ? (
<motion.div
key="more-results"
initial={enter}
animate={visible}
exit={exit}
transition={transition}
className="px-1.5 py-1 pl-8 text-muted-foreground/55"
>
+{item.moreCount} more
</motion.div>
) : null}
</AnimatePresence>
</div>
);
}
function ActionIcon({ action }: { action: string }) {
if (action === "read") return <FileText className="size-4" />;
if (action === "edit" || action === "write") {
return <PencilLine className="size-4" />;
}
if (action === "run") return <SquareTerminal className="size-4" />;
return <Wrench className="size-4" />;
}
function ToolRow({ item }: { item: AgentActivityTool }) {
const action = item.action.charAt(0).toUpperCase() + item.action.slice(1);
return (
<div className="flex min-h-8 min-w-0 items-center gap-2.5 rounded-md px-1.5 py-0.5 leading-5">
<span
aria-hidden="true"
className="grid size-4 shrink-0 place-items-center text-muted-foreground/70"
>
<ActionIcon action={item.action} />
</span>
<span className="shrink-0 font-medium text-foreground/90">{action}</span>
<span className="min-w-0 flex-1 truncate rounded-lg bg-muted/80 px-2.5 py-1 font-mono text-xs text-muted-foreground/70">
{item.target}
</span>
{typeof item.additions === "number" || typeof item.deletions === "number" ? (
<span className="flex shrink-0 items-center gap-2 font-mono tabular-nums">
{typeof item.additions === "number" ? (
<span className="text-emerald-500">+{item.additions}</span>
) : null}
{typeof item.deletions === "number" ? (
<span className="text-rose-500">−{item.deletions}</span>
) : null}
</span>
) : null}
</div>
);
}
function TraceIcon({ kind }: { kind: AgentActivityTrace["kind"] }) {
if (kind === "thinking") return <Sparkles className="size-4" />;
if (kind === "message") return <MessageSquare className="size-4" />;
if (kind === "write") return <PencilLine className="size-4" />;
if (kind === "run") return <SquareTerminal className="size-4" />;
if (kind === "read") return <ImageIcon className="size-4" />;
return <Wrench className="size-4" />;
}
function TraceRow({ item }: { item: AgentActivityTrace }) {
return (
<div className="grid min-h-8 grid-cols-[1rem_auto_minmax(0,1fr)] items-center gap-2.5 rounded-md px-1.5 py-0.5">
<span
aria-hidden="true"
className="grid size-4 place-items-center text-muted-foreground/70"
>
{item.icon ?? <TraceIcon kind={item.kind} />}
</span>
<span className="font-medium text-foreground/90">{item.label}</span>
{item.detail ? (
<span className="min-w-0 truncate rounded-lg bg-muted/80 px-2.5 py-1 font-mono text-xs text-muted-foreground/70">
{item.detail}
</span>
) : (
<span />
)}
</div>
);
}
export function ActivityRow({ item }: { item: AgentActivityItem }) {
if (item.type === "text") return <TextRow item={item} />;
if (item.type === "search") return <SearchRow item={item} />;
if (item.type === "tool") return <ToolRow item={item} />;
if (item.type === "trace") return <TraceRow item={item} />;
return <StepRow item={item} />;
}
import type { ReactNode } from "react";
export type AgentActivityStatus = "working" | "complete";
export type AgentStepStatus = "pending" | "active" | "complete";
export interface AgentActivityStep {
id: string;
type: "step";
label: ReactNode;
status?: AgentStepStatus;
meta?: ReactNode;
}
export interface AgentActivityText {
id: string;
type: "text";
content: ReactNode;
}
export interface AgentSearchResult {
id: string;
title: ReactNode;
domain?: ReactNode;
url?: string;
icon?: ReactNode;
}
export interface AgentActivitySearch {
id: string;
type: "search";
query: ReactNode;
results?: AgentSearchResult[];
moreCount?: number;
}
export interface AgentActivityTool {
id: string;
type: "tool";
action: "read" | "edit" | "run" | (string & {});
target: ReactNode;
additions?: number;
deletions?: number;
}
export type AgentTraceKind =
| "thinking"
| "message"
| "write"
| "run"
| "read"
| (string & {});
export interface AgentActivityTrace {
id: string;
type: "trace";
kind: AgentTraceKind;
label: ReactNode;
detail?: ReactNode;
icon?: ReactNode;
}
export type AgentActivityItem =
| AgentActivityStep
| AgentActivityText
| AgentActivitySearch
| AgentActivityTool
| AgentActivityTrace;
export type AgentActivityContentType = AgentActivityItem["type"] | "mixed";
export interface AgentActivityProps {
/** Chronological activity entries. Append or update items as events stream. */
items: AgentActivityItem[];
/** Expected activity kind before the first streamed item arrives. */
contentType?: AgentActivityContentType;
/** Current run phase. Active runs always stay expanded. */
status?: AgentActivityStatus;
/** Elapsed run time, in seconds. Used by the step-only summary. */
duration?: number;
/** Controlled expanded state used after the run completes. */
open?: boolean;
/** Initial expanded state used after the run completes. */
defaultOpen?: boolean;
/** Called when the completed activity disclosure changes state. */
onOpenChange?: (open: boolean) => void;
/** Collapse the disclosure when status changes from working to complete. */
collapseOnComplete?: boolean;
/** Optional label shown while the run is active. */
activeLabel?: ReactNode;
/** Optional completed summary. Derived from the item types by default. */
summary?: ReactNode;
/** Optional renderer for the contents of the active status row. */
renderWorkingStatus?: (context: {
label: ReactNode;
duration: number;
}) => ReactNode;
/** Optional renderer for the contents before the built-in disclosure chevron. */
renderCompletedStatus?: (context: {
summary: ReactNode;
duration: number;
}) => ReactNode;
/** Maximum visible activity height before the stream begins gliding. */
maxHeight?: number;
className?: string;
contentClassName?: string;
}
"use client";
import { motion, type HTMLMotionProps, useReducedMotion } from "motion/react";
import type { CSSProperties } from "react";
import { EASE_OUT } from "@/lib/ease";
import { cn } from "@/lib/utils";
export interface AgentDisclosureProps
extends Omit<HTMLMotionProps<"div">, "animate" | "initial"> {
open: boolean;
openHeight?: CSSProperties["height"];
}
/** Shared transform-only reveal for collapsible agent content. */
export function AgentDisclosure({
open,
openHeight = "auto",
className,
style,
transition,
...props
}: AgentDisclosureProps) {
const reduce = useReducedMotion() ?? false;
return (
<motion.div
{...props}
aria-hidden={!open}
inert={!open}
initial={false}
animate={
reduce
? { opacity: open ? 1 : 0 }
: {
opacity: open ? 1 : 0,
clipPath: open ? "inset(0 0 0% 0)" : "inset(0 0 100% 0)",
y: open ? 0 : -4,
}
}
transition={
transition ?? {
duration: reduce ? 0 : open ? 0.22 : 0.14,
ease: EASE_OUT,
}
}
className={cn("overflow-hidden", className)}
style={{
...style,
height: open ? openHeight : 0,
pointerEvents: open ? undefined : "none",
transformOrigin: "top",
}}
/>
);
}
import type { ReactNode } from "react";
import { TextShimmer } from "@/components/motion/text-shimmer";
import { cn } from "@/lib/utils";
export interface ThinkingShimmerProps {
/** Loading message shown to the user. */
children?: ReactNode;
/** Seconds taken for one shimmer pass. */
duration?: number;
className?: string;
}
export function ThinkingShimmer({
children = "Thinking…",
duration = 1.8,
className,
}: ThinkingShimmerProps) {
return (
<TextShimmer
as="span"
duration={duration}
className={cn("font-medium", className)}
>
{children}
</TextShimmer>
);
}
import { cn } from "@/lib/utils";
import type { ElementType, ReactNode } from "react";
import {
TEXT_SHIMMER_CLASS_NAME,
TEXT_SHIMMER_KEYFRAMES,
textShimmerStyle,
} from "@/lib/text-shimmer";
export interface TextShimmerProps {
children: ReactNode;
as?: ElementType;
duration?: number;
className?: string;
}
export function TextShimmer({ children, as: Comp = "span", duration = 2.5, className }: TextShimmerProps) {
return (
<>
<style>
{TEXT_SHIMMER_KEYFRAMES}
</style>
<Comp
style={textShimmerStyle(duration)}
className={cn(
"inline-block",
TEXT_SHIMMER_CLASS_NAME,
className,
)}
>
{children}
</Comp>
</>
);
}
@karaui not found? Add it to components.json
{
"registries": {
"@karaui": "https://karaui.kamisama-v0.my.id/r/{name}.json"
}
}@karaui is in shadcn's public registry directory, so the latest CLI needs no setup. On an older CLI, merge this into the existing file once and every @karaui/… install resolves.
Composition
Render the feed inside the message content of the turn that produced the work.
Message
└── MessageContent
└── AgentActivityNote: Agent Loading States fills the time before the first event arrives. Todo List keeps planned tasks apart from passing events. Tool Result opens one finished call into its full output.
How it works
Show people how the work is going without handing them the raw internal transcript. Events land in order, are quick to skim, and boil down to a short summary once the run is over.
Updated