Code Reader
首页
帮助
设计文档
首页
帮助
设计文档
  • TUI 架构设计

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
ToastProviderKVProvider-
RouteProvider无App, SDK
SDKProviderRouteProviderSync, Event handlers
SyncProviderSDKProviderApp, Session
ThemeProvider无All components
LocalProvider无Prompt, Session
KeybindProvider无Command, Prompt
PromptStashProviderKVProviderPrompt
DialogProvider无Command, App
CommandProviderKeybindProviderApp, Prompt, Session
FrecencyProvider无Prompt
PromptHistoryProviderKVProviderPrompt
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 Search O(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
}

可扩展性

添加新工具

  1. 创建工具组件
  2. 在 PART_MAPPING 中注册
  3. 在 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>