UI에 Code Mode 표시
서버에서 Code Mode가 작동하고 있습니다. LLM이 TypeScript를 작성하고 실행하면 결과를 돌려받습니다. 하지만 샌드박스가 실행되는 동안 사용자는 아무것도 볼 수 없습니다. 이 가이드를 마치면 React 앱에 콘솔 출력, 외부 함수 호출, 최종 결과가 스트리밍되는 실시간 실행 진행 상황이 표시됩니다.
이벤트가 클라이언트에 도달하는 방식
샌드박스 내부에서 코드가 실행되면 Code Mode는 AG-UI 스트리밍 프로토콜을 통해 custom events를 내보냅니다. 이 이벤트는 일반 채팅 청크(텍스트, 도구 호출)와 함께 이동하며 onCustomEvent 콜백을 통해 클라이언트에 도착합니다.
각 execute_typescript 호출 중에 내보내는 이벤트는 다음과 같습니다:
| 이벤트 | 시점 | 주요 필드 |
|---|---|---|
code_mode:execution_started | 샌드박스 실행 시작 | timestamp, codeLength |
code_mode:console | 각 console.log/error/warn/info 실행 | level, message, timestamp |
code_mode:external_call | external_* 함수 실행 전 | function, args, timestamp |
code_mode:external_result | external_* 호출 성공 후 | function, result, duration |
code_mode:external_error | external_* 호출 실패 시 | function, error, duration |
모든 이벤트에는 특정 execute_typescript 도구 호출과 연결하는 toolCallId가 포함되므로, 올바른 메시지 옆에 이벤트를 렌더링할 수 있습니다.
useChat으로 이벤트 수신
useChat에 onCustomEvent 콜백을 전달합니다. 콜백은 이벤트 유형, 페이로드, toolCallId가 포함된 컨텍스트 객체를 받습니다:
import { useCallback, useRef, useState } from "react";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
interface VMEvent {
id: string;
eventType: string;
data: unknown;
timestamp: number;
}
export function CodeModeChat() {
const [toolCallEvents, setToolCallEvents] = useState<
Map<string, Array<VMEvent>>
>(new Map());
const eventIdCounter = useRef(0);
const handleCustomEvent = useCallback(
(
eventType: string,
data: unknown,
context: { toolCallId?: string },
) => {
const { toolCallId } = context;
if (!toolCallId) return;
const event: VMEvent = {
id: `event-${eventIdCounter.current++}`,
eventType,
data,
timestamp: Date.now(),
};
setToolCallEvents((prev) => {
const next = new Map(prev);
const events = next.get(toolCallId) || [];
next.set(toolCallId, [...events, event]);
return next;
});
},
[],
);
const { messages, sendMessage, isLoading } = useChat({
connection: fetchServerSentEvents("/api/chat"),
onCustomEvent: handleCustomEvent,
});
// Render messages with events — see next section
}
이벤트는 toolCallId를 키로 사용하므로 각 execute_typescript 호출에 고유한 이벤트 타임라인이 생성됩니다.
실행 진행 상황 렌더링
메시지를 렌더링할 때 execute_typescript 도구 호출을 확인하고 해당 이벤트를 표시합니다:
function MessageList({
messages,
toolCallEvents,
}: {
messages: Array<{ id: string; role: string; parts: Array<any> }>;
toolCallEvents: Map<string, Array<VMEvent>>;
}) {
return (
<div>
{messages.map((message) => (
<div key={message.id}>
{message.parts.map((part) => {
if (part.type === "text") {
return <p key={part.id}>{part.content}</p>;
}
if (
part.type === "tool-call" &&
part.name === "execute_typescript"
) {
const events = toolCallEvents.get(part.id) || [];
const result = part.output;
return (
<div key={part.id}>
<CodeExecutionPanel
code={part.input?.typescriptCode}
events={events}
result={result}
isRunning={!result}
/>
</div>
);
}
return null;
})}
</div>
))}
</div>
);
}
실행 패널 구축
다음은 생성된 코드, 실시간 이벤트 스트림, 최종 결과를 표시하는 완전한 CodeExecutionPanel 컴포넌트입니다:
function CodeExecutionPanel({
code,
events,
result,
isRunning,
}: {
code?: string;
events: Array<VMEvent>;
result?: { success: boolean; result?: unknown; logs?: string[]; error?: { message: string } };
isRunning: boolean;
}) {
return (
<div className="border rounded-lg overflow-hidden my-2">
{/* Generated code */}
{code && (
<details open>
<summary className="px-3 py-2 bg-gray-100 font-mono text-sm cursor-pointer">
TypeScript code
</summary>
<pre className="p-3 text-sm overflow-x-auto bg-gray-50">
<code>{code}</code>
</pre>
</details>
)}
{/* Live event stream */}
{events.length > 0 && (
<div className="border-t px-3 py-2">
<div className="text-xs font-semibold text-gray-500 mb-1">
Execution log
</div>
<div className="space-y-1 font-mono text-xs">
{events.map((event) => (
<EventLine key={event.id} event={event} />
))}
{isRunning && (
<div className="text-blue-500 animate-pulse">Running...</div>
)}
</div>
</div>
)}
{/* Final result */}
{result && (
<div
className={`border-t px-3 py-2 text-sm ${
result.success ? "bg-green-50" : "bg-red-50"
}`}
>
{result.error && (
<div className="text-red-700">Error: {result.error.message}</div>
)}
{result.logs && result.logs.length > 0 && (
<pre className="text-gray-600 text-xs mt-1">
{result.logs.join("\n")}
</pre>
)}
{result.success && result.result !== undefined && (
<pre className="text-green-800 text-xs mt-1">
{JSON.stringify(result.result, null, 2)}
</pre>
)}
</div>
)}
</div>
);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function EventLine({ event }: { event: VMEvent }) {
if (!isRecord(event.data)) return null;
const data = event.data;
switch (event.eventType) {
case "code_mode:console":
return (
<div
className={
data.level === "error"
? "text-red-600"
: data.level === "warn"
? "text-yellow-600"
: "text-gray-600"
}
>
[{String(data.level)}] {String(data.message)}
</div>
);
case "code_mode:external_call":
return (
<div className="text-amber-600">
→ {String(data.function)}(
{JSON.stringify(data.args)})
</div>
);
case "code_mode:external_result":
return (
<div className="text-green-600">
← {String(data.function)} ({String(data.duration)}ms)
</div>
);
case "code_mode:external_error":
return (
<div className="text-red-600">
✗ {String(data.function)}: {String(data.error)}
</div>
);
case "code_mode:execution_started":
return <div className="text-cyan-600">▶ Execution started</div>;
default:
return (
<div className="text-gray-400">
{event.eventType}: {JSON.stringify(data)}
</div>
);
}
}
다음 항목이 제공됩니다:
- 모델이 작성한 TypeScript를 표시하는 접을 수 있는 코드 블록
- 콘솔 출력, 인수가 포함된 외부 함수 호출, 기간이 포함된 결과 및 오류를 표시하는 실시간 이벤트 로그
- 로그와 반환 값을 포함하는 상태 색상 결과 패널
다른 프레임워크에 맞게 조정
onCustomEvent 콜백은 모든 프레임워크 통합이 내부적으로 사용하는 @tanstack/ai-client의 ChatClient를 통해 사용할 수 있습니다. Solid, Vue 또는 Svelte에서는 React의 useChat에 전달하는 것과 동일한 방식으로 onCustomEvent를 전달합니다. 콜백 시그니처는 동일합니다:
(eventType: string, data: unknown, context: { toolCallId?: string }) => void
Code Mode에서 서버 측 설정 방법을, Code Mode with Snippets에서 영속적인 스니펫 라이브러리 추가 방법을 확인합니다.