第10章:UI 组件
本章深入分析 Claude Code 的 UI 组件设计,涵盖核心组件、交互组件和布局组件的实现原理。
10.1 components/ 目录结构
整体架构
Claude Code 的 UI 组件库是一个大型 React/Ink 组件集合,位于 src/components/ 目录:
graph TD
subgraph Core["核心层"]
APP[App.tsx<br/>顶层 Context 包装]
MSG[Message.tsx<br/>消息渲染入口]
MSGS[Messages.tsx<br/>消息列表管理]
end
subgraph Input["输入组件"]
TI[TextInput.tsx<br/>文本输入框]
BTI[BaseTextInput.tsx<br/>输入基础组件]
VI[VimTextInput.tsx<br/>Vim 模式输入]
SB[SearchBox.tsx<br/>搜索框]
end
subgraph Output["输出组件"]
SL[StatusLine.tsx<br/>状态栏]
VML[VirtualMessageList.tsx<br/>虚拟消息列表]
MD[Markdown.tsx<br/>Markdown 渲染]
HC[HighlightedCode.tsx<br/>代码高亮]
end
subgraph Interaction["交互组件"]
SP[Spinner.tsx<br/>加载动画]
TP[TokenWarning.tsx<br/>Token 警告]
FD[Feedback.tsx<br/>反馈收集]
DLG[Dialog.tsx<br/>对话框系统]
end
subgraph Messages["消息渲染"]
ATM[AssistantTextMessage]
ATU[AssistantToolUseMessage]
UTM[UserTextMessage]
UTR[UserToolResultMessage]
STM[SystemTextMessage]
end
APP --> MSGS
MSGS --> VML
VML --> MSG
MSG --> Messages
TI --> BTI
VML --> SL
Messages --> ATM
Messages --> ATU
Messages --> UTM
Messages --> UTR
Messages --> STM
文件组织
src/components/
├── App.tsx # 顶层 Context Provider 包装
├── Message.tsx # 单条消息渲染(79KB)
├── Messages.tsx # 消息列表管理(147KB)
├── VirtualMessageList.tsx # 虚拟滚动列表(148KB)
├── StatusLine.tsx # 状态栏(49KB)
├── TextInput.tsx # 文本输入组件(21KB)
├── BaseTextInput.tsx # 输入基础逻辑(19KB)
├── VimTextInput.tsx # Vim 模式输入(16KB)
├── Spinner.tsx # 加载动画(87KB)
├── Markdown.tsx # Markdown 渲染(28KB)
├── HighlightedCode.tsx # 语法高亮代码(17KB)
├── FileEditToolDiff.tsx # 文件编辑差异展示(22KB)
├── StructuredDiff.tsx # 结构化 Diff(25KB)
├── Feedback.tsx # 用户反馈(87KB)
├── TokenWarning.tsx # Token 警告提示(21KB)
├── FullscreenLayout.tsx # 全屏布局(84KB)
├── Stats.tsx # 统计信息展示(152KB)
├── GlobalSearchDialog.tsx # 全局搜索对话框(43KB)
├── ModelPicker.tsx # 模型选择器(54KB)
├── ThemePicker.tsx # 主题选择器(35KB)
├── messages/ # 消息类型子组件
│ ├── AssistantTextMessage.tsx
│ ├── AssistantToolUseMessage.tsx
│ ├── AssistantThinkingMessage.tsx
│ ├── UserTextMessage.tsx
│ ├── UserToolResultMessage/
│ ├── SystemTextMessage.tsx
│ └── AttachmentMessage.tsx
├── design-system/ # 设计系统基础组件
│ ├── Dialog.tsx
│ ├── ListItem.tsx
│ ├── Tabs.tsx
│ ├── ThemedText.tsx
│ ├── ThemedBox.tsx
│ ├── ProgressBar.tsx
│ └── ThemeProvider.tsx
├── hooks/ # 组件专用 Hooks
│ ├── HooksConfigMenu.tsx
│ ├── PromptDialog.tsx
│ └── SelectHookMode.tsx
├── shell/ # Shell 输出组件
├── mcp/ # MCP 相关组件
├── permissions/ # 权限相关组件
├── skills/ # Skill 组件
├── tasks/ # 任务状态组件
└── ui/ # 通用 UI 元素
组件目录规模庞大(150+ 文件),体现了 Claude Code 作为交互式 CLI 工具的复杂度。核心组件如 Message.tsx、Messages.tsx、VirtualMessageList.tsx 均超过 100KB,说明消息渲染是系统最复杂的部分。
10.2 核心组件解析
App.tsx - 顶层 Context 包装
App 组件是整个交互会话的顶层包装器,提供全局 Context:
// App.tsx
type Props = {
getFpsMetrics: () => FpsMetrics | undefined; // FPS 性能监控
stats?: StatsStore; // 统计数据存储
initialState: AppState; // 应用初始状态
children: React.ReactNode;
};
export function App({ getFpsMetrics, stats, initialState, children }: Props) {
return (
<FpsMetricsProvider getFpsMetrics={getFpsMetrics}>
<StatsProvider store={stats}>
<AppStateProvider
initialState={initialState}
onChangeAppState={onChangeAppState}
>
{children}
</AppStateProvider>
</StatsProvider>
</FpsMetricsProvider>
);
}
App 组件使用三层 Provider 包装:
FpsMetricsProvider- 提供 FPS 性能数据StatsProvider- 提供统计信息(Token、Cost 等)AppStateProvider- 提供全局应用状态 这种分层设计允许子组件按需订阅不同层级的数据。
Message.tsx - 消息渲染分发
Message 组件是单条消息的渲染入口,根据消息类型分发到对应子组件:
// Message.tsx
export type Props = {
message: NormalizedUserMessage | AssistantMessage | AttachmentMessage
| SystemMessage | GroupedToolUseMessage | CollapsedReadSearchGroup;
lookups: ReturnType<typeof buildMessageLookups>;
containerWidth?: number;
tools: Tools;
commands: Command[];
verbose: boolean;
inProgressToolUseIDs: Set<string>;
progressMessagesForMessage: ProgressMessage[];
shouldAnimate: boolean;
isTranscriptMode: boolean;
};
function MessageImpl({ message, ...props }: Props) {
switch (message.type) {
case "attachment":
return <AttachmentMessage
attachment={message.attachment}
verbose={verbose}
/>;
case "assistant":
return (
<Box flexDirection="column" width={containerWidth ?? "100%"}>
{message.message.content.map((block, index) =>
<AssistantMessageBlock
key={index}
param={block}
tools={tools}
inProgressToolUseIDs={inProgressToolUseIDs}
shouldAnimate={shouldAnimate}
...
/>
)}
</Box>
);
case "user":
// 用户消息渲染...
case "system":
return <SystemTextMessage ... />;
default:
return null;
}
}
消息类型映射
graph LR
subgraph Input["消息类型"]
UM[UserMessage]
AM[AssistantMessage]
SM[SystemMessage]
AT[AttachmentMessage]
GT[GroupedToolUseMessage]
end
subgraph Render["渲染组件"]
UTM[UserTextMessage]
UTR[UserToolResultMessage]
ATM[AssistantTextMessage]
ATU[AssistantToolUseMessage]
ATH[AssistantThinkingMessage]
STM[SystemTextMessage]
ATT[AttachmentMessage]
GTC[GroupedToolUseContent]
end
UM --> UTM
UM --> UTR
AM --> ATM
AM --> ATU
AM --> ATH
SM --> STM
AT --> ATT
GT --> GTC
AssistantMessageBlock 组件
对于 Assistant 消息,根据 content block 类型分发:
// Message.tsx 内部函数
function AssistantMessageBlock({ param, tools, ...props }) {
switch (param.type) {
case 'text':
return <AssistantTextMessage
text={param.text}
shouldAnimate={shouldAnimate}
width={width}
/>;
case 'thinking':
return <AssistantThinkingMessage
thinking={param.thinking}
isTranscriptMode={isTranscriptMode}
/>;
case 'tool_use':
const tool = tools[param.name];
return <AssistantToolUseMessage
toolUse={param}
tool={tool}
inProgress={inProgressToolUseIDs.has(param.id)}
shouldAnimate={shouldAnimate}
/>;
case 'redacted_thinking':
return <AssistantRedactedThinkingMessage />;
default:
return null;
}
}
10.3 输入组件
TextInput.tsx - 文本输入框
TextInput 是用户输入的主要入口,支持多种模式:
// TextInput.tsx
export type Props = BaseTextInputProps & {
highlights?: TextHighlight[]; // 文本高亮区域
};
export default function TextInput(props: Props): React.ReactNode {
const [theme] = useTheme();
const isTerminalFocused = useTerminalFocus();
const settings = useSettings();
const reducedMotion = settings.prefersReducedMotion ?? false;
// Voice 模式状态
const voiceState = feature('VOICE_MODE')
? useVoiceState(s => s.voiceState)
: 'idle' as const;
const isVoiceRecording = voiceState === 'recording';
const audioLevels = feature('VOICE_MODE')
? useVoiceState(s => s.voiceAudioLevels)
: [];
// 动画帧引用(语音波形)
const smoothedRef = useRef<number[]>(new Array(CURSOR_WAVEFORM_WIDTH).fill(0));
const needsAnimation = isVoiceRecording && !reducedMotion;
const [animRef, animTime] = feature('VOICE_MODE')
? useAnimationFrame(needsAnimation ? 50 : null)
: [() => {}, 0];
// 剪贴板图片提示
useClipboardImageHint(isTerminalFocused, !!props.onImagePaste);
// 光标渲染:语音波形 vs 标准 inverse
const canShowCursor = isTerminalFocused && !accessibilityEnabled;
let invert: (text: string) => string;
if (!canShowCursor) {
invert = (text) => text;
} else if (isVoiceRecording && !reducedMotion) {
// 语音波形光标
const smoothed = smoothedRef.current;
const raw = audioLevels.length > 0 ? audioLevels[audioLevels.length - 1] : 0;
const target = Math.min(raw * LEVEL_BOOST, 1);
smoothed[0] = (smoothed[0] ?? 0) * SMOOTH + target * (1 - SMOOTH);
const barIndex = Math.max(1, Math.min(
Math.round(displayLevel * (BARS.length - 1)),
BARS.length - 1
));
const hue = (animTime / 1000) * 90 % 360;
const { r, g, b } = isSilent ? { r: 128, g: 128, b: 128 } : hueToRgb(hue);
invert = () => chalk.rgb(r, g, b)(BARS[barIndex]);
} else {
invert = chalk.inverse;
}
// 文本输入状态管理
const textInputState = useTextInput({
value: props.value,
onChange: props.onChange,
onSubmit: props.onSubmit,
onExit: props.onExit,
onHistoryUp: props.onHistoryUp,
onHistoryDown: props.onHistoryDown,
multiline: props.multiline,
cursorChar: props.showCursor ? ' ' : '',
invert,
themeText: color('text', theme),
columns: props.columns,
maxVisibleLines: props.maxVisibleLines,
onImagePaste: props.onImagePaste,
inlineGhostText: props.inlineGhostText,
});
return (
<Box ref={animRef}>
<BaseTextInput
inputState={textInputState}
terminalFocus={isTerminalFocused}
highlights={props.highlights}
invert={invert}
hidePlaceholderText={isVoiceRecording}
{...props}
/>
</Box>
);
}
TextInput 支持语音模式下的动态波形光标:使用 useAnimationFrame 50ms 周期刷新,根据 audioLevels 计算波形高度,通过 EMA(指数移动平均)平滑过渡,避免抖动。
BaseTextInput.tsx - 输入基础组件
BaseTextInput 处理文本输入的核心逻辑:
// BaseTextInput.tsx
export function BaseTextInput({ inputState, terminalFocus, invert, ...props }) {
const { onInput, renderedValue, cursorLine, cursorColumn } = inputState;
// 光标位置声明
const cursorRef = useDeclaredCursor({
line: cursorLine,
column: cursorColumn,
active: Boolean(props.focus && props.showCursor && terminalFocus)
});
// 粘贴处理
const { wrappedOnInput, isPasting } = usePasteHandler({
onPaste: props.onPaste,
onInput: (input, key) => {
if (isPasting && key.return) return; // 粘贴时忽略 Enter
onInput(input, key);
},
onImagePaste: props.onImagePaste
});
// Placeholder 渲染
const { showPlaceholder, renderedPlaceholder } = renderPlaceholder({
placeholder: props.placeholder,
value: props.value,
showCursor: props.showCursor,
focus: props.focus,
terminalFocus,
invert
});
// 注册输入监听
useInput(wrappedOnInput, { isActive: props.focus });
// 命令参数提示
const commandWithoutArg = props.value?.trim().indexOf(" ") === -1;
const showArgumentHint = Boolean(
props.argumentHint &&
commandWithoutArg &&
props.value?.startsWith("/")
);
// 高亮过滤(排除光标位置)
const cursorFiltered = props.highlights?.filter(h =>
h.dimColor ||
props.cursorOffset < h.start ||
props.cursorOffset >= h.end
);
return (
<Box flexDirection="column">
{showPlaceholder && renderedPlaceholder}
<HighlightedInput
value={renderedValue}
highlights={filteredHighlights}
invert={invert}
...
/>
{showArgumentHint && <ArgumentHint />}
{children}
</Box>
);
}
10.4 输出组件
StatusLine.tsx - 状态栏
StatusLine 显示会话状态信息(模型、Token、Cost 等):
// StatusLine.tsx
type Props = {
messagesRef: React.RefObject<Message[]>;
lastAssistantMessageId: string | null;
vimMode?: VimMode;
};
function StatusLineInner({ messagesRef, lastAssistantMessageId, vimMode }: Props) {
const settings = useSettings();
const permissionMode = useAppState(s => s.toolPermissionContext.mode);
const statusLineText = useAppState(s => s.statusLineText);
// 构建状态栏命令输入
const input = buildStatusLineCommandInput(
permissionMode,
exceeds200kTokens,
settings,
messages,
addedDirs,
mainLoopModel,
vimMode
);
return (
<Box flexDirection="row">
<Text>{statusLineText || renderDefaultStatusLine(input)}</Text>
</Box>
);
}
// 状态栏数据结构
type StatusLineCommandInput = {
session_name?: string;
model: { id: string; display_name: string };
workspace: {
current_dir: string;
project_dir: string;
added_dirs: string[];
};
cost: {
total_cost_usd: number;
total_duration_ms: number;
total_lines_added: number;
total_lines_removed: number;
};
context_window: {
total_input_tokens: number;
total_output_tokens: number;
context_window_size: number;
used_percentage: number;
remaining_percentage: number;
};
rate_limits?: {
five_hour?: { used_percentage: number; resets_at: string };
seven_day?: { used_percentage: number; resets_at: string };
};
worktree?: {
name: string;
path: string;
branch: string;
};
};
VirtualMessageList.tsx - 虚拟滚动列表
VirtualMessageList 实现高效的虚拟滚动,处理大量消息:
// VirtualMessageList.tsx
const HEADROOM = 3; // scrollTo 时保留的顶部缓冲行数
export type StickyPrompt = {
text: string;
scrollTo: () => void;
} | 'clicked';
export type JumpHandle = {
jumpToIndex: (i: number) => void;
setSearchQuery: (q: string) => void;
nextMatch: () => void;
prevMatch: () => void;
setAnchor: () => void; // 搜索锚点
warmSearchIndex: () => Promise<number>; // 预热搜索缓存
disarmSearch: () => void; // 手动滚动退出搜索
};
type Props = {
messages: RenderableMessage[];
scrollRef: RefObject<ScrollBoxHandle>;
columns: number; // 宽度变化时失效缓存
itemKey: (msg: RenderableMessage) => string;
renderItem: (msg: RenderableMessage, index: number) => React.ReactNode;
onItemClick?: (msg: RenderableMessage) => void;
isItemClickable?: (msg: RenderableMessage) => boolean;
isItemExpanded?: (msg: RenderableMessage) => boolean;
extractSearchText?: (msg: RenderableMessage) => string;
trackStickyPrompt?: boolean;
jumpRef?: RefObject<JumpHandle>;
onSearchMatchesChange?: (count: number, current: number) => void;
scanElement?: (el: DOMElement) => MatchPosition[];
setPositions?: (state: PositionsState | null) => void;
};
function stickyPromptText(msg: RenderableMessage): string | null {
// WeakMap 缓存,避免重复计算
const cached = promptTextCache.get(msg);
if (cached !== undefined) return cached;
const result = computeStickyPromptText(msg);
promptTextCache.set(msg, result);
return result;
}
function computeStickyPromptText(msg: RenderableMessage): string | null {
if (msg.type === 'user') {
if (msg.isMeta || msg.isVisibleInTranscriptOnly) return null;
const block = msg.message.content[0];
if (block?.type !== 'text') return null;
// 去除系统提醒前缀
const text = stripSystemReminders(block.text);
if (text.startsWith('<')) return null; // XML 包装内容不显示
return text.slice(0, STICKY_TEXT_CAP); // 截断超长文本
}
return null;
}
- 高度缓存依赖
columns参数,窗口宽度变化时需要失效 itemKey函数用于 React 列表 diff,必须稳定唯一- 搜索文本使用 WeakMap 缓存,消息对象作为 key,自动 GC
- 粘性提示文本有 500 字符上限,避免超大粘贴内容撑爆内存
Markdown.tsx - Markdown 渲染
Markdown 组件处理富文本渲染:
// Markdown.tsx(简化)
export function Markdown({ children, width, theme }: Props) {
const parsed = parseMarkdown(children);
return (
<Box flexDirection="column" width={width}>
{parsed.blocks.map((block, i) => {
switch (block.type) {
case 'paragraph':
return <Text key={i}>{renderInline(block.content)}</Text>;
case 'code':
return <HighlightedCode
key={i}
code={block.code}
language={block.lang}
theme={theme}
/>;
case 'heading':
return <Text key={i} bold>{block.content}</Text>;
case 'list':
return <ListItems items={block.items} />;
case 'blockquote':
return <Box borderStyle="round">
<Text dimColor>{block.content}</Text>
</Box>;
case 'table':
return <MarkdownTable data={block.data} />;
default:
return null;
}
})}
</Box>
);
}
10.5 交互组件
Spinner.tsx - 加载动画
Spinner 组件提供多种加载动画风格:
// Spinner.tsx
const FRAMES = {
dots: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
line: ['─', '━', '┃', '┏', '┓', '┫', '┣', '┳', '┻', '╋'],
pipe: ['┏', '┓', '┛', '┗', '┏', '┓', '┛', '┗'],
moon: ['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘'],
circle: ['◜', '◠', '◝', '◞', '◟', '◡', '◟'],
arrow: ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙'],
};
export function Spinner({ type = 'dots', color }: Props) {
const [frameIndex, setFrameIndex] = useState(0);
const frames = FRAMES[type];
const interval = type === 'moon' ? 80 : 80; // 月相稍慢
useEffect(() => {
if (reducedMotion) return;
const timer = setInterval(() => {
setFrameIndex(i => (i + 1) % frames.length);
}, interval);
return () => clearInterval(timer);
}, [frames, interval, reducedMotion]);
return (
<Text color={color}>
{reducedMotion ? '...' : frames[frameIndex]}
</Text>
);
}
交互流程图
sequenceDiagram
participant User as 用户
participant TI as TextInput
participant BTI as BaseTextInput
participant App as App Context
participant API as API Layer
participant VML as VirtualMessageList
participant MSG as Message
User->>TI: 输入文本
TI->>BTI: useTextInput()
BTI->>BTI: useInput() 监听
BTI->>App: onSubmit()
App->>API: 发送请求
API-->>App: 流式响应
loop 每个响应块
App->>VML: 添加消息
VML->>MSG: 渲染消息
MSG->>MSG: 类型分发
end
VML-->>User: 更新显示
User->>VML: 滚动/点击
VML->>VML: 虚拟滚动优化
10.6 设计系统
design-system/ 组件
设计系统提供统一风格的 UI 基础组件:
// design-system/ThemedText.tsx
export function ThemedText({ color: colorKey, children, ...props }: Props) {
const [theme] = useTheme();
const textColor = color(colorKey, theme);
return <Text color={textColor} {...props}>{children}</Text>;
}
// design-system/Dialog.tsx
export function Dialog({ title, children, onClose }: Props) {
const [theme] = useTheme();
return (
<Box
flexDirection="column"
borderStyle="round"
borderColor={color('border', theme)}
>
<Box borderBottom>
<ThemedText color="accent">{title}</ThemedText>
</Box>
<Box flexDirection="column">
{children}
</Box>
<Box borderTop>
<KeyboardShortcutHint keys={['Esc']} action="关闭" />
</Box>
</Box>
);
}
// design-system/ListItem.tsx
export function ListItem({ selected, children, onSelect }: Props) {
const [theme] = useTheme();
const isHovered = useHover();
return (
<Box
backgroundColor={selected ? color('selection', theme) : undefined}
onClick={onSelect}
>
<Text>
{selected ? '❯' : ' '}
{children}
</Text>
</Box>
);
}
组件层级图
graph TD
subgraph Providers["Context Providers"]
TP[ThemeProvider]
FP[FpsMetricsProvider]
SP[StatsProvider]
AP[AppStateProvider]
end
subgraph Layouts["布局组件"]
FL[FullscreenLayout]
SB[ScrollBox]
PAN[Pane]
end
subgraph Primitives["基础组件"]
TT[ThemedText]
TB[ThemedBox]
DLG[Dialog]
LI[ListItem]
TB[Tabs]
PB[ProgressBar]
end
subgraph Composite["复合组件"]
TI[TextInput]
VML[VirtualMessageList]
MSG[Message]
SL[StatusLine]
end
TP --> TT
TP --> TB
TP --> DLG
FL --> SB
SB --> VML
VML --> MSG
TI --> TT
MSG --> TT
SL --> TT
AP --> VML
SP --> SL
10.7 状态管理与交互设计
AppState 状态结构
// state/AppState.ts
export type AppState = {
// 工具权限上下文
toolPermissionContext: {
mode: PermissionMode;
additionalWorkingDirectories: string[];
};
// 状态栏文本
statusLineText: string | null;
// 光标导航状态
cursorNavState: MessageActionsState | null;
// 会话状态
sessionId: string;
isPaused: boolean;
// UI 状态
vimMode: VimMode | null;
pendingExit: boolean;
};
// 状态变更回调
export const onChangeAppState = (state: AppState) => {
// 同步到持久化存储
// 触发副作用
};
交互模式
graph TD
subgraph InputModes["输入模式"]
NM[Normal 模式<br/>默认输入]
VM[Vim 模式<br/>Vi 键绑定]
MM[Multiline 模式<br/>多行编辑]
VMODE[Voice 模式<br/>语音输入]
end
subgraph Navigation["导航操作"]
JK[j/k 滚动]
GD[G/D 翻页]
NK[n/N 搜索跳转]
MMODE[m/M 标记跳转]
end
subgraph Commands["命令操作"]
SC[/斜杠命令]
ENTER[Enter 提交]
ESC[Esc 退出]
CTRL[Ctrl+C 中断]
end
NM --> VM
VM --> JK
VM --> GD
VM --> NK
NM --> MM
NM --> VMODE
NM --> SC
NM --> ENTER
NM --> ESC
NM --> CTRL
交互设计遵循渐进增强原则:
- 基础模式:Normal 输入 + 标准键绑定
- 高级模式:Vim 键绑定 + 搜索导航
- 辅助模式:Multiline 编辑 + Voice 语音 每种模式独立开关,用户可按偏好组合。
10.8 性能优化策略
虚拟滚动优化
// VirtualMessageList.tsx 内部
const heightCache = new Map<string, number>();
function getItemHeight(msg: RenderableMessage, index: number): number {
const key = itemKey(msg);
const cached = heightCache.get(key);
if (cached !== undefined && columnsValid) return cached;
// 计算实际高度
const height = computeMessageHeight(msg, width);
heightCache.set(key, height);
return height;
}
// 粘性提示追踪
function StickyTracker({ scrollTop, messages }: Props) {
// 找到最后一个可见的用户提示
const lastVisiblePrompt = findLastVisiblePrompt(scrollTop, messages);
// 更新粘性状态
updateStickyPrompt({
text: stickyPromptText(lastVisiblePrompt),
scrollTo: () => scrollToMessage(lastVisiblePrompt)
});
}
渲染缓存
// 消息搜索文本缓存
const fallbackLowerCache = new WeakMap<RenderableMessage, string>();
function defaultExtractSearchText(msg: RenderableMessage): string {
const cached = fallbackLowerCache.get(msg);
if (cached !== undefined) return cached;
const lowered = renderableSearchText(msg).toLowerCase();
fallbackLowerCache.set(msg, lowered);
return lowered;
}
// 光标位置缓存
const promptTextCache = new WeakMap<RenderableMessage, string | null>();
function stickyPromptText(msg: RenderableMessage): string | null {
const cached = promptTextCache.get(msg);
if (cached !== undefined) return cached;
const result = computeStickyPromptText(msg);
promptTextCache.set(msg, result);
return result;
}
下一章将探讨 服务层架构,分析 MCP、API、OAuth 等服务的实现原理。