Appearance
04 — REPL 与 Query 循环
概述
本章是整个项目中 最核心 的部分,覆盖两个紧密关联的模块:
- REPL (
screens/REPL.tsx) — 用户看到的交互界面,负责输入采集、消息渲染、权限弹窗 - Query 循环 (
query.ts) — 用户看不到的对话引擎,负责 API 调用、工具执行、错误恢复
它们的关系是:REPL 启动 Query 循环并消费它 yield 的事件;Query 循环通过回调请求 REPL 显示权限弹窗或通知。
REPL 组件结构
REPL.tsx 是项目中最大的单文件之一(~3000 行),它组合了大量子组件和 Hooks:
关键 Hooks 一览
REPL 通过 Hooks 集成了 30+ 个功能模块。理解哪些 Hook 做什么事,可以快速定位功能代码:
| Hook | 功能 | 为什么在 REPL 层 |
|---|---|---|
useTerminalSize | 终端尺寸监听 | 影响布局的全局属性 |
useApiKeyVerification | API Key 验证 | 无有效 Key 时阻止查询 |
useCostSummary | 费用汇总 | 显示在状态栏 |
useReplBridge | IDE Bridge 集成 | 接收 IDE 发来的消息/文件 |
useRemoteSession | 远程会话 | claude.ai Web 端控制 |
useDirectConnect | 直连模式 | 无需 Bridge 的直连 |
useSSHSession | SSH 会话 | SSH 隧道模式 |
useSwarmInitialization | Swarm 初始化 | Teammate 启动/重连 |
useBackgroundTaskNavigation | 后台任务导航 | Tab 切换查看 Agent 输出 |
useVoiceIntegration | 语音集成 | 语音转文字输入 |
useDeferredHookMessages | 延迟 Hook 消息 | Hook 异步完成后注入消息 |
useSearchInput / useSearchHighlight | 搜索 | Ctrl+F 搜索消息 |
useFpsMetrics | 帧率监控 | 性能调试 |
用户输入处理流程
当用户按下回车提交输入时,REPL 会判断输入类型并路由到不同的处理器:
processSlashCommand 支持模糊匹配——用户输入 /com 可以匹配到 /compact。如果有多个匹配,会提示歧义。
query.ts — 核心对话循环
query.ts 是整个系统最核心的文件。它是一个 AsyncGenerator 函数,实现了一个协程式状态机。理解它需要抓住三个要点:
1. 它是一个生成器,不是普通函数
typescript
async function* query(params: QueryParams): AsyncGenerator<StreamEvent> {
// ... yield 事件给消费方 (REPL 或 SDK)
return { reason: 'completed' }
}REPL 通过 for await (const event of query(...)) 消费事件。每个 yield 都是一次 UI 更新机会。
2. 它内部是一个无限循环
queryLoop() 是一个 while(true) 循环。每次迭代是一个 API 调用轮次。如果模型返回 tool_use,工具执行后循环继续(再次调用 API);如果模型返回纯文本且没有工具调用,循环结束。
3. 它有 6 种"继续"路径
除了正常的工具→继续循环,queryLoop 还有多种异常恢复路径:
恢复路径详解
| # | 触发条件 | 恢复策略 | 细节 |
|---|---|---|---|
| ① | 413 Prompt Too Long | 上下文折叠 (Collapse Drain) | 廉价操作:删除可折叠内容块(如旧的文件读取结果),只尝试一次 |
| ② | 413 且折叠不够 | 反应式压缩 (Reactive Compact) | 昂贵操作:调用 API 生成对话摘要,替换旧消息。只尝试一次防止死循环 |
| ③ | Max Output Tokens (被 8k 截断) | 升级到 64k | Feature-gated,只在模型支持时尝试 |
| ④ | Max Output Tokens (已经 64k) | 多轮恢复 | 注入 "Resume directly — no apology, no recap..." 消息,最多 3 轮 |
| ⑤ | 服务端 5xx | 切换备用模型 | Tombstone 孤儿消息,剥离 thinking 签名 |
| ⑥ | Stop Hook 返回阻塞错误 | 重新进入循环 | Hook 可能注入新的系统消息要求模型重新响应 |
System Prompt 组装
Query 循环发送 API 请求前,需要组装完整的 System Prompt。它分为 静态 和 动态 两部分,中间有一个显式的 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 标记。
为什么分静态和动态? 因为 Anthropic API 支持 Prompt Caching。静态部分在多次 API 调用间可以复用缓存,节省 token 费用。SYSTEM_PROMPT_DYNAMIC_BOUNDARY 之前的内容被标记为全局缓存范围。
为什么 CLAUDE.md 不在 System Prompt 里? 因为 CLAUDE.md 内容可能很长(用户自定义),放在 system prompt 里会影响缓存效率。作为 User Message 注入,可以利用 <system-reminder> 标签让模型知道这是元信息,同时不破坏 system prompt 的缓存。
screens/ 其他视图
| 文件 | 说明 |
|---|---|
Doctor.tsx | 诊断界面 (/doctor):检查 API Key、Git、Node 版本、MCP 连接等 |
ResumeConversation.tsx | 恢复会话:选择历史会话并恢复上下文 |
组件库要点 (components/)
components/ 下有 100+ 组件。以下是按职责分组的核心组件:
消息渲染
| 组件 | 说明 |
|---|---|
VirtualMessageList.tsx | 虚拟滚动列表——数千条消息只渲染可见部分 |
Messages.tsx | 消息列表容器 |
Message.tsx | 单条消息渲染(根据 role 分发到子组件) |
MessageResponse.tsx | 助手回复渲染(Markdown、代码块、thinking) |
Markdown.tsx | Markdown 渲染器 |
HighlightedCode.tsx | 代码语法高亮 |
输入
| 组件 | 说明 |
|---|---|
PromptInput/ | 输入框(多行编辑、历史浏览、补全、Vim 模式) |
TextInput.tsx | 基础文本输入 |
VimTextInput.tsx | Vim 模式文本输入 |
权限(详见 08-权限)
| 组件 | 说明 |
|---|---|
permissions/PermissionRequest.tsx | 权限请求弹窗主框架 |
permissions/BashPermissionRequest/ | Bash 命令专用审批界面 |
permissions/FileEditPermissionRequest/ | 文件编辑 Diff 预览 + 审批 |
工具结果
| 组件 | 说明 |
|---|---|
StructuredDiff.tsx | 结构化 Diff 展示 |
FileEditToolDiff.tsx | 文件编辑前后对比 |
ToolUseLoader.tsx | 工具执行中的加载状态 |
状态与信息
| 组件 | 说明 |
|---|---|
StatusLine.tsx | 底部状态栏 |
Spinner.tsx | 多种加载动画(querying、thinking、tool...) |
Stats.tsx | 统计信息展示 |
TokenWarning.tsx | Token 用量警告 |