Digital Strategy Review | 2026
Claude Code 源码导读 04:回看 REPL.tsx 与 AppStateStore.ts,为什么 UI 层会长成这样
文 / 果叔 · 阅读时间 / 8 Min

写在前面
到了第四篇,前面的铺垫终于可以收束一下了。
如果你一上来就看 REPL.tsx,很容易觉得它大得离谱;如果先看 AppStateStore.ts,也会觉得状态怎么什么都往里装。可当前三篇的上下文补齐之后,这两个文件反而会变得很顺。因为它们压根不是普通 UI 层,而是 Claude Code 整个 agent runtime 的控制面。
换句话说,这一篇真正想回答的问题是:一个终端里的多 Agent 系统,为什么会需要这样一层看起来“像 UI、又不只是 UI”的总控结构。
它们不是普通 UI 层,而是 Claude Code 整个 agent runtime 的统一控制面。
01
一、先看 AppStateStore.ts:这不是页面状态,是会话级运行时状态
这一节值得先读,因为只要把 AppStateStore.ts 看成页面状态容器,后面很多设计都会显得臃肿;可一旦把它看成会话级 runtime store,很多东西就都对上了。
AppState 最大的特点是:它并不只管显示。
它同时承载了三类状态:
01 交互状态
02 执行状态
03 协作状态
1. 交互状态
包括:
• 当前设置
• status line
• expanded view
• footer selection
• prompt suggestion
• notifications
• elicitation queue
• thinkingEnabled
这部分看起来像普通 UI state。
2. 执行状态
包括:
• tasks
• foregroundedTaskId
• viewingAgentTaskId
• toolPermissionContext
• mcp.clients / tools / resources
• plugins
• agentDefinitions
• todos
• sessionHooks
这部分已经明显不是“页面状态”了,而是会话运行时的核心对象仓库。
3. 协作状态
包括:
• agentNameRegistry
• teammate 相关视图状态
• remote connection status
• bridge 状态
• computer use / bagel / tungsten 相关状态
也就是说,AppState 本质上是:
当前这次 Claude Code 会话所处“世界”的统一状态树。
02
二、为什么 tasks 必须在 AppState 顶层
这是理解整套 UI 形态最关键的一点。
Claude Code 的任务系统并不是藏在某个服务内部,而是直接进入状态树顶层:
• tasks
• foregroundedTaskId
• viewingAgentTaskId
• agentNameRegistry
这会直接带来几个结果:
01 背景 agent 可以被 UI 直接消费
02 同进程 teammate 可以被切换查看
03 远程任务可以进入统一列表
04 通知、转前台、关闭、恢复都能挂到同一套交互机制上
如果没有这一层,REPL 不可能统一承载多 agent 控制体验。

看懂 AppStateStore 之后,再回头看 REPL 的体量,就不会觉得它只是一个 UI 文件太大这么简单。
03
三、REPL.tsx 为什么这么大:因为它不是聊天窗口,而是控制台
现在再看 REPL.tsx,就会明白它为什么会长成一个巨型文件。
它要同时处理这些事情:
• prompt 输入
• 消息渲染
• query 发起与流式事件接收
• command queue
• permission dialog
• sandbox permission
• background task navigation
• swarm 初始化
• mailbox bridge
• merged tools / merged commands
• MCP / plugin / IDE 联动
• teammate 视图
• task 列表
• 各类通知和 callout
这不是一个“聊天组件”该做的事,但这是一个agent 控制平面必须做的事。
04
四、REPL 的核心角色:把不同执行实体翻译成统一交互体验
从架构角度看,REPL 至少做了四类翻译。
1. 把 query loop 翻译成流式界面
query(...) 是底层异步生成器,REPL 负责把它变成:
• 消息流
• spinner
• progress
• thinking / tool progress 可视化
2. 把 task runtime 翻译成用户可理解的对象
任务在底层只是状态和输出文件,但 REPL 把它们变成:
• 任务列表
• teammate 视图
• 前台化 transcript
• 完成通知
3. 把权限系统翻译成统一的审批体验
工具权限、sandbox 权限、worker 权限,在底层来源不同,但 REPL 负责把它们收束成用户可处理的 dialog / request overlay。
4. 把分散的能力源翻译成单一输入体验
命令、MCP、plugin、skills、slash command、queued command,本来是多条通路,但 REPL 最终要让用户在一个输入口里感知和使用它们。
05
五、为什么会有 useMergedTools、useMergedCommands 这类结构
这两个 hook 很能说明 Claude Code 的 UI 层思路。
因为在运行时里,能力源不是单一的:
• 本地 built-in tools
• MCP tools
• plugin commands
• MCP commands
如果 UI 层直接分别处理这些来源,交互就会非常碎。 所以 REPL 需要一个统一的“当前可用能力视图”。
这也是为什么前几篇一直强调:
• tools 是动态能力表面
• commands 是动态能力表面
• agent definitions 也是动态能力表面
REPL 的职责之一,就是把这些动态表面组合成用户实际感知到的操作世界。

