mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 07:04:48 +00:00
- Fix connectDesktopStream silently dropping RTCPeerConnection and rtcConfig options (client.ts) - Fix DesktopViewer useEffect dependency causing reconnect loop (store callbacks in refs) - Fix TOCTOU race condition in DesktopRecordingManager::start() (merge lock scope) - Fix incomplete cursor bounds check in composite_cursor_region (add right/bottom checks) - Add DesktopViewer to react-components.mdx documentation - Remove hardcoded visual styles from DesktopViewer (make unstyled by default per sdks/CLAUDE.md) - Export DesktopViewerClassNames type for consumer styling Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
290 lines
9.1 KiB
Text
290 lines
9.1 KiB
Text
---
|
|
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
|
|
- `DesktopViewer` for rendering a live desktop stream with mouse and keyboard input
|
|
- `useTranscriptVirtualizer` for wiring large transcript lists to a scroll container
|
|
|
|
## 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<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`: 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<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>}
|
|
/>
|
|
);
|
|
}
|
|
```
|
|
|
|
```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
|
|
- `scrollRef` + `virtualize`: opt into TanStack Virtual against an external scroll container
|
|
- `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
|
|
|
|
## 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 `scrollRef` that points at the actual scrollable element
|
|
- Use it when you need transcript-aware virtualization outside the stock `AgentTranscript` renderer
|
|
|
|
## 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 (
|
|
<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:
|
|
|
|
- `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.
|
|
|
|
## Desktop viewer
|
|
|
|
`DesktopViewer` connects to a live desktop stream via WebRTC and renders the video feed with interactive mouse and keyboard input forwarding.
|
|
|
|
```tsx DesktopPane.tsx
|
|
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { SandboxAgent } from "sandbox-agent";
|
|
import { DesktopViewer } from "@sandbox-agent/react";
|
|
|
|
export default function DesktopPane() {
|
|
const [client, setClient] = useState<SandboxAgent | null>(null);
|
|
|
|
useEffect(() => {
|
|
let sdk: SandboxAgent | null = null;
|
|
const start = async () => {
|
|
sdk = await SandboxAgent.connect({ baseUrl: "http://127.0.0.1:2468" });
|
|
await sdk.startDesktop();
|
|
await sdk.startDesktopStream();
|
|
setClient(sdk);
|
|
};
|
|
void start();
|
|
return () => { void sdk?.dispose(); };
|
|
}, []);
|
|
|
|
if (!client) return <div>Starting desktop...</div>;
|
|
|
|
return <DesktopViewer client={client} height={600} />;
|
|
}
|
|
```
|
|
|
|
Props:
|
|
|
|
- `client`: a `SandboxAgent` client (or any object with `connectDesktopStream`)
|
|
- `height`, `style`, `imageStyle`: optional layout overrides
|
|
- `showStatusBar`: toggle the connection status bar (default `true`)
|
|
- `onConnect`, `onDisconnect`, `onError`: optional lifecycle callbacks
|
|
- `className` and `classNames`: external styling hooks
|
|
|
|
The component is unstyled by default. Use `classNames` slots (`root`, `statusBar`, `statusText`, `statusResolution`, `viewport`, `video`) and `data-slot`/`data-state` attributes for styling from outside the package.
|
|
|
|
See [Computer Use](/computer-use) for the lower-level desktop APIs.
|