Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第 20 章:REPL 实现

“REPL —— Read-Eval-Print Loop,一个看似简单的概念,在 Agent 系统中却演化为一个包含查询循环、权限协商、推测执行、多 Agent 路由的复杂有机体。”

src/screens/REPL.tsx 是 Claude Code 的主界面组件,5000+ 行代码使其成为整个代码库中最大的单个文件。它不仅仅是一个“输入-输出“界面,而是整个应用的中枢神经系统 —— 管理着消息流、工具调用、权限协商、会话恢复、多 Agent 协调等几乎所有核心功能。本章将解构这一巨型组件的内部结构。

20.1 REPL 组件 —— 5000 行主界面的结构

20.1.1 导入分析

REPL 组件的导入列表本身就是一份架构文档。仅前 200 行就导入了超过 100 个模块:

// src/screens/REPL.tsx
import { c as _c } from "react/compiler-runtime";
import { feature } from 'bun:bundle';

// 状态管理
import { useAppState, useSetAppState, useAppStateStore } from '../state/AppState.js';

// 核心查询引擎
import { query } from '../query.js';

// 消息处理
import { handleMessageFromStream, createUserMessage, createAssistantMessage,
         isCompactBoundaryMessage, getMessagesAfterCompactBoundary } from '../utils/messages.js';

// 工具系统
import { getTools, assembleToolPool } from '../tools.js';
import useCanUseTool from '../hooks/useCanUseTool.js';

// 权限系统
import { applyPermissionUpdate, persistPermissionUpdate } from '../utils/permissions/PermissionUpdate.js';
import { PermissionRequest } from '../components/permissions/PermissionRequest.js';

// 会话管理
import { deserializeMessages } from '../utils/conversationRecovery.js';
import { partialCompactConversation } from '../services/compact/compact.js';

// 多 Agent
import { injectUserMessageToTeammate } from '../tasks/InProcessTeammateTask/InProcessTeammateTask.js';
import { isLocalAgentTask, queuePendingMessage } from '../tasks/LocalAgentTask/LocalAgentTask.js';

20.1.2 条件导入与死代码消除

REPL 大量使用 feature() 宏进行条件导入,确保内部特性不泄露到外部构建:

// 语音集成 -- 仅 VOICE_MODE 特性开启时加载
const useVoiceIntegration = feature('VOICE_MODE')
  ? require('../hooks/useVoiceIntegration.js').useVoiceIntegration
  : () => ({ stripTrailing: () => 0, handleKeyEvent: () => {}, resetAnchor: () => {} });

// 挫折检测 -- 仅内部(ant)构建
const useFrustrationDetection = "external" === 'ant'
  ? require('../components/FeedbackSurvey/useFrustrationDetection.js').useFrustrationDetection
  : () => ({ state: 'closed', handleTranscriptSelect: () => {} });

// 协调器模式
const getCoordinatorUserContext = feature('COORDINATOR_MODE')
  ? require('../coordinator/coordinatorMode.js').getCoordinatorUserContext
  : () => ({});

这种模式的精妙之处在于:当 feature('VOICE_MODE') 在编译时为 false 时,整个 require 分支被消除,相关模块根本不会进入产品包。替代的空实现保持了类型兼容性,调用方无需任何条件判断。

20.1.3 组件结构

REPL 的渲染输出可以分解为以下布局结构:

graph TD
    REPL[REPL Component]
    REPL --> KBS[KeybindingSetup<br/>键绑定初始化]
    REPL --> GKH[GlobalKeybindingHandlers<br/>全局快捷键]
    REPL --> CKH[CommandKeybindingHandlers<br/>命令快捷键]
    REPL --> CRH[CancelRequestHandler<br/>取消请求处理]

    REPL --> Layout[主布局 Box]
    Layout --> Header[状态栏区域]
    Layout --> MsgArea[消息区域]
    Layout --> InputArea[输入区域]

    MsgArea --> VML[VirtualMessageList<br/>虚拟消息列表]
    MsgArea --> TaskList[TaskListV2<br/>任务面板]
    MsgArea --> TVH[TeammateViewHeader<br/>队友视图]

    InputArea --> PI[PromptInput<br/>输入框]
    InputArea --> PIC[PromptInputQueuedCommands<br/>排队命令]

    REPL --> Dialogs[对话框层]
    Dialogs --> PR[PermissionRequest<br/>权限请求]
    Dialogs --> ED[ElicitationDialog<br/>MCP 表单]
    Dialogs --> PD[PromptDialog<br/>Hook 提示]
    Dialogs --> CTD[CostThresholdDialog<br/>费用阈值]
    Dialogs --> IRD[IdleReturnDialog<br/>空闲返回]

