Code Reader
首页
帮助
设计文档
首页
帮助
设计文档
  • TUI 核心组件

TUI 核心组件

本文档详细介绍 OpenCode TUI 的核心组件及其功能。

Prompt 组件

功能概述

Prompt 组件是用户与 AI 交互的核心输入组件,提供丰富的输入功能。

主要特性

特性说明
多行输入支持 1-6 行高度自适应
语法高亮Markdown 语法实时高亮
虚拟文本文件/图像引用显示为占位符
自动补全智能补全文件、命令、agent
历史记录上下键导航历史
暂存功能多个提示的快速切换
Shell 模式! 前缀执行 shell 命令

组件结构

虚拟文本(extmarks)

虚拟文本允许在输入框中显示文件引用的占位符,节省空间:

// 创建文件引用的虚拟文本
const extmarkId = input.extmarks.create({
  start: extmarkStart,
  end: extmarkEnd,
  virtual: true,
  styleId: fileStyleId,
  typeId: promptPartTypeId,
})

效果: [Image 1] 点击展开为实际内容

自动补全

提供以下补全类型:

类型触发条件内容
文件/ 开头文件路径
命令/ 开头斜杠命令
Agent@ 开头Agent 名称
链接http 开头URL 验证

历史记录

// 上下键导航
if (keybind.match("history_previous", e)) {
  const item = history.move(-1, input.plainText)
  if (item) {
    input.setText(item.input)
    // ...
  }
}

Shell 模式

以 ! 开头进入 Shell 模式:

if (e.name === "!" && input.visualCursor.offset === 0) {
  setStore("mode", "shell")
  e.preventDefault()
  return
}

// 提交时
if (store.mode === "shell") {
  sdk.client.session.shell({
    sessionID,
    agent: local.agent.current().name,
    command: inputText,
  })
}

API

export type PromptRef = {
  focused: boolean
  current: PromptInfo
  set(prompt: PromptInfo): void
  reset(): void
  blur(): void
  focus(): void
  submit(): void
}

export type PromptProps = {
  sessionID?: string
  visible?: boolean
  disabled?: boolean
  onSubmit?: () => void
  ref?: (ref: PromptRef) => void
  hint?: JSX.Element
  showPlaceholder?: boolean
}

消息组件

UserMessage

显示用户发送的消息,包含文本和文件附件。

function UserMessage(props: {
  message: UserMessage
  parts: Part[]
  onMouseUp: () => void
  index: number
  pending?: string
})

结构:

┌────────────────────────────┐
│ 用户消息内容                │
│ 📄 filename.txt            │
│ 📷 Image 1                │
│ [时间戳/QUEUED 标记]        │
└────────────────────────────┘

特性:

  • 左边框颜色标识 Agent
  • 鼠标悬停显示背景高亮
  • 点击打开消息菜单
  • 支持显示图像、PDF、文件附件

AssistantMessage

显示 AI 助手的响应消息。

function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; last: boolean })

消息部分映射:

const PART_MAPPING = {
  text: TextPart, // 普通文本
  tool: ToolPart, // 工具调用
  reasoning: ReasoningPart, // 推理过程
}

渲染逻辑:

<For each={props.parts}>
  {(part, index) => {
    const component = createMemo(() => PART_MAPPING[part.type as keyof typeof PART_MAPPING])
    return (
      <Show when={component()}>
        <Dynamic
          last={index() === props.parts.length - 1}
          component={component()}
          part={part}
          message={props.message}
        />
      </Show>
    )
  }}
</For>

TextPart

渲染 Markdown 文本内容。

<code
  filetype="markdown"
  drawUnstyledText={false}
  streaming={true} // 流式渲染
  syntaxStyle={syntax()} // 语法高亮
  content={props.part.text.trim()}
  conceal={ctx.conceal()} // 代码折叠
  fg={theme.text}
/>

ReasoningPart

显示 AI 的推理过程(如 Claude 的 thinking)。

