Skip to content

04 — REPL 与 Query 循环

概述

本章是整个项目中 最核心 的部分,覆盖两个紧密关联的模块:

  1. REPL (screens/REPL.tsx) — 用户看到的交互界面,负责输入采集、消息渲染、权限弹窗
  2. Query 循环 (query.ts) — 用户看不到的对话引擎,负责 API 调用、工具执行、错误恢复

它们的关系是:REPL 启动 Query 循环并消费它 yield 的事件;Query 循环通过回调请求 REPL 显示权限弹窗或通知。

REPL 组件结构

REPL.tsx 是项目中最大的单文件之一(~3000 行),它组合了大量子组件和 Hooks:

关键 Hooks 一览

REPL 通过 Hooks 集成了 30+ 个功能模块。理解哪些 Hook 做什么事,可以快速定位功能代码:

Hook功能为什么在 REPL 层
useTerminalSize终端尺寸监听影响布局的全局属性
useApiKeyVerificationAPI Key 验证无有效 Key 时阻止查询
useCostSummary费用汇总显示在状态栏
useReplBridgeIDE Bridge 集成接收 IDE 发来的消息/文件
useRemoteSession远程会话claude.ai Web 端控制
useDirectConnect直连模式无需 Bridge 的直连
useSSHSessionSSH 会话SSH 隧道模式
useSwarmInitializationSwarm 初始化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 截断)升级到 64kFeature-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.tsxMarkdown 渲染器
HighlightedCode.tsx代码语法高亮

输入

组件说明
PromptInput/输入框(多行编辑、历史浏览、补全、Vim 模式)
TextInput.tsx基础文本输入
VimTextInput.tsxVim 模式文本输入

权限(详见 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.tsxToken 用量警告