Install

npx @react-native-reusables/cli@latest add https://rnraielements.com/r/nativewind/agent.json

Dependencies

https://reactnativereusables.com/r/nativewind/badge.json
https://reactnativereusables.com/r/nativewind/button.json
https://reactnativereusables.com/r/nativewind/card.json
https://reactnativereusables.com/r/nativewind/icon.json
https://reactnativereusables.com/r/nativewind/text.json
https://rnraielements.com/r/nativewind/persona.json
https://rnraielements.com/r/nativewind/status.json

AI SDK peers:ai@^7.0.89@ai-sdk/react@^4.0.92

Source

agent.tsx
import { Badge } from '@/registry/nativewind/components/ui/badge'; import { Button } from '@/registry/nativewind/components/ui/button'; import { Card } from '@/registry/nativewind/components/ui/card'; import { Icon } from '@/registry/nativewind/components/ui/icon'; import { Text } from '@/registry/nativewind/components/ui/text'; import { cn } from '@/registry/nativewind/lib/utils'; import { CircleCheckIcon, CircleIcon, CirclePauseIcon, CircleXIcon, LoaderCircleIcon, PauseIcon, PlayIcon, SquareIcon, type LucideIcon, } from 'lucide-react-native'; import * as React from 'react'; import { View, type ViewProps } from 'react-native'; import Animated, { ReduceMotion, useAnimatedStyle, useReducedMotion, useSharedValue, withRepeat, withTiming, } from 'react-native-reanimated'; import { Persona } from './persona'; import { agentRunMeta, enabledRunActions, AGENT_RUN_ACTIONS, type AgentRunAction, type AgentRunIconName, type AgentRunStatus, } from './agent.logic'; /** * Agent — the identity/run surface. * * COMPOSITION IS THE POINT: the header is the existing `persona` molecule — avatar, * name, description — never re-implemented, plus a run-state badge whose tones come from * lib/status.ts, the ONE status map. The web original's Agent is a configuration card * (instructions, tools, output schema); the PRD's porting verdict narrows `agent` to an * identity card, and UC-AGENT-05 AC-4 adds what the web card never had: start / pause / * stop controls in thumb reach. The config body (instructions markdown, the tools * accordion, the output schema) is deferred, not dropped — instructions is a markdown * surface (message.tsx's injected-renderer seam), and the schema belongs to * schema-display's own wave; this organism composes the identity and the run. * * THE RUN IS THE CALLER'S. Like every queue and answer in this registry (speech-input * precedent), the component holds no run state: `runStatus` is controlled, * `onRunAction` reports the verb, and the component never advances a run by itself. * With no `onRunAction` wired the control bar does not render at all — a dead row of * buttons teaches nothing. * * The Running badge's loader pulses on RNR Skeleton's own 1000ms signature and is gated * on useReducedMotion, exactly as tool.tsx's clock — the one looping animation this * surface carries. Impossible controls DISABLE rather than vanish (see * enabledRunActions in agent.logic.ts). */ type AgentContextValue = { runStatus: AgentRunStatus; /** Present only when the consumer wired onRunAction. */ fire?: (action: AgentRunAction) => void; }; const AgentContext = React.createContext<AgentContextValue | null>(null); function useAgent() { const ctx = React.useContext(AgentContext); if (!ctx) throw new Error('Agent sub-components must be used within <Agent>'); return ctx; } type AgentProps = Omit<ViewProps, 'children'> & { /** Controlled run state. The caller owns the run — this only mirrors it. */ runStatus?: AgentRunStatus; /** Fires on Start / Pause / Stop. Omit it and the control bar never renders. */ onRunAction?: (action: AgentRunAction) => void; children?: React.ReactNode; }; function Agent({ runStatus = 'idle', onRunAction, className, children, ...props }: AgentProps) { const fire = React.useCallback( (action: AgentRunAction) => { onRunAction?.(action); }, [onRunAction], ); return ( <AgentContext.Provider value={{ runStatus, fire: onRunAction ? fire : undefined }}> <Card className={cn('gap-3 p-3', className)} {...props}> {children} </Card> </AgentContext.Provider> ); } /** iconName (from agent.logic.ts) → Lucide component. Static and exhaustive by type. */ const RUN_ICONS: Record<AgentRunIconName, LucideIcon> = { circle: CircleIcon, 'loader-circle': LoaderCircleIcon, 'circle-pause': CirclePauseIcon, 'circle-x': CircleXIcon, 'circle-check': CircleCheckIcon, }; /** * The run badge. For the running state the loader pulses on RNR Skeleton's signature * (1→0.5, 1000ms, reversed repeat) and stands perfectly still under reduced motion — * tool.tsx's clock, reused as a pattern, not forked into a second tempo. */ function RunBadge({ runStatus }: { runStatus: AgentRunStatus }) { const meta = agentRunMeta(runStatus); const reduced = useReducedMotion(); const looping = runStatus === 'running'; const opacity = useSharedValue(1); React.useEffect(() => { if (!looping || reduced) { opacity.value = 1; return; } opacity.value = withRepeat( withTiming(0.5, { duration: 1000, reduceMotion: ReduceMotion.System }), -1, true, ); }, [looping, reduced, opacity]); const style = useAnimatedStyle(() => ({ opacity: opacity.value })); return ( <Badge variant="outline" accessibilityLabel={`Run status: ${meta.label}`} accessibilityLiveRegion="polite" className="shrink-0 gap-1" > <Animated.View style={style}> <Icon as={RUN_ICONS[meta.iconName]} size={12} className={cn(meta.className)} /> </Animated.View> <Text className={cn('text-xs', meta.className)}>{meta.label}</Text> </Badge> ); } type AgentHeaderProps = { /** Passed straight to the Persona molecule — never re-implemented here. */ name: string; description?: string; avatarUri?: string; className?: string; }; function AgentHeader({ name, description, avatarUri, className }: AgentHeaderProps) { const { runStatus } = useAgent(); return ( <View className={cn('flex-row items-center gap-2', className)}> {/* flex-1 + min-w-0 let the name truncate while the badge keeps its shape. */} <Persona name={name} description={description} avatarUri={avatarUri} className="min-w-0 flex-1" /> <RunBadge runStatus={runStatus} /> </View> ); } /** The identity card's body slot — reasoning, tools, artifacts, whatever the run shows. */ function AgentContent({ className, children, ...props }: ViewProps & { children?: React.ReactNode }) { return ( <View className={cn('gap-2', className)} {...props}> {children} </View> ); } /** The control bar's verb → row entry. Static table; order is the thumb's reading order. */ const ACTION_BUTTONS: Record< AgentRunAction, { label: string; icon: LucideIcon; variant: 'default' | 'outline' | 'ghost' } > = { start: { label: 'Start', icon: PlayIcon, variant: 'default' }, pause: { label: 'Pause', icon: PauseIcon, variant: 'outline' }, stop: { label: 'Stop', icon: SquareIcon, variant: 'ghost' }, }; /** * The thumb-reachable control bar (UC-AGENT-05 AC-4). Renders only when a handler is * wired; impossible verbs are disabled in place — a control that disappears * mid-press is worse than one that refuses the press. */ function AgentActions({ className }: { className?: string }) { const { runStatus, fire } = useAgent(); if (!fire) return null; const enabled = enabledRunActions(runStatus); return ( <View className={cn('flex-row items-center gap-2', className)}> {AGENT_RUN_ACTIONS.map((action) => { const button = ACTION_BUTTONS[action]; // From paused, the verb semantically resumes — same 'start' action the run // machine speaks, the label the user hears (M8). const label = action === 'start' && runStatus === 'paused' ? 'Resume' : button.label; const isEnabled = enabled.includes(action); return ( <Button key={action} variant={button.variant} disabled={!isEnabled} onPress={() => fire(action)} accessibilityLabel={`${label} the run`} accessibilityState={{ disabled: !isEnabled }} className="flex-1" > <Icon as={button.icon} size={14} /> <Text>{label}</Text> </Button> ); })} </View> ); } export { Agent, AgentActions, AgentContent, AgentHeader, useAgent };
agent.logic.ts
/** * Agent — pure logic. Zero react-native imports, so the Vitest tier owns the run-state * map and the control-bar state machine; agent.tsx holds only the static iconName → * Lucide table, typed exhaustively against `iconName` (see tool.logic.ts for the full * rationale). * * THE WEB ORIGINAL IS A CONFIG CARD (instructions, tools, output schema); it has no run * state at all. The run vocabulary here is this port's addition, drawn from PRD * UC-AGENT-05 AC-4 — "start, pause, or stop an agent run from a thumb-reachable control * bar" — plus the two outcomes a run can end in. The PRD's porting verdict for `agent` * is an identity card; the run state is what makes the identity card an identity/run * surface rather than a static header. * * Tones resolve exclusively through lib/status.ts — the ONE status-tone map. No second * color vocabulary. */ import { statusColor, type StatusTone } from '@/registry/nativewind/lib/status'; /** The run states the control bar can drive and the transcript can show. */ export type AgentRunStatus = 'idle' | 'running' | 'paused' | 'error' | 'done'; export const AGENT_RUN_STATUS_KEYS = [ 'idle', 'running', 'paused', 'error', 'done', ] as const satisfies readonly AgentRunStatus[]; /** The three verbs of UC-AGENT-05 AC-4's control bar. */ export type AgentRunAction = 'start' | 'pause' | 'stop'; export const AGENT_RUN_ACTIONS = [ 'start', 'pause', 'stop', ] as const satisfies readonly AgentRunAction[]; /** Lucide icon NAME (kebab-case); the component resolves the component. */ export type AgentRunIconName = | 'circle' | 'loader-circle' | 'circle-pause' | 'circle-x' | 'circle-check'; export type AgentRunMeta = { /** Badge text. */ label: string; tone: StatusTone; iconName: AgentRunIconName; /** Precomposed text class from the shared statusColor map. */ className: string; }; export const AGENT_RUN_STATUS_META: Record<AgentRunStatus, AgentRunMeta> = { idle: { label: 'Idle', tone: 'pending', iconName: 'circle', className: statusColor.pending }, running: { label: 'Running', tone: 'running', iconName: 'loader-circle', className: statusColor.running }, paused: { label: 'Paused', tone: 'pending', iconName: 'circle-pause', className: statusColor.pending }, error: { label: 'Error', tone: 'error', iconName: 'circle-x', className: statusColor.error }, done: { label: 'Done', tone: 'success', iconName: 'circle-check', className: statusColor.success }, }; export function agentRunMeta(status: AgentRunStatus): AgentRunMeta { return AGENT_RUN_STATUS_META[status]; } /** * Which controls each run state enables — the whole state machine behind the control * bar, kept pure so the Vitest tier owns it. A run that has already ended (error, done) * has nothing left to stop, and an idle run has nothing to pause: the impossible * controls are disabled, never hidden — a control that vanishes under a thumb mid-press * is the failure this shape prevents. */ export function enabledRunActions(status: AgentRunStatus): AgentRunAction[] { switch (status) { case 'idle': return ['start']; case 'running': return ['pause', 'stop']; case 'paused': return ['start', 'stop']; case 'error': case 'done': return ['start']; } }
persona.tsx
import { Avatar, AvatarFallback, AvatarImage } from '@/registry/nativewind/components/ui/avatar'; import { Text } from '@/registry/nativewind/components/ui/text'; import { cn } from '@/registry/nativewind/lib/utils'; import { View, type ViewProps } from 'react-native'; /** * Persona — who is speaking. * * DIVERGES FROM THE WEB ORIGINAL, deliberately. The web `persona` is a RIVE animation on a * WebGL2 canvas — six variants driven by five state-machine inputs — not the avatar card it * resembles. `rive-react-native` is a first-party runtime, so an animated port is possible, * but it would make this the only registry item pulling a native animation runtime. * * What ships here is the identity surface: avatar, name, description, status. The animated * avatar is NOT ported. If it is ever wanted it belongs as a separate opt-in item that * declares rive-react-native, rather than as a dependency every consumer inherits for a * component most will use as a header. * * That is a real gap, and the porting-verdict table says so rather than implying parity. */ type PersonaProps = ViewProps & { name: string; description?: string; avatarUri?: string; /** Shown as a muted trailing label — "thinking", "idle", a model id. */ status?: string; }; function Persona({ name, description, avatarUri, status, className, ...props }: PersonaProps) { const initials = name .split(' ') .map((w) => w[0]) .filter(Boolean) .slice(0, 2) .join('') .toUpperCase(); return ( <View className={cn('flex-row items-center gap-3', className)} {...props}> {/* Explicit RN style sizing, redundant with `size-10` on purpose. The avatar's percent-sized fallback (`size-full`) resolves against the nearest DEFINITE ancestor when the root loses its class-driven size — observed on device as a full-screen `bg-muted` pill when the runtime stylesheet dropped the size rules (stale uniwind stylesheet state). A numeric style bypasses the stylesheet, so the fallback can never exceed 40pt in any engine. */} <Avatar alt={name} className="size-10" style={{ width: 40, height: 40 }}> {avatarUri ? <AvatarImage source={{ uri: avatarUri }} /> : null} <AvatarFallback> <Text className="text-sm font-medium">{initials}</Text> </AvatarFallback> </Avatar> <View className="min-w-0 flex-1 gap-0.5"> <Text numberOfLines={1} className="text-sm font-medium text-foreground"> {name} </Text> {description ? ( <Text numberOfLines={2} className="text-xs text-muted-foreground"> {description} </Text> ) : null} </View> {status ? ( <Text numberOfLines={1} className="shrink-0 text-xs text-muted-foreground"> {status} </Text> ) : null} </View> ); } export { Persona };

Props

NameTypeDefaultDescription
runStatusAgentRunStatus | undefined'idle'Controlled run state. The caller owns the run — this only mirrors it.
onRunAction((action: AgentRunAction) => void) | undefined—Fires on Start / Pause / Stop. Omit it and the control bar never renders.
childrenReact.ReactNode——

Theming

Themed via bg-muted, text-foreground, text-muted-foreground.

Usage

(o) => ( <Agent runStatus={(o.runStatus as 'idle' | 'running' | 'paused' | 'error' | 'done') ?? 'running'} onRunAction={() => {}} > <AgentHeader name="Registry Porter" description="Installs the 56-item set into consumer apps" avatarUri={AVATAR_URI} /> <AgentContent> <Text className="text-sm text-muted-foreground"> Run 12 · installing conversation.json through the RNR CLI </Text> </AgentContent> <AgentActions /> </Agent> )