Claude Code 使用 React Ink 在终端中渲染富交互界面。这是一个 “React for Terminal” 的应用。
一、技术原理 React Ink 是什么? 传统 React: React → Virtual DOM → 浏览器 DOM → 像素 React Ink: React → Virtual DOM → 终端字符 → ANSI escape codes
React Ink 把 React 的组件模型映射到终端:
<Box> → Flexbox 布局(类似 <div>)
<Text> → 带样式的文本(类似 <span>)
useState/useEffect → 完全一致
但没有浏览器 API(无 DOM、无 CSS)
二、UI 架构层次 ┌─────────────────────────────────────────┐ │ App.tsx (根组件) │ │ ├─ StoreProvider (状态) │ │ ├─ ThemeProvider (主题) │ │ ├─ NotificationProvider (通知) │ │ └─ children │ │ └─ REPL.tsx (主屏幕, 5006 行!) │ │ ├─ Messages (消息列表) │ │ │ ├─ MessageRow → Message │ │ │ ├─ ToolUseLoader │ │ │ ├─ FileEditToolDiff │ │ │ └─ ... │ │ ├─ StatusLine (底部状态栏) │ │ ├─ PromptInput (输入框) │ │ └─ 各种 Dialog (权限/配置/...) │ └─────────────────────────────────────────┘
三、REPL.tsx — 主屏幕 (5006 行) 这是整个 UI 的 超级组件 ,管理着:
3.1 核心状态 const [messages, setMessages] = useState<Message []>([]);const [isLoading, setIsLoading] = useState (false );const [toolJSX, setToolJSX] = useState<React .ReactNode | null >(null );const [permissionRequest, setPermissionRequest] = useState (null );const [elicitation, setElicitation] = useState (null );const [inputValue, setInputValue] = useState ('' );const [promptMode, setPromptMode] = useState<PromptInputMode >('normal' );
3.2 消息处理循环 async function handleSubmit (text : string ) { if (isSlashCommand (text)) { await processCommand (text); return ; } const userMessage = createUserMessage ({ content : text }); setMessages (prev => [...prev, userMessage]); setIsLoading (true ); for await (const event of query (userMessage, messages, ...)) { handleMessageFromStream (event, setMessages); } setIsLoading (false ); }
3.3 React Hooks 集成 (~90 个 hooks!) REPL.tsx 使用了大量自定义 hooks:
const tools = useMergedTools (...); const commands = useMergedCommands (...); const canUseTool = useCanUseTool (...); useReplBridge (...); useRemoteSession (...); useDirectConnect (...); useSSHSession (...); useTerminalSize (); useSearchInput (); useVimInput (); useArrowKeyHistory (); useCopyOnSelect (); useVirtualScroll (); useLogMessages (); useUpdateNotification (); useApiKeyVerification (); useCostSummary ();
四、组件库详解 4.1 消息渲染 Message.tsx ├─ UserMessage → 蓝色 "You:" 前缀 ├─ AssistantMessage → 紫色 "Claude:" 前缀 │ ├─ 文本内容 → Markdown 渲染 │ ├─ 代码块 → 语法高亮 (HighlightedCode) │ └─ 工具调用 → ToolUseLoader ├─ SystemMessage → 灰色系统消息 └─ ToolResult → 工具结果展示 ├─ FileEditToolDiff → diff 视图 ├─ BashToolResult → 命令输出 └─ 其他工具结果
type PromptInputMode = | 'normal' | 'vim-normal' | 'vim-insert' | 'vim-visual' | 'search' | 'command'
4.3 StatusLine — 状态栏 ┌─────────────────────────────────────────────────────┐ │ claude-4-sonnet │ $0.042 │ 2.1k tokens │ ⠋ 思考中... │ └─────────────────────────────────────────────────────┘
显示:模型名、累计费用、token 用量、当前状态动画
4.4 权限对话框
4.5 Diff 视图
五、虚拟滚动
六、Spinner 动画 type SpinnerMode = | 'thinking' | 'tool' | 'compact' | 'streaming'
七、主题系统 type Theme = { name : ThemeName ; colors : { primary : string ; secondary : string ; error : string ; warning : string ; success : string ; muted : string ; }; }; type ThemeName = 'dark' | 'light' | 'high-contrast' | 'custom' ;
八、Store 系统 (React Ink 的状态管理) function createStore<T>(initialState : T, onChange ?: OnChange <T>): Store <T> { let state = initialState; const listeners = new Set <Listener >(); return { getState : () => state, setState : (updater ) => { const prev = state; const next = updater (prev); if (Object .is (next, prev)) return ; state = next; onChange?.({ newState : next, oldState : prev }); for (const listener of listeners) listener (); }, subscribe : (listener ) => { listeners.add (listener); return () => listeners.delete (listener); }, }; }
为什么不用 Redux/Zustand?
包体积 :Claude Code 是 CLI 工具,每多一个依赖都增加启动时间
简单够用 :只需要 getState/setState/subscribe 三个操作
与 React Ink 配合 :React Ink 的渲染模型比浏览器简单,不需要复杂的状态管理
九、AppState — 应用状态 (570 行) type AppState = DeepImmutable <{ settings : SettingsJson ; verbose : boolean ; mainLoopModel : ModelSetting ; messages : Message []; statusLineText : string | undefined ; expandedView : 'none' | 'tasks' | 'teammates' ; toolPermissionContext : ToolPermissionContext ; mcpClients : MCPServerConnection []; mcpTools : Tool []; tasks : Map <string , TaskState >; agents : AgentDefinition []; agentColors : Map <string , AgentColorName >; speculationState : SpeculationState ; attributionState : AttributionState ; fileHistoryState : FileHistoryState ; notifications : Notification []; loadedPlugins : LoadedPlugin []; pluginErrors : PluginError []; }>;
DeepImmutable<T> 的作用所有 AppState 的属性都是深度只读的。修改必须通过 setAppState(prev => ({ ...prev, field: newValue })) 创建新对象。这保证了:
React 的引用比较能正确检测变化
防止工具执行期间意外修改状态
状态变更可追踪
十、渲染性能优化 10.1 FPS 监控 type FpsMetrics = { fps : number ; maxFrameTime : number ; jank : number ; };
10.2 内存使用监控
10.3 终端输出优化 const SHOW_CURSOR = '\x1b[?25h' ;const HIDE_CURSOR = '\x1b[?25l' ;