阶段 2 | REPL 主循环¶
目标:理解 Claude Code 的核心交互循环——用户键入 → 提交 → 流式响应 → 渲染反馈——在 5005 行的
REPL.tsx里是怎么组织的。 时长:2~3 天 前端类比:整个<App />根组件(5005 行),但宿主从<div id="root">换成了 TTY。
2.1 为什么 REPL 这么大¶
REPL 巨型化的根因是 Claude Code 把"状态派生 + 副作用编排 + UI 渲染"三层都集中在一个组件里。对前端工程师来说,相当于把 <App /> 写 5000 行——坏味道,但受限于 TUI 框架的能力(Ink 没有 React Server Components、没有好的代码分割机制),不得不如此。
阅读时的心态:别试图顺序读,按"数据流"切分。
2.2 顶层结构¶
REPL 函数体可粗分为 6 段:
| 行号区间 | 内容 | 性质 |
|---|---|---|
| ~580-800 | State Hooks 块 | useState/useRef/useMemo 声明 30+ 个本地 state |
| ~800-1100 | Custom Hooks 块 | 调用 ~40 个 use* 钩子编排副作用 |
| ~1100-1500 | 派生状态 / 选择器 | useMemo 计算 from state |
| ~1500-2500 | 事件处理函数 | onSubmit / onCancel / onAbort / onToolConfirm 等回调 |
| ~2500-4500 | 子组件树渲染 | JSX 返回,组装 Box/Text/子组件 |
| ~4500-5000 | 辅助渲染分支 | 各种 dialog、modal、status bar 条件渲染 |
💡 不要从 1 行读到 5000 行。先看 imports(第 1~120 行)+ props 类型(约 572~620 行)+ JSX return(约 4200~4500 行)三段,画出"输入什么、状态什么、输出什么"。
2.3 核心 imports 透出的依赖图¶
REPL.tsx 的 import 块(第 1~120 行)是整个项目的依赖汇总:
2.3.1 渲染层(Ink)¶
import { Box, Text, useStdin, useTheme, useTerminalFocus, useTerminalTitle, useTabStatus } from '../ink.js';
import { useInput } from '../ink.js';
ink.js 暴露的 React 风格 hook:
- useInput(handler) —— 监听键盘事件,相当于 Web 的 onKeyDown 全局监听
- Box、Text —— 终端里的 <div> 和 <span>,支持 flex 布局
- useStdin —— 拿到 raw stdin(用于图片粘贴等)
- useTheme —— 当前主题色
- useTerminalFocus —— 终端窗口是否聚焦
- useTabStatus —— Tab 状态
2.3.2 输入层¶
import PromptInput from '../components/PromptInput/PromptInput.js';
import { PromptInputQueuedCommands } from '../components/PromptInput/PromptInputQueuedCommands.js';
import { prependModeCharacterToInput } from '../components/PromptInput/inputModes.js';
PromptInput(2338 行)—— 输入框本体inputModes.ts—— 输入模式:默认、命令、shell、vim、voice ...
2.3.3 状态层¶
import { ... } from '../bootstrap/state.js'; // 全局会话状态
import { ... } from '../cost-tracker.js';
import { useCostSummary } from '../costHook.js';
2.3.4 副作用层(hooks)¶
import { useLogMessages } from '../hooks/useLogMessages.js';
import { useReplBridge } from '../hooks/useReplBridge.js';
import { useRemoteSession } from '../hooks/useRemoteSession.js';
import { useDirectConnect } from '../hooks/useDirectConnect.js';
import { useSSHSession } from '../hooks/useSSHSession.js';
import { useAssistantHistory } from '../hooks/useAssistantHistory.js';
import { useIdeLogging } from '../hooks/useIdeLogging.js';
import { useApiKeyVerification } from '../hooks/useApiKeyVerification.js';
import { useSkillImprovementSurvey } from '../hooks/useSkillImprovementSurvey.js';
import { useAfterFirstRender } from '../hooks/useAfterFirstRender.js';
import { useDeferredHookMessages } from '../hooks/useDeferredHookMessages.js';
import { useBackgroundTaskNavigation } from '../hooks/useBackgroundTaskNavigation.js';
import { useSwarmInitialization } from '../hooks/useSwarmInitialization.js';
import { useTeammateViewAutoExit } from '../hooks/useTeammateViewAutoExit.js';
use* 命名的 hooks 全部放在 src/hooks/,是 REPL 的"业务编排"层。每个 hook 通常负责一个独立 concern。
2.3.5 键位层¶
import { GlobalKeybindingHandlers } from '../hooks/useGlobalKeybindings.js';
import { CommandKeybindingHandlers } from '../hooks/useCommandKeybindings.js';
import { KeybindingSetup } from '../keybindings/KeybindingProviderSetup.js';
import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js';
import { getShortcutDisplay } from '../keybindings/shortcutFormat.js';
import { CancelRequestHandler } from '../hooks/useCancelRequest.js';
useGlobalKeybindings—— 全局快捷键(Ctrl+C 退出、Ctrl+L 清屏等)useCommandKeybindings—— 命令模式快捷键KeybindingSetup—— 全局键位 Provider
2.3.6 工具/权限层¶
import { PermissionRequest, type ToolUseConfirm } from '../components/permissions/PermissionRequest.js';
import { WorkerPendingPermission } from '../components/permissions/WorkerPendingPermission.js';
import { ElicitationDialog } from '../components/mcp/ElicitationDialog.js';
import { PromptDialog } from '../components/hooks/PromptDialog.js';
2.3.7 上下文/通知层¶
import { useNotifications } from '../context/notifications.js';
import { sendNotification } from '../services/notifier.js';
import { useFpsMetrics } from '../context/fpsMetrics.js';
import { useTerminalNotification } from '../ink/useTerminalNotification.js';
💡 REPL 的 import 列表就是"项目功能矩阵"。读懂这些 import,等于读懂了 Claude Code 的全部能力。
2.4 三大数据流¶
把 REPL 想象成一个有 3 条数据流的"Web 应用":
┌─────────────────────────────────────────────────────────────────┐
│ 输入流 PromptInput → REPL.handleSubmit │
│ ↓ │
│ 状态流 AppState.setState() → store 通知 listeners │
│ ↓ │
│ 输出流 AppState.getState() → 子组件 re-render → Ink → TTY │
└─────────────────────────────────────────────────────────────────┘
2.4.1 输入流:用户键入 → 提交¶
链路:
1. 用户键入 → Ink useInput 触发 → PromptInput 维护本地 input buffer
2. 用户按 Enter → PromptInput 调 onSubmit(text) 回调
3. REPL 的 handleSubmit 接收 → 构造 UserMessage → push 到 AppState
4. 调用 query() 发起 agent 循环
关键文件:
- src/components/PromptInput/PromptInput.tsx(2338 行)
- src/hooks/useInputBuffer.ts(输入缓冲区管理)
- src/hooks/useCommandKeybindings.tsx(命令模式快捷键)
PromptInput 内部组件结构(看 2338 行的拆分):
PromptInput.tsx
├── HistorySearchInput.tsx (Ctrl+R 历史搜索)
├── inputModes.ts (模式定义: normal/shell/vim/...)
├── inputPaste.ts (粘贴大段文本处理)
├── IssueFlagBanner.tsx (错误提示条)
├── Notifications.tsx (输入框上方通知)
├── PromptInputFooter.tsx (底部 hint 栏)
│ ├── PromptInputFooterLeftSide.tsx
│ ├── PromptInputFooterSuggestions.tsx
│ └── PromptInputHelpMenu.tsx
├── PromptInputModeIndicator.tsx (显示当前模式)
├── PromptInputQueuedCommands.tsx (排队中的命令)
├── PromptInputStashNotice.tsx
├── ShimmeredInput.tsx (流光动效输入)
├── usePromptInputPlaceholder.ts
├── useShowFastIconHint.ts
├── useSwarmBanner.ts
├── useMaybeTruncateInput.ts
└── VoiceIndicator.tsx
💡 学习技巧:先看
PromptInput.tsx顶部的interface PromptInputProps,了解它接什么 props。Props 决定 API 边界。
2.4.2 状态流:副作用 + 派生¶
链路:
1. useLogMessages 监听 store 的消息变化
2. useReplBridge 维护 REPL ↔ IDE/Remote 的桥接
3. useRemoteSession / useSSHSession 处理远程会话
4. useApiKeyVerification 定期检查 API key 有效性
5. useAfterFirstRender 第一次渲染后的初始化
每个 use* hook 都是"独立 concern":
- 单一职责——只关心一件事
- 挂载即订阅——useEffect 内部 store.subscribe()
- 卸载即清理——返回 cleanup 函数
💡 这正是 Web 项目
useEffect的最佳实践——只是 Claude Code 把它标准化成了 85 个具名 hook。
2.4.3 输出流:渲染反馈¶
链路:
1. AppState 变化(store 通知 listeners)
2. REPL 通过 useLogMessages 拿到 messages
3. 传给 <VirtualMessageList messages={...} />
4. VirtualMessageList 渲染可见窗口(虚拟滚动)
5. 每条 message 用对应的 components/messages/*Message.tsx 渲染
关键文件:
- src/components/VirtualMessageList.tsx(1081 行)—— 长会话虚拟化
- src/components/messages/*(21 个)—— 消息渲染器
- src/components/Messages.tsx + Message.tsx + MessageRow.tsx + MessageModel.tsx —— 消息管线
VirtualMessageList 的关键设计(前端重点):
- 维护一个 JumpHandle ref
- 暴露 jumpToMessage / scrollToBottom / scrollToTop 给父组件
- 内部用 useLayoutEffect 测量每条消息高度
- 用 cache(createFileStateCacheWithSizeLimit)缓存已渲染消息的高度
REPL 用 useRef<JumpHandle> 拿到句柄,可以在用户按 PgDn/PgUp 时调用 jump。
2.5 关键交互:消息流¶
消息在 AppState 里是个数组(推测),每条 message 有 type 字段,对应不同的渲染器:
| Message Type | 渲染器文件 | 内容 |
|---|---|---|
user |
AttachmentMessage.tsx + 主消息组件 |
用户输入(含附件) |
assistant |
AssistantTextMessage.tsx / AssistantThinkingMessage.tsx / AssistantToolUseMessage.tsx |
LLM 回复(文本/思考/工具调用) |
attachment |
AttachmentMessage.tsx |
图片、PDF 等 |
tool_use |
FileEditToolDiff.tsx / BashToolDiff.tsx 等 |
工具调用展示 |
tool_result |
各种 *ToolResult*.tsx |
工具执行结果 |
plan_approval |
PlanApprovalMessage.tsx |
Plan Mode 审批 |
compact_boundary |
CompactBoundaryMessage.tsx |
上下文压缩分界点 |
rate_limit |
RateLimitMessage.tsx |
限流提示 |
task_assignment |
TaskAssignmentMessage.tsx |
多 agent 任务分发 |
system_* |
SystemTextMessage.tsx / SystemAPIErrorMessage.tsx |
系统消息 |
shutdown |
ShutdownMessage.tsx |
关闭 |
💡 这种 dispatch 模式在 Web 项目里也很常见:一份数据数组 + type-to-component map,避免大量 if/else。
2.6 键位系统详解¶
目录:
src/keybindings/文件数:~10 个
2.6.1 三层结构¶
GlobalKeybindingHandlers 全局(Ctrl+C 取消、Ctrl+L 清屏...)
CommandKeybindingHandlers 命令模式(输入 `/` 后的快捷键)
useInput from ink.js 兜底(单字符快捷键如 Esc、g、G)
2.6.2 关键文件¶
src/keybindings/KeybindingProviderSetup.tsx—— 把所有快捷键注册到全局src/keybindings/useShortcutDisplay.ts—— 把快捷键转成展示文案("⌘C" vs "^C")src/keybindings/shortcutFormat.ts—— 跨平台格式化
2.6.3 为什么不用 mousetrap 那种库?¶
Claude Code 用了自研的 keybinding 系统,因为: - 需要支持 chord 组合(先按 Ctrl+K 再按 Ctrl+S) - 需要 context-aware(在输入框里 Esc 是取消输入,在历史模式是退出) - 需要 可发现性——所有快捷键都能查到提示文案
src/keybindings/ 目录就是 TUI 版的"键盘事件总线"。
2.7 子组件树¶
REPL 渲染的子组件(推测层级):
<REPL>
<KeybindingSetup> 全局键位 Provider
<GlobalKeybindingHandlers>
<CommandKeybindingHandlers>
<CancelRequestHandler>
<Box flexDirection="column" height="100%">
<StatusBar /> 顶部状态条
<VirtualMessageList /> 中间消息流
<PromptInput /> 底部输入框
<PromptInputFooter /> 输入框底部 hint
<DialogLayer> 模态对话框层
<PermissionRequest /> 权限询问
<PromptDialog /> 普通提问
<ElicitationDialog /> MCP elicit
<CostThresholdDialog /> 费用阈值
<IdleReturnDialog /> 闲置返回
<SkillImprovementSurvey /> 技能调研
...
</DialogLayer>
</Box>
</KeybindingSetup>
</REPL>
⚠️ Dialog Layer 的实现技巧:在 TUI 里,没有真正的 z-index。"模态"靠"渲染顺序靠后 + 全屏 Box 覆盖"实现。
2.8 性能优化点¶
REPL 处理长会话(几千条消息)+ 流式响应(每秒数十个 token 更新),性能至关重要:
2.8.1 虚拟化¶
VirtualMessageList维护一个滑动窗口,只渲染可见的 ~50 条- 滚动时用
requestAnimationFrame节流
2.8.2 Deferred updates¶
-useDeferredValue —— 流式响应时低优先级更新(React 18 Concurrent)
- useDeferredHookMessages —— 自研的 hook 消息延迟更新
2.8.3 后台 housekeeping¶
不阻塞主线程的清理任务(清理文件缓存、压缩 session 等)。2.8.4 FPS 监控¶
监听渲染帧率,< 30 fps 时降级渲染(前端 TUI 的卡顿监控)。2.9 关键洞察¶
2.9.1 REPL 是"功能清单"而非"组件"¶
不要把 REPL 当成"一个组件"读。它是 85 个 hooks + 50+ 子组件的编排器。理解这个本质后,5000 行就没那么可怕了——大部分是 import 和 hook 调用。
2.9.2 Inbox Polling 与多 agent¶
import { useInboxPoller } from '../hooks/useInboxPoller.js';
import { useSwarmInitialization } from '../hooks/useSwarmInitialization.js';
import { useTeammateViewAutoExit } from '../hooks/useTeammateViewAutoExit.js';
2.9.3 Voice / SSH / IDE 集成¶
REPL 同时支持: - 本地 TTY 交互 - Voice mode(语音输入) - SSH 远程会话 - IDE bridge(VSCode/JetBrains 集成) - Remote session(云端会话)
每种模式对应一个 use*Session hook,REPL 内部按条件启用。
2.9.4 Ant-only / DCE 分支¶
REPL.tsx 顶部有大量:
const AntModelSwitchCallout = "external" === 'ant' ? require(...) : null;
const WebBrowserPanelModule = feature('WEB_BROWSER_TOOL') ? require(...) : null;
null,可以直接跳过。
2.10 阅读清单(按优先级)¶
- ✅
src/screens/REPL.tsx:1-120(imports)—— 看依赖图 - ✅
src/screens/REPL.tsx:572-650(REPL函数签名 + props 类型)—— 看 API 边界 - ✅
src/components/PromptInput/PromptInput.tsx:1-80(imports + props)—— 输入组件全貌 - ✅
src/components/VirtualMessageList.tsx:1-60(imports + props)—— 虚拟列表 API - 🔍
src/screens/REPL.tsx:4200-4500(JSX return)—— 渲染树结构 - 🔍
src/components/PromptInput/PromptInput.tsx:2200-2338(事件处理)—— 提交流程 - 📌
src/keybindings/KeybindingProviderSetup.tsx(通读)—— 键位系统 - 📌
src/hooks/useLogMessages.ts+useAfterFirstRender.ts(通读)—— 副作用模板
2.11 练习任务¶
- 列出 REPL.tsx 的所有 import 类别(渲染/输入/状态/键位/工具/服务/hooks),归类写下来
- 画一张"用户键入到消息渲染"的数据流图,标注每个节点对应的文件
- 找到
useInput的所有用法(grep -rn "useInput(" src/),理解键位系统全貌 - 对比 Web React:如果让你把
<VirtualMessageList>改成 Web 版的虚拟列表(用react-window),你会在哪里改?怎么改?
2.12 下一步¶
进入 阶段 3:状态管理 —— REPL 引用的 state/* 是个 60 行的极简 store,和 Zustand 几乎一样。