<Show when={content() && ctx.showThinking()}>
  <box border={["left"]} borderColor={theme.backgroundElement}>
    <code
      filetype="markdown"
      streaming={true}
      syntaxStyle={subtleSyntax()}
      content={"_Thinking:_ " + content()}
      conceal={ctx.conceal()}
      fg={theme.textMuted}
    />
  </box>
</Show>

特性:

  • 可折叠显示(showThinking 状态)
  • 过滤掉 OpenRouter 的 REDACTED 内容
  • 使用灰色文本标识

ToolPart

根据工具类型动态渲染工具输出。

<Switch>
  <Match when={props.part.tool === "bash"}>
    <Bash {...toolprops} />
  </Match>
  <Match when={props.part.tool === "glob"}>
    <Glob {...toolprops} />
  </Match>
  <Match when={props.part.tool === "read"}>
    <Read {...toolprops} />
  </Match>
  <Match when={props.part.tool === "write"}>
    <Write {...toolprops} />
  </Match>
  <Match when={props.part.tool === "edit"}>
    <Edit {...toolprops} />
  </Match>
  <Match when={props.part.tool === "grep"}>
    <Grep {...toolprops} />
  </Match>
  <Match when={props.part.tool === "task"}>
    <Task {...toolprops} />
  </Match>
  <Match when={props.part.tool === "todowrite"}>
    <TodoWrite {...toolprops} />
  </Match>
  <Match when={props.part.tool === "question"}>
    <Question {...toolprops} />
  </Match>
  <Match when={true}>
    <GenericTool {...toolprops} />
  </Match>
</Switch>

工具组件类型:

工具组件显示方式
bashBash内联或块级
readRead代码块
writeWrite内联
editEdit差异显示
globGlob内联
grepGrep内联或块级
taskTask内联
todowriteTodoWrite内联
questionQuestion内联

Sidebar 组件

显示会话的上下文信息和状态。

分区说明

1. Context 区

显示上下文使用情况:

const context = createMemo(() => {
  const last = messages().findLast((x) => x.role === "assistant")
  const total =
    last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
  const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID]
  return {
    tokens: total.toLocaleString(),
    percentage: model?.limit.context ? Math.round((total / model.limit.context) * 100) : null,
  }
})

显示内容:

  • Token 总数
  • 上下文使用百分比
  • 当前会话成本

2. MCP 区

显示 MCP 服务器状态:

<For each={mcpEntries()}>
  {([key, item]) => (
    <text fg={statusColor[item.status]}>•</text>
    <text>{key}</text>
    <text fg={theme.textMuted}>{statusText[item.status]}</text>
  )}
</For>

状态映射:

状态颜色文本
connectedsuccessConnected
failederror错误信息
disabledtextMutedDisabled
needs_authwarningNeeds auth
needs_client_registrationerrorNeeds client ID

折叠逻辑: 服务器数量 > 2 时可折叠

3. LSP 区

显示语言服务器状态:

<For each={sync.data.lsp}>
  {(item) => (
    <text fg={item.status === 'connected' ? theme.success : theme.error}>
      •
    </text>
    <text>{item.id} {item.root}</text>
  )}
</For>

4. Todo 区

显示未完成的任务:

<For each={todo()}>{(todo) => <TodoItem status={todo.status} content={todo.content} />}</For>

TodoItem 组件:

export function TodoItem(props: { status: "pending" | "in_progress" | "completed"; content: string }) {
  // 显示图标:⭕ (pending), ⏳ (in_progress), ✓ (completed)
  // 删除线显示已完成任务
}

5. Modified Files 区

显示已修改的文件和差异统计:

<For each={diff()}>
  {(item) => (
    <text>{item.file}</text>
    <text fg={theme.diffAdded}>+{item.additions}</text>
    <text fg={theme.diffRemoved}>-{item.deletions}</text>
  )}
</For>

Home 组件

首页显示,用于开始新会话。

export function Home() {
  return (
    <box flexGrow={1} justifyContent="center" alignItems="center">
      <Logo />
      <Prompt ref={promptRef} hint={Hint} />
      <Tips />
      <Toast />
    </box>
    <box>  {/* 状态栏 */ }
      <text>{directory()}</text>
      <text>{connectedMcpCount()} MCP</text>
      <text>{Installation.VERSION}</text>
    </box>
  )
}

