TUI 状态管理
本文档详细介绍 OpenCode TUI 的状态管理系统,包括数据流、同步机制和状态更新策略。
概述
TUI 采用分层状态管理架构:
Sync Context
Sync Context 是 TUI 的核心状态管理中心,负责与后端同步数据。
Store 结构
const [store, setStore] = createStore<{
// 同步状态
status: "loading" | "partial" | "complete"
// 提供商和模型
provider: Provider[]
provider_default: Record<string, string>
provider_next: ProviderListResponse
provider_auth: Record<string, ProviderAuthMethod[]>
// Agent 和命令
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 }
mcp_resource: { [key: string]: McpResource }
formatter: FormatterStatus[]
vcs: VcsInfo | undefined
path: Path
}>(initialState)
状态转换
状态说明:
| 状态 | 说明 | 可用数据 |
|---|---|---|
| loading | 正在初始化 | 无 |
| partial | 核心数据已加载 | provider, agent, config |
| complete | 所有数据已加载 | 全部数据 |
Bootstrap 流程
async function bootstrap() {
// 1. 阻塞请求(必须等待)
const blockingRequests = [
sdk.client.config.providers({}), // 提供商配置
sdk.client.provider.list({}), // 可用提供商
sdk.client.app.agents({}), // Agent 列表
sdk.client.config.get({}), // 配置
...(args.continue ? [sessionListPromise] : []),
]
await Promise.all(blockingRequests).then(() => {
setStore("status", "partial")
// 2. 非阻塞请求
Promise.all([
...(args.continue ? [] : [sessionListPromise]),
sdk.client.command.list(),
sdk.client.lsp.status(),
sdk.client.mcp.status(),
sdk.client.experimental.resource.list(),
sdk.client.formatter.status(),
sdk.client.session.status(),
sdk.client.provider.auth(),
sdk.client.vcs.get(),
sdk.client.path.get(),
]).then(() => {
setStore("status", "complete")
})
})
}
事件监听
Sync Context 监听 SDK 事件并更新 Store:
sdk.event.listen((e) => {
const event = e.details
switch (event.type) {
// 会话事件
case "session.updated":
handleSessionUpdated(event.properties.info)
break
case "session.deleted":
handleSessionDeleted(event.properties.info.id)
break
case "session.status":
setStore("session_status", event.properties.sessionID, event.properties.status)
break
case "session.diff":
setStore("session_diff", event.properties.sessionID, event.properties.diff)
break
// 消息事件
case "message.updated":
handleMessageUpdated(event.properties.info)
break
case "message.removed":
handleMessageRemoved(event)
break
// 消息部分事件
case "message.part.updated":
handlePartUpdated(event.properties.part)
break
case "message.part.removed":
handlePartRemoved(event)
break
// 权限和提问
case "permission.asked":
handlePermissionAsked(event.properties)
break
case "permission.replied":
handlePermissionReplied(event)
break
case "question.asked":
handleQuestionAsked(event.properties)
break
case "question.replied":
case "question.rejected":
handleQuestionReplied(event)
break
// Todo 事件
case "todo.updated":
setStore("todo", event.properties.sessionID, event.properties.todos)
break
}
})
数据更新策略
消息更新
case "message.updated": {
const messages = store.message[event.properties.info.sessionID]
if (!messages) {
// 首次创建消息数组
setStore("message", event.properties.info.sessionID,
[event.properties.info])
break
}
// 使用 Binary Search 查找消息
const result = Binary.search(messages, event.properties.info.id, (m) => m.id)
if (result.found) {
// 已存在,使用 reconcile 优化更新
setStore("message", event.properties.info.sessionID,
result.index, reconcile(event.properties.info))
break
}
// 新消息,插入到正确位置
setStore("message", event.properties.info.sessionID,
produce((draft) => {
draft.splice(result.index, 0, event.properties.info)
}))
// 限制消息数量,避免内存泄漏
if (messages.length > 100) {
const oldest = messages[0]
batch(() => {
setStore("message", event.properties.info.sessionID,
produce((draft) => draft.shift()))
setStore("part",
produce((draft) => delete draft[oldest.id]))
})
}
break
}
优化点:
- Binary Search O(log n) 查找
- reconcile 减少组件重渲染
- produce 批量更新
- 限制消息数量(100条)
- 同步清理关联的 part 数据
消息部分更新
case "message.part.updated": {
const parts = store.part[event.properties.part.messageID]
if (!parts) {
// 首次创建 parts 数组
setStore("part", event.properties.part.messageID,
[event.properties.part])
break
}
const result = Binary.search(parts, event.properties.part.id, (p) => p.id)
if (result.found) {
// 已存在,更新
setStore("part", event.properties.part.messageID,
result.index, reconcile(event.properties.part))
break
}
// 新 part,插入
setStore("part", event.properties.part.messageID,
produce((draft) => {
draft.splice(result.index, 0, event.properties.part)
}))
break
}
权限管理
case "permission.asked": {
const request = event.properties
const requests = store.permission[request.sessionID]
if (!requests) {
// 首次创建
setStore("permission", request.sessionID, [request])
break
}
const match = Binary.search(requests, request.id, (r) => r.id)
if (match.found) {
// 已存在,更新
setStore("permission", request.sessionID,
match.index, reconcile(request))
break
}
// 新请求,插入到正确位置
setStore("permission", request.sessionID,
produce((draft) => {
draft.splice(match.index, 0, request)
}))
break
}
case "permission.replied": {
const requests = store.permission[event.properties.sessionID]
if (!requests) break
const match = Binary.search(requests, event.properties.requestID, (r) => r.id)
if (!match.found) break
// 删除已处理的请求
setStore("permission", event.properties.sessionID,
produce((draft) => {
draft.splice(match.index, 1)
}))
break
}
Local Context
Local Context 管理用户本地偏好设置。
状态结构
export type LocalStore = {
agent: {
current: string
list: Agent[]
}
: {
current: { providerID: string; modelID: string }
list: ModelInfo[]
variant: {
current: string | undefined
list: string[]
}
}
editor: {
command: string
args: string[]
}
}
Agent 管理
const agent = {
get current() {
return store.agent.current
},
set(name: string) {
setStore("agent", "current", name)
},
get list() {
return store.agent.list
},
color(name: string): RGBA {
const agent = store.agent.list.find((a) => a.name === name)
return agent?.color ?? theme.primary
},
move(direction: number): void {
const list = this.list
const index = list.findIndex((a) => a.name === this.current)
const next = (index + direction + list.length) % list.length
this.set(list[next].name)
},
}
Model 管理
const model = {
get current() {
return store.model.current
},
set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) {
setStore("model", "current", model)
if (options?.recent) {
// 更新最近使用列表
this.addToRecent(model)
}
},
get list() {
return store.model.list
},
get parsed() {
const current = this.current
const match = this.list.find((m) => m.providerID === current.providerID && m.modelID === current.modelID)
return {
model: match?.model ?? current.modelID,
provider: match?.provider ?? current.providerID,
}
},
cycle(direction: number): void {
const recents = this.getRecent()
const index = recents.findIndex(
(m) => m.providerID === this.current.providerID && m.modelID === this.current.modelID,
)
const next = (index + direction + recents.length) % recents.length
this.set(recents[next])
},
variant: {
get current() {
return store.model.variant.current
},
set(variant: string) {
setStore("model", "variant", "current", variant)
},
get list() {
return store.model.variant.list
},
cycle(): void {
const list = this.list
const index = list.indexOf(this.current ?? "")
const next = (index + 1) % list.length
this.set(list[next])
},
},
}
Route Context
Route Context 管理应用路由状态。
路由类型
export type HomeRoute = {
type: "home"
initialPrompt?: PromptInfo
}
export type SessionRoute = {
type: "session"
sessionID: string
initialPrompt?: PromptInfo
}
export type Route = HomeRoute | SessionRoute
路由导航
const route = {
get data() {
return store
},
navigate(route: Route) {
console.log("navigate", route)
setStore(route)
},
}
使用示例
// 在组件中
const route = useRoute()
// 切换到首页
route.navigate({ type: "home" })
// 切换到会话
route.navigate({
type: "session",
sessionID: "session-123",
})
// 带初始提示
route.navigate({
type: "session",
sessionID: "session-123",
initialPrompt: { input: "help me", parts: [] },
})
KV Context
KV Context 提供简单的键值存储,用于持久化用户偏好。
API
export const kv = useKV()
// 设置值
kv.set("tips_hidden", true)
kv.set("theme", "dark")
// 获取值(带默认值)
const hidden = kv.get("tips_hidden", false)
const theme = kv.get("theme", "dark")
// 使用信号
const signal = kv.signal("sidebar", "auto")
const sidebar = signal() // 读取
signal("hide") // 设置
使用场景
| 用途 | 示例 |
|---|---|
| 主题偏好 | kv.get("theme", "dark") |
| 提示显示 | kv.get("tips_hidden", false) |
| 侧边栏状态 | kv.signal("sidebar", "auto") |
| 快捷键自定义 | kv.get("keybinds", {}) |
| 编辑器设置 | kv.get("editor.command", "vim") |
SDK Context
SDK Context 提供与后端服务器的通信接口。
初始化
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
name: "SDK",
init: () => {
const sdk = new SDK({
url: input.url,
directory: input.directory,
fetch: input.fetch,
})
return {
url: sdk.url,
client: sdk.client,
event: sdk.event,
}
},
})
API 调用
const sdk = useSDK()
// 创建会话
const session = await sdk.client.session.create({})
const sessionID = session.data.id
// 发送提示
await sdk.client.session.prompt({
sessionID,
agent: "opencode",
model: { providerID: "openrouter", modelID: "anthropic/claude-3-5-sonnet" },
messageID: "msg-123",
parts: [
{
id: "part-123",
type: "text",
text: "help me",
},
],
})
// 获取消息
const messages = await sdk.client.session.messages({
sessionID,
limit: 100,
})
// 获取会话状态
const status = await sdk.client.session.status()
事件监听
// 监听事件
sdk.event.on("message.part.updated", (evt) => {
console.log("Part updated:", evt.properties.part)
})
// 监听所有事件
sdk.event.listen((e) => {
console.log("Event:", e.type, e.properties)
})
Theme Context
Theme Context 管理主题和颜色方案。
主题结构
export type Theme = {
// 背景色
background: RGBA
backgroundPanel: RGBA
backgroundElement: RGBA
backgroundMenu: RGBA
// 文本色
text: RGBA
textMuted: RGBA
// 强调色
primary: RGBA
secondary: RGBA
accent: RGBA
// 状态色
success: RGBA
warning: RGBA
error: RGBA
// 差异色
diffAdded: RGBA
diffRemoved: RGBA
diffModified: RGBA
// 边框色
border: RGBA
borderActive: RGBA
// 语法高亮
syntax: Record<string, RGBA>
}
主题切换
const { theme, mode, setMode } = useTheme()
// 当前模式: "dark" | "light"
console.log(mode())
// 切换模式
setMode("light")
// 使用主题
<text fg={theme.primary}>Primary text</text>
<box backgroundColor={theme.backgroundPanel}>
Content
</box>
自动检测
TUI 启动时自动检测终端背景色:
async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
return new Promise((resolve) => {
const handler = (data: Buffer) => {
const str = data.toString()
const match = str.match(/\x1b]11;([^\x07\x1b]+)/)
if (match) {
const color = match[1]
// 解析 RGB 并计算亮度
const r = parseInt(color.substring(1, 3), 16)
const g = parseInt(color.substring(3, 5), 16)
const b = parseInt(color.substring(5, 7), 16)
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
resolve(luminance > 0.5 ? "light" : "dark")
}
}
process.stdin.setRawMode(true)
process.stdin.on("data", handler)
process.stdout.write("\x1b]11;?\x07") // 查询背景色
})
}
Keybind Context
Keybind Context 管理快捷键系统。
快捷键配置
export type KeybindConfig = {
leader: boolean // Leader 模式激活
bindings: Record<string, string[]> // 快捷键映射
}
const defaultBindings = {
command_list: [
["ctrl", "x"],
["ctrl", "x"],
],
input_submit: [["enter"]],
session_interrupt: [["escape"]],
history_previous: [["up"]],
history_next: [["down"]],
// ...
}
快捷键匹配
const keybind = useKeybind()
// 检查查键
if (keybind.match("session_interrupt", event)) {
// 处理中断
}
// 打印快捷键提示
const hint = keybind.print("command_list") // "Ctrl+X"
Leader 模式
Leader 模式允许组合键操作:
性能优化
响应式优化
// ✅ 使用 createMemo 缓存计算结果
const messages = createMemo(() => sync.data.message[sessionID] ?? [])
// ❌ 避免在 render 中重复计算
function Component() {
const messages = sync.data.message[sessionID] ?? [] // 每次渲染都计算
// ...
}
批量更新
// ✅ 使用 batch 批量更新
batch(() => {
setStore("session", sessionID, "title", newTitle)
setStore("session", sessionID, "time", "updated", Date.now())
})
// ❌ 避免多次单独更新
setStore("session", sessionID, "title", newTitle)
setStore("session", sessionID, "time", "updated", Date.now()) // 触发两次渲染
内存管理
// 限制数组大小
if (messages.length > 100) {
const oldest = messages[0]
batch(() => {
setStore(
"message",
sessionID,
produce((draft) => draft.shift()),
)
setStore(
"part",
produce((draft) => delete draft[oldest.id]),
)
})
}
// 使用 Binary Search 而非 Array.find
const result = Binary.search(array, id, (item) => item.id)
// O(log n) vs O(n)