📄

Request My Resume

Thank you for your interest! To receive my resume, please reach out to me through any of the following channels:

Claude Code 源码导读 04:REPL 与控制面

Digital Strategy Review | 2026

Claude Code 源码导读 04:回看 REPL.tsxAppStateStore.ts,为什么 UI 层会长成这样

文 / 果叔 · 阅读时间 / 8 Min

REPL 控制面封面图

写在前面

到了第四篇,前面的铺垫终于可以收束一下了。

如果你一上来就看 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 控制体验。

AppState 状态地图

看懂 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

五、为什么会有 useMergedToolsuseMergedCommands 这类结构

这两个 hook 很能说明 Claude Code 的 UI 层思路。

因为在运行时里,能力源不是单一的:

• 本地 built-in tools

• MCP tools

• plugin commands

• MCP commands

如果 UI 层直接分别处理这些来源,交互就会非常碎。 所以 REPL 需要一个统一的“当前可用能力视图”。

这也是为什么前几篇一直强调:

• tools 是动态能力表面

• commands 是动态能力表面

• agent definitions 也是动态能力表面

REPL 的职责之一,就是把这些动态表面组合成用户实际感知到的操作世界。

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 的关系

Mermaid 图 1

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 终端运行时。

Mr. Guo Logo

© 2026 Mr'Guo

Twitter Github WeChat