模块二:启动与状态 | 前置依赖:第 05 课 | 预计学习时间:60 分钟
学习目标
完成本课后,你将能够:
- 描述用户输入从 PromptInput 到 query() 的完整处理路径
- 解释 REPL 如何管理并发查询(queryGuard 机制)
- 说明权限请求弹窗的触发和渲染流程
- 理解流式事件如何驱动消息列表更新
6.1 REPL.tsx 概览
screens/REPL.tsx 是整个应用的主屏幕,也是最大的 React 组件文件(895KB)。它是 Agent 循环的指挥中心,协调以下所有子系统:
REPL.tsx 职责清单:
├── 用户输入接收(PromptInput)
├── 消息历史管理
├── query() 调用与流式事件处理
├── 权限请求展示与用户决策
├── 工具执行进度显示
├── 任务列表管理
├── 通知展示
├── 会话恢复
└── 键盘快捷键处理
启动入口
// replLauncher.tsx — REPL 的启动器
async function launchRepl(root, appProps, replProps, renderAndRun) {
// 动态导入(代码分割)
const { App } = await import('./components/App.js')
const { REPL } = await import('./screens/REPL.js')
// 渲染 App 壳 + REPL 内容
renderAndRun(
<App {...appProps}>
<REPL {...replProps} />
</App>
)
}
6.2 输入处理流程
从按键到 onSubmit
用户按下键盘
│
▼
ink/parse-keypress.ts — 解析终端字节为按键事件
│
▼
ink/hooks/use-input.ts — useInput hook 传递给组件
│
▼
PromptInput 组件 — 管理输入框状态(文本、光标、选区)
│ 支持多行编辑、Vim 模式、自动补全
│
▼ (按下 Enter)
onSubmit(input, helpers, speculationAccept?, options?)
onSubmit 的处理逻辑
// REPL.tsx 约第 3142 行
const onSubmit = useCallback(async (
input: string,
helpers: PromptInputHelpers,
speculationAccept?: boolean,
options?: { fromKeybinding?: boolean }
) => {
// ① 检查是否是立即命令(/clear, /help 等)
if (isImmediateCommand(input)) {
executeImmediately(input)
return
}
// ② 扩展粘贴的文本引用
expandPastedTextRefs(input)
// ③ 普通输入 → 路由到 handlePromptSubmit
handlePromptSubmit(input, helpers)
}, [deps])
命令分流
用户输入
│
├── /clear → 立即清空消息历史
├── /help → 立即显示帮助
├── /compact → 触发手动压缩
├── /model xxx → 切换模型
├── /skill xxx → 调用技能
│
└── (普通文本) → handlePromptSubmit()
→ 创建 UserMessage
→ 调用 onQuery()
6.3 Query 调用链路
onQuery:查询入口
// REPL.tsx 约第 2855 行
const onQuery = useCallback(async (messages, context) => {
// ① 并发保护:同一时间只允许一个 query
if (!queryGuard.tryStart()) {
return // 已有查询在进行中,拒绝新查询
}
try {
// ② 重置计时器和状态
resetTiming()
setMessages(messages)
// ③ 调用实际实现
await onQueryImpl(messages, context)
} finally {
queryGuard.end()
}
}, [deps])
onQueryImpl:实际执行
// REPL.tsx 约第 2661 行
const onQueryImpl = useCallback(async (messages, context) => {
// 调用 query() 异步生成器
for await (const event of query({
messages,
systemPrompt,
userContext: getUserContext(),
systemContext: getSystemContext(),
canUseTool, // 权限检查函数
toolUseContext, // 工具执行上下文
querySource, // 查询来源标记
})) {
// 处理每个流式事件
handleMessageFromStream(event)
}
}, [deps])
queryGuard:并发保护
为什么需要并发保护?
场景:用户快速按两次 Enter
第一次 Enter → onQuery #1 开始 → API 调用中...
第二次 Enter → onQuery #2 开始 → ❌ 危险!
没有 queryGuard 的后果:
- 两个 query 同时修改消息列表 → 状态混乱
- 两个 API 请求同时进行 → 费用浪费
- 工具执行交叉 → 不可预测的结果
有 queryGuard:
第一次 Enter → onQuery #1 开始 → queryGuard.tryStart() = true ✓
第二次 Enter → onQuery #2 尝试 → queryGuard.tryStart() = false ✗
→ 请求被忽略
6.4 流式事件处理
query() 返回的是一个 AsyncGenerator,每次 yield 一个事件。REPL 通过 for await 循环逐个处理:
事件类型
StreamEvent 类型:
├── RequestStartEvent — API 请求开始
├── AssistantMessage — Claude 的文字响应(部分或完整)
├── ToolUseSummaryMessage — 工具调用摘要
├── TombstoneMessage — 被压缩消息的墓碑
├── ProgressMessage — 工具执行进度
└── Message — 通用消息
handleMessageFromStream
// 简化的流式事件处理
function handleMessageFromStream(event: StreamEvent) {
switch (event.type) {
case 'request_start':
// 更新 UI:显示"思考中..."
break
case 'assistant_message':
// 追加到消息列表,触发 UI 更新
// 如果包含 tool_use 块,准备工具执行
appendMessage(event)
break
case 'tool_use_summary':
// 显示工具调用的简要描述
appendSummary(event)
break
case 'progress':
// 更新进度条(如 Bash 命令的实时输出)
updateProgress(event)
break
}
}
流式渲染的效果
时间 ─────────────────────────────────────────►
API 返回流:
"让" → "让我" → "让我看看" → "让我看看这个文件" → [tool_use: Read]
用户看到的终端:
t=0: 让
t=1: 让我
t=2: 让我看看
t=3: 让我看看这个文件
t=4: [权限请求弹窗:是否允许读取 /src/index.ts?]
6.5 权限交互
权限检查链路
query() 循环中检测到 tool_use
│
▼
findToolByName('Read')
│
▼
tool.checkPermissions(input)
│
├── behavior: 'allow' → 直接执行
├── behavior: 'deny' → 返回拒绝消息
└── behavior: 'ask' → 需要用户确认
│
▼
REPL 渲染 PermissionRequest 组件
│
├── 用户点击 Allow → 执行工具 → 结果返回
├── 用户点击 Deny → 拒绝消息 → denialTracking 记录
└── 用户点击 Always → 持久化规则 → 执行工具
权限相关 Hooks
// REPL.tsx 中的权限相关导入和 hooks
// 核心权限检查
const canUseTool = useCanUseTool()
// 权限状态管理
const applyPermissionUpdate = useApplyPermissionUpdate()
const persistPermissionUpdate = usePersistPermissionUpdate()
// 自动模式的安全降级
const stripDangerousPermissionsForAutoMode = useStripDangerousPermissions()
// 旁路开关监控
const bypassPermissionsKillswitch = useBypassKillswitch()
// 沙箱管理(网络访问等)
const sandboxManager = useSandboxManager()
多 Agent 场景的权限同步
在 Coordinator-Worker 模式中,Worker Agent 的权限请求需要传递给用户:
Worker Agent 请求权限
│
▼
sendSandboxPermissionRequestViaMailbox()
│ 通过 Agent 间邮箱传递
▼
REPL 接收到权限请求
│
▼
WorkerPendingPermission 组件渲染
│
▼
用户决策 → 通过邮箱返回给 Worker
6.6 会话管理
三种屏幕
screens/
├── REPL.tsx — 主交互屏幕(新会话或活跃会话)
├── ResumeConversation.tsx — 会话恢复屏幕(选择历史会话)
└── Doctor.tsx — 健康检查屏幕(诊断问题)
会话恢复
用户运行 `claude --resume`
│
▼
ResumeConversation.tsx
│ 显示历史会话列表
│ 用户选择一个会话
▼
加载会话消息历史
│
▼
REPL.tsx(带历史消息初始化)
│
▼
继续之前的对话
6.7 REPL 的完整渲染结构
<REPL>
├── <Header /> — 顶部状态栏
│
├── <VirtualMessageList> — 消息列表(虚拟滚动)
│ ├── <UserMessage /> — 用户消息
│ ├── <AssistantMessage /> — Claude 响应
│ ├── <ToolUseMessage /> — 工具调用展示
│ └── <ToolResultMessage /> — 工具结果展示
│
├── <TaskList /> — 后台任务列表
│
├── <PermissionRequest /> — 权限请求弹窗
├── <WorkerPendingPermission /> — Worker 权限请求
├── <SandboxPermissionRequest /> — 沙箱权限请求
│
├── <NotificationArea /> — 通知区域
│
├── <PromptInput /> — 用户输入框
│ ├── 多行编辑
│ ├── Vim 模式(可选)
│ └── 自动补全
│
└── <Footer /> — 底部快捷键提示
课后练习
练习 1:输入追踪
在 REPL.tsx 中搜索 onSubmit,从用户按 Enter 开始,逐步追踪到 query() 被调用。列出中间经过的所有函数/回调。
练习 2:queryGuard 重现
编写一个简化的 queryGuard 实现(不超过 15 行),支持 tryStart() 和 end() 方法。思考:它本质上是什么并发原语?
练习 3:事件流模拟
假设用户输入"读取 package.json 并告诉我版本号",列出 REPL 会收到的所有流式事件类型和它们的大致顺序。
练习 4:权限场景分析
对以下场景,判断权限检查的结果(allow/ask/deny):
- a) FileReadTool 读取
/src/index.ts - b) BashTool 执行
rm -rf / - c) BashTool 执行
git status - d) FileEditTool 修改
~/.bashrc
本课小结
| 要点 | 内容 |
|---|---|
| REPL 职责 | 输入接收、query 调用、权限管理、消息渲染 |
| 输入流 | PromptInput → onSubmit → 命令分流 → query() |
| 并发保护 | queryGuard 确保同一时间只有一个活跃查询 |
| 流式处理 | AsyncGenerator + for await 逐事件处理 |
| 权限交互 | checkPermissions → ask → PermissionRequest 组件 |
| 会话管理 | 新会话(REPL) / 恢复(Resume) / 诊断(Doctor) |
下一课预告
第 07 课:query.ts — Agent 循环的心脏 — 深入 Agent 循环的核心实现,理解 API 调用、流式处理、工具分发、错误恢复和上下文压缩触发的完整逻辑。