沙箱文件工作区 (Sandbox UI)
构建带有文件树、代码编辑器和 Diff 差异对比的类 IDE 沉浸式前端界面
当 deep agent 与沙箱后端配合执行代码生成、项目重构或多文件编辑任务时,将前端扩展为一个类 IDE 的代码工作空间能够极大提升用户体验。 用户可以在左侧浏览代码文件树,在中间查看代码修改的 Git 风格 Diff 差异对比,并在右侧通过对话驱动智能体继续编码。
架构设计 (Architecture)
前端沙箱 UI 通常由以下三个核心模块协同驱动:
- 沙箱后端 (Sandbox Backend):运行在云端隔离容器中,提供完整的文件系统与 Shell 执行环境。
- 文件浏览 API 服务 (File Browsing API):轻量级 REST/WebSocket 服务,向前端提供目录列表读取、文件拉取与文件监控通知。
- 前端 IDE 界面 (Frontend Web IDE):React/Vue 客户端,负责渲染文件树、Monaco 代码编辑器以及版本 Diff 差异对比面板。
%%{
init: {
"fontFamily": "monospace",
"flowchart": {
"curve": "curve"
}
}
}%%
graph LR
UI["IDE Frontend"]
API["API Server"]
AGENT["createDeepAgent()"]
SANDBOX["Sandbox"]
UI --"useStream()"--> AGENT
UI --"/sandbox/:threadId/*"--> API
AGENT --"read/write/execute"--> SANDBOX
API --"ls / read"--> SANDBOX
classDef blueHighlight fill:#E5F4FF,stroke:#006DDD,color:#030710;
classDef greenHighlight fill:#F6FFDB,stroke:#6E8900,color:#2E3900;
classDef purpleHighlight fill:#EBD0F0,stroke:#885270,color:#441E33;
classDef orangeHighlight fill:#FDF3FF,stroke:#7E65AE,color:#504B5F;
class UI blueHighlight;
class AGENT greenHighlight;
class SANDBOX purpleHighlight;
class API orangeHighlight;沙箱生命周期管理 (Sandbox lifecycle)
在多会话架构中,通常将沙箱的生命周期绑定至特定对话线程(Thread)。 当用户开启新对话时,自动为该会话初始化专属沙箱;当用户切换历史会话时,根据线程元数据重新挂载对应的沙箱实例。
根据线程元数据动态解析沙箱
在 API 服务端通过中间件从会话上下文中提取沙箱 ID:
sequenceDiagram
participant FE as Frontend
participant LG as LangGraph API
participant HTTP as API Server
participant SB as Sandbox
Note over FE: Page loads
FE->>LG: POST /threads
LG-->>FE: threadId
FE->>HTTP: GET /sandbox/:threadId/tree
HTTP->>LG: threads.get(threadId) → metadata.sandbox_id
alt No sandbox yet
HTTP->>SB: LangSmithSandbox.create()
HTTP->>LG: threads.update(threadId, metadata.sandbox_id)
else Existing sandbox
HTTP->>SB: connect(sandbox_id)
end
HTTP-->>FE: file tree
Note over FE: User sends message
FE->>LG: POST /threads/:threadId/runs/stream
LG->>LG: backend reads thread_id from config
LG->>SB: connect to same sandbox初始工程文件灌入 (Seed project files)
在会话启动时,通过沙箱文件 API 将目标项目的初始源码脚手架解压注入到沙箱内部目录中。
添加文件浏览 API 服务 (Adding the file browsing API)
构建轻量级 Express/Fastify 服务端端点,用于向前端返回文件列表和文件内容:
// src/api/utils.ts
import { Client } from "@langchain/langgraph-sdk";
import { LangSmithSandbox } from "deepagents";
import { SandboxClient } from "langsmith/sandbox";
export async function getOrCreateSandboxForThread(threadId: string) {
const client = new Client({ apiUrl: "http://localhost:2024" });
const thread = await client.threads.get(threadId);
const sandboxId = thread.metadata?.sandbox_id;
if (sandboxId) {
const existing = await new SandboxClient().getSandbox(sandboxId);
if (existing.status === "ready") {
return new LangSmithSandbox({ sandbox: existing });
}
}
const sandbox = await LangSmithSandbox.create({ templateName: "my-template" });
await seedSandbox(sandbox);
await client.threads.update(threadId, { metadata: { sandbox_id: sandbox.id } });
return sandbox;
}配置 langgraph.json
将自定义的 HTTP API 服务与 LangGraph 智能体运行时注册在同一个部署配置中:
import { createDeepAgent } from "deepagents";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";
import { getOrCreateSandboxForThread } from "./api/utils.js";
export async function agent(config: LangGraphRunnableConfig) {
const threadId = config.configurable?.thread_id;
if (!threadId) throw new Error("No thread_id — agent must run on a thread");
const backend = await getOrCreateSandboxForThread(threadId);
return createDeepAgent({
model: "google:gemini-3.6-flash",
backend,
systemPrompt: "You are an expert developer working on a project in /app.",
});
}前端界面构建全流程 (Building the frontend)
1. 线程创建与沙箱绑定
在前端发起会话时,向服务端请求预配沙箱:
import { createDeepAgent } from "deepagents";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";
import { getOrCreateSandboxForThread } from "./api/utils.js";
export async function agent(config: LangGraphRunnableConfig) {
const threadId = config.configurable?.thread_id;
if (!threadId) throw new Error("No thread_id — agent must run on a thread");
const backend = await getOrCreateSandboxForThread(threadId);
return createDeepAgent({
model: "openai:gpt-5.5",
backend,
systemPrompt: "You are an expert developer working on a project in /app.",
});
}2. 文件状态响应式管理
使用 Zustand、Pinia 或 React Context 统一管理当前工作区的文件目录树、当前选中的文件路径与文件内容缓存。
3. 实时文件同步 (Real-time file sync)
当智能体在后台调用 write_file 或 edit_file 时,前端通过 WebSocket 或轮询机制自动感知文件变更,并无刷新刷新代码编辑区内容:
import { createDeepAgent } from "deepagents";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";
import { getOrCreateSandboxForThread } from "./api/utils.js";
export async function agent(config: LangGraphRunnableConfig) {
const threadId = config.configurable?.thread_id;
if (!threadId) throw new Error("No thread_id — agent must run on a thread");
const backend = await getOrCreateSandboxForThread(threadId);
return createDeepAgent({
model: "anthropic:claude-sonnet-5",
backend,
systemPrompt: "You are an expert developer working on a project in /app.",
});
}4. 智能体变更文件检测 (Detecting changed files)
追踪智能体在当前轮次中具体改动了哪些文件路径。
5. Git 风格 Diff 差异比对展示 (Displaying diffs)
在前端集成 Monaco Diff Editor,高亮展示智能体修改前后的行级增删差异,方便用户直观审查:
import { createDeepAgent } from "deepagents";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";
import { getOrCreateSandboxForThread } from "./api/utils.js";
export async function agent(config: LangGraphRunnableConfig) {
const threadId = config.configurable?.thread_id;
if (!threadId) throw new Error("No thread_id — agent must run on a thread");
const backend = await getOrCreateSandboxForThread(threadId);
return createDeepAgent({
model: "openrouter:z-ai/glm-5.2",
backend,
systemPrompt: "You are an expert developer working on a project in /app.",
});
}6. 改动文件摘要看板 (Changed files summary)
在消息末尾输出带有改动行数(+35, -12)的文件变更卡片汇总。
典型生产适用场景 (Use cases)
- 自主全栈代码生成:从零生成完整的 Vue/React 项目并实时提供在线文件浏览与预览。
- 自动化缺陷修复 (Bug Fix):智能体定位 Bug、修改多个相关联的文件并展示清晰的审查 Diff。
- 数据科学分析报告:生成数据清洗脚本、处理后的 CSV 数据集并直观导出。
前端工程最佳实践
- 避免频繁拉取超大二进制文件:针对大图片或构建产物采用按需懒加载。
- 做好文件编辑防冲突:如果允许人类用户与智能体同时编辑代码,采用明确的状态锁定或乐观合并机制。
- 保存变更版本快照:支持用户在任何时刻一键回退到智能体某一步修改前的版本。
import { createDeepAgent } from "deepagents";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";
import { getOrCreateSandboxForThread } from "./api/utils.js";
export async function agent(config: LangGraphRunnableConfig) {
const threadId = config.configurable?.thread_id;
if (!threadId) throw new Error("No thread_id — agent must run on a thread");
const backend = await getOrCreateSandboxForThread(threadId);
return createDeepAgent({
model: "fireworks:accounts/fireworks/models/glm-5p2",
backend,
systemPrompt: "You are an expert developer working on a project in /app.",
});
}import { createDeepAgent } from "deepagents";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";
import { getOrCreateSandboxForThread } from "./api/utils.js";
export async function agent(config: LangGraphRunnableConfig) {
const threadId = config.configurable?.thread_id;
if (!threadId) throw new Error("No thread_id — agent must run on a thread");
const backend = await getOrCreateSandboxForThread(threadId);
return createDeepAgent({
model: "baseten:zai-org/GLM-5.2",
backend,
systemPrompt: "You are an expert developer working on a project in /app.",
});
}import { createDeepAgent } from "deepagents";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";
import { getOrCreateSandboxForThread } from "./api/utils.js";
export async function agent(config: LangGraphRunnableConfig) {
const threadId = config.configurable?.thread_id;
if (!threadId) throw new Error("No thread_id — agent must run on a thread");
const backend = await getOrCreateSandboxForThread(threadId);
return createDeepAgent({
model: "ollama:north-mini-code-1.0",
backend,
systemPrompt: "You are an expert developer working on a project in /app.",
});
}NOTE
Similar to the example in Going to production, the agent is an async graph factory invoked on each run. Store the sandbox ID on thread metadata so custom http.app routes can call the same getOrCreateSandboxForThread helper. Going to production uses provider label lookup instead when the LangGraph SDK is the only entry point.
Seed project files
Before the agent runs, upload starter files with uploadFiles / upload_files. See File transfers for seeding patterns, provider examples, and syncing memories or skills into the sandbox. For LangSmith sandboxes, pass templateName from a sandbox snapshot when creating the container.
TIP
Run sandbox.execute("cd /app && npm install") after uploading package.json so dependencies are ready before the first agent turn.
Adding the file browsing API
The agent can read and write files, but the frontend also needs direct access to browse the sandbox filesystem. Add a custom Hono API server and expose it through the http.app field in langgraph.json.
Create the API server
The sandbox API endpoints use the thread ID as a URL path parameter. This ensures the frontend always accesses the correct sandbox for the current conversation, using the same getOrCreateSandboxForThread function as the agent's backend:
// src/api/app.ts
import { Hono } from "hono";
import { getOrCreateSandboxForThread } from "./utils.js";
export const app = new Hono();
app.get("/sandbox/:threadId/tree", async (c) => {
const threadId = c.req.param("threadId");
const rootPath = c.req.query("filePath") || "/app";
const sandbox = await getOrCreateSandboxForThread(threadId);
const result = await sandbox.execute(
`find '${rootPath}' -printf '%y\\t%s\\t%p\\n' 2>/dev/null | sort -t$'\\t' -k3`,
);
const entries = result.output
.trim()
.split("\n")
.filter(Boolean)
.map((line) => {
const [typeChar, sizeStr, fullPath] = line.split("\t");
return {
name: fullPath.split("/").pop(),
type: typeChar === "d" ? "directory" : "file",
path: fullPath,
size: parseInt(sizeStr, 10) || 0,
};
});
return c.json({ path: rootPath, entries, sandboxId: sandbox.id });
});
app.get("/sandbox/:threadId/file", async (c) => {
const threadId = c.req.param("threadId");
const filePath = c.req.query("filePath");
if (!filePath) return c.json({ error: "filePath is required" }, 400);
const sandbox = await getOrCreateSandboxForThread(threadId);
const results = await sandbox.downloadFiles([filePath]);
const file = results[0];
if (file.error) return c.json({ error: file.error }, 404);
const content = new TextDecoder().decode(file.content!);
return c.json({ path: filePath, content });
});NOTE
Both the agent's backend and the API server call the same
getOrCreateSandboxForThread function. This ensures they always resolve
to the same sandbox for a given thread. The sandbox ID in thread metadata is the single source of truth—no in-memory caches needed.
Configure langgraph.json
Register both the agent graph and the API server. The http.app field tells the LangGraph platform to serve your custom routes alongside the default ones. See application structure and LangSmith Deployments for the full set of langgraph.json options.
{
"node_version": "22",
"graphs": {
"deep_agent_ide": "./src/agents/deep-agent-ide.ts:agent"
},
"env": ".env",
"http": {
"app": "./src/api/app.ts:app"
}
}Your custom routes are available at the same host as the LangGraph API. For local development with langgraph dev, that's http://localhost:2024.
NOTE
Custom routes defined in http.app take priority over default LangGraph routes. This means you can shadow built-in endpoints if needed, but be careful not to accidentally override routes like /threads or /runs.
Building the frontend
The frontend has three panels: a file tree sidebar, a code/diff viewer, and a chat panel. It uses useStream for the agent conversation and the custom API endpoints for file browsing.
For production deployment, point apiUrl at your LangSmith Deployment, and pass a stable thread_id on each run. See Frontend in Going to production for those settings and for invoking the agent with thread_id and runtime context.
Thread creation
Create a LangGraph thread when the page loads and persist its ID in sessionStorage so page reloads reconnect to the same sandbox:
const THREAD_KEY = "sandbox-thread-id";
function IDEPreview() {
const [threadId, setThreadId] = useState<string | null>(
() => sessionStorage.getItem(THREAD_KEY),
);
const updateThreadId = useCallback((id: string | null) => {
setThreadId(id);
if (id) sessionStorage.setItem(THREAD_KEY, id);
else sessionStorage.removeItem(THREAD_KEY);
}, []);
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "deep_agent_ide",
threadId,
onThreadId: updateThreadId,
});
// Create thread on first mount
useEffect(() => {
if (threadId) return;
stream.client.threads.create().then((t) => updateThreadId(t.thread_id));
}, [stream.client, threadId, updateThreadId]);
// Pass threadId to sandbox file hooks
const { tree, files } = useSandboxFiles(threadId);
// ...
}The "new thread" button clears the stored ID so the next mount creates a fresh thread (and sandbox):
function handleNewThread() {
updateThreadId(null);
}File state management
Track two snapshots of the sandbox filesystem: the original state (before the agent runs) and the current state (updated in real time). The thread ID is included in the API URL so requests always hit the correct sandbox:
const AGENT_URL = "http://localhost:2024";
async function fetchTree(threadId: string): Promise<FileEntry[]> {
const res = await fetch(
`${AGENT_URL}/sandbox/${encodeURIComponent(threadId)}/tree?filePath=/app`,
);
const data = await res.json();
return data.entries.filter((e: FileEntry) => !e.path.includes("node_modules"));
}
async function fetchFile(threadId: string, path: string): Promise<string | null> {
const res = await fetch(
`${AGENT_URL}/sandbox/${encodeURIComponent(threadId)}/file?filePath=${encodeURIComponent(path)}`,
);
const data = await res.json();
return data.content ?? null;
}Real-time file sync
The key to the IDE experience is updating files as the agent works, not after it finishes. Watch the stream's messages for ToolMessage instances from file-mutating tools. When a write_file or edit_file tool call completes, refresh that specific file. When execute completes, refresh everything (since a shell command could modify any file):
import { useStream } from "@langchain/react";
import { ToolMessage, AIMessage } from "langchain";
const FILE_MUTATING_TOOLS = new Set(["write_file", "edit_file", "execute"]);
export function IDEPreview() {
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "deep_agent_ide",
});
const processedIds = useRef(new Set<string>());
useEffect(() => {
// Build a map of file-mutating tool calls from AI messages
const toolCallMap = new Map();
for (const msg of stream.messages) {
if (!AIMessage.isInstance(msg)) continue;
for (const tc of msg.tool_calls ?? []) {
if (tc.id && FILE_MUTATING_TOOLS.has(tc.name)) {
toolCallMap.set(tc.id, { name: tc.name, args: tc.args });
}
}
}
// When a ToolMessage appears for a file-mutating tool, refresh
for (const msg of stream.messages) {
if (!ToolMessage.isInstance(msg)) continue;
const id = msg.id ?? msg.tool_call_id;
if (!id || processedIds.current.has(id)) continue;
const call = toolCallMap.get(msg.tool_call_id);
if (!call) continue;
processedIds.current.add(id);
if (call.name === "write_file" || call.name === "edit_file") {
refreshSingleFile(call.args.path ?? call.args.file_path);
} else if (call.name === "execute") {
refreshTreeAndFiles();
}
}
}, [stream.messages]);
}<script setup lang="ts">
import { useStream } from "@langchain/vue";
import { ToolMessage, AIMessage } from "langchain";
import { watch } from "vue";
const FILE_MUTATING_TOOLS = new Set(["write_file", "edit_file", "execute"]);
const processedIds = new Set<string>();
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "deep_agent_ide",
});
watch(
() => stream.messages.value,
(messages) => {
const toolCallMap = new Map();
for (const msg of messages) {
if (AIMessage.isInstance(msg)) {
for (const tc of msg.tool_calls ?? []) {
if (tc.id && FILE_MUTATING_TOOLS.has(tc.name)) {
toolCallMap.set(tc.id, { name: tc.name, args: tc.args });
}
}
}
}
for (const msg of messages) {
if (!ToolMessage.isInstance(msg)) continue;
const id = msg.id ?? msg.tool_call_id;
if (!id || processedIds.has(id)) continue;
const call = toolCallMap.get(msg.tool_call_id);
if (!call) continue;
processedIds.add(id);
if (call.name === "write_file" || call.name === "edit_file") {
refreshSingleFile(call.args.path ?? call.args.file_path);
} else if (call.name === "execute") {
refreshTreeAndFiles();
}
}
},
{ deep: true },
);
</script><script lang="ts">
import { useStream } from "@langchain/svelte";
import { ToolMessage, AIMessage } from "langchain";
const FILE_MUTATING_TOOLS = new Set(["write_file", "edit_file", "execute"]);
const processedIds = new Set<string>();
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "deep_agent_ide",
});
$effect(() => {
const msgs = stream.messages;
const toolCallMap = new Map();
for (const msg of msgs) {
if (AIMessage.isInstance(msg)) {
for (const tc of msg.tool_calls ?? []) {
if (tc.id && FILE_MUTATING_TOOLS.has(tc.name)) {
toolCallMap.set(tc.id, { name: tc.name, args: tc.args });
}
}
}
}
for (const msg of msgs) {
if (!ToolMessage.isInstance(msg)) continue;
const id = msg.id ?? msg.tool_call_id;
if (!id || processedIds.has(id)) continue;
const call = toolCallMap.get(msg.tool_call_id);
if (!call) continue;
processedIds.add(id);
if (call.name === "write_file" || call.name === "edit_file") {
refreshSingleFile(call.args.path ?? call.args.file_path);
} else if (call.name === "execute") {
refreshTreeAndFiles();
}
}
});
</script>import { Component, effect } from "@angular/core";
import { injectStream } from "@langchain/angular";
import { ToolMessage, AIMessage } from "langchain";
const FILE_MUTATING_TOOLS = new Set(["write_file", "edit_file", "execute"]);
@Component({
selector: "app-ide-preview",
template: `<!-- ... -->`,
})
export class IdePreviewComponent {
stream = injectStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "deep_agent_ide",
});
private processedIds = new Set<string>();
constructor() {
effect(() => {
const messages = this.stream.messages();
const toolCallMap = new Map();
for (const msg of messages) {
if (AIMessage.isInstance(msg)) {
for (const tc of (msg as AIMessage).tool_calls ?? []) {
if (tc.id && FILE_MUTATING_TOOLS.has(tc.name)) {
toolCallMap.set(tc.id, { name: tc.name, args: tc.args });
}
}
}
}
for (const msg of messages) {
if (!ToolMessage.isInstance(msg)) continue;
const id = (msg as ToolMessage).id ?? (msg as ToolMessage).tool_call_id;
if (!id || this.processedIds.has(id)) continue;
const call = toolCallMap.get((msg as ToolMessage).tool_call_id);
if (!call) continue;
this.processedIds.add(id);
if (call.name === "write_file" || call.name === "edit_file") {
this.refreshSingleFile(call.args.path ?? call.args.file_path);
} else if (call.name === "execute") {
this.refreshTreeAndFiles();
}
}
});
}
}Detecting changed files
Before each agent run, snapshot the current file contents. After files refresh, compare against the snapshot to identify which files changed:
function detectChanges(
current: FileSnapshot,
original: FileSnapshot,
): Set<string> {
const changed = new Set<string>();
for (const [path, content] of Object.entries(current)) {
if (original[path] !== content) changed.add(path);
}
for (const path of Object.keys(original)) {
if (!(path in current)) changed.add(path);
}
return changed;
}When a user selects a changed file, default to the diff view so they immediately see what the agent modified.
Displaying diffs
Use a framework-appropriate diff library to render unified diffs:
| Framework | Library | Component |
|---|---|---|
| React | @pierre/diffs | <FileDiff> with parseDiffFromFile |
| Vue | @git-diff-view/vue | <DiffView> with generateDiffFile from @git-diff-view/file |
| Svelte | @git-diff-view/svelte | <DiffView> with generateDiffFile from @git-diff-view/file |
| Angular | ngx-diff | <ngx-unified-diff> with [before] and [after] |
Example with @pierre/diffs (React):
import { FileDiff } from "@pierre/diffs/react";
import { parseDiffFromFile } from "@pierre/diffs";
function DiffPanel({ original, current, fileName }) {
const diff = parseDiffFromFile(
{ name: fileName, contents: original },
{ name: fileName, contents: current },
);
return (
<FileDiff
fileDiff={diff}
options={{ theme: "github-dark", diffStyle: "unified", diffIndicators: "bars" }}
/>
);
}Changed files summary
Show a summary of all modified files with line-level addition/deletion counts. This gives users a quick overview of the agent's impact—similar to a git status:
function ChangedFilesSummary({ changedFiles, files, originalFiles, onSelect }) {
const stats = [...changedFiles].map((path) => {
const oldLines = (originalFiles[path] ?? "").split("\n");
const newLines = (files[path] ?? "").split("\n");
// Compute additions/deletions by comparing lines
return { path, additions, deletions };
});
return (
<div>
<h3>{stats.length} Files Changed</h3>
{stats.map((file) => (
<button key={file.path} onClick={() => onSelect(file.path)}>
{file.path}
<span className="text-green-400">+{file.additions}</span>
<span className="text-red-400">-{file.deletions}</span>
</button>
))}
</div>
);
}Use cases
A sandbox is the right choice when:
- Coding agents that create, modify, and run code need a visual interface beyond chat
- Code review workflows where the agent suggests changes and the user reviews diffs before accepting
- Tutorial or learning apps where an AI assistant helps users build a project step by step, showing changes in context
- Prototyping tools where users describe features in natural language and watch the agent implement them in real time
Best practices
Frontend-specific:
Persist
threadIdinsessionStorageso page reloads reconnect to the same thread and sandbox instead of creating new ones.Sync files on every relevant tool call, not just when the run finishes. Watch for
write_file,edit_file, andexecutetool messages and refresh immediately.Default to diff view for changed files. When a user clicks a file that was modified by the agent, show the diff first—that's what they care about.
Show compact tool results for read-only operations. Instead of dumping the full output of
read_filein the chat, show a one-liner likeRead router.js L1-42. Reserve the full output display for mutating tools.Filter
node_modulesfrom the file tree. Nobody wants to browse thousands of dependency files. Filter them out when fetching the tree.
For backends and sandboxes:
- Use thread-scoped sandboxes for production apps. See Sandbox lifecycle.
- Share sandbox resolution between the agent backend and the API server via thread metadata so both resolve the same environment with no in-memory caches.
- Seed the sandbox with a real project. See File transfers.
- Keep secrets out of the sandbox. Use the sandbox auth proxy instead of environment variables or file uploads for API keys.
- Add guardrails before launch. Configure rate limits, error handling, and data privacy middleware for autonomous coding agents.
Related
Deploy the agent with persistent sandboxes, auth, guardrails, and production
useStreamsettings.
Sandbox providers, security model, and file transfer APIs.
Other deep agent UI patterns: subagent streaming, todo lists, and custom state.
Full
langgraph.jsonreference, including customhttp.approutes.
Related LangChain guides
The sandbox IDE builds on core LangChain frontend patterns. These guides cover the same useStream primitives used in this page:
Render file and execution tool calls as purpose-built UI cards with type-safe results.
Run browser and device APIs on the client side—useful for embedding live preview or terminal output in the IDE panel.
Render sandboxed app previews and MCP-generated interfaces at the open end of the generative UI spectrum.
Reconnect to a running coding session after a page reload without losing the sandbox state.
Connect these docs to your agent of choice via MCP for real-time answers.
