Claude Code 源码分析Claude Code 源码分析
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
  • 概览

    • Claude Code 源码分析
    • 源码统计
    • CodeGraph 图谱
  • 架构

    • 系统架构
    • UML 图表索引
    • 查询引擎
    • 核心流程
    • 消息系统
    • 状态管理
  • 功能模块

    • 工具系统
    • 斜杠命令
    • 服务层
    • MCP 协议
    • Skills 技能
    • 子代理系统
  • 分层深度

    • 入口层
    • UI / Ink 层
    • utils 基础设施
    • 桥接 / 远程
    • 上下文压缩
  • 原理与安全

    • 底层原理
    • 技术难点
    • 权限与安全
    • 内部机制
    • 遥测与分析
  • 深度专题

    • Hooks 系统
    • 插件系统
    • 记忆系统
    • API 通信层
    • Ink 终端 UI
    • 认证系统
    • 构建与发布
    • 术语表
  • 调用分析

    • 调用链分析
    • 核心文件索引
  • 模块详解

    • utils

      • 模块: utils
      • messages · 消息工厂与规范化
      • session-storage · JSONL 会话持久化
      • permissions · 工具权限决策
      • shell-hooks · 用户 Shell Hook 系统
    • components

      • 模块: components
      • REPL · 主屏编排
      • messages · 消息行渲染
      • PermissionRequest · 权限弹窗
      • PromptInput · 底部输入
    • services

      • 模块: services
      • api-claude · Anthropic API 流式与重试
      • mcp-client · MCP 连接与工具调用
      • compact · 上下文压缩与自动触发
      • analytics · GrowthBook、Datadog 与 1P 事件
    • tools

      • 模块: tools
      • tool-interface · Tool 契约与注册表
      • bash-tool · Shell 执行与权限
      • streaming-executor · 流式工具并发调度
      • agent-tool · 子 Agent 委派
    • commands

      • 模块: commands
      • command-registry · commands.ts 注册与分派
      • model-command · /model 模型选择
      • mcp-commands · /mcp 服务器管理
      • compact-memory-commands · /compact 与 /memory
    • ink

      • 模块: ink
      • Ink 渲染管线 · Screen 与终端输出
      • 终端事件 · resize、paste、stdin
      • Ink Hooks · 输入、搜索、终端状态
      • Ink 组件 · Box、Text、ScrollBox 原语
    • hooks

      • 模块: hooks
      • useCanUseTool · 权限 UI 接缝
      • 输入与快捷键 Hook
      • 合并态 Hook(MCP + 本地)
      • notifs 通知 Hook
    • bridge

      • 模块: bridge
      • repl-bridge · REPL 桥初始化与传输
      • bridge-messaging · 桥消息路由与入站处理
      • remote-bridge-core · env-less 核心与守护主循环
      • bridge-permissions-ui · 权限、API 与 TUI
    • cli

      • 模块: cli
      • Structured IO · NDJSON SDK 协议
      • CLI Transports · Session Ingress 传输层
      • CLI Handlers · 子命令懒加载实现
      • Update & Upload · 自更新与串行上传原语
    • screens

      • 模块: screens
      • REPL 屏 · Screen 类型与顶层路由
      • ResumeConversation · 会话恢复选择器
      • Doctor · 安装诊断全屏
    • entrypoints

      • 模块: entrypoints
      • cli-entrypoint · Bootstrap 与快路径
      • sdk-types · core / control / runtime 类型体系
      • mcp-entrypoint · MCP stdio 服务器
      • sandbox-types · 沙箱配置单一真相源
    • skills

      • 模块: skills
      • skills-loading · 磁盘加载与 bundled 注册表
      • bundled-skills · 内置 skill 与 initBundledSkills
      • mcp-skills · MCP prompt 转 skill
      • skill-tool-integration · SkillTool 与命令注册
    • types

      • 模块: types
      • message-types · Message 联合与 content blocks
      • tool-permission-types · Tool、Permission、Command 类型
      • api-sdk-types · API 与 Hooks 协议类型
      • misc-types · ids、plugin、generated 与其余类型
    • tasks

      • 模块: tasks
      • local-agent-task · 本地 Agent 与主会话后台化
      • remote-agent-task · 远程 CCR 与 In-Process Teammate
      • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
      • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybindings

      • 模块: keybindings
      • keybinding-registry · 注册、Provider 与 useKeybinding
      • default-bindings · 默认键位表与平台差异
      • command-bindings · command:* 动态斜杠命令绑定
      • vim-bindings · Vim 模式与 keybindings 边界
    • memdir

      • 模块: memdir
      • memdir-core · 路径、加载与 MEMORY.md
      • memory-extraction · extractMemories 与 SessionMemory
      • memdir-commands · /memory、/remember 与命令集成
    • state

      • 模块: state
      • app-state-core · store、AppState 类型与 Provider
      • app-state-selectors · selectors 与 onChangeAppState
      • teammate-state · 队友视图与 swarm 状态
      • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query

      • 模块: query
      • query config 与 deps · 配置快照与依赖注入
      • query tokenBudget · +500k 自动续跑
      • query transitions · Continue / Terminal 状态机
      • query stopHooks · Stop 事件与 turn 结束编排
  • 模块详解(扩展)

    • messages · 消息工厂与规范化
    • session-storage · JSONL 会话持久化
    • permissions · 工具权限决策
    • shell-hooks · 用户 Shell Hook 系统
    • REPL · 主屏编排
    • messages · 消息行渲染
    • PermissionRequest · 权限弹窗
    • PromptInput · 底部输入
    • api-claude · Anthropic API 流式与重试
    • mcp-client · MCP 连接与工具调用
    • compact · 上下文压缩与自动触发
    • analytics · GrowthBook、Datadog 与 1P 事件
    • tool-interface · Tool 契约与注册表
    • bash-tool · Shell 执行与权限
    • streaming-executor · 流式工具并发调度
    • agent-tool · 子 Agent 委派
    • command-registry · commands.ts 注册与分派
    • model-command · /model 模型选择
    • mcp-commands · /mcp 服务器管理
    • compact-memory-commands · /compact 与 /memory
    • Ink 渲染管线 · Screen 与终端输出
    • 终端事件 · resize、paste、stdin
    • Ink Hooks · 输入、搜索、终端状态
    • Ink 组件 · Box、Text、ScrollBox 原语
    • useCanUseTool · 权限 UI 接缝
    • 输入与快捷键 Hook
    • 合并态 Hook(MCP + 本地)
    • notifs 通知 Hook
    • repl-bridge · REPL 桥初始化与传输
    • bridge-messaging · 桥消息路由与入站处理
    • remote-bridge-core · env-less 核心与守护主循环
    • bridge-permissions-ui · 权限、API 与 TUI
    • Structured IO · NDJSON SDK 协议
    • CLI Transports · Session Ingress 传输层
    • CLI Handlers · 子命令懒加载实现
    • Update & Upload · 自更新与串行上传原语
    • REPL 屏 · Screen 类型与顶层路由
    • ResumeConversation · 会话恢复选择器
    • Doctor · 安装诊断全屏
    • cli-entrypoint · Bootstrap 与快路径
    • sdk-types · core / control / runtime 类型体系
    • mcp-entrypoint · MCP stdio 服务器
    • sandbox-types · 沙箱配置单一真相源
    • skills-loading · 磁盘加载与 bundled 注册表
    • bundled-skills · 内置 skill 与 initBundledSkills
    • mcp-skills · MCP prompt 转 skill
    • skill-tool-integration · SkillTool 与命令注册
    • message-types · Message 联合与 content blocks
    • tool-permission-types · Tool、Permission、Command 类型
    • api-sdk-types · API 与 Hooks 协议类型
    • misc-types · ids、plugin、generated 与其余类型
    • local-agent-task · 本地 Agent 与主会话后台化
    • remote-agent-task · 远程 CCR 与 In-Process Teammate
    • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
    • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybinding-registry · 注册、Provider 与 useKeybinding
    • default-bindings · 默认键位表与平台差异
    • command-bindings · command:* 动态斜杠命令绑定
    • vim-bindings · Vim 模式与 keybindings 边界
    • memdir-core · 路径、加载与 MEMORY.md
    • memory-extraction · extractMemories 与 SessionMemory
    • memdir-commands · /memory、/remember 与命令集成
    • app-state-core · store、AppState 类型与 Provider
    • app-state-selectors · selectors 与 onChangeAppState
    • teammate-state · 队友视图与 swarm 状态
    • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query config 与 deps · 配置快照与依赖注入
    • query tokenBudget · +500k 自动续跑
    • query transitions · Continue / Terminal 状态机
    • query stopHooks · Stop 事件与 turn 结束编排
  • 工具详解

    • tool-interface · Tool 契约与注册表
    • tool-permission-types · Tool、Permission、Command 类型
    • 工具: Bash
    • 工具: PowerShell
    • 工具: Agent
    • 工具: LSP
    • 工具: FileEdit
    • 工具: FileRead
    • 工具: Skill
    • 工具: WebFetch
    • 工具: MCP
    • 工具: SendMessage
    • 工具: FileWrite
    • 工具: Config
    • 工具: Grep
    • 工具: Brief
    • 工具: ExitPlanMode
    • 工具: ToolSearch
    • 工具: NotebookEdit
    • 工具: TaskOutput
    • 工具: WebSearch
    • 工具: ScheduleCron

