Code Reader
首页
帮助
设计文档
首页
帮助
设计文档
  • TUI 状态管理

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
}

优化点:

  1. Binary Search O(log n) 查找
  2. reconcile 减少组件重渲染
  3. produce 批量更新
  4. 限制消息数量(100条)
  5. 同步清理关联的 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)