20.2 输入处理 —— PromptInput 组件

20.2.1 输入组件体系

src/components/PromptInput/ 目录包含 21 个文件,构成一个完整的输入子系统:

PromptInput/
├── PromptInput.tsx              # 主输入组件
├── PromptInputFooter.tsx        # 底部栏(模式指示器、快捷键提示)
├── PromptInputFooterLeftSide.tsx # 底部左侧(成本、Token 统计)
├── PromptInputFooterSuggestions.tsx # 自动补全建议
├── PromptInputHelpMenu.tsx      # 帮助菜单
├── PromptInputModeIndicator.tsx # 模式指示器(default/plan/auto)
├── PromptInputQueuedCommands.tsx # 排队的命令显示
├── PromptInputStashNotice.tsx   # Stash 提示
├── ShimmeredInput.tsx           # 推测执行的闪烁输入
├── inputModes.ts               # 输入模式逻辑
├── inputPaste.ts               # 粘贴处理
├── useMaybeTruncateInput.ts    # 超长输入截断
├── usePromptInputPlaceholder.ts # 占位符文本
├── useShowFastIconHint.ts      # Fast 模式提示
├── useSwarmBanner.ts           # Swarm 模式横幅
├── VoiceIndicator.tsx          # 语音指示器
└── utils.ts                    # 工具函数

20.2.2 输入模式

Claude Code 支持多种输入模式,每种模式改变输入的解释方式:

// src/components/PromptInput/inputModes.ts
export function prependModeCharacterToInput(
  input: string,
  mode: PromptInputMode,
): string {
  // 不同模式使用不同前缀字符
  // '!' -> bash 模式(直接执行 shell 命令)
  // '#' -> 注释模式
  // 其他特殊前缀
}

模式切换通过 Shift+Tab(或 Windows 上的 Meta+M)循环:

stateDiagram-v2
    [*] --> Default: 启动
    Default --> Plan: Shift+Tab
    Plan --> Auto: Shift+Tab
    Auto --> Default: Shift+Tab

    Default: 默认模式(需确认工具使用)
    Plan: 计划模式(只规划不执行)
    Auto: 自动模式(自动确认所有工具)

20.2.3 输入历史与搜索

REPL 维护命令历史,支持 Up/Down 浏览和 Ctrl+R 反向搜索:

// src/screens/REPL.tsx 导入
import { addToHistory, removeLastFromHistory, expandPastedTextRefs, parseReferences } from '../history.js';
import { useSearchInput } from '../hooks/useSearchInput.js';

HistorySearchInput 组件实现了类似 Bash 的 Ctrl+R 交互 —— 输入搜索词实时过滤历史记录,Enter 选择当前匹配项。

20.2.4 Vim 模式

Claude Code 内置了完整的 Vim 模式输入编辑器。状态机定义在 src/vim/types.ts

export type VimState =
  | { mode: 'INSERT'; insertedText: string }
  | { mode: 'NORMAL'; command: CommandState }

export type CommandState =
  | { type: 'idle' }
  | { type: 'count'; digits: string }
  | { type: 'operator'; op: Operator; count: number }
  | { type: 'operatorCount'; op: Operator; count: number; digits: string }
  | { type: 'operatorFind'; op: Operator; count: number; find: FindType }
  | { type: 'operatorTextObj'; op: Operator; count: number; scope: TextObjScope }
  | { type: 'find'; find: FindType; count: number }
  | { type: 'g'; count: number }
  | { type: 'replace'; count: number }
  | { type: 'indent'; dir: '>' | '<'; count: number }
stateDiagram-v2
    state NORMAL {
        idle --> count: 数字键
        idle --> operator: d/c/y
        idle --> find: f/F/t/T
        idle --> g: g
        idle --> replace: r

        operator --> operatorCount: 数字键
        operator --> operatorFind: f/F/t/T
        operator --> operatorTextObj: i/a
        operator --> idle: motion 键 → 执行

        count --> idle: motion 键 → 执行
        count --> operator: d/c/y
    }

    INSERT --> NORMAL: Escape
    NORMAL --> INSERT: i/a/o/I/A/O

