Basic Streaming
import { EnclaveClient } from '@enclave-vm/client';
const client = new EnclaveClient({
serverUrl: 'http://localhost:3001',
});
const code = `
console.log('Starting...');
const users = await callTool('users:list', {});
console.log('Found', users.length, 'users');
for (const user of users) {
console.log('Processing:', user.name);
await callTool('process', { userId: user.id });
}
console.log('Done!');
return users.length;
`;
// Execute with streaming
const stream = client.execute(code);
for await (const event of stream) {
switch (event.type) {
case 'stdout':
console.log('[Output]', event.payload.data);
break;
case 'tool_call':
console.log('[Tool]', event.payload.tool, event.payload.args);
break;
case 'final':
console.log('[Result]', event.payload.result);
break;
case 'error':
console.error('[Error]', event.payload.message);
break;
}
}
React Streaming Hook
import { useState, useCallback } from 'react';
import { EnclaveClient, StreamEvent } from '@enclave-vm/client';
interface StreamState {
status: 'idle' | 'running' | 'completed' | 'error';
output: string[];
toolCalls: ToolCall[];
result: unknown;
error: string | null;
}
interface ToolCall {
id: string;
tool: string;
args: Record<string, unknown>;
status: 'pending' | 'completed';
}
export function useStreaming(client: EnclaveClient) {
const [state, setState] = useState<StreamState>({
status: 'idle',
output: [],
toolCalls: [],
result: null,
error: null,
});
const execute = useCallback(async (code: string) => {
setState({
status: 'running',
output: [],
toolCalls: [],
result: null,
error: null,
});
try {
const stream = client.execute(code);
for await (const event of stream) {
setState(prev => processEvent(prev, event));
}
} catch (error) {
setState(prev => ({
...prev,
status: 'error',
error: (error as Error).message,
}));
}
}, [client]);
return { ...state, execute };
}
function processEvent(state: StreamState, event: StreamEvent): StreamState {
switch (event.type) {
case 'stdout':
return {
...state,
output: [...state.output, event.payload.data],
};
case 'tool_call':
return {
...state,
toolCalls: [
...state.toolCalls,
{
id: event.payload.callId,
tool: event.payload.tool,
args: event.payload.args,
status: 'pending',
},
],
};
case 'tool_result_applied':
return {
...state,
toolCalls: state.toolCalls.map(tc =>
tc.id === event.payload.callId
? { ...tc, status: 'completed' }
: tc
),
};
case 'final':
return {
...state,
status: 'completed',
result: event.payload.result,
};
case 'error':
return {
...state,
status: 'error',
error: event.payload.message,
};
default:
return state;
}
}
Streaming Console Component
import React, { useRef, useEffect } from 'react';
interface StreamingConsoleProps {
output: string[];
autoScroll?: boolean;
}
export function StreamingConsole({ output, autoScroll = true }: StreamingConsoleProps) {
const consoleRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (autoScroll && consoleRef.current) {
consoleRef.current.scrollTop = consoleRef.current.scrollHeight;
}
}, [output, autoScroll]);
return (
<div
ref={consoleRef}
style={{
fontFamily: 'monospace',
backgroundColor: '#1e1e1e',
color: '#d4d4d4',
padding: '1rem',
borderRadius: '4px',
height: '300px',
overflowY: 'auto',
}}
>
{output.length === 0 ? (
<span style={{ color: '#666' }}>Waiting for output...</span>
) : (
output.map((line, i) => (
<div key={i} style={{ whiteSpace: 'pre-wrap' }}>
{line}
</div>
))
)}
</div>
);
}
Tool Call Timeline
import React from 'react';
interface ToolCall {
id: string;
tool: string;
args: Record<string, unknown>;
status: 'pending' | 'completed';
}
interface ToolTimelineProps {
toolCalls: ToolCall[];
}
export function ToolTimeline({ toolCalls }: ToolTimelineProps) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{toolCalls.map((call, i) => (
<div
key={call.id}
style={{
display: 'flex',
alignItems: 'center',
padding: '8px 12px',
backgroundColor: '#f5f5f5',
borderRadius: '4px',
borderLeft: `4px solid ${
call.status === 'completed' ? '#22c55e' : '#f59e0b'
}`,
}}
>
<span style={{ marginRight: '8px', fontSize: '14px' }}>
{call.status === 'completed' ? '✓' : '...'}
</span>
<span style={{ fontWeight: 600, marginRight: '8px' }}>
{call.tool}
</span>
<span style={{ color: '#666', fontSize: '12px' }}>
{JSON.stringify(call.args)}
</span>
</div>
))}
</div>
);
}
Complete Streaming UI
import React, { useState } from 'react';
import { EnclaveClient } from '@enclave-vm/client';
import { useStreaming } from './hooks/useStreaming';
import { StreamingConsole } from './components/StreamingConsole';
import { ToolTimeline } from './components/ToolTimeline';
const client = new EnclaveClient({
serverUrl: 'http://localhost:3001',
});
export function CodeExecutor() {
const [code, setCode] = useState(`
console.log('Hello, streaming!');
const users = await callTool('users:list', { limit: 5 });
console.log('Loaded', users.length, 'users');
for (const user of users) {
console.log('Processing:', user.name);
}
return { count: users.length };
`);
const { status, output, toolCalls, result, error, execute } = useStreaming(client);
return (
<div style={{ padding: '2rem', maxWidth: '1200px', margin: '0 auto' }}>
<h1>Code Executor</h1>
{/* Code Input */}
<div style={{ marginBottom: '1rem' }}>
<textarea
value={code}
onChange={(e) => setCode(e.target.value)}
style={{
width: '100%',
height: '200px',
fontFamily: 'monospace',
padding: '1rem',
}}
disabled={status === 'running'}
/>
</div>
{/* Execute Button */}
<button
onClick={() => execute(code)}
disabled={status === 'running'}
style={{
padding: '0.5rem 1rem',
backgroundColor: status === 'running' ? '#ccc' : '#0070f3',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: status === 'running' ? 'not-allowed' : 'pointer',
marginBottom: '1rem',
}}
>
{status === 'running' ? 'Running...' : 'Execute'}
</button>
{/* Status */}
<div style={{ marginBottom: '1rem' }}>
Status: <strong>{status}</strong>
</div>
{/* Output Grid */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
{/* Console Output */}
<div>
<h3>Console Output</h3>
<StreamingConsole output={output} />
</div>
{/* Tool Calls */}
<div>
<h3>Tool Calls</h3>
<ToolTimeline toolCalls={toolCalls} />
</div>
</div>
{/* Result */}
{result !== null && (
<div style={{ marginTop: '1rem' }}>
<h3>Result</h3>
<pre style={{
backgroundColor: '#f5f5f5',
padding: '1rem',
borderRadius: '4px',
overflow: 'auto',
}}>
{JSON.stringify(result, null, 2)}
</pre>
</div>
)}
{/* Error */}
{error && (
<div style={{
marginTop: '1rem',
padding: '1rem',
backgroundColor: '#fee2e2',
color: '#dc2626',
borderRadius: '4px',
}}>
<strong>Error:</strong> {error}
</div>
)}
</div>
);
}
Server-Sent Events (SSE) Alternative
For simpler setups, use SSE instead of WebSockets:// Server
import express from 'express';
import { Enclave } from '@enclave-vm/core';
const app = express();
app.use(express.json());
app.post('/execute', async (req, res) => {
// Set SSE headers
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const enclave = new Enclave({
toolHandler: async (name, args) => {
// Send tool call event
res.write(`event: tool_call\ndata: ${JSON.stringify({ tool: name, args })}\n\n`);
const result = await executeTool(name, args);
res.write(`event: tool_result\ndata: ${JSON.stringify({ tool: name })}\n\n`);
return result;
},
onConsole: (level, ...args) => {
res.write(`event: stdout\ndata: ${JSON.stringify({ data: args.join(' ') })}\n\n`);
},
});
try {
const result = await enclave.run(req.body.code);
res.write(`event: final\ndata: ${JSON.stringify(result)}\n\n`);
} catch (error) {
res.write(`event: error\ndata: ${JSON.stringify({ message: (error as Error).message })}\n\n`);
} finally {
res.end();
enclave.dispose();
}
});
// Client
async function executeWithSSE(code: string) {
const response = await fetch('/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code }),
});
if (!response.body) {
throw new Error('Response body is null');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n\n');
buffer = lines.pop() || '';
for (const chunk of lines) {
if (chunk.startsWith('event:')) {
const [eventLine, dataLine] = chunk.split('\n');
const event = eventLine.replace('event: ', '');
const data = JSON.parse(dataLine.replace('data: ', ''));
handleEvent(event, data);
}
}
}
}
Progress Indicators
interface ProgressProps {
status: 'idle' | 'running' | 'completed' | 'error';
toolCallCount: number;
duration?: number;
}
export function ExecutionProgress({ status, toolCallCount, duration }: ProgressProps) {
return (
<div style={{
display: 'flex',
alignItems: 'center',
gap: '16px',
padding: '12px',
backgroundColor: '#f5f5f5',
borderRadius: '8px',
}}>
{/* Status Indicator */}
<div style={{
width: '12px',
height: '12px',
borderRadius: '50%',
backgroundColor: {
idle: '#9ca3af',
running: '#f59e0b',
completed: '#22c55e',
error: '#ef4444',
}[status],
animation: status === 'running' ? 'pulse 1s infinite' : 'none',
}} />
{/* Status Text */}
<span style={{ fontWeight: 500 }}>
{status.charAt(0).toUpperCase() + status.slice(1)}
</span>
{/* Tool Calls */}
{toolCallCount > 0 && (
<span style={{ color: '#666' }}>
{toolCallCount} tool call{toolCallCount !== 1 ? 's' : ''}
</span>
)}
{/* Duration */}
{duration !== undefined && (
<span style={{ color: '#666' }}>
{duration}ms
</span>
)}
</div>
);
}
Next Steps
- React Code Editor Guide - Full implementation
- @enclave-vm/react - React SDK
- Multi-Tenant - Per-user isolation