본문으로 건너뛰기

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:consoleconsole.log/error/warn/info 실행level, message, timestamp
code_mode:external_callexternal_* 함수 실행 전function, args, timestamp
code_mode:external_resultexternal_* 호출 성공 후function, result, duration
code_mode:external_errorexternal_* 호출 실패 시function, error, duration

모든 이벤트에는 특정 execute_typescript 도구 호출과 연결하는 toolCallId가 포함되므로, 올바른 메시지 옆에 이벤트를 렌더링할 수 있습니다.

useChat으로 이벤트 수신

useChatonCustomEvent 콜백을 전달합니다. 콜백은 이벤트 유형, 페이로드, 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-clientChatClient를 통해 사용할 수 있습니다. Solid, Vue 또는 Svelte에서는 React의 useChat에 전달하는 것과 동일한 방식으로 onCustomEvent를 전달합니다. 콜백 시그니처는 동일합니다:

(eventType: string, data: unknown, context: { toolCallId?: string }) => void

Code Mode에서 서버 측 설정 방법을, Code Mode with Snippets에서 영속적인 스니펫 라이브러리 추가 방법을 확인합니다.