UI 交互层
基于 Ink (React for CLI) 的终端渲染框架、组件架构与事件系统
职责概述
解决的问题:CC 跑在终端里,但终端只有纯文本。需要把模型的流式回复(带格式)、工具执行进度、权限确认弹窗、错误提示等,全部渲染成好看且可交互的终端界面,同时处理键盘输入和鼠标事件。
应用场景:① 模型回复时实时渲染 Markdown 格式(代码高亮、表格、列表)② 工具执行时显示旋转动画和进度条 ③ 权限请求时弹出"允许/拒绝"选择界面 ④ 用户在输入框中编辑多行文本时的光标移动和自动补全 ⑤ 上下滚动查看历史对话的虚拟列表。
一句话理解:就像终端版的 ChatGPT 网页——虽然跑在命令行里,但该有的格式、动画、交互一个不少。
架构设计
核心数据流
渲染管线
reconciler.ts 使用 react-reconciler(LegacyRoot 同步模式)将 React 组件树转换为 DOM 树(DOMElement 节点)layout/yoga.ts 调用 Yoga 引擎计算每个节点的精确位置和尺寸(类 Flexbox)renderer.ts 遍历 DOM 树,将每个节点渲染为 Screen 缓冲区的字符单元(含样式、超链接池)LogUpdate 对比前后两帧 Screen 缓冲区,生成 ANSI 补丁序列,通过 stdout 写入终端。使用 BSU/ESU 同步输出防止闪烁事件处理流程
ParsedInput(字符)+ ParsedKey(修饰键标志)+ ParsedMouse(鼠标坐标)InputEvent,通过 EventEmitter 通知订阅者events/dispatcher.ts 模拟 DOM 标准:capture(根→目标)→ bubble(目标→根)。查找匹配的 DOM 元素并执行处理器关键类型与接口
Ink 核心类
// ink/ink.tsx:76-120 — Ink 主类关键结构
export default class Ink {
private readonly log: LogUpdate // 差量输出管理器
private readonly terminal: Terminal // 终端能力适配
private readonly container: FiberRoot // React reconciler 容器
private rootNode: DOMElement // 虚拟 DOM 根节点
readonly focusManager: FocusManager // 焦点管理
private renderer: Renderer // DOM → Screen 渲染器
private readonly stylePool: StylePool // 样式对象池
private charPool: CharPool // 字符对象池
private hyperlinkPool: HyperlinkPool // 超链接对象池
private frontFrame: Frame // 前帧缓冲区
private backFrame: Frame // 后帧缓冲区
}
Frame 数据结构
// ink/frame.ts:12-20
export type Frame = {
readonly screen: Screen // 渲染后的屏幕缓冲区
readonly viewport: Size // 视口尺寸
readonly cursor: Cursor // 光标位置与可见性
readonly scrollHint?: ScrollHint | null // DECSTBM 滚动优化提示
readonly scrollDrainPending?: boolean // 是否有待处理的滚动增量
}
DOMElement — Ink 虚拟 DOM 节点
// ink/dom.ts:31-80
export type DOMElement = {
nodeName: ElementNames // 'ink-root' | 'ink-box' | 'ink-text' | ...
attributes: Record<string, DOMNodeAttribute>
childNodes: DOMNode[]
style: Styles
yogaNode?: LayoutNode // Yoga 布局节点
dirty: boolean // 是否需要重渲染
_eventHandlers?: Record<string, unknown> // 事件处理器
// 滚动状态(overflow: 'scroll' 的 Box)
scrollTop?: number
scrollHeight?: number
scrollViewportHeight?: number
stickyScroll?: boolean
}
ThemeProvider — 主题系统
// components/design-system/ThemeProvider.tsx:8-16
type ThemeContextValue = {
themeSetting: ThemeSetting // 用户偏好(可以是 'auto')
setThemeSetting: (s) => void
setPreviewTheme: (s) => void // 主题选择器预览
savePreview: () => void
cancelPreview: () => void
currentTheme: ThemeName // 解析后的实际主题(永远不是 'auto')
}
ThemedText — 主题感知文本组件
// components/design-system/ThemedText.tsx:12-61
export type Props = {
color?: keyof Theme | Color // 接受主题键或原始颜色值
backgroundColor?: keyof Theme
dimColor?: boolean // 使用主题的 inactive 颜色
bold?: boolean; italic?: boolean
underline?: boolean; strikethrough?: boolean
inverse?: boolean
wrap?: Styles['textWrap'] // 'wrap' | 'truncate' | 'truncate-start' | ...
}
// resolveColor() 自动将主题键(如 'primary')解析为实际 RGB/ANSI 颜色值
事件双阶段派发
// ink/events/dispatcher.ts:46-79
function collectListeners(target, event): DispatchListener[] {
// 从目标到根遍历:
// capture 处理器 unshift(根优先)
// bubble 处理器 push(目标优先)
// 结果: [root-cap, ..., parent-cap, target-cap, target-bub, parent-bub, ..., root-bub]
let node = target
while (node) {
const captureHandler = getHandler(node, event.type, true)
const bubbleHandler = getHandler(node, event.type, false)
// ...收集到 listeners 数组
node = node.parentNode
}
return listeners
}
设计模式与亮点
1. React-for-CLI 架构
Ink 将 Web React 的声明式组件模型完美移植到终端。开发者用 JSX 编写 UI,reconciler 将组件树映射到虚拟 DOM(DOMElement),
Yoga 引擎做 Flexbox 布局,最终输出为 ANSI 字符流。这让复杂的终端 UI(选择器、对话框、进度条)可以用 React 组件的方式构建。
2. 双缓冲差量渲染
Ink 使用 frontFrame/backFrame 双缓冲策略。每帧渲染时,backFrame 接收新内容,然后与 frontFrame 做 cell-by-cell 对比, 只输出变化的 ANSI 序列。这避免了每帧全屏刷新的闪烁问题。StylePool/CharPool/HyperlinkPool 使用对象池模式减少 GC 压力。
3. 虚拟列表优化
VirtualMessageList.tsx 实现了虚拟滚动:只渲染视口内的消息组件,配合 OffscreenFreeze 冻结不可见组件。
在长会话(2800+ 消息)中,通过 LogoHeader memo 化防止不必要的子树 dirty 级联——这是经过性能调优的关键模式:
如果 Logo 未 memo,每次 re-render 会导致所有 MessageRow 放弃 blit 优化(150K+ writes/frame)。
4. 终端能力自适应
terminal.ts 检测终端能力并自适应:
// ink/terminal.ts — 能力检测示例
function isSynchronizedOutputSupported(): boolean {
// tmux 不支持 DEC 2026 — 跳过
if (process.env.TMUX) return false
// 已知支持 DEC 2026 的终端
if (['iTerm.app', 'WezTerm', 'ghostty', 'kitty'].includes(termProgram))
return true
return false
}
function isProgressReportingAvailable(): boolean {
// OSC 9;4 进度报告:Ghostty 1.2+、iTerm2 3.6.6+
}
5. Design System 主题层
ThemedText/ThemedBox 封装了 Ink 的基础 Text/Box 组件,添加主题解析层。颜色属性接受 keyof Theme(语义键)或原始 ANSI 值,
resolveColor() 在运行时自动解析。ThemeProvider 还支持 'auto' 模式——通过 OSC 11 查询终端背景色自动切换明暗主题。
6. 按键序列解析
termio/ 目录实现了完整的终端转义序列解析栈:
csi.ts— CSI 序列(光标移动、屏幕擦除)dec.ts— DEC 私有模式(交替屏幕、鼠标追踪、光标控制)osc.ts— OSC 序列(超链接、剪贴板、标签页状态)sgr.ts— SGR 样式序列parser.ts— 统一的状态机解析器
开发者实践指南
创建新的 UI 组件
所有 CC 组件使用 ink.ts 导出的封装 API:
// ink.ts — 项目统一入口(自动包裹 ThemeProvider)
import { Box, Text, render, createRoot } from '../ink.js'
// 创建带主题的根节点
const root = await createRoot({ stdout: process.stdout })
root.render(<Box flexDirection="column">
<Text color="primary" bold>标题</Text>
<Text dimColor>次要信息</Text>
</Box>)
PromptInput 集成
PromptInput 是 CC 最复杂的组件(~80 行导入、数十个 hook)。要集成新功能:
- 使用
useInput或useKeybinding注册快捷键 - 使用
useTypeahead添加命令自动补全 - 使用
useAppState读写全局状态 - 使用
useCommandQueue管理命令排队
性能优化技巧
- 使用
React.memo包裹静态子树(参考 LogoHeader 模式) - 避免在 render 路径中创建新对象/数组——Ink 的 blit 优化依赖 dirty 标记
- 使用
OffscreenFreeze冻结视口外组件 - 大量文本使用
ScrollBox而非全量渲染
架构师决策指南
为什么选择 Ink(React for CLI)而非直接 ANSI
CC 选择 Ink 的核心原因是组件复用和声明式状态管理。一个权限对话框(PermissionRequest) 需要:列表导航、确认/拒绝按钮、模式切换、实时更新——用纯 ANSI 实现需要数百行状态机代码。 用 Ink,这就是一个 React 组件,状态通过 hooks 管理,渲染自动响应。
渲染性能的极限与应对
终端渲染的瓶颈不在 Yoga 布局(~1ms/帧),而在 ANSI diff + stdout 写入(~5-15ms/帧)。 Ink 的优化策略:
- Blit 优化:前后帧 cell 对比只输出差异行(减少 ANSI 序列体积)
- BSU/ESU 同步:DEC 2026 模式确保一帧的输出原子写入(消除闪烁)
- 帧调度:
FRAME_INTERVAL_MS控制渲染频率,避免空转 - 对象池:StylePool/CharPool 跨帧复用,减少 GC 停顿
组件架构的权衡
事件系统的设计取舍
Ink 的事件系统模拟了 DOM 的 capture/bubble 双阶段模型。这在功能上很强大(支持 onKeyDownCapture 等场景),
但增加了复杂度。在 CLI 场景中,绝大多数事件处理都是简单的 useInput 或 useKeybinding,
双阶段派发只在鼠标点击 hit-test 和嵌套焦点管理中使用。
UI 组件层级图
◈ 可视化处理拓扑图
①
useVirtualScroll 按需渲染可见区域消息②
computeSliceStart(Messages.tsx:315)计算可视窗口起始位置③ 避免渲染历史消息 → 5000 行 REPL 中仅遍历活跃消息
性能关键:将每帧 DOM 树遍历范围压缩到流式输出中的活跃消息
⇉ 核心处理流程详解
Claude Code 的 UI 渲染管线是一个精心设计的多层架构:从 React 组件树出发,经过自定义 Reconciler 转换为虚拟 DOM,再通过 Yoga 布局引擎计算终端坐标,最终由 Screen 缓冲区和双缓冲 blit 机制将内容写入终端。整个流程在每帧(约 60fps)内完成,同时支持搜索高亮、文本选择、虚拟滚动等高级特性。
REPL(screens/REPL.tsx:572)作为根组件,组合 Messages(消息列表)、TextInput(输入框)、StatusLine(状态栏)等组件。通过 useAppState 订阅全局状态,状态变更触发 React 调度。App(components/App.tsx:19)提供 AppStateProvider、StatsProvider、FpsMetricsProvider 三层上下文。reconciler.ts(ink/reconciler.ts)使用 react-reconciler 创建自定义宿主环境。createInstance(L331)将 React 元素转为 DOMElement 节点,commitUpdate(L426)处理属性差异更新。resetAfterCommit(L247)在每次提交后触发 Ink.onRender,启动渲染管线。ink/layout/yoga.ts 将虚拟 DOM 树映射到 Yoga 节点树。每个 DOMElement 携带一个 Yoga 节点,通过 Flexbox 属性(width/height/flex/padding 等)计算出每个节点的终端行列坐标。布局结果回写到 DOMElement 的 yi(Yoga info)字段。createRenderer(ink/renderer.ts:31)创建渲染闭包。遍历 DOM 树,对每个节点调用 renderNodeToOutput 将文本/样式写入 Output 对象。Output 最终转换为 Screen 缓冲区——一个二维字符数组(rows x cols),每个单元格存储字符、前景色、背景色、超链接等样式信息。Ink.onRender(ink/ink.tsx:420-789)是渲染管线的核心。维护 prev 和 back 两个 Frame 缓冲区。每帧对比 prev 和 back 的差异(blit),只将变化的行写入终端。这避免了全屏刷新的闪烁问题。关键优化:queueMicrotask(L212)确保同一事件循环内的多次 setState 只触发一次渲染。Messages.tsx(L341)管理消息的渲染、折叠和上下文窗口。通过 computeSliceStart(L315)计算可视窗口的起始位置,避免渲染历史消息。VirtualMessageList.tsx(L289)在全屏模式下启用虚拟滚动,使用 useVirtualScroll hook 实现按需渲染。消息组件 Message.tsx(L58)处理 tool_use、thinking、text 等多种内容块的渲染。useTextInput(hooks/useTextInput.ts:73)处理所有键盘输入:多行编辑、历史导航、Kill/Yank ring、粘贴检测。mapKey(L318)将按键映射到编辑操作,handleEnter(L247)区分提交与换行。TextInput.tsx(components/TextInput.tsx:37)将 hook 与 Ink 的 useAnimationFrame 集成,实现光标闪烁。Ink.writeRaw(ink/ink.tsx:1433)将 blit 差异转换为 ANSI 转义序列(光标移动 + SGR 样式 + OSC 超链接),通过 process.stdout.write 写入终端。支持 256 色、真彩色、OSC 52 剪贴板、DEC 2026 模式等终端能力。Alt Screen 模式(L357)通过保存/恢复终端状态实现全屏 UI。shouldRenderStatically(Messages.tsx:779)对已完成的消息使用静态渲染缓存,跳过 React 渲染和 Yoga 布局,将每帧的 DOM 树遍历范围压缩到仅包含流式输出中的活跃消息。★ 设计精华
1. 自定义 React Reconciler 实现终端渲染
Claude Code 没有使用传统的终端 UI 库(如 blessed、ncurses),而是复用了 React 的组件模型和调度器。通过 react-reconciler(ink/reconciler.ts)创建自定义宿主环境,将 React 组件树映射到终端屏幕。这意味着开发者可以使用 React 的 hooks、context、memo 等所有特性来构建终端 UI,与 Web 开发体验完全一致。
// ink/reconciler.ts:331-359
createInstance(originalType, newProps, _root, hostContext) {
const node = dom.createNode(originalType)
for (const [key, value] of Object.entries(newProps)) {
if (key === 'children') continue
applyProp(node, key, value) // 样式 → Yoga 节点属性
}
return node
}
// ink/reconciler.ts:247-315
resetAfterCommit(rootNode) {
// 每次 React 提交后触发一次渲染
deferredRender() // queueMicrotask → Ink.onRender
}
queueMicrotask(L212),同一事件循环中的多次 setState 被自动批处理,避免了对同一帧的重复渲染。React 的优先级调度还确保用户输入(高优先级)不会被长消息渲染(低优先级)阻塞。2. 双缓冲 Blit 渲染策略
Ink.onRender(ink/ink.tsx:420)维护两个 Frame:prev(上一帧已写入终端的内容)和 back(当前帧的新内容)。Blit 对比两者差异,只输出变化的行。这种策略避免了全屏刷新导致的闪烁,同时将终端 I/O 降到最低。
// ink/ink.tsx:420 — onRender 核心流程
onRender() {
// 1. Yoga 布局计算
const nextFrame = renderer({ terminal, ... })
// 2. 差异对比 + ANSI 输出
for (let y = 0; y < rows; y++) {
if (!prev[y].equals(back[y])) {
// 只输出变化的行
writeRaw(moveCursor(y) + back[y].toAnsi())
}
}
// 3. 交换缓冲区
prev = back; back = nextFrame
}
3. 静态渲染缓存 — shouldRenderStatically
在 Messages.tsx 中,shouldRenderStatically(L779-833)判断已完成的消息是否可以跳过重新渲染。判断条件包括:消息不是正在流式输出、没有活跃的 tool_use、没有兄弟 tool_use、屏幕尺寸未变等。符合条件的消息直接使用缓存的渲染结果,避免重复的 React 渲染和 Yoga 布局计算。
// Messages.tsx:779-833
export function shouldRenderStatically(
message, streamingToolUseIDs, inProgressToolUseIDs,
siblingToolUseIDs, screen, lookup) {
// 非活跃消息:无流式/进行中/兄弟 tool_use
const isActive = streamingToolUseIDs.has(message.uuid)
|| inProgressToolUseIDs.has(message.uuid)
if (isActive) return false
// 屏幕尺寸未变 + 无 pending 操作
return screen === lookup.screen
}
4. 消息切片窗口 — computeSliceStart
computeSliceStart(Messages.tsx:315-340)实现了上下文窗口裁剪:只渲染最近 N 条消息,历史消息被折叠为 "N messages collapsed"。这不是简单的固定数量裁剪——它考虑了折叠边界(不在 tool_use 和 tool_result 之间切断)、锚点消息(保持最近用户消息可见)和增量更新。
◈ Agent 实践借鉴 — 客服 Agent 面板设计
1. 场景映射:客服坐席面板的实时需求
客服坐席面板和 Claude Code 的终端有一个共同的核心需求:实时流式渲染。坐席不能等 agent 想完 5 秒才看到结果,客户的消息也不能有延迟。不同之处在于,客服面板是 Web 应用,不需要终端的黑魔法:
// 坐席面板的实时渲染需求
// 1. Agent 回复必须流式显示 — token 到达即渲染
// 客户等 5 秒才看到第一个字 → 差评
//
// 2. 工具结果需要不同渲染样式:
// /查订单 → 订单卡片(商品名、金额、状态)
// /查物流 → 时间线(已发货→运输中→派送中)
// /退单 → 进度条(审核中→退款中→已完成)
//
// 3. 多个客户同时在线,面板需要切换会话
// 坐席同时服务 3-5 个客户
//
// 4. 工具调用状态实时指示
// "正在查询订单..." / "查询完成" / "查询失败"
// Claude Code → 客服 Agent 面板
// ──────────────────────────────────────────────
流式 token 渲染 → WebSocket/SSE 实时追加
工具结果注册表 → 订单→卡片、物流→时间线
双缓冲差异输出 → 不需要(浏览器 DOM 天然增量)
Yoga 布局引擎 → 不需要(CSS flexbox 就行)
React Reconciler → 不需要(标准 Web React/Vue)
消息窗口裁剪 → 虚拟滚动列表(成熟的 Web 方案)
2. 借鉴 CC + 客服改造:伪代码实现
以下是客服面板的核心实现,借鉴 CC 的流式渲染和工具结果注册表,但用 Web 原生方案替代终端黑魔法:
// 流式消息组件 — 借鉴 CC 的 StreamingText,用 Web 原生实现
function StreamingMessage({ sessionId }: { sessionId: string }) {
const [tokens, setTokens] = useState<string[]>([])
const [status, setStatus] = useState<"streaming"|"done"|"error">("streaming")
useEffect(() => {
// SSE 连接 — 每个 token 到达就追加渲染
const eventSource = new EventSource(`/api/agent/stream/${sessionId}`)
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data)
if (data.type === "token") {
setTokens(prev => [...prev, data.content]) // 追加渲染
} else if (data.type === "done") {
setStatus("done")
eventSource.close()
} else if (data.type === "error") {
setStatus("error")
eventSource.close()
}
}
return () => eventSource.close()
}, [sessionId])
return (
<div className="agent-message">
<div className="message-content">
{tokens.join("")}
{status === "streaming" && <span className="cursor">|</span>}
</div>
{status === "error" && <div className="error-hint">生成中断,点击重试</div>}
</div>
)
}
// 工具结果渲染注册表 — 借鉴 CC 的 toolRenderers.set() 模式
const toolResultRenderers = new Map<string, React.ComponentType<ToolResultProps>>()
// 注册:每种工具有自己的渲染组件
toolResultRenderers.set("query_order", OrderCardRenderer)
toolResultRenderers.set("query_logistics", TimelineRenderer)
toolResultRenderers.set("process_refund", ProgressRenderer)
toolResultRenderers.set("issue_coupon", CouponDetailRenderer)
toolResultRenderers.set("knowledge_search", ArticleListRenderer)
// 通用渲染器 — 根据工具名查找对应组件
function ToolResultDisplay({ toolUse, result }: ToolResultProps) {
const Renderer = toolResultRenderers.get(toolUse.name) ?? DefaultTextRenderer
return <Renderer input={toolUse.input} output={result} />
}
// 订单卡片渲染器示例
function OrderCardRenderer({ output }: { output: OrderResult }) {
return (
<div className="order-card">
<div className="order-header">订单号:{output.orderId}</div>
<div className="order-items">
{output.items.map(item => (
<div key={item.sku}>{item.name} x{item.qty} ¥{item.price}</div>
))}
</div>
<div className="order-status">状态:{output.statusText}</div>
</div>
)
}
// 会话管理器 — 多客户并行
function SessionManager() {
const [sessions, setSessions] = useState<Map<string, Session>>(new Map())
const [activeSessionId, setActiveSessionId] = useState<string>(null)
// 切换会话时,保存当前会话状态(流式输出不中断)
const switchSession = (targetId: string) => {
// 后台继续接收其他会话的 SSE 消息
setActiveSessionId(targetId)
}
return (
<div className="session-container">
{/* 左侧:客户列表 */}
<div className="session-list">
{Array.from(sessions.values()).map(s => (
<SessionTab key={s.id} session={s}
isActive={s.id === activeSessionId}
onClick={() => switchSession(s.id)} />
))}
</div>
{/* 右侧:当前会话的消息区 */}
<div className="message-area">
{sessions.get(activeSessionId) &&
<MessageList messages={sessions.get(activeSessionId).messages} />}
</div>
</div>
)
}
// 工具调用状态指示器
function ToolCallIndicator({ status, toolName }: { status: string, toolName: string }) {
const statusConfig = {
pending: { icon: "⏳", text: `正在${toolName}...`, className: "pending" },
complete: { icon: "✓", text: `${toolName}完成`, className: "complete" },
failed: { icon: "✗", text: `${toolName}失败`, className: "failed" },
}
const cfg = statusConfig[status]
return <div className={`tool-indicator ${cfg.className}`}>{cfg.icon} {cfg.text}</div>
}
3. 落地清单:必须抄 vs 不需要抄
- 流式渲染 — token 到达即显示,不等完整响应。CC 用 Observable,客服用 SSE/WebSocket,本质相同
- 工具结果渲染器注册表 — 每种工具注册独立渲染组件,新增工具不改核心逻辑。CC 用 Map,客服也用 Map
- 截断策略 — 超长输出必须截断,保留头尾 N 行 + 折叠中间。CC 在终端做,客服在 Web 做更简单
- 自定义 React Reconciler — CC 为了在终端渲染 React 组件造了整个 reconciler,客服面板是 Web 应用,直接用标准 React/Vue
- Ink 组件模型 + Yoga 布局引擎 — 终端没有 CSS,CC 用 Yoga 实现 flexbox。客服面板有原生 CSS,完全不需要
- 双缓冲差异输出 — CC 为了避免终端闪烁。浏览器 DOM 天然支持增量更新,不需要手动管理帧缓冲
4. 常见坑
div.innerHTML += token 在 token 包含用户输入时会产生 XSS 注入。解决方案:用 document.createTextNode(token) 或 React 的 setState(prev => [...prev, token]),框架自动转义。
代码索引
| 文件 | 行数 | 说明 |
|---|---|---|
ink.ts | ~86 | Ink 统一导出入口,封装 ThemeProvider |
ink/ink.tsx | ~1722 | Ink 核心类:双缓冲渲染、事件循环、终端 I/O |
ink/reconciler.ts | ~200 | React reconciler 适配器(react-reconciler) |
ink/renderer.ts | ~150 | DOM 树 → Screen 缓冲区渲染器 |
ink/render-to-screen.ts | ~120 | 独立渲染(搜索高亮等场景) |
ink/layout/yoga.ts | ~309 | Yoga 布局引擎适配(Flexbox → 终端坐标) |
ink/dom.ts | ~200 | 虚拟 DOM 节点定义和操作 |
ink/screen.ts | ~300 | Screen 缓冲区、StylePool/CharPool |
ink/frame.ts | ~90 | Frame 数据结构(screen + viewport + cursor) |
ink/terminal.ts | ~150 | 终端能力检测(DEC 2026、OSC 9;4 等) |
ink/events/dispatcher.ts | ~120 | 双阶段事件派发器 |
ink/parse-keypress.ts | ~300 | 按键序列解析状态机 |
ink/components/App.tsx | ~250 | 顶层组件:stdin 事件接收与分发 |
ink/components/Box.tsx | ~200 | 基础布局容器(映射到 Yoga 节点) |
ink/components/Text.tsx | ~150 | 基础文本组件 |
ink/components/ScrollBox.tsx | ~300 | 可滚动容器组件 |
ink/hooks/use-input.ts | ~80 | 键盘输入 hook |
ink/hooks/use-selection.ts | ~200 | 文本选择 hook |
ink/termio/ | ~600 | ANSI 序列库(CSI/DEC/OSC/SGR) |
screens/REPL.tsx | ~2000 | 主 REPL 屏幕 |
components/PromptInput.tsx | ~1500 | 用户输入组件 |
components/VirtualMessageList.tsx | ~500 | 虚拟消息列表 |
components/Messages.tsx | ~800 | 消息列表管理与渲染 |
components/design-system/ | ~1000 | 设计系统(ThemeProvider/ThemedText/ThemedBox 等 14 个文件) |
components/messages/ | ~3000 | 30+ 消息类型组件 |