本章总览

REPL.tsx(v2.1.88 反编译,约 5000 行)是 Claude Code 交互式会话的「导演」:从第 572 行起的 export function REPL 汇聚 query 循环、权限队列、消息列表、底部输入与全屏布局。本章带源码走读,要求你能从用户按 Enter 追到 canUseTool、Messages 与 PermissionRequest overlay。

学完本章你应该能

  • 说明 REPL Props 中 remote/ssh/taskList 等模式开关的含义
  • 解释 toolUseConfirmQueue 与 focusedInputDialog 的焦点模型
  • 描述 prompt 屏与 transcript 屏(Screen 类型)的切换
  • 定位 canUseTool 注册点与 PermissionRequest 渲染条件
  • 理解 displayedMessages 在 agent 视图与 deferred 路径下的差异

核心概念(先读懂这些)

REPL 是状态机而非纯布局

REPL 维护大量 useState:screen、toolUseConfirmQueue、promptQueue、sandbox 队列、loading、cursor 等。sessionStatus 三元组 idle/busy/waiting 驱动终端标题动画与 macOS 防休眠。waiting 时 tab 状态会写入 PID 文件供 claude ps 使用。这不是简单的「聊天窗口」,而是把引擎异步事件映射成可消费的 UI 状态。

FullscreenLayout 的 slot 模型

