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

本章总览

PromptInput.tsx(约 2300 行)实现 REPL 底部输入区:多行文本、Vim 模式、斜杠命令补全、权限模式切换、图片粘贴、语音插入、footer 状态与提交到 onSubmit。本章要求你能从按键事件追到 processUserInput,并理解与 overlay / hasSuppressedDialogs 的焦点互斥。

学完本章你应该能

  • 列举 PromptInput Props 中与 REPL 对接的关键回调
  • 说明 inputModes(!/#)与 permission mode 循环的关系
  • 描述 insertTextRef 与语音/STT 的协作
  • 理解 hasSuppressedDialogs 与 isLocalJSXCommandActive 的模态互斥
  • 定位 PromptInputFooter、Notifications 的职责划分

核心概念(先读懂这些)

PromptInput 是「控制器」而非纯 TextInput

组件组合了 useInputBuffer、useTypeahead、useArrowKeyHistory、usePromptSuggestion、keybindings 等,自身维护 cursorOffset、helpOpen、exitMessage、isAutoUpdating。提交路径最终调用 REPL 传入的 onSubmit,并附带 PromptInputHelpers(含 getToolUseContext 等)。底部 50% maxHeight 限制来自 PROMPT_FOOTER_LINES 与 MIN_INPUT_VIEWPORT_LINES 常量。

模态叠加与导航键泄漏

local-jsx 命令(如 /mcp)可能 shouldHidePromptInput: false 但仍全屏遮罩。PromptInput 用 isModalOverlayActive || isLocalJSXCommandActive 阻止方向键穿透到 TextInput。hasSuppressedDialogs 在 REPL 侧表示权限/队列对话框占用焦点,footer 快捷键需让路。

建议学习步骤

  1. 阅读 Props 类型定义(源码块 A)
  2. 阅读 PromptInput 函数入口与模态检测(源码块 B)
  3. 查看 insertTextRef 与 trackAndSetInput(源码块 C)
  4. 打开 inputModes.ts 的 mode 解析(源码块 D)
  5. 对照 PromptInputFooter 与 Notifications(源码块 E、F)

常见误区

注意

外部修改 input(语音注入)会强制 cursor 移到末尾,勿与 Vim 光标逻辑冲突

注意

showBashesDialog 为 true 时会 early-return BackgroundTasksDialog,影响 companion 布局

注意

permission mode 切换写回 setToolPermissionContext,与 useCanUseTool 共用上下文

PromptInput 在布局中的位置

全屏 REPL 的 FullscreenLayout bottom slot 结构:

bottom
  ├─ permissionStickyFooter (可选)
  ├─ PromptInput
  │    ├─ PromptInputModeIndicator
  │    ├─ ShimmeredInput / TextInput / VimTextInput
  │    ├─ PromptInputFooter (模型、权限、任务 pill)
  │    └─ Notifications (临时状态行)
  ├─ immediate local-jsx commands
  └─ CompanionSprite (宽屏右侧)

transcript 模式(screen === 'transcript')下 PromptInput 不挂载,editorStatus 改由 REPL footer 显示。QueuedCommands 在 scrollable 区底部(PromptInputQueuedCommands)展示排队斜杠命令。

Props:与 REPL 的契约

下列 Props 块定义 REPL ↔ PromptInput 接口(节选 124–189 行):

Prop作用
toolPermissionContext / setToolPermissionContext权限模式、auto mode
input / onInputChange / mode / onModeChange受控输入与 !/# 模式
getToolUseContext构造 processUserInput 上下文
onSubmit / onAgentSubmit主线程 vs agent 提交
pastedContents / setPastedContents图片与长文本引用
hasSuppressedDialogs权限等对话框占用
insertTextRef外部插入(语音)
voiceInterimRange实时语音高亮区间

缺失 onAgentSubmit 时 agent 视图仍可能通过 teammate 管道注入消息,但底部输入行为不同。

源码引用: src/components/PromptInput/PromptInput.tsx · 第 124–189 行(共 3178 行)

 124| import {
 125|   parseDirectMemberMessage,
 126|   sendDirectMemberMessage,
 127| } from '../../utils/directMemberMessage.js'
 128| import type { EffortLevel } from '../../utils/effort.js'
 129| import { env } from '../../utils/env.js'
 130| import { errorMessage } from '../../utils/errors.js'
 131| import { isBilledAsExtraUsage } from '../../utils/extraUsage.js'
 132| import {
 133|   getFastModeUnavailableReason,
 134|   isFastModeAvailable,
 135|   isFastModeCooldown,
 136|   isFastModeEnabled,
 137|   isFastModeSupportedByModel,
 138| } from '../../utils/fastMode.js'
 139| import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'
 140| import type { PromptInputHelpers } from '../../utils/handlePromptSubmit.js'
 141| import {
 142|   getImageFromClipboard,
 143|   PASTE_THRESHOLD,
 144| } from '../../utils/imagePaste.js'
 145| import type { ImageDimensions } from '../../utils/imageResizer.js'
 146| import { cacheImagePath, storeImage } from '../../utils/imageStore.js'
 147| import {
 148|   isMacosOptionChar,
 149|   MACOS_OPTION_SPECIAL_CHARS,
 150| } from '../../utils/keyboardShortcuts.js'
 151| import { logError } from '../../utils/log.js'
 152| import {
 153|   isOpus1mMergeEnabled,
 154|   modelDisplayString,
 155| } from '../../utils/model/model.js'
 156| import { setAutoModeActive } from '../../utils/permissions/autoModeState.js'
 157| import {
 158|   cyclePermissionMode,
 159|   getNextPermissionMode,
 160| } from '../../utils/permissions/getNextPermissionMode.js'
 161| import { transitionPermissionMode } from '../../utils/permissions/permissionSetup.js'
 162| import { getPlatform } from '../../utils/platform.js'
 163| import type { ProcessUserInputContext } from '../../utils/processUserInput/processUserInput.js'
 164| import { editPromptInEditor } from '../../utils/promptEditor.js'
 165| import { hasAutoModeOptIn } from '../../utils/settings/settings.js'
 166| import { findBtwTriggerPositions } from '../../utils/sideQuestion.js'
 167| import { findSlashCommandPositions } from '../../utils/suggestions/commandSuggestions.js'
 168| import {
 169|   findSlackChannelPositions,
 170|   getKnownChannelsVersion,
 171|   hasSlackMcpServer,
 172|   subscribeKnownChannels,
 173| } from '../../utils/suggestions/slackChannelSuggestions.js'
 174| import { isInProcessEnabled } from '../../utils/swarm/backends/registry.js'
 175| import { syncTeammateMode } from '../../utils/swarm/teamHelpers.js'
 176| import type { TeamSummary } from '../../utils/teamDiscovery.js'
 177| import { getTeammateColor } from '../../utils/teammate.js'
 178| import { isInProcessTeammate } from '../../utils/teammateContext.js'
 179| import { writeToMailbox } from '../../utils/teammateMailbox.js'
 180| import type { TextHighlight } from '../../utils/textHighlighting.js'
 181| import type { Theme } from '../../utils/theme.js'
 182| import {
 183|   findThinkingTriggerPositions,
 184|   getRainbowColor,
 185|   isUltrathinkEnabled,
 186| } from '../../utils/thinking.js'
 187| import { findTokenBudgetPositions } from '../../utils/tokenBudget.js'
 188| import {
 189|   findUltraplanTriggerPositions,

源码引用: src/components/PromptInput/PromptInput.tsx · 第 191–193 行(共 3178 行)

 191| } from '../../utils/ultraplan/keyword.js'
 192| import { AutoModeOptInDialog } from '../AutoModeOptInDialog.js'
 193| import { BridgeDialog } from '../BridgeDialog.js'

函数入口:模态与光标

PromptInput 函数体开头处理三类互斥:

  1. isModalOverlayActive || isLocalJSXCommandActive:视为 modal,阻断 footer 导航键
  2. input 外部变化:对比 lastInternalInputRef,非内部 set 则 cursor 跳到末尾(语音 STT)
  3. insertTextRef:暴露 insert / setInputWithCursor 给 REPL 层 voice 集成

trackAndSetInput 包装 onInputChange,保证内部编辑与外部注入可区分。

源码引用: src/components/PromptInput/PromptInput.tsx · 第 194–270 行(共 3178 行)

 194| import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
 195| import {
 196|   getVisibleAgentTasks,
 197|   useCoordinatorTaskCount,
 198| } from '../CoordinatorAgentStatus.js'
 199| import { getEffortNotificationText } from '../EffortIndicator.js'
 200| import { getFastIconString } from '../FastIcon.js'
 201| import { GlobalSearchDialog } from '../GlobalSearchDialog.js'
 202| import { HistorySearchDialog } from '../HistorySearchDialog.js'
 203| import { ModelPicker } from '../ModelPicker.js'
 204| import { QuickOpenDialog } from '../QuickOpenDialog.js'
 205| import TextInput from '../TextInput.js'
 206| import { ThinkingToggle } from '../ThinkingToggle.js'
 207| import { BackgroundTasksDialog } from '../tasks/BackgroundTasksDialog.js'
 208| import { shouldHideTasksFooter } from '../tasks/taskStatusUtils.js'
 209| import { TeamsDialog } from '../teams/TeamsDialog.js'
 210| import VimTextInput from '../VimTextInput.js'
 211| import { getModeFromInput, getValueFromInput } from './inputModes.js'
 212| import {
 213|   FOOTER_TEMPORARY_STATUS_TIMEOUT,
 214|   Notifications,
 215| } from './Notifications.js'
 216| import PromptInputFooter from './PromptInputFooter.js'
 217| import type { SuggestionItem } from './PromptInputFooterSuggestions.js'
 218| import { PromptInputModeIndicator } from './PromptInputModeIndicator.js'
 219| import { PromptInputQueuedCommands } from './PromptInputQueuedCommands.js'
 220| import { PromptInputStashNotice } from './PromptInputStashNotice.js'
 221| import { useMaybeTruncateInput } from './useMaybeTruncateInput.js'
 222| import { usePromptInputPlaceholder } from './usePromptInputPlaceholder.js'
 223| import { useShowFastIconHint } from './useShowFastIconHint.js'
 224| import { useSwarmBanner } from './useSwarmBanner.js'
 225| import { isNonSpacePrintable, isVimModeEnabled } from './utils.js'
 226| 
 227| type Props = {
 228|   debug: boolean
 229|   ideSelection: IDESelection | undefined
 230|   toolPermissionContext: ToolPermissionContext
 231|   setToolPermissionContext: (ctx: ToolPermissionContext) => void
 232|   apiKeyStatus: VerificationStatus
 233|   commands: Command[]
 234|   agents: AgentDefinition[]
 235|   isLoading: boolean
 236|   verbose: boolean
 237|   messages: Message[]
 238|   onAutoUpdaterResult: (result: AutoUpdaterResult) => void
 239|   autoUpdaterResult: AutoUpdaterResult | null
 240|   input: string
 241|   onInputChange: (value: string) => void
 242|   mode: PromptInputMode
 243|   onModeChange: (mode: PromptInputMode) => void
 244|   stashedPrompt:
 245|     | {
 246|         text: string
 247|         cursorOffset: number
 248|         pastedContents: Record<number, PastedContent>
 249|       }
 250|     | undefined
 251|   setStashedPrompt: (
 252|     value:
 253|       | {
 254|           text: string
 255|           cursorOffset: number
 256|           pastedContents: Record<number, PastedContent>
 257|         }
 258|       | undefined,
 259|   ) => void
 260|   submitCount: number
 261|   onShowMessageSelector: () => void
 262|   /** Fullscreen message actions: shift+↑ enters cursor. */
 263|   onMessageActionsEnter?: () => void
 264|   mcpClients: MCPServerConnection[]
 265|   pastedContents: Record<number, PastedContent>
 266|   setPastedContents: React.Dispatch<
 267|     React.SetStateAction<Record<number, PastedContent>>
 268|   >
 269|   vimMode: VimMode
 270|   setVimMode: (mode: VimMode) => void

inputModes 与提交前缀

inputModes.ts 提供:

  • getModeFromInput / getValueFromInput:解析 leading ! bash 模式、# 记忆模式等
  • prependModeCharacterToInput:模式切换时保留字符

REPL 在 history 与 submit 路径调用这些函数,PromptInputModeIndicator 向用户展示当前模式。permission mode(plan/auto/bypass)通过 cyclePermissionMode 与 footer 点击切换,写入 toolPermissionContext,影响下一轮 canUseTool 求值。

源码引用: src/components/PromptInput/inputModes.ts · 第 1–34 行(共 34 行)

   1| import type { HistoryMode } from 'src/hooks/useArrowKeyHistory.js'
   2| import type { PromptInputMode } from 'src/types/textInputTypes.js'
   3| 
   4| export function prependModeCharacterToInput(
   5|   input: string,
   6|   mode: PromptInputMode,
   7| ): string {
   8|   switch (mode) {
   9|     case 'bash':
  10|       return `!${input}`
  11|     default:
  12|       return input
  13|   }
  14| }
  15| 
  16| export function getModeFromInput(input: string): HistoryMode {
  17|   if (input.startsWith('!')) {
  18|     return 'bash'
  19|   }
  20|   return 'prompt'
  21| }
  22| 
  23| export function getValueFromInput(input: string): string {
  24|   const mode = getModeFromInput(input)
  25|   if (mode === 'prompt') {
  26|     return input
  27|   }
  28|   return input.slice(1)
  29| }
  30| 
  31| export function isInputModeCharacter(input: string): boolean {
  32|   return input === '!'
  33| }
  34| 

源码引用: src/components/PromptInput/PromptInputModeIndicator.tsx · 第 1–40 行(共 105 行)

   1| import figures from 'figures'
   2| import * as React from 'react'
   3| import { Box, Text } from 'src/ink.js'
   4| import {
   5|   AGENT_COLOR_TO_THEME_COLOR,
   6|   AGENT_COLORS,
   7|   type AgentColorName,
   8| } from 'src/tools/AgentTool/agentColorManager.js'
   9| import type { PromptInputMode } from 'src/types/textInputTypes.js'
  10| import { getTeammateColor } from 'src/utils/teammate.js'
  11| import type { Theme } from 'src/utils/theme.js'
  12| import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
  13| 
  14| type Props = {
  15|   mode: PromptInputMode
  16|   isLoading: boolean
  17|   viewingAgentName?: string
  18|   viewingAgentColor?: AgentColorName
  19| }
  20| 
  21| /**
  22|  * Gets the theme color key for the teammate's assigned color.
  23|  * Returns undefined if not a teammate or if the color is invalid.
  24|  */
  25| function getTeammateThemeColor(): keyof Theme | undefined {
  26|   if (!isAgentSwarmsEnabled()) {
  27|     return undefined
  28|   }
  29|   const colorName = getTeammateColor()
  30|   if (!colorName) {
  31|     return undefined
  32|   }
  33|   if (AGENT_COLORS.includes(colorName as AgentColorName)) {
  34|     return AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName]
  35|   }
  36|   return undefined
  37| }
  38| 
  39| type PromptCharProps = {
  40|   isLoading: boolean

输入控件:TextInput 与 Vim

PromptInput 在 isVimModeEnabled() 时渲染 VimTextInput,否则 ShimmeredInput 或 TextInput。

相关 hooks:

  • useInputBuffer:多行编辑、软换行
  • useMaybeTruncateInput:极长输入截断显示
  • usePromptInputPlaceholder:占位符随 loading/agent 变化
  • useTypeahead / usePromptSuggestion:斜杠命令与 prompt 建议
  • useArrowKeyHistory / useHistorySearch:HistorySearchDialog

图片粘贴走 imagePaste / imageStore;超过 PASTE_THRESHOLD 转 pastedContents 引用号。

源码引用: src/components/PromptInput/ShimmeredInput.tsx · 第 1–35 行(共 122 行)

   1| import * as React from 'react'
   2| import { Ansi, Box, Text, useAnimationFrame } from '../../ink.js'
   3| import {
   4|   segmentTextByHighlights,
   5|   type TextHighlight,
   6| } from '../../utils/textHighlighting.js'
   7| import { ShimmerChar } from '../Spinner/ShimmerChar.js'
   8| 
   9| type Props = {
  10|   text: string
  11|   highlights: TextHighlight[]
  12| }
  13| 
  14| type LinePart = {
  15|   text: string
  16|   highlight: TextHighlight | undefined
  17|   start: number
  18| }
  19| 
  20| export function HighlightedInput({ text, highlights }: Props): React.ReactNode {
  21|   // The shimmer animation (below) re-renders this component at 20fps while the
  22|   // ultrathink keyword is present. text/highlights are referentially stable
  23|   // across animation ticks (parent doesn't re-render), so memoize everything
  24|   // that derives from them: segmentTextByHighlights alone is ~85µs/call
  25|   // (tokenize + sort + O(n²) overlap), which adds up fast at 20fps.
  26|   const { lines, hasShimmer, sweepStart, cycleLength } = React.useMemo(() => {
  27|     const segments = segmentTextByHighlights(text, highlights)
  28| 
  29|     // Split segments by newlines into per-line groups. Ink's row-direction Box
  30|     // indents continuation lines of a multi-line child to that child's X offset.
  31|     // By splitting at newlines, each line renders as its own row, avoiding the
  32|     // incorrect indentation when highlighted text is followed by wrapped content.
  33|     const lines: LinePart[][] = [[]]
  34|     let pos = 0
  35|     for (const segment of segments) {

源码引用: src/components/PromptInput/useMaybeTruncateInput.ts · 第 1–30 行(共 59 行)

   1| import { useEffect, useState } from 'react'
   2| import type { PastedContent } from 'src/utils/config.js'
   3| import { maybeTruncateInput } from './inputPaste.js'
   4| 
   5| type Props = {
   6|   input: string
   7|   pastedContents: Record<number, PastedContent>
   8|   onInputChange: (input: string) => void
   9|   setCursorOffset: (offset: number) => void
  10|   setPastedContents: (contents: Record<number, PastedContent>) => void
  11| }
  12| 
  13| export function useMaybeTruncateInput({
  14|   input,
  15|   pastedContents,
  16|   onInputChange,
  17|   setCursorOffset,
  18|   setPastedContents,
  19| }: Props) {
  20|   // Track if we've initialized this specific input value
  21|   const [hasAppliedTruncationToInput, setHasAppliedTruncationToInput] =
  22|     useState(false)
  23| 
  24|   // Process input for truncation and pasted images from MessageSelector.
  25|   useEffect(() => {
  26|     if (hasAppliedTruncationToInput) {
  27|       return
  28|     }
  29| 
  30|     if (input.length <= 10_000) {

PromptInputFooter 与状态 pill

Footer 左侧(PromptInputFooterLeftSide)聚合:

  • 模型名、fast mode、effort、opus merge 提示
  • 权限模式图标与切换
  • Coordinator agent 数量、background tasks
  • IDE @ mention、sandbox hint

Footer 右侧 suggestions(PromptInputFooterSuggestions)展示可点击 suggestion item。

shouldHideTasksFooter 等 util 避免 agent 运行时重复展示 tasks 区域。Footer 与 Notifications 分工:footer 偏持久配置,Notifications 偏临时事件(FOOTER_TEMPORARY_STATUS_TIMEOUT)。

源码引用: src/components/PromptInput/PromptInputFooter.tsx · 第 1–45 行(共 280 行)

   1| import { feature } from 'bun:bundle'
   2| import * as React from 'react'
   3| import { memo, type ReactNode, useMemo, useRef } from 'react'
   4| import { isBridgeEnabled } from '../../bridge/bridgeEnabled.js'
   5| import { getBridgeStatus } from '../../bridge/bridgeStatusUtil.js'
   6| import { useSetPromptOverlay } from '../../context/promptOverlayContext.js'
   7| import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js'
   8| import type { IDESelection } from '../../hooks/useIdeSelection.js'
   9| import { useSettings } from '../../hooks/useSettings.js'
  10| import { useTerminalSize } from '../../hooks/useTerminalSize.js'
  11| import { Box, Text } from '../../ink.js'
  12| import type { MCPServerConnection } from '../../services/mcp/types.js'
  13| import { useAppState } from '../../state/AppState.js'
  14| import type { ToolPermissionContext } from '../../Tool.js'
  15| import type { Message } from '../../types/message.js'
  16| import type { PromptInputMode, VimMode } from '../../types/textInputTypes.js'
  17| import type { AutoUpdaterResult } from '../../utils/autoUpdater.js'
  18| import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'
  19| import { isUndercover } from '../../utils/undercover.js'
  20| import {
  21|   CoordinatorTaskPanel,
  22|   useCoordinatorTaskCount,
  23| } from '../CoordinatorAgentStatus.js'
  24| import {
  25|   getLastAssistantMessageId,
  26|   StatusLine,
  27|   statusLineShouldDisplay,
  28| } from '../StatusLine.js'
  29| import { Notifications } from './Notifications.js'
  30| import { PromptInputFooterLeftSide } from './PromptInputFooterLeftSide.js'
  31| import {
  32|   PromptInputFooterSuggestions,
  33|   type SuggestionItem,
  34| } from './PromptInputFooterSuggestions.js'
  35| import { PromptInputHelpMenu } from './PromptInputHelpMenu.js'
  36| 
  37| type Props = {
  38|   apiKeyStatus: VerificationStatus
  39|   debug: boolean
  40|   exitMessage: {
  41|     show: boolean
  42|     key?: string
  43|   }
  44|   vimMode: VimMode | undefined
  45|   mode: PromptInputMode

源码引用: src/components/PromptInput/Notifications.tsx · 第 1–40 行(共 367 行)

   1| import { feature } from 'bun:bundle'
   2| import * as React from 'react'
   3| import { type ReactNode, useEffect, useMemo, useState } from 'react'
   4| import {
   5|   type Notification,
   6|   useNotifications,
   7| } from 'src/context/notifications.js'
   8| import { logEvent } from 'src/services/analytics/index.js'
   9| import { useAppState } from 'src/state/AppState.js'
  10| import { useVoiceState } from '../../context/voice.js'
  11| import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js'
  12| import { useIdeConnectionStatus } from '../../hooks/useIdeConnectionStatus.js'
  13| import type { IDESelection } from '../../hooks/useIdeSelection.js'
  14| import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'
  15| import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js'
  16| import { Box, Text } from '../../ink.js'
  17| import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js'
  18| import { calculateTokenWarningState } from '../../services/compact/autoCompact.js'
  19| import type { MCPServerConnection } from '../../services/mcp/types.js'
  20| import type { Message } from '../../types/message.js'
  21| import {
  22|   getApiKeyHelperElapsedMs,
  23|   getConfiguredApiKeyHelper,
  24|   getSubscriptionType,
  25| } from '../../utils/auth.js'
  26| import type { AutoUpdaterResult } from '../../utils/autoUpdater.js'
  27| import { getExternalEditor } from '../../utils/editor.js'
  28| import { isEnvTruthy } from '../../utils/envUtils.js'
  29| import { formatDuration } from '../../utils/format.js'
  30| import { setEnvHookNotifier } from '../../utils/hooks/fileChangedWatcher.js'
  31| import { toIDEDisplayName } from '../../utils/ide.js'
  32| import { getMessagesAfterCompactBoundary } from '../../utils/messages.js'
  33| import { tokenCountFromLastAPIResponse } from '../../utils/tokens.js'
  34| import { AutoUpdaterWrapper } from '../AutoUpdaterWrapper.js'
  35| import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
  36| import { IdeStatusIndicator } from '../IdeStatusIndicator.js'
  37| import { MemoryUsageIndicator } from '../MemoryUsageIndicator.js'
  38| import { SentryErrorBoundary } from '../SentryErrorBoundary.js'
  39| import { TokenWarning } from '../TokenWarning.js'
  40| import { SandboxPromptFooterHint } from './SandboxPromptFooterHint.js'

提交路径与 speculation

onSubmit prop 类型包含可选 speculationAccept:

  • 与 PromptSuggestion speculation 服务联动
  • 接受预生成内容时记录 session 节省时间

REPL 传入的 onSubmit 最终进入 handlePromptSubmit / processUserInput,携带:

  • helpers:abort、addToHistory、expandPastedTextRefs
  • options.fromKeybinding:区分键绑提交与按钮提交

onAgentSubmit 在查看 teammate/local agent 时走并行管道,不经过主线程 query。

源码引用: src/components/PromptInput/PromptInput.tsx · 第 164–171 行(共 3178 行)

 164| import { editPromptInEditor } from '../../utils/promptEditor.js'
 165| import { hasAutoModeOptIn } from '../../utils/settings/settings.js'
 166| import { findBtwTriggerPositions } from '../../utils/sideQuestion.js'
 167| import { findSlashCommandPositions } from '../../utils/suggestions/commandSuggestions.js'
 168| import {
 169|   findSlackChannelPositions,
 170|   getKnownChannelsVersion,
 171|   hasSlackMcpServer,

源码引用: src/utils/handlePromptSubmit.ts · 第 1–40 行(共 611 行)

   1| import type { UUID } from 'crypto'
   2| import { logEvent } from 'src/services/analytics/index.js'
   3| import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from 'src/services/analytics/metadata.js'
   4| import { type Command, getCommandName, isCommandEnabled } from '../commands.js'
   5| import { selectableUserMessagesFilter } from '../components/MessageSelector.js'
   6| import type { SpinnerMode } from '../components/Spinner/types.js'
   7| import type { QuerySource } from '../constants/querySource.js'
   8| import { expandPastedTextRefs, parseReferences } from '../history.js'
   9| import type { CanUseToolFn } from '../hooks/useCanUseTool.js'
  10| import type { IDESelection } from '../hooks/useIdeSelection.js'
  11| import type { AppState } from '../state/AppState.js'
  12| import type { SetToolJSXFn } from '../Tool.js'
  13| import type { LocalJSXCommandOnDone } from '../types/command.js'
  14| import type { Message } from '../types/message.js'
  15| import {
  16|   isValidImagePaste,
  17|   type PromptInputMode,
  18|   type QueuedCommand,
  19| } from '../types/textInputTypes.js'
  20| import { createAbortController } from './abortController.js'
  21| import type { PastedContent } from './config.js'
  22| import { logForDebugging } from './debug.js'
  23| import type { EffortValue } from './effort.js'
  24| import type { FileHistoryState } from './fileHistory.js'
  25| import { fileHistoryEnabled, fileHistoryMakeSnapshot } from './fileHistory.js'
  26| import { gracefulShutdownSync } from './gracefulShutdown.js'
  27| import { enqueue } from './messageQueueManager.js'
  28| import { resolveSkillModelOverride } from './model/model.js'
  29| import type { ProcessUserInputContext } from './processUserInput/processUserInput.js'
  30| import { processUserInput } from './processUserInput/processUserInput.js'
  31| import type { QueryGuard } from './QueryGuard.js'
  32| import { queryCheckpoint, startQueryProfile } from './queryProfiler.js'
  33| import { runWithWorkload } from './workloadContext.js'
  34| 
  35| function exit(): void {
  36|   gracefulShutdownSync(0)
  37| }
  38| 
  39| type BaseExecutionParams = {
  40|   queuedCommands?: QueuedCommand[]

辅助组件与 banner

文件职责
PromptInputHelpMenu快捷键与帮助浮层
PromptInputQueuedCommands展示排队命令(scrollable 区也有)
PromptInputStashNoticestashed prompt 恢复提示
IssueFlagBannerissue 标记横幅
SandboxPromptFooterHintsandbox 模式说明
VoiceIndicator语音模式指示
useSwarmBannerswarm 团队横幅

HistorySearchInput 在 isSearchingHistory 时替换或叠加输入区。GlobalSearchDialog、ModelPicker 等由 footer 快捷键打开,通过 overlayContext 协调。

源码引用: src/components/PromptInput/PromptInputHelpMenu.tsx · 第 1–30 行(共 150 行)

   1| import { feature } from 'bun:bundle'
   2| import * as React from 'react'
   3| import { Box, Text } from 'src/ink.js'
   4| import { getPlatform } from 'src/utils/platform.js'
   5| import { isKeybindingCustomizationEnabled } from '../../keybindings/loadUserBindings.js'
   6| import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'
   7| import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
   8| import { isFastModeAvailable, isFastModeEnabled } from '../../utils/fastMode.js'
   9| import { getNewlineInstructions } from './utils.js'
  10| 
  11| /** Format a shortcut for display in the help menu (e.g., "ctrl+o" → "ctrl + o") */
  12| function formatShortcut(shortcut: string): string {
  13|   return shortcut.replace(/\+/g, ' + ')
  14| }
  15| 
  16| type Props = {
  17|   dimColor?: boolean
  18|   fixedWidth?: boolean
  19|   gap?: number
  20|   paddingX?: number
  21| }
  22| 
  23| export function PromptInputHelpMenu(props: Props): React.ReactNode {
  24|   const { dimColor, fixedWidth, gap, paddingX } = props
  25| 
  26|   // Get configured shortcuts from keybinding system
  27|   const transcriptShortcut = formatShortcut(
  28|     useShortcutDisplay('app:toggleTranscript', 'Global', 'ctrl+o'),
  29|   )
  30|   const todosShortcut = formatShortcut(

源码引用: src/components/PromptInput/IssueFlagBanner.tsx · 第 1–25 行(共 29 行)

   1| import * as React from 'react'
   2| import { FLAG_ICON } from '../../constants/figures.js'
   3| import { Box, Text } from '../../ink.js'
   4| 
   5| /**
   6|  * ANT-ONLY: Banner shown in the transcript that prompts users to report
   7|  * issues via /issue. Appears when friction is detected in the conversation.
   8|  */
   9| export function IssueFlagBanner(): React.ReactNode {
  10|   if ("external" !== 'ant') {
  11|     return null
  12|   }
  13| 
  14|   return (
  15|     <Box flexDirection="row" marginTop={1} width="100%">
  16|       <Box minWidth={2}>
  17|         <Text color="warning">{FLAG_ICON}</Text>
  18|       </Box>
  19|       <Text>
  20|         <Text dimColor>[ANT-ONLY] </Text>
  21|         <Text color="warning" bold>
  22|           Something off with Claude?
  23|         </Text>
  24|         <Text dimColor> /issue to report it</Text>
  25|       </Text>

REPL 侧对接要点

REPL 渲染 PromptInput 时传入:

  • messages:用于 context suggestions、@file 解析
  • isLoading:禁用提交、改 placeholder
  • hasSuppressedDialogs:toolUseConfirmQueue 等有项时为 true
  • disabled prop(REPL 级):整个输入隐藏

当 focusedInputDialog 非空,companion 与部分 footer 交互隐藏。Permission 批准期间用户焦点在 overlay,PromptInput 仍可见但键盘事件由 Permission 组件消费。

阅读 PromptInput 时宜对照 REPL 4590+ 行 bottom JSX 传参列表。

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

  58|   mergeFileStateCaches,
  59|   READ_FILE_STATE_CACHE_SIZE,

源码引用: src/components/PromptInput/PromptInputQueuedCommands.tsx · 第 1–30 行(共 167 行)

   1| import { feature } from 'bun:bundle'
   2| import * as React from 'react'
   3| import { useMemo } from 'react'
   4| import { Box } from 'src/ink.js'
   5| import { useAppState } from 'src/state/AppState.js'
   6| import {
   7|   STATUS_TAG,
   8|   SUMMARY_TAG,
   9|   TASK_NOTIFICATION_TAG,
  10| } from '../../constants/xml.js'
  11| import { QueuedMessageProvider } from '../../context/QueuedMessageContext.js'
  12| import { useCommandQueue } from '../../hooks/useCommandQueue.js'
  13| import type { QueuedCommand } from '../../types/textInputTypes.js'
  14| import { isQueuedCommandVisible } from '../../utils/messageQueueManager.js'
  15| import {
  16|   createUserMessage,
  17|   EMPTY_LOOKUPS,
  18|   normalizeMessages,
  19| } from '../../utils/messages.js'
  20| import { jsonParse } from '../../utils/slowOperations.js'
  21| import { Message } from '../Message.js'
  22| 
  23| const EMPTY_SET = new Set<string>()
  24| 
  25| /**
  26|  * Check if a command value is an idle notification that should be hidden.
  27|  * Idle notifications are processed silently without showing to the user.
  28|  */
  29| function isIdleNotification(value: string): boolean {
  30|   try {

源码目录

PromptInput/ 子目录 21 文件,建议从 PromptInput.tsx → Footer → inputModes 顺序阅读。

动手练习

  1. 切换 permission mode(快捷键或 footer),观察 setToolPermissionContext 后下一次 tool_use 是否仍 ask
  2. 开启 Vim 模式,确认 hjkl 不触发 transcript 滚动
  3. 粘贴超大文本,检查 pastedContents 引用是否出现在提交后的 UserPromptMessage
  4. 在权限弹窗打开时尝试输入,验证 hasSuppressedDialogs 行为

本章小结与延伸

PromptInput = 用户意图入口。提交后见 query 引擎;等待批准时 REPL 抑制输入但组件可能仍挂载。 继续学习:

  • components 总览
  • REPL 主屏
  • hooks 输入快捷键
Prev
PermissionRequest · 权限弹窗
Next
api-claude · Anthropic API 流式与重试