1. 项目缘起:为什么一个代码代理需要一个干净的Web界面?

如果你和我一样,长期在终端里和Claude Code这类AI代码代理打交道,那你一定体会过那种“精神分裂”般的痛苦。一边是AI在疯狂输出代码片段、调试信息和文件变更,另一边是你需要在编辑器、终端、浏览器和项目文件之间来回切换,试图跟上它的节奏。屏幕被分割成无数个窗口,上下文在频繁切换中丢失,一个简单的代码审查或功能实现,最后演变成一场注意力管理的灾难。

我最近就深陷这种困境。Claude Code代理在终端里运行时,它的输出是线性的、混杂的。代码建议、系统命令执行结果、文件读写状态、错误堆栈,所有这些信息都挤在同一个黑色窗口里,按照时间顺序一股脑地涌出来。我需要实现一个API端点,代理生成了路由文件、控制器逻辑、数据库模型,还跑了几个测试。但当我需要回头查看它最初对数据结构的定义时,我得在几百行的终端历史里疯狂滚动搜索。更别提当它同时操作多个文件时,我根本分不清哪段输出对应哪个文件的修改。

这种体验不是在“增强”我的工作流,而是在“杀死”它。效率的瓶颈不再是AI的能力,而是我作为人类消化和处理信息的方式。我需要一个能结构化展示AI工作过程、能让我快速定位关键信息、并且能与我现有的开发环境无缝集成的界面。既然没有现成的,那就自己造一个。这就是“Claude Code Agent Web UI”项目的起点:一个为终端里的AI代码代理量身定制的、清爽的Web控制台,目标是把人从混乱的终端输出中解放出来,重新获得对开发过程的掌控感。

2. 核心设计思路:从终端日志到可视化工作流

这个项目的核心,不是简单地给终端输出套个网页壳子。它的本质是 对AI代理工作过程的信息进行解构、分类和可视化重述 。终端输出是原始的、面向机器的日志流;而Web UI需要将其转化为结构化的、面向人类的操作叙事。

2.1 信息流的解构与分类

首先,我分析了Claude Code代理在终端中的典型输出。它大致可以分解为以下几种类型:

  1. 思考与规划 :代理在行动前对问题的分析、步骤拆解。例如:“我需要先检查现有的用户模型,然后创建迁移文件来添加 avatar_url 字段。”
  2. 命令执行 :代理实际在系统终端中运行的命令及其输出。例如: $ npm install lodash 以及随后的安装日志。
  3. 代码操作 :对文件的具体修改,包括创建、读取、更新、删除。这是最核心的部分,需要展示代码差异(Diff)。
  4. 结果与状态 :操作执行后的总结、成功/失败状态、以及下一步建议。
  5. 用户交互请求 :代理遇到模糊点时,向用户提出的澄清问题。例如:“您希望这个API返回分页数据吗?”

基于这个分类,UI的设计思路就清晰了。我们不能用一个大文本框展示所有内容,而应该像开发者的IDE或Git历史界面一样,提供一个 时间线或活动流 的视图,其中每一项活动都根据其类型被渲染成不同的视觉组件。

2.2 架构选型:轻量、实时、可嵌入

我的目标是快速构建一个可用的工具,而不是一个重型企业应用。因此技术选型上遵循“轻量、实时、可嵌入”的原则:

  • 后端框架 :选择了 FastAPI 。原因很简单:异步支持好(对处理实时日志流至关重要),性能高,编写API简洁直观,而且自带交互式API文档(Swagger UI),这在开发调试阶段非常有用。
  • 前端框架 :选择了 Vue 3 + Composition API 。对于这种高度动态、状态复杂的单页面应用,Vue的响应式系统非常合适。Vue 3的Composition API让逻辑组织更灵活,尤其适合管理代理活动流的状态。相比React,Vue的模板语法在快速构建结构化UI时,我个人感觉更直观一些。
  • 实时通信 :这是关键。终端输出是持续不断的流。传统的HTTP轮询(Polling)效率低下且延迟高。 WebSocket 是唯一的选择。它能在客户端和服务器之间建立全双工通信通道,让服务器在收到代理新输出的瞬间,就主动推送到所有连接的Web UI客户端,实现真正的实时更新。
  • 进程交互 :如何捕获终端中Claude Code代理的输出?最直接的方式是使用子进程(Subprocess)管理。我们的后端服务可以启动一个子进程来运行Claude Code代理命令(例如 claude code --project ./myapp ),然后实时捕获这个子进程的 stdout (标准输出)和 stderr (标准错误)。捕获到的每一行数据,通过WebSocket广播出去。
  • 代码高亮与Diff展示 :这是提升体验的核心。前端需要集成强大的代码编辑器组件来展示代码片段和高亮。我选择了 Monaco Editor ,它就是VS Code编辑器背后的组件,对代码高亮、Diff显示的支持是行业顶尖的。虽然体积有点大,但为了完美的代码查看体验,值得。

