7.6 KiB
React Components
Source:
docs/react-components.mdxCanonical URL: https://sandboxagent.dev/docs/react-components Description: Drop-in React components for Sandbox Agent frontends.
@sandbox-agent/react exposes small React components built on top of the sandbox-agent SDK.
Current exports:
AgentConversationfor a combined transcript + composer surfaceProcessTerminalfor attaching to a running tty processAgentTranscriptfor rendering session/message timelines without bundling any stylesChatComposerfor a reusable prompt input/send surfaceuseTranscriptVirtualizerfor wiring large transcript lists to a scroll container
Install
npm install @sandbox-agent/react@0.4.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.
"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<SandboxAgent | null>(null);
const [processId, setProcessId] = useState<string | null>(null);
const [error, setError] = useState<string | null>(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 <div>{error}</div>;
}
if (!client || !processId) {
return <div>Starting terminal...</div>;
}
return <ProcessTerminal client={client} processId={processId} height={480} />;
}
Component
ProcessTerminal attaches to a running tty process.
client: aSandboxAgentclientprocessId: the process to attach toheight,style,terminalStyle: optional layout overridesonExit,onError: optional lifecycle callbacks
See 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.
import {
AgentTranscript,
type AgentTranscriptClassNames,
type TranscriptEntry,
} from "@sandbox-agent/react";
const transcriptClasses: Partial<AgentTranscriptClassNames> = {
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 (
<AgentTranscript
entries={entries}
classNames={transcriptClasses}
renderMessageText={(entry) => <div>{entry.text}</div>}
renderInlinePendingIndicator={() => <span>...</span>}
renderToolGroupIcon={() => <span>Events</span>}
renderChevron={(expanded) => <span>{expanded ? "Hide" : "Show"}</span>}
/>
);
}
.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:
messageentries render user/assistant texttoolentries render expandable tool input/output sectionsreasoningentries render expandable reasoning blocksmetaentries render status rows or expandable metadata details
Useful props:
className: root class hookclassNames: slot-level class hooks for styling from outside the packagescrollRef+virtualize: opt into TanStack Virtual against an external scroll containerrenderMessageText: custom text or markdown rendererrenderToolItemIcon,renderToolGroupIcon,renderChevron,renderEventLinkContent: presentation overridesrenderInlinePendingIndicator,renderThinkingState: loading/thinking UI overridesisDividerEntry,canOpenEvent,getToolGroupSummary: behavior overrides for grouping and labels
Transcript virtualization hook
useTranscriptVirtualizer exposes the same TanStack Virtual behavior used by AgentTranscript when virtualize is enabled.
- Pass the grouped transcript rows you want to virtualize
- Pass a
scrollRefthat points at the actual scrollable element - Use it when you need transcript-aware virtualization outside the stock
AgentTranscriptrenderer
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.
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 (
<AgentConversation
entries={entries}
emptyState={<div>Start the conversation.</div>}
transcriptProps={{
renderMessageText: (entry) => <div>{entry.text}</div>,
}}
composerProps={{
message,
onMessageChange,
onSubmit,
placeholder: "Send a message...",
}}
/>
);
}
Useful ChatComposer props:
classNameandclassNamesfor external stylinginputRefto manage focus or autoresize from the consumertextareaPropsfor lower-level textarea behaviorallowEmptySubmitwhen 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.