--- title: "React Components" description: "Drop-in React components for Sandbox Agent frontends." icon: "react" --- `@sandbox-agent/react` exposes small React components built on top of the `sandbox-agent` SDK. Current exports: - `AgentConversation` for a combined transcript + composer surface - `ProcessTerminal` for attaching to a running tty process - `AgentTranscript` for rendering session/message timelines without bundling any styles - `ChatComposer` for a reusable prompt input/send surface ## Install ```bash npm install @sandbox-agent/react@0.3.x ``` ## Full example This example connects to a running Sandbox Agent server, starts a tty shell, renders `ProcessTerminal`, and cleans up the process when the component unmounts. ```tsx TerminalPane.tsx expandable highlight={5,32-36,71} "use client"; import { useEffect, useState } from "react"; import { SandboxAgent } from "sandbox-agent"; import { ProcessTerminal } from "@sandbox-agent/react"; export default function TerminalPane() { const [client, setClient] = useState(null); const [processId, setProcessId] = useState(null); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; let sdk: SandboxAgent | null = null; let createdProcessId: string | null = null; const cleanup = async () => { if (!sdk || !createdProcessId) { return; } await sdk.killProcess(createdProcessId, { waitMs: 1_000 }).catch(() => {}); await sdk.deleteProcess(createdProcessId).catch(() => {}); }; const start = async () => { try { sdk = await SandboxAgent.connect({ baseUrl: "http://127.0.0.1:2468", }); const process = await sdk.createProcess({ command: "sh", interactive: true, tty: true, }); if (cancelled) { createdProcessId = process.id; await cleanup(); await sdk.dispose(); return; } createdProcessId = process.id; setClient(sdk); setProcessId(process.id); } catch (err) { const message = err instanceof Error ? err.message : "Failed to start terminal."; setError(message); } }; void start(); return () => { cancelled = true; void cleanup(); void sdk?.dispose(); }; }, []); if (error) { return
{error}
; } if (!client || !processId) { return
Starting terminal...
; } return ; } ``` ## Component `ProcessTerminal` attaches to a running tty process. - `client`: a `SandboxAgent` client - `processId`: the process to attach to - `height`, `style`, `terminalStyle`: optional layout overrides - `onExit`, `onError`: optional lifecycle callbacks See [Processes](/processes) for the lower-level terminal APIs. ## Headless transcript `AgentTranscript` is intentionally unstyled. It follows the common headless React pattern used by libraries like Radix, Headless UI, and React Aria: behavior lives in the component, while styling stays in your app through `className`, slot-level `classNames`, and `data-*` state attributes on the rendered DOM. ```tsx TranscriptPane.tsx import { AgentTranscript, type AgentTranscriptClassNames, type TranscriptEntry, } from "@sandbox-agent/react"; const transcriptClasses: Partial = { root: "transcript", message: "transcript-message", messageContent: "transcript-message-content", toolGroupContainer: "transcript-tools", toolGroupHeader: "transcript-tools-header", toolItem: "transcript-tool-item", toolItemHeader: "transcript-tool-item-header", toolItemBody: "transcript-tool-item-body", divider: "transcript-divider", dividerText: "transcript-divider-text", error: "transcript-error", }; export function TranscriptPane({ entries }: { entries: TranscriptEntry[] }) { return (
{entry.text}
} renderInlinePendingIndicator={() => ...} renderToolGroupIcon={() => Events} renderChevron={(expanded) => {expanded ? "Hide" : "Show"}} /> ); } ``` ```css .transcript { display: grid; gap: 12px; } .transcript [data-slot="message"][data-variant="user"] .transcript-message-content { background: #161616; color: white; } .transcript [data-slot="message"][data-variant="assistant"] .transcript-message-content { background: #f4f4f0; color: #161616; } .transcript [data-slot="tool-item"][data-failed="true"] { border-color: #d33; } .transcript [data-slot="tool-item-header"][data-expanded="true"] { background: rgba(0, 0, 0, 0.06); } ``` `AgentTranscript` accepts `TranscriptEntry[]`, which matches the Inspector timeline shape: - `message` entries render user/assistant text - `tool` entries render expandable tool input/output sections - `reasoning` entries render expandable reasoning blocks - `meta` entries render status rows or expandable metadata details Useful props: - `className`: root class hook - `classNames`: slot-level class hooks for styling from outside the package - `renderMessageText`: custom text or markdown renderer - `renderToolItemIcon`, `renderToolGroupIcon`, `renderChevron`, `renderEventLinkContent`: presentation overrides - `renderInlinePendingIndicator`, `renderThinkingState`: loading/thinking UI overrides - `isDividerEntry`, `canOpenEvent`, `getToolGroupSummary`: behavior overrides for grouping and labels ## Composer and conversation `ChatComposer` is the headless message input. `AgentConversation` composes `AgentTranscript` and `ChatComposer` so apps can reuse the transcript/composer pairing without pulling in Inspector session chrome. ```tsx ConversationPane.tsx import { AgentConversation, type TranscriptEntry } from "@sandbox-agent/react"; export function ConversationPane({ entries, message, onMessageChange, onSubmit, }: { entries: TranscriptEntry[]; message: string; onMessageChange: (value: string) => void; onSubmit: () => void; }) { return ( Start the conversation.} transcriptProps={{ renderMessageText: (entry) =>
{entry.text}
, }} composerProps={{ message, onMessageChange, onSubmit, placeholder: "Send a message...", }} /> ); } ``` Useful `ChatComposer` props: - `className` and `classNames` for external styling - `inputRef` to manage focus or autoresize from the consumer - `textareaProps` for lower-level textarea behavior - `allowEmptySubmit` when the submit action is valid without draft text, such as a stop button Use `transcriptProps` and `composerProps` when you want the shared composition but still need custom rendering or behavior. Use `transcriptClassNames` and `composerClassNames` when you want styling hooks for each subcomponent.