Coding agent 需要的不只是聊天窗口。它们需要 file browser、code viewer 和 diff panel,也就是 IDE 体验。这个模式会把 deep agent 连接到 sandbox,使它可以在隔离环境中读取、 写入和执行代码,然后通过 custom API server 暴露 sandbox filesystem,使 frontend 可以在 agent 工作时实时显示文件。 本页介绍 three-panel UI(file tree、code viewer 和 chat),以及向它暴露 sandbox filesystem 的 custom API routes。如需了解 sandbox providers、lifecycle scoping、seeding files、secrets、deployment 和 production useStream 配置,请参阅 Going to production

Architecture

此设置包含三部分:
  1. 带有 sandbox backend 的 deep agent: Agent 会自动从 sandbox 获得 filesystem tools(read_filewrite_fileedit_fileexecute
  2. Custom API server: 通过 langgraph.jsonhttp.app 字段暴露的 Hono app,提供 frontend 可以调用的 file browsing endpoints
  3. Three-panel frontend: File tree、code/diff viewer 和 chat panel, 会在 agent 做出更改时实时同步文件

Sandbox lifecycle

在连接 frontend 之前,先选择 sandbox 的存活时间以及谁可以共享它。 如需了解 thread-scoped 与 assistant-scoped sandboxes、async graph factory 设置、TTL 行为和 SDK 调用示例, 请参阅 Sandbox lifecycle 本指南默认使用 thread-scoped sandboxes。Frontend 和 custom API server 都会从 LangGraph thread ID 解析 sandbox。 这会隔离各个 conversation,并且在你持久化 thread ID 后, 页面重新加载时可以重新连接到同一个环境。 对于 multi-tenant apps, 请改为在 backend factory 中按 user 或 assistant 限定 sandboxes。对于没有 LangGraph threads 的 demo,请在 API URL 中传入客户端生成的 session ID。 该 session ID 不会跨浏览器会话持久化。

Connect the agent and API server

按照 Execution environment 中的说明,使用 sandbox backend 配置 deep agent。 Agent 会自动获得 filesystem tools 和 execute tool,不需要额外的 tool 配置。 构建这个 UI 会在 production 设置之外增加一个要求:需要一个在 agent graph 之外运行的 custom API server。因此,对于每个 thread,agent backend 和 file-browsing routes 必须解析到同一个 sandbox。将 sandbox ID 存储在线程 metadata 上,并在两者之间共享同一个 lookup function。

Resolve the sandbox from thread metadata

在 shared module 中定义 getOrCreateSandboxForThread。Agent graph factory 和 custom API routes 都会导入它:
// 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);  // See File transfers below
  await client.threads.update(threadId, { metadata: { sandbox_id: sandbox.id } });
  return sandbox;
}
将 agent 连接为 async graph factory,它会从 run config 读取 thread_id,并将解析出的 backend 传给 createDeepAgent
// src/agents/deep-agent-ide.ts
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_genai:gemini-3.5-flash",
    backend,
    systemPrompt: "You are an expert developer working on a project in /app.",
  });
}
Going to production 中的示例类似, agent 是一个在每次 run 时调用的 async graph factory。将 sandbox ID 存储在线程 metadata 上,使 custom http.app routes 可以调用同一个 getOrCreateSandboxForThread helper。当 LangGraph SDK 是唯一入口时, Going to production 会改用 provider label lookup。

Seed project files

在 agent 运行前,使用 uploadFiles / upload_files 上传 starter files。 如需了解 seeding patterns、provider examples,以及如何将 memoriesskills 同步到 sandbox,请参阅 File transfers。 对于 LangSmith sandboxes,请在创建 container 时传入来自 sandbox snapshottemplateName
上传 package.json 后运行 sandbox.execute("cd /app && npm install"), 使 dependencies 在第一次 agent turn 之前准备就绪。

Adding the file browsing API

Agent 可以读写文件,但 frontend 也需要直接访问 sandbox filesystem 来浏览文件。添加一个 custom Hono API server, 并通过 langgraph.json 中的 http.app 字段将其暴露出来。

Create the API server

Sandbox API endpoints 使用 thread ID 作为 URL path parameter。这可以确保 frontend 始终访问当前 conversation 对应的正确 sandbox,并使用与 agent backend 相同的 helper: getOrCreateSandboxForThread
// 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 });
});
Agent backend 和 API server 都会调用同一个getOrCreateSandboxForThread function。这可以确保它们始终解析到给定 thread 的同一个 sandbox。Thread metadata 中的 sandbox ID 是唯一事实来源,不需要 in-memory caches。

Configure langgraph.json

同时注册 agent graph 和 API server。http.app 字段会告诉 LangGraph platform 将你的 custom routes 与默认 routes 一起提供服务。有关完整的 langgraph.json 选项,请参阅 application structureLangSmith Deployments
{
  "node_version": "22",
  "graphs": {
    "deep_agent_ide": "./src/agents/deep-agent-ide.ts:agent"
  },
  "env": ".env",
  "http": {
    "app": "./src/api/app.ts:app"
  }
}
你的 custom routes 会在与 LangGraph API 相同的 host 上可用。对于使用 langgraph dev 的本地开发环境,该 host 是 http://localhost:2024
http.app 中定义的 custom routes 优先级高于默认 LangGraph routes。这意味着你 可以在需要时覆盖内置 endpoints,但要小心不要意外覆盖 /threads/runs 这样的 routes。