注意 :直接运行用户提供的命令存在安全风险。在这个个人工具场景下,我默认运行的是可信的、本地的开发命令。但如果要将其设计为开放服务,必须对命令进行严格的沙箱(Sandbox)隔离和白名单过滤,防止命令注入攻击。

整个架构的流程可以概括为: 终端代理进程 -> 子进程捕获 -> FastAPI后端(WebSocket广播) -> Vue前端(分类渲染) 。形成了一个从命令行到可视化界面的实时管道。

3. 关键实现细节与踩坑实录

有了设计蓝图,接下来就是动手实现。这个过程充满了“细节魔鬼”,也踩了不少坑。

3.1 后端实现:构建实时日志管道

后端首先要解决的是如何“粘合”子进程输出和WebSocket。

1. 子进程输出捕获与解析 我使用Python的 asyncio 库来创建和管理子进程,因为FastAPI本身是异步框架。

import asyncio
from fastapi import FastAPI, WebSocket
import json

app = FastAPI()

async def run_agent_command(websocket: WebSocket):
    # 启动Claude Code代理进程
    process = await asyncio.create_subprocess_exec(
        'claude', 'code', '--project', './my_project',
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE
    )

    # 实时读取stdout和stderr
    while True:
        # 同时读取标准输出和错误输出
        stdout_data = await process.stdout.readline()
        stderr_data = await process.stderr.readline()

        if stdout_data:
            line = stdout_data.decode('utf-8').rstrip()
            # 初步解析:尝试判断行类型
            parsed_data = parse_agent_output(line)
            # 通过WebSocket发送给前端
            await websocket.send_json(parsed_data)

        if stderr_data:
            line = stderr_data.decode('utf-8').rstrip()
            # 错误信息单独归类
            await websocket.send_json({"type": "error", "content": line})

        # 如果进程结束,退出循环
        if process.returncode is not None:
            break

这里的 parse_agent_output 函数是关键。Claude Code代理的输出通常有某种模式(比如以特定前缀开头,如 THOUGHT: COMMAND: FILE: )。我需要编写规则或简单的正则表达式来初步分类。更复杂的解析(比如从代码块中提取语言类型)可以放在前端或后端更深入的处理器中。

2. WebSocket连接管理 FastAPI的WebSocket支持很简洁。我需要管理多个连接,以便多个浏览器标签页可以同时查看日志。

from fastapi import WebSocket, WebSocketDisconnect

class ConnectionManager:
    def __init__(self):
        self.active_connections: List[WebSocket] = []

    async def connect(self, websocket: WebSocket):
        await websocket.accept()
        self.active_connections.append(websocket)

    def disconnect(self, websocket: WebSocket):
        self.active_connections.remove(websocket)

    async def broadcast(self, message: dict):
        # 向所有活跃连接广播消息
        for connection in self.active_connections:
            try:
                await connection.send_json(message)
            except:
                # 如果发送失败,可能是连接已断开,将其移除
                self.disconnect(connection)

manager = ConnectionManager()

@app.websocket("/ws/logs")
async def websocket_endpoint(websocket: WebSocket):
    await manager.connect(websocket)
    try:
        # 这里可以发送已有的历史日志,或者等待新日志
        await websocket.receive_text() # 保持连接,实际可能不需要接收客户端消息
    except WebSocketDisconnect:
        manager.disconnect(websocket)

踩坑一:输出流阻塞与缓冲区 最初我直接使用 process.communicate() ,但发现它会等待进程结束才返回所有输出,完全无法实现“实时”。换成 readline() 异步读取才是正解。另外,需要注意子进程的缓冲区。有些程序(或Python自身的print)在输出不是终端时会进行缓冲,导致日志延迟。解决方法是在启动子进程时设置环境变量 PYTHONUNBUFFERED=1 ,或者让代理命令本身使用 flush 机制。

踩坑二:异常处理与连接清理 WebSocket连接很不稳定,浏览器标签页关闭、网络抖动都会导致断开。如果不在 broadcast 方法里做好 try-except ,一个失效的连接会导致向所有连接发送消息失败。同时,子进程也需要在WebSocket断开后被正确终止,防止僵尸进程。

3.2 前端实现:构建动态活动流视图

前端的目标是将后端推送来的、分类好的数据,渲染成一个直观的“活动流”。