Vim 模式支持完整的 motion(h/j/k/l/w/b/e/0/$)、operator(d/c/y)、text object(iw/aw/i"/a()和 dot repeat(.),堪比一个嵌入式的 mini-vim。

20.3 消息渲染 —— 虚拟列表与 Diff 可视化

20.3.1 消息类型

REPL 需要渲染多种消息类型:

// 来自 types/message.ts
export type RenderableMessage =
  | UserMessage      // 用户输入
  | AssistantMessage // 模型响应
  | ToolUseMessage   // 工具调用
  | ToolResultMessage // 工具结果
  | AttachmentMessage // 附件(CLAUDE.md、技能等)
  | SystemMessage    // 系统消息
  | ProgressMessage  // 进度信息

20.3.2 Diff 可视化

文件编辑操作使用结构化 Diff 展示变更:

src/components/
├── diff/                    # 基础 diff 算法
├── StructuredDiff.tsx       # 主 diff 组件
├── StructuredDiffList.tsx   # diff 列表
├── FileEditToolDiff.tsx     # FileEdit 工具的 diff 展示
└── FileEditToolUpdatedMessage.tsx  # 更新消息

Diff 渲染使用 HighlightedCode 组件配合语法高亮,在终端中实现了类似 GitHub PR 页面的 diff 视图:

  src/utils/auth.ts
  ┌──────────────────────────────
  │ - function getApiKey() {
  │ + function getApiKey(): string {
  │     const key = process.env.ANTHROPIC_API_KEY
  │ -   return key
  │ +   if (!key) throw new Error('Missing API key')
  │ +   return key
  │   }
  └──────────────────────────────

20.3.3 流式渲染

模型响应是流式到达的。handleMessageFromStream 处理每个流式块,增量更新消息列表:

// REPL 中的查询调用
const result = await query(
  messages,
  systemPrompt,
  tools,
  // ... 配置
  {
    onMessage: (msg) => {
      // 流式消息到达时增量更新
      setMessages(prev => handleMessageFromStream(prev, msg))
    },
    onToolUse: (toolUse) => {
      // 工具调用触发权限检查
    },
  }
)

20.3.4 消息选择器

MessageSelector 组件允许用户通过 Shift+Up 导航历史消息并执行操作(编辑、重试、复制):

// src/components/MessageSelector.tsx
export function selectableUserMessagesFilter(msg: RenderableMessage): boolean {
  // 过滤出可选择的用户消息
  // 排除 meta 消息、compact summary、tool results 等
}

export function messagesAfterAreOnlySynthetic(
  messages: RenderableMessage[],
  index: number,
): boolean {
  // 检查选中消息之后是否只有合成消息
  // 用于判断是否可以"从此处重试"
}

20.4 权限对话框 —— 交互式权限确认

20.4.1 权限请求流程

当模型请求使用工具时,REPL 需要决定是否需要用户确认。整个流程涉及多个异步参与者的竞争:

sequenceDiagram
    participant Model as AI Model
    participant Query as Query Engine
    participant REPL as REPL
    participant Classifier as Permission Classifier
    participant User as User

    Model->>Query: tool_use (e.g., BashTool)
    Query->>REPL: onToolUse callback
    REPL->>Classifier: 评估权限规则

    alt 已授权
        Classifier-->>REPL: allow
        REPL-->>Query: 执行
    else 需要确认
        Classifier-->>REPL: needs_confirm
        REPL->>User: 显示 PermissionRequest 对话框
        User->>REPL: Allow / Deny / Always Allow
        REPL-->>Query: 执行 / 拒绝
    else 拒绝
        Classifier-->>REPL: deny
        REPL-->>Query: 拒绝
    end

20.4.2 多通道权限竞争

在连接了 Claude.ai Bridge 的场景下,权限确认可以来自多个通道:

// 权限确认可以来自:
// 1. 本地终端 UI(PermissionRequest 对话框)
// 2. Bridge(claude.ai 网页端)
// 3. Channel(Telegram 等消息通道)
// 4. Hook(自动化钩子)
// 5. Classifier(权限分类器自动决策)

第一个响应的通道“赢得“控制权,其他通道被取消。这通过 claim() 机制实现 —— 类似分布式系统中的“领导选举“。

20.4.3 权限更新持久化

用户的权限决定可以被持久化,避免反复确认相同的操作:

import { applyPermissionUpdate, persistPermissionUpdate }
  from '../utils/permissions/PermissionUpdate.js';

// "Always Allow" 选项
function handleAlwaysAllow(toolName: string, command: string) {
  const update = { type: 'allow', tool: toolName, pattern: command }
  applyPermissionUpdate(update, store.setState)
  persistPermissionUpdate(update)  // 写入 settings.json
}

20.5 全局快捷键 —— Vim 模式、命令快捷键

20.5.1 快捷键处理器层次

REPL 中有三层快捷键处理器:

// 1. 全局快捷键(始终活跃)
<GlobalKeybindingHandlers />

// 2. 命令快捷键(Chat 上下文)
<CommandKeybindingHandlers />

// 3. 取消请求处理器(Escape 键)
<CancelRequestHandler />

20.5.2 Transcript 搜索

在查看转录时,REPL 支持 Vim 风格的搜索:

// REPL.tsx 中的注释
// eslint-disable-next-line custom-rules/prefer-use-keybindings --
// / n N Esc [ v 是裸字母键在转录模态上下文中,
// 与 ScrollKeybindingHandler 中的 g/G/j/k 同类
import { useInput } from '../ink.js';
  • / —— 打开搜索框
  • n —— 下一个匹配
  • N —— 上一个匹配
  • g/G —— 跳到开头/结尾
  • j/k —— 上下滚动

20.5.3 特殊交互模式

StashCtrl+S 将当前输入“暂存“,允许用户临时输入其他内容后恢复:

'ctrl+s': 'chat:stash',

External EditorCtrl+GCtrl+X Ctrl+E 打开系统编辑器编辑当前输入,这对于编写长提示尤为有用:

import { openFileInExternalEditor } from '../utils/editor.js';

Image PasteCtrl+V(或 Windows 上 Alt+V)粘贴剪贴板中的图片:

[IMAGE_PASTE_KEY]: 'chat:imagePaste',

20.5.4 Token 预算系统

REPL 集成了 Token 预算追踪:

import {
  snapshotOutputTokensForTurn,
  getCurrentTurnTokenBudget,
  getTurnOutputTokens,
  getBudgetContinuationCount,
  getTotalInputTokens,
} from '../bootstrap/state.js';
import { parseTokenBudget } from '../utils/tokenBudget.js';

当用户设置了输出 Token 预算时,REPL 在每个轮次结束后检查累计输出是否超出预算。这是控制 Agent 成本的重要机制。

20.5.5 推测执行集成

REPL 的底层集成了推测执行(Speculation)系统:

import { ShimmeredInput } from '../components/PromptInput/ShimmeredInput.js';

当用户输入时,系统可能已经在后台预测用户的意图并预执行。ShimmeredInput 以“闪烁“效果显示推测建议,用户按 Tab 接受。这类似于代码编辑器的自动补全,但作用于整个 Agent 操作序列。

20.6 查询循环

20.6.1 核心循环

REPL 的查询循环是整个应用的“心跳“。用户提交输入后,循环启动:

flowchart TD
    Input[用户输入] --> Preprocess[预处理<br/>解析引用、展开粘贴]
    Preprocess --> BuildContext[构建上下文<br/>系统提示、工具集]
    BuildContext --> Query[调用 query()<br/>流式 API 请求]
    Query --> Stream[处理流式响应]
    Stream --> ToolUse{工具调用?}
    ToolUse -->|是| Permission{需要权限?}
    Permission -->|是| Ask[显示权限对话框]
    Permission -->|否| Execute[执行工具]
    Ask -->|允许| Execute
    Ask -->|拒绝| Reject[返回拒绝结果]
    Execute --> Result[工具结果]
    Result --> Query
    ToolUse -->|否 (end_turn)| Done[轮次结束]
    Done --> AutoCompact{需要压缩?}
    AutoCompact -->|是| Compact[执行压缩]
    AutoCompact -->|否| Idle[等待下一次输入]
    Compact --> Idle
    Reject --> Query

20.6.2 会话恢复集成

REPL 启动时可能需要恢复之前的会话:

import { computeStandaloneAgentContext, restoreAgentFromSession,
         restoreSessionStateFromLog, restoreWorktreeForResume,
         exitRestoredWorktree } from '../utils/sessionRestore.js';

恢复过程包括:加载消息历史、重建文件状态缓存、恢复 Agent 上下文(名称、颜色)、恢复 worktree 状态、重播 session hooks。

本章小结

5000 行的 REPL 组件是 Claude Code 的“大脑“。它不仅是一个用户界面,更是整个 Agent 系统的编排中心。从输入处理到消息渲染,从权限协商到推测执行,从会话恢复到多 Agent 路由 —— 所有这些功能在一个组件中交织运行。

React Compiler 的自动 memoization 使得这个巨型组件能够高效运行而无需手动优化。feature() 宏的条件导入确保了内部特性不泄露到外部构建。Vim 模式的完整状态机实现则体现了对开发者体验的极致追求。

下一章我们将从 UI 层上升到工程层面,探讨 Claude Code 的性能优化策略。