REPL 的职责不是把模型输出渲染出来,而是把不同执行实体翻译成一套用户能驾驭的交互体验。
06
六、为什么 teammate 视图和 task 视图必须在 REPL 里
很多系统会把“后台任务”和“聊天界面”完全分开。Claude Code 没这么做。
原因很简单: 在 agent runtime 里,后台任务不是附属物,而是主系统的一部分。
所以你会在 REPL 里看到:
• TaskListV2
• TeammateViewHeader
• viewing agent bootstrap
• background task navigation
这意味着 REPL 不只是显示主 agent 的对话,而是显示整个会话里的所有活跃执行体与其关系。
这一步非常关键,因为它解决了多 agent 产品最难的一件事:
用户如何理解“系统里现在不止一个智能体在工作”。
Claude Code 的答案是:不要开第二个界面,直接把它们统一回 REPL 控制面。
07
七、权限 UI 为什么会显得这么重
因为在 Claude Code 里,权限不是偶发弹窗,而是核心控制回路的一部分。
REPL.tsx 里会同时协调:
• PermissionRequest
• sandbox permission queue
• worker sandbox permission
• prompt queue
• elicitation queue
这并不是 UI 过度设计,而是前面 agent runtime 复杂性的自然结果:
• 主 agent 会请求权限
• worker agent 也会请求权限
• 某些权限来自工具
• 某些权限来自 sandbox/network
• 某些权限来自 MCP elicitation
如果没有统一的队列和聚合 UI,整套系统会非常难用。
08
八、REPL 为何要直接理解 swarm / mailbox / backgrounding
这一点也很值得注意。
它并没有把多 agent 协作完全封装在后端,而是显式接入了:
• useSwarmInitialization
• useMailboxBridge
• useBackgroundTaskNavigation
这说明 Claude Code 的产品设计并不是“后端多 agent,前端假装单 agent”,而是承认协作本身就是一等体验。
所以 REPL 必须理解:
• teammate 正在运行
• 谁在等待权限
• 当前查看的是哪个 agent
• 是否有后台任务已完成
只有这样,多 agent 才不会变成“后台发生了很多事,但用户一脸懵”。
09
九、从状态设计反推 UI 形态
你可以从几个字段直接反推 REPL 为什么长成这样:
tasks
说明 UI 必须有任务列表和任务导航。
foregroundedTaskId
说明后台任务可以被切回主舞台。
viewingAgentTaskId
说明 agent transcript 是可切换查看对象,而不是只看主对话。
agentNameRegistry
说明系统支持名字到 agent 的路由,协作不是匿名的。
toolPermissionContext
说明权限不是局部逻辑,而是全局运行条件。
mcp / plugins / agentDefinitions
说明 REPL 里的“当前可用能力”不是静态写死的。
于是你会发现,UI 层长得复杂并不是意外,而是状态模型天然要求它这样。
10
十、一张图看懂 REPL 与 AppState 的关系

11
十一、如果只给一句总结
AppStateStore.ts 定义的是会话级运行时状态模型。 REPL.tsx 做的是统一交互控制面。
所以它们的复杂,不是因为 UI 写散了,而是因为 Claude Code 把下面这些东西都统一塞到了一个控制面里:
• 主 agent
• 子 agent
• teammate
• 后台任务
• 远程任务
• 权限审批
• plugin / MCP / tools
• 多种视图切换
从产品上看,这让 Claude Code 很像一个“终端里的操作系统壳”。 从源码上看,这就是为什么 REPL 和 AppState 都必须长成现在这样。
12
十二、整个系列的收束
到这里,四篇文章刚好拼成一个完整阅读路线:
01 cli.tsx + main.tsx:系统如何装配
02 query.ts + tools.ts + toolOrchestration.ts:系统如何执行
03 AgentTool + tasks + swarm:系统如何把 agent 变成 runtime 对象
04 REPL.tsx + AppStateStore.ts:系统如何把复杂 runtime 统一呈现给用户
如果你已经顺着这四篇走完,再回看 Claude Code 的 src,它就不再像一堆巨型文件,而会更像一个边界非常清晰的多 Agent 终端运行时。