TUI 架构设计
整体架构
OpenCode TUI 采用分层架构,将应用逻辑、状态管理、渲染和交互清晰地分离。
Provider 层级
TUI 使用嵌套 Provider 模式,每个 Provider 提供特定功能:
<ArgsProvider {...input.args}>
<ExitProvider onExit={onExit}>
<KVProvider>
<ToastProvider>
<RouteProvider>
<SDKProvider url={input.url} {...}>
<SyncProvider>
<ThemeProvider mode={mode}>
<LocalProvider>
<KeybindProvider>
<PromptStashProvider>
<DialogProvider>
<CommandProvider>
<FrecencyProvider>
<PromptHistoryProvider>
<PromptRefProvider>
<App />
</PromptRefProvider>
</PromptHistoryProvider>
</FrecencyProvider>
</CommandProvider>
</DialogProvider>
</PromptStashProvider>
</KeybindProvider>
</LocalProvider>
</ThemeProvider>
</SyncProvider>
</SDKProvider>
</RouteProvider>
</ToastProvider>
</KVProvider>
</ExitProvider>
</ArgsProvider>
Provider 依赖关系
| Provider | 依赖 | 被依赖 |
|---|---|---|
| ArgsProvider | 无 | - |
| ExitProvider | 无 | - |
| KVProvider | 无 | PromptHistory, PromptStash |
| ToastProvider | KVProvider | - |
| RouteProvider | 无 | App, SDK |
| SDKProvider | RouteProvider | Sync, Event handlers |
| SyncProvider | SDKProvider | App, Session |
| ThemeProvider | 无 | All components |
| LocalProvider | 无 | Prompt, Session |
| KeybindProvider | 无 | Command, Prompt |
| PromptStashProvider | KVProvider | Prompt |
| DialogProvider | 无 | Command, App |
| CommandProvider | KeybindProvider | App, Prompt, Session |
| FrecencyProvider | 无 | Prompt |
| PromptHistoryProvider | KVProvider | Prompt |
| PromptRefProvider | 无 | App, Home |
渲染架构
组件树结构
app.tsx
└── box (根容器)
└── Switch (路由切换)
├── Match (home)
│ └── Home
│ ├── box (居中容器)
│ │ ├── Logo
│ │ ├── Prompt
│ │ └── Tips
│ └── box (状态栏)
└── Match (session)
└── Session
├── box (主布局)
│ ├── box (内容区)
│ │ ├── Header (条件显示)
│ │ ├── scrollbox (消息列表)
│ │ │ └── For each message
│ │ │ ├── UserMessage
│ │ │ │ └── box
│ │ │ │ ├── text (内容)
│ │ │ │ └── For files (附件)
│ │ │ └── AssistantMessage
│ │ │ ├── For each part
│ │ │ │ ├── TextPart
│ │ │ │ ├── ReasoningPart
│ │ │ │ └── ToolPart
│ │ │ │ └── Switch (tool type)
│ │ │ │ ├── Match (bash)
│ │ │ │ ├── Match (read)
│ │ │ │ ├── Match (write)
│ │ │ │ └── ...
│ │ │ └── box (元数据)
│ │ ├── box (权限提示)
│ │ └── Prompt
│ │ ├── Autocomplete
│ │ ├── textarea
│ │ └── box (状态栏)
│ └── Show (条件)
│ └── Sidebar
│ ├── scrollbox
│ │ ├── box (Context)
│ │ ├── box (MCP)
│ │ ├── box (LSP)
│ │ ├── box (Todo)
│ │ └── box (Modified Files)
│ └── box (底部信息)
└── Toast
渲染优化
1. 虚拟滚动
消息列表使用 scrollbox 组件实现虚拟滚动:
<scrollbox
ref={(r) => (scroll = r)}
stickyScroll={true}
stickyStart="bottom"
flexGrow={1}
scrollAcceleration={scrollAcceleration()}
>
<For each={messages()}>{(message) => <Message message={message} />}</For>
</scrollbox>
特性:
- 只渲染可见区域内的消息
- 自动滚动到底部(sticky)
- 自定义滚动加速度
2. 流式渲染
文本内容支持流式更新:
<code
filetype="markdown"
streaming={true} // 启用流式渲染
content={props.part.text.trim()}
conceal={ctx.conceal()}
fg={theme.text}
/>
3. 增量更新
使用 SolidJS 的细粒度响应式系统:
const message = createMemo(() => sync.session.get(route.sessionID))
// 只有 route.sessionID 变化时才重新计算
状态管理架构
数据流
Store 设计
const [store, setStore] = createStore<{
status: "loading" | "partial" | "complete"
provider: Provider[]
agent: Agent[]
command: Command[]
permission: { [sessionID: string]: PermissionRequest[] }
question: { [sessionID: string]: QuestionRequest[] }
config: Config
session: Session[]
session_status: { [sessionID: string]: SessionStatus }
session_diff: { [sessionID: string]: Snapshot.FileDiff[] }
todo: { [sessionID: string]: Todo[] }
message: { [sessionID: string]: Message[] }
part: { [messageID: string]: Part[] }
lsp: LspStatus[]
mcp: { [key: string]: McpStatus }
formatter: FormatterStatus[]
vcs: VcsInfo | undefined
path: Path
}>(initialState)
事件驱动更新
Sync Context 监听 SDK 事件并更新 Store:
sdk.event.listen((e) => {
{
const event = e.details
switch (event.type) {
case "message.updated":
const messages = store.message[event.properties.info.sessionID]
const result = Binary.search(messages, event.properties.info.id, (m) => m.id)
if (result.found) {
setStore("message", event.properties.info.sessionID,
result.index, reconcile(event.properties.info))
} else {
setStore("message", event.properties.info.sessionID,
produce((draft) => draft.splice(result.index, 0, event.properties.info)))
}
break
// ... 其他事件处理
}
})
优化点:
- 使用
Binary SearchO(log n) 查找 - 使用
reconcile优化组件更新 - 使用
produce批量更新
组件通信
Context 通信
组件通过 Context 共享状态:
// 读取
const sync = useSync()
const messages = createMemo(() => sync.data.message[sessionID] ?? [])
// 更新 (通过 SDK)
sdk.client.session.prompt({ ... })
事件总线
通过 SDK 事件系统:
// 发送事件
sdk.event.emit(TuiEvent.CommandExecute.type, {
command: "session.new",
})
// 监听事件
sdk.event.on(TuiEvent.CommandExecute.type, (evt) => {
command.trigger(evt.properties.command)
})
Ref 传递
父子组件通过 Ref 通信:
// 父组件
let prompt: PromptRef
;<Prompt ref={(r) => (prompt = r)} />
// 子组件
export type PromptRef = {
focused: boolean
current: PromptInfo
set(prompt: PromptInfo): void
reset(): void
blur(): void
focus(): void
submit(): void
}
性能优化策略
1. 组件优化
| 技术 | 应用场景 | 效果 |
|---|---|---|
| createMemo | 计算密集型操作 | 避免重复计算 |
| Show | 条件渲染 | 减少DOM节点 |
| For | 列表渲染 | 高效列表更新 |
| createEffect | 副作用 | 精确依赖追踪 |
2. 数据优化
| 技术 | 应用场景 | 效果 |
|---|---|---|
| Binary Search | 数组查找 | O(log n) 查找 |
| reconcile | 对象更新 | 减少组件重渲染 |
| produce | 批量更新 | 一次性提交 |
| createStore | 状态管理 | 细粒度响应式 |
3. 渲染优化
| 技术 | 应用场景 | 效果 |
|---|---|---|
| 虚拟滚动 | 长列表 | 只渲染可见项 |
| 流式渲染 | 文本输出 | 实时显示 |
| 语法高亮缓存 | 代码显示 | 避免重复解析 |
错误处理
全局错误边界
<ErrorBoundary fallback={(error, reset) => <ErrorComponent error={error} reset={reset} />}>
<App />
</ErrorBoundary>
错误显示
function ErrorComponent(props: { error: Error; reset: () => void; onExit: () => Promise<void> }) {
// 显示错误堆栈
// 提供重置和退出选项
// 生成 GitHub issue URL
}
可扩展性
添加新工具
- 创建工具组件
- 在
PART_MAPPING中注册 - 在
ToolPart中添加Match
// 1. 创建组件
function CustomTool(props: ToolProps<CustomTool.Info>) {
return <BlockTool title="Custom" {...props} />
}
// 2. 注册
const PART_MAPPING = {
text: TextPart,
tool: ToolPart,
reasoning: ReasoningPart,
custom: CustomTool, // 新增
}
// 3. 添加 Match
<Match when={props.part.tool === "custom"}>
<CustomTool {...toolprops} />
</Match>
添加新命令
command.register(() => [
{
title: "Custom command",
value: "custom.command",
category: "Custom",
onSelect: (dialog) => {
// 执行命令
dialog.clear()
},
},
])
添加新页面
// 路由类型
export type CustomRoute = {
type: "custom"
id: string
}
export type Route = HomeRoute | SessionRoute | CustomRoute
// 页面组件
export function Custom() {
const route = useRouteData("custom")
// ...
}
// 在 App 中添加路由
;<Match when={route.data.type === "custom"}>
<Custom />
</Match>