Logo 组件

显示 OpenCode Logo:

<text>
  <span fg={theme.primary}>Open</span>
  <span fg={theme.accent}>Code</span>
</text>

Tips 组件

显示使用提示,可隐藏:

const tipsHidden = createMemo(() => kv.get("tips_hidden", false))
const showTips = createMemo(() => {
  if (isFirstTimeUser()) return false
  return !tipsHidden()
})

提示内容:

  • 使用 / 访问命令
  • 使用 @ 选择 agent
  • 使用 Ctrl+X 打开命令面板

Dialog 组件

DialogProvider

对话框管理器,支持对话框堆栈。

export const { use: useDialog, provider: DialogProvider } = createSimpleContext({
  name: "Dialog",
  init: () => {
    const [stack, setStack] = createStore<JSX.Element[]>([])

    return {
      replace(component: () => JSX.Element) {
        setStack([component()])
      },
      clear() {
        setStack([])
      },
      push(component: () => JSX.Element) {
        setStack(produce((draft) => draft.push(component())))
      },
      pop() {
        setStack(produce((draft) => draft.pop()))
      },
    }
  },
})

常用对话框

对话框用途文件
DialogCommand命令面板component/dialog-command.tsx
DialogModel模型选择component/dialog-model.tsx
DialogAgentAgent 选择component/dialog-agent.tsx
DialogSessionList会话列表component/dialog-session-list.tsx
DialogStatus状态查看component/dialog-status.tsx
DialogThemeList主题选择component/dialog-theme-list.tsx
DialogHelp帮助信息ui/dialog-help.tsx
DialogConfirm确认对话框ui/dialog-confirm.tsx
DialogAlert警告对话框ui/dialog-alert.tsx

DialogCommand

命令面板,提供模糊搜索的命令选择:

export function DialogCommand() {
  const commands = useCommands()

  return (
    <dialog>
      <textarea placeholder="Search commands..." onInput={handleInput} />
      <For each={filteredCommands()}>{(cmd) => <item onClick={() => cmd.onSelect(dialog)}>{cmd.title}</item>}</For>
    </dialog>
  )
}

特性:

  • 模糊搜索命令
  • 分类分组
  • 快捷键提示
  • Leader 模式触发(Ctrl+X)

Toast 组件

显示临时提示消息。

export function Toast() {
  const [toasts, setToasts] = createSignal<ToastItem[]>([])

  return (
    <For each={toasts()}>
      {(toast) => (
        <box>
          <text>{toast.title}</text>
          <text>{toast.message}</text>
        </box>
      )}
    </For>
  )
}

API:

export const toast = useToast()

toast.show({
  title: "Success",
  message: "操作成功",
  variant: "success", // success | error | warning | info
  duration: 3000,
})

toast.error("错误消息")
toast.success("成功消息")
toast.warning("警告消息")

工具组件

Logo

显示品牌 Logo:

export function Logo() {
  const { theme } = useTheme()
  return (
    <text>
      <span fg={theme.primary}>Open</span>
      <span fg={theme.accent}>Code</span>
    </text>
  )
}

Tips

显示使用提示:

export function Tips() {
  const tips = [
    { key: "/", text: "access commands" },
    { key: "@", text: "select agent" },
    { key: "Ctrl+X", text: "command palette" },
  ]

  return (
    <For each={tips}>
      {(tip) => (
        <text>
          <span fg={theme.primary}>{tip.key}</span>
          <span>{tip.text}</span>
        </text>
      )}
    </For>
  )
}

Border

自定义边框样式:

export const SplitBorder = {
  customBorderChars: {
    ...EmptyBorder,
    vertical: "┃",
    horizontal: "─",
    topLeft: "┌",
    topRight: "┐",
    bottomLeft: "└",
    bottomRight: "┘",
    verticalLeft: "├",
    verticalRight: "┤",
    horizontalUp: "┴",
    horizontalDown: "┬",
    cross: "┼",
  },
}