1. 状态管理与活动流组织 我使用Vue 3的 reactive ref 来管理一个代表活动流的数组。

import { ref } from 'vue';

const activityStream = ref([]); // 存储所有活动项

// 活动项的数据结构
// {
//   id: 'unique-id',
//   timestamp: '2023-10-27T10:00:00Z',
//   type: 'thought' | 'command' | 'file_create' | 'file_edit' | 'info' | 'error' | 'question',
//   title: '创建用户模型',
//   content: '...', // 原始内容或结构化数据
//   details: { ... } // 附加信息,如文件路径、代码diff等
// }

当WebSocket接收到新消息时,就解析它,创建一个新的活动项对象,并 unshift activityStream 数组的开头(这样最新的活动显示在最上面)。

2. 组件化渲染 根据 activity.type ,渲染不同的UI组件。这是Vue的强项。

<template>
  <div class="activity-feed">
    <div v-for="activity in activityStream" :key="activity.id" class="activity-item">
      <ActivityThought v-if="activity.type === 'thought'" :activity="activity" />
      <ActivityCommand v-if="activity.type === 'command'" :activity="activity" />
      <ActivityCode v-if="activity.type === 'file_edit'" :activity="activity" />
      <ActivityQuestion v-if="activity.type === 'question'" :activity="activity" @answer="handleAnswer"/>
      <!-- ... 其他类型 -->
    </div>
  </div>
</template>
  • ActivityThought :可能用一个便签纸样式的卡片展示AI的思考过程。
  • ActivityCommand :展示一个终端风格的代码块,包含执行的命令和输出结果。可以用颜色区分命令(如 $ 前缀)和输出。
  • ActivityCode :这是最复杂的组件。它需要接收代码变更的详细信息(旧内容、新内容、文件路径),并使用 Monaco Editor 来展示一个并排的Diff视图。这需要动态加载Monaco,并配置 original modified 模型。
  • ActivityQuestion :当代理需要用户输入时,渲染一个输入框和按钮,用户输入的回答可以通过一个新的WebSocket消息发回后端,后端再转发给代理进程的 stdin

3. 集成Monaco Editor展示代码Diff 集成Monaco需要一些步骤。我使用 @monaco-editor/loader 来动态加载。

<template>
  <div class="code-activity">
    <div class="file-header">{{ activity.details.filePath }}</div>
    <div ref="editorContainer" class="diff-editor"></div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import loader from '@monaco-editor/loader';

const props = defineProps(['activity']);
const editorContainer = ref(null);
let diffEditor = null;

onMounted(async () => {
  const monaco = await loader.init();
  diffEditor = monaco.editor.createDiffEditor(editorContainer.value, {
    readOnly: true,
    automaticLayout: true,
    theme: 'vs-dark' // 保持和终端类似的深色主题
  });

  const originalModel = monaco.editor.createModel(
    props.activity.details.oldContent || '',
    props.activity.details.language // 如 'javascript', 'python'
  );
  const modifiedModel = monaco.editor.createModel(
    props.activity.details.newContent || '',
    props.activity.details.language
  );

  diffEditor.setModel({
    original: originalModel,
    modified: modifiedModel
  });
});

onUnmounted(() => {
  if (diffEditor) {
    diffEditor.dispose();
  }
});
</script>

踩坑三:Monaco Editor的性能与内存 当活动流很长,包含大量代码Diff时,每个Diff编辑器实例都会占用不少内存。如果无限制地创建,会导致浏览器标签页内存占用飙升。解决方案是 虚拟滚动 。只渲染可视区域内的活动项,离开可视区域的活动项对应的Monaco实例被销毁或缓存。这对于Vue来说,可以借助 vue-virtual-scroller 这类库实现,但集成复杂度会上升。我的第一个版本没有做虚拟滚动,在代理进行大规模重构(比如修改几十个文件)时,浏览器确实会变卡。这是一个已知的待优化项。

踩坑四:活动流的排序与过滤 活动项多了之后,查找特定文件或特定类型的操作会变困难。我后来增加了过滤栏(按类型过滤:只看代码修改、只看命令等)和搜索框(按文件路径或内容关键词搜索)。这要求前端在接收数据时,不仅渲染,还要建立索引。我使用了一个独立的 searchIndex 引用,存储所有活动的可搜索文本,过滤和搜索时操作这个索引,而不是直接遍历和过滤 activityStream 这个响应式数组,以避免不必要的UI重渲染。

4. 超越展示:交互与工作流集成

一个只读的仪表板还不够。真正的价值在于让Web UI成为与AI代理 交互 的界面。

4.1 实现双向通信:从界面控制代理