全屏模式下 scrollable 区域放 Messages + spinner,bottom 放 PromptInput,overlay 放 PermissionRequest,modal 放 /config 等 local-jsx 命令。permissionStickyFooter 由 ExitPlanMode 等权限组件注册,保证长计划滚动时选项仍可见。理解 slot 后,新增全局浮层时知道应挂 overlay 还是 modal。

建议学习步骤

  1. 阅读 REPL 函数签名与 Props(源码块 A)
  2. 跟踪 toolUseConfirmQueue 声明与 isWaitingForApproval(源码块 B)
  3. 阅读 canUseTool = useCanUseTool(...) 注册(源码块 C)
  4. 对照 toolPermissionOverlay 与 Messages 传参(源码块 D、E)
  5. 在源码树展开 screens/REPL.tsx 核对 import 清单

常见误区

注意

React Compiler 的 _c / $ 缓存语法可忽略,聚焦业务状态名

注意

viewedAgentTask 时 displayedMessages 来自 task,勿与 leader messages 混淆

注意

deferredMessages 仅在非 streaming 且 loading 时启用,影响占位符显示时机

在架构中的位置

Claude Code 启动后,main 路由最终渲染 <REPL />。数据流概览:

用户输入 (PromptInput.onSubmit)
  → processUserInput / query 循环
  → API 流式返回 assistant blocks
  → setMessages 更新 transcript
  → Messages → MessageRow → messages/* 叶子组件

并行:tool_use 前 await canUseTool()
  → ask 时 push ToolUseConfirm 到 toolUseConfirmQueue
  → focusedInputDialog === 'tool-permission'
  → overlay 渲染 PermissionRequest

REPL 同时 import 了 VirtualMessageList、PermissionRequest、PromptInput、数十个 hooks。改「会话行为」优先查 REPL;改「单条消息样式」查 messages/;改「Allow 按钮文案」查 permissions/。

REPL 入口与 Props

下列源码为 export function REPL 起始段(约 572 行)。注意 thinkingConfig 为必填项,disabled 会隐藏输入并禁用 message selector,taskListId 启用任务列表自动处理模式。

阅读要点:

  1. remoteSessionConfig / directConnectConfig / sshSession 三套远程形态互斥使用场景不同
  2. mount 时 useEffect 打调试日志,便于排查 REPL 意外卸载
  3. useAppState 批量订阅 MCP、plugins、tasks、elicitation 等全局切片

源码引用: src/screens/REPL.tsx · 第 571–640 行(共 7050 行)

 571|   MessageActionsKeybindings,
 572|   MessageActionsBar,
 573|   type MessageActionsState,
 574|   type MessageActionsNav,
 575|   type MessageActionCaps,
 576| } from '../components/messageActions.js'
 577| import { setClipboard } from '../ink/termio/osc.js'
 578| import type { ScrollBoxHandle } from '../ink/components/ScrollBox.js'
 579| import {
 580|   createAttachmentMessage,
 581|   getQueuedCommandAttachments,
 582| } from '../utils/attachments.js'
 583| 
 584| // Stable empty array for hooks that accept MCPServerConnection[] — avoids
 585| // creating a new [] literal on every render in remote mode, which would
 586| // cause useEffect dependency changes and infinite re-render loops.
 587| const EMPTY_MCP_CLIENTS: MCPServerConnection[] = []
 588| 
 589| // Stable stub for useAssistantHistory's non-KAIROS branch — avoids a new
 590| // function identity each render, which would break composedOnScroll's memo.
 591| const HISTORY_STUB = { maybeLoadOlder: (_: ScrollBoxHandle) => {} }
 592| // Window after a user-initiated scroll during which type-into-empty does NOT
 593| // repin to bottom. Josh Rosen's workflow: Claude emits long output → scroll
 594| // up to read the start → start typing → before this fix, snapped to bottom.
 595| // https://anthropic.slack.com/archives/C07VBSHV7EV/p1773545449871739
 596| const RECENT_SCROLL_REPIN_WINDOW_MS = 3000
 597| 
 598| // Use LRU cache to prevent unbounded memory growth
 599| // 100 files should be sufficient for most coding sessions while preventing
 600| // memory issues when working across many files in large projects
 601| 
 602| function median(values: number[]): number {
 603|   const sorted = [...values].sort((a, b) => a - b)
 604|   const mid = Math.floor(sorted.length / 2)
 605|   return sorted.length % 2 === 0
 606|     ? Math.round((sorted[mid - 1]! + sorted[mid]!) / 2)
 607|     : sorted[mid]!
 608| }
 609| 
 610| /**
 611|  * Small component to display transcript mode footer with dynamic keybinding.
 612|  * Must be rendered inside KeybindingSetup to access keybinding context.
 613|  */
 614| function TranscriptModeFooter({
 615|   showAllInTranscript,
 616|   virtualScroll,
 617|   searchBadge,
 618|   suppressShowAll = false,
 619|   status,
 620| }: {
 621|   showAllInTranscript: boolean
 622|   virtualScroll: boolean
 623|   /** Minimap while navigating a closed-bar search. Shows n/N hints +
 624|    *  right-aligned count instead of scroll hints. */
 625|   searchBadge?: { current: number; count: number }
 626|   /** Hide the ctrl+e hint. The [ dump path shares this footer with
 627|    *  env-opted dump (CLAUDE_CODE_NO_FLICKER=0 / DISABLE_VIRTUAL_SCROLL=1),
 628|    *  but ctrl+e only works in the env case — useGlobalKeybindings.tsx
 629|    *  gates on !virtualScrollActive which is env-derived, doesn't know
 630|    *  [ happened. */
 631|   suppressShowAll?: boolean
 632|   /** Transient status (v-for-editor progress). Notifications render inside
 633|    *  PromptInput which isn't mounted in transcript — addNotification queues
 634|    *  but nothing draws it. */
 635|   status?: string
 636| }): React.ReactNode {
 637|   const toggleShortcut = useShortcutDisplay(
 638|     'app:toggleTranscript',
 639|     'Global',
 640|     'ctrl+o',

Screen 模式与 AppState 订阅

REPL 用 useState<Screen>('prompt' | 'transcript') 切换主视图。transcript 模式用于全文导出、搜索跳转(配合 VirtualMessageList 的 jumpRef)。同一段还展示 localCommands 热重载:skill 文件变更时 useSkillsChange 触发 setLocalCommands。

localTools = useMemo(() => getTools(toolPermissionContext), ...) 说明工具列表随权限上下文变化——与 PromptInput 展示的 permission mode 指示器联动。

源码引用: src/screens/REPL.tsx · 第 680–710 行(共 7050 行)

 680|         // Engine-counted — close enough for a rough location hint. May
 681|         // drift from render-count for ghost/phantom messages.
 682|         <>
 683|           <Box flexGrow={1} />
 684|           <Text dimColor>
 685|             {searchBadge.current}/{searchBadge.count}
 686|             {'  '}
 687|           </Text>
 688|         </>
 689|       ) : null}
 690|     </Box>
 691|   )
 692| }
 693| 
 694| /** less-style / bar. 1-row, same border-top styling as TranscriptModeFooter
 695|  *  so swapping them in the bottom slot doesn't shift ScrollBox height.
 696|  *  useSearchInput handles readline editing; we report query changes and
 697|  *  render the counter. Incremental — re-search + highlight per keystroke. */
 698| function TranscriptSearchBar({
 699|   jumpRef,
 700|   count,
 701|   current,
 702|   onClose,
 703|   onCancel,
 704|   setHighlight,
 705|   initialQuery,
 706| }: {
 707|   jumpRef: RefObject<JumpHandle | null>
 708|   count: number
 709|   current: number
 710|   /** Enter — commit. Query persists for n/N. */

源码引用: src/screens/REPL.tsx · 第 616–625 行(共 7050 行)

 616|   virtualScroll,
 617|   searchBadge,
 618|   suppressShowAll = false,
 619|   status,
 620| }: {
 621|   showAllInTranscript: boolean
 622|   virtualScroll: boolean
 623|   /** Minimap while navigating a closed-bar search. Shows n/N hints +
 624|    *  right-aligned count instead of scroll hints. */
 625|   searchBadge?: { current: number; count: number }

toolUseConfirmQueue 与等待态

权限确认队列是 REPL 最关键的 UI 状态之一:

  • toolUseConfirmQueue: ToolUseConfirm[] FIFO 处理多个待确认 tool_use
  • isWaitingForApproval 合并 promptQueue、worker、sandbox 等等待源
  • sessionStatus 变为 waiting 时,终端标题动画停止,并可能触发防休眠逻辑取消
  • waitingFor 字符串写入 session activity,供外部 ps 子命令展示

permissionStickyFooter 与队列并列:子权限组件通过 setStickyFooter 注册底部固定 JSX(长计划场景)。

源码引用: src/screens/REPL.tsx · 第 1101–1156 行(共 7050 行)

1101|   )
1102|   const editorRenderingRef = useRef(false)
1103|   const { addNotification, removeNotification } = useNotifications()
1104| 
1105|   // eslint-disable-next-line prefer-const
1106|   let trySuggestBgPRIntercept = SUGGEST_BG_PR_NOOP
1107| 
1108|   const mcpClients = useMergedClients(initialMcpClients, mcp.clients)
1109| 
1110|   // IDE integration
1111|   const [ideSelection, setIDESelection] = useState<IDESelection | undefined>(
1112|     undefined,
1113|   )
1114|   const [ideToInstallExtension, setIDEToInstallExtension] =
1115|     useState<IdeType | null>(null)
1116|   const [ideInstallationStatus, setIDEInstallationStatus] =
1117|     useState<IDEExtensionInstallationStatus | null>(null)
1118|   const [showIdeOnboarding, setShowIdeOnboarding] = useState(false)
1119|   // Dead code elimination: model switch callout state (ant-only)
1120|   const [showModelSwitchCallout, setShowModelSwitchCallout] = useState(() => {
1121|     if ("external" === 'ant') {
1122|       return shouldShowAntModelSwitch()
1123|     }
1124|     return false
1125|   })
1126|   const [showEffortCallout, setShowEffortCallout] = useState(() =>
1127|     shouldShowEffortCallout(mainLoopModel),
1128|   )
1129|   const showRemoteCallout = useAppState(s => s.showRemoteCallout)
1130|   const [showDesktopUpsellStartup, setShowDesktopUpsellStartup] = useState(() =>
1131|     shouldShowDesktopUpsellStartup(),
1132|   )
1133|   // notifications
1134|   useModelMigrationNotifications()
1135|   useCanSwitchToExistingSubscription()
1136|   useIDEStatusIndicator({ ideSelection, mcpClients, ideInstallationStatus })
1137|   useMcpConnectivityStatus({ mcpClients })
1138|   useAutoModeUnavailableNotification()
1139|   usePluginInstallationStatus()
1140|   usePluginAutoupdateNotification()
1141|   useSettingsErrors()
1142|   useRateLimitWarningNotification(mainLoopModel)
1143|   useFastModeNotification()
1144|   useDeprecationWarningNotification(mainLoopModel)
1145|   useNpmDeprecationNotification()
1146|   useAntOrgWarningNotification()
1147|   useInstallMessages()
1148|   useChromeExtensionNotification()
1149|   useOfficialMarketplaceNotification()
1150|   useLspInitializationNotification()
1151|   useTeammateLifecycleNotification()
1152|   const {
1153|     recommendation: lspRecommendation,
1154|     handleResponse: handleLspResponse,
1155|   } = useLspPluginRecommendation()
1156|   const {

源码引用: src/screens/REPL.tsx · 第 1105–1116 行(共 7050 行)

1105|   // eslint-disable-next-line prefer-const
1106|   let trySuggestBgPRIntercept = SUGGEST_BG_PR_NOOP
1107| 
1108|   const mcpClients = useMergedClients(initialMcpClients, mcp.clients)
1109| 
1110|   // IDE integration
1111|   const [ideSelection, setIDESelection] = useState<IDESelection | undefined>(
1112|     undefined,
1113|   )
1114|   const [ideToInstallExtension, setIDEToInstallExtension] =
1115|     useState<IdeType | null>(null)
1116|   const [ideInstallationStatus, setIDEInstallationStatus] =

canUseTool 注册与 ToolUseContext

const canUseTool = useCanUseTool(setToolUseConfirmQueue, setToolPermissionContext) 把 Hook 返回值注入 query 引擎。

相邻的 getToolUseContext 回调从 store 读取最新 MCP/tools,避免闭包捕获陈旧工具列表——注释明确提到未来 headless 循环也需要此模式。

registerLeaderSetToolPermissionContext 表明 swarm leader 会把 setState 暴露给 in-process teammate,权限 UI 可能出现在 worker 徽章路径。

源码引用: src/screens/REPL.tsx · 第 2377–2420 行(共 7050 行)

2377|             /* eslint-enable @typescript-eslint/no-require-imports */
2378|             getAgentDefinitionsWithOverrides.cache.clear?.()
2379|             const freshAgentDefs = await getAgentDefinitionsWithOverrides(
2380|               getOriginalCwd(),
2381|             )
2382| 
2383|             setAppState(prev => ({
2384|               ...prev,
2385|               agentDefinitions: {
2386|                 ...freshAgentDefs,
2387|                 allAgents: freshAgentDefs.allAgents,
2388|                 activeAgents: getActiveAgentsFromList(freshAgentDefs.allAgents),
2389|               },
2390|             }))
2391|             messages.push(createSystemMessage(warning, 'warning'))
2392|           }
2393|         }
2394| 
2395|         // Fire SessionEnd hooks for the current session before starting the
2396|         // resumed one, mirroring the /clear flow in conversation.ts.
2397|         const sessionEndTimeoutMs = getSessionEndHookTimeoutMs()
2398|         await executeSessionEndHooks('resume', {
2399|           getAppState: () => store.getState(),
2400|           setAppState,
2401|           signal: AbortSignal.timeout(sessionEndTimeoutMs),
2402|           timeoutMs: sessionEndTimeoutMs,
2403|         })
2404| 
2405|         // Process session start hooks for resume
2406|         const hookMessages = await processSessionStartHooks('resume', {
2407|           sessionId,
2408|           agentType: mainThreadAgentDefinition?.agentType,
2409|           model: mainLoopModel,
2410|         })
2411| 
2412|         // Append hook messages to the conversation
2413|         messages.push(...hookMessages)
2414|         // For forks, generate a new plan slug and copy the plan content so the
2415|         // original and forked sessions don't clobber each other's plan files.
2416|         // For regular resumes, reuse the original session's plan slug.
2417|         if (entrypoint === 'fork') {
2418|           void copyPlanForFork(log, asSessionId(sessionId))
2419|         } else {
2420|           void copyPlanForResume(log, asSessionId(sessionId))

displayedMessages 与占位符

渲染前列举三路消息来源:

  1. viewedAgentTask:只看 agent task 的 messages,绝不 fallthrough 到 leader(防 footgun)
  2. usesSyncMessages:流式文本或直接 sync,跳过 deferred
  3. deferredMessages:loading 时降低重绘频率

placeholderText 在 userInputOnProcessing 且消息数未超过 baseline 时显示 UserTextMessage 占位,让用户看到自己刚提交的内容;modal 打开时抑制,避免与 /config 等对话框重复。

源码引用: src/screens/REPL.tsx · 第 4505–4520 行(共 7050 行)

4505|       // returns early without calling handlePromptSubmit).
4506|       const submitsNow =
4507|         !isLoading || speculationAccept || activeRemote.isRemoteMode
4508|       if (stashedPrompt !== undefined && !isSlashCommand && submitsNow) {
4509|         setInputValue(stashedPrompt.text)
4510|         helpers.setCursorOffset(stashedPrompt.cursorOffset)
4511|         setPastedContents(stashedPrompt.pastedContents)
4512|         setStashedPrompt(undefined)
4513|       } else if (submitsNow) {
4514|         if (!options?.fromKeybinding) {
4515|           // Clear input when not loading or accepting speculation.
4516|           // Preserve input for keybinding-triggered commands.
4517|           setInputValue('')
4518|           helpers.setCursorOffset(0)
4519|         }
4520|         setPastedContents({})

主布局 return:Messages 与 Permission overlay

mainReturn JSX 结构值得整张记忆:

  • ScrollKeybindingHandler 在 CancelRequestHandler 之前挂载,保证有选区时 Ctrl+C 复制而非取消
  • scrollable 内 &lt;Messages ... toolUseConfirmQueue={toolUseConfirmQueue} /&gt; 把队列传给列表(用于行内状态,如 in-progress 点)
  • toolPermissionOverlay 仅在 focusedInputDialog === 'tool-permission' 时挂载 PermissionRequest
  • bottom 槽:permissionStickyFooter + PromptInput + 各类 dialog

全屏时 FullscreenLayout overlay={toolPermissionOverlay} 使权限框浮在 transcript 之上,PgUp/PgDn 仍可滚动背后内容(modal 时 onScroll 被抑制)。

源码引用: src/screens/REPL.tsx · 第 4548–4590 行(共 7050 行)

4548|         // The snapshot persists promptCount so it survives compaction
4549|         if (feature('COMMIT_ATTRIBUTION')) {
4550|           setAppState(prev => ({
4551|             ...prev,
4552|             attribution: incrementPromptCount(prev.attribution, snapshot => {
4553|               void recordAttributionSnapshot(snapshot).catch(error => {
4554|                 logForDebugging(
4555|                   `Attribution: Failed to save snapshot: ${error}`,
4556|                 )
4557|               })
4558|             }),
4559|           }))
4560|         }
4561|       }
4562| 
4563|       // Handle speculation acceptance
4564|       if (speculationAccept) {
4565|         const { queryRequired } = await handleSpeculationAccept(
4566|           speculationAccept.state,
4567|           speculationAccept.speculationSessionTimeSavedMs,
4568|           speculationAccept.setAppState,
4569|           input,
4570|           {
4571|             setMessages,
4572|             readFileState,
4573|             cwd: getOriginalCwd(),
4574|           },
4575|         )
4576|         if (queryRequired) {
4577|           const newAbortController = createAbortController()
4578|           setAbortController(newAbortController)
4579|           void onQuery([], newAbortController, true, [], mainLoopModel)
4580|         }
4581|         return
4582|       }
4583| 
4584|       // Remote mode: send input via stream-json instead of local query.
4585|       // Permission requests from the remote are bridged into toolUseConfirmQueue
4586|       // and rendered using the standard PermissionRequest component.
4587|       //
4588|       // local-jsx slash commands (e.g. /agents, /config) render UI in THIS
4589|       // process — they have no remote equivalent. Let those fall through to
4590|       // handlePromptSubmit so they execute locally. Prompt commands and

源码引用: src/screens/REPL.tsx · 第 4568–4571 行(共 7050 行)

4568|           speculationAccept.setAppState,
4569|           input,
4570|           {
4571|             setMessages,

REPL 顶部 import 接缝图

文件前 120 行 import 密度极高,可按层归类:

层代表 import
Ink / 终端ink.js, useTerminalSize, VirtualMessageList
组件PermissionRequest, PromptInput, MessageSelector, Spinner
HooksuseCanUseTool, useLogMessages, useReplBridge, useGlobalKeybindings
引擎query 相关通过 getToolUseContext 间接使用

读 REPL 时宜跳过 feature() 条件编译的 require 块,先抓「始终存在」的 PermissionRequest + PromptInput + useCanUseTool 三角关系。

源码引用: src/screens/REPL.tsx · 第 54–90 行(共 7050 行)

  54| import { useTerminalNotification } from '../ink/useTerminalNotification.js'
  55| import { hasCursorUpViewportYankBug } from '../ink/terminal.js'
  56| import {
  57|   createFileStateCacheWithSizeLimit,
  58|   mergeFileStateCaches,
  59|   READ_FILE_STATE_CACHE_SIZE,
  60| } from '../utils/fileStateCache.js'
  61| import {
  62|   updateLastInteractionTime,
  63|   getLastInteractionTime,
  64|   getOriginalCwd,
  65|   getProjectRoot,
  66|   getSessionId,
  67|   switchSession,
  68|   setCostStateForRestore,
  69|   getTurnHookDurationMs,
  70|   getTurnHookCount,
  71|   resetTurnHookDuration,
  72|   getTurnToolDurationMs,
  73|   getTurnToolCount,
  74|   resetTurnToolDuration,
  75|   getTurnClassifierDurationMs,
  76|   getTurnClassifierCount,
  77|   resetTurnClassifierDuration,
  78| } from '../bootstrap/state.js'
  79| import { asSessionId, asAgentId } from '../types/ids.js'
  80| import { logForDebugging } from '../utils/debug.js'
  81| import { QueryGuard } from '../utils/QueryGuard.js'
  82| import { isEnvTruthy } from '../utils/envUtils.js'
  83| import { formatTokens, truncateToWidth } from '../utils/format.js'
  84| import { consumeEarlyInput } from '../utils/earlyInput.js'
  85| 
  86| import { setMemberActive } from '../utils/swarm/teamHelpers.js'
  87| import {
  88|   isSwarmWorker,
  89|   generateSandboxRequestId,
  90|   sendSandboxPermissionRequestViaMailbox,

源码引用: src/screens/REPL.tsx · 第 16–17 行(共 7050 行)

  16| // eslint-disable-next-line custom-rules/prefer-use-keybindings -- / n N Esc [ v are bare letters in transcript modal context, same class as g/G/j/k in ScrollKeybindingHandler
  17| import { useInput } from '../ink.js'

源码目录

REPL 单文件过大,建议结合子章节 message-components / permissions-ui 分块阅读。

Spinner、dialog 与 hasActivePrompt

REPL 用 showSpinner 与 hasActivePrompt 协调「模型仍在思考」与「用户必须先点击 Allow」两种等待:

  • 当 toolUseConfirmQueue.length &gt; 0 时,通常 不显示 主 spinner(避免用户以为系统卡住,实际在等权限)
  • promptQueue 承载 Hook 系统的 PromptDialog 请求(与 permissions 不同,来自 utils/hooks.ts)
  • elicitation.queue、workerSandboxPermissions 与 sandboxPermissionRequestQueue 各自映射 focusedInputDialog 枚举值

hasSuppressedDialogs 传给 PromptInput,在任一队列非空时抑制底部快捷键,防止与 Select 组件抢键。理解这张表后,调试「弹窗出现但键盘无响应」时先查 focusedInputDialog 当前值,再查对应队列 head 元素是否为空。

源码引用: src/screens/REPL.tsx · 第 1672–1690 行(共 7050 行)

1672|         typeof action === 'function' ? action(messagesRef.current) : action
1673|       messagesRef.current = next
1674|       if (next.length < userInputBaselineRef.current) {
1675|         // Shrank (compact/rewind/clear) — clamp so placeholderText's length
1676|         // check can't go stale.
1677|         userInputBaselineRef.current = 0
1678|       } else if (next.length > prev.length && userMessagePendingRef.current) {
1679|         // Grew while the submitted user message hasn't landed yet. If the
1680|         // added messages don't include it (bridge status, hook results,
1681|         // scheduled tasks landing async during processUserInputBase), bump
1682|         // baseline so the placeholder stays visible. Once the user message
1683|         // lands, stop tracking — later additions (assistant stream) should
1684|         // not re-show the placeholder.
1685|         const delta = next.length - prev.length
1686|         const added =
1687|           prev.length === 0 || next[0] === prev[0]
1688|             ? next.slice(-delta)
1689|             : next.slice(0, delta)
1690|         if (added.some(isHumanTurn)) {

源码引用: src/screens/REPL.tsx · 第 2026–2032 行(共 7050 行)

2026|   lastQueryCompletionTimeRef.current = lastQueryCompletionTime
2027| 
2028|   // Aggregate tool result budget: per-conversation decision tracking.
2029|   // When the GrowthBook flag is on, query.ts enforces the budget; when
2030|   // off (undefined), enforcement is skipped entirely. Stale entries after
2031|   // /clear, rewind, or compact are harmless (tool_use_ids are UUIDs, stale
2032|   // keys are never looked up). Memory is bounded by total replacement count

动手练习

  1. 触发一次需确认的 Bash,观察 tab waitingFor 是否变为 approve Bash
  2. 按 Esc 或 interrupt 绑定,对照 PermissionRequest 的 onDone/onReject 链
  3. 切换 transcript 模式(若环境支持),确认 PromptInput 未挂载时 editorStatus 显示位置
  4. 在源码树点击 REPL.tsx 4519 行,跳转到 toolPermissionOverlay 源码块
  5. 对比 loading 与 waiting 两种状态下终端标题动画与防休眠是否按预期切换

本章小结与延伸

REPL = 会话编排中枢。权限弹窗见 permissions-ui 章;消息行见 message-components;输入见 prompt-input。 继续学习:

  • components 总览
  • 消息组件
  • 权限 UI
Prev
模块: components
Next
messages · 消息行渲染