Building the frontend

Frontend 有三个 panel:file tree sidebar、code/diff viewer 和 chat panel。 它使用 useStream 处理 agent conversation,并使用 custom API endpoints 进行 file browsing。 对于 production deployment,请将 apiUrl 指向你的 LangSmith Deployment,启用 reconnectOnMountfetchStateHistory,并在每次 run 时传入稳定的 thread_id。 如需了解这些设置,以及如何使用 thread_id 和 runtime context 调用 agent,请参阅 Going to production 中的 Frontend

Thread creation

在页面加载时创建 LangGraph thread,并将其 ID 持久化到 sessionStorage, 使页面重新加载后可以重新连接到同一个 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,
  });

  // 首次 mount 时创建 thread。
  useEffect(() => {
    if (threadId) return;
    stream.client.threads.create().then((t) => updateThreadId(t.thread_id));
  }, [stream.client, threadId, updateThreadId]);

  // 将 threadId 传给 sandbox file hooks。
  const { tree, files } = useSandboxFiles(threadId);
  // ...
}
“new thread” 按钮会清除已存储的 ID,使下一次 mount 创建新的 thread (以及 sandbox):
function handleNewThread() {
  updateThreadId(null);
}

File state management

跟踪 sandbox filesystem 的两个 snapshot:original state(agent 运行前) 和 current state(实时更新)。API URL 中包含 thread ID,因此请求始终会命中 正确的 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

IDE 体验的关键是在 agent 工作时更新文件,而不是等它结束后再更新。 监听 stream messages 中来自 file-mutating tools 的 ToolMessage 实例。 当 write_fileedit_file tool call 完成时,刷新对应文件。当 execute 完成时,刷新全部内容,因为 shell command 可能修改任意文件:
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(() => {
    // 从 AI messages 构建 file-mutating tool calls 的映射。
    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 });
        }
      }
    }

    // 当 file-mutating tool 对应的 ToolMessage 出现时刷新。
    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]);
}

Detecting changed files

在每次 agent run 之前,对当前文件内容创建 snapshot。文件刷新后, 将其与 snapshot 比较,以识别哪些文件发生了更改:
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;
}
当用户选择已更改文件时,默认显示 diff view,使用户可以立即看到 agent 修改了什么。

Displaying diffs

使用适合 framework 的 diff library 渲染 unified diffs:
FrameworkLibraryComponent
React@pierre/diffs<FileDiff> 搭配 parseDiffFromFile
Vue@git-diff-view/vue<DiffView> 搭配来自 @git-diff-view/filegenerateDiffFile
Svelte@git-diff-view/svelte<DiffView> 搭配来自 @git-diff-view/filegenerateDiffFile
Angularngx-diff<ngx-unified-diff> 搭配 [before][after]
使用 @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

显示所有已修改文件的摘要,并包含行级 addition/deletion 计数。这会让用户快速了解 agent 的影响,类似于 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");
    // 通过比较行来计算 additions/deletions。
    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

在以下场景中,sandbox 是合适选择:
  • Coding agents 会创建、修改和运行代码,并且需要聊天之外的可视化界面。
  • Code review workflows 中,agent 建议更改,用户在接受前查看 diffs。
  • Tutorial or learning apps 中,AI assistant 帮助用户逐步构建项目, 并在上下文中显示变更。
  • Prototyping tools 中,用户用自然语言描述功能,并实时观察 agent 实现它们。

Best practices

Frontend 相关:
  • sessionStorage 中持久化 threadId,使页面重新加载时重新连接到同一个 thread 和 sandbox,而不是创建新的 thread 和 sandbox。
  • 在每个相关 tool call 上同步文件,不要只在 run 结束时同步。监听 write_fileedit_fileexecute tool messages,并立即刷新。
  • 对已更改文件默认显示 diff view。当用户点击 agent 修改过的文件时, 先显示 diff,因为这是他们最关心的内容。
  • 对 read-only operations 显示紧凑的 tool results。不要在 chat 中倾倒 read_file 的完整输出,而是显示类似 Read router.js L1-42 的单行内容。 将完整输出展示留给 mutating tools。
  • 从 file tree 中过滤 node_modules。用户不需要浏览数千个 dependency files。 获取 tree 时将它们过滤掉。
对于 backends 和 sandboxes:
  • 对 production apps 使用 thread-scoped sandboxes。请参阅 Sandbox lifecycle
  • 在 agent backend 和 API server 之间共享 sandbox resolution。通过 thread metadata 让两者解析同一个环境,且不需要 in-memory caches。
  • 用真实项目填充 sandbox。请参阅 File transfers
  • 将 secrets 留在 sandbox 外部。对于 API keys,请使用 sandbox auth proxy, 而不是 environment variables 或 file uploads。
  • 在上线前添加 guardrails。为 autonomous coding agents 配置 rate limitserror handlingdata privacy middleware。

Going to production

使用 persistent sandboxes、auth、guardrails 和 production useStream 设置部署 agent。

Sandboxes

Sandbox providers、security model 和 file transfer APIs。

Frontend overview

其他 deep agent UI patterns:subagent streaming、todo lists 和 custom state。

Application structure

完整的 langgraph.json reference,包括 custom http.app routes。