我扩展了WebSocket协议,不仅支持服务器向客户端推送日志,也支持客户端向服务器发送“指令”。

  1. 回答代理提问 :当 ActivityQuestion 组件出现时,用户输入答案,点击发送。前端通过同一个WebSocket连接发送一个如 {"type": "response", "query_id": "abc123", "answer": "是的,请使用分页"} 的消息。后端接收到后,将其写入代理子进程的 stdin
  2. 发送自定义命令 :我在UI侧边栏添加了一个小终端输入框。用户可以手动输入任何命令(如 git status , npm test ),发送后,后端会创建一个新的子进程执行该命令,并将其输出同样以活动流的形式广播回来。这样,Web UI就变成了一个增强版的终端,所有操作历史都结构化地保存了下来。
  3. 批准/回滚操作 :这是一个更高级的功能。对于文件修改这类“高风险”操作,UI可以展示一个“批准”按钮。只有用户点击批准后,后端才真正执行文件写入操作。甚至可以提供一个“回滚”按钮,一键将文件恢复到修改前的状态。这需要后端维护一个文件操作的事务日志。

4.2 与开发环境深度集成

为了让这个工具不止是一个独立的网页,我探索了两种集成方式:

方式一:浏览器插件 开发一个简单的浏览器插件(如Chrome Extension)。它可以在任何页面(如GitHub、文档网站)的侧边栏或弹出窗口中,显示本地的Claude Code Agent Web UI。这样,你在查阅文档时,无需切换标签页就能看到代理的执行状态。插件通过 chrome.runtime.connect 与本地后端通信(需要后端开启特定的API端点并处理CORS)。

方式二:编辑器插件 这是更理想的场景。我尝试为VS Code开发了一个插件。这个插件在VS Code内创建一个新的Webview面板,直接嵌入我开发的Web UI。最大的好处是 上下文共享

  • 文件跳转 :点击Web UI中提到的文件路径,可以直接在VS Code主编辑器中打开该文件。
  • 代码定位 :点击Diff视图中的某一行,可以在编辑器中定位到对应位置。
  • 直接操作 :在Webview中“批准”的代码变更,可以直接应用到VS Code工作区的文件中。

VS Code插件通过 vscode API与编辑器交互,通过 fetch WebSocket 与本地后端通信。虽然实现起来比纯Web复杂,但提供了无缝的体验,感觉AI代理真正成为了IDE的一部分。

5. 实际效果与未来思考

构建完成并投入使用几周后,这个工具彻底改变了我与Claude Code代理的协作方式。

效率提升是立竿见影的 。以前在终端里,我需要像侦探一样从文字流中拼凑信息。现在,所有操作一目了然:左边是清晰的时间线,点击任何一个“创建文件”或“修改文件”的活动,右边立刻展开一个清晰的代码Diff视图,绿色和红色高亮显示出精确的变更。代理的“思考过程”被放在便签卡片里,让我能理解它的决策逻辑,而不是只看到最终结果。当它提问时,输入框就在问题下方,我不需要切回终端去打字。

更重要的是,它带来了工作流的可追溯性 。这个Web UI本质上成为了一个 AI辅助开发的“黑匣子”记录仪 。项目结束后,我可以回顾整个活动流,清楚地看到AI是如何一步步构建出某个功能的。这对于复盘、知识沉淀乃至团队分享都极具价值。我可以把某个复杂任务的活动流导出为一份可读的报告。

当然,这个初版工具还有很大进化空间。除了前面提到的虚拟滚动性能优化,我还想:

  • 引入“会话”概念 :将一次完整的代理交互(从启动到任务完成)保存为一个会话,支持重放、分支对比。
  • 更智能的解析 :目前对代理输出的解析还比较基于规则。可以尝试用一个小型LLM来实时分析输出流,进行更精准的分类和摘要生成,甚至自动提取出“本次修改的核心目标是什么”。
  • 多代理协同视图 :如果未来同时运行多个AI代理处理不同任务,需要一个总控面板来监控所有代理的状态。

这个项目始于一个简单的痛点——终端太混乱。但解决它的过程,让我深入思考了人机协作界面应有的形态。AI代码代理不是另一个需要我去“命令”的工具,而是一个拥有自主性的协作者。我们的界面,不应该只是传递指令的通道,而应该是一个 共享的工作空间 ,在这里,双方的动作、意图、成果都能被清晰、结构化的呈现和理解。这个Web UI是我向这个方向迈出的一小步,而它已经让我的日常工作舒畅了太多。如果你也受困于终端的混沌,不妨也尝试构建属于你自己的“协作者面板”,你会发现,给AI一个清晰的界面,也是给你自己一片清明的思维空间。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